Repository: helm/helm
Pull request #13283 (2024-08-29 05:30:44)

adding support for JSON Schema 2020
<!--  Thanks for sending a pull request!  Here are some tips for you:
1. Make sure to read the Contributing Guide before submitting your PR: https://github.com/helm/helm/blob/main/CONTRIBUTING.md
2. If this PR closes another issue, add 'closes #<issue number>' somewhere in the PR summary. GitHub will automatically close that issue when this PR gets merged. Alternatively, adding 'refs #<issue number>' will not close the issue, but help provide the reviewer more context.-->

**What this PR does / why we need it**:

It adds support for json schema ~~2019 and~~ 2020 as requested in https://github.com/helm/helm/issues/13069

**Special notes for your reviewer**:

the new json schema validator is only used when the chart explicitly wants to use it in `$schema` field, this means it will be backward-compatible with the existing chart that doesn't specify `$schema` field.

IMO, we cannot completely remove `github.com/xeipuuv/gojsonschema`, because some users might depend on its behavior when no `$schema` is specified, https://xkcd.com/1172/
![image](https://imgs.xkcd.com/comics/workflow_2x.png)

**If applicable**:
- [ ] this PR contains user facing changes (the `docs needed` label should be applied if so)
- [x] this PR contains unit tests
- [x] this PR has been tested for backwards compatibility


win-t: The focus of this PR is just to introduce schema 2019 and 2020 only. By doing this, we will unblock the users. 

We can improve it later if we want to remove `github.com/xeipuuv/gojsonschema` and fully depend on `github.com/santhosh-tekuri/jsonschema`, I just don't want this issue to become a blocker for introducing schema 2019/2020.
mattfarina: @win-t can you rebase?
win-t: > @win-t can you rebase?

sure
win-t: > One more comment, is we will need to update the docs (helm-www) to detail support for the new versions

here the PR for it https://github.com/helm/helm-www/pull/1642

Patch:
diff --git a/go.mod b/go.mod
index 190fcff0f..ad119b6b8 100644
--- a/go.mod
+++ b/go.mod
@@ -29,6 +29,7 @@ require (
 	github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
 	github.com/pkg/errors v0.9.1
 	github.com/rubenv/sql-migrate v1.7.1
+	github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
 	github.com/spf13/cobra v1.9.1
 	github.com/spf13/pflag v1.0.6
 	github.com/stretchr/testify v1.10.0
diff --git a/go.sum b/go.sum
index 675bed1d8..4fcd483a4 100644
--- a/go.sum
+++ b/go.sum
@@ -67,6 +67,8 @@ github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN
 github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU=
 github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
+github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
 github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
 github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
@@ -297,6 +299,8 @@ github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmi
 github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
 github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
 github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
diff --git a/pkg/chart/v2/util/jsonschema.go b/pkg/chart/v2/util/jsonschema.go
index 615dc5320..66ab42542 100644
--- a/pkg/chart/v2/util/jsonschema.go
+++ b/pkg/chart/v2/util/jsonschema.go
@@ -18,10 +18,12 @@ package util
 
 import (
 	"bytes"
+	"encoding/json"
 	"fmt"
 	"strings"
 
 	"github.com/pkg/errors"
+	"github.com/santhosh-tekuri/jsonschema/v6"
 	"github.com/xeipuuv/gojsonschema"
 	"sigs.k8s.io/yaml"
 
@@ -73,6 +75,11 @@ func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) (reterr error
 	if bytes.Equal(valuesJSON, []byte("null")) {
 		valuesJSON = []byte("{}")
 	}
+
+	if schemaIs2020(schemaJSON) {
+		return validateUsingNewValidator(valuesJSON, schemaJSON)
+	}
+
 	schemaLoader := gojsonschema.NewBytesLoader(schemaJSON)
 	valuesLoader := gojsonschema.NewBytesLoader(valuesJSON)
 
@@ -91,3 +98,35 @@ func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) (reterr error
 
 	return nil
 }
+
+func validateUsingNewValidator(valuesJSON, schemaJSON []byte) error {
+	schema, err := jsonschema.UnmarshalJSON(bytes.NewReader(schemaJSON))
+	if err != nil {
+		return err
+	}
+	values, err := jsonschema.UnmarshalJSON(bytes.NewReader(valuesJSON))
+	if err != nil {
+		return err
+	}
+
+	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
+	}
+
+	return validator.Validate(values)
+}
+
+func schemaIs2020(schemaJSON []byte) bool {
+	var partialSchema struct {
+		Schema string `json:"$schema"`
+	}
+	_ = json.Unmarshal(schemaJSON, &partialSchema)
+	return partialSchema.Schema == "https://json-schema.org/draft/2020-12/schema"
+}


Test patch:
diff --git a/pkg/chart/v2/util/jsonschema_test.go b/pkg/chart/v2/util/jsonschema_test.go
index 3e3315732..6337ab259 100644
--- a/pkg/chart/v2/util/jsonschema_test.go
+++ b/pkg/chart/v2/util/jsonschema_test.go
@@ -104,6 +104,21 @@ const subchartSchema = `{
 }
 `
 
+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{
@@ -165,3 +180,68 @@ func TestValidateAgainstSchemaNegative(t *testing.T) {
 		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:
+jsonschema validation failed with 'file:///values.schema.json#'
+- 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)
+	}
+}