diff --git a/cmd/helm/testdata/output/schema-negative-cli.txt b/cmd/helm/testdata/output/schema-negative-cli.txt index c4a5cc516..12bcc5103 100644 --- a/cmd/helm/testdata/output/schema-negative-cli.txt +++ b/cmd/helm/testdata/output/schema-negative-cli.txt @@ -1,4 +1,4 @@ Error: INSTALLATION FAILED: values don't meet the specifications of the schema(s) in the following chart(s): empty: -- age: Must be greater than or equal to 0 +- at '/age': minimum: got -5, want 0 diff --git a/cmd/helm/testdata/output/schema-negative.txt b/cmd/helm/testdata/output/schema-negative.txt index 929af5518..daf132635 100644 --- a/cmd/helm/testdata/output/schema-negative.txt +++ b/cmd/helm/testdata/output/schema-negative.txt @@ -1,5 +1,5 @@ Error: INSTALLATION FAILED: values don't meet the specifications of the schema(s) in the following chart(s): empty: -- (root): employmentInfo is required -- age: Must be greater than or equal to 0 +- at '': missing property 'employmentInfo' +- at '/age': minimum: got -5, want 0 diff --git a/cmd/helm/testdata/output/subchart-schema-cli-negative.txt b/cmd/helm/testdata/output/subchart-schema-cli-negative.txt index 7396b4bfe..179550f69 100644 --- a/cmd/helm/testdata/output/subchart-schema-cli-negative.txt +++ b/cmd/helm/testdata/output/subchart-schema-cli-negative.txt @@ -1,4 +1,4 @@ Error: INSTALLATION FAILED: values don't meet the specifications of the schema(s) in the following chart(s): subchart-with-schema: -- age: Must be greater than or equal to 0 +- at '/age': minimum: got -25, want 0 diff --git a/cmd/helm/testdata/output/subchart-schema-negative.txt b/cmd/helm/testdata/output/subchart-schema-negative.txt index 7b1f654a2..7522ef3e4 100644 --- a/cmd/helm/testdata/output/subchart-schema-negative.txt +++ b/cmd/helm/testdata/output/subchart-schema-negative.txt @@ -1,6 +1,6 @@ Error: INSTALLATION FAILED: values don't meet the specifications of the schema(s) in the following chart(s): chart-without-schema: -- (root): lastname is required +- at '': missing property 'lastname' subchart-with-schema: -- (root): age is required +- at '': missing property 'age' diff --git a/go.mod b/go.mod index b748e6c3d..a7c2d75f5 100644 --- a/go.mod +++ b/go.mod @@ -29,22 +29,22 @@ require ( github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 github.com/pkg/errors v0.9.1 github.com/rubenv/sql-migrate v1.8.0 + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.7 - github.com/stretchr/testify v1.10.0 - github.com/xeipuuv/gojsonschema v1.2.0 + github.com/stretchr/testify v1.11.1 golang.org/x/crypto v0.41.0 golang.org/x/term v0.34.0 golang.org/x/text v0.28.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.33.3 - k8s.io/apiextensions-apiserver v0.33.3 - k8s.io/apimachinery v0.33.3 - k8s.io/apiserver v0.33.3 - k8s.io/cli-runtime v0.33.3 - k8s.io/client-go v0.33.3 + k8s.io/api v0.33.4 + k8s.io/apiextensions-apiserver v0.33.4 + k8s.io/apimachinery v0.33.4 + k8s.io/apiserver v0.33.4 + k8s.io/cli-runtime v0.33.4 + k8s.io/client-go v0.33.4 k8s.io/klog/v2 v2.130.1 - k8s.io/kubectl v0.33.3 + k8s.io/kubectl v0.33.4 oras.land/oras-go/v2 v2.6.0 sigs.k8s.io/yaml v1.6.0 ) @@ -134,8 +134,6 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/cast v1.7.0 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xlab/treeprint v1.2.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 // indirect @@ -176,7 +174,7 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/component-base v0.33.3 // indirect + k8s.io/component-base v0.33.4 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect diff --git a/go.sum b/go.sum index 139dcc7ad..0658c2b84 100644 --- a/go.sum +++ b/go.sum @@ -76,6 +76,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= @@ -305,6 +307,8 @@ github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2N github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw= 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.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/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= @@ -332,17 +336,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -527,26 +524,26 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= -k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= -k8s.io/apiextensions-apiserver v0.33.3 h1:qmOcAHN6DjfD0v9kxL5udB27SRP6SG/MTopmge3MwEs= -k8s.io/apiextensions-apiserver v0.33.3/go.mod h1:oROuctgo27mUsyp9+Obahos6CWcMISSAPzQ77CAQGz8= -k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= -k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.3 h1:Wv0hGc+QFdMJB4ZSiHrCgN3zL3QRatu56+rpccKC3J4= -k8s.io/apiserver v0.33.3/go.mod h1:05632ifFEe6TxwjdAIrwINHWE2hLwyADFk5mBsQa15E= -k8s.io/cli-runtime v0.33.3 h1:Dgy4vPjNIu8LMJBSvs8W0LcdV0PX/8aGG1DA1W8lklA= -k8s.io/cli-runtime v0.33.3/go.mod h1:yklhLklD4vLS8HNGgC9wGiuHWze4g7x6XQZ+8edsKEo= -k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= -k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= -k8s.io/component-base v0.33.3 h1:mlAuyJqyPlKZM7FyaoM/LcunZaaY353RXiOd2+B5tGA= -k8s.io/component-base v0.33.3/go.mod h1:ktBVsBzkI3imDuxYXmVxZ2zxJnYTZ4HAsVj9iF09qp4= +k8s.io/api v0.33.4 h1:oTzrFVNPXBjMu0IlpA2eDDIU49jsuEorGHB4cvKupkk= +k8s.io/api v0.33.4/go.mod h1:VHQZ4cuxQ9sCUMESJV5+Fe8bGnqAARZ08tSTdHWfeAc= +k8s.io/apiextensions-apiserver v0.33.4 h1:rtq5SeXiDbXmSwxsF0MLe2Mtv3SwprA6wp+5qh/CrOU= +k8s.io/apiextensions-apiserver v0.33.4/go.mod h1:mWXcZQkQV1GQyxeIjYApuqsn/081hhXPZwZ2URuJeSs= +k8s.io/apimachinery v0.33.4 h1:SOf/JW33TP0eppJMkIgQ+L6atlDiP/090oaX0y9pd9s= +k8s.io/apimachinery v0.33.4/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.4 h1:6N0TEVA6kASUS3owYDIFJjUH6lgN8ogQmzZvaFFj1/Y= +k8s.io/apiserver v0.33.4/go.mod h1:8ODgXMnOoSPLMUg1aAzMFx+7wTJM+URil+INjbTZCok= +k8s.io/cli-runtime v0.33.4 h1:V8NSxGfh24XzZVhXmIGzsApdBpGq0RQS2u/Fz1GvJwk= +k8s.io/cli-runtime v0.33.4/go.mod h1:V+ilyokfqjT5OI+XE+O515K7jihtr0/uncwoyVqXaIU= +k8s.io/client-go v0.33.4 h1:TNH+CSu8EmXfitntjUPwaKVPN0AYMbc9F1bBS8/ABpw= +k8s.io/client-go v0.33.4/go.mod h1:LsA0+hBG2DPwovjd931L/AoaezMPX9CmBgyVyBZmbCY= +k8s.io/component-base v0.33.4 h1:Jvb/aw/tl3pfgnJ0E0qPuYLT0NwdYs1VXXYQmSuxJGY= +k8s.io/component-base v0.33.4/go.mod h1:567TeSdixWW2Xb1yYUQ7qk5Docp2kNznKL87eygY8Rc= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/kubectl v0.33.3 h1:r/phHvH1iU7gO/l7tTjQk2K01ER7/OAJi8uFHHyWSac= -k8s.io/kubectl v0.33.3/go.mod h1:euj2bG56L6kUGOE/ckZbCoudPwuj4Kud7BR0GzyNiT0= +k8s.io/kubectl v0.33.4 h1:nXEI6Vi+oB9hXxoAHyHisXolm/l1qutK3oZQMak4N98= +k8s.io/kubectl v0.33.4/go.mod h1:Xe7P9X4DfILvKmlBsVqUtzktkI56lEj22SJW7cFy6nE= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= diff --git a/pkg/chartutil/dependencies.go b/pkg/chartutil/dependencies.go index 36a341927..37452cec7 100644 --- a/pkg/chartutil/dependencies.go +++ b/pkg/chartutil/dependencies.go @@ -16,6 +16,7 @@ limitations under the License. package chartutil import ( + "fmt" "log" "strings" @@ -255,8 +256,8 @@ func processImportValues(c *chart.Chart, merge bool) error { for _, riv := range r.ImportValues { switch iv := riv.(type) { case map[string]interface{}: - child := iv["child"].(string) - parent := iv["parent"].(string) + child := fmt.Sprintf("%v", iv["child"]) + parent := fmt.Sprintf("%v", iv["parent"]) outiv = append(outiv, map[string]string{ "child": child, diff --git a/pkg/chartutil/jsonschema.go b/pkg/chartutil/jsonschema.go index 7b9768fd3..d712316c5 100644 --- a/pkg/chartutil/jsonschema.go +++ b/pkg/chartutil/jsonschema.go @@ -18,20 +18,62 @@ package chartutil import ( "bytes" + "crypto/tls" + "errors" "fmt" "strings" + "time" - "github.com/pkg/errors" - "github.com/xeipuuv/gojsonschema" - "sigs.k8s.io/yaml" + "github.com/santhosh-tekuri/jsonschema/v6" + "net/http" + + "helm.sh/helm/v3/internal/version" "helm.sh/helm/v3/pkg/chart" ) +// HTTPURLLoader implements a loader for HTTP/HTTPS URLs +type HTTPURLLoader http.Client + +func (l *HTTPURLLoader) Load(urlStr string) (any, error) { + client := (*http.Client)(l) + + req, err := http.NewRequest(http.MethodGet, urlStr, nil) + if err != nil { + return nil, fmt.Errorf("failed to create HTTP request for %s: %w", urlStr, err) + } + req.Header.Set("User-Agent", version.GetUserAgent()) + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("HTTP request failed for %s: %w", urlStr, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP request to %s returned status %d (%s)", urlStr, resp.StatusCode, http.StatusText(resp.StatusCode)) + } + + return jsonschema.UnmarshalJSON(resp.Body) +} + +// newHTTPURLLoader creates a HTTP URL loader with proxy support. +func newHTTPURLLoader() *HTTPURLLoader { + httpLoader := HTTPURLLoader(http.Client{ + Timeout: 15 * time.Second, + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{}, + }, + }) + return &httpLoader +} + // 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 { + err := ValidateAgainstSingleSchema(values, chrt.Schema) if err != nil { sb.WriteString(fmt.Sprintf("%s:\n", chrt.Name())) @@ -39,7 +81,6 @@ func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) err } } - // 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 { @@ -62,32 +103,48 @@ func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) (reterr error } }() - valuesData, err := yaml.Marshal(values) + // 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 } - valuesJSON, err := yaml.YAMLToJSON(valuesData) + + // Configure compiler with loaders for different URL schemes + loader := jsonschema.SchemeURLLoader{ + "file": jsonschema.FileLoader{}, + "http": newHTTPURLLoader(), + "https": newHTTPURLLoader(), + } + + compiler := jsonschema.NewCompiler() + compiler.UseLoader(loader) + err = compiler.AddResource("file:///values.schema.json", schema) if err != nil { return err } - if bytes.Equal(valuesJSON, []byte("null")) { - valuesJSON = []byte("{}") - } - schemaLoader := gojsonschema.NewBytesLoader(schemaJSON) - valuesLoader := gojsonschema.NewBytesLoader(valuesJSON) - result, err := gojsonschema.Validate(schemaLoader, valuesLoader) + validator, err := compiler.Compile("file:///values.schema.json") if err != nil { return err } - if !result.Valid() { - var sb strings.Builder - for _, desc := range result.Errors() { - sb.WriteString(fmt.Sprintf("- %s\n", desc)) - } - return errors.New(sb.String()) + err = validator.Validate(values.AsMap()) + if err != nil { + return JSONSchemaValidationError{err} } return nil } + +type JSONSchemaValidationError struct { + embeddedErr error +} + +func (e JSONSchemaValidationError) Error() string { + errStr := e.embeddedErr.Error() + + errStr = strings.TrimPrefix(errStr, "jsonschema validation failed with 'file:///values.schema.json#'\n") + + return errStr + "\n" +} diff --git a/pkg/chartutil/jsonschema_test.go b/pkg/chartutil/jsonschema_test.go index 7610db337..8084b8602 100644 --- a/pkg/chartutil/jsonschema_test.go +++ b/pkg/chartutil/jsonschema_test.go @@ -17,7 +17,10 @@ limitations under the License. package chartutil import ( + "net/http" + "net/http/httptest" "os" + "strings" "testing" "helm.sh/helm/v3/pkg/chart" @@ -55,8 +58,9 @@ func TestValidateAgainstInvalidSingleSchema(t *testing.T) { errString = err.Error() } - expectedErrString := "unable to validate schema: runtime error: invalid " + - "memory address or nil pointer dereference" + 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) } @@ -79,8 +83,8 @@ func TestValidateAgainstSingleSchemaNegative(t *testing.T) { errString = err.Error() } - expectedErrString := `- (root): employmentInfo is required -- age: Must be greater than or equal to 0 + 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) @@ -104,6 +108,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{ @@ -159,9 +178,111 @@ func TestValidateAgainstSchemaNegative(t *testing.T) { } expectedErrString := `subchart: -- (root): age is required +- 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) } } + +func TestHTTPURLLoader_Load(t *testing.T) { + // Test successful JSON schema loading + t.Run("successful load", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"type": "object", "properties": {"name": {"type": "string"}}}`)) + })) + defer server.Close() + + loader := newHTTPURLLoader() + result, err := loader.Load(server.URL) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + if result == nil { + t.Fatal("Expected result to be non-nil") + } + }) + + t.Run("HTTP error status", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + loader := newHTTPURLLoader() + _, err := loader.Load(server.URL) + if err == nil { + t.Fatal("Expected error for HTTP 404") + } + if !strings.Contains(err.Error(), "404") { + t.Errorf("Expected error message to contain '404', got: %v", err) + } + }) +} diff --git a/pkg/getter/httpgetter.go b/pkg/getter/httpgetter.go index df3dcd910..d9ca90759 100644 --- a/pkg/getter/httpgetter.go +++ b/pkg/getter/httpgetter.go @@ -26,7 +26,6 @@ import ( "github.com/pkg/errors" "helm.sh/helm/v3/internal/tlsutil" - "helm.sh/helm/v3/internal/urlutil" "helm.sh/helm/v3/internal/version" ) @@ -133,12 +132,6 @@ func (g *HTTPGetter) httpClient() (*http.Client, error) { return nil, errors.Wrap(err, "can't create TLS config for client") } - sni, err := urlutil.ExtractHostname(g.opts.url) - if err != nil { - return nil, err - } - tlsConf.ServerName = sni - g.transport.TLSClientConfig = tlsConf } diff --git a/pkg/getter/httpgetter_test.go b/pkg/getter/httpgetter_test.go index 2c38c6154..df0ac2438 100644 --- a/pkg/getter/httpgetter_test.go +++ b/pkg/getter/httpgetter_test.go @@ -354,6 +354,122 @@ func TestDownloadTLS(t *testing.T) { } } +func TestDownloadTLSWithRedirect(t *testing.T) { + cd := "../../testdata" + srv2Resp := "hello" + + // Server 2 that will actually fulfil the request. + ca, pub, priv := filepath.Join(cd, "rootca.crt"), filepath.Join(cd, "localhost-crt.pem"), filepath.Join(cd, "key.pem") + tlsConf, err := tlsutil.NewClientTLS(pub, priv, ca, false) + + if err != nil { + t.Fatal(errors.Wrap(err, "can't create TLS config for client")) + } + + tlsSrv2 := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { + rw.Header().Set("Content-Type", "text/plain") + rw.Write([]byte(srv2Resp)) + })) + + tlsSrv2.TLS = tlsConf + tlsSrv2.StartTLS() + defer tlsSrv2.Close() + + // Server 1 responds with a redirect to Server 2. + ca, pub, priv = filepath.Join(cd, "rootca.crt"), filepath.Join(cd, "crt.pem"), filepath.Join(cd, "key.pem") + tlsConf, err = tlsutil.NewClientTLS(pub, priv, ca, false) + + if err != nil { + t.Fatal(errors.Wrap(err, "can't create TLS config for client")) + } + + tlsSrv1 := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + u, _ := url.ParseRequestURI(tlsSrv2.URL) + + // Make the request using the hostname 'localhost' (to which 'localhost-crt.pem' is issued) + // to verify that a successful TLS connection is made even if the client doesn't specify + // the hostname (SNI) in `tls.Config.ServerName`. By default the hostname is derived from the + // request URL for every request (including redirects). Setting `tls.Config.ServerName` on the + // client just overrides the remote endpoint's hostname. + // See https://github.com/golang/go/blob/3979fb9/src/net/http/transport.go#L1505-L1513. + u.Host = fmt.Sprintf("localhost:%s", u.Port()) + + http.Redirect(rw, r, u.String(), http.StatusTemporaryRedirect) + })) + + tlsSrv1.TLS = tlsConf + tlsSrv1.StartTLS() + defer tlsSrv1.Close() + + u, _ := url.ParseRequestURI(tlsSrv1.URL) + + t.Run("Test with TLS", func(t *testing.T) { + g, err := NewHTTPGetter( + WithURL(u.String()), + WithTLSClientConfig(pub, priv, ca), + ) + if err != nil { + t.Fatal(err) + } + + buf, err := g.Get(u.String()) + if err != nil { + t.Error(err) + } + + b, err := io.ReadAll(buf) + if err != nil { + t.Error(err) + } + + if string(b) != srv2Resp { + t.Errorf("expected response from Server2 to be '%s', instead got: %s", srv2Resp, string(b)) + } + }) + + t.Run("Test with TLS config being passed along in .Get (see #6635)", func(t *testing.T) { + g, err := NewHTTPGetter() + if err != nil { + t.Fatal(err) + } + + buf, err := g.Get(u.String(), WithURL(u.String()), WithTLSClientConfig(pub, priv, ca)) + if err != nil { + t.Error(err) + } + + b, err := io.ReadAll(buf) + if err != nil { + t.Error(err) + } + + if string(b) != srv2Resp { + t.Errorf("expected response from Server2 to be '%s', instead got: %s", srv2Resp, string(b)) + } + }) + + t.Run("Test with only the CA file (see also #6635)", func(t *testing.T) { + g, err := NewHTTPGetter() + if err != nil { + t.Fatal(err) + } + + buf, err := g.Get(u.String(), WithURL(u.String()), WithTLSClientConfig("", "", ca)) + if err != nil { + t.Error(err) + } + + b, err := io.ReadAll(buf) + if err != nil { + t.Error(err) + } + + if string(b) != srv2Resp { + t.Errorf("expected response from Server2 to be '%s', instead got: %s", srv2Resp, string(b)) + } + }) +} + func TestDownloadInsecureSkipTLSVerify(t *testing.T) { ts := httptest.NewTLSServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {})) defer ts.Close() @@ -446,9 +562,6 @@ func TestHttpClientInsecureSkipVerify(t *testing.T) { if len(transport.TLSClientConfig.Certificates) <= 0 { t.Fatal("transport.TLSClientConfig.Certificates is not present") } - if transport.TLSClientConfig.ServerName == "" { - t.Fatal("TLSClientConfig.ServerName is blank") - } } func verifyInsecureSkipVerify(t *testing.T, g *HTTPGetter, caseName string, expectedValue bool) *http.Transport { diff --git a/pkg/lint/rules/chartfile.go b/pkg/lint/rules/chartfile.go index 910602b7d..555ec71ba 100644 --- a/pkg/lint/rules/chartfile.go +++ b/pkg/lint/rules/chartfile.go @@ -151,6 +151,9 @@ func validateChartVersion(cf *chart.Metadata) error { 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) { diff --git a/pkg/lint/rules/chartfile_test.go b/pkg/lint/rules/chartfile_test.go index f4c836cf7..4740047be 100644 --- a/pkg/lint/rules/chartfile_test.go +++ b/pkg/lint/rules/chartfile_test.go @@ -143,6 +143,16 @@ func TestValidateChartMaintainer(t *testing.T) { 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) { diff --git a/pkg/lint/rules/values_test.go b/pkg/lint/rules/values_test.go index faa29d48a..824ff2c63 100644 --- a/pkg/lint/rules/values_test.go +++ b/pkg/lint/rules/values_test.go @@ -96,7 +96,7 @@ func TestValidateValuesFileSchemaFailure(t *testing.T) { t.Fatal("expected values file to fail parsing") } - assert.Contains(t, err.Error(), "Expected: string, given: integer", "integer should be caught by schema") + assert.Contains(t, err.Error(), "- at '/username': got number, want string") } func TestValidateValuesFileSchemaOverrides(t *testing.T) { @@ -129,7 +129,7 @@ func TestValidateValuesFile(t *testing.T) { name: "value not overridden", yaml: "username: admin\npassword:", overrides: map[string]interface{}{"username": "anotherUser"}, - errorMessage: "Expected: string, given: null", + errorMessage: "- at '/password': got null, want string", }, { name: "value overridden", diff --git a/pkg/repo/index.go b/pkg/repo/index.go index e1ce3c62d..a93314ab8 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -357,6 +357,7 @@ func loadIndex(data []byte, source string) (*IndexFile, error) { for idx := len(cvs) - 1; idx >= 0; idx-- { if cvs[idx] == nil { log.Printf("skipping loading invalid entry for chart %q from %s: empty entry", name, source) + cvs = append(cvs[:idx], cvs[idx+1:]...) continue } // When metadata section missing, initialize with no data diff --git a/pkg/repo/index_test.go b/pkg/repo/index_test.go index a1e3e9438..1fb30f3c0 100644 --- a/pkg/repo/index_test.go +++ b/pkg/repo/index_test.go @@ -68,6 +68,7 @@ entries: grafana: - apiVersion: v2 name: grafana + - null foo: - bar: diff --git a/testdata/localhost-crt.pem b/testdata/localhost-crt.pem new file mode 100644 index 000000000..70fa0a429 --- /dev/null +++ b/testdata/localhost-crt.pem @@ -0,0 +1,73 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 7f:5e:fa:21:fa:ee:e4:6a:be:9b:c2:80:bf:ed:42:f3:2d:47:f5:d2 + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=CO, L=Boulder, O=Helm, CN=helm.sh + Validity + Not Before: Nov 6 21:59:18 2023 GMT + Not After : Nov 3 21:59:18 2033 GMT + Subject: C=CA, ST=ON, L=Kitchener, O=Helm, CN=localhost + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:c8:89:55:0d:0b:f1:da:e6:c0:70:7d:d3:27:cd: + b8:a8:81:8b:7c:a4:89:e5:d1:b1:78:01:1d:df:44: + 88:0b:fc:d6:81:35:3d:d1:3b:5e:8f:bb:93:b3:7e: + 28:db:ed:ff:a0:13:3a:70:a3:fe:94:6b:0b:fe:fb: + 63:00:b0:cb:dc:81:cd:80:dc:d0:2f:bf:b2:4f:9a: + 81:d4:22:dc:97:c8:8f:27:86:59:91:fa:92:05:75: + c4:cc:6b:f5:a9:6b:74:1e:f5:db:a9:f8:bf:8c:a2: + 25:fd:a0:cc:79:f4:25:57:74:a9:23:9b:e2:b7:22: + 7a:14:7a:3d:ea:f1:7e:32:6b:57:6c:2e:c6:4f:75: + 54:f9:6b:54:d2:ca:eb:54:1c:af:39:15:9b:d0:7c: + 0f:f8:55:51:04:ea:da:fa:7b:8b:63:0f:ac:39:b1: + f6:4b:8e:4e:f6:ea:e9:7b:e6:ba:5e:5a:8e:91:ef: + dc:b1:7d:52:3f:73:83:52:46:83:48:49:ff:f2:2d: + ca:54:f2:36:bb:49:cc:59:99:c0:9e:cf:8e:78:55: + 6c:ed:7d:7e:83:b8:59:2c:7d:f8:1a:81:f0:7d:f5: + 27:f2:db:ae:d4:31:54:38:fe:47:b2:ee:16:20:0f: + f1:db:2d:28:bf:6f:38:eb:11:bb:9a:d4:b2:5a:3a: + 4a:7f + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Alternative Name: + DNS:localhost + Signature Algorithm: sha256WithRSAEncryption + 47:47:fe:29:ca:94:28:75:59:ba:ab:67:ab:c6:a6:0b:0a:f2: + 0f:26:d9:1d:35:db:68:a5:d8:f5:1f:d1:87:e7:a7:74:fd:c0: + 22:aa:c8:ec:6c:d3:ac:8a:0b:ed:59:3a:a0:12:77:7c:53:74: + fd:30:59:34:8f:a4:ef:5b:98:3f:ff:cf:89:87:ed:d3:7f:41: + 2f:b1:9a:12:71:bb:fe:3a:cf:77:16:32:bc:83:90:cc:52:2f: + 3b:f4:ae:db:b1:bb:f0:dd:30:d4:03:17:5e:47:b7:06:86:7a: + 16:b1:72:2f:80:5d:d4:c0:f9:6c:91:df:5a:c5:15:86:66:68: + c8:90:8e:f1:a2:bb:40:0f:ef:26:1b:02:c4:42:de:8c:69:ec: + ad:27:d0:bc:da:7c:76:33:86:de:b7:c4:04:64:e6:f6:dc:44: + 89:7b:b8:2f:c7:28:7a:4c:a6:01:ad:a5:17:64:3a:23:da:aa: + db:ce:3f:86:e9:92:dc:0d:c4:5a:b4:52:a8:8a:ee:3d:62:7d: + b1:c8:fa:ef:96:2b:ab:f1:e1:6d:6f:7d:1e:ce:bc:7a:d0:92: + 02:1b:c8:55:36:77:bf:d4:42:d3:fc:57:ca:b7:cc:95:be:ce: + f8:6e:b2:28:ca:4d:9a:00:7d:78:c8:56:04:2e:b3:ac:03:fa: + 05:d8:42:bd +-----BEGIN CERTIFICATE----- +MIIDRDCCAiygAwIBAgIUf176Ifru5Gq+m8KAv+1C8y1H9dIwDQYJKoZIhvcNAQEL +BQAwTTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNPMRAwDgYDVQQHDAdCb3VsZGVy +MQ0wCwYDVQQKDARIZWxtMRAwDgYDVQQDDAdoZWxtLnNoMB4XDTIzMTEwNjIxNTkx +OFoXDTMzMTEwMzIxNTkxOFowUTELMAkGA1UEBhMCQ0ExCzAJBgNVBAgMAk9OMRIw +EAYDVQQHDAlLaXRjaGVuZXIxDTALBgNVBAoMBEhlbG0xEjAQBgNVBAMMCWxvY2Fs +aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMiJVQ0L8drmwHB9 +0yfNuKiBi3ykieXRsXgBHd9EiAv81oE1PdE7Xo+7k7N+KNvt/6ATOnCj/pRrC/77 +YwCwy9yBzYDc0C+/sk+agdQi3JfIjyeGWZH6kgV1xMxr9alrdB7126n4v4yiJf2g +zHn0JVd0qSOb4rciehR6PerxfjJrV2wuxk91VPlrVNLK61QcrzkVm9B8D/hVUQTq +2vp7i2MPrDmx9kuOTvbq6Xvmul5ajpHv3LF9Uj9zg1JGg0hJ//ItylTyNrtJzFmZ +wJ7PjnhVbO19foO4WSx9+BqB8H31J/LbrtQxVDj+R7LuFiAP8dstKL9vOOsRu5rU +slo6Sn8CAwEAAaMYMBYwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEB +CwUAA4IBAQBHR/4pypQodVm6q2erxqYLCvIPJtkdNdtopdj1H9GH56d0/cAiqsjs +bNOsigvtWTqgEnd8U3T9MFk0j6TvW5g//8+Jh+3Tf0EvsZoScbv+Os93FjK8g5DM +Ui879K7bsbvw3TDUAxdeR7cGhnoWsXIvgF3UwPlskd9axRWGZmjIkI7xortAD+8m +GwLEQt6MaeytJ9C82nx2M4bet8QEZOb23ESJe7gvxyh6TKYBraUXZDoj2qrbzj+G +6ZLcDcRatFKoiu49Yn2xyPrvliur8eFtb30ezrx60JICG8hVNne/1ELT/FfKt8yV +vs74brIoyk2aAH14yFYELrOsA/oF2EK9 +-----END CERTIFICATE----- diff --git a/testdata/openssl.conf b/testdata/openssl.conf index 9b27e445b..be5ff04b7 100644 --- a/testdata/openssl.conf +++ b/testdata/openssl.conf @@ -40,3 +40,7 @@ subjectAltName = @alternate_names [alternate_names] DNS.1 = helm.sh IP.1 = 127.0.0.1 + +# # Used to generate localhost-crt.pem +# [alternate_names] +# DNS.1 = localhost