From 17794c3f46c90a7518814cdac5bb834c3adc9708 Mon Sep 17 00:00:00 2001 From: Josh Dolitsky <393494+jdolitsky@users.noreply.github.com> Date: Mon, 16 Dec 2019 20:19:47 -0600 Subject: [PATCH] Fix for missing files in Helm 2 charts (#53) * add failing test for requirements.yaml Signed-off-by: Josh Dolitsky <393494+jdolitsky@users.noreply.github.com> * fix for missing requirements.yaml in helm 2 Signed-off-by: Josh Dolitsky <393494+jdolitsky@users.noreply.github.com> * check based on chart apiversion vs helm exe Signed-off-by: Josh Dolitsky <393494+jdolitsky@users.noreply.github.com> * add vendored test chart dependency Signed-off-by: Josh Dolitsky <393494+jdolitsky@users.noreply.github.com> --- .gitignore | 1 + acceptance_tests/02-chartmuseum.robot | 26 +++ acceptance_tests/lib/ChartMuseum.py | 18 ++ acceptance_tests/lib/Helm.py | 6 + acceptance_tests/lib/HelmPush.py | 9 +- acceptance_tests/lib/common.py | 1 + pkg/helm/chart.go | 35 +++- pkg/helm/chart_test.go | 12 +- .../helm2/mychart/charts/mariadb-5.11.3.tgz | Bin 0 -> 15190 bytes .../charts/helm2/mychart/requirements.lock | 6 + .../charts/helm2/mychart/requirements.yaml | 4 + .../helm3/my-v3-chart/values.schema.json | 179 ++++++++++++++++++ 12 files changed, 284 insertions(+), 13 deletions(-) create mode 100644 testdata/charts/helm2/mychart/charts/mariadb-5.11.3.tgz create mode 100644 testdata/charts/helm2/mychart/requirements.lock create mode 100644 testdata/charts/helm2/mychart/requirements.yaml create mode 100644 testdata/charts/helm3/my-v3-chart/values.schema.json diff --git a/.gitignore b/.gitignore index 2296c40..d3a50ad 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ bin/ dist/ releases/ testbin/ +testdata/**/tmp vendor/ diff --git a/acceptance_tests/02-chartmuseum.robot b/acceptance_tests/02-chartmuseum.robot index 32889aa..1c28fc7 100644 --- a/acceptance_tests/02-chartmuseum.robot +++ b/acceptance_tests/02-chartmuseum.robot @@ -20,6 +20,16 @@ Test ChartMuseum integration set helm version ${version} install helm plugin helm major version detected by plugin is ${version} + + use test chart built by same helm version + clear chartmuseum storage + Chart directory can be pushed to ChartMuseum + Chart directory can be pushed to ChartMuseum with custom version + Chart package can be pushed to ChartMuseum + Chart package can be pushed to ChartMuseum with custom version + + use test chart built by opposite helm version + clear chartmuseum storage Chart directory can be pushed to ChartMuseum Chart directory can be pushed to ChartMuseum with custom version Chart package can be pushed to ChartMuseum @@ -30,6 +40,8 @@ Chart directory can be pushed to ChartMuseum push chart directory HelmPush.return code should be 0 package exists in chartmuseum storage + package contains expected files + HelmPush.return code should be 0 ChartMuseum.return code should be 0 clear chartmuseum storage @@ -37,6 +49,8 @@ Chart directory can be pushed to ChartMuseum push chart directory to url HelmPush.return code should be 0 package exists in chartmuseum storage + package contains expected files + HelmPush.return code should be 0 ChartMuseum.return code should be 0 clear chartmuseum storage @@ -45,6 +59,8 @@ Chart directory can be pushed to ChartMuseum with custom version push chart directory latest HelmPush.return code should be 0 package exists in chartmuseum storage latest + package contains expected files + HelmPush.return code should be 0 ChartMuseum.return code should be 0 clear chartmuseum storage @@ -52,6 +68,8 @@ Chart directory can be pushed to ChartMuseum with custom version push chart directory to url latest HelmPush.return code should be 0 package exists in chartmuseum storage latest + package contains expected files + HelmPush.return code should be 0 ChartMuseum.return code should be 0 clear chartmuseum storage @@ -60,6 +78,8 @@ Chart package can be pushed to ChartMuseum push chart package HelmPush.return code should be 0 package exists in chartmuseum storage + package contains expected files + HelmPush.return code should be 0 ChartMuseum.return code should be 0 clear chartmuseum storage @@ -67,6 +87,8 @@ Chart package can be pushed to ChartMuseum push chart package to url HelmPush.return code should be 0 package exists in chartmuseum storage + package contains expected files + HelmPush.return code should be 0 ChartMuseum.return code should be 0 clear chartmuseum storage @@ -75,6 +97,8 @@ Chart package can be pushed to ChartMuseum with custom version push chart package latest HelmPush.return code should be 0 package exists in chartmuseum storage latest + package contains expected files + HelmPush.return code should be 0 ChartMuseum.return code should be 0 clear chartmuseum storage @@ -82,6 +106,8 @@ Chart package can be pushed to ChartMuseum with custom version push chart package to url latest HelmPush.return code should be 0 package exists in chartmuseum storage latest + package contains expected files + HelmPush.return code should be 0 ChartMuseum.return code should be 0 clear chartmuseum storage diff --git a/acceptance_tests/lib/ChartMuseum.py b/acceptance_tests/lib/ChartMuseum.py index a0608cf..0fa0591 100644 --- a/acceptance_tests/lib/ChartMuseum.py +++ b/acceptance_tests/lib/ChartMuseum.py @@ -29,5 +29,23 @@ def print_chartmuseum_logs(self): def package_exists_in_chartmuseum_storage(self, find=''): self.run_command('find %s -maxdepth 1 -name "*%s.tgz" | grep tgz' % (common.STORAGE_DIR, find)) + def package_contains_expected_files(self): + # Check for requirements.yaml in Helm 2 (a Helm 2-specific file) + checkRequirementsYamlCmd = '(cd %s && mkdir -p tmp && tar -xf *.tgz --directory tmp && find tmp -name requirements.yaml | grep requirements.yaml)' % common.STORAGE_DIR + + # Check for values.schema.json in Helm 3 (a Helm 3-specific file) + checkValuesSchemaJsonCmd = '(cd %s && mkdir -p tmp && tar -xf *.tgz --directory tmp && find tmp -name values.schema.json | grep values.schema.json)' % common.STORAGE_DIR + + if common.USE_OPPOSITE_VERSION: + if 'helm3' in common.HELM_EXE: + self.run_command(checkRequirementsYamlCmd) + else: + self.run_command(checkValuesSchemaJsonCmd) + else: + if 'helm3' in common.HELM_EXE: + self.run_command(checkValuesSchemaJsonCmd) + else: + self.run_command(checkRequirementsYamlCmd) + def clear_chartmuseum_storage(self): self.run_command('rm %s*.tgz' % common.STORAGE_DIR) diff --git a/acceptance_tests/lib/Helm.py b/acceptance_tests/lib/Helm.py index e27a03d..90d93b9 100644 --- a/acceptance_tests/lib/Helm.py +++ b/acceptance_tests/lib/Helm.py @@ -14,6 +14,12 @@ def set_helm_version(self, version): else: raise Exception('invalid Helm version provided: %s' % version) + def use_test_chart_built_by_same_helm_version(self): + common.USE_OPPOSITE_VERSION = False + + def use_test_chart_built_by_opposite_helm_version(self): + common.USE_OPPOSITE_VERSION = True + def add_chart_repo(self): self.remove_chart_repo() self.run_command('%s repo add %s %s' % (common.HELM_EXE, common.HELM_REPO_NAME, common.HELM_REPO_URL)) diff --git a/acceptance_tests/lib/HelmPush.py b/acceptance_tests/lib/HelmPush.py index 695a94b..58697fe 100644 --- a/acceptance_tests/lib/HelmPush.py +++ b/acceptance_tests/lib/HelmPush.py @@ -3,9 +3,14 @@ class HelmPush(common.CommandRunner): def _testchart_path(self): - if 'helm3' in common.HELM_EXE: + if common.USE_OPPOSITE_VERSION: + if 'helm3' in common.HELM_EXE: + return '%s/helm2/mychart' % common.TESTCHARTS_DIR return '%s/helm3/my-v3-chart' % common.TESTCHARTS_DIR - return '%s/helm2/mychart' % common.TESTCHARTS_DIR + else: + if 'helm3' in common.HELM_EXE: + return '%s/helm3/my-v3-chart' % common.TESTCHARTS_DIR + return '%s/helm2/mychart' % common.TESTCHARTS_DIR def helm_major_version_detected_by_plugin_is(self, version): cmd = '%s push --check-helm-version' % common.HELM_EXE diff --git a/acceptance_tests/lib/common.py b/acceptance_tests/lib/common.py index 1cd9a05..e3b05f5 100644 --- a/acceptance_tests/lib/common.py +++ b/acceptance_tests/lib/common.py @@ -11,6 +11,7 @@ STORAGE_DIR = os.path.join(ACCEPTANCE_DIR, 'storage/') LOGFILE = '.chartmuseum.log' HELM_EXE = 'HELM_HOME=%s helm2' % os.getenv('TEST_HELM_HOME', '') +USE_OPPOSITE_VERSION = False class CommandRunner(object): diff --git a/pkg/helm/chart.go b/pkg/helm/chart.go index 847dd29..e6b0bb0 100644 --- a/pkg/helm/chart.go +++ b/pkg/helm/chart.go @@ -4,31 +4,56 @@ import ( "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chartutil" + v2chartutil "k8s.io/helm/pkg/chartutil" + v2chart "k8s.io/helm/pkg/proto/hapi/chart" ) type ( // Chart is a helm package that contains metadata Chart struct { - *chart.Chart + V3 *chart.Chart + V2 *v2chart.Chart } ) // SetVersion overrides the chart version func (c *Chart) SetVersion(version string) { - c.Metadata.Version = version + if c.V2 != nil { + c.V2.Metadata.Version = version + } else { + c.V3.Metadata.Version = version + } } // GetChartByName returns a chart by "name", which can be // either a directory or .tgz package func GetChartByName(name string) (*Chart, error) { - cc, err := loader.Load(name) + c := &Chart{} + v3c, err := loader.Load(name) if err != nil { return nil, err } - return &Chart{cc}, nil + + // If the Helm 2 API version (v1) is detected, use the old + // method to load the chart + if v3c.Metadata.APIVersion == chart.APIVersionV1 { + v2c, err := v2chartutil.Load(name) + if err != nil { + return nil, err + } + c.V2 = v2c + } else { + c.V3 = v3c + } + + return c, nil } // CreateChartPackage creates a new .tgz package in directory func CreateChartPackage(c *Chart, outDir string) (string, error) { - return chartutil.Save(c.Chart, outDir) + if c.V2 != nil { + return v2chartutil.Save(c.V2, outDir) + } else { + return chartutil.Save(c.V3, outDir) + } } diff --git a/pkg/helm/chart_test.go b/pkg/helm/chart_test.go index f8ad039..94c04df 100644 --- a/pkg/helm/chart_test.go +++ b/pkg/helm/chart_test.go @@ -15,8 +15,8 @@ func TestSetVersion(t *testing.T) { t.Error("unexpected error getting test tarball chart", err) } c.SetVersion("latest") - if c.Metadata.Version != "latest" { - t.Errorf("expected chart version to be latest, instead got %s", c.Metadata.Version) + if c.V2.Metadata.Version != "latest" { + t.Errorf("expected chart version to be latest, instead got %s", c.V2.Metadata.Version) } } @@ -32,11 +32,11 @@ func TestGetChartByName(t *testing.T) { if err != nil { t.Error("unexpected error getting test tarball chart", err) } - if c.Metadata.Name != "mychart" { - t.Errorf("expexted chart name to be mychart, instead got %s", c.Metadata.Name) + if c.V2.Metadata.Name != "mychart" { + t.Errorf("expexted chart name to be mychart, instead got %s", c.V2.Metadata.Name) } - if c.Metadata.Version != "0.1.0" { - t.Errorf("expexted chart version to be 0.1.0, instead got %s", c.Metadata.Version) + if c.V2.Metadata.Version != "0.1.0" { + t.Errorf("expexted chart version to be 0.1.0, instead got %s", c.V2.Metadata.Version) } } diff --git a/testdata/charts/helm2/mychart/charts/mariadb-5.11.3.tgz b/testdata/charts/helm2/mychart/charts/mariadb-5.11.3.tgz new file mode 100644 index 0000000000000000000000000000000000000000..5c543da40155989c82d640a3a2e5bee8dc7951fe GIT binary patch literal 15190 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 K+ynCi2m4