diff --git a/.golangci.yml b/.golangci.yml index a9b13c35f..3df31b997 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -33,6 +33,7 @@ linters: - usetesting exclusions: + generated: lax presets: @@ -41,7 +42,10 @@ linters: - legacy - std-error-handling - rules: [] + rules: + - linters: + - revive + text: 'var-naming: avoid meaningless package names' warn-unused: true diff --git a/Makefile b/Makefile index 5e424bf05..5e1bfc6c2 100644 --- a/Makefile +++ b/Makefile @@ -63,10 +63,12 @@ K8S_MODULES_VER=$(subst ., ,$(subst v,,$(shell go list -f '{{.Version}}' -m k8s. K8S_MODULES_MAJOR_VER=$(shell echo $$(($(firstword $(K8S_MODULES_VER)) + 1))) K8S_MODULES_MINOR_VER=$(word 2,$(K8S_MODULES_VER)) -LDFLAGS += -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER) -LDFLAGS += -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER) -LDFLAGS += -X helm.sh/helm/v4/pkg/chart/v2/util.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER) -LDFLAGS += -X helm.sh/helm/v4/pkg/chart/v2/util.k8sVersionMinor=$(K8S_MODULES_MINOR_VER) +LDFLAGS += -X helm.sh/helm/v4/pkg/chart/v2/lint/rules.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER) +LDFLAGS += -X helm.sh/helm/v4/pkg/chart/v2/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER) +LDFLAGS += -X helm.sh/helm/v4/pkg/internal/v3/lint/rules.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER) +LDFLAGS += -X helm.sh/helm/v4/pkg/internal/v3/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER) +LDFLAGS += -X helm.sh/helm/v4/pkg/chart/common/util.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER) +LDFLAGS += -X helm.sh/helm/v4/pkg/chart/common/util.k8sVersionMinor=$(K8S_MODULES_MINOR_VER) .PHONY: all all: build @@ -112,7 +114,8 @@ test-unit: # based on older versions, this is run separately. When run without the ldflags in the unit test (above) or coverage # test, it still passes with a false-positive result as the resources shouldn’t be deprecated in the older Kubernetes # version if it only starts failing with the latest. - go test $(GOFLAGS) -run ^TestHelmCreateChart_CheckDeprecatedWarnings$$ ./pkg/lint/ $(TESTFLAGS) -ldflags '$(LDFLAGS)' + go test $(GOFLAGS) -run ^TestHelmCreateChart_CheckDeprecatedWarnings$$ ./pkg/chart/v2/lint/ $(TESTFLAGS) -ldflags '$(LDFLAGS)' + go test $(GOFLAGS) -run ^TestHelmCreateChart_CheckDeprecatedWarnings$$ ./internal/chart/v3/lint/ $(TESTFLAGS) -ldflags '$(LDFLAGS)' .PHONY: test-coverage diff --git a/internal/chart/v3/chart.go b/internal/chart/v3/chart.go index 4d59fa5ec..2edc6c339 100644 --- a/internal/chart/v3/chart.go +++ b/internal/chart/v3/chart.go @@ -19,6 +19,8 @@ import ( "path/filepath" "regexp" "strings" + + "helm.sh/helm/v4/pkg/chart/common" ) // APIVersionV3 is the API version number for version 3. @@ -34,20 +36,20 @@ type Chart struct { // // This should not be used except in special cases like `helm show values`, // where we want to display the raw values, comments and all. - Raw []*File `json:"-"` + Raw []*common.File `json:"-"` // Metadata is the contents of the Chartfile. Metadata *Metadata `json:"metadata"` // Lock is the contents of Chart.lock. Lock *Lock `json:"lock"` // Templates for this chart. - Templates []*File `json:"templates"` + Templates []*common.File `json:"templates"` // Values are default config for this chart. Values map[string]interface{} `json:"values"` // Schema is an optional JSON schema for imposing structure on Values Schema []byte `json:"schema"` // Files are miscellaneous files in a chart archive, // e.g. README, LICENSE, etc. - Files []*File `json:"files"` + Files []*common.File `json:"files"` parent *Chart dependencies []*Chart @@ -59,7 +61,7 @@ type CRD struct { // Filename is the File obj Name including (sub-)chart.ChartFullPath Filename string // File is the File obj for the crd - File *File + File *common.File } // SetDependencies replaces the chart dependencies. @@ -134,8 +136,8 @@ func (ch *Chart) AppVersion() string { // CRDs returns a list of File objects in the 'crds/' directory of a Helm chart. // Deprecated: use CRDObjects() -func (ch *Chart) CRDs() []*File { - files := []*File{} +func (ch *Chart) CRDs() []*common.File { + files := []*common.File{} // Find all resources in the crds/ directory for _, f := range ch.Files { if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) { diff --git a/internal/chart/v3/chart_test.go b/internal/chart/v3/chart_test.go index f93b3356b..b1820ac0a 100644 --- a/internal/chart/v3/chart_test.go +++ b/internal/chart/v3/chart_test.go @@ -20,11 +20,13 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "helm.sh/helm/v4/pkg/chart/common" ) func TestCRDs(t *testing.T) { chrt := Chart{ - Files: []*File{ + Files: []*common.File{ { Name: "crds/foo.yaml", Data: []byte("hello"), @@ -57,7 +59,7 @@ func TestCRDs(t *testing.T) { func TestSaveChartNoRawData(t *testing.T) { chrt := Chart{ - Raw: []*File{ + Raw: []*common.File{ { Name: "fhqwhgads.yaml", Data: []byte("Everybody to the Limit"), @@ -76,7 +78,7 @@ func TestSaveChartNoRawData(t *testing.T) { t.Fatal(err) } - is.Equal([]*File(nil), res.Raw) + is.Equal([]*common.File(nil), res.Raw) } func TestMetadata(t *testing.T) { @@ -162,7 +164,7 @@ func TestChartFullPath(t *testing.T) { func TestCRDObjects(t *testing.T) { chrt := Chart{ - Files: []*File{ + Files: []*common.File{ { Name: "crds/foo.yaml", Data: []byte("hello"), @@ -190,7 +192,7 @@ func TestCRDObjects(t *testing.T) { { Name: "crds/foo.yaml", Filename: "crds/foo.yaml", - File: &File{ + File: &common.File{ Name: "crds/foo.yaml", Data: []byte("hello"), }, @@ -198,7 +200,7 @@ func TestCRDObjects(t *testing.T) { { Name: "crds/foo/bar/baz.yaml", Filename: "crds/foo/bar/baz.yaml", - File: &File{ + File: &common.File{ Name: "crds/foo/bar/baz.yaml", Data: []byte("hello"), }, diff --git a/internal/chart/v3/lint/lint.go b/internal/chart/v3/lint/lint.go new file mode 100644 index 000000000..231bb6803 --- /dev/null +++ b/internal/chart/v3/lint/lint.go @@ -0,0 +1,66 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package lint // import "helm.sh/helm/v4/internal/chart/v3/lint" + +import ( + "path/filepath" + + "helm.sh/helm/v4/internal/chart/v3/lint/rules" + "helm.sh/helm/v4/internal/chart/v3/lint/support" + "helm.sh/helm/v4/pkg/chart/common" +) + +type linterOptions struct { + KubeVersion *common.KubeVersion + SkipSchemaValidation bool +} + +type LinterOption func(lo *linterOptions) + +func WithKubeVersion(kubeVersion *common.KubeVersion) LinterOption { + return func(lo *linterOptions) { + lo.KubeVersion = kubeVersion + } +} + +func WithSkipSchemaValidation(skipSchemaValidation bool) LinterOption { + return func(lo *linterOptions) { + lo.SkipSchemaValidation = skipSchemaValidation + } +} + +func RunAll(baseDir string, values map[string]interface{}, namespace string, options ...LinterOption) support.Linter { + + chartDir, _ := filepath.Abs(baseDir) + + lo := linterOptions{} + for _, option := range options { + option(&lo) + } + + result := support.Linter{ + ChartDir: chartDir, + } + + rules.Chartfile(&result) + rules.ValuesWithOverrides(&result, values) + rules.TemplatesWithSkipSchemaValidation(&result, values, namespace, lo.KubeVersion, lo.SkipSchemaValidation) + rules.Dependencies(&result) + rules.Crds(&result) + + return result +} diff --git a/internal/chart/v3/lint/lint_test.go b/internal/chart/v3/lint/lint_test.go new file mode 100644 index 000000000..af44cc58d --- /dev/null +++ b/internal/chart/v3/lint/lint_test.go @@ -0,0 +1,246 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package lint + +import ( + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "helm.sh/helm/v4/internal/chart/v3/lint/support" + chartutil "helm.sh/helm/v4/internal/chart/v3/util" +) + +var values map[string]interface{} + +const namespace = "testNamespace" + +const badChartDir = "rules/testdata/badchartfile" +const badValuesFileDir = "rules/testdata/badvaluesfile" +const badYamlFileDir = "rules/testdata/albatross" +const badCrdFileDir = "rules/testdata/badcrdfile" +const goodChartDir = "rules/testdata/goodone" +const subChartValuesDir = "rules/testdata/withsubchart" +const malformedTemplate = "rules/testdata/malformed-template" +const invalidChartFileDir = "rules/testdata/invalidchartfile" + +func TestBadChartV3(t *testing.T) { + m := RunAll(badChartDir, values, namespace).Messages + if len(m) != 8 { + t.Errorf("Number of errors %v", len(m)) + t.Errorf("All didn't fail with expected errors, got %#v", m) + } + // There should be one INFO, one WARNING, and 2 ERROR messages, check for them + var i, w, e, e2, e3, e4, e5, e6 bool + for _, msg := range m { + if msg.Severity == support.InfoSev { + if strings.Contains(msg.Err.Error(), "icon is recommended") { + i = true + } + } + if msg.Severity == support.WarningSev { + if strings.Contains(msg.Err.Error(), "does not exist") { + w = true + } + } + if msg.Severity == support.ErrorSev { + if strings.Contains(msg.Err.Error(), "version '0.0.0.0' is not a valid SemVer") { + e = true + } + if strings.Contains(msg.Err.Error(), "name is required") { + e2 = true + } + + if strings.Contains(msg.Err.Error(), "apiVersion is required. The value must be \"v3\"") { + e3 = true + } + + if strings.Contains(msg.Err.Error(), "chart type is not valid in apiVersion") { + e4 = true + } + + if strings.Contains(msg.Err.Error(), "dependencies are not valid in the Chart file with apiVersion") { + e5 = true + } + // This comes from the dependency check, which loads dependency info from the Chart.yaml + if strings.Contains(msg.Err.Error(), "unable to load chart") { + e6 = true + } + } + } + if !e || !e2 || !e3 || !e4 || !e5 || !i || !e6 || !w { + t.Errorf("Didn't find all the expected errors, got %#v", m) + } +} + +func TestInvalidYaml(t *testing.T) { + m := RunAll(badYamlFileDir, values, namespace).Messages + if len(m) != 1 { + t.Fatalf("All didn't fail with expected errors, got %#v", m) + } + if !strings.Contains(m[0].Err.Error(), "deliberateSyntaxError") { + t.Errorf("All didn't have the error for deliberateSyntaxError") + } +} + +func TestInvalidChartYamlV3(t *testing.T) { + m := RunAll(invalidChartFileDir, values, namespace).Messages + t.Log(m) + if len(m) != 3 { + t.Fatalf("All didn't fail with expected errors, got %#v", m) + } + if !strings.Contains(m[0].Err.Error(), "failed to strictly parse chart metadata file") { + t.Errorf("All didn't have the error for duplicate YAML keys") + } +} + +func TestBadValuesV3(t *testing.T) { + m := RunAll(badValuesFileDir, values, namespace).Messages + if len(m) < 1 { + t.Fatalf("All didn't fail with expected errors, got %#v", m) + } + if !strings.Contains(m[0].Err.Error(), "unable to parse YAML") { + t.Errorf("All didn't have the error for invalid key format: %s", m[0].Err) + } +} + +func TestBadCrdFileV3(t *testing.T) { + m := RunAll(badCrdFileDir, values, namespace).Messages + assert.Lenf(t, m, 2, "All didn't fail with expected errors, got %#v", m) + assert.ErrorContains(t, m[0].Err, "apiVersion is not in 'apiextensions.k8s.io'") + assert.ErrorContains(t, m[1].Err, "object kind is not 'CustomResourceDefinition'") +} + +func TestGoodChart(t *testing.T) { + m := RunAll(goodChartDir, values, namespace).Messages + if len(m) != 0 { + t.Error("All returned linter messages when it shouldn't have") + for i, msg := range m { + t.Logf("Message %d: %s", i, msg) + } + } +} + +// TestHelmCreateChart tests that a `helm create` always passes a `helm lint` test. +// +// See https://github.com/helm/helm/issues/7923 +func TestHelmCreateChart(t *testing.T) { + dir := t.TempDir() + + createdChart, err := chartutil.Create("testhelmcreatepasseslint", dir) + if err != nil { + t.Error(err) + // Fatal is bad because of the defer. + return + } + + // Note: we test with strict=true here, even though others have + // strict = false. + m := RunAll(createdChart, values, namespace, WithSkipSchemaValidation(true)).Messages + if ll := len(m); ll != 1 { + t.Errorf("All should have had exactly 1 error. Got %d", ll) + for i, msg := range m { + t.Logf("Message %d: %s", i, msg.Error()) + } + } else if msg := m[0].Err.Error(); !strings.Contains(msg, "icon is recommended") { + t.Errorf("Unexpected lint error: %s", msg) + } +} + +// TestHelmCreateChart_CheckDeprecatedWarnings checks if any default template created by `helm create` throws +// deprecated warnings in the linter check against the current Kubernetes version (provided using ldflags). +// +// See https://github.com/helm/helm/issues/11495 +// +// Resources like hpa and ingress, which are disabled by default in values.yaml are enabled here using the equivalent +// of the `--set` flag. +// +// Note: This test requires the following ldflags to be set per the current Kubernetes version to avoid false-positive +// results. +// 1. -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMajor= +// 2. -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMinor= +// or directly use '$(LDFLAGS)' in Makefile. +// +// When run without ldflags, the test passes giving a false-positive result. This is because the variables +// `k8sVersionMajor` and `k8sVersionMinor` by default are set to an older version of Kubernetes, with which, there +// might not be the deprecation warning. +func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) { + createdChart, err := chartutil.Create("checkdeprecatedwarnings", t.TempDir()) + if err != nil { + t.Error(err) + return + } + + // Add values to enable hpa, and ingress which are disabled by default. + // This is the equivalent of: + // helm lint checkdeprecatedwarnings --set 'autoscaling.enabled=true,ingress.enabled=true' + updatedValues := map[string]interface{}{ + "autoscaling": map[string]interface{}{ + "enabled": true, + }, + "ingress": map[string]interface{}{ + "enabled": true, + }, + } + + linterRunDetails := RunAll(createdChart, updatedValues, namespace, WithSkipSchemaValidation(true)) + for _, msg := range linterRunDetails.Messages { + if strings.HasPrefix(msg.Error(), "[WARNING]") && + strings.Contains(msg.Error(), "deprecated") { + // When there is a deprecation warning for an object created + // by `helm create` for the current Kubernetes version, fail. + t.Errorf("Unexpected deprecation warning for %q: %s", msg.Path, msg.Error()) + } + } +} + +// lint ignores import-values +// See https://github.com/helm/helm/issues/9658 +func TestSubChartValuesChart(t *testing.T) { + m := RunAll(subChartValuesDir, values, namespace).Messages + if len(m) != 0 { + t.Error("All returned linter messages when it shouldn't have") + for i, msg := range m { + t.Logf("Message %d: %s", i, msg) + } + } +} + +// lint stuck with malformed template object +// See https://github.com/helm/helm/issues/11391 +func TestMalformedTemplate(t *testing.T) { + c := time.After(3 * time.Second) + ch := make(chan int, 1) + var m []support.Message + go func() { + m = RunAll(malformedTemplate, values, namespace).Messages + ch <- 1 + }() + select { + case <-c: + t.Fatalf("lint malformed template timeout") + case <-ch: + if len(m) != 1 { + t.Fatalf("All didn't fail with expected errors, got %#v", m) + } + if !strings.Contains(m[0].Err.Error(), "invalid character '{'") { + t.Errorf("All didn't have the error for invalid character '{'") + } + } +} diff --git a/internal/chart/v3/lint/rules/chartfile.go b/internal/chart/v3/lint/rules/chartfile.go new file mode 100644 index 000000000..e72a0d3b2 --- /dev/null +++ b/internal/chart/v3/lint/rules/chartfile.go @@ -0,0 +1,225 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules" + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/Masterminds/semver/v3" + "github.com/asaskevich/govalidator" + "sigs.k8s.io/yaml" + + chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/internal/chart/v3/lint/support" + chartutil "helm.sh/helm/v4/internal/chart/v3/util" +) + +// Chartfile runs a set of linter rules related to Chart.yaml file +func Chartfile(linter *support.Linter) { + chartFileName := "Chart.yaml" + chartPath := filepath.Join(linter.ChartDir, chartFileName) + + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlNotDirectory(chartPath)) + + chartFile, err := chartutil.LoadChartfile(chartPath) + validChartFile := linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlFormat(err)) + + // Guard clause. Following linter rules require a parsable ChartFile + if !validChartFile { + return + } + + _, err = chartutil.StrictLoadChartfile(chartPath) + linter.RunLinterRule(support.WarningSev, chartFileName, validateChartYamlStrictFormat(err)) + + // type check for Chart.yaml . ignoring error as any parse + // errors would already be caught in the above load function + chartFileForTypeCheck, _ := loadChartFileForTypeCheck(chartPath) + + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartName(chartFile)) + + // Chart metadata + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAPIVersion(chartFile)) + + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersionType(chartFileForTypeCheck)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersion(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAppVersionType(chartFileForTypeCheck)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartMaintainer(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartSources(chartFile)) + linter.RunLinterRule(support.InfoSev, chartFileName, validateChartIconPresence(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartIconURL(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartType(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartDependencies(chartFile)) +} + +func validateChartVersionType(data map[string]interface{}) error { + return isStringValue(data, "version") +} + +func validateChartAppVersionType(data map[string]interface{}) error { + return isStringValue(data, "appVersion") +} + +func isStringValue(data map[string]interface{}, key string) error { + value, ok := data[key] + if !ok { + return nil + } + valueType := fmt.Sprintf("%T", value) + if valueType != "string" { + return fmt.Errorf("%s should be of type string but it's of type %s", key, valueType) + } + return nil +} + +func validateChartYamlNotDirectory(chartPath string) error { + fi, err := os.Stat(chartPath) + + if err == nil && fi.IsDir() { + return errors.New("should be a file, not a directory") + } + return nil +} + +func validateChartYamlFormat(chartFileError error) error { + if chartFileError != nil { + return fmt.Errorf("unable to parse YAML\n\t%w", chartFileError) + } + return nil +} + +func validateChartYamlStrictFormat(chartFileError error) error { + if chartFileError != nil { + return fmt.Errorf("failed to strictly parse chart metadata file\n\t%w", chartFileError) + } + return nil +} + +func validateChartName(cf *chart.Metadata) error { + if cf.Name == "" { + return errors.New("name is required") + } + name := filepath.Base(cf.Name) + if name != cf.Name { + return fmt.Errorf("chart name %q is invalid", cf.Name) + } + return nil +} + +func validateChartAPIVersion(cf *chart.Metadata) error { + if cf.APIVersion == "" { + return errors.New("apiVersion is required. The value must be \"v3\"") + } + + if cf.APIVersion != chart.APIVersionV3 { + return fmt.Errorf("apiVersion '%s' is not valid. The value must be \"v3\"", cf.APIVersion) + } + + return nil +} + +func validateChartVersion(cf *chart.Metadata) error { + if cf.Version == "" { + return errors.New("version is required") + } + + version, err := semver.NewVersion(cf.Version) + if err != nil { + return fmt.Errorf("version '%s' is not a valid SemVer", cf.Version) + } + + c, err := semver.NewConstraint(">0.0.0-0") + if err != nil { + return err + } + valid, msg := c.Validate(version) + + if !valid && len(msg) > 0 { + return fmt.Errorf("version %v", msg[0]) + } + + return nil +} + +func validateChartMaintainer(cf *chart.Metadata) error { + for _, maintainer := range cf.Maintainers { + if maintainer == nil { + return errors.New("a maintainer entry is empty") + } + if maintainer.Name == "" { + return errors.New("each maintainer requires a name") + } else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) { + return fmt.Errorf("invalid email '%s' for maintainer '%s'", maintainer.Email, maintainer.Name) + } else if maintainer.URL != "" && !govalidator.IsURL(maintainer.URL) { + return fmt.Errorf("invalid url '%s' for maintainer '%s'", maintainer.URL, maintainer.Name) + } + } + return nil +} + +func validateChartSources(cf *chart.Metadata) error { + for _, source := range cf.Sources { + if source == "" || !govalidator.IsRequestURL(source) { + return fmt.Errorf("invalid source URL '%s'", source) + } + } + return nil +} + +func validateChartIconPresence(cf *chart.Metadata) error { + if cf.Icon == "" { + return errors.New("icon is recommended") + } + return nil +} + +func validateChartIconURL(cf *chart.Metadata) error { + if cf.Icon != "" && !govalidator.IsRequestURL(cf.Icon) { + return fmt.Errorf("invalid icon URL '%s'", cf.Icon) + } + return nil +} + +func validateChartDependencies(cf *chart.Metadata) error { + if len(cf.Dependencies) > 0 && cf.APIVersion != chart.APIVersionV3 { + return fmt.Errorf("dependencies are not valid in the Chart file with apiVersion '%s'. They are valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV3) + } + return nil +} + +func validateChartType(cf *chart.Metadata) error { + if len(cf.Type) > 0 && cf.APIVersion != chart.APIVersionV3 { + return fmt.Errorf("chart type is not valid in apiVersion '%s'. It is valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV3) + } + return nil +} + +// loadChartFileForTypeCheck loads the Chart.yaml +// in a generic form of a map[string]interface{}, so that the type +// of the values can be checked +func loadChartFileForTypeCheck(filename string) (map[string]interface{}, error) { + b, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + y := make(map[string]interface{}) + err = yaml.Unmarshal(b, &y) + return y, err +} diff --git a/internal/chart/v3/lint/rules/chartfile_test.go b/internal/chart/v3/lint/rules/chartfile_test.go new file mode 100644 index 000000000..070cc244d --- /dev/null +++ b/internal/chart/v3/lint/rules/chartfile_test.go @@ -0,0 +1,276 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "errors" + "os" + "path/filepath" + "strings" + "testing" + + chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/internal/chart/v3/lint/support" + chartutil "helm.sh/helm/v4/internal/chart/v3/util" +) + +const ( + badChartNameDir = "testdata/badchartname" + badChartDir = "testdata/badchartfile" + anotherBadChartDir = "testdata/anotherbadchartfile" +) + +var ( + badChartNamePath = filepath.Join(badChartNameDir, "Chart.yaml") + badChartFilePath = filepath.Join(badChartDir, "Chart.yaml") + nonExistingChartFilePath = filepath.Join(os.TempDir(), "Chart.yaml") +) + +var badChart, _ = chartutil.LoadChartfile(badChartFilePath) +var badChartName, _ = chartutil.LoadChartfile(badChartNamePath) + +// Validation functions Test +func TestValidateChartYamlNotDirectory(t *testing.T) { + _ = os.Mkdir(nonExistingChartFilePath, os.ModePerm) + defer os.Remove(nonExistingChartFilePath) + + err := validateChartYamlNotDirectory(nonExistingChartFilePath) + if err == nil { + t.Errorf("validateChartYamlNotDirectory to return a linter error, got no error") + } +} + +func TestValidateChartYamlFormat(t *testing.T) { + err := validateChartYamlFormat(errors.New("Read error")) + if err == nil { + t.Errorf("validateChartYamlFormat to return a linter error, got no error") + } + + err = validateChartYamlFormat(nil) + if err != nil { + t.Errorf("validateChartYamlFormat to return no error, got a linter error") + } +} + +func TestValidateChartName(t *testing.T) { + err := validateChartName(badChart) + if err == nil { + t.Errorf("validateChartName to return a linter error, got no error") + } + + err = validateChartName(badChartName) + if err == nil { + t.Error("expected validateChartName to return a linter error for an invalid name, got no error") + } +} + +func TestValidateChartVersion(t *testing.T) { + var failTest = []struct { + Version string + ErrorMsg string + }{ + {"", "version is required"}, + {"1.2.3.4", "version '1.2.3.4' is not a valid SemVer"}, + {"waps", "'waps' is not a valid SemVer"}, + {"-3", "'-3' is not a valid SemVer"}, + } + + var successTest = []string{"0.0.1", "0.0.1+build", "0.0.1-beta"} + + for _, test := range failTest { + badChart.Version = test.Version + err := validateChartVersion(badChart) + if err == nil || !strings.Contains(err.Error(), test.ErrorMsg) { + t.Errorf("validateChartVersion(%s) to return \"%s\", got no error", test.Version, test.ErrorMsg) + } + } + + for _, version := range successTest { + badChart.Version = version + err := validateChartVersion(badChart) + if err != nil { + t.Errorf("validateChartVersion(%s) to return no error, got a linter error", version) + } + } +} + +func TestValidateChartMaintainer(t *testing.T) { + var failTest = []struct { + Name string + Email string + ErrorMsg string + }{ + {"", "", "each maintainer requires a name"}, + {"", "test@test.com", "each maintainer requires a name"}, + {"John Snow", "wrongFormatEmail.com", "invalid email"}, + } + + var successTest = []struct { + Name string + Email string + }{ + {"John Snow", ""}, + {"John Snow", "john@winterfell.com"}, + } + + for _, test := range failTest { + badChart.Maintainers = []*chart.Maintainer{{Name: test.Name, Email: test.Email}} + err := validateChartMaintainer(badChart) + if err == nil || !strings.Contains(err.Error(), test.ErrorMsg) { + t.Errorf("validateChartMaintainer(%s, %s) to return \"%s\", got no error", test.Name, test.Email, test.ErrorMsg) + } + } + + for _, test := range successTest { + badChart.Maintainers = []*chart.Maintainer{{Name: test.Name, Email: test.Email}} + err := validateChartMaintainer(badChart) + if err != nil { + t.Errorf("validateChartMaintainer(%s, %s) to return no error, got %s", test.Name, test.Email, err.Error()) + } + } + + // Testing for an empty maintainer + badChart.Maintainers = []*chart.Maintainer{nil} + err := validateChartMaintainer(badChart) + if err == nil { + t.Errorf("validateChartMaintainer did not return error for nil maintainer as expected") + } + if err.Error() != "a maintainer entry is empty" { + t.Errorf("validateChartMaintainer returned unexpected error for nil maintainer: %s", err.Error()) + } +} + +func TestValidateChartSources(t *testing.T) { + var failTest = []string{"", "RiverRun", "john@winterfell", "riverrun.io"} + var successTest = []string{"http://riverrun.io", "https://riverrun.io", "https://riverrun.io/blackfish"} + for _, test := range failTest { + badChart.Sources = []string{test} + err := validateChartSources(badChart) + if err == nil || !strings.Contains(err.Error(), "invalid source URL") { + t.Errorf("validateChartSources(%s) to return \"invalid source URL\", got no error", test) + } + } + + for _, test := range successTest { + badChart.Sources = []string{test} + err := validateChartSources(badChart) + if err != nil { + t.Errorf("validateChartSources(%s) to return no error, got %s", test, err.Error()) + } + } +} + +func TestValidateChartIconPresence(t *testing.T) { + t.Run("Icon absent", func(t *testing.T) { + testChart := &chart.Metadata{ + Icon: "", + } + + err := validateChartIconPresence(testChart) + + if err == nil { + t.Errorf("validateChartIconPresence to return a linter error, got no error") + } else if !strings.Contains(err.Error(), "icon is recommended") { + t.Errorf("expected %q, got %q", "icon is recommended", err.Error()) + } + }) + t.Run("Icon present", func(t *testing.T) { + testChart := &chart.Metadata{ + Icon: "http://example.org/icon.png", + } + + err := validateChartIconPresence(testChart) + + if err != nil { + t.Errorf("Unexpected error: %q", err.Error()) + } + }) +} + +func TestValidateChartIconURL(t *testing.T) { + var failTest = []string{"RiverRun", "john@winterfell", "riverrun.io"} + var successTest = []string{"http://riverrun.io", "https://riverrun.io", "https://riverrun.io/blackfish.png"} + for _, test := range failTest { + badChart.Icon = test + err := validateChartIconURL(badChart) + if err == nil || !strings.Contains(err.Error(), "invalid icon URL") { + t.Errorf("validateChartIconURL(%s) to return \"invalid icon URL\", got no error", test) + } + } + + for _, test := range successTest { + badChart.Icon = test + err := validateChartSources(badChart) + if err != nil { + t.Errorf("validateChartIconURL(%s) to return no error, got %s", test, err.Error()) + } + } +} + +func TestV3Chartfile(t *testing.T) { + t.Run("Chart.yaml basic validity issues", func(t *testing.T) { + linter := support.Linter{ChartDir: badChartDir} + Chartfile(&linter) + msgs := linter.Messages + expectedNumberOfErrorMessages := 6 + + if len(msgs) != expectedNumberOfErrorMessages { + t.Errorf("Expected %d errors, got %d", expectedNumberOfErrorMessages, len(msgs)) + return + } + + if !strings.Contains(msgs[0].Err.Error(), "name is required") { + t.Errorf("Unexpected message 0: %s", msgs[0].Err) + } + + if !strings.Contains(msgs[1].Err.Error(), "apiVersion is required. The value must be \"v3\"") { + t.Errorf("Unexpected message 1: %s", msgs[1].Err) + } + + if !strings.Contains(msgs[2].Err.Error(), "version '0.0.0.0' is not a valid SemVer") { + t.Errorf("Unexpected message 2: %s", msgs[2].Err) + } + + if !strings.Contains(msgs[3].Err.Error(), "icon is recommended") { + t.Errorf("Unexpected message 3: %s", msgs[3].Err) + } + }) + + t.Run("Chart.yaml validity issues due to type mismatch", func(t *testing.T) { + linter := support.Linter{ChartDir: anotherBadChartDir} + Chartfile(&linter) + msgs := linter.Messages + expectedNumberOfErrorMessages := 3 + + if len(msgs) != expectedNumberOfErrorMessages { + t.Errorf("Expected %d errors, got %d", expectedNumberOfErrorMessages, len(msgs)) + return + } + + if !strings.Contains(msgs[0].Err.Error(), "version should be of type string") { + t.Errorf("Unexpected message 0: %s", msgs[0].Err) + } + + if !strings.Contains(msgs[1].Err.Error(), "version '7.2445e+06' is not a valid SemVer") { + t.Errorf("Unexpected message 1: %s", msgs[1].Err) + } + + if !strings.Contains(msgs[2].Err.Error(), "appVersion should be of type string") { + t.Errorf("Unexpected message 2: %s", msgs[2].Err) + } + }) +} diff --git a/internal/chart/v3/lint/rules/crds.go b/internal/chart/v3/lint/rules/crds.go new file mode 100644 index 000000000..6bafb52eb --- /dev/null +++ b/internal/chart/v3/lint/rules/crds.go @@ -0,0 +1,113 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + + "k8s.io/apimachinery/pkg/util/yaml" + + "helm.sh/helm/v4/internal/chart/v3/lint/support" + "helm.sh/helm/v4/internal/chart/v3/loader" +) + +// Crds lints the CRDs in the Linter. +func Crds(linter *support.Linter) { + fpath := "crds/" + crdsPath := filepath.Join(linter.ChartDir, fpath) + + // crds directory is optional + if _, err := os.Stat(crdsPath); errors.Is(err, fs.ErrNotExist) { + return + } + + crdsDirValid := linter.RunLinterRule(support.ErrorSev, fpath, validateCrdsDir(crdsPath)) + if !crdsDirValid { + return + } + + // Load chart and parse CRDs + chart, err := loader.Load(linter.ChartDir) + + chartLoaded := linter.RunLinterRule(support.ErrorSev, fpath, err) + + if !chartLoaded { + return + } + + /* Iterate over all the CRDs to check: + 1. It is a YAML file and not a template + 2. The API version is apiextensions.k8s.io + 3. The kind is CustomResourceDefinition + */ + for _, crd := range chart.CRDObjects() { + fileName := crd.Name + fpath = fileName + + decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(crd.File.Data), 4096) + for { + var yamlStruct *k8sYamlStruct + + err := decoder.Decode(&yamlStruct) + if err == io.EOF { + break + } + + // If YAML parsing fails here, it will always fail in the next block as well, so we should return here. + // This also confirms the YAML is not a template, since templates can't be decoded into a K8sYamlStruct. + if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) { + return + } + + linter.RunLinterRule(support.ErrorSev, fpath, validateCrdAPIVersion(yamlStruct)) + linter.RunLinterRule(support.ErrorSev, fpath, validateCrdKind(yamlStruct)) + } + } +} + +// Validation functions +func validateCrdsDir(crdsPath string) error { + fi, err := os.Stat(crdsPath) + if err != nil { + return err + } + if !fi.IsDir() { + return errors.New("not a directory") + } + return nil +} + +func validateCrdAPIVersion(obj *k8sYamlStruct) error { + if !strings.HasPrefix(obj.APIVersion, "apiextensions.k8s.io") { + return fmt.Errorf("apiVersion is not in 'apiextensions.k8s.io'") + } + return nil +} + +func validateCrdKind(obj *k8sYamlStruct) error { + if obj.Kind != "CustomResourceDefinition" { + return fmt.Errorf("object kind is not 'CustomResourceDefinition'") + } + return nil +} diff --git a/internal/chart/v3/lint/rules/crds_test.go b/internal/chart/v3/lint/rules/crds_test.go new file mode 100644 index 000000000..d93e3d978 --- /dev/null +++ b/internal/chart/v3/lint/rules/crds_test.go @@ -0,0 +1,36 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "helm.sh/helm/v4/internal/chart/v3/lint/support" +) + +const invalidCrdsDir = "./testdata/invalidcrdsdir" + +func TestInvalidCrdsDir(t *testing.T) { + linter := support.Linter{ChartDir: invalidCrdsDir} + Crds(&linter) + res := linter.Messages + + assert.Len(t, res, 1) + assert.ErrorContains(t, res[0].Err, "not a directory") +} diff --git a/internal/chart/v3/lint/rules/dependencies.go b/internal/chart/v3/lint/rules/dependencies.go new file mode 100644 index 000000000..f45153728 --- /dev/null +++ b/internal/chart/v3/lint/rules/dependencies.go @@ -0,0 +1,101 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules" + +import ( + "fmt" + "strings" + + chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/internal/chart/v3/lint/support" + "helm.sh/helm/v4/internal/chart/v3/loader" +) + +// Dependencies runs lints against a chart's dependencies +// +// See https://github.com/helm/helm/issues/7910 +func Dependencies(linter *support.Linter) { + c, err := loader.LoadDir(linter.ChartDir) + if !linter.RunLinterRule(support.ErrorSev, "", validateChartFormat(err)) { + return + } + + linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependencyInMetadata(c)) + linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependenciesUnique(c)) + linter.RunLinterRule(support.WarningSev, linter.ChartDir, validateDependencyInChartsDir(c)) +} + +func validateChartFormat(chartError error) error { + if chartError != nil { + return fmt.Errorf("unable to load chart\n\t%w", chartError) + } + return nil +} + +func validateDependencyInChartsDir(c *chart.Chart) (err error) { + dependencies := map[string]struct{}{} + missing := []string{} + for _, dep := range c.Dependencies() { + dependencies[dep.Metadata.Name] = struct{}{} + } + for _, dep := range c.Metadata.Dependencies { + if _, ok := dependencies[dep.Name]; !ok { + missing = append(missing, dep.Name) + } + } + if len(missing) > 0 { + err = fmt.Errorf("chart directory is missing these dependencies: %s", strings.Join(missing, ",")) + } + return err +} + +func validateDependencyInMetadata(c *chart.Chart) (err error) { + dependencies := map[string]struct{}{} + missing := []string{} + for _, dep := range c.Metadata.Dependencies { + dependencies[dep.Name] = struct{}{} + } + for _, dep := range c.Dependencies() { + if _, ok := dependencies[dep.Metadata.Name]; !ok { + missing = append(missing, dep.Metadata.Name) + } + } + if len(missing) > 0 { + err = fmt.Errorf("chart metadata is missing these dependencies: %s", strings.Join(missing, ",")) + } + return err +} + +func validateDependenciesUnique(c *chart.Chart) (err error) { + dependencies := map[string]*chart.Dependency{} + shadowing := []string{} + + for _, dep := range c.Metadata.Dependencies { + key := dep.Name + if dep.Alias != "" { + key = dep.Alias + } + if dependencies[key] != nil { + shadowing = append(shadowing, key) + } + dependencies[key] = dep + } + if len(shadowing) > 0 { + err = fmt.Errorf("multiple dependencies with name or alias: %s", strings.Join(shadowing, ",")) + } + return err +} diff --git a/internal/chart/v3/lint/rules/dependencies_test.go b/internal/chart/v3/lint/rules/dependencies_test.go new file mode 100644 index 000000000..b80e4b8a9 --- /dev/null +++ b/internal/chart/v3/lint/rules/dependencies_test.go @@ -0,0 +1,157 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package rules + +import ( + "path/filepath" + "testing" + + chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/internal/chart/v3/lint/support" + chartutil "helm.sh/helm/v4/internal/chart/v3/util" +) + +func chartWithBadDependencies() chart.Chart { + badChartDeps := chart.Chart{ + Metadata: &chart.Metadata{ + Name: "badchart", + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{ + { + Name: "sub2", + }, + { + Name: "sub3", + }, + }, + }, + } + + badChartDeps.SetDependencies( + &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "sub1", + Version: "0.1.0", + APIVersion: "v2", + }, + }, + &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "sub2", + Version: "0.1.0", + APIVersion: "v2", + }, + }, + ) + return badChartDeps +} + +func TestValidateDependencyInChartsDir(t *testing.T) { + c := chartWithBadDependencies() + + if err := validateDependencyInChartsDir(&c); err == nil { + t.Error("chart should have been flagged for missing deps in chart directory") + } +} + +func TestValidateDependencyInMetadata(t *testing.T) { + c := chartWithBadDependencies() + + if err := validateDependencyInMetadata(&c); err == nil { + t.Errorf("chart should have been flagged for missing deps in chart metadata") + } +} + +func TestValidateDependenciesUnique(t *testing.T) { + tests := []struct { + chart chart.Chart + }{ + {chart.Chart{ + Metadata: &chart.Metadata{ + Name: "badchart", + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{ + { + Name: "foo", + }, + { + Name: "foo", + }, + }, + }, + }}, + {chart.Chart{ + Metadata: &chart.Metadata{ + Name: "badchart", + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{ + { + Name: "foo", + Alias: "bar", + }, + { + Name: "bar", + }, + }, + }, + }}, + {chart.Chart{ + Metadata: &chart.Metadata{ + Name: "badchart", + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{ + { + Name: "foo", + Alias: "baz", + }, + { + Name: "bar", + Alias: "baz", + }, + }, + }, + }}, + } + + for _, tt := range tests { + if err := validateDependenciesUnique(&tt.chart); err == nil { + t.Errorf("chart should have been flagged for dependency shadowing") + } + } +} + +func TestDependencies(t *testing.T) { + tmp := t.TempDir() + + c := chartWithBadDependencies() + err := chartutil.SaveDir(&c, tmp) + if err != nil { + t.Fatal(err) + } + linter := support.Linter{ChartDir: filepath.Join(tmp, c.Metadata.Name)} + + Dependencies(&linter) + if l := len(linter.Messages); l != 2 { + t.Errorf("expected 2 linter errors for bad chart dependencies. Got %d.", l) + for i, msg := range linter.Messages { + t.Logf("Message: %d, Error: %#v", i, msg) + } + } +} diff --git a/pkg/lint/rules/deprecations.go b/internal/chart/v3/lint/rules/deprecations.go similarity index 95% rename from pkg/lint/rules/deprecations.go rename to internal/chart/v3/lint/rules/deprecations.go index c6d635a5e..6f86bdbbd 100644 --- a/pkg/lint/rules/deprecations.go +++ b/internal/chart/v3/lint/rules/deprecations.go @@ -14,18 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -package rules // import "helm.sh/helm/v4/pkg/lint/rules" +package rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules" import ( "fmt" "strconv" + "helm.sh/helm/v4/pkg/chart/common" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/endpoints/deprecation" kscheme "k8s.io/client-go/kubernetes/scheme" - - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" ) var ( @@ -47,7 +47,7 @@ func (e deprecatedAPIError) Error() string { return msg } -func validateNoDeprecations(resource *k8sYamlStruct, kubeVersion *chartutil.KubeVersion) error { +func validateNoDeprecations(resource *k8sYamlStruct, kubeVersion *common.KubeVersion) error { // if `resource` does not have an APIVersion or Kind, we cannot test it for deprecation if resource.APIVersion == "" { return nil diff --git a/internal/chart/v3/lint/rules/deprecations_test.go b/internal/chart/v3/lint/rules/deprecations_test.go new file mode 100644 index 000000000..35e541e5c --- /dev/null +++ b/internal/chart/v3/lint/rules/deprecations_test.go @@ -0,0 +1,41 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules" + +import "testing" + +func TestValidateNoDeprecations(t *testing.T) { + deprecated := &k8sYamlStruct{ + APIVersion: "extensions/v1beta1", + Kind: "Deployment", + } + err := validateNoDeprecations(deprecated, nil) + if err == nil { + t.Fatal("Expected deprecated extension to be flagged") + } + depErr := err.(deprecatedAPIError) + if depErr.Message == "" { + t.Fatalf("Expected error message to be non-blank: %v", err) + } + + if err := validateNoDeprecations(&k8sYamlStruct{ + APIVersion: "v1", + Kind: "Pod", + }, nil); err != nil { + t.Errorf("Expected a v1 Pod to not be deprecated") + } +} diff --git a/internal/chart/v3/lint/rules/template.go b/internal/chart/v3/lint/rules/template.go new file mode 100644 index 000000000..d4c62839f --- /dev/null +++ b/internal/chart/v3/lint/rules/template.go @@ -0,0 +1,348 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "os" + "path" + "path/filepath" + "slices" + "strings" + + "k8s.io/apimachinery/pkg/api/validation" + apipath "k8s.io/apimachinery/pkg/api/validation/path" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apimachinery/pkg/util/yaml" + + "helm.sh/helm/v4/internal/chart/v3/lint/support" + "helm.sh/helm/v4/internal/chart/v3/loader" + chartutil "helm.sh/helm/v4/internal/chart/v3/util" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/common/util" + "helm.sh/helm/v4/pkg/engine" +) + +// Templates lints the templates in the Linter. +func Templates(linter *support.Linter, values map[string]interface{}, namespace string, _ bool) { + TemplatesWithKubeVersion(linter, values, namespace, nil) +} + +// TemplatesWithKubeVersion lints the templates in the Linter, allowing to specify the kubernetes version. +func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion) { + TemplatesWithSkipSchemaValidation(linter, values, namespace, kubeVersion, false) +} + +// TemplatesWithSkipSchemaValidation lints the templates in the Linter, allowing to specify the kubernetes version and if schema validation is enabled or not. +func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion, skipSchemaValidation bool) { + fpath := "templates/" + templatesPath := filepath.Join(linter.ChartDir, fpath) + + // Templates directory is optional for now + templatesDirExists := linter.RunLinterRule(support.WarningSev, fpath, templatesDirExists(templatesPath)) + if !templatesDirExists { + return + } + + validTemplatesDir := linter.RunLinterRule(support.ErrorSev, fpath, validateTemplatesDir(templatesPath)) + if !validTemplatesDir { + return + } + + // Load chart and parse templates + chart, err := loader.Load(linter.ChartDir) + + chartLoaded := linter.RunLinterRule(support.ErrorSev, fpath, err) + + if !chartLoaded { + return + } + + options := common.ReleaseOptions{ + Name: "test-release", + Namespace: namespace, + } + + caps := common.DefaultCapabilities.Copy() + if kubeVersion != nil { + caps.KubeVersion = *kubeVersion + } + + // lint ignores import-values + // See https://github.com/helm/helm/issues/9658 + if err := chartutil.ProcessDependencies(chart, values); err != nil { + return + } + + cvals, err := util.CoalesceValues(chart, values) + if err != nil { + return + } + + valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, skipSchemaValidation) + if err != nil { + linter.RunLinterRule(support.ErrorSev, fpath, err) + return + } + var e engine.Engine + e.LintMode = true + renderedContentMap, err := e.Render(chart, valuesToRender) + + renderOk := linter.RunLinterRule(support.ErrorSev, fpath, err) + + if !renderOk { + return + } + + /* Iterate over all the templates to check: + - It is a .yaml file + - All the values in the template file is defined + - {{}} include | quote + - Generated content is a valid Yaml file + - Metadata.Namespace is not set + */ + for _, template := range chart.Templates { + fileName := template.Name + fpath = fileName + + linter.RunLinterRule(support.ErrorSev, fpath, validateAllowedExtension(fileName)) + + // We only apply the following lint rules to yaml files + if filepath.Ext(fileName) != ".yaml" || filepath.Ext(fileName) == ".yml" { + continue + } + + // NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1463 + // Check that all the templates have a matching value + // linter.RunLinterRule(support.WarningSev, fpath, validateNoMissingValues(templatesPath, valuesToRender, preExecutedTemplate)) + + // NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1037 + // linter.RunLinterRule(support.WarningSev, fpath, validateQuotes(string(preExecutedTemplate))) + + renderedContent := renderedContentMap[path.Join(chart.Name(), fileName)] + if strings.TrimSpace(renderedContent) != "" { + linter.RunLinterRule(support.WarningSev, fpath, validateTopIndentLevel(renderedContent)) + + decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(renderedContent), 4096) + + // Lint all resources if the file contains multiple documents separated by --- + for { + // Even though k8sYamlStruct only defines a few fields, an error in any other + // key will be raised as well + var yamlStruct *k8sYamlStruct + + err := decoder.Decode(&yamlStruct) + if err == io.EOF { + break + } + + // If YAML linting fails here, it will always fail in the next block as well, so we should return here. + // fix https://github.com/helm/helm/issues/11391 + if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) { + return + } + if yamlStruct != nil { + // NOTE: set to warnings to allow users to support out-of-date kubernetes + // Refs https://github.com/helm/helm/issues/8596 + linter.RunLinterRule(support.WarningSev, fpath, validateMetadataName(yamlStruct)) + linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct, kubeVersion)) + + linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent)) + linter.RunLinterRule(support.ErrorSev, fpath, validateListAnnotations(yamlStruct, renderedContent)) + } + } + } + } +} + +// validateTopIndentLevel checks that the content does not start with an indent level > 0. +// +// This error can occur when a template accidentally inserts space. It can cause +// unpredictable errors depending on whether the text is normalized before being passed +// into the YAML parser. So we trap it here. +// +// See https://github.com/helm/helm/issues/8467 +func validateTopIndentLevel(content string) error { + // Read lines until we get to a non-empty one + scanner := bufio.NewScanner(bytes.NewBufferString(content)) + for scanner.Scan() { + line := scanner.Text() + // If line is empty, skip + if strings.TrimSpace(line) == "" { + continue + } + // If it starts with one or more spaces, this is an error + if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") { + return fmt.Errorf("document starts with an illegal indent: %q, which may cause parsing problems", line) + } + // Any other condition passes. + return nil + } + return scanner.Err() +} + +// Validation functions +func templatesDirExists(templatesPath string) error { + _, err := os.Stat(templatesPath) + if errors.Is(err, os.ErrNotExist) { + return errors.New("directory does not exist") + } + return nil +} + +func validateTemplatesDir(templatesPath string) error { + fi, err := os.Stat(templatesPath) + if err != nil { + return err + } + if !fi.IsDir() { + return errors.New("not a directory") + } + return nil +} + +func validateAllowedExtension(fileName string) error { + ext := filepath.Ext(fileName) + validExtensions := []string{".yaml", ".yml", ".tpl", ".txt"} + + if slices.Contains(validExtensions, ext) { + return nil + } + + return fmt.Errorf("file extension '%s' not valid. Valid extensions are .yaml, .yml, .tpl, or .txt", ext) +} + +func validateYamlContent(err error) error { + if err != nil { + return fmt.Errorf("unable to parse YAML: %w", err) + } + return nil +} + +// validateMetadataName uses the correct validation function for the object +// Kind, or if not set, defaults to the standard definition of a subdomain in +// DNS (RFC 1123), used by most resources. +func validateMetadataName(obj *k8sYamlStruct) error { + fn := validateMetadataNameFunc(obj) + allErrs := field.ErrorList{} + for _, msg := range fn(obj.Metadata.Name, false) { + allErrs = append(allErrs, field.Invalid(field.NewPath("metadata").Child("name"), obj.Metadata.Name, msg)) + } + if len(allErrs) > 0 { + return fmt.Errorf("object name does not conform to Kubernetes naming requirements: %q: %w", obj.Metadata.Name, allErrs.ToAggregate()) + } + return nil +} + +// validateMetadataNameFunc will return a name validation function for the +// object kind, if defined below. +// +// Rules should match those set in the various api validations: +// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/core/validation/validation.go#L205-L274 +// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/apps/validation/validation.go#L39 +// ... +// +// Implementing here to avoid importing k/k. +// +// If no mapping is defined, returns NameIsDNSSubdomain. This is used by object +// kinds that don't have special requirements, so is the most likely to work if +// new kinds are added. +func validateMetadataNameFunc(obj *k8sYamlStruct) validation.ValidateNameFunc { + switch strings.ToLower(obj.Kind) { + case "pod", "node", "secret", "endpoints", "resourcequota", // core + "controllerrevision", "daemonset", "deployment", "replicaset", "statefulset", // apps + "autoscaler", // autoscaler + "cronjob", "job", // batch + "lease", // coordination + "endpointslice", // discovery + "networkpolicy", "ingress", // networking + "podsecuritypolicy", // policy + "priorityclass", // scheduling + "podpreset", // settings + "storageclass", "volumeattachment", "csinode": // storage + return validation.NameIsDNSSubdomain + case "service": + return validation.NameIsDNS1035Label + case "namespace": + return validation.ValidateNamespaceName + case "serviceaccount": + return validation.ValidateServiceAccountName + case "certificatesigningrequest": + // No validation. + // https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/certificates/validation/validation.go#L137-L140 + return func(_ string, _ bool) []string { return nil } + case "role", "clusterrole", "rolebinding", "clusterrolebinding": + // https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/rbac/validation/validation.go#L32-L34 + return func(name string, _ bool) []string { + return apipath.IsValidPathSegmentName(name) + } + default: + return validation.NameIsDNSSubdomain + } +} + +// validateMatchSelector ensures that template specs have a selector declared. +// See https://github.com/helm/helm/issues/1990 +func validateMatchSelector(yamlStruct *k8sYamlStruct, manifest string) error { + switch yamlStruct.Kind { + case "Deployment", "ReplicaSet", "DaemonSet", "StatefulSet": + // verify that matchLabels or matchExpressions is present + if !strings.Contains(manifest, "matchLabels") && !strings.Contains(manifest, "matchExpressions") { + return fmt.Errorf("a %s must contain matchLabels or matchExpressions, and %q does not", yamlStruct.Kind, yamlStruct.Metadata.Name) + } + } + return nil +} + +func validateListAnnotations(yamlStruct *k8sYamlStruct, manifest string) error { + if yamlStruct.Kind == "List" { + m := struct { + Items []struct { + Metadata struct { + Annotations map[string]string + } + } + }{} + + if err := yaml.Unmarshal([]byte(manifest), &m); err != nil { + return validateYamlContent(err) + } + + for _, i := range m.Items { + if _, ok := i.Metadata.Annotations["helm.sh/resource-policy"]; ok { + return errors.New("annotation 'helm.sh/resource-policy' within List objects are ignored") + } + } + } + return nil +} + +// k8sYamlStruct stubs a Kubernetes YAML file. +type k8sYamlStruct struct { + APIVersion string `json:"apiVersion"` + Kind string + Metadata k8sYamlMetadata +} + +type k8sYamlMetadata struct { + Namespace string + Name string +} diff --git a/internal/chart/v3/lint/rules/template_test.go b/internal/chart/v3/lint/rules/template_test.go new file mode 100644 index 000000000..40bcfa26b --- /dev/null +++ b/internal/chart/v3/lint/rules/template_test.go @@ -0,0 +1,441 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/internal/chart/v3/lint/support" + chartutil "helm.sh/helm/v4/internal/chart/v3/util" + "helm.sh/helm/v4/pkg/chart/common" +) + +const templateTestBasedir = "./testdata/albatross" + +func TestValidateAllowedExtension(t *testing.T) { + var failTest = []string{"/foo", "/test.toml"} + for _, test := range failTest { + err := validateAllowedExtension(test) + if err == nil || !strings.Contains(err.Error(), "Valid extensions are .yaml, .yml, .tpl, or .txt") { + t.Errorf("validateAllowedExtension('%s') to return \"Valid extensions are .yaml, .yml, .tpl, or .txt\", got no error", test) + } + } + var successTest = []string{"/foo.yaml", "foo.yaml", "foo.tpl", "/foo/bar/baz.yaml", "NOTES.txt"} + for _, test := range successTest { + err := validateAllowedExtension(test) + if err != nil { + t.Errorf("validateAllowedExtension('%s') to return no error but got \"%s\"", test, err.Error()) + } + } +} + +var values = map[string]interface{}{"nameOverride": "", "httpPort": 80} + +const namespace = "testNamespace" +const strict = false + +func TestTemplateParsing(t *testing.T) { + linter := support.Linter{ChartDir: templateTestBasedir} + Templates(&linter, values, namespace, strict) + res := linter.Messages + + if len(res) != 1 { + t.Fatalf("Expected one error, got %d, %v", len(res), res) + } + + if !strings.Contains(res[0].Err.Error(), "deliberateSyntaxError") { + t.Errorf("Unexpected error: %s", res[0]) + } +} + +var wrongTemplatePath = filepath.Join(templateTestBasedir, "templates", "fail.yaml") +var ignoredTemplatePath = filepath.Join(templateTestBasedir, "fail.yaml.ignored") + +// Test a template with all the existing features: +// namespaces, partial templates +func TestTemplateIntegrationHappyPath(t *testing.T) { + // Rename file so it gets ignored by the linter + os.Rename(wrongTemplatePath, ignoredTemplatePath) + defer os.Rename(ignoredTemplatePath, wrongTemplatePath) + + linter := support.Linter{ChartDir: templateTestBasedir} + Templates(&linter, values, namespace, strict) + res := linter.Messages + + if len(res) != 0 { + t.Fatalf("Expected no error, got %d, %v", len(res), res) + } +} + +func TestMultiTemplateFail(t *testing.T) { + linter := support.Linter{ChartDir: "./testdata/multi-template-fail"} + Templates(&linter, values, namespace, strict) + res := linter.Messages + + if len(res) != 1 { + t.Fatalf("Expected 1 error, got %d, %v", len(res), res) + } + + if !strings.Contains(res[0].Err.Error(), "object name does not conform to Kubernetes naming requirements") { + t.Errorf("Unexpected error: %s", res[0].Err) + } +} + +func TestValidateMetadataName(t *testing.T) { + tests := []struct { + obj *k8sYamlStruct + wantErr bool + }{ + // Most kinds use IsDNS1123Subdomain. + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: ""}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "FOO"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo.BAR.baz"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "one-two"}}, false}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "-two"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "one_two"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "a..b"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true}, + {&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false}, + {&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "FOO"}}, true}, + {&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "operator:sa"}}, true}, + + // Service uses IsDNS1035Label. + {&k8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "123baz"}}, true}, + {&k8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, true}, + + // Namespace uses IsDNS1123Label. + {&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, true}, + {&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo-bar"}}, false}, + + // CertificateSigningRequest has no validation. + {&k8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: ""}}, false}, + {&k8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, false}, + + // RBAC uses path validation. + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, false}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator/role"}}, true}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator%role"}}, true}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator/role"}}, true}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator%role"}}, true}, + {&k8sYamlStruct{Kind: "RoleBinding", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRoleBinding", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + + // Unknown Kind + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: ""}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "FOO"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo.BAR.baz"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "one-two"}}, false}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "-two"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "one_two"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "a..b"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true}, + + // No kind + {&k8sYamlStruct{Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true}, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("%s/%s", tt.obj.Kind, tt.obj.Metadata.Name), func(t *testing.T) { + if err := validateMetadataName(tt.obj); (err != nil) != tt.wantErr { + t.Errorf("validateMetadataName() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestDeprecatedAPIFails(t *testing.T) { + mychart := chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: "v2", + Name: "failapi", + Version: "0.1.0", + Icon: "satisfy-the-linting-gods.gif", + }, + Templates: []*common.File{ + { + Name: "templates/baddeployment.yaml", + Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"), + }, + { + Name: "templates/goodsecret.yaml", + Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"), + }, + }, + } + tmpdir := t.TempDir() + + if err := chartutil.SaveDir(&mychart, tmpdir); err != nil { + t.Fatal(err) + } + + linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())} + Templates(&linter, values, namespace, strict) + if l := len(linter.Messages); l != 1 { + for i, msg := range linter.Messages { + t.Logf("Message %d: %s", i, msg) + } + t.Fatalf("Expected 1 lint error, got %d", l) + } + + err := linter.Messages[0].Err.(deprecatedAPIError) + if err.Deprecated != "apps/v1beta1 Deployment" { + t.Errorf("Surprised to learn that %q is deprecated", err.Deprecated) + } +} + +const manifest = `apiVersion: v1 +kind: ConfigMap +metadata: + name: foo +data: + myval1: {{default "val" .Values.mymap.key1 }} + myval2: {{default "val" .Values.mymap.key2 }} +` + +// TestStrictTemplateParsingMapError is a regression test. +// +// The template engine should not produce an error when a map in values.yaml does +// not contain all possible keys. +// +// See https://github.com/helm/helm/issues/7483 +func TestStrictTemplateParsingMapError(t *testing.T) { + + ch := chart.Chart{ + Metadata: &chart.Metadata{ + Name: "regression7483", + APIVersion: "v2", + Version: "0.1.0", + }, + Values: map[string]interface{}{ + "mymap": map[string]string{ + "key1": "val1", + }, + }, + Templates: []*common.File{ + { + Name: "templates/configmap.yaml", + Data: []byte(manifest), + }, + }, + } + dir := t.TempDir() + if err := chartutil.SaveDir(&ch, dir); err != nil { + t.Fatal(err) + } + linter := &support.Linter{ + ChartDir: filepath.Join(dir, ch.Metadata.Name), + } + Templates(linter, ch.Values, namespace, strict) + if len(linter.Messages) != 0 { + t.Errorf("expected zero messages, got %d", len(linter.Messages)) + for i, msg := range linter.Messages { + t.Logf("Message %d: %q", i, msg) + } + } +} + +func TestValidateMatchSelector(t *testing.T) { + md := &k8sYamlStruct{ + APIVersion: "apps/v1", + Kind: "Deployment", + Metadata: k8sYamlMetadata{ + Name: "mydeployment", + }, + } + manifest := ` + apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ` + if err := validateMatchSelector(md, manifest); err != nil { + t.Error(err) + } + manifest = ` + apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchExpressions: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ` + if err := validateMatchSelector(md, manifest); err != nil { + t.Error(err) + } + manifest = ` + apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ` + if err := validateMatchSelector(md, manifest); err == nil { + t.Error("expected Deployment with no selector to fail") + } +} + +func TestValidateTopIndentLevel(t *testing.T) { + for doc, shouldFail := range map[string]bool{ + // Should not fail + "\n\n\n\t\n \t\n": false, + "apiVersion:foo\n bar:baz": false, + "\n\n\napiVersion:foo\n\n\n": false, + // Should fail + " apiVersion:foo": true, + "\n\n apiVersion:foo\n\n": true, + } { + if err := validateTopIndentLevel(doc); (err == nil) == shouldFail { + t.Errorf("Expected %t for %q", shouldFail, doc) + } + } + +} + +// TestEmptyWithCommentsManifests checks the lint is not failing against empty manifests that contains only comments +// See https://github.com/helm/helm/issues/8621 +func TestEmptyWithCommentsManifests(t *testing.T) { + mychart := chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: "v2", + Name: "emptymanifests", + Version: "0.1.0", + Icon: "satisfy-the-linting-gods.gif", + }, + Templates: []*common.File{ + { + Name: "templates/empty-with-comments.yaml", + Data: []byte("#@formatter:off\n"), + }, + }, + } + tmpdir := t.TempDir() + + if err := chartutil.SaveDir(&mychart, tmpdir); err != nil { + t.Fatal(err) + } + + linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())} + Templates(&linter, values, namespace, strict) + if l := len(linter.Messages); l > 0 { + for i, msg := range linter.Messages { + t.Logf("Message %d: %s", i, msg) + } + t.Fatalf("Expected 0 lint errors, got %d", l) + } +} +func TestValidateListAnnotations(t *testing.T) { + md := &k8sYamlStruct{ + APIVersion: "v1", + Kind: "List", + Metadata: k8sYamlMetadata{ + Name: "list", + }, + } + manifest := ` +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: ConfigMap + metadata: + annotations: + helm.sh/resource-policy: keep +` + + if err := validateListAnnotations(md, manifest); err == nil { + t.Fatal("expected list with nested keep annotations to fail") + } + + manifest = ` +apiVersion: v1 +kind: List +metadata: + annotations: + helm.sh/resource-policy: keep +items: + - apiVersion: v1 + kind: ConfigMap +` + + if err := validateListAnnotations(md, manifest); err != nil { + t.Fatalf("List objects keep annotations should pass. got: %s", err) + } +} diff --git a/internal/chart/v3/lint/rules/testdata/albatross/Chart.yaml b/internal/chart/v3/lint/rules/testdata/albatross/Chart.yaml new file mode 100644 index 000000000..5e1ed515c --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/albatross/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: albatross +description: testing chart +version: 199.44.12345-Alpha.1+cafe009 +icon: http://riverrun.io diff --git a/pkg/lint/rules/testdata/albatross/templates/_helpers.tpl b/internal/chart/v3/lint/rules/testdata/albatross/templates/_helpers.tpl similarity index 100% rename from pkg/lint/rules/testdata/albatross/templates/_helpers.tpl rename to internal/chart/v3/lint/rules/testdata/albatross/templates/_helpers.tpl diff --git a/pkg/lint/rules/testdata/albatross/templates/fail.yaml b/internal/chart/v3/lint/rules/testdata/albatross/templates/fail.yaml similarity index 100% rename from pkg/lint/rules/testdata/albatross/templates/fail.yaml rename to internal/chart/v3/lint/rules/testdata/albatross/templates/fail.yaml diff --git a/pkg/lint/rules/testdata/albatross/templates/svc.yaml b/internal/chart/v3/lint/rules/testdata/albatross/templates/svc.yaml similarity index 100% rename from pkg/lint/rules/testdata/albatross/templates/svc.yaml rename to internal/chart/v3/lint/rules/testdata/albatross/templates/svc.yaml diff --git a/pkg/lint/rules/testdata/albatross/values.yaml b/internal/chart/v3/lint/rules/testdata/albatross/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/albatross/values.yaml rename to internal/chart/v3/lint/rules/testdata/albatross/values.yaml diff --git a/internal/chart/v3/lint/rules/testdata/anotherbadchartfile/Chart.yaml b/internal/chart/v3/lint/rules/testdata/anotherbadchartfile/Chart.yaml new file mode 100644 index 000000000..8a598473b --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/anotherbadchartfile/Chart.yaml @@ -0,0 +1,15 @@ +name: "some-chart" +apiVersion: v3 +description: A Helm chart for Kubernetes +version: 72445e2 +home: "" +type: application +appVersion: 72225e2 +icon: "https://some-url.com/icon.jpeg" +dependencies: + - name: mariadb + version: 5.x.x + repository: https://charts.helm.sh/stable/ + condition: mariadb.enabled + tags: + - database diff --git a/pkg/lint/rules/testdata/badchartfile/Chart.yaml b/internal/chart/v3/lint/rules/testdata/badchartfile/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/badchartfile/Chart.yaml rename to internal/chart/v3/lint/rules/testdata/badchartfile/Chart.yaml diff --git a/pkg/lint/rules/testdata/badchartfile/values.yaml b/internal/chart/v3/lint/rules/testdata/badchartfile/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/badchartfile/values.yaml rename to internal/chart/v3/lint/rules/testdata/badchartfile/values.yaml diff --git a/internal/chart/v3/lint/rules/testdata/badchartname/Chart.yaml b/internal/chart/v3/lint/rules/testdata/badchartname/Chart.yaml new file mode 100644 index 000000000..41f452354 --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/badchartname/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +description: A Helm chart for Kubernetes +version: 0.1.0 +name: "../badchartname" +type: application diff --git a/pkg/lint/rules/testdata/badchartname/values.yaml b/internal/chart/v3/lint/rules/testdata/badchartname/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/badchartname/values.yaml rename to internal/chart/v3/lint/rules/testdata/badchartname/values.yaml diff --git a/internal/chart/v3/lint/rules/testdata/badcrdfile/Chart.yaml b/internal/chart/v3/lint/rules/testdata/badcrdfile/Chart.yaml new file mode 100644 index 000000000..3bf007393 --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/badcrdfile/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v3 +description: A Helm chart for Kubernetes +version: 0.1.0 +name: badcrdfile +type: application +icon: http://riverrun.io diff --git a/pkg/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml b/internal/chart/v3/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml similarity index 100% rename from pkg/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml rename to internal/chart/v3/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml diff --git a/pkg/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml b/internal/chart/v3/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml similarity index 100% rename from pkg/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml rename to internal/chart/v3/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml diff --git a/pkg/lint/rules/testdata/badcrdfile/templates/.gitkeep b/internal/chart/v3/lint/rules/testdata/badcrdfile/templates/.gitkeep similarity index 100% rename from pkg/lint/rules/testdata/badcrdfile/templates/.gitkeep rename to internal/chart/v3/lint/rules/testdata/badcrdfile/templates/.gitkeep diff --git a/pkg/lint/rules/testdata/badcrdfile/values.yaml b/internal/chart/v3/lint/rules/testdata/badcrdfile/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/badcrdfile/values.yaml rename to internal/chart/v3/lint/rules/testdata/badcrdfile/values.yaml diff --git a/internal/chart/v3/lint/rules/testdata/badvaluesfile/Chart.yaml b/internal/chart/v3/lint/rules/testdata/badvaluesfile/Chart.yaml new file mode 100644 index 000000000..aace27e21 --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/badvaluesfile/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v3 +name: badvaluesfile +description: A Helm chart for Kubernetes +version: 0.0.1 +home: "" +icon: http://riverrun.io diff --git a/pkg/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml b/internal/chart/v3/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml similarity index 100% rename from pkg/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml rename to internal/chart/v3/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml diff --git a/pkg/lint/rules/testdata/badvaluesfile/values.yaml b/internal/chart/v3/lint/rules/testdata/badvaluesfile/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/badvaluesfile/values.yaml rename to internal/chart/v3/lint/rules/testdata/badvaluesfile/values.yaml diff --git a/internal/chart/v3/lint/rules/testdata/goodone/Chart.yaml b/internal/chart/v3/lint/rules/testdata/goodone/Chart.yaml new file mode 100644 index 000000000..bf8f5e309 --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/goodone/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: goodone +description: good testing chart +version: 199.44.12345-Alpha.1+cafe009 +icon: http://riverrun.io diff --git a/pkg/lint/rules/testdata/goodone/crds/test-crd.yaml b/internal/chart/v3/lint/rules/testdata/goodone/crds/test-crd.yaml similarity index 100% rename from pkg/lint/rules/testdata/goodone/crds/test-crd.yaml rename to internal/chart/v3/lint/rules/testdata/goodone/crds/test-crd.yaml diff --git a/pkg/lint/rules/testdata/goodone/templates/goodone.yaml b/internal/chart/v3/lint/rules/testdata/goodone/templates/goodone.yaml similarity index 100% rename from pkg/lint/rules/testdata/goodone/templates/goodone.yaml rename to internal/chart/v3/lint/rules/testdata/goodone/templates/goodone.yaml diff --git a/pkg/lint/rules/testdata/goodone/values.yaml b/internal/chart/v3/lint/rules/testdata/goodone/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/goodone/values.yaml rename to internal/chart/v3/lint/rules/testdata/goodone/values.yaml diff --git a/pkg/lint/rules/testdata/invalidchartfile/Chart.yaml b/internal/chart/v3/lint/rules/testdata/invalidchartfile/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/invalidchartfile/Chart.yaml rename to internal/chart/v3/lint/rules/testdata/invalidchartfile/Chart.yaml diff --git a/pkg/lint/rules/testdata/invalidchartfile/values.yaml b/internal/chart/v3/lint/rules/testdata/invalidchartfile/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/invalidchartfile/values.yaml rename to internal/chart/v3/lint/rules/testdata/invalidchartfile/values.yaml diff --git a/internal/chart/v3/lint/rules/testdata/invalidcrdsdir/Chart.yaml b/internal/chart/v3/lint/rules/testdata/invalidcrdsdir/Chart.yaml new file mode 100644 index 000000000..0f6d1ee98 --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/invalidcrdsdir/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v3 +description: A Helm chart for Kubernetes +version: 0.1.0 +name: invalidcrdsdir +type: application +icon: http://riverrun.io diff --git a/pkg/lint/rules/testdata/invalidcrdsdir/crds b/internal/chart/v3/lint/rules/testdata/invalidcrdsdir/crds similarity index 100% rename from pkg/lint/rules/testdata/invalidcrdsdir/crds rename to internal/chart/v3/lint/rules/testdata/invalidcrdsdir/crds diff --git a/pkg/lint/rules/testdata/invalidcrdsdir/values.yaml b/internal/chart/v3/lint/rules/testdata/invalidcrdsdir/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/invalidcrdsdir/values.yaml rename to internal/chart/v3/lint/rules/testdata/invalidcrdsdir/values.yaml diff --git a/pkg/lint/rules/testdata/malformed-template/.helmignore b/internal/chart/v3/lint/rules/testdata/malformed-template/.helmignore similarity index 100% rename from pkg/lint/rules/testdata/malformed-template/.helmignore rename to internal/chart/v3/lint/rules/testdata/malformed-template/.helmignore diff --git a/internal/chart/v3/lint/rules/testdata/malformed-template/Chart.yaml b/internal/chart/v3/lint/rules/testdata/malformed-template/Chart.yaml new file mode 100644 index 000000000..d46b98cb5 --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/malformed-template/Chart.yaml @@ -0,0 +1,25 @@ +apiVersion: v3 +name: test +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" +icon: https://riverrun.io \ No newline at end of file diff --git a/pkg/lint/rules/testdata/malformed-template/templates/bad.yaml b/internal/chart/v3/lint/rules/testdata/malformed-template/templates/bad.yaml similarity index 100% rename from pkg/lint/rules/testdata/malformed-template/templates/bad.yaml rename to internal/chart/v3/lint/rules/testdata/malformed-template/templates/bad.yaml diff --git a/pkg/lint/rules/testdata/malformed-template/values.yaml b/internal/chart/v3/lint/rules/testdata/malformed-template/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/malformed-template/values.yaml rename to internal/chart/v3/lint/rules/testdata/malformed-template/values.yaml diff --git a/internal/chart/v3/lint/rules/testdata/multi-template-fail/Chart.yaml b/internal/chart/v3/lint/rules/testdata/multi-template-fail/Chart.yaml new file mode 100644 index 000000000..bfb580bea --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/multi-template-fail/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v3 +name: multi-template-fail +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application and it is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/pkg/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml b/internal/chart/v3/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml similarity index 100% rename from pkg/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml rename to internal/chart/v3/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml diff --git a/internal/chart/v3/lint/rules/testdata/v3-fail/Chart.yaml b/internal/chart/v3/lint/rules/testdata/v3-fail/Chart.yaml new file mode 100644 index 000000000..2a29c33fa --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/v3-fail/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v3 +name: v3-fail +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application and it is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/pkg/lint/rules/testdata/v3-fail/templates/_helpers.tpl b/internal/chart/v3/lint/rules/testdata/v3-fail/templates/_helpers.tpl similarity index 100% rename from pkg/lint/rules/testdata/v3-fail/templates/_helpers.tpl rename to internal/chart/v3/lint/rules/testdata/v3-fail/templates/_helpers.tpl diff --git a/pkg/lint/rules/testdata/v3-fail/templates/deployment.yaml b/internal/chart/v3/lint/rules/testdata/v3-fail/templates/deployment.yaml similarity index 100% rename from pkg/lint/rules/testdata/v3-fail/templates/deployment.yaml rename to internal/chart/v3/lint/rules/testdata/v3-fail/templates/deployment.yaml diff --git a/pkg/lint/rules/testdata/v3-fail/templates/ingress.yaml b/internal/chart/v3/lint/rules/testdata/v3-fail/templates/ingress.yaml similarity index 100% rename from pkg/lint/rules/testdata/v3-fail/templates/ingress.yaml rename to internal/chart/v3/lint/rules/testdata/v3-fail/templates/ingress.yaml diff --git a/pkg/lint/rules/testdata/v3-fail/templates/service.yaml b/internal/chart/v3/lint/rules/testdata/v3-fail/templates/service.yaml similarity index 100% rename from pkg/lint/rules/testdata/v3-fail/templates/service.yaml rename to internal/chart/v3/lint/rules/testdata/v3-fail/templates/service.yaml diff --git a/pkg/lint/rules/testdata/v3-fail/values.yaml b/internal/chart/v3/lint/rules/testdata/v3-fail/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/v3-fail/values.yaml rename to internal/chart/v3/lint/rules/testdata/v3-fail/values.yaml diff --git a/internal/chart/v3/lint/rules/testdata/withsubchart/Chart.yaml b/internal/chart/v3/lint/rules/testdata/withsubchart/Chart.yaml new file mode 100644 index 000000000..fa15eabaf --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/withsubchart/Chart.yaml @@ -0,0 +1,16 @@ +apiVersion: v3 +name: withsubchart +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "1.16.0" +icon: http://riverrun.io + +dependencies: + - name: subchart + version: 0.1.16 + repository: "file://../subchart" + import-values: + - child: subchart + parent: subchart + diff --git a/internal/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml b/internal/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml new file mode 100644 index 000000000..35b13e70d --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v3 +name: subchart +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "1.16.0" diff --git a/pkg/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml b/internal/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml similarity index 100% rename from pkg/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml rename to internal/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml diff --git a/pkg/lint/rules/testdata/withsubchart/charts/subchart/values.yaml b/internal/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/withsubchart/charts/subchart/values.yaml rename to internal/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/values.yaml diff --git a/pkg/lint/rules/testdata/withsubchart/templates/mainchart.yaml b/internal/chart/v3/lint/rules/testdata/withsubchart/templates/mainchart.yaml similarity index 100% rename from pkg/lint/rules/testdata/withsubchart/templates/mainchart.yaml rename to internal/chart/v3/lint/rules/testdata/withsubchart/templates/mainchart.yaml diff --git a/pkg/lint/rules/testdata/withsubchart/values.yaml b/internal/chart/v3/lint/rules/testdata/withsubchart/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/withsubchart/values.yaml rename to internal/chart/v3/lint/rules/testdata/withsubchart/values.yaml diff --git a/internal/chart/v3/lint/rules/values.go b/internal/chart/v3/lint/rules/values.go new file mode 100644 index 000000000..adf2e2c52 --- /dev/null +++ b/internal/chart/v3/lint/rules/values.go @@ -0,0 +1,79 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "fmt" + "os" + "path/filepath" + + "helm.sh/helm/v4/internal/chart/v3/lint/support" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/common/util" +) + +// ValuesWithOverrides tests the values.yaml file. +// +// If a schema is present in the chart, values are tested against that. Otherwise, +// they are only tested for well-formedness. +// +// If additional values are supplied, they are coalesced into the values in values.yaml. +func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]interface{}) { + file := "values.yaml" + vf := filepath.Join(linter.ChartDir, file) + fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(vf)) + + if !fileExists { + return + } + + linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, valueOverrides)) +} + +func validateValuesFileExistence(valuesPath string) error { + _, err := os.Stat(valuesPath) + if err != nil { + return fmt.Errorf("file does not exist") + } + return nil +} + +func validateValuesFile(valuesPath string, overrides map[string]interface{}) error { + values, err := common.ReadValuesFile(valuesPath) + if err != nil { + return fmt.Errorf("unable to parse YAML: %w", err) + } + + // Helm 3.0.0 carried over the values linting from Helm 2.x, which only tests the top + // level values against the top-level expectations. Subchart values are not linted. + // We could change that. For now, though, we retain that strategy, and thus can + // coalesce tables (like reuse-values does) instead of doing the full chart + // CoalesceValues + coalescedValues := util.CoalesceTables(make(map[string]interface{}, len(overrides)), overrides) + coalescedValues = util.CoalesceTables(coalescedValues, values) + + ext := filepath.Ext(valuesPath) + schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json" + schema, err := os.ReadFile(schemaPath) + if len(schema) == 0 { + return nil + } + if err != nil { + return err + } + return util.ValidateAgainstSingleSchema(coalescedValues, schema) +} diff --git a/pkg/lint/rules/values_test.go b/internal/chart/v3/lint/rules/values_test.go similarity index 100% rename from pkg/lint/rules/values_test.go rename to internal/chart/v3/lint/rules/values_test.go diff --git a/internal/chart/v3/util/errors_test.go b/internal/chart/v3/lint/support/doc.go similarity index 67% rename from internal/chart/v3/util/errors_test.go rename to internal/chart/v3/lint/support/doc.go index b8ae86384..2d54a9b7d 100644 --- a/internal/chart/v3/util/errors_test.go +++ b/internal/chart/v3/lint/support/doc.go @@ -14,24 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util - -import ( - "testing" -) - -func TestErrorNoTableDoesNotPanic(t *testing.T) { - x := "empty" - - y := ErrNoTable{x} - - t.Logf("error is: %s", y) -} - -func TestErrorNoValueDoesNotPanic(t *testing.T) { - x := "empty" - - y := ErrNoValue{x} +/* +Package support contains tools for linting charts. - t.Logf("error is: %s", y) -} +Linting is the process of testing charts for errors or warnings regarding +formatting, compilation, or standards compliance. +*/ +package support // import "helm.sh/helm/v4/internal/chart/v3/lint/support" diff --git a/pkg/lint/support/message.go b/internal/chart/v3/lint/support/message.go similarity index 100% rename from pkg/lint/support/message.go rename to internal/chart/v3/lint/support/message.go diff --git a/pkg/lint/support/message_test.go b/internal/chart/v3/lint/support/message_test.go similarity index 100% rename from pkg/lint/support/message_test.go rename to internal/chart/v3/lint/support/message_test.go diff --git a/internal/chart/v3/loader/load.go b/internal/chart/v3/loader/load.go index 30bafdad4..2959fc71d 100644 --- a/internal/chart/v3/loader/load.go +++ b/internal/chart/v3/loader/load.go @@ -31,6 +31,7 @@ import ( "sigs.k8s.io/yaml" chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/pkg/chart/common" ) // ChartLoader loads a chart. @@ -79,7 +80,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { // do not rely on assumed ordering of files in the chart and crash // if Chart.yaml was not coming early enough to initialize metadata for _, f := range files { - c.Raw = append(c.Raw, &chart.File{Name: f.Name, Data: f.Data}) + c.Raw = append(c.Raw, &common.File{Name: f.Name, Data: f.Data}) if f.Name == "Chart.yaml" { if c.Metadata == nil { c.Metadata = new(chart.Metadata) @@ -115,10 +116,10 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { c.Schema = f.Data case strings.HasPrefix(f.Name, "templates/"): - c.Templates = append(c.Templates, &chart.File{Name: f.Name, Data: f.Data}) + c.Templates = append(c.Templates, &common.File{Name: f.Name, Data: f.Data}) case strings.HasPrefix(f.Name, "charts/"): if filepath.Ext(f.Name) == ".prov" { - c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) continue } @@ -126,7 +127,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { cname := strings.SplitN(fname, "/", 2)[0] subcharts[cname] = append(subcharts[cname], &BufferedFile{Name: fname, Data: f.Data}) default: - c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) } } diff --git a/internal/chart/v3/loader/load_test.go b/internal/chart/v3/loader/load_test.go index e770923ff..1d8ca836a 100644 --- a/internal/chart/v3/loader/load_test.go +++ b/internal/chart/v3/loader/load_test.go @@ -31,6 +31,7 @@ import ( "time" chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/pkg/chart/common" ) func TestLoadDir(t *testing.T) { @@ -491,7 +492,7 @@ foo: } } -func TestMergeValues(t *testing.T) { +func TestMergeValuesV3(t *testing.T) { nestedMap := map[string]interface{}{ "foo": "bar", "baz": map[string]string{ @@ -701,7 +702,7 @@ func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) { } } -func verifyBomStripped(t *testing.T, files []*chart.File) { +func verifyBomStripped(t *testing.T, files []*common.File) { t.Helper() for _, file := range files { if bytes.HasPrefix(file.Data, utf8bom) { diff --git a/internal/chart/v3/util/capabilities.go b/internal/chart/v3/util/capabilities.go deleted file mode 100644 index 23b6d46fa..000000000 --- a/internal/chart/v3/util/capabilities.go +++ /dev/null @@ -1,122 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "fmt" - "slices" - "strconv" - - "github.com/Masterminds/semver/v3" - "k8s.io/client-go/kubernetes/scheme" - - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - - helmversion "helm.sh/helm/v4/internal/version" -) - -var ( - // The Kubernetes version can be set by LDFLAGS. In order to do that the value - // must be a string. - k8sVersionMajor = "1" - k8sVersionMinor = "20" - - // DefaultVersionSet is the default version set, which includes only Core V1 ("v1"). - DefaultVersionSet = allKnownVersions() - - // DefaultCapabilities is the default set of capabilities. - DefaultCapabilities = &Capabilities{ - KubeVersion: KubeVersion{ - Version: fmt.Sprintf("v%s.%s.0", k8sVersionMajor, k8sVersionMinor), - Major: k8sVersionMajor, - Minor: k8sVersionMinor, - }, - APIVersions: DefaultVersionSet, - HelmVersion: helmversion.Get(), - } -) - -// Capabilities describes the capabilities of the Kubernetes cluster. -type Capabilities struct { - // KubeVersion is the Kubernetes version. - KubeVersion KubeVersion - // APIVersions are supported Kubernetes API versions. - APIVersions VersionSet - // HelmVersion is the build information for this helm version - HelmVersion helmversion.BuildInfo -} - -func (capabilities *Capabilities) Copy() *Capabilities { - return &Capabilities{ - KubeVersion: capabilities.KubeVersion, - APIVersions: capabilities.APIVersions, - HelmVersion: capabilities.HelmVersion, - } -} - -// KubeVersion is the Kubernetes version. -type KubeVersion struct { - Version string // Kubernetes version - Major string // Kubernetes major version - Minor string // Kubernetes minor version -} - -// String implements fmt.Stringer -func (kv *KubeVersion) String() string { return kv.Version } - -// GitVersion returns the Kubernetes version string. -// -// Deprecated: use KubeVersion.Version. -func (kv *KubeVersion) GitVersion() string { return kv.Version } - -// ParseKubeVersion parses kubernetes version from string -func ParseKubeVersion(version string) (*KubeVersion, error) { - sv, err := semver.NewVersion(version) - if err != nil { - return nil, err - } - return &KubeVersion{ - Version: "v" + sv.String(), - Major: strconv.FormatUint(sv.Major(), 10), - Minor: strconv.FormatUint(sv.Minor(), 10), - }, nil -} - -// VersionSet is a set of Kubernetes API versions. -type VersionSet []string - -// Has returns true if the version string is in the set. -// -// vs.Has("apps/v1") -func (v VersionSet) Has(apiVersion string) bool { - return slices.Contains(v, apiVersion) -} - -func allKnownVersions() VersionSet { - // We should register the built in extension APIs as well so CRDs are - // supported in the default version set. This has caused problems with `helm - // template` in the past, so let's be safe - apiextensionsv1beta1.AddToScheme(scheme.Scheme) - apiextensionsv1.AddToScheme(scheme.Scheme) - - groups := scheme.Scheme.PrioritizedVersionsAllGroups() - vs := make(VersionSet, 0, len(groups)) - for _, gv := range groups { - vs = append(vs, gv.String()) - } - return vs -} diff --git a/internal/chart/v3/util/capabilities_test.go b/internal/chart/v3/util/capabilities_test.go deleted file mode 100644 index aa9be9db8..000000000 --- a/internal/chart/v3/util/capabilities_test.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "testing" -) - -func TestVersionSet(t *testing.T) { - vs := VersionSet{"v1", "apps/v1"} - if d := len(vs); d != 2 { - t.Errorf("Expected 2 versions, got %d", d) - } - - if !vs.Has("apps/v1") { - t.Error("Expected to find apps/v1") - } - - if vs.Has("Spanish/inquisition") { - t.Error("No one expects the Spanish/inquisition") - } -} - -func TestDefaultVersionSet(t *testing.T) { - if !DefaultVersionSet.Has("v1") { - t.Error("Expected core v1 version set") - } -} - -func TestDefaultCapabilities(t *testing.T) { - kv := DefaultCapabilities.KubeVersion - if kv.String() != "v1.20.0" { - t.Errorf("Expected default KubeVersion.String() to be v1.20.0, got %q", kv.String()) - } - if kv.Version != "v1.20.0" { - t.Errorf("Expected default KubeVersion.Version to be v1.20.0, got %q", kv.Version) - } - if kv.GitVersion() != "v1.20.0" { - t.Errorf("Expected default KubeVersion.GitVersion() to be v1.20.0, got %q", kv.Version) - } - if kv.Major != "1" { - t.Errorf("Expected default KubeVersion.Major to be 1, got %q", kv.Major) - } - if kv.Minor != "20" { - t.Errorf("Expected default KubeVersion.Minor to be 20, got %q", kv.Minor) - } -} - -func TestDefaultCapabilitiesHelmVersion(t *testing.T) { - hv := DefaultCapabilities.HelmVersion - - if hv.Version != "v4.0" { - t.Errorf("Expected default HelmVersion to be v4.0, got %q", hv.Version) - } -} - -func TestParseKubeVersion(t *testing.T) { - kv, err := ParseKubeVersion("v1.16.0") - if err != nil { - t.Errorf("Expected v1.16.0 to parse successfully") - } - if kv.Version != "v1.16.0" { - t.Errorf("Expected parsed KubeVersion.Version to be v1.16.0, got %q", kv.String()) - } - if kv.Major != "1" { - t.Errorf("Expected parsed KubeVersion.Major to be 1, got %q", kv.Major) - } - if kv.Minor != "16" { - t.Errorf("Expected parsed KubeVersion.Minor to be 16, got %q", kv.Minor) - } -} diff --git a/internal/chart/v3/util/coalesce.go b/internal/chart/v3/util/coalesce.go deleted file mode 100644 index caea2e119..000000000 --- a/internal/chart/v3/util/coalesce.go +++ /dev/null @@ -1,308 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "fmt" - "log" - "maps" - - "github.com/mitchellh/copystructure" - - chart "helm.sh/helm/v4/internal/chart/v3" -) - -func concatPrefix(a, b string) string { - if a == "" { - return b - } - return fmt.Sprintf("%s.%s", a, b) -} - -// CoalesceValues coalesces all of the values in a chart (and its subcharts). -// -// Values are coalesced together using the following rules: -// -// - Values in a higher level chart always override values in a lower-level -// dependency chart -// - Scalar values and arrays are replaced, maps are merged -// - A chart has access to all of the variables for it, as well as all of -// the values destined for its dependencies. -func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) { - valsCopy, err := copyValues(vals) - if err != nil { - return vals, err - } - return coalesce(log.Printf, chrt, valsCopy, "", false) -} - -// MergeValues is used to merge the values in a chart and its subcharts. This -// is different from Coalescing as nil/null values are preserved. -// -// Values are coalesced together using the following rules: -// -// - Values in a higher level chart always override values in a lower-level -// dependency chart -// - Scalar values and arrays are replaced, maps are merged -// - A chart has access to all of the variables for it, as well as all of -// the values destined for its dependencies. -// -// Retaining Nils is useful when processes early in a Helm action or business -// logic need to retain them for when Coalescing will happen again later in the -// business logic. -func MergeValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) { - valsCopy, err := copyValues(vals) - if err != nil { - return vals, err - } - return coalesce(log.Printf, chrt, valsCopy, "", true) -} - -func copyValues(vals map[string]interface{}) (Values, error) { - v, err := copystructure.Copy(vals) - if err != nil { - return vals, err - } - - valsCopy := v.(map[string]interface{}) - // if we have an empty map, make sure it is initialized - if valsCopy == nil { - valsCopy = make(map[string]interface{}) - } - - return valsCopy, nil -} - -type printFn func(format string, v ...interface{}) - -// coalesce coalesces the dest values and the chart values, giving priority to the dest values. -// -// This is a helper function for CoalesceValues and MergeValues. -// -// Note, the merge argument specifies whether this is being used by MergeValues -// or CoalesceValues. Coalescing removes null values and their keys in some -// situations while merging keeps the null values. -func coalesce(printf printFn, ch *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) { - coalesceValues(printf, ch, dest, prefix, merge) - return coalesceDeps(printf, ch, dest, prefix, merge) -} - -// coalesceDeps coalesces the dependencies of the given chart. -func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) { - for _, subchart := range chrt.Dependencies() { - if c, ok := dest[subchart.Name()]; !ok { - // If dest doesn't already have the key, create it. - dest[subchart.Name()] = make(map[string]interface{}) - } else if !istable(c) { - return dest, fmt.Errorf("type mismatch on %s: %t", subchart.Name(), c) - } - if dv, ok := dest[subchart.Name()]; ok { - dvmap := dv.(map[string]interface{}) - subPrefix := concatPrefix(prefix, chrt.Metadata.Name) - // Get globals out of dest and merge them into dvmap. - coalesceGlobals(printf, dvmap, dest, subPrefix, merge) - // Now coalesce the rest of the values. - var err error - dest[subchart.Name()], err = coalesce(printf, subchart, dvmap, subPrefix, merge) - if err != nil { - return dest, err - } - } - } - return dest, nil -} - -// coalesceGlobals copies the globals out of src and merges them into dest. -// -// For convenience, returns dest. -func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix string, _ bool) { - var dg, sg map[string]interface{} - - if destglob, ok := dest[GlobalKey]; !ok { - dg = make(map[string]interface{}) - } else if dg, ok = destglob.(map[string]interface{}); !ok { - printf("warning: skipping globals because destination %s is not a table.", GlobalKey) - return - } - - if srcglob, ok := src[GlobalKey]; !ok { - sg = make(map[string]interface{}) - } else if sg, ok = srcglob.(map[string]interface{}); !ok { - printf("warning: skipping globals because source %s is not a table.", GlobalKey) - return - } - - // EXPERIMENTAL: In the past, we have disallowed globals to test tables. This - // reverses that decision. It may somehow be possible to introduce a loop - // here, but I haven't found a way. So for the time being, let's allow - // tables in globals. - for key, val := range sg { - if istable(val) { - vv := copyMap(val.(map[string]interface{})) - if destv, ok := dg[key]; !ok { - // Here there is no merge. We're just adding. - dg[key] = vv - } else { - if destvmap, ok := destv.(map[string]interface{}); !ok { - printf("Conflict: cannot merge map onto non-map for %q. Skipping.", key) - } else { - // Basically, we reverse order of coalesce here to merge - // top-down. - subPrefix := concatPrefix(prefix, key) - // In this location coalesceTablesFullKey should always have - // merge set to true. The output of coalesceGlobals is run - // through coalesce where any nils will be removed. - coalesceTablesFullKey(printf, vv, destvmap, subPrefix, true) - dg[key] = vv - } - } - } else if dv, ok := dg[key]; ok && istable(dv) { - // It's not clear if this condition can actually ever trigger. - printf("key %s is table. Skipping", key) - } else { - // TODO: Do we need to do any additional checking on the value? - dg[key] = val - } - } - dest[GlobalKey] = dg -} - -func copyMap(src map[string]interface{}) map[string]interface{} { - m := make(map[string]interface{}, len(src)) - maps.Copy(m, src) - return m -} - -// coalesceValues builds up a values map for a particular chart. -// -// Values in v will override the values in the chart. -func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, prefix string, merge bool) { - subPrefix := concatPrefix(prefix, c.Metadata.Name) - - // Using c.Values directly when coalescing a table can cause problems where - // the original c.Values is altered. Creating a deep copy stops the problem. - // This section is fault-tolerant as there is no ability to return an error. - valuesCopy, err := copystructure.Copy(c.Values) - var vc map[string]interface{} - var ok bool - if err != nil { - // If there is an error something is wrong with copying c.Values it - // means there is a problem in the deep copying package or something - // wrong with c.Values. In this case we will use c.Values and report - // an error. - printf("warning: unable to copy values, err: %s", err) - vc = c.Values - } else { - vc, ok = valuesCopy.(map[string]interface{}) - if !ok { - // c.Values has a map[string]interface{} structure. If the copy of - // it cannot be treated as map[string]interface{} there is something - // strangely wrong. Log it and use c.Values - printf("warning: unable to convert values copy to values type") - vc = c.Values - } - } - - for key, val := range vc { - if value, ok := v[key]; ok { - if value == nil && !merge { - // When the YAML value is null and we are coalescing instead of - // merging, we remove the value's key. - // This allows Helm's various sources of values (value files or --set) to - // remove incompatible keys from any previous chart, file, or set values. - delete(v, key) - } else if dest, ok := value.(map[string]interface{}); ok { - // if v[key] is a table, merge nv's val table into v[key]. - src, ok := val.(map[string]interface{}) - if !ok { - // If the original value is nil, there is nothing to coalesce, so we don't print - // the warning - if val != nil { - printf("warning: skipped value for %s.%s: Not a table.", subPrefix, key) - } - } else { - // If the key is a child chart, coalesce tables with Merge set to true - merge := childChartMergeTrue(c, key, merge) - - // Because v has higher precedence than nv, dest values override src - // values. - coalesceTablesFullKey(printf, dest, src, concatPrefix(subPrefix, key), merge) - } - } - } else { - // If the key is not in v, copy it from nv. - v[key] = val - } - } -} - -func childChartMergeTrue(chrt *chart.Chart, key string, merge bool) bool { - for _, subchart := range chrt.Dependencies() { - if subchart.Name() == key { - return true - } - } - return merge -} - -// CoalesceTables merges a source map into a destination map. -// -// dest is considered authoritative. -func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} { - return coalesceTablesFullKey(log.Printf, dst, src, "", false) -} - -func MergeTables(dst, src map[string]interface{}) map[string]interface{} { - return coalesceTablesFullKey(log.Printf, dst, src, "", true) -} - -// coalesceTablesFullKey merges a source map into a destination map. -// -// dest is considered authoritative. -func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, prefix string, merge bool) map[string]interface{} { - // When --reuse-values is set but there are no modifications yet, return new values - if src == nil { - return dst - } - if dst == nil { - return src - } - for key, val := range dst { - if val == nil { - src[key] = nil - } - } - // Because dest has higher precedence than src, dest values override src - // values. - for key, val := range src { - fullkey := concatPrefix(prefix, key) - if dv, ok := dst[key]; ok && !merge && dv == nil { - delete(dst, key) - } else if !ok { - dst[key] = val - } else if istable(val) { - if istable(dv) { - coalesceTablesFullKey(printf, dv.(map[string]interface{}), val.(map[string]interface{}), fullkey, merge) - } else { - printf("warning: cannot overwrite table with non table for %s (%v)", fullkey, val) - } - } else if istable(dv) && val != nil { - printf("warning: destination for %s is a table. Ignoring non-table value (%v)", fullkey, val) - } - } - return dst -} diff --git a/internal/chart/v3/util/coalesce_test.go b/internal/chart/v3/util/coalesce_test.go deleted file mode 100644 index 4770b601d..000000000 --- a/internal/chart/v3/util/coalesce_test.go +++ /dev/null @@ -1,723 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "encoding/json" - "fmt" - "maps" - "testing" - - "github.com/stretchr/testify/assert" - - chart "helm.sh/helm/v4/internal/chart/v3" -) - -// ref: http://www.yaml.org/spec/1.2/spec.html#id2803362 -var testCoalesceValuesYaml = []byte(` -top: yup -bottom: null -right: Null -left: NULL -front: ~ -back: "" -nested: - boat: null - -global: - name: Ishmael - subject: Queequeg - nested: - boat: true - -pequod: - boat: null - global: - name: Stinky - harpooner: Tashtego - nested: - boat: false - sail: true - foo2: null - ahab: - scope: whale - boat: null - nested: - foo: true - boat: null - object: null -`) - -func withDeps(c *chart.Chart, deps ...*chart.Chart) *chart.Chart { - c.AddDependency(deps...) - return c -} - -func TestCoalesceValues(t *testing.T) { - is := assert.New(t) - - c := withDeps(&chart.Chart{ - Metadata: &chart.Metadata{Name: "moby"}, - Values: map[string]interface{}{ - "back": "exists", - "bottom": "exists", - "front": "exists", - "left": "exists", - "name": "moby", - "nested": map[string]interface{}{"boat": true}, - "override": "bad", - "right": "exists", - "scope": "moby", - "top": "nope", - "global": map[string]interface{}{ - "nested2": map[string]interface{}{"l0": "moby"}, - }, - "pequod": map[string]interface{}{ - "boat": "maybe", - "ahab": map[string]interface{}{ - "boat": "maybe", - "nested": map[string]interface{}{"boat": "maybe"}, - }, - }, - }, - }, - withDeps(&chart.Chart{ - Metadata: &chart.Metadata{Name: "pequod"}, - Values: map[string]interface{}{ - "name": "pequod", - "scope": "pequod", - "global": map[string]interface{}{ - "nested2": map[string]interface{}{"l1": "pequod"}, - }, - "boat": false, - "ahab": map[string]interface{}{ - "boat": false, - "nested": map[string]interface{}{"boat": false}, - }, - }, - }, - &chart.Chart{ - Metadata: &chart.Metadata{Name: "ahab"}, - Values: map[string]interface{}{ - "global": map[string]interface{}{ - "nested": map[string]interface{}{"foo": "bar", "foo2": "bar2"}, - "nested2": map[string]interface{}{"l2": "ahab"}, - }, - "scope": "ahab", - "name": "ahab", - "boat": true, - "nested": map[string]interface{}{"foo": false, "boat": true}, - "object": map[string]interface{}{"foo": "bar"}, - }, - }, - ), - &chart.Chart{ - Metadata: &chart.Metadata{Name: "spouter"}, - Values: map[string]interface{}{ - "scope": "spouter", - "global": map[string]interface{}{ - "nested2": map[string]interface{}{"l1": "spouter"}, - }, - }, - }, - ) - - vals, err := ReadValues(testCoalesceValuesYaml) - if err != nil { - t.Fatal(err) - } - - // taking a copy of the values before passing it - // to CoalesceValues as argument, so that we can - // use it for asserting later - valsCopy := make(Values, len(vals)) - maps.Copy(valsCopy, vals) - - v, err := CoalesceValues(c, vals) - if err != nil { - t.Fatal(err) - } - j, _ := json.MarshalIndent(v, "", " ") - t.Logf("Coalesced Values: %s", string(j)) - - tests := []struct { - tpl string - expect string - }{ - {"{{.top}}", "yup"}, - {"{{.back}}", ""}, - {"{{.name}}", "moby"}, - {"{{.global.name}}", "Ishmael"}, - {"{{.global.subject}}", "Queequeg"}, - {"{{.global.harpooner}}", ""}, - {"{{.pequod.name}}", "pequod"}, - {"{{.pequod.ahab.name}}", "ahab"}, - {"{{.pequod.ahab.scope}}", "whale"}, - {"{{.pequod.ahab.nested.foo}}", "true"}, - {"{{.pequod.ahab.global.name}}", "Ishmael"}, - {"{{.pequod.ahab.global.nested.foo}}", "bar"}, - {"{{.pequod.ahab.global.nested.foo2}}", ""}, - {"{{.pequod.ahab.global.subject}}", "Queequeg"}, - {"{{.pequod.ahab.global.harpooner}}", "Tashtego"}, - {"{{.pequod.global.name}}", "Ishmael"}, - {"{{.pequod.global.nested.foo}}", ""}, - {"{{.pequod.global.subject}}", "Queequeg"}, - {"{{.spouter.global.name}}", "Ishmael"}, - {"{{.spouter.global.harpooner}}", ""}, - - {"{{.global.nested.boat}}", "true"}, - {"{{.pequod.global.nested.boat}}", "true"}, - {"{{.spouter.global.nested.boat}}", "true"}, - {"{{.pequod.global.nested.sail}}", "true"}, - {"{{.spouter.global.nested.sail}}", ""}, - - {"{{.global.nested2.l0}}", "moby"}, - {"{{.global.nested2.l1}}", ""}, - {"{{.global.nested2.l2}}", ""}, - {"{{.pequod.global.nested2.l0}}", "moby"}, - {"{{.pequod.global.nested2.l1}}", "pequod"}, - {"{{.pequod.global.nested2.l2}}", ""}, - {"{{.pequod.ahab.global.nested2.l0}}", "moby"}, - {"{{.pequod.ahab.global.nested2.l1}}", "pequod"}, - {"{{.pequod.ahab.global.nested2.l2}}", "ahab"}, - {"{{.spouter.global.nested2.l0}}", "moby"}, - {"{{.spouter.global.nested2.l1}}", "spouter"}, - {"{{.spouter.global.nested2.l2}}", ""}, - } - - for _, tt := range tests { - if o, err := ttpl(tt.tpl, v); err != nil || o != tt.expect { - t.Errorf("Expected %q to expand to %q, got %q", tt.tpl, tt.expect, o) - } - } - - nullKeys := []string{"bottom", "right", "left", "front"} - for _, nullKey := range nullKeys { - if _, ok := v[nullKey]; ok { - t.Errorf("Expected key %q to be removed, still present", nullKey) - } - } - - if _, ok := v["nested"].(map[string]interface{})["boat"]; ok { - t.Error("Expected nested boat key to be removed, still present") - } - - subchart := v["pequod"].(map[string]interface{}) - if _, ok := subchart["boat"]; ok { - t.Error("Expected subchart boat key to be removed, still present") - } - - subsubchart := subchart["ahab"].(map[string]interface{}) - if _, ok := subsubchart["boat"]; ok { - t.Error("Expected sub-subchart ahab boat key to be removed, still present") - } - - if _, ok := subsubchart["nested"].(map[string]interface{})["boat"]; ok { - t.Error("Expected sub-subchart nested boat key to be removed, still present") - } - - if _, ok := subsubchart["object"]; ok { - t.Error("Expected sub-subchart object map to be removed, still present") - } - - // CoalesceValues should not mutate the passed arguments - is.Equal(valsCopy, vals) -} - -func TestMergeValues(t *testing.T) { - is := assert.New(t) - - c := withDeps(&chart.Chart{ - Metadata: &chart.Metadata{Name: "moby"}, - Values: map[string]interface{}{ - "back": "exists", - "bottom": "exists", - "front": "exists", - "left": "exists", - "name": "moby", - "nested": map[string]interface{}{"boat": true}, - "override": "bad", - "right": "exists", - "scope": "moby", - "top": "nope", - "global": map[string]interface{}{ - "nested2": map[string]interface{}{"l0": "moby"}, - }, - }, - }, - withDeps(&chart.Chart{ - Metadata: &chart.Metadata{Name: "pequod"}, - Values: map[string]interface{}{ - "name": "pequod", - "scope": "pequod", - "global": map[string]interface{}{ - "nested2": map[string]interface{}{"l1": "pequod"}, - }, - }, - }, - &chart.Chart{ - Metadata: &chart.Metadata{Name: "ahab"}, - Values: map[string]interface{}{ - "global": map[string]interface{}{ - "nested": map[string]interface{}{"foo": "bar"}, - "nested2": map[string]interface{}{"l2": "ahab"}, - }, - "scope": "ahab", - "name": "ahab", - "boat": true, - "nested": map[string]interface{}{"foo": false, "bar": true}, - }, - }, - ), - &chart.Chart{ - Metadata: &chart.Metadata{Name: "spouter"}, - Values: map[string]interface{}{ - "scope": "spouter", - "global": map[string]interface{}{ - "nested2": map[string]interface{}{"l1": "spouter"}, - }, - }, - }, - ) - - vals, err := ReadValues(testCoalesceValuesYaml) - if err != nil { - t.Fatal(err) - } - - // taking a copy of the values before passing it - // to MergeValues as argument, so that we can - // use it for asserting later - valsCopy := make(Values, len(vals)) - maps.Copy(valsCopy, vals) - - v, err := MergeValues(c, vals) - if err != nil { - t.Fatal(err) - } - j, _ := json.MarshalIndent(v, "", " ") - t.Logf("Coalesced Values: %s", string(j)) - - tests := []struct { - tpl string - expect string - }{ - {"{{.top}}", "yup"}, - {"{{.back}}", ""}, - {"{{.name}}", "moby"}, - {"{{.global.name}}", "Ishmael"}, - {"{{.global.subject}}", "Queequeg"}, - {"{{.global.harpooner}}", ""}, - {"{{.pequod.name}}", "pequod"}, - {"{{.pequod.ahab.name}}", "ahab"}, - {"{{.pequod.ahab.scope}}", "whale"}, - {"{{.pequod.ahab.nested.foo}}", "true"}, - {"{{.pequod.ahab.global.name}}", "Ishmael"}, - {"{{.pequod.ahab.global.nested.foo}}", "bar"}, - {"{{.pequod.ahab.global.subject}}", "Queequeg"}, - {"{{.pequod.ahab.global.harpooner}}", "Tashtego"}, - {"{{.pequod.global.name}}", "Ishmael"}, - {"{{.pequod.global.nested.foo}}", ""}, - {"{{.pequod.global.subject}}", "Queequeg"}, - {"{{.spouter.global.name}}", "Ishmael"}, - {"{{.spouter.global.harpooner}}", ""}, - - {"{{.global.nested.boat}}", "true"}, - {"{{.pequod.global.nested.boat}}", "true"}, - {"{{.spouter.global.nested.boat}}", "true"}, - {"{{.pequod.global.nested.sail}}", "true"}, - {"{{.spouter.global.nested.sail}}", ""}, - - {"{{.global.nested2.l0}}", "moby"}, - {"{{.global.nested2.l1}}", ""}, - {"{{.global.nested2.l2}}", ""}, - {"{{.pequod.global.nested2.l0}}", "moby"}, - {"{{.pequod.global.nested2.l1}}", "pequod"}, - {"{{.pequod.global.nested2.l2}}", ""}, - {"{{.pequod.ahab.global.nested2.l0}}", "moby"}, - {"{{.pequod.ahab.global.nested2.l1}}", "pequod"}, - {"{{.pequod.ahab.global.nested2.l2}}", "ahab"}, - {"{{.spouter.global.nested2.l0}}", "moby"}, - {"{{.spouter.global.nested2.l1}}", "spouter"}, - {"{{.spouter.global.nested2.l2}}", ""}, - } - - for _, tt := range tests { - if o, err := ttpl(tt.tpl, v); err != nil || o != tt.expect { - t.Errorf("Expected %q to expand to %q, got %q", tt.tpl, tt.expect, o) - } - } - - // nullKeys is different from coalescing. Here the null/nil values are not - // removed. - nullKeys := []string{"bottom", "right", "left", "front"} - for _, nullKey := range nullKeys { - if vv, ok := v[nullKey]; !ok { - t.Errorf("Expected key %q to be present but it was removed", nullKey) - } else if vv != nil { - t.Errorf("Expected key %q to be null but it has a value of %v", nullKey, vv) - } - } - - if _, ok := v["nested"].(map[string]interface{})["boat"]; !ok { - t.Error("Expected nested boat key to be present but it was removed") - } - - subchart := v["pequod"].(map[string]interface{})["ahab"].(map[string]interface{}) - if _, ok := subchart["boat"]; !ok { - t.Error("Expected subchart boat key to be present but it was removed") - } - - if _, ok := subchart["nested"].(map[string]interface{})["bar"]; !ok { - t.Error("Expected subchart nested bar key to be present but it was removed") - } - - // CoalesceValues should not mutate the passed arguments - is.Equal(valsCopy, vals) -} - -func TestCoalesceTables(t *testing.T) { - dst := map[string]interface{}{ - "name": "Ishmael", - "address": map[string]interface{}{ - "street": "123 Spouter Inn Ct.", - "city": "Nantucket", - "country": nil, - }, - "details": map[string]interface{}{ - "friends": []string{"Tashtego"}, - }, - "boat": "pequod", - "hole": nil, - } - src := map[string]interface{}{ - "occupation": "whaler", - "address": map[string]interface{}{ - "state": "MA", - "street": "234 Spouter Inn Ct.", - "country": "US", - }, - "details": "empty", - "boat": map[string]interface{}{ - "mast": true, - }, - "hole": "black", - } - - // What we expect is that anything in dst overrides anything in src, but that - // otherwise the values are coalesced. - CoalesceTables(dst, src) - - if dst["name"] != "Ishmael" { - t.Errorf("Unexpected name: %s", dst["name"]) - } - if dst["occupation"] != "whaler" { - t.Errorf("Unexpected occupation: %s", dst["occupation"]) - } - - addr, ok := dst["address"].(map[string]interface{}) - if !ok { - t.Fatal("Address went away.") - } - - if addr["street"].(string) != "123 Spouter Inn Ct." { - t.Errorf("Unexpected address: %v", addr["street"]) - } - - if addr["city"].(string) != "Nantucket" { - t.Errorf("Unexpected city: %v", addr["city"]) - } - - if addr["state"].(string) != "MA" { - t.Errorf("Unexpected state: %v", addr["state"]) - } - - if _, ok = addr["country"]; ok { - t.Error("The country is not left out.") - } - - if det, ok := dst["details"].(map[string]interface{}); !ok { - t.Fatalf("Details is the wrong type: %v", dst["details"]) - } else if _, ok := det["friends"]; !ok { - t.Error("Could not find your friends. Maybe you don't have any. :-(") - } - - if dst["boat"].(string) != "pequod" { - t.Errorf("Expected boat string, got %v", dst["boat"]) - } - - if _, ok = dst["hole"]; ok { - t.Error("The hole still exists.") - } - - dst2 := map[string]interface{}{ - "name": "Ishmael", - "address": map[string]interface{}{ - "street": "123 Spouter Inn Ct.", - "city": "Nantucket", - "country": "US", - }, - "details": map[string]interface{}{ - "friends": []string{"Tashtego"}, - }, - "boat": "pequod", - "hole": "black", - } - - // What we expect is that anything in dst should have all values set, - // this happens when the --reuse-values flag is set but the chart has no modifications yet - CoalesceTables(dst2, nil) - - if dst2["name"] != "Ishmael" { - t.Errorf("Unexpected name: %s", dst2["name"]) - } - - addr2, ok := dst2["address"].(map[string]interface{}) - if !ok { - t.Fatal("Address went away.") - } - - if addr2["street"].(string) != "123 Spouter Inn Ct." { - t.Errorf("Unexpected address: %v", addr2["street"]) - } - - if addr2["city"].(string) != "Nantucket" { - t.Errorf("Unexpected city: %v", addr2["city"]) - } - - if addr2["country"].(string) != "US" { - t.Errorf("Unexpected Country: %v", addr2["country"]) - } - - if det2, ok := dst2["details"].(map[string]interface{}); !ok { - t.Fatalf("Details is the wrong type: %v", dst2["details"]) - } else if _, ok := det2["friends"]; !ok { - t.Error("Could not find your friends. Maybe you don't have any. :-(") - } - - if dst2["boat"].(string) != "pequod" { - t.Errorf("Expected boat string, got %v", dst2["boat"]) - } - - if dst2["hole"].(string) != "black" { - t.Errorf("Expected hole string, got %v", dst2["boat"]) - } -} - -func TestMergeTables(t *testing.T) { - dst := map[string]interface{}{ - "name": "Ishmael", - "address": map[string]interface{}{ - "street": "123 Spouter Inn Ct.", - "city": "Nantucket", - "country": nil, - }, - "details": map[string]interface{}{ - "friends": []string{"Tashtego"}, - }, - "boat": "pequod", - "hole": nil, - } - src := map[string]interface{}{ - "occupation": "whaler", - "address": map[string]interface{}{ - "state": "MA", - "street": "234 Spouter Inn Ct.", - "country": "US", - }, - "details": "empty", - "boat": map[string]interface{}{ - "mast": true, - }, - "hole": "black", - } - - // What we expect is that anything in dst overrides anything in src, but that - // otherwise the values are coalesced. - MergeTables(dst, src) - - if dst["name"] != "Ishmael" { - t.Errorf("Unexpected name: %s", dst["name"]) - } - if dst["occupation"] != "whaler" { - t.Errorf("Unexpected occupation: %s", dst["occupation"]) - } - - addr, ok := dst["address"].(map[string]interface{}) - if !ok { - t.Fatal("Address went away.") - } - - if addr["street"].(string) != "123 Spouter Inn Ct." { - t.Errorf("Unexpected address: %v", addr["street"]) - } - - if addr["city"].(string) != "Nantucket" { - t.Errorf("Unexpected city: %v", addr["city"]) - } - - if addr["state"].(string) != "MA" { - t.Errorf("Unexpected state: %v", addr["state"]) - } - - // This is one test that is different from CoalesceTables. Because country - // is a nil value and it's not removed it's still present. - if _, ok = addr["country"]; !ok { - t.Error("The country is left out.") - } - - if det, ok := dst["details"].(map[string]interface{}); !ok { - t.Fatalf("Details is the wrong type: %v", dst["details"]) - } else if _, ok := det["friends"]; !ok { - t.Error("Could not find your friends. Maybe you don't have any. :-(") - } - - if dst["boat"].(string) != "pequod" { - t.Errorf("Expected boat string, got %v", dst["boat"]) - } - - // This is one test that is different from CoalesceTables. Because hole - // is a nil value and it's not removed it's still present. - if _, ok = dst["hole"]; !ok { - t.Error("The hole no longer exists.") - } - - dst2 := map[string]interface{}{ - "name": "Ishmael", - "address": map[string]interface{}{ - "street": "123 Spouter Inn Ct.", - "city": "Nantucket", - "country": "US", - }, - "details": map[string]interface{}{ - "friends": []string{"Tashtego"}, - }, - "boat": "pequod", - "hole": "black", - "nilval": nil, - } - - // What we expect is that anything in dst should have all values set, - // this happens when the --reuse-values flag is set but the chart has no modifications yet - MergeTables(dst2, nil) - - if dst2["name"] != "Ishmael" { - t.Errorf("Unexpected name: %s", dst2["name"]) - } - - addr2, ok := dst2["address"].(map[string]interface{}) - if !ok { - t.Fatal("Address went away.") - } - - if addr2["street"].(string) != "123 Spouter Inn Ct." { - t.Errorf("Unexpected address: %v", addr2["street"]) - } - - if addr2["city"].(string) != "Nantucket" { - t.Errorf("Unexpected city: %v", addr2["city"]) - } - - if addr2["country"].(string) != "US" { - t.Errorf("Unexpected Country: %v", addr2["country"]) - } - - if det2, ok := dst2["details"].(map[string]interface{}); !ok { - t.Fatalf("Details is the wrong type: %v", dst2["details"]) - } else if _, ok := det2["friends"]; !ok { - t.Error("Could not find your friends. Maybe you don't have any. :-(") - } - - if dst2["boat"].(string) != "pequod" { - t.Errorf("Expected boat string, got %v", dst2["boat"]) - } - - if dst2["hole"].(string) != "black" { - t.Errorf("Expected hole string, got %v", dst2["boat"]) - } - - if dst2["nilval"] != nil { - t.Error("Expected nilvalue to have nil value but it does not") - } -} - -func TestCoalesceValuesWarnings(t *testing.T) { - - c := withDeps(&chart.Chart{ - Metadata: &chart.Metadata{Name: "level1"}, - Values: map[string]interface{}{ - "name": "moby", - }, - }, - withDeps(&chart.Chart{ - Metadata: &chart.Metadata{Name: "level2"}, - Values: map[string]interface{}{ - "name": "pequod", - }, - }, - &chart.Chart{ - Metadata: &chart.Metadata{Name: "level3"}, - Values: map[string]interface{}{ - "name": "ahab", - "boat": true, - "spear": map[string]interface{}{ - "tip": true, - "sail": map[string]interface{}{ - "cotton": true, - }, - }, - }, - }, - ), - ) - - vals := map[string]interface{}{ - "level2": map[string]interface{}{ - "level3": map[string]interface{}{ - "boat": map[string]interface{}{"mast": true}, - "spear": map[string]interface{}{ - "tip": map[string]interface{}{ - "sharp": true, - }, - "sail": true, - }, - }, - }, - } - - warnings := make([]string, 0) - printf := func(format string, v ...interface{}) { - t.Logf(format, v...) - warnings = append(warnings, fmt.Sprintf(format, v...)) - } - - _, err := coalesce(printf, c, vals, "", false) - if err != nil { - t.Fatal(err) - } - - t.Logf("vals: %v", vals) - assert.Contains(t, warnings, "warning: skipped value for level1.level2.level3.boat: Not a table.") - assert.Contains(t, warnings, "warning: destination for level1.level2.level3.spear.tip is a table. Ignoring non-table value (true)") - assert.Contains(t, warnings, "warning: cannot overwrite table with non table for level1.level2.level3.spear.sail (map[cotton:true])") - -} - -func TestConcatPrefix(t *testing.T) { - assert.Equal(t, "b", concatPrefix("", "b")) - assert.Equal(t, "a.b", concatPrefix("a", "b")) -} diff --git a/internal/chart/v3/util/create.go b/internal/chart/v3/util/create.go index 6a28f99d4..9f742e646 100644 --- a/internal/chart/v3/util/create.go +++ b/internal/chart/v3/util/create.go @@ -28,6 +28,7 @@ import ( chart "helm.sh/helm/v4/internal/chart/v3" "helm.sh/helm/v4/internal/chart/v3/loader" + "helm.sh/helm/v4/pkg/chart/common" ) // chartName is a regular expression for testing the supplied name of a chart. @@ -655,11 +656,11 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error { schart.Metadata = chartfile - var updatedTemplates []*chart.File + var updatedTemplates []*common.File for _, template := range schart.Templates { newData := transform(string(template.Data), schart.Name()) - updatedTemplates = append(updatedTemplates, &chart.File{Name: template.Name, Data: newData}) + updatedTemplates = append(updatedTemplates, &common.File{Name: template.Name, Data: newData}) } schart.Templates = updatedTemplates diff --git a/internal/chart/v3/util/dependencies.go b/internal/chart/v3/util/dependencies.go index 129c46372..489772115 100644 --- a/internal/chart/v3/util/dependencies.go +++ b/internal/chart/v3/util/dependencies.go @@ -23,10 +23,12 @@ import ( "github.com/mitchellh/copystructure" chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/common/util" ) // ProcessDependencies checks through this chart's dependencies, processing accordingly. -func ProcessDependencies(c *chart.Chart, v Values) error { +func ProcessDependencies(c *chart.Chart, v common.Values) error { if err := processDependencyEnabled(c, v, ""); err != nil { return err } @@ -34,7 +36,7 @@ func ProcessDependencies(c *chart.Chart, v Values) error { } // processDependencyConditions disables charts based on condition path value in values -func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath string) { +func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values, cpath string) { if reqs == nil { return } @@ -50,7 +52,7 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath s break } slog.Warn("returned non-bool value", "path", c, "chart", r.Name) - } else if _, ok := err.(ErrNoValue); !ok { + } else if _, ok := err.(common.ErrNoValue); !ok { // this is a real error slog.Warn("the method PathValue returned error", slog.Any("error", err)) } @@ -60,7 +62,7 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath s } // processDependencyTags disables charts based on tags in values -func processDependencyTags(reqs []*chart.Dependency, cvals Values) { +func processDependencyTags(reqs []*chart.Dependency, cvals common.Values) { if reqs == nil { return } @@ -177,7 +179,7 @@ Loop: for _, lr := range c.Metadata.Dependencies { lr.Enabled = true } - cvals, err := CoalesceValues(c, v) + cvals, err := util.CoalesceValues(c, v) if err != nil { return err } @@ -232,6 +234,8 @@ func pathToMap(path string, data map[string]interface{}) map[string]interface{} return set(parsePath(path), data) } +func parsePath(key string) []string { return strings.Split(key, ".") } + func set(path []string, data map[string]interface{}) map[string]interface{} { if len(path) == 0 { return nil @@ -249,12 +253,12 @@ func processImportValues(c *chart.Chart, merge bool) error { return nil } // combine chart values and empty config to get Values - var cvals Values + var cvals common.Values var err error if merge { - cvals, err = MergeValues(c, nil) + cvals, err = util.MergeValues(c, nil) } else { - cvals, err = CoalesceValues(c, nil) + cvals, err = util.CoalesceValues(c, nil) } if err != nil { return err @@ -282,9 +286,9 @@ func processImportValues(c *chart.Chart, merge bool) error { } // create value map from child to be merged into parent if merge { - b = MergeTables(b, pathToMap(parent, vv.AsMap())) + b = util.MergeTables(b, pathToMap(parent, vv.AsMap())) } else { - b = CoalesceTables(b, pathToMap(parent, vv.AsMap())) + b = util.CoalesceTables(b, pathToMap(parent, vv.AsMap())) } case string: child := "exports." + iv @@ -298,9 +302,9 @@ func processImportValues(c *chart.Chart, merge bool) error { continue } if merge { - b = MergeTables(b, vm.AsMap()) + b = util.MergeTables(b, vm.AsMap()) } else { - b = CoalesceTables(b, vm.AsMap()) + b = util.CoalesceTables(b, vm.AsMap()) } } } @@ -315,14 +319,14 @@ func processImportValues(c *chart.Chart, merge bool) error { // deep copying the cvals as there are cases where pointers can end // up in the cvals when they are copied onto b in ways that break things. cvals = deepCopyMap(cvals) - c.Values = MergeTables(cvals, b) + c.Values = util.MergeTables(cvals, b) } else { // Trimming the nil values from cvals is needed for backwards compatibility. // Previously, the b value had been populated with cvals along with some // overrides. This caused the coalescing functionality to remove the // nil/null values. This trimming is for backwards compat. cvals = trimNilValues(cvals) - c.Values = CoalesceTables(cvals, b) + c.Values = util.CoalesceTables(cvals, b) } return nil @@ -355,6 +359,12 @@ func trimNilValues(vals map[string]interface{}) map[string]interface{} { return valsCopyMap } +// istable is a special-purpose function to see if the present thing matches the definition of a YAML table. +func istable(v interface{}) bool { + _, ok := v.(map[string]interface{}) + return ok +} + // processDependencyImportValues imports specified chart values from child to parent. func processDependencyImportValues(c *chart.Chart, merge bool) error { for _, d := range c.Dependencies() { diff --git a/internal/chart/v3/util/dependencies_test.go b/internal/chart/v3/util/dependencies_test.go index 55839fe65..3c5bb96f7 100644 --- a/internal/chart/v3/util/dependencies_test.go +++ b/internal/chart/v3/util/dependencies_test.go @@ -23,6 +23,7 @@ import ( chart "helm.sh/helm/v4/internal/chart/v3" "helm.sh/helm/v4/internal/chart/v3/loader" + "helm.sh/helm/v4/pkg/chart/common" ) func loadChart(t *testing.T, path string) *chart.Chart { @@ -221,7 +222,7 @@ func TestProcessDependencyImportValues(t *testing.T) { if err := processDependencyImportValues(c, false); err != nil { t.Fatalf("processing import values dependencies %v", err) } - cc := Values(c.Values) + cc := common.Values(c.Values) for kk, vv := range e { pv, err := cc.PathValue(kk) if err != nil { @@ -251,7 +252,7 @@ func TestProcessDependencyImportValues(t *testing.T) { t.Error("expect nil value not found but found it") } switch xerr := err.(type) { - case ErrNoValue: + case common.ErrNoValue: // We found what we expected default: t.Errorf("expected an ErrNoValue but got %q instead", xerr) @@ -261,7 +262,7 @@ func TestProcessDependencyImportValues(t *testing.T) { if err := processDependencyImportValues(c, true); err != nil { t.Fatalf("processing import values dependencies %v", err) } - cc = Values(c.Values) + cc = common.Values(c.Values) val, err := cc.PathValue("ensurenull") if err != nil { t.Error("expect value but ensurenull was not found") @@ -291,7 +292,7 @@ func TestProcessDependencyImportValuesFromSharedDependencyToAliases(t *testing.T e["foo.grandchild.defaults.defaultValue"] = "42" e["bar.grandchild.defaults.defaultValue"] = "42" - cValues := Values(c.Values) + cValues := common.Values(c.Values) for kk, vv := range e { pv, err := cValues.PathValue(kk) if err != nil { @@ -329,7 +330,7 @@ func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) { if err := processDependencyImportValues(c, true); err != nil { t.Fatalf("processing import values dependencies %v", err) } - cc := Values(c.Values) + cc := common.Values(c.Values) for kk, vv := range e { pv, err := cc.PathValue(kk) if err != nil { diff --git a/internal/chart/v3/util/errors.go b/internal/chart/v3/util/errors.go deleted file mode 100644 index a175b9758..000000000 --- a/internal/chart/v3/util/errors.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "fmt" -) - -// ErrNoTable indicates that a chart does not have a matching table. -type ErrNoTable struct { - Key string -} - -func (e ErrNoTable) Error() string { return fmt.Sprintf("%q is not a table", e.Key) } - -// ErrNoValue indicates that Values does not contain a key with a value -type ErrNoValue struct { - Key string -} - -func (e ErrNoValue) Error() string { return fmt.Sprintf("%q is not a value", e.Key) } - -type ErrInvalidChartName struct { - Name string -} - -func (e ErrInvalidChartName) Error() string { - return fmt.Sprintf("%q is not a valid chart name", e.Name) -} diff --git a/internal/chart/v3/util/jsonschema.go b/internal/chart/v3/util/jsonschema.go deleted file mode 100644 index 9fe35904e..000000000 --- a/internal/chart/v3/util/jsonschema.go +++ /dev/null @@ -1,113 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "bytes" - "errors" - "fmt" - "log/slog" - "strings" - - "github.com/santhosh-tekuri/jsonschema/v6" - - chart "helm.sh/helm/v4/internal/chart/v3" -) - -// ValidateAgainstSchema checks that values does not violate the structure laid out in schema -func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) error { - var sb strings.Builder - if chrt.Schema != nil { - slog.Debug("chart name", "chart-name", chrt.Name()) - err := ValidateAgainstSingleSchema(values, chrt.Schema) - if err != nil { - sb.WriteString(fmt.Sprintf("%s:\n", chrt.Name())) - sb.WriteString(err.Error()) - } - } - slog.Debug("number of dependencies in the chart", "dependencies", len(chrt.Dependencies())) - // For each dependency, recursively call this function with the coalesced values - for _, subchart := range chrt.Dependencies() { - subchartValues := values[subchart.Name()].(map[string]interface{}) - if err := ValidateAgainstSchema(subchart, subchartValues); err != nil { - sb.WriteString(err.Error()) - } - } - - if sb.Len() > 0 { - return errors.New(sb.String()) - } - - return nil -} - -// ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema -func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) (reterr error) { - defer func() { - if r := recover(); r != nil { - reterr = fmt.Errorf("unable to validate schema: %s", r) - } - }() - - // This unmarshal function leverages UseNumber() for number precision. The parser - // used for values does this as well. - schema, err := jsonschema.UnmarshalJSON(bytes.NewReader(schemaJSON)) - if err != nil { - return err - } - slog.Debug("unmarshalled JSON schema", "schema", schemaJSON) - - compiler := jsonschema.NewCompiler() - err = compiler.AddResource("file:///values.schema.json", schema) - if err != nil { - return err - } - - validator, err := compiler.Compile("file:///values.schema.json") - if err != nil { - return err - } - - err = validator.Validate(values.AsMap()) - if err != nil { - return JSONSchemaValidationError{err} - } - - return nil -} - -// Note, JSONSchemaValidationError is used to wrap the error from the underlying -// validation package so that Helm has a clean interface and the validation package -// could be replaced without changing the Helm SDK API. - -// JSONSchemaValidationError is the error returned when there is a schema validation -// error. -type JSONSchemaValidationError struct { - embeddedErr error -} - -// Error prints the error message -func (e JSONSchemaValidationError) Error() string { - errStr := e.embeddedErr.Error() - - // This string prefixes all of our error details. Further up the stack of helm error message - // building more detail is provided to users. This is removed. - errStr = strings.TrimPrefix(errStr, "jsonschema validation failed with 'file:///values.schema.json#'\n") - - // The extra new line is needed for when there are sub-charts. - return errStr + "\n" -} diff --git a/internal/chart/v3/util/jsonschema_test.go b/internal/chart/v3/util/jsonschema_test.go deleted file mode 100644 index 0a3820377..000000000 --- a/internal/chart/v3/util/jsonschema_test.go +++ /dev/null @@ -1,247 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "os" - "testing" - - chart "helm.sh/helm/v4/internal/chart/v3" -) - -func TestValidateAgainstSingleSchema(t *testing.T) { - values, err := ReadValuesFile("./testdata/test-values.yaml") - if err != nil { - t.Fatalf("Error reading YAML file: %s", err) - } - schema, err := os.ReadFile("./testdata/test-values.schema.json") - if err != nil { - t.Fatalf("Error reading YAML file: %s", err) - } - - if err := ValidateAgainstSingleSchema(values, schema); err != nil { - t.Errorf("Error validating Values against Schema: %s", err) - } -} - -func TestValidateAgainstInvalidSingleSchema(t *testing.T) { - values, err := ReadValuesFile("./testdata/test-values.yaml") - if err != nil { - t.Fatalf("Error reading YAML file: %s", err) - } - schema, err := os.ReadFile("./testdata/test-values-invalid.schema.json") - if err != nil { - t.Fatalf("Error reading YAML file: %s", err) - } - - var errString string - if err := ValidateAgainstSingleSchema(values, schema); err == nil { - t.Fatalf("Expected an error, but got nil") - } else { - errString = err.Error() - } - - expectedErrString := `"file:///values.schema.json#" is not valid against metaschema: jsonschema validation failed with 'https://json-schema.org/draft/2020-12/schema#' -- at '': got number, want boolean or object` - if errString != expectedErrString { - t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) - } -} - -func TestValidateAgainstSingleSchemaNegative(t *testing.T) { - values, err := ReadValuesFile("./testdata/test-values-negative.yaml") - if err != nil { - t.Fatalf("Error reading YAML file: %s", err) - } - schema, err := os.ReadFile("./testdata/test-values.schema.json") - if err != nil { - t.Fatalf("Error reading JSON file: %s", err) - } - - var errString string - if err := ValidateAgainstSingleSchema(values, schema); err == nil { - t.Fatalf("Expected an error, but got nil") - } else { - errString = err.Error() - } - - expectedErrString := `- at '': missing property 'employmentInfo' -- at '/age': minimum: got -5, want 0 -` - if errString != expectedErrString { - t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) - } -} - -const subchartSchema = `{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Values", - "type": "object", - "properties": { - "age": { - "description": "Age", - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "age" - ] -} -` - -const subchartSchema2020 = `{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "Values", - "type": "object", - "properties": { - "data": { - "type": "array", - "contains": { "type": "string" }, - "unevaluatedItems": { "type": "number" } - } - }, - "required": ["data"] -} -` - -func TestValidateAgainstSchema(t *testing.T) { - subchartJSON := []byte(subchartSchema) - subchart := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "subchart", - }, - Schema: subchartJSON, - } - chrt := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "chrt", - }, - } - chrt.AddDependency(subchart) - - vals := map[string]interface{}{ - "name": "John", - "subchart": map[string]interface{}{ - "age": 25, - }, - } - - if err := ValidateAgainstSchema(chrt, vals); err != nil { - t.Errorf("Error validating Values against Schema: %s", err) - } -} - -func TestValidateAgainstSchemaNegative(t *testing.T) { - subchartJSON := []byte(subchartSchema) - subchart := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "subchart", - }, - Schema: subchartJSON, - } - chrt := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "chrt", - }, - } - chrt.AddDependency(subchart) - - vals := map[string]interface{}{ - "name": "John", - "subchart": map[string]interface{}{}, - } - - var errString string - if err := ValidateAgainstSchema(chrt, vals); err == nil { - t.Fatalf("Expected an error, but got nil") - } else { - errString = err.Error() - } - - expectedErrString := `subchart: -- at '': missing property 'age' -` - if errString != expectedErrString { - t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) - } -} - -func TestValidateAgainstSchema2020(t *testing.T) { - subchartJSON := []byte(subchartSchema2020) - subchart := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "subchart", - }, - Schema: subchartJSON, - } - chrt := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "chrt", - }, - } - chrt.AddDependency(subchart) - - vals := map[string]interface{}{ - "name": "John", - "subchart": map[string]interface{}{ - "data": []any{"hello", 12}, - }, - } - - if err := ValidateAgainstSchema(chrt, vals); err != nil { - t.Errorf("Error validating Values against Schema: %s", err) - } -} - -func TestValidateAgainstSchema2020Negative(t *testing.T) { - subchartJSON := []byte(subchartSchema2020) - subchart := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "subchart", - }, - Schema: subchartJSON, - } - chrt := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "chrt", - }, - } - chrt.AddDependency(subchart) - - vals := map[string]interface{}{ - "name": "John", - "subchart": map[string]interface{}{ - "data": []any{12}, - }, - } - - var errString string - if err := ValidateAgainstSchema(chrt, vals); err == nil { - t.Fatalf("Expected an error, but got nil") - } else { - errString = err.Error() - } - - expectedErrString := `subchart: -- at '/data': no items match contains schema - - at '/data/0': got number, want string -` - if errString != expectedErrString { - t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) - } -} diff --git a/internal/chart/v3/util/save.go b/internal/chart/v3/util/save.go index 3125cc3c9..49d93bf40 100644 --- a/internal/chart/v3/util/save.go +++ b/internal/chart/v3/util/save.go @@ -30,6 +30,7 @@ import ( "sigs.k8s.io/yaml" chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/pkg/chart/common" ) var headerBytes = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=") @@ -76,7 +77,7 @@ func SaveDir(c *chart.Chart, dest string) error { } // Save templates and files - for _, o := range [][]*chart.File{c.Templates, c.Files} { + for _, o := range [][]*common.File{c.Templates, c.Files} { for _, f := range o { n := filepath.Join(outdir, f.Name) if err := writeFile(n, f.Data); err != nil { @@ -246,7 +247,7 @@ func validateName(name string) error { nname := filepath.Base(name) if nname != name { - return ErrInvalidChartName{name} + return common.ErrInvalidChartName{Name: name} } return nil diff --git a/internal/chart/v3/util/save_test.go b/internal/chart/v3/util/save_test.go index 852675bb0..9b1b14a4c 100644 --- a/internal/chart/v3/util/save_test.go +++ b/internal/chart/v3/util/save_test.go @@ -31,6 +31,7 @@ import ( chart "helm.sh/helm/v4/internal/chart/v3" "helm.sh/helm/v4/internal/chart/v3/loader" + "helm.sh/helm/v4/pkg/chart/common" ) func TestSave(t *testing.T) { @@ -47,7 +48,7 @@ func TestSave(t *testing.T) { Lock: &chart.Lock{ Digest: "testdigest", }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, Schema: []byte("{\n \"title\": \"Values\"\n}"), @@ -113,7 +114,7 @@ func TestSave(t *testing.T) { Lock: &chart.Lock{ Digest: "testdigest", }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, } @@ -153,7 +154,7 @@ func TestSavePreservesTimestamps(t *testing.T) { "imageName": "testimage", "imageId": 42, }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, Schema: []byte("{\n \"title\": \"Values\"\n}"), @@ -219,10 +220,10 @@ func TestSaveDir(t *testing.T) { Name: "ahab", Version: "1.2.3", }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), Data: []byte("abc: {{ .Values.abc }}")}, }, } diff --git a/internal/chart/v3/util/values.go b/internal/chart/v3/util/values.go deleted file mode 100644 index 8e1a14b45..000000000 --- a/internal/chart/v3/util/values.go +++ /dev/null @@ -1,220 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "errors" - "fmt" - "io" - "os" - "strings" - - "sigs.k8s.io/yaml" - - chart "helm.sh/helm/v4/internal/chart/v3" -) - -// GlobalKey is the name of the Values key that is used for storing global vars. -const GlobalKey = "global" - -// Values represents a collection of chart values. -type Values map[string]interface{} - -// YAML encodes the Values into a YAML string. -func (v Values) YAML() (string, error) { - b, err := yaml.Marshal(v) - return string(b), err -} - -// Table gets a table (YAML subsection) from a Values object. -// -// The table is returned as a Values. -// -// Compound table names may be specified with dots: -// -// foo.bar -// -// The above will be evaluated as "The table bar inside the table -// foo". -// -// An ErrNoTable is returned if the table does not exist. -func (v Values) Table(name string) (Values, error) { - table := v - var err error - - for _, n := range parsePath(name) { - if table, err = tableLookup(table, n); err != nil { - break - } - } - return table, err -} - -// AsMap is a utility function for converting Values to a map[string]interface{}. -// -// It protects against nil map panics. -func (v Values) AsMap() map[string]interface{} { - if len(v) == 0 { - return map[string]interface{}{} - } - return v -} - -// Encode writes serialized Values information to the given io.Writer. -func (v Values) Encode(w io.Writer) error { - out, err := yaml.Marshal(v) - if err != nil { - return err - } - _, err = w.Write(out) - return err -} - -func tableLookup(v Values, simple string) (Values, error) { - v2, ok := v[simple] - if !ok { - return v, ErrNoTable{simple} - } - if vv, ok := v2.(map[string]interface{}); ok { - return vv, nil - } - - // This catches a case where a value is of type Values, but doesn't (for some - // reason) match the map[string]interface{}. This has been observed in the - // wild, and might be a result of a nil map of type Values. - if vv, ok := v2.(Values); ok { - return vv, nil - } - - return Values{}, ErrNoTable{simple} -} - -// ReadValues will parse YAML byte data into a Values. -func ReadValues(data []byte) (vals Values, err error) { - err = yaml.Unmarshal(data, &vals) - if len(vals) == 0 { - vals = Values{} - } - return vals, err -} - -// ReadValuesFile will parse a YAML file into a map of values. -func ReadValuesFile(filename string) (Values, error) { - data, err := os.ReadFile(filename) - if err != nil { - return map[string]interface{}{}, err - } - return ReadValues(data) -} - -// ReleaseOptions represents the additional release options needed -// for the composition of the final values struct -type ReleaseOptions struct { - Name string - Namespace string - Revision int - IsUpgrade bool - IsInstall bool -} - -// ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files -// -// This takes both ReleaseOptions and Capabilities to merge into the render values. -func ToRenderValues(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities) (Values, error) { - return ToRenderValuesWithSchemaValidation(chrt, chrtVals, options, caps, false) -} - -// ToRenderValuesWithSchemaValidation composes the struct from the data coming from the Releases, Charts and Values files -// -// This takes both ReleaseOptions and Capabilities to merge into the render values. -func ToRenderValuesWithSchemaValidation(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities, skipSchemaValidation bool) (Values, error) { - if caps == nil { - caps = DefaultCapabilities - } - top := map[string]interface{}{ - "Chart": chrt.Metadata, - "Capabilities": caps, - "Release": map[string]interface{}{ - "Name": options.Name, - "Namespace": options.Namespace, - "IsUpgrade": options.IsUpgrade, - "IsInstall": options.IsInstall, - "Revision": options.Revision, - "Service": "Helm", - }, - } - - vals, err := CoalesceValues(chrt, chrtVals) - if err != nil { - return top, err - } - - if !skipSchemaValidation { - if err := ValidateAgainstSchema(chrt, vals); err != nil { - return top, fmt.Errorf("values don't meet the specifications of the schema(s) in the following chart(s):\n%w", err) - } - } - - top["Values"] = vals - return top, nil -} - -// istable is a special-purpose function to see if the present thing matches the definition of a YAML table. -func istable(v interface{}) bool { - _, ok := v.(map[string]interface{}) - return ok -} - -// PathValue takes a path that traverses a YAML structure and returns the value at the end of that path. -// The path starts at the root of the YAML structure and is comprised of YAML keys separated by periods. -// Given the following YAML data the value at path "chapter.one.title" is "Loomings". -// -// chapter: -// one: -// title: "Loomings" -func (v Values) PathValue(path string) (interface{}, error) { - if path == "" { - return nil, errors.New("YAML path cannot be empty") - } - return v.pathValue(parsePath(path)) -} - -func (v Values) pathValue(path []string) (interface{}, error) { - if len(path) == 1 { - // if exists must be root key not table - if _, ok := v[path[0]]; ok && !istable(v[path[0]]) { - return v[path[0]], nil - } - return nil, ErrNoValue{path[0]} - } - - key, path := path[len(path)-1], path[:len(path)-1] - // get our table for table path - t, err := v.Table(joinPath(path...)) - if err != nil { - return nil, ErrNoValue{key} - } - // check table for key and ensure value is not a table - if k, ok := t[key]; ok && !istable(k) { - return k, nil - } - return nil, ErrNoValue{key} -} - -func parsePath(key string) []string { return strings.Split(key, ".") } - -func joinPath(path ...string) string { return strings.Join(path, ".") } diff --git a/internal/chart/v3/util/values_test.go b/internal/chart/v3/util/values_test.go deleted file mode 100644 index 34c664581..000000000 --- a/internal/chart/v3/util/values_test.go +++ /dev/null @@ -1,293 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "bytes" - "fmt" - "testing" - "text/template" - - chart "helm.sh/helm/v4/internal/chart/v3" -) - -func TestReadValues(t *testing.T) { - doc := `# Test YAML parse -poet: "Coleridge" -title: "Rime of the Ancient Mariner" -stanza: - - "at" - - "length" - - "did" - - cross - - an - - Albatross - -mariner: - with: "crossbow" - shot: "ALBATROSS" - -water: - water: - where: "everywhere" - nor: "any drop to drink" -` - - data, err := ReadValues([]byte(doc)) - if err != nil { - t.Fatalf("Error parsing bytes: %s", err) - } - matchValues(t, data) - - tests := []string{`poet: "Coleridge"`, "# Just a comment", ""} - - for _, tt := range tests { - data, err = ReadValues([]byte(tt)) - if err != nil { - t.Fatalf("Error parsing bytes (%s): %s", tt, err) - } - if data == nil { - t.Errorf(`YAML string "%s" gave a nil map`, tt) - } - } -} - -func TestToRenderValues(t *testing.T) { - - chartValues := map[string]interface{}{ - "name": "al Rashid", - "where": map[string]interface{}{ - "city": "Basrah", - "title": "caliph", - }, - } - - overrideValues := map[string]interface{}{ - "name": "Haroun", - "where": map[string]interface{}{ - "city": "Baghdad", - "date": "809 CE", - }, - } - - c := &chart.Chart{ - Metadata: &chart.Metadata{Name: "test"}, - Templates: []*chart.File{}, - Values: chartValues, - Files: []*chart.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, - }, - } - c.AddDependency(&chart.Chart{ - Metadata: &chart.Metadata{Name: "where"}, - }) - - o := ReleaseOptions{ - Name: "Seven Voyages", - Namespace: "default", - Revision: 1, - IsInstall: true, - } - - res, err := ToRenderValuesWithSchemaValidation(c, overrideValues, o, nil, false) - if err != nil { - t.Fatal(err) - } - - // Ensure that the top-level values are all set. - if name := res["Chart"].(*chart.Metadata).Name; name != "test" { - t.Errorf("Expected chart name 'test', got %q", name) - } - relmap := res["Release"].(map[string]interface{}) - if name := relmap["Name"]; name.(string) != "Seven Voyages" { - t.Errorf("Expected release name 'Seven Voyages', got %q", name) - } - if namespace := relmap["Namespace"]; namespace.(string) != "default" { - t.Errorf("Expected namespace 'default', got %q", namespace) - } - if revision := relmap["Revision"]; revision.(int) != 1 { - t.Errorf("Expected revision '1', got %d", revision) - } - if relmap["IsUpgrade"].(bool) { - t.Error("Expected upgrade to be false.") - } - if !relmap["IsInstall"].(bool) { - t.Errorf("Expected install to be true.") - } - if !res["Capabilities"].(*Capabilities).APIVersions.Has("v1") { - t.Error("Expected Capabilities to have v1 as an API") - } - if res["Capabilities"].(*Capabilities).KubeVersion.Major != "1" { - t.Error("Expected Capabilities to have a Kube version") - } - - vals := res["Values"].(Values) - if vals["name"] != "Haroun" { - t.Errorf("Expected 'Haroun', got %q (%v)", vals["name"], vals) - } - where := vals["where"].(map[string]interface{}) - expects := map[string]string{ - "city": "Baghdad", - "date": "809 CE", - "title": "caliph", - } - for field, expect := range expects { - if got := where[field]; got != expect { - t.Errorf("Expected %q, got %q (%v)", expect, got, where) - } - } -} - -func TestReadValuesFile(t *testing.T) { - data, err := ReadValuesFile("./testdata/coleridge.yaml") - if err != nil { - t.Fatalf("Error reading YAML file: %s", err) - } - matchValues(t, data) -} - -func ExampleValues() { - doc := ` -title: "Moby Dick" -chapter: - one: - title: "Loomings" - two: - title: "The Carpet-Bag" - three: - title: "The Spouter Inn" -` - d, err := ReadValues([]byte(doc)) - if err != nil { - panic(err) - } - ch1, err := d.Table("chapter.one") - if err != nil { - panic("could not find chapter one") - } - fmt.Print(ch1["title"]) - // Output: - // Loomings -} - -func TestTable(t *testing.T) { - doc := ` -title: "Moby Dick" -chapter: - one: - title: "Loomings" - two: - title: "The Carpet-Bag" - three: - title: "The Spouter Inn" -` - d, err := ReadValues([]byte(doc)) - if err != nil { - t.Fatalf("Failed to parse the White Whale: %s", err) - } - - if _, err := d.Table("title"); err == nil { - t.Fatalf("Title is not a table.") - } - - if _, err := d.Table("chapter"); err != nil { - t.Fatalf("Failed to get the chapter table: %s\n%v", err, d) - } - - if v, err := d.Table("chapter.one"); err != nil { - t.Errorf("Failed to get chapter.one: %s", err) - } else if v["title"] != "Loomings" { - t.Errorf("Unexpected title: %s", v["title"]) - } - - if _, err := d.Table("chapter.three"); err != nil { - t.Errorf("Chapter three is missing: %s\n%v", err, d) - } - - if _, err := d.Table("chapter.OneHundredThirtySix"); err == nil { - t.Errorf("I think you mean 'Epilogue'") - } -} - -func matchValues(t *testing.T, data map[string]interface{}) { - t.Helper() - if data["poet"] != "Coleridge" { - t.Errorf("Unexpected poet: %s", data["poet"]) - } - - if o, err := ttpl("{{len .stanza}}", data); err != nil { - t.Errorf("len stanza: %s", err) - } else if o != "6" { - t.Errorf("Expected 6, got %s", o) - } - - if o, err := ttpl("{{.mariner.shot}}", data); err != nil { - t.Errorf(".mariner.shot: %s", err) - } else if o != "ALBATROSS" { - t.Errorf("Expected that mariner shot ALBATROSS") - } - - if o, err := ttpl("{{.water.water.where}}", data); err != nil { - t.Errorf(".water.water.where: %s", err) - } else if o != "everywhere" { - t.Errorf("Expected water water everywhere") - } -} - -func ttpl(tpl string, v map[string]interface{}) (string, error) { - var b bytes.Buffer - tt := template.Must(template.New("t").Parse(tpl)) - err := tt.Execute(&b, v) - return b.String(), err -} - -func TestPathValue(t *testing.T) { - doc := ` -title: "Moby Dick" -chapter: - one: - title: "Loomings" - two: - title: "The Carpet-Bag" - three: - title: "The Spouter Inn" -` - d, err := ReadValues([]byte(doc)) - if err != nil { - t.Fatalf("Failed to parse the White Whale: %s", err) - } - - if v, err := d.PathValue("chapter.one.title"); err != nil { - t.Errorf("Got error instead of title: %s\n%v", err, d) - } else if v != "Loomings" { - t.Errorf("No error but got wrong value for title: %s\n%v", err, d) - } - if _, err := d.PathValue("chapter.one.doesnotexist"); err == nil { - t.Errorf("Non-existent key should return error: %s\n%v", err, d) - } - if _, err := d.PathValue("chapter.doesnotexist.one"); err == nil { - t.Errorf("Non-existent key in middle of path should return error: %s\n%v", err, d) - } - if _, err := d.PathValue(""); err == nil { - t.Error("Asking for the value from an empty path should yield an error") - } - if v, err := d.PathValue("title"); err == nil { - if v != "Moby Dick" { - t.Errorf("Failed to return values for root key title") - } - } -} diff --git a/pkg/action/action.go b/pkg/action/action.go index 522226a1a..bcf6ca8ef 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -39,6 +39,7 @@ import ( "sigs.k8s.io/kustomize/kyaml/kio" kyaml "sigs.k8s.io/kustomize/kyaml/yaml" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/engine" @@ -84,7 +85,7 @@ type Configuration struct { RegistryClient *registry.Client // Capabilities describes the capabilities of the Kubernetes cluster. - Capabilities *chartutil.Capabilities + Capabilities *common.Capabilities // CustomTemplateFuncs is defined by users to provide custom template funcs CustomTemplateFuncs template.FuncMap @@ -176,7 +177,7 @@ func splitAndDeannotate(postrendered string) (map[string]string, error) { // TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed // // This code has to do with writing files to disk. -func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret bool) ([]*release.Hook, *bytes.Buffer, string, error) { +func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret bool) ([]*release.Hook, *bytes.Buffer, string, error) { var hs []*release.Hook b := bytes.NewBuffer(nil) @@ -337,7 +338,7 @@ type RESTClientGetter interface { } // capabilities builds a Capabilities from discovery information. -func (cfg *Configuration) getCapabilities() (*chartutil.Capabilities, error) { +func (cfg *Configuration) getCapabilities() (*common.Capabilities, error) { if cfg.Capabilities != nil { return cfg.Capabilities, nil } @@ -366,14 +367,14 @@ func (cfg *Configuration) getCapabilities() (*chartutil.Capabilities, error) { } } - cfg.Capabilities = &chartutil.Capabilities{ + cfg.Capabilities = &common.Capabilities{ APIVersions: apiVersions, - KubeVersion: chartutil.KubeVersion{ + KubeVersion: common.KubeVersion{ Version: kubeVersion.GitVersion, Major: kubeVersion.Major, Minor: kubeVersion.Minor, }, - HelmVersion: chartutil.DefaultCapabilities.HelmVersion, + HelmVersion: common.DefaultCapabilities.HelmVersion, } return cfg.Capabilities, nil } @@ -409,10 +410,10 @@ func (cfg *Configuration) releaseContent(name string, version int) (*release.Rel } // GetVersionSet retrieves a set of available k8s API versions -func GetVersionSet(client discovery.ServerResourcesInterface) (chartutil.VersionSet, error) { +func GetVersionSet(client discovery.ServerResourcesInterface) (common.VersionSet, error) { groups, resources, err := client.ServerGroupsAndResources() if err != nil && !discovery.IsGroupDiscoveryFailedError(err) { - return chartutil.DefaultVersionSet, fmt.Errorf("could not get apiVersions from Kubernetes: %w", err) + return common.DefaultVersionSet, fmt.Errorf("could not get apiVersions from Kubernetes: %w", err) } // FIXME: The Kubernetes test fixture for cli appears to always return nil @@ -420,7 +421,7 @@ func GetVersionSet(client discovery.ServerResourcesInterface) (chartutil.Version // return the default API list. This is also a safe value to return in any // other odd-ball case. if len(groups) == 0 && len(resources) == 0 { - return chartutil.DefaultVersionSet, nil + return common.DefaultVersionSet, nil } versionMap := make(map[string]interface{}) @@ -453,7 +454,7 @@ func GetVersionSet(client discovery.ServerResourcesInterface) (chartutil.Version versions = append(versions, k) } - return chartutil.VersionSet(versions), nil + return common.VersionSet(versions), nil } // recordRelease with an update operation in case reuse has been set. diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index 7a510ace6..b65e40024 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -30,8 +30,8 @@ import ( fakeclientset "k8s.io/client-go/kubernetes/fake" "helm.sh/helm/v4/internal/logging" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/kube" kubefake "helm.sh/helm/v4/pkg/kube/fake" "helm.sh/helm/v4/pkg/registry" @@ -64,7 +64,7 @@ func actionConfigFixtureWithDummyResources(t *testing.T, dummyResources kube.Res return &Configuration{ Releases: storage.Init(driver.NewMemory()), KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: dummyResources}, - Capabilities: chartutil.DefaultCapabilities, + Capabilities: common.DefaultCapabilities, RegistryClient: registryClient, } } @@ -122,14 +122,14 @@ type chartOptions struct { type chartOption func(*chartOptions) func buildChart(opts ...chartOption) *chart.Chart { - defaultTemplates := []*chart.File{ + defaultTemplates := []*common.File{ {Name: "templates/hello", Data: []byte("hello: world")}, {Name: "templates/hooks", Data: []byte(manifestWithHook)}, } return buildChartWithTemplates(defaultTemplates, opts...) } -func buildChartWithTemplates(templates []*chart.File, opts ...chartOption) *chart.Chart { +func buildChartWithTemplates(templates []*common.File, opts ...chartOption) *chart.Chart { c := &chartOptions{ Chart: &chart.Chart{ // TODO: This should be more complete. @@ -179,7 +179,7 @@ func withValues(values map[string]interface{}) chartOption { func withNotes(notes string) chartOption { return func(opts *chartOptions) { - opts.Templates = append(opts.Templates, &chart.File{ + opts.Templates = append(opts.Templates, &common.File{ Name: "templates/NOTES.txt", Data: []byte(notes), }) @@ -200,7 +200,7 @@ func withMetadataDependency(dependency chart.Dependency) chartOption { func withSampleTemplates() chartOption { return func(opts *chartOptions) { - sampleTemplates := []*chart.File{ + sampleTemplates := []*common.File{ // This adds basic templates and partials. {Name: "templates/goodbye", Data: []byte("goodbye: world")}, {Name: "templates/empty", Data: []byte("")}, @@ -213,14 +213,14 @@ func withSampleTemplates() chartOption { func withSampleSecret() chartOption { return func(opts *chartOptions) { - sampleSecret := &chart.File{Name: "templates/secret.yaml", Data: []byte("apiVersion: v1\nkind: Secret\n")} + sampleSecret := &common.File{Name: "templates/secret.yaml", Data: []byte("apiVersion: v1\nkind: Secret\n")} opts.Templates = append(opts.Templates, sampleSecret) } } func withSampleIncludingIncorrectTemplates() chartOption { return func(opts *chartOptions) { - sampleTemplates := []*chart.File{ + sampleTemplates := []*common.File{ // This adds basic templates and partials. {Name: "templates/goodbye", Data: []byte("goodbye: world")}, {Name: "templates/empty", Data: []byte("")}, @@ -234,7 +234,7 @@ func withSampleIncludingIncorrectTemplates() chartOption { func withMultipleManifestTemplate() chartOption { return func(opts *chartOptions) { - sampleTemplates := []*chart.File{ + sampleTemplates := []*common.File{ {Name: "templates/rbac", Data: []byte(rbacManifests)}, } opts.Templates = append(opts.Templates, sampleTemplates...) @@ -851,7 +851,7 @@ func TestRenderResources_PostRenderer_MergeError(t *testing.T) { Name: "test-chart", Version: "0.1.0", }, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/invalid", Data: []byte("invalid: yaml: content:")}, }, } diff --git a/pkg/action/get_values.go b/pkg/action/get_values.go index 18b8b4838..a0b5d92c1 100644 --- a/pkg/action/get_values.go +++ b/pkg/action/get_values.go @@ -16,9 +16,7 @@ limitations under the License. package action -import ( - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" -) +import "helm.sh/helm/v4/pkg/chart/common/util" // GetValues is the action for checking a given release's values. // @@ -50,7 +48,7 @@ func (g *GetValues) Run(name string) (map[string]interface{}, error) { // If the user wants all values, compute the values and return. if g.AllValues { - cfg, err := chartutil.CoalesceValues(rel.Chart, rel.Config) + cfg, err := util.CoalesceValues(rel.Chart, rel.Config) if err != nil { return nil, err } diff --git a/pkg/action/hooks_test.go b/pkg/action/hooks_test.go index e3a2c0808..091155bc2 100644 --- a/pkg/action/hooks_test.go +++ b/pkg/action/hooks_test.go @@ -29,8 +29,7 @@ import ( "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/cli-runtime/pkg/resource" - chart "helm.sh/helm/v4/pkg/chart/v2" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + "helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/kube" kubefake "helm.sh/helm/v4/pkg/kube/fake" release "helm.sh/helm/v4/pkg/release/v1" @@ -178,7 +177,7 @@ func runInstallForHooksWithSuccess(t *testing.T, manifest, expectedNamespace str outBuffer := &bytes.Buffer{} instAction.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer} - templates := []*chart.File{ + templates := []*common.File{ {Name: "templates/hello", Data: []byte("hello: world")}, {Name: "templates/hooks", Data: []byte(manifest)}, } @@ -205,7 +204,7 @@ func runInstallForHooksWithFailure(t *testing.T, manifest, expectedNamespace str outBuffer := &bytes.Buffer{} failingClient.PrintingKubeClient = kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer} - templates := []*chart.File{ + templates := []*common.File{ {Name: "templates/hello", Data: []byte("hello: world")}, {Name: "templates/hooks", Data: []byte(manifest)}, } @@ -382,7 +381,7 @@ data: configuration := &Configuration{ Releases: storage.Init(driver.NewMemory()), KubeClient: kubeClient, - Capabilities: chartutil.DefaultCapabilities, + Capabilities: common.DefaultCapabilities, } serverSideApply := true diff --git a/pkg/action/install.go b/pkg/action/install.go index b2330d551..0fe3ebc4b 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -41,6 +41,8 @@ import ( "k8s.io/cli-runtime/pkg/resource" "sigs.k8s.io/yaml" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/common/util" chart "helm.sh/helm/v4/pkg/chart/v2" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/cli" @@ -113,8 +115,8 @@ type Install struct { // KubeVersion allows specifying a custom kubernetes version to use and // APIVersions allows a manual set of supported API Versions to be passed // (for things like templating). These are ignored if ClientOnly is false - KubeVersion *chartutil.KubeVersion - APIVersions chartutil.VersionSet + KubeVersion *common.KubeVersion + APIVersions common.VersionSet // Used by helm template to render charts with .Release.IsUpgrade. Ignored if Dry-Run is false IsUpgrade bool // Enable DNS lookups when rendering templates @@ -292,7 +294,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma if i.ClientOnly { // Add mock objects in here so it doesn't use Kube API server // NOTE(bacongobbler): used for `helm template` - i.cfg.Capabilities = chartutil.DefaultCapabilities.Copy() + i.cfg.Capabilities = common.DefaultCapabilities.Copy() if i.KubeVersion != nil { i.cfg.Capabilities.KubeVersion = *i.KubeVersion } @@ -319,14 +321,14 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma // special case for helm template --is-upgrade isUpgrade := i.IsUpgrade && i.isDryRun() - options := chartutil.ReleaseOptions{ + options := common.ReleaseOptions{ Name: i.ReleaseName, Namespace: i.Namespace, Revision: 1, IsInstall: !isUpgrade, IsUpgrade: isUpgrade, } - valuesToRender, err := chartutil.ToRenderValuesWithSchemaValidation(chrt, vals, options, caps, i.SkipSchemaValidation) + valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chrt, vals, options, caps, i.SkipSchemaValidation) if err != nil { return nil, err } diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index f567b3df4..92bb64b4d 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -45,8 +45,7 @@ import ( "k8s.io/client-go/rest/fake" "helm.sh/helm/v4/internal/test" - chart "helm.sh/helm/v4/pkg/chart/v2" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + "helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/kube" kubefake "helm.sh/helm/v4/pkg/kube/fake" release "helm.sh/helm/v4/pkg/release/v1" @@ -258,7 +257,7 @@ func TestInstallReleaseClientOnly(t *testing.T) { instAction.ClientOnly = true instAction.Run(buildChart(), nil) // disregard output - is.Equal(instAction.cfg.Capabilities, chartutil.DefaultCapabilities) + is.Equal(instAction.cfg.Capabilities, common.DefaultCapabilities) is.Equal(instAction.cfg.KubeClient, &kubefake.PrintingKubeClient{Out: io.Discard}) } @@ -429,7 +428,7 @@ func TestInstallRelease_DryRun_Lookup(t *testing.T) { vals := map[string]interface{}{} mockChart := buildChart(withSampleTemplates()) - mockChart.Templates = append(mockChart.Templates, &chart.File{ + mockChart.Templates = append(mockChart.Templates, &common.File{ Name: "templates/lookup", Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`), }) diff --git a/pkg/action/lint.go b/pkg/action/lint.go index 7b3c00ad2..208fd4637 100644 --- a/pkg/action/lint.go +++ b/pkg/action/lint.go @@ -22,9 +22,10 @@ import ( "path/filepath" "strings" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/v2/lint" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" - "helm.sh/helm/v4/pkg/lint" - "helm.sh/helm/v4/pkg/lint/support" ) // Lint is the action for checking that the semantics of a chart are well-formed. @@ -36,7 +37,7 @@ type Lint struct { WithSubcharts bool Quiet bool SkipSchemaValidation bool - KubeVersion *chartutil.KubeVersion + KubeVersion *common.KubeVersion } // LintResult is the result of Lint @@ -86,7 +87,7 @@ func HasWarningsOrErrors(result *LintResult) bool { return len(result.Errors) > 0 } -func lintChart(path string, vals map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion, skipSchemaValidation bool) (support.Linter, error) { +func lintChart(path string, vals map[string]interface{}, namespace string, kubeVersion *common.KubeVersion, skipSchemaValidation bool) (support.Linter, error) { var chartPath string linter := support.Linter{} diff --git a/pkg/action/show.go b/pkg/action/show.go index 6d6e10d24..4195d69a5 100644 --- a/pkg/action/show.go +++ b/pkg/action/show.go @@ -24,6 +24,7 @@ import ( "k8s.io/cli-runtime/pkg/printers" "sigs.k8s.io/yaml" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/chart/v2/loader" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" @@ -140,7 +141,7 @@ func (s *Show) Run(chartpath string) (string, error) { return out.String(), nil } -func findReadme(files []*chart.File) (file *chart.File) { +func findReadme(files []*common.File) (file *common.File) { for _, file := range files { for _, n := range readmeFileNames { if file == nil { diff --git a/pkg/action/show_test.go b/pkg/action/show_test.go index 67eba2338..faf306f2a 100644 --- a/pkg/action/show_test.go +++ b/pkg/action/show_test.go @@ -19,6 +19,7 @@ package action import ( "testing" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" ) @@ -27,14 +28,14 @@ func TestShow(t *testing.T) { client := NewShow(ShowAll, config) client.chart = &chart.Chart{ Metadata: &chart.Metadata{Name: "alpine"}, - Files: []*chart.File{ + Files: []*common.File{ {Name: "README.md", Data: []byte("README\n")}, {Name: "crds/ignoreme.txt", Data: []byte("error")}, {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, {Name: "crds/bar.json", Data: []byte("---\nbar\n")}, {Name: "crds/baz.yaml", Data: []byte("baz\n")}, }, - Raw: []*chart.File{ + Raw: []*common.File{ {Name: "values.yaml", Data: []byte("VALUES\n")}, }, Values: map[string]interface{}{}, @@ -105,7 +106,7 @@ func TestShowCRDs(t *testing.T) { client := NewShow(ShowCRDs, config) client.chart = &chart.Chart{ Metadata: &chart.Metadata{Name: "alpine"}, - Files: []*chart.File{ + Files: []*common.File{ {Name: "crds/ignoreme.txt", Data: []byte("error")}, {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, {Name: "crds/bar.json", Data: []byte("---\nbar\n")}, @@ -138,7 +139,7 @@ func TestShowNoReadme(t *testing.T) { client := NewShow(ShowAll, config) client.chart = &chart.Chart{ Metadata: &chart.Metadata{Name: "alpine"}, - Files: []*chart.File{ + Files: []*common.File{ {Name: "crds/ignoreme.txt", Data: []byte("error")}, {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, {Name: "crds/bar.json", Data: []byte("---\nbar\n")}, diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index c00a59079..3688adf0e 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -28,6 +28,8 @@ import ( "k8s.io/cli-runtime/pkg/resource" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/common/util" chart "helm.sh/helm/v4/pkg/chart/v2" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/kube" @@ -260,7 +262,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin // the release object. revision := lastRelease.Version + 1 - options := chartutil.ReleaseOptions{ + options := common.ReleaseOptions{ Name: name, Namespace: currentRelease.Namespace, Revision: revision, @@ -271,7 +273,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin if err != nil { return nil, nil, false, err } - valuesToRender, err := chartutil.ToRenderValuesWithSchemaValidation(chart, vals, options, caps, u.SkipSchemaValidation) + valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, vals, options, caps, u.SkipSchemaValidation) if err != nil { return nil, nil, false, err } @@ -588,12 +590,12 @@ func (u *Upgrade) reuseValues(chart *chart.Chart, current *release.Release, newV slog.Debug("reusing the old release's values") // We have to regenerate the old coalesced values: - oldVals, err := chartutil.CoalesceValues(current.Chart, current.Config) + oldVals, err := util.CoalesceValues(current.Chart, current.Config) if err != nil { return nil, fmt.Errorf("failed to rebuild old values: %w", err) } - newVals = chartutil.CoalesceTables(newVals, current.Config) + newVals = util.CoalesceTables(newVals, current.Config) chart.Values = oldVals @@ -604,7 +606,7 @@ func (u *Upgrade) reuseValues(chart *chart.Chart, current *release.Release, newV if u.ResetThenReuseValues { slog.Debug("merging values from old release to new values") - newVals = chartutil.CoalesceTables(newVals, current.Config) + newVals = util.CoalesceTables(newVals, current.Config) return newVals, nil } diff --git a/pkg/chart/common.go b/pkg/chart/common.go new file mode 100644 index 000000000..8b1dd58c3 --- /dev/null +++ b/pkg/chart/common.go @@ -0,0 +1,219 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package chart + +import ( + "errors" + "fmt" + "log/slog" + "reflect" + "strings" + + v3chart "helm.sh/helm/v4/internal/chart/v3" + common "helm.sh/helm/v4/pkg/chart/common" + v2chart "helm.sh/helm/v4/pkg/chart/v2" +) + +var NewAccessor func(chrt Charter) (Accessor, error) = NewDefaultAccessor //nolint:revive + +func NewDefaultAccessor(chrt Charter) (Accessor, error) { + switch v := chrt.(type) { + case v2chart.Chart: + return &v2Accessor{&v}, nil + case *v2chart.Chart: + return &v2Accessor{v}, nil + case v3chart.Chart: + return &v3Accessor{&v}, nil + case *v3chart.Chart: + return &v3Accessor{v}, nil + default: + return nil, errors.New("unsupported chart type") + } +} + +type v2Accessor struct { + chrt *v2chart.Chart +} + +func (r *v2Accessor) Name() string { + return r.chrt.Metadata.Name +} + +func (r *v2Accessor) IsRoot() bool { + return r.chrt.IsRoot() +} + +func (r *v2Accessor) MetadataAsMap() map[string]interface{} { + var ret map[string]interface{} + if r.chrt.Metadata == nil { + return ret + } + + ret, err := structToMap(r.chrt.Metadata) + if err != nil { + slog.Error("error converting metadata to map", "error", err) + } + return ret +} + +func (r *v2Accessor) Files() []*common.File { + return r.chrt.Files +} + +func (r *v2Accessor) Templates() []*common.File { + return r.chrt.Templates +} + +func (r *v2Accessor) ChartFullPath() string { + return r.chrt.ChartFullPath() +} + +func (r *v2Accessor) IsLibraryChart() bool { + return strings.EqualFold(r.chrt.Metadata.Type, "library") +} + +func (r *v2Accessor) Dependencies() []Charter { + var deps = make([]Charter, len(r.chrt.Dependencies())) + for i, c := range r.chrt.Dependencies() { + deps[i] = c + } + return deps +} + +func (r *v2Accessor) Values() map[string]interface{} { + return r.chrt.Values +} + +func (r *v2Accessor) Schema() []byte { + return r.chrt.Schema +} + +type v3Accessor struct { + chrt *v3chart.Chart +} + +func (r *v3Accessor) Name() string { + return r.chrt.Metadata.Name +} + +func (r *v3Accessor) IsRoot() bool { + return r.chrt.IsRoot() +} + +func (r *v3Accessor) MetadataAsMap() map[string]interface{} { + var ret map[string]interface{} + if r.chrt.Metadata == nil { + return ret + } + + ret, err := structToMap(r.chrt.Metadata) + if err != nil { + slog.Error("error converting metadata to map", "error", err) + } + return ret +} + +func (r *v3Accessor) Files() []*common.File { + return r.chrt.Files +} + +func (r *v3Accessor) Templates() []*common.File { + return r.chrt.Templates +} + +func (r *v3Accessor) ChartFullPath() string { + return r.chrt.ChartFullPath() +} + +func (r *v3Accessor) IsLibraryChart() bool { + return strings.EqualFold(r.chrt.Metadata.Type, "library") +} + +func (r *v3Accessor) Dependencies() []Charter { + var deps = make([]Charter, len(r.chrt.Dependencies())) + for i, c := range r.chrt.Dependencies() { + deps[i] = c + } + return deps +} + +func (r *v3Accessor) Values() map[string]interface{} { + return r.chrt.Values +} + +func (r *v3Accessor) Schema() []byte { + return r.chrt.Schema +} + +func structToMap(obj interface{}) (map[string]interface{}, error) { + objValue := reflect.ValueOf(obj) + + // If the value is a pointer, dereference it + if objValue.Kind() == reflect.Ptr { + objValue = objValue.Elem() + } + + // Check if the input is a struct + if objValue.Kind() != reflect.Struct { + return nil, fmt.Errorf("input must be a struct or a pointer to a struct") + } + + result := make(map[string]interface{}) + objType := objValue.Type() + + for i := 0; i < objValue.NumField(); i++ { + field := objType.Field(i) + value := objValue.Field(i) + + switch value.Kind() { + case reflect.Struct: + nestedMap, err := structToMap(value.Interface()) + if err != nil { + return nil, err + } + result[field.Name] = nestedMap + case reflect.Ptr: + // Recurse for pointers by dereferencing + if value.IsNil() { + result[field.Name] = nil + } else { + nestedMap, err := structToMap(value.Interface()) + if err != nil { + return nil, err + } + result[field.Name] = nestedMap + } + case reflect.Slice: + sliceOfMaps := make([]interface{}, value.Len()) + for j := 0; j < value.Len(); j++ { + sliceElement := value.Index(j) + if sliceElement.Kind() == reflect.Struct || sliceElement.Kind() == reflect.Ptr { + nestedMap, err := structToMap(sliceElement.Interface()) + if err != nil { + return nil, err + } + sliceOfMaps[j] = nestedMap + } else { + sliceOfMaps[j] = sliceElement.Interface() + } + } + result[field.Name] = sliceOfMaps + default: + result[field.Name] = value.Interface() + } + } + return result, nil +} diff --git a/pkg/chart/v2/util/capabilities.go b/pkg/chart/common/capabilities.go similarity index 99% rename from pkg/chart/v2/util/capabilities.go rename to pkg/chart/common/capabilities.go index 19d62c5e3..355c3978a 100644 --- a/pkg/chart/v2/util/capabilities.go +++ b/pkg/chart/common/capabilities.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util +package common import ( "fmt" diff --git a/pkg/chart/v2/util/capabilities_test.go b/pkg/chart/common/capabilities_test.go similarity index 99% rename from pkg/chart/v2/util/capabilities_test.go rename to pkg/chart/common/capabilities_test.go index e5513b3fd..bf32b1f3f 100644 --- a/pkg/chart/v2/util/capabilities_test.go +++ b/pkg/chart/common/capabilities_test.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util +package common import ( "testing" diff --git a/pkg/chart/v2/util/errors.go b/pkg/chart/common/errors.go similarity index 98% rename from pkg/chart/v2/util/errors.go rename to pkg/chart/common/errors.go index a175b9758..b0a2d650e 100644 --- a/pkg/chart/v2/util/errors.go +++ b/pkg/chart/common/errors.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util +package common import ( "fmt" diff --git a/pkg/chart/v2/util/errors_test.go b/pkg/chart/common/errors_test.go similarity index 98% rename from pkg/chart/v2/util/errors_test.go rename to pkg/chart/common/errors_test.go index b8ae86384..06b3b054c 100644 --- a/pkg/chart/v2/util/errors_test.go +++ b/pkg/chart/common/errors_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util +package common import ( "testing" diff --git a/internal/chart/v3/file.go b/pkg/chart/common/file.go similarity index 98% rename from internal/chart/v3/file.go rename to pkg/chart/common/file.go index ba04e106d..304643f1a 100644 --- a/internal/chart/v3/file.go +++ b/pkg/chart/common/file.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v3 +package common // File represents a file as a name/value pair. // diff --git a/pkg/chart/v2/util/testdata/coleridge.yaml b/pkg/chart/common/testdata/coleridge.yaml similarity index 100% rename from pkg/chart/v2/util/testdata/coleridge.yaml rename to pkg/chart/common/testdata/coleridge.yaml diff --git a/pkg/chart/v2/util/coalesce.go b/pkg/chart/common/util/coalesce.go similarity index 81% rename from pkg/chart/v2/util/coalesce.go rename to pkg/chart/common/util/coalesce.go index a3e0f5ae8..5bfa1c608 100644 --- a/pkg/chart/v2/util/coalesce.go +++ b/pkg/chart/common/util/coalesce.go @@ -23,7 +23,8 @@ import ( "github.com/mitchellh/copystructure" - chart "helm.sh/helm/v4/pkg/chart/v2" + chart "helm.sh/helm/v4/pkg/chart" + "helm.sh/helm/v4/pkg/chart/common" ) func concatPrefix(a, b string) string { @@ -42,7 +43,7 @@ func concatPrefix(a, b string) string { // - Scalar values and arrays are replaced, maps are merged // - A chart has access to all of the variables for it, as well as all of // the values destined for its dependencies. -func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) { +func CoalesceValues(chrt chart.Charter, vals map[string]interface{}) (common.Values, error) { valsCopy, err := copyValues(vals) if err != nil { return vals, err @@ -64,7 +65,7 @@ func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, err // Retaining Nils is useful when processes early in a Helm action or business // logic need to retain them for when Coalescing will happen again later in the // business logic. -func MergeValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) { +func MergeValues(chrt chart.Charter, vals map[string]interface{}) (common.Values, error) { valsCopy, err := copyValues(vals) if err != nil { return vals, err @@ -72,7 +73,7 @@ func MergeValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) return coalesce(log.Printf, chrt, valsCopy, "", true) } -func copyValues(vals map[string]interface{}) (Values, error) { +func copyValues(vals map[string]interface{}) (common.Values, error) { v, err := copystructure.Copy(vals) if err != nil { return vals, err @@ -96,28 +97,36 @@ type printFn func(format string, v ...interface{}) // Note, the merge argument specifies whether this is being used by MergeValues // or CoalesceValues. Coalescing removes null values and their keys in some // situations while merging keeps the null values. -func coalesce(printf printFn, ch *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) { +func coalesce(printf printFn, ch chart.Charter, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) { coalesceValues(printf, ch, dest, prefix, merge) return coalesceDeps(printf, ch, dest, prefix, merge) } // coalesceDeps coalesces the dependencies of the given chart. -func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) { - for _, subchart := range chrt.Dependencies() { - if c, ok := dest[subchart.Name()]; !ok { +func coalesceDeps(printf printFn, chrt chart.Charter, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) { + ch, err := chart.NewAccessor(chrt) + if err != nil { + return dest, err + } + for _, subchart := range ch.Dependencies() { + sub, err := chart.NewAccessor(subchart) + if err != nil { + return dest, err + } + if c, ok := dest[sub.Name()]; !ok { // If dest doesn't already have the key, create it. - dest[subchart.Name()] = make(map[string]interface{}) + dest[sub.Name()] = make(map[string]interface{}) } else if !istable(c) { - return dest, fmt.Errorf("type mismatch on %s: %t", subchart.Name(), c) + return dest, fmt.Errorf("type mismatch on %s: %t", sub.Name(), c) } - if dv, ok := dest[subchart.Name()]; ok { + if dv, ok := dest[sub.Name()]; ok { dvmap := dv.(map[string]interface{}) - subPrefix := concatPrefix(prefix, chrt.Metadata.Name) + subPrefix := concatPrefix(prefix, ch.Name()) // Get globals out of dest and merge them into dvmap. coalesceGlobals(printf, dvmap, dest, subPrefix, merge) // Now coalesce the rest of the values. var err error - dest[subchart.Name()], err = coalesce(printf, subchart, dvmap, subPrefix, merge) + dest[sub.Name()], err = coalesce(printf, subchart, dvmap, subPrefix, merge) if err != nil { return dest, err } @@ -132,17 +141,17 @@ func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{} func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix string, _ bool) { var dg, sg map[string]interface{} - if destglob, ok := dest[GlobalKey]; !ok { + if destglob, ok := dest[common.GlobalKey]; !ok { dg = make(map[string]interface{}) } else if dg, ok = destglob.(map[string]interface{}); !ok { - printf("warning: skipping globals because destination %s is not a table.", GlobalKey) + printf("warning: skipping globals because destination %s is not a table.", common.GlobalKey) return } - if srcglob, ok := src[GlobalKey]; !ok { + if srcglob, ok := src[common.GlobalKey]; !ok { sg = make(map[string]interface{}) } else if sg, ok = srcglob.(map[string]interface{}); !ok { - printf("warning: skipping globals because source %s is not a table.", GlobalKey) + printf("warning: skipping globals because source %s is not a table.", common.GlobalKey) return } @@ -178,7 +187,7 @@ func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix st dg[key] = val } } - dest[GlobalKey] = dg + dest[common.GlobalKey] = dg } func copyMap(src map[string]interface{}) map[string]interface{} { @@ -190,13 +199,18 @@ func copyMap(src map[string]interface{}) map[string]interface{} { // coalesceValues builds up a values map for a particular chart. // // Values in v will override the values in the chart. -func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, prefix string, merge bool) { - subPrefix := concatPrefix(prefix, c.Metadata.Name) +func coalesceValues(printf printFn, c chart.Charter, v map[string]interface{}, prefix string, merge bool) { + ch, err := chart.NewAccessor(c) + if err != nil { + return + } + + subPrefix := concatPrefix(prefix, ch.Name()) // Using c.Values directly when coalescing a table can cause problems where // the original c.Values is altered. Creating a deep copy stops the problem. // This section is fault-tolerant as there is no ability to return an error. - valuesCopy, err := copystructure.Copy(c.Values) + valuesCopy, err := copystructure.Copy(ch.Values()) var vc map[string]interface{} var ok bool if err != nil { @@ -205,7 +219,7 @@ func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, pr // wrong with c.Values. In this case we will use c.Values and report // an error. printf("warning: unable to copy values, err: %s", err) - vc = c.Values + vc = ch.Values() } else { vc, ok = valuesCopy.(map[string]interface{}) if !ok { @@ -213,7 +227,7 @@ func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, pr // it cannot be treated as map[string]interface{} there is something // strangely wrong. Log it and use c.Values printf("warning: unable to convert values copy to values type") - vc = c.Values + vc = ch.Values() } } @@ -250,9 +264,17 @@ func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, pr } } -func childChartMergeTrue(chrt *chart.Chart, key string, merge bool) bool { - for _, subchart := range chrt.Dependencies() { - if subchart.Name() == key { +func childChartMergeTrue(chrt chart.Charter, key string, merge bool) bool { + ch, err := chart.NewAccessor(chrt) + if err != nil { + return merge + } + for _, subchart := range ch.Dependencies() { + sub, err := chart.NewAccessor(subchart) + if err != nil { + return merge + } + if sub.Name() == key { return true } } @@ -306,3 +328,9 @@ func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, pref } return dst } + +// istable is a special-purpose function to see if the present thing matches the definition of a YAML table. +func istable(v interface{}) bool { + _, ok := v.(map[string]interface{}) + return ok +} diff --git a/pkg/chart/v2/util/coalesce_test.go b/pkg/chart/common/util/coalesce_test.go similarity index 97% rename from pkg/chart/v2/util/coalesce_test.go rename to pkg/chart/common/util/coalesce_test.go index e2c45a435..871bfa8da 100644 --- a/pkg/chart/v2/util/coalesce_test.go +++ b/pkg/chart/common/util/coalesce_test.go @@ -17,13 +17,16 @@ limitations under the License. package util import ( + "bytes" "encoding/json" "fmt" "maps" "testing" + "text/template" "github.com/stretchr/testify/assert" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" ) @@ -136,7 +139,7 @@ func TestCoalesceValues(t *testing.T) { }, ) - vals, err := ReadValues(testCoalesceValuesYaml) + vals, err := common.ReadValues(testCoalesceValuesYaml) if err != nil { t.Fatal(err) } @@ -144,7 +147,7 @@ func TestCoalesceValues(t *testing.T) { // taking a copy of the values before passing it // to CoalesceValues as argument, so that we can // use it for asserting later - valsCopy := make(Values, len(vals)) + valsCopy := make(common.Values, len(vals)) maps.Copy(valsCopy, vals) v, err := CoalesceValues(c, vals) @@ -238,6 +241,13 @@ func TestCoalesceValues(t *testing.T) { is.Equal(valsCopy, vals) } +func ttpl(tpl string, v map[string]interface{}) (string, error) { + var b bytes.Buffer + tt := template.Must(template.New("t").Parse(tpl)) + err := tt.Execute(&b, v) + return b.String(), err +} + func TestMergeValues(t *testing.T) { is := assert.New(t) @@ -294,7 +304,7 @@ func TestMergeValues(t *testing.T) { }, ) - vals, err := ReadValues(testCoalesceValuesYaml) + vals, err := common.ReadValues(testCoalesceValuesYaml) if err != nil { t.Fatal(err) } @@ -302,7 +312,7 @@ func TestMergeValues(t *testing.T) { // taking a copy of the values before passing it // to MergeValues as argument, so that we can // use it for asserting later - valsCopy := make(Values, len(vals)) + valsCopy := make(common.Values, len(vals)) maps.Copy(valsCopy, vals) v, err := MergeValues(c, vals) diff --git a/pkg/chart/v2/util/jsonschema.go b/pkg/chart/common/util/jsonschema.go similarity index 89% rename from pkg/chart/v2/util/jsonschema.go rename to pkg/chart/common/util/jsonschema.go index 72e133363..acd2ca100 100644 --- a/pkg/chart/v2/util/jsonschema.go +++ b/pkg/chart/common/util/jsonschema.go @@ -30,7 +30,8 @@ import ( "helm.sh/helm/v4/internal/version" - chart "helm.sh/helm/v4/pkg/chart/v2" + chart "helm.sh/helm/v4/pkg/chart" + "helm.sh/helm/v4/pkg/chart/common" ) // HTTPURLLoader implements a loader for HTTP/HTTPS URLs @@ -71,11 +72,15 @@ func newHTTPURLLoader() *HTTPURLLoader { } // ValidateAgainstSchema checks that values does not violate the structure laid out in schema -func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) error { +func ValidateAgainstSchema(ch chart.Charter, values map[string]interface{}) error { + chrt, err := chart.NewAccessor(ch) + if err != nil { + return err + } var sb strings.Builder - if chrt.Schema != nil { + if chrt.Schema() != nil { slog.Debug("chart name", "chart-name", chrt.Name()) - err := ValidateAgainstSingleSchema(values, chrt.Schema) + err := ValidateAgainstSingleSchema(values, chrt.Schema()) if err != nil { sb.WriteString(fmt.Sprintf("%s:\n", chrt.Name())) sb.WriteString(err.Error()) @@ -84,7 +89,11 @@ func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) err slog.Debug("number of dependencies in the chart", "dependencies", len(chrt.Dependencies())) // For each dependency, recursively call this function with the coalesced values for _, subchart := range chrt.Dependencies() { - subchartValues := values[subchart.Name()].(map[string]interface{}) + sub, err := chart.NewAccessor(subchart) + if err != nil { + return err + } + subchartValues := values[sub.Name()].(map[string]interface{}) if err := ValidateAgainstSchema(subchart, subchartValues); err != nil { sb.WriteString(err.Error()) } @@ -98,7 +107,7 @@ func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) err } // ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema -func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) (reterr error) { +func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte) (reterr error) { defer func() { if r := recover(); r != nil { reterr = fmt.Errorf("unable to validate schema: %s", r) diff --git a/pkg/chart/v2/util/jsonschema_test.go b/pkg/chart/common/util/jsonschema_test.go similarity index 96% rename from pkg/chart/v2/util/jsonschema_test.go rename to pkg/chart/common/util/jsonschema_test.go index cd95b7faf..b34f9d514 100644 --- a/pkg/chart/v2/util/jsonschema_test.go +++ b/pkg/chart/common/util/jsonschema_test.go @@ -23,11 +23,12 @@ import ( "strings" "testing" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" ) func TestValidateAgainstSingleSchema(t *testing.T) { - values, err := ReadValuesFile("./testdata/test-values.yaml") + values, err := common.ReadValuesFile("./testdata/test-values.yaml") if err != nil { t.Fatalf("Error reading YAML file: %s", err) } @@ -42,7 +43,7 @@ func TestValidateAgainstSingleSchema(t *testing.T) { } func TestValidateAgainstInvalidSingleSchema(t *testing.T) { - values, err := ReadValuesFile("./testdata/test-values.yaml") + values, err := common.ReadValuesFile("./testdata/test-values.yaml") if err != nil { t.Fatalf("Error reading YAML file: %s", err) } @@ -66,7 +67,7 @@ func TestValidateAgainstInvalidSingleSchema(t *testing.T) { } func TestValidateAgainstSingleSchemaNegative(t *testing.T) { - values, err := ReadValuesFile("./testdata/test-values-negative.yaml") + values, err := common.ReadValuesFile("./testdata/test-values-negative.yaml") if err != nil { t.Fatalf("Error reading YAML file: %s", err) } diff --git a/pkg/chart/v2/util/testdata/test-values-invalid.schema.json b/pkg/chart/common/util/testdata/test-values-invalid.schema.json similarity index 100% rename from pkg/chart/v2/util/testdata/test-values-invalid.schema.json rename to pkg/chart/common/util/testdata/test-values-invalid.schema.json diff --git a/pkg/chart/v2/util/testdata/test-values-negative.yaml b/pkg/chart/common/util/testdata/test-values-negative.yaml similarity index 100% rename from pkg/chart/v2/util/testdata/test-values-negative.yaml rename to pkg/chart/common/util/testdata/test-values-negative.yaml diff --git a/pkg/chart/v2/util/testdata/test-values.schema.json b/pkg/chart/common/util/testdata/test-values.schema.json similarity index 100% rename from pkg/chart/v2/util/testdata/test-values.schema.json rename to pkg/chart/common/util/testdata/test-values.schema.json diff --git a/pkg/chart/v2/util/testdata/test-values.yaml b/pkg/chart/common/util/testdata/test-values.yaml similarity index 100% rename from pkg/chart/v2/util/testdata/test-values.yaml rename to pkg/chart/common/util/testdata/test-values.yaml diff --git a/pkg/chart/common/util/values.go b/pkg/chart/common/util/values.go new file mode 100644 index 000000000..85cb29012 --- /dev/null +++ b/pkg/chart/common/util/values.go @@ -0,0 +1,70 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "fmt" + + "helm.sh/helm/v4/pkg/chart" + "helm.sh/helm/v4/pkg/chart/common" +) + +// ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files +// +// This takes both ReleaseOptions and Capabilities to merge into the render values. +func ToRenderValues(chrt chart.Charter, chrtVals map[string]interface{}, options common.ReleaseOptions, caps *common.Capabilities) (common.Values, error) { + return ToRenderValuesWithSchemaValidation(chrt, chrtVals, options, caps, false) +} + +// ToRenderValuesWithSchemaValidation composes the struct from the data coming from the Releases, Charts and Values files +// +// This takes both ReleaseOptions and Capabilities to merge into the render values. +func ToRenderValuesWithSchemaValidation(chrt chart.Charter, chrtVals map[string]interface{}, options common.ReleaseOptions, caps *common.Capabilities, skipSchemaValidation bool) (common.Values, error) { + if caps == nil { + caps = common.DefaultCapabilities + } + accessor, err := chart.NewAccessor(chrt) + if err != nil { + return nil, err + } + top := map[string]interface{}{ + "Chart": accessor.MetadataAsMap(), + "Capabilities": caps, + "Release": map[string]interface{}{ + "Name": options.Name, + "Namespace": options.Namespace, + "IsUpgrade": options.IsUpgrade, + "IsInstall": options.IsInstall, + "Revision": options.Revision, + "Service": "Helm", + }, + } + + vals, err := CoalesceValues(chrt, chrtVals) + if err != nil { + return common.Values(top), err + } + + if !skipSchemaValidation { + if err := ValidateAgainstSchema(chrt, vals); err != nil { + return top, fmt.Errorf("values don't meet the specifications of the schema(s) in the following chart(s):\n%w", err) + } + } + + top["Values"] = vals + return top, nil +} diff --git a/pkg/chart/common/util/values_test.go b/pkg/chart/common/util/values_test.go new file mode 100644 index 000000000..5fc030567 --- /dev/null +++ b/pkg/chart/common/util/values_test.go @@ -0,0 +1,111 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "testing" + + "helm.sh/helm/v4/pkg/chart/common" + chart "helm.sh/helm/v4/pkg/chart/v2" +) + +func TestToRenderValues(t *testing.T) { + + chartValues := map[string]interface{}{ + "name": "al Rashid", + "where": map[string]interface{}{ + "city": "Basrah", + "title": "caliph", + }, + } + + overrideValues := map[string]interface{}{ + "name": "Haroun", + "where": map[string]interface{}{ + "city": "Baghdad", + "date": "809 CE", + }, + } + + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "test"}, + Templates: []*common.File{}, + Values: chartValues, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + }, + } + c.AddDependency(&chart.Chart{ + Metadata: &chart.Metadata{Name: "where"}, + }) + + o := common.ReleaseOptions{ + Name: "Seven Voyages", + Namespace: "default", + Revision: 1, + IsInstall: true, + } + + res, err := ToRenderValuesWithSchemaValidation(c, overrideValues, o, nil, false) + if err != nil { + t.Fatal(err) + } + + // Ensure that the top-level values are all set. + metamap := res["Chart"].(map[string]interface{}) + if name := metamap["Name"]; name.(string) != "test" { + t.Errorf("Expected chart name 'test', got %q", name) + } + relmap := res["Release"].(map[string]interface{}) + if name := relmap["Name"]; name.(string) != "Seven Voyages" { + t.Errorf("Expected release name 'Seven Voyages', got %q", name) + } + if namespace := relmap["Namespace"]; namespace.(string) != "default" { + t.Errorf("Expected namespace 'default', got %q", namespace) + } + if revision := relmap["Revision"]; revision.(int) != 1 { + t.Errorf("Expected revision '1', got %d", revision) + } + if relmap["IsUpgrade"].(bool) { + t.Error("Expected upgrade to be false.") + } + if !relmap["IsInstall"].(bool) { + t.Errorf("Expected install to be true.") + } + if !res["Capabilities"].(*common.Capabilities).APIVersions.Has("v1") { + t.Error("Expected Capabilities to have v1 as an API") + } + if res["Capabilities"].(*common.Capabilities).KubeVersion.Major != "1" { + t.Error("Expected Capabilities to have a Kube version") + } + + vals := res["Values"].(common.Values) + if vals["name"] != "Haroun" { + t.Errorf("Expected 'Haroun', got %q (%v)", vals["name"], vals) + } + where := vals["where"].(map[string]interface{}) + expects := map[string]string{ + "city": "Baghdad", + "date": "809 CE", + "title": "caliph", + } + for field, expect := range expects { + if got := where[field]; got != expect { + t.Errorf("Expected %q, got %q (%v)", expect, got, where) + } + } +} diff --git a/pkg/chart/v2/util/values.go b/pkg/chart/common/values.go similarity index 74% rename from pkg/chart/v2/util/values.go rename to pkg/chart/common/values.go index 6850e8b9b..94958a779 100644 --- a/pkg/chart/v2/util/values.go +++ b/pkg/chart/common/values.go @@ -14,18 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util +package common import ( "errors" - "fmt" "io" "os" "strings" "sigs.k8s.io/yaml" - - chart "helm.sh/helm/v4/pkg/chart/v2" ) // GlobalKey is the name of the Values key that is used for storing global vars. @@ -131,48 +128,6 @@ type ReleaseOptions struct { IsInstall bool } -// ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files -// -// This takes both ReleaseOptions and Capabilities to merge into the render values. -func ToRenderValues(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities) (Values, error) { - return ToRenderValuesWithSchemaValidation(chrt, chrtVals, options, caps, false) -} - -// ToRenderValuesWithSchemaValidation composes the struct from the data coming from the Releases, Charts and Values files -// -// This takes both ReleaseOptions and Capabilities to merge into the render values. -func ToRenderValuesWithSchemaValidation(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities, skipSchemaValidation bool) (Values, error) { - if caps == nil { - caps = DefaultCapabilities - } - top := map[string]interface{}{ - "Chart": chrt.Metadata, - "Capabilities": caps, - "Release": map[string]interface{}{ - "Name": options.Name, - "Namespace": options.Namespace, - "IsUpgrade": options.IsUpgrade, - "IsInstall": options.IsInstall, - "Revision": options.Revision, - "Service": "Helm", - }, - } - - vals, err := CoalesceValues(chrt, chrtVals) - if err != nil { - return top, err - } - - if !skipSchemaValidation { - if err := ValidateAgainstSchema(chrt, vals); err != nil { - return top, fmt.Errorf("values don't meet the specifications of the schema(s) in the following chart(s):\n%w", err) - } - } - - top["Values"] = vals - return top, nil -} - // istable is a special-purpose function to see if the present thing matches the definition of a YAML table. func istable(v interface{}) bool { _, ok := v.(map[string]interface{}) diff --git a/pkg/chart/v2/util/values_test.go b/pkg/chart/common/values_test.go similarity index 66% rename from pkg/chart/v2/util/values_test.go rename to pkg/chart/common/values_test.go index 1a25fafb8..3cceeb2b5 100644 --- a/pkg/chart/v2/util/values_test.go +++ b/pkg/chart/common/values_test.go @@ -14,15 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util +package common import ( "bytes" "fmt" "testing" "text/template" - - chart "helm.sh/helm/v4/pkg/chart/v2" ) func TestReadValues(t *testing.T) { @@ -66,92 +64,6 @@ water: } } -func TestToRenderValues(t *testing.T) { - - chartValues := map[string]interface{}{ - "name": "al Rashid", - "where": map[string]interface{}{ - "city": "Basrah", - "title": "caliph", - }, - } - - overrideValues := map[string]interface{}{ - "name": "Haroun", - "where": map[string]interface{}{ - "city": "Baghdad", - "date": "809 CE", - }, - } - - c := &chart.Chart{ - Metadata: &chart.Metadata{Name: "test"}, - Templates: []*chart.File{}, - Values: chartValues, - Files: []*chart.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, - }, - } - c.AddDependency(&chart.Chart{ - Metadata: &chart.Metadata{Name: "where"}, - }) - - o := ReleaseOptions{ - Name: "Seven Voyages", - Namespace: "default", - Revision: 1, - IsInstall: true, - } - - res, err := ToRenderValuesWithSchemaValidation(c, overrideValues, o, nil, false) - if err != nil { - t.Fatal(err) - } - - // Ensure that the top-level values are all set. - if name := res["Chart"].(*chart.Metadata).Name; name != "test" { - t.Errorf("Expected chart name 'test', got %q", name) - } - relmap := res["Release"].(map[string]interface{}) - if name := relmap["Name"]; name.(string) != "Seven Voyages" { - t.Errorf("Expected release name 'Seven Voyages', got %q", name) - } - if namespace := relmap["Namespace"]; namespace.(string) != "default" { - t.Errorf("Expected namespace 'default', got %q", namespace) - } - if revision := relmap["Revision"]; revision.(int) != 1 { - t.Errorf("Expected revision '1', got %d", revision) - } - if relmap["IsUpgrade"].(bool) { - t.Error("Expected upgrade to be false.") - } - if !relmap["IsInstall"].(bool) { - t.Errorf("Expected install to be true.") - } - if !res["Capabilities"].(*Capabilities).APIVersions.Has("v1") { - t.Error("Expected Capabilities to have v1 as an API") - } - if res["Capabilities"].(*Capabilities).KubeVersion.Major != "1" { - t.Error("Expected Capabilities to have a Kube version") - } - - vals := res["Values"].(Values) - if vals["name"] != "Haroun" { - t.Errorf("Expected 'Haroun', got %q (%v)", vals["name"], vals) - } - where := vals["where"].(map[string]interface{}) - expects := map[string]string{ - "city": "Baghdad", - "date": "809 CE", - "title": "caliph", - } - for field, expect := range expects { - if got := where[field]; got != expect { - t.Errorf("Expected %q, got %q (%v)", expect, got, where) - } - } -} - func TestReadValuesFile(t *testing.T) { data, err := ReadValuesFile("./testdata/coleridge.yaml") if err != nil { diff --git a/pkg/chart/v2/file.go b/pkg/chart/interfaces.go similarity index 60% rename from pkg/chart/v2/file.go rename to pkg/chart/interfaces.go index a2eeb0fcd..e87dd2c08 100644 --- a/pkg/chart/v2/file.go +++ b/pkg/chart/interfaces.go @@ -13,15 +13,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v2 +package chart -// File represents a file as a name/value pair. -// -// By convention, name is a relative path within the scope of the chart's -// base directory. -type File struct { - // Name is the path-like name of the template. - Name string `json:"name"` - // Data is the template as byte data. - Data []byte `json:"data"` +import ( + common "helm.sh/helm/v4/pkg/chart/common" +) + +type Charter interface{} + +type Accessor interface { + Name() string + IsRoot() bool + MetadataAsMap() map[string]interface{} + Files() []*common.File + Templates() []*common.File + ChartFullPath() string + IsLibraryChart() bool + Dependencies() []Charter + Values() map[string]interface{} + Schema() []byte } diff --git a/pkg/chart/v2/chart.go b/pkg/chart/v2/chart.go index 66ddf98a5..f59bcd8b3 100644 --- a/pkg/chart/v2/chart.go +++ b/pkg/chart/v2/chart.go @@ -19,6 +19,8 @@ import ( "path/filepath" "regexp" "strings" + + "helm.sh/helm/v4/pkg/chart/common" ) // APIVersionV1 is the API version number for version 1. @@ -37,20 +39,20 @@ type Chart struct { // // This should not be used except in special cases like `helm show values`, // where we want to display the raw values, comments and all. - Raw []*File `json:"-"` + Raw []*common.File `json:"-"` // Metadata is the contents of the Chartfile. Metadata *Metadata `json:"metadata"` // Lock is the contents of Chart.lock. Lock *Lock `json:"lock"` // Templates for this chart. - Templates []*File `json:"templates"` + Templates []*common.File `json:"templates"` // Values are default config for this chart. Values map[string]interface{} `json:"values"` // Schema is an optional JSON schema for imposing structure on Values Schema []byte `json:"schema"` // Files are miscellaneous files in a chart archive, // e.g. README, LICENSE, etc. - Files []*File `json:"files"` + Files []*common.File `json:"files"` parent *Chart dependencies []*Chart @@ -62,7 +64,7 @@ type CRD struct { // Filename is the File obj Name including (sub-)chart.ChartFullPath Filename string // File is the File obj for the crd - File *File + File *common.File } // SetDependencies replaces the chart dependencies. @@ -137,8 +139,8 @@ func (ch *Chart) AppVersion() string { // CRDs returns a list of File objects in the 'crds/' directory of a Helm chart. // Deprecated: use CRDObjects() -func (ch *Chart) CRDs() []*File { - files := []*File{} +func (ch *Chart) CRDs() []*common.File { + files := []*common.File{} // Find all resources in the crds/ directory for _, f := range ch.Files { if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) { diff --git a/pkg/chart/v2/chart_test.go b/pkg/chart/v2/chart_test.go index d6311085b..a96d8c0c0 100644 --- a/pkg/chart/v2/chart_test.go +++ b/pkg/chart/v2/chart_test.go @@ -20,11 +20,13 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "helm.sh/helm/v4/pkg/chart/common" ) func TestCRDs(t *testing.T) { chrt := Chart{ - Files: []*File{ + Files: []*common.File{ { Name: "crds/foo.yaml", Data: []byte("hello"), @@ -57,7 +59,7 @@ func TestCRDs(t *testing.T) { func TestSaveChartNoRawData(t *testing.T) { chrt := Chart{ - Raw: []*File{ + Raw: []*common.File{ { Name: "fhqwhgads.yaml", Data: []byte("Everybody to the Limit"), @@ -76,7 +78,7 @@ func TestSaveChartNoRawData(t *testing.T) { t.Fatal(err) } - is.Equal([]*File(nil), res.Raw) + is.Equal([]*common.File(nil), res.Raw) } func TestMetadata(t *testing.T) { @@ -162,7 +164,7 @@ func TestChartFullPath(t *testing.T) { func TestCRDObjects(t *testing.T) { chrt := Chart{ - Files: []*File{ + Files: []*common.File{ { Name: "crds/foo.yaml", Data: []byte("hello"), @@ -190,7 +192,7 @@ func TestCRDObjects(t *testing.T) { { Name: "crds/foo.yaml", Filename: "crds/foo.yaml", - File: &File{ + File: &common.File{ Name: "crds/foo.yaml", Data: []byte("hello"), }, @@ -198,7 +200,7 @@ func TestCRDObjects(t *testing.T) { { Name: "crds/foo/bar/baz.yaml", Filename: "crds/foo/bar/baz.yaml", - File: &File{ + File: &common.File{ Name: "crds/foo/bar/baz.yaml", Data: []byte("hello"), }, diff --git a/pkg/lint/lint.go b/pkg/chart/v2/lint/lint.go similarity index 83% rename from pkg/lint/lint.go rename to pkg/chart/v2/lint/lint.go index 64b2a6057..773c9bc5e 100644 --- a/pkg/lint/lint.go +++ b/pkg/chart/v2/lint/lint.go @@ -14,24 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -package lint // import "helm.sh/helm/v4/pkg/lint" +package lint // import "helm.sh/helm/v4/pkg/chart/v2/lint" import ( "path/filepath" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" - "helm.sh/helm/v4/pkg/lint/rules" - "helm.sh/helm/v4/pkg/lint/support" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/v2/lint/rules" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" ) type linterOptions struct { - KubeVersion *chartutil.KubeVersion + KubeVersion *common.KubeVersion SkipSchemaValidation bool } type LinterOption func(lo *linterOptions) -func WithKubeVersion(kubeVersion *chartutil.KubeVersion) LinterOption { +func WithKubeVersion(kubeVersion *common.KubeVersion) LinterOption { return func(lo *linterOptions) { lo.KubeVersion = kubeVersion } diff --git a/pkg/lint/lint_test.go b/pkg/chart/v2/lint/lint_test.go similarity index 99% rename from pkg/lint/lint_test.go rename to pkg/chart/v2/lint/lint_test.go index 5b590c010..3c777e2bb 100644 --- a/pkg/lint/lint_test.go +++ b/pkg/chart/v2/lint/lint_test.go @@ -23,8 +23,8 @@ import ( "github.com/stretchr/testify/assert" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" - "helm.sh/helm/v4/pkg/lint/support" ) var values map[string]interface{} diff --git a/pkg/lint/rules/chartfile.go b/pkg/chart/v2/lint/rules/chartfile.go similarity index 98% rename from pkg/lint/rules/chartfile.go rename to pkg/chart/v2/lint/rules/chartfile.go index 103c28374..185f524a4 100644 --- a/pkg/lint/rules/chartfile.go +++ b/pkg/chart/v2/lint/rules/chartfile.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package rules // import "helm.sh/helm/v4/pkg/lint/rules" +package rules // import "helm.sh/helm/v4/pkg/chart/v2/lint/rules" import ( "errors" @@ -27,8 +27,8 @@ import ( "sigs.k8s.io/yaml" chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" - "helm.sh/helm/v4/pkg/lint/support" ) // Chartfile runs a set of linter rules related to Chart.yaml file diff --git a/pkg/lint/rules/chartfile_test.go b/pkg/chart/v2/lint/rules/chartfile_test.go similarity index 99% rename from pkg/lint/rules/chartfile_test.go rename to pkg/chart/v2/lint/rules/chartfile_test.go index 1719a2011..5a1ad2f24 100644 --- a/pkg/lint/rules/chartfile_test.go +++ b/pkg/chart/v2/lint/rules/chartfile_test.go @@ -24,8 +24,8 @@ import ( "testing" chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" - "helm.sh/helm/v4/pkg/lint/support" ) const ( diff --git a/pkg/lint/rules/crds.go b/pkg/chart/v2/lint/rules/crds.go similarity index 98% rename from pkg/lint/rules/crds.go rename to pkg/chart/v2/lint/rules/crds.go index 1b8a73139..49e30192a 100644 --- a/pkg/lint/rules/crds.go +++ b/pkg/chart/v2/lint/rules/crds.go @@ -28,8 +28,8 @@ import ( "k8s.io/apimachinery/pkg/util/yaml" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" "helm.sh/helm/v4/pkg/chart/v2/loader" - "helm.sh/helm/v4/pkg/lint/support" ) // Crds lints the CRDs in the Linter. diff --git a/pkg/lint/rules/crds_test.go b/pkg/chart/v2/lint/rules/crds_test.go similarity index 95% rename from pkg/lint/rules/crds_test.go rename to pkg/chart/v2/lint/rules/crds_test.go index d497b29ba..e644f182f 100644 --- a/pkg/lint/rules/crds_test.go +++ b/pkg/chart/v2/lint/rules/crds_test.go @@ -21,7 +21,7 @@ import ( "github.com/stretchr/testify/assert" - "helm.sh/helm/v4/pkg/lint/support" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" ) const invalidCrdsDir = "./testdata/invalidcrdsdir" diff --git a/pkg/lint/rules/dependencies.go b/pkg/chart/v2/lint/rules/dependencies.go similarity index 96% rename from pkg/lint/rules/dependencies.go rename to pkg/chart/v2/lint/rules/dependencies.go index 16c9d6435..d944a016d 100644 --- a/pkg/lint/rules/dependencies.go +++ b/pkg/chart/v2/lint/rules/dependencies.go @@ -14,15 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package rules // import "helm.sh/helm/v4/pkg/lint/rules" +package rules // import "helm.sh/helm/v4/pkg/chart/v2/lint/rules" import ( "fmt" "strings" chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" "helm.sh/helm/v4/pkg/chart/v2/loader" - "helm.sh/helm/v4/pkg/lint/support" ) // Dependencies runs lints against a chart's dependencies diff --git a/pkg/lint/rules/dependencies_test.go b/pkg/chart/v2/lint/rules/dependencies_test.go similarity index 98% rename from pkg/lint/rules/dependencies_test.go rename to pkg/chart/v2/lint/rules/dependencies_test.go index 1369b2372..08a6646cd 100644 --- a/pkg/lint/rules/dependencies_test.go +++ b/pkg/chart/v2/lint/rules/dependencies_test.go @@ -20,8 +20,8 @@ import ( "testing" chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" - "helm.sh/helm/v4/pkg/lint/support" ) func chartWithBadDependencies() chart.Chart { diff --git a/pkg/chart/v2/lint/rules/deprecations.go b/pkg/chart/v2/lint/rules/deprecations.go new file mode 100644 index 000000000..6eba316bc --- /dev/null +++ b/pkg/chart/v2/lint/rules/deprecations.go @@ -0,0 +1,106 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules // import "helm.sh/helm/v4/pkg/chart/v2/lint/rules" + +import ( + "fmt" + "strconv" + + "helm.sh/helm/v4/pkg/chart/common" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/endpoints/deprecation" + kscheme "k8s.io/client-go/kubernetes/scheme" +) + +var ( + // This should be set in the Makefile based on the version of client-go being imported. + // These constants will be overwritten with LDFLAGS. The version components must be + // strings in order for LDFLAGS to set them. + k8sVersionMajor = "1" + k8sVersionMinor = "20" +) + +// deprecatedAPIError indicates than an API is deprecated in Kubernetes +type deprecatedAPIError struct { + Deprecated string + Message string +} + +func (e deprecatedAPIError) Error() string { + msg := e.Message + return msg +} + +func validateNoDeprecations(resource *k8sYamlStruct, kubeVersion *common.KubeVersion) error { + // if `resource` does not have an APIVersion or Kind, we cannot test it for deprecation + if resource.APIVersion == "" { + return nil + } + if resource.Kind == "" { + return nil + } + + majorVersion := k8sVersionMajor + minorVersion := k8sVersionMinor + + if kubeVersion != nil { + majorVersion = kubeVersion.Major + minorVersion = kubeVersion.Minor + } + + runtimeObject, err := resourceToRuntimeObject(resource) + if err != nil { + // do not error for non-kubernetes resources + if runtime.IsNotRegisteredError(err) { + return nil + } + return err + } + + major, err := strconv.Atoi(majorVersion) + if err != nil { + return err + } + minor, err := strconv.Atoi(minorVersion) + if err != nil { + return err + } + + if !deprecation.IsDeprecated(runtimeObject, major, minor) { + return nil + } + gvk := fmt.Sprintf("%s %s", resource.APIVersion, resource.Kind) + return deprecatedAPIError{ + Deprecated: gvk, + Message: deprecation.WarningMessage(runtimeObject), + } +} + +func resourceToRuntimeObject(resource *k8sYamlStruct) (runtime.Object, error) { + scheme := runtime.NewScheme() + kscheme.AddToScheme(scheme) + + gvk := schema.FromAPIVersionAndKind(resource.APIVersion, resource.Kind) + out, err := scheme.New(gvk) + if err != nil { + return nil, err + } + out.GetObjectKind().SetGroupVersionKind(gvk) + return out, nil +} diff --git a/pkg/lint/rules/deprecations_test.go b/pkg/chart/v2/lint/rules/deprecations_test.go similarity index 94% rename from pkg/lint/rules/deprecations_test.go rename to pkg/chart/v2/lint/rules/deprecations_test.go index 6add843ce..e153f67e6 100644 --- a/pkg/lint/rules/deprecations_test.go +++ b/pkg/chart/v2/lint/rules/deprecations_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package rules // import "helm.sh/helm/v4/pkg/lint/rules" +package rules // import "helm.sh/helm/v4/pkg/chart/v2/lint/rules" import "testing" diff --git a/pkg/lint/rules/template.go b/pkg/chart/v2/lint/rules/template.go similarity index 95% rename from pkg/lint/rules/template.go rename to pkg/chart/v2/lint/rules/template.go index b36153ec6..5c84d0f68 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/chart/v2/lint/rules/template.go @@ -33,10 +33,12 @@ import ( "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/yaml" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/common/util" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" "helm.sh/helm/v4/pkg/chart/v2/loader" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/engine" - "helm.sh/helm/v4/pkg/lint/support" ) // Templates lints the templates in the Linter. @@ -45,12 +47,12 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace } // TemplatesWithKubeVersion lints the templates in the Linter, allowing to specify the kubernetes version. -func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion) { +func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion) { TemplatesWithSkipSchemaValidation(linter, values, namespace, kubeVersion, false) } // TemplatesWithSkipSchemaValidation lints the templates in the Linter, allowing to specify the kubernetes version and if schema validation is enabled or not. -func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion, skipSchemaValidation bool) { +func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion, skipSchemaValidation bool) { fpath := "templates/" templatesPath := filepath.Join(linter.ChartDir, fpath) @@ -74,12 +76,12 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string return } - options := chartutil.ReleaseOptions{ + options := common.ReleaseOptions{ Name: "test-release", Namespace: namespace, } - caps := chartutil.DefaultCapabilities.Copy() + caps := common.DefaultCapabilities.Copy() if kubeVersion != nil { caps.KubeVersion = *kubeVersion } @@ -90,12 +92,12 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string return } - cvals, err := chartutil.CoalesceValues(chart, values) + cvals, err := util.CoalesceValues(chart, values) if err != nil { return } - valuesToRender, err := chartutil.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, skipSchemaValidation) + valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, skipSchemaValidation) if err != nil { linter.RunLinterRule(support.ErrorSev, fpath, err) return diff --git a/pkg/lint/rules/template_test.go b/pkg/chart/v2/lint/rules/template_test.go similarity index 98% rename from pkg/lint/rules/template_test.go rename to pkg/chart/v2/lint/rules/template_test.go index 787bd6e4b..3e8e0b371 100644 --- a/pkg/lint/rules/template_test.go +++ b/pkg/chart/v2/lint/rules/template_test.go @@ -23,9 +23,10 @@ import ( "strings" "testing" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" - "helm.sh/helm/v4/pkg/lint/support" ) const templateTestBasedir = "./testdata/albatross" @@ -189,7 +190,7 @@ func TestDeprecatedAPIFails(t *testing.T) { Version: "0.1.0", Icon: "satisfy-the-linting-gods.gif", }, - Templates: []*chart.File{ + Templates: []*common.File{ { Name: "templates/baddeployment.yaml", Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"), @@ -249,7 +250,7 @@ func TestStrictTemplateParsingMapError(t *testing.T) { "key1": "val1", }, }, - Templates: []*chart.File{ + Templates: []*common.File{ { Name: "templates/configmap.yaml", Data: []byte(manifest), @@ -378,7 +379,7 @@ func TestEmptyWithCommentsManifests(t *testing.T) { Version: "0.1.0", Icon: "satisfy-the-linting-gods.gif", }, - Templates: []*chart.File{ + Templates: []*common.File{ { Name: "templates/empty-with-comments.yaml", Data: []byte("#@formatter:off\n"), diff --git a/pkg/lint/rules/testdata/albatross/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/albatross/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/albatross/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/albatross/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/albatross/templates/_helpers.tpl b/pkg/chart/v2/lint/rules/testdata/albatross/templates/_helpers.tpl new file mode 100644 index 000000000..24f76db73 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/albatross/templates/_helpers.tpl @@ -0,0 +1,16 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{define "name"}}{{default "nginx" .Values.nameOverride | trunc 63 | trimSuffix "-" }}{{end}} + +{{/* +Create a default fully qualified app name. + +We truncate at 63 chars because some Kubernetes name fields are limited to this +(by the DNS naming spec). +*/}} +{{define "fullname"}} +{{- $name := default "nginx" .Values.nameOverride -}} +{{printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{end}} diff --git a/pkg/chart/v2/lint/rules/testdata/albatross/templates/fail.yaml b/pkg/chart/v2/lint/rules/testdata/albatross/templates/fail.yaml new file mode 100644 index 000000000..a11e0e90e --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/albatross/templates/fail.yaml @@ -0,0 +1 @@ +{{ deliberateSyntaxError }} diff --git a/pkg/chart/v2/lint/rules/testdata/albatross/templates/svc.yaml b/pkg/chart/v2/lint/rules/testdata/albatross/templates/svc.yaml new file mode 100644 index 000000000..16bb27d55 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/albatross/templates/svc.yaml @@ -0,0 +1,19 @@ +# This is a service gateway to the replica set created by the deployment. +# Take a look at the deployment.yaml for general notes about this chart. +apiVersion: v1 +kind: Service +metadata: + name: "{{ .Values.name }}" + labels: + app.kubernetes.io/managed-by: {{ .Release.Service | quote }} + app.kubernetes.io/instance: {{ .Release.Name | quote }} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" + kubeVersion: {{ .Capabilities.KubeVersion.Major }} +spec: + ports: + - port: {{default 80 .Values.httpPort | quote}} + targetPort: 80 + protocol: TCP + name: http + selector: + app.kubernetes.io/name: {{template "fullname" .}} diff --git a/pkg/chart/v2/lint/rules/testdata/albatross/values.yaml b/pkg/chart/v2/lint/rules/testdata/albatross/values.yaml new file mode 100644 index 000000000..74cc6a0dc --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/albatross/values.yaml @@ -0,0 +1 @@ +name: "mariner" diff --git a/pkg/lint/rules/testdata/anotherbadchartfile/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/anotherbadchartfile/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/anotherbadchartfile/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/anotherbadchartfile/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/badchartfile/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/badchartfile/Chart.yaml new file mode 100644 index 000000000..3564ede3e --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/badchartfile/Chart.yaml @@ -0,0 +1,11 @@ +description: A Helm chart for Kubernetes +version: 0.0.0.0 +home: "" +type: application +dependencies: +- name: mariadb + version: 5.x.x + repository: https://charts.helm.sh/stable/ + condition: mariadb.enabled + tags: + - database diff --git a/pkg/chart/v2/lint/rules/testdata/badchartfile/values.yaml b/pkg/chart/v2/lint/rules/testdata/badchartfile/values.yaml new file mode 100644 index 000000000..9f367033b --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/badchartfile/values.yaml @@ -0,0 +1 @@ +# Default values for badchartfile. diff --git a/pkg/lint/rules/testdata/badchartname/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/badchartname/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/badchartname/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/badchartname/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/badchartname/values.yaml b/pkg/chart/v2/lint/rules/testdata/badchartname/values.yaml new file mode 100644 index 000000000..9f367033b --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/badchartname/values.yaml @@ -0,0 +1 @@ +# Default values for badchartfile. diff --git a/pkg/lint/rules/testdata/badcrdfile/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/badcrdfile/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/badcrdfile/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/badcrdfile/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml b/pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml new file mode 100644 index 000000000..468916053 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml @@ -0,0 +1,2 @@ +apiVersion: bad.k8s.io/v1beta1 +kind: CustomResourceDefinition diff --git a/pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml b/pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml new file mode 100644 index 000000000..523b97f85 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml @@ -0,0 +1,2 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: NotACustomResourceDefinition diff --git a/pkg/chart/v2/lint/rules/testdata/badcrdfile/templates/.gitkeep b/pkg/chart/v2/lint/rules/testdata/badcrdfile/templates/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/chart/v2/lint/rules/testdata/badcrdfile/values.yaml b/pkg/chart/v2/lint/rules/testdata/badcrdfile/values.yaml new file mode 100644 index 000000000..2fffc7715 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/badcrdfile/values.yaml @@ -0,0 +1 @@ +# Default values for badcrdfile. diff --git a/pkg/lint/rules/testdata/badvaluesfile/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/badvaluesfile/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/badvaluesfile/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/badvaluesfile/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml b/pkg/chart/v2/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml new file mode 100644 index 000000000..6c2ceb8db --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml @@ -0,0 +1,2 @@ +metadata: + name: {{.name | default "foo" | title}} diff --git a/pkg/chart/v2/lint/rules/testdata/badvaluesfile/values.yaml b/pkg/chart/v2/lint/rules/testdata/badvaluesfile/values.yaml new file mode 100644 index 000000000..b5a10271c --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/badvaluesfile/values.yaml @@ -0,0 +1,2 @@ +# Invalid value for badvaluesfile for testing lint fails with invalid yaml format +name= "value" diff --git a/pkg/lint/rules/testdata/goodone/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/goodone/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/goodone/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/goodone/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/goodone/crds/test-crd.yaml b/pkg/chart/v2/lint/rules/testdata/goodone/crds/test-crd.yaml new file mode 100644 index 000000000..1d7350f1d --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/goodone/crds/test-crd.yaml @@ -0,0 +1,19 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: tests.test.io +spec: + group: test.io + names: + kind: Test + listKind: TestList + plural: tests + singular: test + scope: Namespaced + versions: + - name : v1alpha2 + served: true + storage: true + - name : v1alpha1 + served: true + storage: false diff --git a/pkg/chart/v2/lint/rules/testdata/goodone/templates/goodone.yaml b/pkg/chart/v2/lint/rules/testdata/goodone/templates/goodone.yaml new file mode 100644 index 000000000..cd46f62c7 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/goodone/templates/goodone.yaml @@ -0,0 +1,2 @@ +metadata: + name: {{ .Values.name | default "foo" | lower }} diff --git a/pkg/chart/v2/lint/rules/testdata/goodone/values.yaml b/pkg/chart/v2/lint/rules/testdata/goodone/values.yaml new file mode 100644 index 000000000..92c3d9bb9 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/goodone/values.yaml @@ -0,0 +1 @@ +name: "goodone-here" diff --git a/pkg/chart/v2/lint/rules/testdata/invalidchartfile/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/invalidchartfile/Chart.yaml new file mode 100644 index 000000000..0fd58d1d4 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/invalidchartfile/Chart.yaml @@ -0,0 +1,6 @@ +name: some-chart +apiVersion: v2 +apiVersion: v1 +description: A Helm chart for Kubernetes +version: 1.3.0 +icon: http://example.com diff --git a/pkg/chart/v2/lint/rules/testdata/invalidchartfile/values.yaml b/pkg/chart/v2/lint/rules/testdata/invalidchartfile/values.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/lint/rules/testdata/invalidcrdsdir/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/invalidcrdsdir/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/crds b/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/crds new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/values.yaml b/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/values.yaml new file mode 100644 index 000000000..6b1611a64 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/values.yaml @@ -0,0 +1 @@ +# Default values for invalidcrdsdir. diff --git a/pkg/chart/v2/lint/rules/testdata/malformed-template/.helmignore b/pkg/chart/v2/lint/rules/testdata/malformed-template/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/malformed-template/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/pkg/lint/rules/testdata/malformed-template/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/malformed-template/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/malformed-template/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/malformed-template/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/malformed-template/templates/bad.yaml b/pkg/chart/v2/lint/rules/testdata/malformed-template/templates/bad.yaml new file mode 100644 index 000000000..213198fda --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/malformed-template/templates/bad.yaml @@ -0,0 +1 @@ +{ {- $relname := .Release.Name -}} diff --git a/pkg/chart/v2/lint/rules/testdata/malformed-template/values.yaml b/pkg/chart/v2/lint/rules/testdata/malformed-template/values.yaml new file mode 100644 index 000000000..1cc3182ea --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/malformed-template/values.yaml @@ -0,0 +1,82 @@ +# Default values for test. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/pkg/lint/rules/testdata/multi-template-fail/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/multi-template-fail/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/multi-template-fail/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/multi-template-fail/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml b/pkg/chart/v2/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml new file mode 100644 index 000000000..835be07be --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: game-config +data: + game.properties: cheat +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: -this:name-is-not_valid$ +data: + game.properties: empty diff --git a/pkg/lint/rules/testdata/v3-fail/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/v3-fail/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/v3-fail/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/v3-fail/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/_helpers.tpl b/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/_helpers.tpl new file mode 100644 index 000000000..0b89e723b --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/_helpers.tpl @@ -0,0 +1,63 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "v3-fail.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "v3-fail.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "v3-fail.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Common labels +*/}} +{{- define "v3-fail.labels" -}} +helm.sh/chart: {{ include "v3-fail.chart" . }} +{{ include "v3-fail.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{/* +Selector labels +*/}} +{{- define "v3-fail.selectorLabels" -}} +app.kubernetes.io/name: {{ include "v3-fail.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "v3-fail.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} + {{ default (include "v3-fail.fullname" .) .Values.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.serviceAccount.name }} +{{- end -}} +{{- end -}} diff --git a/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/deployment.yaml b/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/deployment.yaml new file mode 100644 index 000000000..6d651ab8e --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/deployment.yaml @@ -0,0 +1,56 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "v3-fail.fullname" . }} + labels: + nope: {{ .Release.Time }} + {{- include "v3-fail.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "v3-fail.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "v3-fail.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "v3-fail.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/ingress.yaml b/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/ingress.yaml new file mode 100644 index 000000000..4790650d0 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/ingress.yaml @@ -0,0 +1,62 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "v3-fail.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "v3-fail.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + "helm.sh/hook": crd-install + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/service.yaml b/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/service.yaml new file mode 100644 index 000000000..79a0f40b0 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "v3-fail.fullname" . }} + annotations: + helm.sh/hook: crd-install + labels: + {{- include "v3-fail.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "v3-fail.selectorLabels" . | nindent 4 }} diff --git a/pkg/chart/v2/lint/rules/testdata/v3-fail/values.yaml b/pkg/chart/v2/lint/rules/testdata/v3-fail/values.yaml new file mode 100644 index 000000000..01d99b4e6 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/v3-fail/values.yaml @@ -0,0 +1,66 @@ +# Default values for v3-fail. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: [] + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/pkg/lint/rules/testdata/withsubchart/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/withsubchart/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/withsubchart/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/withsubchart/Chart.yaml diff --git a/pkg/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml b/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml new file mode 100644 index 000000000..6cb6cc2af --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml @@ -0,0 +1,2 @@ +metadata: + name: {{ .Values.subchart.name | lower }} diff --git a/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/values.yaml b/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/values.yaml new file mode 100644 index 000000000..422a359d5 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/values.yaml @@ -0,0 +1,2 @@ +subchart: + name: subchart \ No newline at end of file diff --git a/pkg/chart/v2/lint/rules/testdata/withsubchart/templates/mainchart.yaml b/pkg/chart/v2/lint/rules/testdata/withsubchart/templates/mainchart.yaml new file mode 100644 index 000000000..6cb6cc2af --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/withsubchart/templates/mainchart.yaml @@ -0,0 +1,2 @@ +metadata: + name: {{ .Values.subchart.name | lower }} diff --git a/pkg/chart/v2/lint/rules/testdata/withsubchart/values.yaml b/pkg/chart/v2/lint/rules/testdata/withsubchart/values.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/lint/rules/values.go b/pkg/chart/v2/lint/rules/values.go similarity index 84% rename from pkg/lint/rules/values.go rename to pkg/chart/v2/lint/rules/values.go index 019e74fa7..5260bf8b3 100644 --- a/pkg/lint/rules/values.go +++ b/pkg/chart/v2/lint/rules/values.go @@ -21,8 +21,9 @@ import ( "os" "path/filepath" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" - "helm.sh/helm/v4/pkg/lint/support" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/common/util" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" ) // ValuesWithOverrides tests the values.yaml file. @@ -52,7 +53,7 @@ func validateValuesFileExistence(valuesPath string) error { } func validateValuesFile(valuesPath string, overrides map[string]interface{}) error { - values, err := chartutil.ReadValuesFile(valuesPath) + values, err := common.ReadValuesFile(valuesPath) if err != nil { return fmt.Errorf("unable to parse YAML: %w", err) } @@ -62,8 +63,8 @@ func validateValuesFile(valuesPath string, overrides map[string]interface{}) err // We could change that. For now, though, we retain that strategy, and thus can // coalesce tables (like reuse-values does) instead of doing the full chart // CoalesceValues - coalescedValues := chartutil.CoalesceTables(make(map[string]interface{}, len(overrides)), overrides) - coalescedValues = chartutil.CoalesceTables(coalescedValues, values) + coalescedValues := util.CoalesceTables(make(map[string]interface{}, len(overrides)), overrides) + coalescedValues = util.CoalesceTables(coalescedValues, values) ext := filepath.Ext(valuesPath) schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json" @@ -74,5 +75,5 @@ func validateValuesFile(valuesPath string, overrides map[string]interface{}) err if err != nil { return err } - return chartutil.ValidateAgainstSingleSchema(coalescedValues, schema) + return util.ValidateAgainstSingleSchema(coalescedValues, schema) } diff --git a/pkg/chart/v2/lint/rules/values_test.go b/pkg/chart/v2/lint/rules/values_test.go new file mode 100644 index 000000000..348695785 --- /dev/null +++ b/pkg/chart/v2/lint/rules/values_test.go @@ -0,0 +1,169 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + + "helm.sh/helm/v4/internal/test/ensure" +) + +var nonExistingValuesFilePath = filepath.Join("/fake/dir", "values.yaml") + +const testSchema = ` +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "helm values test schema", + "type": "object", + "additionalProperties": false, + "required": [ + "username", + "password" + ], + "properties": { + "username": { + "description": "Your username", + "type": "string" + }, + "password": { + "description": "Your password", + "type": "string" + } + } +} +` + +func TestValidateValuesYamlNotDirectory(t *testing.T) { + _ = os.Mkdir(nonExistingValuesFilePath, os.ModePerm) + defer os.Remove(nonExistingValuesFilePath) + + err := validateValuesFileExistence(nonExistingValuesFilePath) + if err == nil { + t.Errorf("validateValuesFileExistence to return a linter error, got no error") + } +} + +func TestValidateValuesFileWellFormed(t *testing.T) { + badYaml := ` + not:well[]{}formed + ` + tmpdir := ensure.TempFile(t, "values.yaml", []byte(badYaml)) + valfile := filepath.Join(tmpdir, "values.yaml") + if err := validateValuesFile(valfile, map[string]interface{}{}); err == nil { + t.Fatal("expected values file to fail parsing") + } +} + +func TestValidateValuesFileSchema(t *testing.T) { + yaml := "username: admin\npassword: swordfish" + tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + if err := validateValuesFile(valfile, map[string]interface{}{}); err != nil { + t.Fatalf("Failed validation with %s", err) + } +} + +func TestValidateValuesFileSchemaFailure(t *testing.T) { + // 1234 is an int, not a string. This should fail. + yaml := "username: 1234\npassword: swordfish" + tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + + err := validateValuesFile(valfile, map[string]interface{}{}) + if err == nil { + t.Fatal("expected values file to fail parsing") + } + + assert.Contains(t, err.Error(), "- at '/username': got number, want string") +} + +func TestValidateValuesFileSchemaOverrides(t *testing.T) { + yaml := "username: admin" + overrides := map[string]interface{}{ + "password": "swordfish", + } + tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + if err := validateValuesFile(valfile, overrides); err != nil { + t.Fatalf("Failed validation with %s", err) + } +} + +func TestValidateValuesFile(t *testing.T) { + tests := []struct { + name string + yaml string + overrides map[string]interface{} + errorMessage string + }{ + { + name: "value added", + yaml: "username: admin", + overrides: map[string]interface{}{"password": "swordfish"}, + }, + { + name: "value not overridden", + yaml: "username: admin\npassword:", + overrides: map[string]interface{}{"username": "anotherUser"}, + errorMessage: "- at '/password': got null, want string", + }, + { + name: "value overridden", + yaml: "username: admin\npassword:", + overrides: map[string]interface{}{"username": "anotherUser", "password": "swordfish"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpdir := ensure.TempFile(t, "values.yaml", []byte(tt.yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + + err := validateValuesFile(valfile, tt.overrides) + + switch { + case err != nil && tt.errorMessage == "": + t.Errorf("Failed validation with %s", err) + case err == nil && tt.errorMessage != "": + t.Error("expected values file to fail parsing") + case err != nil && tt.errorMessage != "": + assert.Contains(t, err.Error(), tt.errorMessage, "Failed with unexpected error") + } + }) + } +} + +func createTestingSchema(t *testing.T, dir string) string { + t.Helper() + schemafile := filepath.Join(dir, "values.schema.json") + if err := os.WriteFile(schemafile, []byte(testSchema), 0700); err != nil { + t.Fatalf("Failed to write schema to tmpdir: %s", err) + } + return schemafile +} diff --git a/pkg/lint/support/doc.go b/pkg/chart/v2/lint/support/doc.go similarity index 91% rename from pkg/lint/support/doc.go rename to pkg/chart/v2/lint/support/doc.go index b007804dc..7e050b8c2 100644 --- a/pkg/lint/support/doc.go +++ b/pkg/chart/v2/lint/support/doc.go @@ -20,4 +20,4 @@ Package support contains tools for linting charts. Linting is the process of testing charts for errors or warnings regarding formatting, compilation, or standards compliance. */ -package support // import "helm.sh/helm/v4/pkg/lint/support" +package support // import "helm.sh/helm/v4/pkg/chart/v2/lint/support" diff --git a/pkg/chart/v2/lint/support/message.go b/pkg/chart/v2/lint/support/message.go new file mode 100644 index 000000000..5efbc7a61 --- /dev/null +++ b/pkg/chart/v2/lint/support/message.go @@ -0,0 +1,76 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package support + +import "fmt" + +// Severity indicates the severity of a Message. +const ( + // UnknownSev indicates that the severity of the error is unknown, and should not stop processing. + UnknownSev = iota + // InfoSev indicates information, for example missing values.yaml file + InfoSev + // WarningSev indicates that something does not meet code standards, but will likely function. + WarningSev + // ErrorSev indicates that something will not likely function. + ErrorSev +) + +// sev matches the *Sev states. +var sev = []string{"UNKNOWN", "INFO", "WARNING", "ERROR"} + +// Linter encapsulates a linting run of a particular chart. +type Linter struct { + Messages []Message + // The highest severity of all the failing lint rules + HighestSeverity int + ChartDir string +} + +// Message describes an error encountered while linting. +type Message struct { + // Severity is one of the *Sev constants + Severity int + Path string + Err error +} + +func (m Message) Error() string { + return fmt.Sprintf("[%s] %s: %s", sev[m.Severity], m.Path, m.Err.Error()) +} + +// NewMessage creates a new Message struct +func NewMessage(severity int, path string, err error) Message { + return Message{Severity: severity, Path: path, Err: err} +} + +// RunLinterRule returns true if the validation passed +func (l *Linter) RunLinterRule(severity int, path string, err error) bool { + // severity is out of bound + if severity < 0 || severity >= len(sev) { + return false + } + + if err != nil { + l.Messages = append(l.Messages, NewMessage(severity, path, err)) + + if severity > l.HighestSeverity { + l.HighestSeverity = severity + } + } + return err == nil +} diff --git a/pkg/chart/v2/lint/support/message_test.go b/pkg/chart/v2/lint/support/message_test.go new file mode 100644 index 000000000..ce5b5e42e --- /dev/null +++ b/pkg/chart/v2/lint/support/message_test.go @@ -0,0 +1,79 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package support + +import ( + "errors" + "testing" +) + +var errLint = errors.New("lint failed") + +func TestRunLinterRule(t *testing.T) { + var tests = []struct { + Severity int + LintError error + ExpectedMessages int + ExpectedReturn bool + ExpectedHighestSeverity int + }{ + {InfoSev, errLint, 1, false, InfoSev}, + {WarningSev, errLint, 2, false, WarningSev}, + {ErrorSev, errLint, 3, false, ErrorSev}, + // No error so it returns true + {ErrorSev, nil, 3, true, ErrorSev}, + // Retains highest severity + {InfoSev, errLint, 4, false, ErrorSev}, + // Invalid severity values + {4, errLint, 4, false, ErrorSev}, + {22, errLint, 4, false, ErrorSev}, + {-1, errLint, 4, false, ErrorSev}, + } + + linter := Linter{} + for _, test := range tests { + isValid := linter.RunLinterRule(test.Severity, "chart", test.LintError) + if len(linter.Messages) != test.ExpectedMessages { + t.Errorf("RunLinterRule(%d, \"chart\", %v), linter.Messages should now have %d message, we got %d", test.Severity, test.LintError, test.ExpectedMessages, len(linter.Messages)) + } + + if linter.HighestSeverity != test.ExpectedHighestSeverity { + t.Errorf("RunLinterRule(%d, \"chart\", %v), linter.HighestSeverity should be %d, we got %d", test.Severity, test.LintError, test.ExpectedHighestSeverity, linter.HighestSeverity) + } + + if isValid != test.ExpectedReturn { + t.Errorf("RunLinterRule(%d, \"chart\", %v), should have returned %t but returned %t", test.Severity, test.LintError, test.ExpectedReturn, isValid) + } + } +} + +func TestMessage(t *testing.T) { + m := Message{ErrorSev, "Chart.yaml", errors.New("Foo")} + if m.Error() != "[ERROR] Chart.yaml: Foo" { + t.Errorf("Unexpected output: %s", m.Error()) + } + + m = Message{WarningSev, "templates/", errors.New("Bar")} + if m.Error() != "[WARNING] templates/: Bar" { + t.Errorf("Unexpected output: %s", m.Error()) + } + + m = Message{InfoSev, "templates/rc.yaml", errors.New("FooBar")} + if m.Error() != "[INFO] templates/rc.yaml: FooBar" { + t.Errorf("Unexpected output: %s", m.Error()) + } +} diff --git a/pkg/chart/v2/loader/load.go b/pkg/chart/v2/loader/load.go index 75c73e959..0c025e183 100644 --- a/pkg/chart/v2/loader/load.go +++ b/pkg/chart/v2/loader/load.go @@ -31,6 +31,7 @@ import ( utilyaml "k8s.io/apimachinery/pkg/util/yaml" "sigs.k8s.io/yaml" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" ) @@ -80,7 +81,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { // do not rely on assumed ordering of files in the chart and crash // if Chart.yaml was not coming early enough to initialize metadata for _, f := range files { - c.Raw = append(c.Raw, &chart.File{Name: f.Name, Data: f.Data}) + c.Raw = append(c.Raw, &common.File{Name: f.Name, Data: f.Data}) if f.Name == "Chart.yaml" { if c.Metadata == nil { c.Metadata = new(chart.Metadata) @@ -128,7 +129,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { return c, fmt.Errorf("cannot load requirements.yaml: %w", err) } if c.Metadata.APIVersion == chart.APIVersionV1 { - c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) } // Deprecated: requirements.lock is deprecated use Chart.lock. case f.Name == "requirements.lock": @@ -143,14 +144,14 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { log.Printf("Warning: Dependency locking is handled in Chart.lock since apiVersion \"v2\". We recommend migrating to Chart.lock.") } if c.Metadata.APIVersion == chart.APIVersionV1 { - c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) } case strings.HasPrefix(f.Name, "templates/"): - c.Templates = append(c.Templates, &chart.File{Name: f.Name, Data: f.Data}) + c.Templates = append(c.Templates, &common.File{Name: f.Name, Data: f.Data}) case strings.HasPrefix(f.Name, "charts/"): if filepath.Ext(f.Name) == ".prov" { - c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) continue } @@ -158,7 +159,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { cname := strings.SplitN(fname, "/", 2)[0] subcharts[cname] = append(subcharts[cname], &BufferedFile{Name: fname, Data: f.Data}) default: - c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) } } diff --git a/pkg/chart/v2/loader/load_test.go b/pkg/chart/v2/loader/load_test.go index 41154421c..c4ae646f6 100644 --- a/pkg/chart/v2/loader/load_test.go +++ b/pkg/chart/v2/loader/load_test.go @@ -30,6 +30,7 @@ import ( "testing" "time" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" ) @@ -543,7 +544,7 @@ foo: } } -func TestMergeValues(t *testing.T) { +func TestMergeValuesV2(t *testing.T) { nestedMap := map[string]interface{}{ "foo": "bar", "baz": map[string]string{ @@ -753,7 +754,7 @@ func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) { } } -func verifyBomStripped(t *testing.T, files []*chart.File) { +func verifyBomStripped(t *testing.T, files []*common.File) { t.Helper() for _, file := range files { if bytes.HasPrefix(file.Data, utf8bom) { diff --git a/pkg/chart/v2/util/create.go b/pkg/chart/v2/util/create.go index a8ae3ab40..d7c1fe31c 100644 --- a/pkg/chart/v2/util/create.go +++ b/pkg/chart/v2/util/create.go @@ -26,6 +26,7 @@ import ( "sigs.k8s.io/yaml" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/chart/v2/loader" ) @@ -655,11 +656,11 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error { schart.Metadata = chartfile - var updatedTemplates []*chart.File + var updatedTemplates []*common.File for _, template := range schart.Templates { newData := transform(string(template.Data), schart.Name()) - updatedTemplates = append(updatedTemplates, &chart.File{Name: template.Name, Data: newData}) + updatedTemplates = append(updatedTemplates, &common.File{Name: template.Name, Data: newData}) } schart.Templates = updatedTemplates diff --git a/pkg/chart/v2/util/dependencies.go b/pkg/chart/v2/util/dependencies.go index 1a2aa1c95..a52f09f82 100644 --- a/pkg/chart/v2/util/dependencies.go +++ b/pkg/chart/v2/util/dependencies.go @@ -22,11 +22,13 @@ import ( "github.com/mitchellh/copystructure" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/common/util" chart "helm.sh/helm/v4/pkg/chart/v2" ) // ProcessDependencies checks through this chart's dependencies, processing accordingly. -func ProcessDependencies(c *chart.Chart, v Values) error { +func ProcessDependencies(c *chart.Chart, v common.Values) error { if err := processDependencyEnabled(c, v, ""); err != nil { return err } @@ -34,7 +36,7 @@ func ProcessDependencies(c *chart.Chart, v Values) error { } // processDependencyConditions disables charts based on condition path value in values -func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath string) { +func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values, cpath string) { if reqs == nil { return } @@ -50,7 +52,7 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath s break } slog.Warn("returned non-bool value", "path", c, "chart", r.Name) - } else if _, ok := err.(ErrNoValue); !ok { + } else if _, ok := err.(common.ErrNoValue); !ok { // this is a real error slog.Warn("the method PathValue returned error", slog.Any("error", err)) } @@ -60,7 +62,7 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath s } // processDependencyTags disables charts based on tags in values -func processDependencyTags(reqs []*chart.Dependency, cvals Values) { +func processDependencyTags(reqs []*chart.Dependency, cvals common.Values) { if reqs == nil { return } @@ -177,7 +179,7 @@ Loop: for _, lr := range c.Metadata.Dependencies { lr.Enabled = true } - cvals, err := CoalesceValues(c, v) + cvals, err := util.CoalesceValues(c, v) if err != nil { return err } @@ -232,6 +234,8 @@ func pathToMap(path string, data map[string]interface{}) map[string]interface{} return set(parsePath(path), data) } +func parsePath(key string) []string { return strings.Split(key, ".") } + func set(path []string, data map[string]interface{}) map[string]interface{} { if len(path) == 0 { return nil @@ -249,12 +253,12 @@ func processImportValues(c *chart.Chart, merge bool) error { return nil } // combine chart values and empty config to get Values - var cvals Values + var cvals common.Values var err error if merge { - cvals, err = MergeValues(c, nil) + cvals, err = util.MergeValues(c, nil) } else { - cvals, err = CoalesceValues(c, nil) + cvals, err = util.CoalesceValues(c, nil) } if err != nil { return err @@ -282,9 +286,9 @@ func processImportValues(c *chart.Chart, merge bool) error { } // create value map from child to be merged into parent if merge { - b = MergeTables(b, pathToMap(parent, vv.AsMap())) + b = util.MergeTables(b, pathToMap(parent, vv.AsMap())) } else { - b = CoalesceTables(b, pathToMap(parent, vv.AsMap())) + b = util.CoalesceTables(b, pathToMap(parent, vv.AsMap())) } case string: child := "exports." + iv @@ -298,9 +302,9 @@ func processImportValues(c *chart.Chart, merge bool) error { continue } if merge { - b = MergeTables(b, vm.AsMap()) + b = util.MergeTables(b, vm.AsMap()) } else { - b = CoalesceTables(b, vm.AsMap()) + b = util.CoalesceTables(b, vm.AsMap()) } } } @@ -315,14 +319,14 @@ func processImportValues(c *chart.Chart, merge bool) error { // deep copying the cvals as there are cases where pointers can end // up in the cvals when they are copied onto b in ways that break things. cvals = deepCopyMap(cvals) - c.Values = MergeTables(cvals, b) + c.Values = util.MergeTables(cvals, b) } else { // Trimming the nil values from cvals is needed for backwards compatibility. // Previously, the b value had been populated with cvals along with some // overrides. This caused the coalescing functionality to remove the // nil/null values. This trimming is for backwards compat. cvals = trimNilValues(cvals) - c.Values = CoalesceTables(cvals, b) + c.Values = util.CoalesceTables(cvals, b) } return nil @@ -355,6 +359,12 @@ func trimNilValues(vals map[string]interface{}) map[string]interface{} { return valsCopyMap } +// istable is a special-purpose function to see if the present thing matches the definition of a YAML table. +func istable(v interface{}) bool { + _, ok := v.(map[string]interface{}) + return ok +} + // processDependencyImportValues imports specified chart values from child to parent. func processDependencyImportValues(c *chart.Chart, merge bool) error { for _, d := range c.Dependencies() { diff --git a/pkg/chart/v2/util/dependencies_test.go b/pkg/chart/v2/util/dependencies_test.go index d645d7bf5..c817b0b89 100644 --- a/pkg/chart/v2/util/dependencies_test.go +++ b/pkg/chart/v2/util/dependencies_test.go @@ -21,6 +21,7 @@ import ( "strconv" "testing" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/chart/v2/loader" ) @@ -221,7 +222,7 @@ func TestProcessDependencyImportValues(t *testing.T) { if err := processDependencyImportValues(c, false); err != nil { t.Fatalf("processing import values dependencies %v", err) } - cc := Values(c.Values) + cc := common.Values(c.Values) for kk, vv := range e { pv, err := cc.PathValue(kk) if err != nil { @@ -251,7 +252,7 @@ func TestProcessDependencyImportValues(t *testing.T) { t.Error("expect nil value not found but found it") } switch xerr := err.(type) { - case ErrNoValue: + case common.ErrNoValue: // We found what we expected default: t.Errorf("expected an ErrNoValue but got %q instead", xerr) @@ -261,7 +262,7 @@ func TestProcessDependencyImportValues(t *testing.T) { if err := processDependencyImportValues(c, true); err != nil { t.Fatalf("processing import values dependencies %v", err) } - cc = Values(c.Values) + cc = common.Values(c.Values) val, err := cc.PathValue("ensurenull") if err != nil { t.Error("expect value but ensurenull was not found") @@ -291,7 +292,7 @@ func TestProcessDependencyImportValuesFromSharedDependencyToAliases(t *testing.T e["foo.grandchild.defaults.defaultValue"] = "42" e["bar.grandchild.defaults.defaultValue"] = "42" - cValues := Values(c.Values) + cValues := common.Values(c.Values) for kk, vv := range e { pv, err := cValues.PathValue(kk) if err != nil { @@ -329,7 +330,7 @@ func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) { if err := processDependencyImportValues(c, true); err != nil { t.Fatalf("processing import values dependencies %v", err) } - cc := Values(c.Values) + cc := common.Values(c.Values) for kk, vv := range e { pv, err := cc.PathValue(kk) if err != nil { diff --git a/pkg/chart/v2/util/save.go b/pkg/chart/v2/util/save.go index 624a5b562..69a98924c 100644 --- a/pkg/chart/v2/util/save.go +++ b/pkg/chart/v2/util/save.go @@ -29,6 +29,7 @@ import ( "sigs.k8s.io/yaml" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" ) @@ -76,7 +77,7 @@ func SaveDir(c *chart.Chart, dest string) error { } // Save templates and files - for _, o := range [][]*chart.File{c.Templates, c.Files} { + for _, o := range [][]*common.File{c.Templates, c.Files} { for _, f := range o { n := filepath.Join(outdir, f.Name) if err := writeFile(n, f.Data); err != nil { @@ -258,7 +259,7 @@ func validateName(name string) error { nname := filepath.Base(name) if nname != name { - return ErrInvalidChartName{name} + return common.ErrInvalidChartName{Name: name} } return nil diff --git a/pkg/chart/v2/util/save_test.go b/pkg/chart/v2/util/save_test.go index ff96331b5..ef822a82a 100644 --- a/pkg/chart/v2/util/save_test.go +++ b/pkg/chart/v2/util/save_test.go @@ -29,6 +29,7 @@ import ( "testing" "time" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/chart/v2/loader" ) @@ -47,7 +48,7 @@ func TestSave(t *testing.T) { Lock: &chart.Lock{ Digest: "testdigest", }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, Schema: []byte("{\n \"title\": \"Values\"\n}"), @@ -116,7 +117,7 @@ func TestSave(t *testing.T) { Lock: &chart.Lock{ Digest: "testdigest", }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, } @@ -156,7 +157,7 @@ func TestSavePreservesTimestamps(t *testing.T) { "imageName": "testimage", "imageId": 42, }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, Schema: []byte("{\n \"title\": \"Values\"\n}"), @@ -222,10 +223,10 @@ func TestSaveDir(t *testing.T) { Name: "ahab", Version: "1.2.3", }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), Data: []byte("abc: {{ .Values.abc }}")}, }, } diff --git a/pkg/cli/values/options_test.go b/pkg/cli/values/options_test.go index 4dbc709f1..fe1afc5d2 100644 --- a/pkg/cli/values/options_test.go +++ b/pkg/cli/values/options_test.go @@ -294,7 +294,7 @@ func TestReadFileOriginal(t *testing.T) { } } -func TestMergeValues(t *testing.T) { +func TestMergeValuesCLI(t *testing.T) { tests := []struct { name string opts Options diff --git a/pkg/cmd/helpers_test.go b/pkg/cmd/helpers_test.go index 40478c30e..55e3a842f 100644 --- a/pkg/cmd/helpers_test.go +++ b/pkg/cmd/helpers_test.go @@ -28,7 +28,7 @@ import ( "helm.sh/helm/v4/internal/test" "helm.sh/helm/v4/pkg/action" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + "helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/cli" kubefake "helm.sh/helm/v4/pkg/kube/fake" release "helm.sh/helm/v4/pkg/release/v1" @@ -91,7 +91,7 @@ func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string) actionConfig := &action.Configuration{ Releases: store, KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard}, - Capabilities: chartutil.DefaultCapabilities, + Capabilities: common.DefaultCapabilities, } root, err := newRootCmdWithConfig(actionConfig, buf, args, SetupLogging) diff --git a/pkg/cmd/lint.go b/pkg/cmd/lint.go index 78083a7ea..71540f1be 100644 --- a/pkg/cmd/lint.go +++ b/pkg/cmd/lint.go @@ -27,10 +27,10 @@ import ( "github.com/spf13/cobra" "helm.sh/helm/v4/pkg/action" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" "helm.sh/helm/v4/pkg/cli/values" "helm.sh/helm/v4/pkg/getter" - "helm.sh/helm/v4/pkg/lint/support" ) var longLintHelp = ` @@ -58,7 +58,7 @@ func newLintCmd(out io.Writer) *cobra.Command { } if kubeVersion != "" { - parsedKubeVersion, err := chartutil.ParseKubeVersion(kubeVersion) + parsedKubeVersion, err := common.ParseKubeVersion(kubeVersion) if err != nil { return fmt.Errorf("invalid kube version '%s': %s", kubeVersion, err) } diff --git a/pkg/cmd/status.go b/pkg/cmd/status.go index aa836f9f3..3d1309c3e 100644 --- a/pkg/cmd/status.go +++ b/pkg/cmd/status.go @@ -30,7 +30,7 @@ import ( coloroutput "helm.sh/helm/v4/internal/cli/output" "helm.sh/helm/v4/pkg/action" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + "helm.sh/helm/v4/pkg/chart/common/util" "helm.sh/helm/v4/pkg/cli/output" "helm.sh/helm/v4/pkg/cmd/require" release "helm.sh/helm/v4/pkg/release/v1" @@ -197,7 +197,7 @@ func (s statusPrinter) WriteTable(out io.Writer) error { // Print an extra newline _, _ = fmt.Fprintln(out) - cfg, err := chartutil.CoalesceValues(s.release.Chart, s.release.Config) + cfg, err := util.CoalesceValues(s.release.Chart, s.release.Config) if err != nil { return err } diff --git a/pkg/cmd/template.go b/pkg/cmd/template.go index aaf848c9e..81c112d51 100644 --- a/pkg/cmd/template.go +++ b/pkg/cmd/template.go @@ -35,7 +35,7 @@ import ( "github.com/spf13/cobra" "helm.sh/helm/v4/pkg/action" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + "helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/cli/values" "helm.sh/helm/v4/pkg/cmd/require" releaseutil "helm.sh/helm/v4/pkg/release/v1/util" @@ -69,7 +69,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { }, RunE: func(_ *cobra.Command, args []string) error { if kubeVersion != "" { - parsedKubeVersion, err := chartutil.ParseKubeVersion(kubeVersion) + parsedKubeVersion, err := common.ParseKubeVersion(kubeVersion) if err != nil { return fmt.Errorf("invalid kube version '%s': %s", kubeVersion, err) } @@ -93,7 +93,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client.ReleaseName = "release-name" client.Replace = true // Skip the name check client.ClientOnly = !validate - client.APIVersions = chartutil.VersionSet(extraAPIs) + client.APIVersions = common.VersionSet(extraAPIs) client.IncludeCRDs = includeCrds rel, err := runInstall(args, client, valueOpts, out) diff --git a/pkg/cmd/upgrade_test.go b/pkg/cmd/upgrade_test.go index d7375dcad..9b17f187d 100644 --- a/pkg/cmd/upgrade_test.go +++ b/pkg/cmd/upgrade_test.go @@ -24,6 +24,7 @@ import ( "strings" "testing" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/chart/v2/loader" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" @@ -382,7 +383,7 @@ func prepareMockRelease(t *testing.T, releaseName string) (func(n string, v int, Description: "A Helm chart for Kubernetes", Version: "0.1.0", }, - Templates: []*chart.File{{Name: "templates/configmap.yaml", Data: configmapData}}, + Templates: []*common.File{{Name: "templates/configmap.yaml", Data: configmapData}}, } chartPath := filepath.Join(tmpChart, cfile.Metadata.Name) if err := chartutil.SaveDir(cfile, tmpChart); err != nil { @@ -490,7 +491,7 @@ func prepareMockReleaseWithSecret(t *testing.T, releaseName string) (func(n stri Description: "A Helm chart for Kubernetes", Version: "0.1.0", }, - Templates: []*chart.File{{Name: "templates/configmap.yaml", Data: configmapData}, {Name: "templates/secret.yaml", Data: secretData}}, + Templates: []*common.File{{Name: "templates/configmap.yaml", Data: configmapData}, {Name: "templates/secret.yaml", Data: secretData}}, } chartPath := filepath.Join(tmpChart, cfile.Metadata.Name) if err := chartutil.SaveDir(cfile, tmpChart); err != nil { diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 6e47a0e39..a0ca17f08 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -30,8 +30,8 @@ import ( "k8s.io/client-go/rest" - chart "helm.sh/helm/v4/pkg/chart/v2" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + ci "helm.sh/helm/v4/pkg/chart" + "helm.sh/helm/v4/pkg/chart/common" ) // taken from https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=141 @@ -88,21 +88,21 @@ func New(config *rest.Config) Engine { // that section of the values will be passed into the "foo" chart. And if that // section contains a value named "bar", that value will be passed on to the // bar chart during render time. -func (e Engine) Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) { +func (e Engine) Render(chrt ci.Charter, values common.Values) (map[string]string, error) { tmap := allTemplates(chrt, values) return e.render(tmap) } // Render takes a chart, optional values, and value overrides, and attempts to // render the Go templates using the default options. -func Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) { +func Render(chrt ci.Charter, values common.Values) (map[string]string, error) { return new(Engine).Render(chrt, values) } // RenderWithClient takes a chart, optional values, and value overrides, and attempts to // render the Go templates using the default options. This engine is client aware and so can have template // functions that interact with the client. -func RenderWithClient(chrt *chart.Chart, values chartutil.Values, config *rest.Config) (map[string]string, error) { +func RenderWithClient(chrt ci.Charter, values common.Values, config *rest.Config) (map[string]string, error) { var clientProvider ClientProvider = clientProviderFromConfig{config} return Engine{ clientProvider: &clientProvider, @@ -113,7 +113,7 @@ func RenderWithClient(chrt *chart.Chart, values chartutil.Values, config *rest.C // render the Go templates using the default options. This engine is client aware and so can have template // functions that interact with the client. // This function differs from RenderWithClient in that it lets you customize the way a dynamic client is constructed. -func RenderWithClientProvider(chrt *chart.Chart, values chartutil.Values, clientProvider ClientProvider) (map[string]string, error) { +func RenderWithClientProvider(chrt ci.Charter, values common.Values, clientProvider ClientProvider) (map[string]string, error) { return Engine{ clientProvider: &clientProvider, }.Render(chrt, values) @@ -124,7 +124,7 @@ type renderable struct { // tpl is the current template. tpl string // vals are the values to be supplied to the template. - vals chartutil.Values + vals common.Values // namespace prefix to the templates of the current chart basePath string } @@ -312,7 +312,7 @@ func (e Engine) render(tpls map[string]renderable) (rendered map[string]string, } // At render time, add information about the template that is being rendered. vals := tpls[filename].vals - vals["Template"] = chartutil.Values{"Name": filename, "BasePath": tpls[filename].basePath} + vals["Template"] = common.Values{"Name": filename, "BasePath": tpls[filename].basePath} var buf strings.Builder if err := t.ExecuteTemplate(&buf, filename, vals); err != nil { return map[string]string{}, reformatExecErrorMsg(filename, err) @@ -455,7 +455,7 @@ func (p byPathLen) Less(i, j int) bool { // allTemplates returns all templates for a chart and its dependencies. // // As it goes, it also prepares the values in a scope-sensitive manner. -func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable { +func allTemplates(c ci.Charter, vals common.Values) map[string]renderable { templates := make(map[string]renderable) recAllTpls(c, templates, vals) return templates @@ -465,40 +465,46 @@ func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable { // // As it recurses, it also sets the values to be appropriate for the template // scope. -func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values) map[string]interface{} { +func recAllTpls(c ci.Charter, templates map[string]renderable, values common.Values) map[string]interface{} { + vals := values.AsMap() subCharts := make(map[string]interface{}) - chartMetaData := struct { - chart.Metadata - IsRoot bool - }{*c.Metadata, c.IsRoot()} + accessor, err := ci.NewAccessor(c) + if err != nil { + slog.Error("error accessing chart", "error", err) + } + chartMetaData := accessor.MetadataAsMap() + fmt.Printf("metadata: %v\n", chartMetaData) + chartMetaData["IsRoot"] = accessor.IsRoot() next := map[string]interface{}{ "Chart": chartMetaData, - "Files": newFiles(c.Files), + "Files": newFiles(accessor.Files()), "Release": vals["Release"], "Capabilities": vals["Capabilities"], - "Values": make(chartutil.Values), + "Values": make(common.Values), "Subcharts": subCharts, } // If there is a {{.Values.ThisChart}} in the parent metadata, // copy that into the {{.Values}} for this template. - if c.IsRoot() { + if accessor.IsRoot() { next["Values"] = vals["Values"] - } else if vs, err := vals.Table("Values." + c.Name()); err == nil { + } else if vs, err := values.Table("Values." + accessor.Name()); err == nil { next["Values"] = vs } - for _, child := range c.Dependencies() { - subCharts[child.Name()] = recAllTpls(child, templates, next) + for _, child := range accessor.Dependencies() { + // TODO: Handle error + sub, _ := ci.NewAccessor(child) + subCharts[sub.Name()] = recAllTpls(child, templates, next) } - newParentID := c.ChartFullPath() - for _, t := range c.Templates { + newParentID := accessor.ChartFullPath() + for _, t := range accessor.Templates() { if t == nil { continue } - if !isTemplateValid(c, t.Name) { + if !isTemplateValid(accessor, t.Name) { continue } templates[path.Join(newParentID, t.Name)] = renderable{ @@ -512,14 +518,9 @@ func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil. } // isTemplateValid returns true if the template is valid for the chart type -func isTemplateValid(ch *chart.Chart, templateName string) bool { - if isLibraryChart(ch) { +func isTemplateValid(accessor ci.Accessor, templateName string) bool { + if accessor.IsLibraryChart() { return strings.HasPrefix(filepath.Base(templateName), "_") } return true } - -// isLibraryChart returns true if the chart is a library chart -func isLibraryChart(c *chart.Chart) bool { - return strings.EqualFold(c.Metadata.Type, "library") -} diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index f4228fbd7..7ac892cec 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -32,8 +32,9 @@ import ( "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/fake" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/common/util" chart "helm.sh/helm/v4/pkg/chart/v2" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" ) func TestSortTemplates(t *testing.T) { @@ -94,7 +95,7 @@ func TestRender(t *testing.T) { Name: "moby", Version: "1.2.3", }, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/test1", Data: []byte("{{.Values.outer | title }} {{.Values.inner | title}}")}, {Name: "templates/test2", Data: []byte("{{.Values.global.callme | lower }}")}, {Name: "templates/test3", Data: []byte("{{.noValue}}")}, @@ -114,7 +115,7 @@ func TestRender(t *testing.T) { }, } - v, err := chartutil.CoalesceValues(c, vals) + v, err := util.CoalesceValues(c, vals) if err != nil { t.Fatalf("Failed to coalesce values: %s", err) } @@ -144,7 +145,7 @@ func TestRenderRefsOrdering(t *testing.T) { Name: "parent", Version: "1.2.3", }, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}parent value{{- end -}}`)}, {Name: "templates/test.yaml", Data: []byte(`{{ tpl "{{ include \"test\" . }}" . }}`)}, }, @@ -154,7 +155,7 @@ func TestRenderRefsOrdering(t *testing.T) { Name: "child", Version: "1.2.3", }, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}child value{{- end -}}`)}, }, } @@ -165,7 +166,7 @@ func TestRenderRefsOrdering(t *testing.T) { } for i := 0; i < 100; i++ { - out, err := Render(parentChart, chartutil.Values{}) + out, err := Render(parentChart, common.Values{}) if err != nil { t.Fatalf("Failed to render templates: %s", err) } @@ -181,7 +182,7 @@ func TestRenderRefsOrdering(t *testing.T) { func TestRenderInternals(t *testing.T) { // Test the internals of the rendering tool. - vals := chartutil.Values{"Name": "one", "Value": "two"} + vals := common.Values{"Name": "one", "Value": "two"} tpls := map[string]renderable{ "one": {tpl: `Hello {{title .Name}}`, vals: vals}, "two": {tpl: `Goodbye {{upper .Value}}`, vals: vals}, @@ -218,7 +219,7 @@ func TestRenderWithDNS(t *testing.T) { Name: "moby", Version: "1.2.3", }, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/test1", Data: []byte("{{getHostByName \"helm.sh\"}}")}, }, Values: map[string]interface{}{}, @@ -228,7 +229,7 @@ func TestRenderWithDNS(t *testing.T) { "Values": map[string]interface{}{}, } - v, err := chartutil.CoalesceValues(c, vals) + v, err := util.CoalesceValues(c, vals) if err != nil { t.Fatalf("Failed to coalesce values: %s", err) } @@ -355,7 +356,7 @@ func TestRenderWithClientProvider(t *testing.T) { } for name, exp := range cases { - c.Templates = append(c.Templates, &chart.File{ + c.Templates = append(c.Templates, &common.File{ Name: path.Join("templates", name), Data: []byte(exp.template), }) @@ -365,7 +366,7 @@ func TestRenderWithClientProvider(t *testing.T) { "Values": map[string]interface{}{}, } - v, err := chartutil.CoalesceValues(c, vals) + v, err := util.CoalesceValues(c, vals) if err != nil { t.Fatalf("Failed to coalesce values: %s", err) } @@ -391,7 +392,7 @@ func TestRenderWithClientProvider_error(t *testing.T) { Name: "moby", Version: "1.2.3", }, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/error", Data: []byte(`{{ lookup "v1" "Error" "" "" }}`)}, }, Values: map[string]interface{}{}, @@ -401,7 +402,7 @@ func TestRenderWithClientProvider_error(t *testing.T) { "Values": map[string]interface{}{}, } - v, err := chartutil.CoalesceValues(c, vals) + v, err := util.CoalesceValues(c, vals) if err != nil { t.Fatalf("Failed to coalesce values: %s", err) } @@ -448,7 +449,7 @@ func TestParallelRenderInternals(t *testing.T) { } func TestParseErrors(t *testing.T) { - vals := chartutil.Values{"Values": map[string]interface{}{}} + vals := common.Values{"Values": map[string]interface{}{}} tplsUndefinedFunction := map[string]renderable{ "undefined_function": {tpl: `{{foo}}`, vals: vals}, @@ -464,7 +465,7 @@ func TestParseErrors(t *testing.T) { } func TestExecErrors(t *testing.T) { - vals := chartutil.Values{"Values": map[string]interface{}{}} + vals := common.Values{"Values": map[string]interface{}{}} cases := []struct { name string tpls map[string]renderable @@ -528,7 +529,7 @@ linebreak`, } func TestFailErrors(t *testing.T) { - vals := chartutil.Values{"Values": map[string]interface{}{}} + vals := common.Values{"Values": map[string]interface{}{}} failtpl := `All your base are belong to us{{ fail "This is an error" }}` tplsFailed := map[string]renderable{ @@ -559,14 +560,14 @@ func TestFailErrors(t *testing.T) { func TestAllTemplates(t *testing.T) { ch1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "ch1"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/foo", Data: []byte("foo")}, {Name: "templates/bar", Data: []byte("bar")}, }, } dep1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "laboratory mice"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/pinky", Data: []byte("pinky")}, {Name: "templates/brain", Data: []byte("brain")}, }, @@ -575,13 +576,13 @@ func TestAllTemplates(t *testing.T) { dep2 := &chart.Chart{ Metadata: &chart.Metadata{Name: "same thing we do every night"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/innermost", Data: []byte("innermost")}, }, } dep1.AddDependency(dep2) - tpls := allTemplates(ch1, chartutil.Values{}) + tpls := allTemplates(ch1, common.Values{}) if len(tpls) != 5 { t.Errorf("Expected 5 charts, got %d", len(tpls)) } @@ -590,19 +591,19 @@ func TestAllTemplates(t *testing.T) { func TestChartValuesContainsIsRoot(t *testing.T) { ch1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "parent"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")}, }, } dep1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "child"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")}, }, } ch1.AddDependency(dep1) - out, err := Render(ch1, chartutil.Values{}) + out, err := Render(ch1, common.Values{}) if err != nil { t.Fatalf("failed to render templates: %s", err) } @@ -622,13 +623,13 @@ func TestRenderDependency(t *testing.T) { toptpl := `Hello {{template "myblock"}}` ch := &chart.Chart{ Metadata: &chart.Metadata{Name: "outerchart"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/outer", Data: []byte(toptpl)}, }, } ch.AddDependency(&chart.Chart{ Metadata: &chart.Metadata{Name: "innerchart"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/inner", Data: []byte(deptpl)}, }, }) @@ -660,7 +661,7 @@ func TestRenderNestedValues(t *testing.T) { deepest := &chart.Chart{ Metadata: &chart.Metadata{Name: "deepest"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: deepestpath, Data: []byte(`And this same {{.Values.what}} that smiles {{.Values.global.when}}`)}, {Name: checkrelease, Data: []byte(`Tomorrow will be {{default "happy" .Release.Name }}`)}, }, @@ -669,7 +670,7 @@ func TestRenderNestedValues(t *testing.T) { inner := &chart.Chart{ Metadata: &chart.Metadata{Name: "herrick"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: innerpath, Data: []byte(`Old {{.Values.who}} is still a-flyin'`)}, }, Values: map[string]interface{}{"who": "Robert", "what": "glasses"}, @@ -678,7 +679,7 @@ func TestRenderNestedValues(t *testing.T) { outer := &chart.Chart{ Metadata: &chart.Metadata{Name: "top"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: outerpath, Data: []byte(`Gather ye {{.Values.what}} while ye may`)}, {Name: subchartspath, Data: []byte(`The glorious Lamp of {{.Subcharts.herrick.Subcharts.deepest.Values.where}}, the {{.Subcharts.herrick.Values.what}}`)}, }, @@ -706,15 +707,15 @@ func TestRenderNestedValues(t *testing.T) { }, } - tmp, err := chartutil.CoalesceValues(outer, injValues) + tmp, err := util.CoalesceValues(outer, injValues) if err != nil { t.Fatalf("Failed to coalesce values: %s", err) } - inject := chartutil.Values{ + inject := common.Values{ "Values": tmp, "Chart": outer.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "dyin", }, } @@ -754,30 +755,30 @@ func TestRenderNestedValues(t *testing.T) { func TestRenderBuiltinValues(t *testing.T) { inner := &chart.Chart{ - Metadata: &chart.Metadata{Name: "Latium"}, - Templates: []*chart.File{ + Metadata: &chart.Metadata{Name: "Latium", APIVersion: chart.APIVersionV2}, + Templates: []*common.File{ {Name: "templates/Lavinia", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, {Name: "templates/From", Data: []byte(`{{.Files.author | printf "%s"}} {{.Files.Get "book/title.txt"}}`)}, }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "author", Data: []byte("Virgil")}, {Name: "book/title.txt", Data: []byte("Aeneid")}, }, } outer := &chart.Chart{ - Metadata: &chart.Metadata{Name: "Troy"}, - Templates: []*chart.File{ + Metadata: &chart.Metadata{Name: "Troy", APIVersion: chart.APIVersionV2}, + Templates: []*common.File{ {Name: "templates/Aeneas", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, {Name: "templates/Amata", Data: []byte(`{{.Subcharts.Latium.Chart.Name}} {{.Subcharts.Latium.Files.author | printf "%s"}}`)}, }, } outer.AddDependency(inner) - inject := chartutil.Values{ + inject := common.Values{ "Values": "", "Chart": outer.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "Aeneid", }, } @@ -806,7 +807,7 @@ func TestRenderBuiltinValues(t *testing.T) { func TestAlterFuncMap_include(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "conrad"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/quote", Data: []byte(`{{include "conrad/templates/_partial" . | indent 2}} dead.`)}, {Name: "templates/_partial", Data: []byte(`{{.Release.Name}} - he`)}, }, @@ -815,16 +816,16 @@ func TestAlterFuncMap_include(t *testing.T) { // Check nested reference in include FuncMap d := &chart.Chart{ Metadata: &chart.Metadata{Name: "nested"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/quote", Data: []byte(`{{include "nested/templates/quote" . | indent 2}} dead.`)}, {Name: "templates/_partial", Data: []byte(`{{.Release.Name}} - he`)}, }, } - v := chartutil.Values{ + v := common.Values{ "Values": "", "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "Mistah Kurtz", }, } @@ -849,19 +850,19 @@ func TestAlterFuncMap_include(t *testing.T) { func TestAlterFuncMap_require(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "conan"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/quote", Data: []byte(`All your base are belong to {{ required "A valid 'who' is required" .Values.who }}`)}, {Name: "templates/bases", Data: []byte(`All {{ required "A valid 'bases' is required" .Values.bases }} of them!`)}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "who": "us", "bases": 2, }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "That 90s meme", }, } @@ -882,12 +883,12 @@ func TestAlterFuncMap_require(t *testing.T) { // test required without passing in needed values with lint mode on // verifies lint replaces required with an empty string (should not fail) - lintValues := chartutil.Values{ - "Values": chartutil.Values{ + lintValues := common.Values{ + "Values": common.Values{ "who": "us", }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "That 90s meme", }, } @@ -911,17 +912,17 @@ func TestAlterFuncMap_require(t *testing.T) { func TestAlterFuncMap_tpl(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplFunction"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/base", Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value}}" .}}`)}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "value": "myvalue", }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -940,17 +941,17 @@ func TestAlterFuncMap_tpl(t *testing.T) { func TestAlterFuncMap_tplfunc(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplFunction"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/base", Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value | quote}}" .}}`)}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "value": "myvalue", }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -969,17 +970,17 @@ func TestAlterFuncMap_tplfunc(t *testing.T) { func TestAlterFuncMap_tplinclude(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplFunction"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/base", Data: []byte(`{{ tpl "{{include ` + "`" + `TplFunction/templates/_partial` + "`" + ` . | quote }}" .}}`)}, {Name: "templates/_partial", Data: []byte(`{{.Template.Name}}`)}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "value": "myvalue", }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -1000,15 +1001,15 @@ func TestRenderRecursionLimit(t *testing.T) { // endless recursion should produce an error c := &chart.Chart{ Metadata: &chart.Metadata{Name: "bad"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/base", Data: []byte(`{{include "recursion" . }}`)}, {Name: "templates/recursion", Data: []byte(`{{define "recursion"}}{{include "recursion" . }}{{end}}`)}, }, } - v := chartutil.Values{ + v := common.Values{ "Values": "", "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -1030,7 +1031,7 @@ func TestRenderRecursionLimit(t *testing.T) { d := &chart.Chart{ Metadata: &chart.Metadata{Name: "overlook"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/quote", Data: []byte(repeatedIncl)}, {Name: "templates/_function", Data: []byte(printFunc)}, }, @@ -1054,23 +1055,23 @@ func TestRenderRecursionLimit(t *testing.T) { func TestRenderLoadTemplateForTplFromFile(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplLoadFromFile"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/base", Data: []byte(`{{ tpl (.Files.Get .Values.filename) . }}`)}, {Name: "templates/_function", Data: []byte(`{{define "test-function"}}test-function{{end}}`)}, }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "test", Data: []byte(`{{ tpl (.Files.Get .Values.filename2) .}}`)}, {Name: "test2", Data: []byte(`{{include "test-function" .}}{{define "nested-define"}}nested-define-content{{end}} {{include "nested-define" .}}`)}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "filename": "test", "filename2": "test2", }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -1089,15 +1090,15 @@ func TestRenderLoadTemplateForTplFromFile(t *testing.T) { func TestRenderTplEmpty(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplEmpty"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/empty-string", Data: []byte(`{{tpl "" .}}`)}, {Name: "templates/empty-action", Data: []byte(`{{tpl "{{ \"\"}}" .}}`)}, {Name: "templates/only-defines", Data: []byte(`{{tpl "{{define \"not-invoked\"}}not-rendered{{end}}" .}}`)}, }, } - v := chartutil.Values{ + v := common.Values{ "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -1123,7 +1124,7 @@ func TestRenderTplTemplateNames(t *testing.T) { // .Template.BasePath and .Name make it through c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplTemplateNames"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/default-basepath", Data: []byte(`{{tpl "{{ .Template.BasePath }}" .}}`)}, {Name: "templates/default-name", Data: []byte(`{{tpl "{{ .Template.Name }}" .}}`)}, {Name: "templates/modified-basepath", Data: []byte(`{{tpl "{{ .Template.BasePath }}" .Values.dot}}`)}, @@ -1131,10 +1132,10 @@ func TestRenderTplTemplateNames(t *testing.T) { {Name: "templates/modified-field", Data: []byte(`{{tpl "{{ .Template.Field }}" .Values.dot}}`)}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ - "dot": chartutil.Values{ - "Template": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ + "dot": common.Values{ + "Template": common.Values{ "BasePath": "path/to/template", "Name": "name-of-template", "Field": "extra-field", @@ -1142,7 +1143,7 @@ func TestRenderTplTemplateNames(t *testing.T) { }, }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -1170,7 +1171,7 @@ func TestRenderTplRedefines(t *testing.T) { // Redefining a template inside 'tpl' does not affect the outer definition c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplRedefines"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/_partials", Data: []byte(`{{define "partial"}}original-in-partial{{end}}`)}, {Name: "templates/partial", Data: []byte( `before: {{include "partial" .}}\n{{tpl .Values.partialText .}}\nafter: {{include "partial" .}}`, @@ -1192,8 +1193,8 @@ func TestRenderTplRedefines(t *testing.T) { )}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "partialText": `{{define "partial"}}redefined-in-tpl{{end}}tpl: {{include "partial" .}}`, "manifestText": `{{define "manifest"}}redefined-in-tpl{{end}}tpl: {{include "manifest" .}}`, "manifestOnlyText": `tpl: {{include "manifest-only" .}}`, @@ -1205,7 +1206,7 @@ func TestRenderTplRedefines(t *testing.T) { "innerText": `{{define "nested"}}redefined-in-inner-tpl{{end}}inner-tpl: {{include "nested" .}} {{include "nested-outer" . }}`, }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -1236,16 +1237,16 @@ func TestRenderTplMissingKey(t *testing.T) { // Rendering a missing key results in empty/zero output. c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplMissingKey"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/manifest", Data: []byte( `missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`, )}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{}, + v := common.Values{ + "Values": common.Values{}, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -1269,16 +1270,16 @@ func TestRenderTplMissingKeyString(t *testing.T) { // Rendering a missing key results in error c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplMissingKeyStrict"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/manifest", Data: []byte( `missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`, )}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{}, + v := common.Values{ + "Values": common.Values{}, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -1301,7 +1302,7 @@ func TestRenderTplMissingKeyString(t *testing.T) { func TestNestedHelpersProducesMultilineStacktrace(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "NestedHelperFunctions"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/svc.yaml", Data: []byte( `name: {{ include "nested_helper.name" . }}`, )}, @@ -1324,9 +1325,9 @@ NestedHelperFunctions/charts/common/templates/_helpers_2.tpl:1:49 executing "common.names.get_name" at <.Values.nonexistant.key>: nil pointer evaluating interface {}.key` - v := chartutil.Values{} + v := common.Values{} - val, _ := chartutil.CoalesceValues(c, v) + val, _ := util.CoalesceValues(c, v) vals := map[string]interface{}{ "Values": val.AsMap(), } @@ -1339,7 +1340,7 @@ NestedHelperFunctions/charts/common/templates/_helpers_2.tpl:1:49 func TestMultilineNoTemplateAssociatedError(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "multiline"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/svc.yaml", Data: []byte( `name: {{ include "nested_helper.name" . }}`, )}, @@ -1357,9 +1358,9 @@ func TestMultilineNoTemplateAssociatedError(t *testing.T) { error calling include: template: no template "nested_helper.name" associated with template "gotpl"` - v := chartutil.Values{} + v := common.Values{} - val, _ := chartutil.CoalesceValues(c, v) + val, _ := util.CoalesceValues(c, v) vals := map[string]interface{}{ "Values": val.AsMap(), } @@ -1373,7 +1374,7 @@ func TestRenderCustomTemplateFuncs(t *testing.T) { // Create a chart with two templates that use custom functions c := &chart.Chart{ Metadata: &chart.Metadata{Name: "CustomFunc"}, - Templates: []*chart.File{ + Templates: []*common.File{ { Name: "templates/manifest", Data: []byte(`{{exclaim .Values.message}}`), @@ -1384,12 +1385,12 @@ func TestRenderCustomTemplateFuncs(t *testing.T) { }, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "message": "hello", }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } diff --git a/pkg/engine/files.go b/pkg/engine/files.go index 87166728c..f0a86988e 100644 --- a/pkg/engine/files.go +++ b/pkg/engine/files.go @@ -23,7 +23,7 @@ import ( "github.com/gobwas/glob" - chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/chart/common" ) // files is a map of files in a chart that can be accessed from a template. @@ -31,7 +31,7 @@ type files map[string][]byte // NewFiles creates a new files from chart files. // Given an []*chart.File (the format for files in a chart.Chart), extract a map of files. -func newFiles(from []*chart.File) files { +func newFiles(from []*common.File) files { files := make(map[string][]byte) for _, f := range from { files[f.Name] = f.Data diff --git a/pkg/engine/lookup_func.go b/pkg/engine/lookup_func.go index 605b43a48..18ed2b63b 100644 --- a/pkg/engine/lookup_func.go +++ b/pkg/engine/lookup_func.go @@ -35,7 +35,7 @@ type lookupFunc = func(apiversion string, resource string, namespace string, nam // NewLookupFunction returns a function for looking up objects in the cluster. // // If the resource does not exist, no error is raised. -func NewLookupFunction(config *rest.Config) lookupFunc { +func NewLookupFunction(config *rest.Config) lookupFunc { //nolint:revive return newLookupFunction(clientProviderFromConfig{config: config}) } diff --git a/pkg/release/v1/mock.go b/pkg/release/v1/mock.go index 3d3b0c2e2..c3a6594cc 100644 --- a/pkg/release/v1/mock.go +++ b/pkg/release/v1/mock.go @@ -20,6 +20,7 @@ import ( "fmt" "math/rand" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/time" ) @@ -98,7 +99,7 @@ func Mock(opts *MockReleaseOptions) *Release { }, }, }, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/foo.tpl", Data: []byte(MockManifest)}, }, } diff --git a/pkg/release/v1/util/manifest_sorter.go b/pkg/release/v1/util/manifest_sorter.go index 21fdec7c6..6f7b4ea8b 100644 --- a/pkg/release/v1/util/manifest_sorter.go +++ b/pkg/release/v1/util/manifest_sorter.go @@ -26,7 +26,7 @@ import ( "sigs.k8s.io/yaml" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + "helm.sh/helm/v4/pkg/chart/common" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -74,7 +74,7 @@ var events = map[string]release.HookEvent{ // // Files that do not parse into the expected format are simply placed into a map and // returned. -func SortManifests(files map[string]string, _ chartutil.VersionSet, ordering KindSortOrder) ([]*release.Hook, []Manifest, error) { +func SortManifests(files map[string]string, _ common.VersionSet, ordering KindSortOrder) ([]*release.Hook, []Manifest, error) { result := &result{} var sortedFilePaths []string