fix(helm-lint): Add HTTP/HTTPS URL support for json schema references

Signed-off-by: Isaiah Lewis <isaiah@roof12.com>
(cherry picked from commit fa73b6743b)
pull/31166/head
Isaiah Lewis 1 month ago committed by Matt Farina
parent 093c885548
commit 854370978e
No known key found for this signature in database
GPG Key ID: 92C44A3D421FF7F9

@ -21,12 +21,52 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"time"
"github.com/santhosh-tekuri/jsonschema/v6" "github.com/santhosh-tekuri/jsonschema/v6"
"net/http"
"helm.sh/helm/v3/internal/version"
"helm.sh/helm/v3/pkg/chart" "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,
},
})
return &httpLoader
}
// ValidateAgainstSchema checks that values does not violate the structure laid out in schema // 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(chrt *chart.Chart, values map[string]interface{}) error {
var sb strings.Builder var sb strings.Builder
@ -68,7 +108,15 @@ func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) (reterr error
return err return err
} }
// Configure compiler with loaders for different URL schemes
loader := jsonschema.SchemeURLLoader{
"file": jsonschema.FileLoader{},
"http": newHTTPURLLoader(),
"https": newHTTPURLLoader(),
}
compiler := jsonschema.NewCompiler() compiler := jsonschema.NewCompiler()
compiler.UseLoader(loader)
err = compiler.AddResource("file:///values.schema.json", schema) err = compiler.AddResource("file:///values.schema.json", schema)
if err != nil { if err != nil {
return err return err

@ -17,7 +17,10 @@ limitations under the License.
package chartutil package chartutil
import ( import (
"net/http"
"net/http/httptest"
"os" "os"
"strings"
"testing" "testing"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
@ -105,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) { func TestValidateAgainstSchema(t *testing.T) {
subchartJSON := []byte(subchartSchema) subchartJSON := []byte(subchartSchema)
subchart := &chart.Chart{ subchart := &chart.Chart{
@ -166,3 +184,105 @@ func TestValidateAgainstSchemaNegative(t *testing.T) {
t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", 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)
}
})
}

Loading…
Cancel
Save