Repository: helm/helm (go)

Issue:
Proposal: disable values schema validation for charts including a values.schema.json file
Even when a chart designer/maintainer has decided to include a ``values.schema.json`` file in the Helm chart he/she releases it could be that the user of that chart (using it directly or as a dependency in an integration/umbrella Helm chart) cannot assure to fulfill the defined schema for the deployment specific values passed to that subchart (e.g. he/she passes a deployment specific values.yaml file to the ``helm install/upgrade`` command applied on that chart, where some "old" Helm parameter removed from the ``values.schema.json`` of a newer version of that (sub)chart still remains).

I think it would be useful to support a new option in ``helm install``, ``helm upgrade``, ``helm lint`` and ``helm template`` allowing to disable the values schema validation (in case a ``values.schema.json`` file is packaged in the target Helm chart or some listed subchart)




Pull request description:
feat(helm): add --skip-schema-validation flag to helm 'install', 'uprade' and 'lint'
**What this PR does / why we need it**:

When --skip-schema-validation is set, any schema contained in the helm chart is ignored. Defaults to 'false'.

Closes #10398

**Special notes for your reviewer**:

I tried to keep the code in the `pkg` folder backwards compatible.

Supersedes https://github.com/helm/helm/pull/11510

Documentation added in https://github.com/helm/helm-www/pull/1549

**If applicable**:
- [x] this PR contains documentation
- [x] this PR contains unit tests
- [x] this PR has been tested for backwards compatibility

----
### maintainer addition
- closes #11510 

Patch:
diff --git a/cmd/helm/install.go b/cmd/helm/install.go
index ed3f74274..23ff29d95 100644
--- a/cmd/helm/install.go
+++ b/cmd/helm/install.go
@@ -197,6 +197,7 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal
 	f.BoolVar(&client.Atomic, "atomic", false, "if set, the installation process deletes the installation on failure. The --wait flag will be set automatically if --atomic is used")
 	f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed. By default, CRDs are installed if not already present")
 	f.BoolVar(&client.SubNotes, "render-subchart-notes", false, "if set, render subchart notes along with the parent")
+	f.BoolVar(&client.SkipSchemaValidation, "skip-schema-validation", false, "if set, disables JSON schema validation")
 	f.StringToStringVarP(&client.Labels, "labels", "l", nil, "Labels that would be added to release metadata. Should be divided by comma.")
 	f.BoolVar(&client.EnableDNS, "enable-dns", false, "enable DNS lookups when rendering templates")
 	f.BoolVar(&client.HideNotes, "hide-notes", false, "if set, do not show notes in install output. Does not affect presence in chart metadata")
diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go
index 6b54bdd3f..4c5e24149 100644
--- a/cmd/helm/lint.go
+++ b/cmd/helm/lint.go
@@ -148,6 +148,7 @@ func newLintCmd(out io.Writer) *cobra.Command {
 	f.BoolVar(&client.Strict, "strict", false, "fail on lint warnings")
 	f.BoolVar(&client.WithSubcharts, "with-subcharts", false, "lint dependent charts")
 	f.BoolVar(&client.Quiet, "quiet", false, "print only warnings and errors")
+	f.BoolVar(&client.SkipSchemaValidation, "skip-schema-validation", false, "if set, disables JSON schema validation")
 	f.StringVar(&kubeVersion, "kube-version", "", "Kubernetes version used for capabilities and deprecation checks")
 	addValueOptionsFlags(f, valueOpts)
 
diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go
index 23472619d..e5dcd5b02 100644
--- a/cmd/helm/upgrade.go
+++ b/cmd/helm/upgrade.go
@@ -145,6 +145,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 					instClient.DisableOpenAPIValidation = client.DisableOpenAPIValidation
 					instClient.SubNotes = client.SubNotes
 					instClient.HideNotes = client.HideNotes
+					instClient.SkipSchemaValidation = client.SkipSchemaValidation
 					instClient.Description = client.Description
 					instClient.DependencyUpdate = client.DependencyUpdate
 					instClient.Labels = client.Labels
@@ -274,6 +275,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 	f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this upgrade when upgrade fails")
 	f.BoolVar(&client.SubNotes, "render-subchart-notes", false, "if set, render subchart notes along with the parent")
 	f.BoolVar(&client.HideNotes, "hide-notes", false, "if set, do not show notes in upgrade output. Does not affect presence in chart metadata")
+	f.BoolVar(&client.SkipSchemaValidation, "skip-schema-validation", false, "if set, disables JSON schema validation")
 	f.StringToStringVarP(&client.Labels, "labels", "l", nil, "Labels that would be added to release metadata. Should be separated by comma. Original release labels will be merged with upgrade labels. You can unset label using null.")
 	f.StringVar(&client.Description, "description", "", "add a custom description")
 	f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "update dependencies if they are missing before installing the chart")
diff --git a/pkg/action/install.go b/pkg/action/install.go
index 6dce3ccbb..63d58a212 100644
--- a/pkg/action/install.go
+++ b/pkg/action/install.go
@@ -94,6 +94,7 @@ type Install struct {
 	SkipCRDs                 bool
 	SubNotes                 bool
 	HideNotes                bool
+	SkipSchemaValidation     bool
 	DisableOpenAPIValidation bool
 	IncludeCRDs              bool
 	Labels                   map[string]string
@@ -298,7 +299,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
 		IsInstall: !isUpgrade,
 		IsUpgrade: isUpgrade,
 	}
-	valuesToRender, err := chartutil.ToRenderValues(chrt, vals, options, caps)
+	valuesToRender, err := chartutil.ToRenderValuesWithSchemaValidation(chrt, vals, options, caps, i.SkipSchemaValidation)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/action/lint.go b/pkg/action/lint.go
index ca497f2b8..63a1bf354 100644
--- a/pkg/action/lint.go
+++ b/pkg/action/lint.go
@@ -32,11 +32,12 @@ import (
 //
 // It provides the implementation of 'helm lint'.
 type Lint struct {
-	Strict        bool
-	Namespace     string
-	WithSubcharts bool
-	Quiet         bool
-	KubeVersion   *chartutil.KubeVersion
+	Strict               bool
+	Namespace            string
+	WithSubcharts        bool
+	Quiet                bool
+	SkipSchemaValidation bool
+	KubeVersion          *chartutil.KubeVersion
 }
 
 // LintResult is the result of Lint
@@ -59,7 +60,7 @@ func (l *Lint) Run(paths []string, vals map[string]interface{}) *LintResult {
 	}
 	result := &LintResult{}
 	for _, path := range paths {
-		linter, err := lintChart(path, vals, l.Namespace, l.KubeVersion)
+		linter, err := lintChart(path, vals, l.Namespace, l.KubeVersion, l.SkipSchemaValidation)
 		if err != nil {
 			result.Errors = append(result.Errors, err)
 			continue
@@ -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) (support.Linter, error) {
+func lintChart(path string, vals map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion, skipSchemaValidation bool) (support.Linter, error) {
 	var chartPath string
 	linter := support.Linter{}
 
@@ -125,5 +126,5 @@ func lintChart(path string, vals map[string]interface{}, namespace string, kubeV
 		return linter, errors.Wrap(err, "unable to check Chart.yaml file in chart")
 	}
 
-	return lint.AllWithKubeVersion(chartPath, vals, namespace, kubeVersion), nil
+	return lint.AllWithKubeVersionAndSchemaValidation(chartPath, vals, namespace, kubeVersion, skipSchemaValidation), nil
 }
diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go
index 6d26a754e..a3ae82801 100644
--- a/pkg/action/upgrade.go
+++ b/pkg/action/upgrade.go
@@ -99,6 +99,8 @@ type Upgrade struct {
 	SubNotes bool
 	// HideNotes determines whether notes are output during upgrade
 	HideNotes bool
+	// SkipSchemaValidation determines if JSON schema validation is disabled.
+	SkipSchemaValidation bool
 	// Description is the description of this operation
 	Description string
 	Labels      map[string]string
@@ -258,7 +260,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
 	if err != nil {
 		return nil, nil, err
 	}
-	valuesToRender, err := chartutil.ToRenderValues(chart, vals, options, caps)
+	valuesToRender, err := chartutil.ToRenderValuesWithSchemaValidation(chart, vals, options, caps, u.SkipSchemaValidation)
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go
index 2fa2bdabb..61c633a6d 100644
--- a/pkg/chartutil/values.go
+++ b/pkg/chartutil/values.go
@@ -135,6 +135,13 @@ type ReleaseOptions struct {
 //
 // 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
 	}
@@ -156,9 +163,11 @@ func ToRenderValues(chrt *chart.Chart, chrtVals map[string]interface{}, options
 		return top, err
 	}
 
-	if err := ValidateAgainstSchema(chrt, vals); err != nil {
-		errFmt := "values don't meet the specifications of the schema(s) in the following chart(s):\n%s"
-		return top, fmt.Errorf(errFmt, err.Error())
+	if !skipSchemaValidation {
+		if err := ValidateAgainstSchema(chrt, vals); err != nil {
+			errFmt := "values don't meet the specifications of the schema(s) in the following chart(s):\n%s"
+			return top, fmt.Errorf(errFmt, err.Error())
+		}
 	}
 
 	top["Values"] = vals
diff --git a/pkg/lint/lint.go b/pkg/lint/lint.go
index c0e79f55b..ef23ee7c8 100644
--- a/pkg/lint/lint.go
+++ b/pkg/lint/lint.go
@@ -24,20 +24,25 @@ import (
 	"helm.sh/helm/v3/pkg/lint/support"
 )
 
-// All runs all of the available linters on the given base directory.
+// All runs all the available linters on the given base directory.
 func All(basedir string, values map[string]interface{}, namespace string, _ bool) support.Linter {
 	return AllWithKubeVersion(basedir, values, namespace, nil)
 }
 
 // AllWithKubeVersion runs all the available linters on the given base directory, allowing to specify the kubernetes version.
 func AllWithKubeVersion(basedir string, values map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion) support.Linter {
+	return AllWithKubeVersionAndSchemaValidation(basedir, values, namespace, kubeVersion, false)
+}
+
+// AllWithKubeVersionAndSchemaValidation runs all the available linters on the given base directory, allowing to specify the kubernetes version and if schema validation is enabled or not.
+func AllWithKubeVersionAndSchemaValidation(basedir string, values map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion, skipSchemaValidation bool) support.Linter {
 	// Using abs path to get directory context
 	chartDir, _ := filepath.Abs(basedir)
 
 	linter := support.Linter{ChartDir: chartDir}
 	rules.Chartfile(&linter)
 	rules.ValuesWithOverrides(&linter, values)
-	rules.TemplatesWithKubeVersion(&linter, values, namespace, kubeVersion)
+	rules.TemplatesWithSkipSchemaValidation(&linter, values, namespace, kubeVersion, skipSchemaValidation)
 	rules.Dependencies(&linter)
 	return linter
 }
diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go
index 661c7f963..41d1a1bab 100644
--- a/pkg/lint/rules/template.go
+++ b/pkg/lint/rules/template.go
@@ -51,6 +51,11 @@ 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) {
+	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) {
 	fpath := "templates/"
 	templatesPath := filepath.Join(linter.ChartDir, fpath)
 
@@ -91,7 +96,7 @@ func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interfac
 		return
 	}
 
-	valuesToRender, err := chartutil.ToRenderValues(chart, cvals, options, caps)
+	valuesToRender, err := chartutil.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, skipSchemaValidation)
 	if err != nil {
 		linter.RunLinterRule(support.ErrorSev, fpath, err)
 		return


Test patch:
diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go
index a3b527031..3709c393e 100644
--- a/cmd/helm/install_test.go
+++ b/cmd/helm/install_test.go
@@ -225,6 +225,12 @@ func TestInstall(t *testing.T) {
 			wantError: true,
 			golden:    "output/subchart-schema-cli-negative.txt",
 		},
+		// Install, values from yaml, schematized with errors but skip schema validation, expect success
+		{
+			name:   "install with schema file and schematized subchart, extra values from cli, skip schema validation",
+			cmd:    "install schema testdata/testcharts/chart-with-schema-and-subchart --set lastname=doe --set subchart-with-schema.age=-25 --skip-schema-validation",
+			golden: "output/schema.txt",
+		},
 		// Install deprecated chart
 		{
 			name:   "install with warning about deprecated chart",
diff --git a/cmd/helm/testdata/testcharts/chart-with-schema-negative-skip-validation/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-schema-negative-skip-validation/Chart.yaml
new file mode 100644
index 000000000..395d24f6a
--- /dev/null
+++ b/cmd/helm/testdata/testcharts/chart-with-schema-negative-skip-validation/Chart.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+description: Empty testing chart
+home: https://k8s.io/helm
+name: empty
+sources:
+- https://github.com/kubernetes/helm
+version: 0.1.0
diff --git a/cmd/helm/testdata/testcharts/chart-with-schema-negative-skip-validation/templates/empty.yaml b/cmd/helm/testdata/testcharts/chart-with-schema-negative-skip-validation/templates/empty.yaml
new file mode 100644
index 000000000..c80812f6e
--- /dev/null
+++ b/cmd/helm/testdata/testcharts/chart-with-schema-negative-skip-validation/templates/empty.yaml
@@ -0,0 +1,1 @@
+# This file is intentionally blank
diff --git a/cmd/helm/testdata/testcharts/chart-with-schema-negative-skip-validation/values.schema.json b/cmd/helm/testdata/testcharts/chart-with-schema-negative-skip-validation/values.schema.json
new file mode 100644
index 000000000..4df89bbe8
--- /dev/null
+++ b/cmd/helm/testdata/testcharts/chart-with-schema-negative-skip-validation/values.schema.json
@@ -0,0 +1,67 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "properties": {
+    "addresses": {
+      "description": "List of addresses",
+      "items": {
+        "properties": {
+          "city": {
+            "type": "string"
+          },
+          "number": {
+            "type": "number"
+          },
+          "street": {
+            "type": "string"
+          }
+        },
+        "type": "object"
+      },
+      "type": "array"
+    },
+    "age": {
+      "description": "Age",
+      "minimum": 0,
+      "type": "integer"
+    },
+    "employmentInfo": {
+      "properties": {
+        "salary": {
+          "minimum": 0,
+          "type": "number"
+        },
+        "title": {
+          "type": "string"
+        }
+      },
+      "required": [
+        "salary"
+      ],
+      "type": "object"
+    },
+    "firstname": {
+      "description": "First name",
+      "type": "string"
+    },
+    "lastname": {
+      "type": "string"
+    },
+    "likesCoffee": {
+      "type": "boolean"
+    },
+    "phoneNumbers": {
+      "items": {
+        "type": "string"
+      },
+      "type": "array"
+    }
+  },
+  "required": [
+    "firstname",
+    "lastname",
+    "addresses",
+    "employmentInfo"
+  ],
+  "title": "Values",
+  "type": "object"
+}
diff --git a/cmd/helm/testdata/testcharts/chart-with-schema-negative-skip-validation/values.yaml b/cmd/helm/testdata/testcharts/chart-with-schema-negative-skip-validation/values.yaml
new file mode 100644
index 000000000..5a1250bff
--- /dev/null
+++ b/cmd/helm/testdata/testcharts/chart-with-schema-negative-skip-validation/values.yaml
@@ -0,0 +1,14 @@
+firstname: John
+lastname: Doe
+age: -5
+likesCoffee: true
+addresses:
+  - city: Springfield
+    street: Main
+    number: 12345
+  - city: New York
+    street: Broadway
+    number: 67890
+phoneNumbers:
+  - "(888) 888-8888"
+  - "(555) 555-5555"
diff --git a/pkg/action/lint_test.go b/pkg/action/lint_test.go
index 80bf4ce7e..a01580b0a 100644
--- a/pkg/action/lint_test.go
+++ b/pkg/action/lint_test.go
@@ -31,9 +31,10 @@ var (
 
 func TestLintChart(t *testing.T) {
 	tests := []struct {
-		name      string
-		chartPath string
-		err       bool
+		name                 string
+		chartPath            string
+		err                  bool
+		skipSchemaValidation bool
 	}{
 		{
 			name:      "decompressed-chart",
@@ -69,6 +70,11 @@ func TestLintChart(t *testing.T) {
 			name:      "chart-with-schema-negative",
 			chartPath: "testdata/charts/chart-with-schema-negative",
 		},
+		{
+			name:                 "chart-with-schema-negative-skip-validation",
+			chartPath:            "testdata/charts/chart-with-schema-negative",
+			skipSchemaValidation: true,
+		},
 		{
 			name:      "pre-release-chart",
 			chartPath: "testdata/charts/pre-release-chart-0.1.0-alpha.tgz",
@@ -77,7 +83,7 @@ func TestLintChart(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			_, err := lintChart(tt.chartPath, map[string]interface{}{}, namespace, nil)
+			_, err := lintChart(tt.chartPath, map[string]interface{}{}, namespace, nil, tt.skipSchemaValidation)
 			switch {
 			case err != nil && !tt.err:
 				t.Errorf("%s", err)
diff --git a/pkg/chartutil/values_test.go b/pkg/chartutil/values_test.go
index c95fa503a..dc8eae3ab 100644
--- a/pkg/chartutil/values_test.go
+++ b/pkg/chartutil/values_test.go
@@ -103,7 +103,7 @@ func TestToRenderValues(t *testing.T) {
 		IsInstall: true,
 	}
 
-	res, err := ToRenderValues(c, overrideValues, o, nil)
+	res, err := ToRenderValuesWithSchemaValidation(c, overrideValues, o, nil, false)
 	if err != nil {
 		t.Fatal(err)
 	}