From 7aba8bbfe616b1c866655a7df4e443c9b10ee5ca Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Wed, 10 Sep 2025 11:36:07 +0200 Subject: [PATCH 1/8] Resolve JSON schema reference paths for relative imports When validating values against JSON schemas, relative file references in the schema (like "$ref": "./other-schema.json") were failing because in some cases. The JSON schema validator couldn't resolve the correct base directory. This change passes the absolute base directory to the schema validator, allowing it to properly resolve relative JSON schema references by constructing the correct file:// URLs with the proper path context. Tested with: - `helm lint .` - `helm lint subfolder` Fixes #31260 Signed-off-by: Benoit Tigeot Add test --- internal/chart/v3/lint/rules/values.go | 4 +- pkg/chart/common/util/jsonschema.go | 11 +-- pkg/chart/common/util/jsonschema_test.go | 82 +++++++++++++++---- .../current-dir-test/base.schema.json | 10 +++ .../current-dir-test/test-values.yaml | 3 + .../current-dir-test/values.schema.json | 14 ++++ .../testdata/subdir-test/shared.schema.json | 10 +++ .../subdir-test/subfolder/test-values.yaml | 3 + .../subdir-test/subfolder/values.schema.json | 14 ++++ pkg/chart/v2/lint/rules/values.go | 4 +- 10 files changed, 132 insertions(+), 23 deletions(-) create mode 100644 pkg/chart/common/util/testdata/current-dir-test/base.schema.json create mode 100644 pkg/chart/common/util/testdata/current-dir-test/test-values.yaml create mode 100644 pkg/chart/common/util/testdata/current-dir-test/values.schema.json create mode 100644 pkg/chart/common/util/testdata/subdir-test/shared.schema.json create mode 100644 pkg/chart/common/util/testdata/subdir-test/subfolder/test-values.yaml create mode 100644 pkg/chart/common/util/testdata/subdir-test/subfolder/values.schema.json diff --git a/internal/chart/v3/lint/rules/values.go b/internal/chart/v3/lint/rules/values.go index ba371cbe2..43eec40c2 100644 --- a/internal/chart/v3/lint/rules/values.go +++ b/internal/chart/v3/lint/rules/values.go @@ -75,9 +75,9 @@ func validateValuesFile(valuesPath string, overrides map[string]any, skipSchemaV if err != nil { return err } - + baseDir := filepath.Dir(schemaPath) if !skipSchemaValidation { - return util.ValidateAgainstSingleSchema(coalescedValues, schema) + return util.ValidateAgainstSingleSchema(coalescedValues, schema, baseDir) } return nil diff --git a/pkg/chart/common/util/jsonschema.go b/pkg/chart/common/util/jsonschema.go index 63ca0c274..a24ee3a21 100644 --- a/pkg/chart/common/util/jsonschema.go +++ b/pkg/chart/common/util/jsonschema.go @@ -23,6 +23,7 @@ import ( "fmt" "log/slog" "net/http" + "path/filepath" "strings" "sync" "time" @@ -80,8 +81,7 @@ func ValidateAgainstSchema(ch chart.Charter, values map[string]any) error { } var sb strings.Builder if chrt.Schema() != nil { - slog.Debug("chart name", "chart-name", chrt.Name()) - err := ValidateAgainstSingleSchema(values, chrt.Schema()) + err := ValidateAgainstSingleSchema(values, chrt.Schema(), chrt.ChartFullPath()) if err != nil { fmt.Fprintf(&sb, "%s:\n", chrt.Name()) sb.WriteString(err.Error()) @@ -121,7 +121,7 @@ func ValidateAgainstSchema(ch chart.Charter, values map[string]any) error { } // ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema -func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte) (reterr error) { +func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte, absBaseDir string) (reterr error) { defer func() { if r := recover(); r != nil { reterr = fmt.Errorf("unable to validate schema: %s", r) @@ -146,12 +146,13 @@ func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte) (reter compiler := jsonschema.NewCompiler() compiler.UseLoader(loader) - err = compiler.AddResource("file:///values.schema.json", schema) + base := "file://" + filepath.ToSlash(absBaseDir) + "/values.schema.json" + err = compiler.AddResource(base, schema) if err != nil { return err } - validator, err := compiler.Compile("file:///values.schema.json") + validator, err := compiler.Compile(base) if err != nil { return err } diff --git a/pkg/chart/common/util/jsonschema_test.go b/pkg/chart/common/util/jsonschema_test.go index 24073175c..06c518d73 100644 --- a/pkg/chart/common/util/jsonschema_test.go +++ b/pkg/chart/common/util/jsonschema_test.go @@ -20,6 +20,7 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" "strings" "testing" @@ -37,7 +38,7 @@ func TestValidateAgainstSingleSchema(t *testing.T) { t.Fatalf("Error reading YAML file: %s", err) } - if err := ValidateAgainstSingleSchema(values, schema); err != nil { + if err := ValidateAgainstSingleSchema(values, schema, ""); err != nil { t.Errorf("Error validating Values against Schema: %s", err) } } @@ -53,7 +54,7 @@ func TestValidateAgainstInvalidSingleSchema(t *testing.T) { } var errString string - if err := ValidateAgainstSingleSchema(values, schema); err == nil { + if err := ValidateAgainstSingleSchema(values, schema, ""); err == nil { t.Fatalf("Expected an error, but got nil") } else { errString = err.Error() @@ -77,7 +78,7 @@ func TestValidateAgainstSingleSchemaNegative(t *testing.T) { } var errString string - if err := ValidateAgainstSingleSchema(values, schema); err == nil { + if err := ValidateAgainstSingleSchema(values, schema, ""); err == nil { t.Fatalf("Expected an error, but got nil") } else { errString = err.Error() @@ -177,11 +178,12 @@ func TestValidateAgainstSchemaNegative(t *testing.T) { 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) + expectedValidationError := "missing property 'age'" + if !strings.Contains(errString, "subchart:") { + t.Errorf("Error string should contain 'subchart:', got: %s", errString) + } + if !strings.Contains(errString, expectedValidationError) { + t.Errorf("Error string should contain '%s', got: %s", expectedValidationError, errString) } } @@ -241,12 +243,64 @@ func TestValidateAgainstSchema2020Negative(t *testing.T) { 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) + expectedValidationErrors := []string{ + "no items match contains schema", + "got number, want string", + } + if !strings.Contains(errString, "subchart:") { + t.Errorf("Error string should contain 'subchart:', got: %s", errString) + } + for _, expectedErr := range expectedValidationErrors { + if !strings.Contains(errString, expectedErr) { + t.Errorf("Error string should contain '%s', got: %s", expectedErr, errString) + } + } +} + +// TestValidateWithRelativeSchemaReferences tests schema validation with relative $ref paths +// This mimics the behavior of "helm lint ." where the schema is in the current directory +func TestValidateWithRelativeSchemaReferencesCurrentDir(t *testing.T) { + values, err := common.ReadValuesFile("./testdata/current-dir-test/test-values.yaml") + if err != nil { + t.Fatalf("Error reading YAML file: %s", err) + } + schema, err := os.ReadFile("./testdata/current-dir-test/values.schema.json") + if err != nil { + t.Fatalf("Error reading JSON schema file: %s", err) + } + + // Test with absolute base directory - this should work with your fix + baseDir := "./testdata/current-dir-test" + absBaseDir, err := filepath.Abs(baseDir) + if err != nil { + t.Fatalf("Error getting absolute path: %s", err) + } + + if err := ValidateAgainstSingleSchema(values, schema, absBaseDir); err != nil { + t.Errorf("Error validating Values against Schema with relative references: %s", err) + } +} + +// TestValidateWithRelativeSchemaReferencesSubfolder tests schema validation with relative $ref paths +// This mimics the behavior of "helm lint subfolder" where the schema is in a subdirectory +func TestValidateWithRelativeSchemaReferencesSubfolder(t *testing.T) { + values, err := common.ReadValuesFile("./testdata/subdir-test/subfolder/test-values.yaml") + if err != nil { + t.Fatalf("Error reading YAML file: %s", err) + } + schema, err := os.ReadFile("./testdata/subdir-test/subfolder/values.schema.json") + if err != nil { + t.Fatalf("Error reading JSON schema file: %s", err) + } + + baseDir := "./testdata/subdir-test/subfolder" + absBaseDir, err := filepath.Abs(baseDir) + if err != nil { + t.Fatalf("Error getting absolute path: %s", err) + } + + if err := ValidateAgainstSingleSchema(values, schema, absBaseDir); err != nil { + t.Errorf("Error validating Values against Schema with relative references from subfolder: %s", err) } } diff --git a/pkg/chart/common/util/testdata/current-dir-test/base.schema.json b/pkg/chart/common/util/testdata/current-dir-test/base.schema.json new file mode 100644 index 000000000..fddf95114 --- /dev/null +++ b/pkg/chart/common/util/testdata/current-dir-test/base.schema.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": ["name"] +} diff --git a/pkg/chart/common/util/testdata/current-dir-test/test-values.yaml b/pkg/chart/common/util/testdata/current-dir-test/test-values.yaml new file mode 100644 index 000000000..817fa4bd1 --- /dev/null +++ b/pkg/chart/common/util/testdata/current-dir-test/test-values.yaml @@ -0,0 +1,3 @@ +user: + name: "John Doe" +age: 30 diff --git a/pkg/chart/common/util/testdata/current-dir-test/values.schema.json b/pkg/chart/common/util/testdata/current-dir-test/values.schema.json new file mode 100644 index 000000000..a4cf4a53d --- /dev/null +++ b/pkg/chart/common/util/testdata/current-dir-test/values.schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "user": { + "$ref": "./base.schema.json" + }, + "age": { + "type": "integer", + "minimum": 0 + } + }, + "required": ["user", "age"] +} diff --git a/pkg/chart/common/util/testdata/subdir-test/shared.schema.json b/pkg/chart/common/util/testdata/subdir-test/shared.schema.json new file mode 100644 index 000000000..2bb750058 --- /dev/null +++ b/pkg/chart/common/util/testdata/subdir-test/shared.schema.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "config": { + "type": "string" + } + }, + "required": ["config"] +} diff --git a/pkg/chart/common/util/testdata/subdir-test/subfolder/test-values.yaml b/pkg/chart/common/util/testdata/subdir-test/subfolder/test-values.yaml new file mode 100644 index 000000000..b888e30f0 --- /dev/null +++ b/pkg/chart/common/util/testdata/subdir-test/subfolder/test-values.yaml @@ -0,0 +1,3 @@ +appConfig: + config: "production" +replicas: 3 diff --git a/pkg/chart/common/util/testdata/subdir-test/subfolder/values.schema.json b/pkg/chart/common/util/testdata/subdir-test/subfolder/values.schema.json new file mode 100644 index 000000000..9a50779b7 --- /dev/null +++ b/pkg/chart/common/util/testdata/subdir-test/subfolder/values.schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "appConfig": { + "$ref": "../shared.schema.json" + }, + "replicas": { + "type": "integer", + "minimum": 1 + } + }, + "required": ["appConfig", "replicas"] +} diff --git a/pkg/chart/v2/lint/rules/values.go b/pkg/chart/v2/lint/rules/values.go index 8fe849c7a..d06875320 100644 --- a/pkg/chart/v2/lint/rules/values.go +++ b/pkg/chart/v2/lint/rules/values.go @@ -75,9 +75,9 @@ func validateValuesFile(valuesPath string, overrides map[string]any, skipSchemaV if err != nil { return err } - + baseDir := filepath.Dir(schemaPath) if !skipSchemaValidation { - return util.ValidateAgainstSingleSchema(coalescedValues, schema) + return util.ValidateAgainstSingleSchema(coalescedValues, schema, baseDir) } return nil From abf95de91e78953206dbe3fa98ce9313737c6fa9 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Wed, 10 Sep 2025 12:09:07 +0200 Subject: [PATCH 2/8] Do not remove necessary logs Signed-off-by: Benoit Tigeot --- pkg/chart/common/util/jsonschema.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/chart/common/util/jsonschema.go b/pkg/chart/common/util/jsonschema.go index a24ee3a21..b910f776b 100644 --- a/pkg/chart/common/util/jsonschema.go +++ b/pkg/chart/common/util/jsonschema.go @@ -81,6 +81,7 @@ func ValidateAgainstSchema(ch chart.Charter, values map[string]any) error { } var sb strings.Builder if chrt.Schema() != nil { + slog.Debug("chart name", "chart-name", chrt.Name()) err := ValidateAgainstSingleSchema(values, chrt.Schema(), chrt.ChartFullPath()) if err != nil { fmt.Fprintf(&sb, "%s:\n", chrt.Name()) From 30d803283d4a635e9da26bbfb69bcc9d2854613d Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Wed, 10 Sep 2025 12:35:53 +0200 Subject: [PATCH 3/8] Fix error message trimming for dynamic schema paths in JSON validation Previously, the error trimming was hardcoded to remove only: 'jsonschema validation failed with 'file:///values.schema.json#'' After the fix for relative schema references (issue #31260), schema URLs now include the full absolute path, making error messages look like: 'jsonschema validation failed with 'file:///full/absolute/path/values.schema.json#'' Signed-off-by: Benoit Tigeot --- pkg/chart/common/util/jsonschema.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/chart/common/util/jsonschema.go b/pkg/chart/common/util/jsonschema.go index b910f776b..8a37e103e 100644 --- a/pkg/chart/common/util/jsonschema.go +++ b/pkg/chart/common/util/jsonschema.go @@ -211,7 +211,11 @@ func (e JSONSchemaValidationError) Error() string { // 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") + if strings.HasPrefix(errStr, "jsonschema validation failed with ") { + if idx := strings.Index(errStr, "#'\n"); idx != -1 { + errStr = errStr[idx+3:] + } + } // The extra new line is needed for when there are sub-charts. return errStr + "\n" From a7884d228d7c1e231f43c5b281596420b4eceb6a Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Sun, 22 Feb 2026 14:25:39 +0100 Subject: [PATCH 4/8] fix(schema): Add schema $ref resolution for all commands Extends JSON schema validation to support relative $ref imports across helm lint, install, template, and upgrade commands. Addresses George's review comments: - Pass chartDir directly instead of deriving from valuesPath - Create new functions instead of changing public API signatures - Use absolute file paths for proper URL-based $ref resolution Implementation: - Added ValidateAgainstSchemaWithPath to resolve $ref using absolute file paths instead of root-relative URLs - Thread chartDir through Install/Upgrade structs and all validation functions - Updated both v2 (public) and v3 (internal) lint rules Fixes relative $ref resolution when running commands like: helm lint ../sample-chart helm install release ../sample-chart Signed-off-by: Benoit Tigeot --- internal/chart/v3/lint/rules/template.go | 2 +- internal/chart/v3/lint/rules/values.go | 13 +++--- internal/chart/v3/lint/rules/values_test.go | 21 +++------ pkg/action/install.go | 5 ++- pkg/action/lint_test.go | 4 ++ .../charts/chart-with-schema-ref/Chart.yaml | 3 ++ .../chart-with-schema-ref/name.schema.json | 4 ++ .../chart-with-schema-ref/values.schema.json | 7 +++ .../charts/chart-with-schema-ref/values.yaml | 1 + pkg/action/upgrade.go | 5 ++- pkg/chart/common/util/jsonschema.go | 43 ++++++++++++++----- pkg/chart/common/util/jsonschema_test.go | 23 +++++----- pkg/chart/common/util/values.go | 8 +++- pkg/chart/v2/lint/rules/template.go | 2 +- pkg/chart/v2/lint/rules/values.go | 13 +++--- pkg/chart/v2/lint/rules/values_test.go | 21 +++------ pkg/cmd/install.go | 1 + pkg/cmd/install_test.go | 5 +++ pkg/cmd/template_test.go | 5 +++ pkg/cmd/testdata/output/schema-ref.txt | 7 +++ .../testdata/output/template-schema-ref.txt | 1 + .../testdata/output/upgrade-schema-ref.txt | 8 ++++ .../testdata/testcharts/chart-with-schema-ref | 1 + pkg/cmd/upgrade.go | 1 + pkg/cmd/upgrade_test.go | 6 +++ 25 files changed, 140 insertions(+), 70 deletions(-) create mode 100644 pkg/action/testdata/charts/chart-with-schema-ref/Chart.yaml create mode 100644 pkg/action/testdata/charts/chart-with-schema-ref/name.schema.json create mode 100644 pkg/action/testdata/charts/chart-with-schema-ref/values.schema.json create mode 100644 pkg/action/testdata/charts/chart-with-schema-ref/values.yaml create mode 100644 pkg/cmd/testdata/output/schema-ref.txt create mode 100644 pkg/cmd/testdata/output/template-schema-ref.txt create mode 100644 pkg/cmd/testdata/output/upgrade-schema-ref.txt create mode 120000 pkg/cmd/testdata/testcharts/chart-with-schema-ref diff --git a/internal/chart/v3/lint/rules/template.go b/internal/chart/v3/lint/rules/template.go index 35e4940ab..22f2e39bf 100644 --- a/internal/chart/v3/lint/rules/template.go +++ b/internal/chart/v3/lint/rules/template.go @@ -97,7 +97,7 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string return } - valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, skipSchemaValidation) + valuesToRender, err := util.ToRenderValuesWithSchemaValidationAndPath(chart, cvals, options, caps, skipSchemaValidation, linter.ChartDir) if err != nil { linter.RunLinterRule(support.ErrorSev, fpath, err) return diff --git a/internal/chart/v3/lint/rules/values.go b/internal/chart/v3/lint/rules/values.go index 43eec40c2..2083a9bbd 100644 --- a/internal/chart/v3/lint/rules/values.go +++ b/internal/chart/v3/lint/rules/values.go @@ -41,7 +41,7 @@ func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]any, return } - linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, valueOverrides, skipSchemaValidation)) + linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(linter.ChartDir, valueOverrides, skipSchemaValidation)) } func validateValuesFileExistence(valuesPath string) error { @@ -52,7 +52,10 @@ func validateValuesFileExistence(valuesPath string) error { return nil } -func validateValuesFile(valuesPath string, overrides map[string]any, skipSchemaValidation bool) error { +func validateValuesFile(chartDir string, overrides map[string]any, skipSchemaValidation bool) error { + valuesPath := filepath.Join(chartDir, "values.yaml") + schemaPath := filepath.Join(chartDir, "values.schema.json") + values, err := common.ReadValuesFile(valuesPath) if err != nil { return fmt.Errorf("unable to parse YAML: %w", err) @@ -66,8 +69,6 @@ func validateValuesFile(valuesPath string, overrides map[string]any, skipSchemaV coalescedValues := util.CoalesceTables(make(map[string]any, 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 @@ -75,9 +76,9 @@ func validateValuesFile(valuesPath string, overrides map[string]any, skipSchemaV if err != nil { return err } - baseDir := filepath.Dir(schemaPath) + if !skipSchemaValidation { - return util.ValidateAgainstSingleSchema(coalescedValues, schema, baseDir) + return util.ValidateAgainstSingleSchemaWithPath(coalescedValues, schema, schemaPath) } return nil diff --git a/internal/chart/v3/lint/rules/values_test.go b/internal/chart/v3/lint/rules/values_test.go index afc544ebd..b40c7b7de 100644 --- a/internal/chart/v3/lint/rules/values_test.go +++ b/internal/chart/v3/lint/rules/values_test.go @@ -66,8 +66,7 @@ func TestValidateValuesFileWellFormed(t *testing.T) { not:well[]{}formed ` tmpdir := ensure.TempFile(t, "values.yaml", []byte(badYaml)) - valfile := filepath.Join(tmpdir, "values.yaml") - if err := validateValuesFile(valfile, map[string]any{}, false); err == nil { + if err := validateValuesFile(tmpdir, map[string]any{}, false); err == nil { t.Fatal("expected values file to fail parsing") } } @@ -77,8 +76,7 @@ func TestValidateValuesFileSchema(t *testing.T) { tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) createTestingSchema(t, tmpdir) - valfile := filepath.Join(tmpdir, "values.yaml") - if err := validateValuesFile(valfile, map[string]any{}, false); err != nil { + if err := validateValuesFile(tmpdir, map[string]any{}, false); err != nil { t.Fatalf("Failed validation with %s", err) } } @@ -89,9 +87,7 @@ func TestValidateValuesFileSchemaFailure(t *testing.T) { tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) createTestingSchema(t, tmpdir) - valfile := filepath.Join(tmpdir, "values.yaml") - - err := validateValuesFile(valfile, map[string]any{}, false) + err := validateValuesFile(tmpdir, map[string]any{}, false) if err == nil { t.Fatal("expected values file to fail parsing") } @@ -105,9 +101,7 @@ func TestValidateValuesFileSchemaFailureButWithSkipSchemaValidation(t *testing.T tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) createTestingSchema(t, tmpdir) - valfile := filepath.Join(tmpdir, "values.yaml") - - err := validateValuesFile(valfile, map[string]any{}, true) + err := validateValuesFile(tmpdir, map[string]any{}, true) if err != nil { t.Fatal("expected values file to pass parsing because of skipSchemaValidation") } @@ -121,8 +115,7 @@ func TestValidateValuesFileSchemaOverrides(t *testing.T) { tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) createTestingSchema(t, tmpdir) - valfile := filepath.Join(tmpdir, "values.yaml") - if err := validateValuesFile(valfile, overrides, false); err != nil { + if err := validateValuesFile(tmpdir, overrides, false); err != nil { t.Fatalf("Failed validation with %s", err) } } @@ -157,9 +150,7 @@ func TestValidateValuesFile(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, false) + err := validateValuesFile(tmpdir, tt.overrides, false) switch { case err != nil && tt.errorMessage == "": diff --git a/pkg/action/install.go b/pkg/action/install.go index 2f747a789..1e138b3c7 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -127,6 +127,9 @@ type Install struct { // Used by helm template to add the release as part of OutputDir path // OutputDir/ UseReleaseName bool + // ChartDir is the local directory path of the chart, used for resolving + // relative $ref in JSON schemas. Empty for remote charts. + ChartDir string // TakeOwnership will ignore the check for helm annotations and take ownership of the resources. TakeOwnership bool PostRenderer postrenderer.PostRenderer @@ -358,7 +361,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st IsInstall: !isUpgrade, IsUpgrade: isUpgrade, } - valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chrt, vals, options, caps, i.SkipSchemaValidation) + valuesToRender, err := util.ToRenderValuesWithSchemaValidationAndPath(chrt, vals, options, caps, i.SkipSchemaValidation, i.ChartDir) if err != nil { return nil, err } diff --git a/pkg/action/lint_test.go b/pkg/action/lint_test.go index 6ee1e07fa..63b2f51ae 100644 --- a/pkg/action/lint_test.go +++ b/pkg/action/lint_test.go @@ -71,6 +71,10 @@ func TestLintChart(t *testing.T) { name: "chart-with-schema", chartPath: "testdata/charts/chart-with-schema", }, + { + name: "chart-with-schema-ref", + chartPath: "testdata/charts/chart-with-schema-ref", + }, { name: "chart-with-schema-negative", chartPath: "testdata/charts/chart-with-schema-negative", diff --git a/pkg/action/testdata/charts/chart-with-schema-ref/Chart.yaml b/pkg/action/testdata/charts/chart-with-schema-ref/Chart.yaml new file mode 100644 index 000000000..c344a04d9 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-schema-ref/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: chart-with-schema-ref +version: 0.1.0 diff --git a/pkg/action/testdata/charts/chart-with-schema-ref/name.schema.json b/pkg/action/testdata/charts/chart-with-schema-ref/name.schema.json new file mode 100644 index 000000000..290e9cca5 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-schema-ref/name.schema.json @@ -0,0 +1,4 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string" +} diff --git a/pkg/action/testdata/charts/chart-with-schema-ref/values.schema.json b/pkg/action/testdata/charts/chart-with-schema-ref/values.schema.json new file mode 100644 index 000000000..e253c4c7e --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-schema-ref/values.schema.json @@ -0,0 +1,7 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { "$ref": "name.schema.json" } + } +} diff --git a/pkg/action/testdata/charts/chart-with-schema-ref/values.yaml b/pkg/action/testdata/charts/chart-with-schema-ref/values.yaml new file mode 100644 index 000000000..0b9fc7e3a --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-schema-ref/values.yaml @@ -0,0 +1 @@ +name: "test" diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 4c5a5b55e..7376c34eb 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -113,6 +113,9 @@ type Upgrade struct { HideNotes bool // SkipSchemaValidation determines if JSON schema validation is disabled. SkipSchemaValidation bool + // ChartDir is the local directory path of the chart, used for resolving + // relative $ref in JSON schemas. Empty for remote charts. + ChartDir string // Description is the description of this operation Description string Labels map[string]string @@ -291,7 +294,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str if err != nil { return nil, nil, false, err } - valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, vals, options, caps, u.SkipSchemaValidation) + valuesToRender, err := util.ToRenderValuesWithSchemaValidationAndPath(chart, vals, options, caps, u.SkipSchemaValidation, u.ChartDir) if err != nil { return nil, nil, false, err } diff --git a/pkg/chart/common/util/jsonschema.go b/pkg/chart/common/util/jsonschema.go index 8a37e103e..e34691f7d 100644 --- a/pkg/chart/common/util/jsonschema.go +++ b/pkg/chart/common/util/jsonschema.go @@ -75,14 +75,31 @@ func newHTTPURLLoader() *HTTPURLLoader { // ValidateAgainstSchema checks that values does not violate the structure laid out in schema func ValidateAgainstSchema(ch chart.Charter, values map[string]any) error { + return ValidateAgainstSchemaWithPath(ch, values, "") +} + +func ValidateAgainstSchemaWithPath(ch chart.Charter, values map[string]any, chartDir string) error { chrt, err := chart.NewAccessor(ch) if err != nil { return err } + + var absChartPath string + if chartDir != "" { + absChartPath, err = filepath.Abs(chartDir) + } else { + absChartPath, err = filepath.Abs(chrt.ChartFullPath()) + } + if err != nil { + return err + } + var sb strings.Builder if chrt.Schema() != nil { slog.Debug("chart name", "chart-name", chrt.Name()) - err := ValidateAgainstSingleSchema(values, chrt.Schema(), chrt.ChartFullPath()) + + schemaPath := filepath.Join(absChartPath, "values.schema.json") + err = ValidateAgainstSingleSchemaWithPath(values, chrt.Schema(), schemaPath) if err != nil { fmt.Fprintf(&sb, "%s:\n", chrt.Name()) sb.WriteString(err.Error()) @@ -109,7 +126,8 @@ func ValidateAgainstSchema(ch chart.Charter, values map[string]any) error { continue } - if err := ValidateAgainstSchema(subchart, subchartValues); err != nil { + subchartPath := filepath.Join(absChartPath, "charts", sub.Name()) + if err := ValidateAgainstSchemaWithPath(subchart, subchartValues, subchartPath); err != nil { sb.WriteString(err.Error()) } } @@ -122,7 +140,13 @@ func ValidateAgainstSchema(ch chart.Charter, values map[string]any) error { } // ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema -func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte, absBaseDir string) (reterr error) { +func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte) (reterr error) { + return ValidateAgainstSingleSchemaWithPath(values, schemaJSON, "/values.schema.json") +} + +// ValidateAgainstSingleSchemaWithPath checks that values does not violate the structure laid out in this schema. +// schemaPath is the absolute path to the schema file, used to resolve relative $ref references. +func ValidateAgainstSingleSchemaWithPath(values common.Values, schemaJSON []byte, schemaPath string) (reterr error) { defer func() { if r := recover(); r != nil { reterr = fmt.Errorf("unable to validate schema: %s", r) @@ -147,13 +171,14 @@ func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte, absBas compiler := jsonschema.NewCompiler() compiler.UseLoader(loader) - base := "file://" + filepath.ToSlash(absBaseDir) + "/values.schema.json" - err = compiler.AddResource(base, schema) + + schemaURL := fmt.Sprintf("file://%s", schemaPath) + err = compiler.AddResource(schemaURL, schema) if err != nil { return err } - validator, err := compiler.Compile(base) + validator, err := compiler.Compile(schemaURL) if err != nil { return err } @@ -211,11 +236,7 @@ func (e JSONSchemaValidationError) Error() string { // 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. - if strings.HasPrefix(errStr, "jsonschema validation failed with ") { - if idx := strings.Index(errStr, "#'\n"); idx != -1 { - errStr = errStr[idx+3:] - } - } + 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/pkg/chart/common/util/jsonschema_test.go b/pkg/chart/common/util/jsonschema_test.go index 06c518d73..33e82cf5f 100644 --- a/pkg/chart/common/util/jsonschema_test.go +++ b/pkg/chart/common/util/jsonschema_test.go @@ -38,7 +38,7 @@ func TestValidateAgainstSingleSchema(t *testing.T) { t.Fatalf("Error reading YAML file: %s", err) } - if err := ValidateAgainstSingleSchema(values, schema, ""); err != nil { + if err := ValidateAgainstSingleSchema(values, schema); err != nil { t.Errorf("Error validating Values against Schema: %s", err) } } @@ -54,7 +54,7 @@ func TestValidateAgainstInvalidSingleSchema(t *testing.T) { } var errString string - if err := ValidateAgainstSingleSchema(values, schema, ""); err == nil { + if err := ValidateAgainstSingleSchema(values, schema); err == nil { t.Fatalf("Expected an error, but got nil") } else { errString = err.Error() @@ -78,7 +78,7 @@ func TestValidateAgainstSingleSchemaNegative(t *testing.T) { } var errString string - if err := ValidateAgainstSingleSchema(values, schema, ""); err == nil { + if err := ValidateAgainstSingleSchema(values, schema); err == nil { t.Fatalf("Expected an error, but got nil") } else { errString = err.Error() @@ -264,19 +264,18 @@ func TestValidateWithRelativeSchemaReferencesCurrentDir(t *testing.T) { if err != nil { t.Fatalf("Error reading YAML file: %s", err) } - schema, err := os.ReadFile("./testdata/current-dir-test/values.schema.json") + schemaPath := "./testdata/current-dir-test/values.schema.json" + schema, err := os.ReadFile(schemaPath) if err != nil { t.Fatalf("Error reading JSON schema file: %s", err) } - // Test with absolute base directory - this should work with your fix - baseDir := "./testdata/current-dir-test" - absBaseDir, err := filepath.Abs(baseDir) + absSchemaPath, err := filepath.Abs(schemaPath) if err != nil { t.Fatalf("Error getting absolute path: %s", err) } - if err := ValidateAgainstSingleSchema(values, schema, absBaseDir); err != nil { + if err := ValidateAgainstSingleSchemaWithPath(values, schema, absSchemaPath); err != nil { t.Errorf("Error validating Values against Schema with relative references: %s", err) } } @@ -288,18 +287,18 @@ func TestValidateWithRelativeSchemaReferencesSubfolder(t *testing.T) { if err != nil { t.Fatalf("Error reading YAML file: %s", err) } - schema, err := os.ReadFile("./testdata/subdir-test/subfolder/values.schema.json") + schemaPath := "./testdata/subdir-test/subfolder/values.schema.json" + schema, err := os.ReadFile(schemaPath) if err != nil { t.Fatalf("Error reading JSON schema file: %s", err) } - baseDir := "./testdata/subdir-test/subfolder" - absBaseDir, err := filepath.Abs(baseDir) + absSchemaPath, err := filepath.Abs(schemaPath) if err != nil { t.Fatalf("Error getting absolute path: %s", err) } - if err := ValidateAgainstSingleSchema(values, schema, absBaseDir); err != nil { + if err := ValidateAgainstSingleSchemaWithPath(values, schema, absSchemaPath); err != nil { t.Errorf("Error validating Values against Schema with relative references from subfolder: %s", err) } } diff --git a/pkg/chart/common/util/values.go b/pkg/chart/common/util/values.go index 95ac7ba4d..492e99f33 100644 --- a/pkg/chart/common/util/values.go +++ b/pkg/chart/common/util/values.go @@ -34,6 +34,12 @@ func ToRenderValues(chrt chart.Charter, chrtVals map[string]any, options common. // // This takes both ReleaseOptions and Capabilities to merge into the render values. func ToRenderValuesWithSchemaValidation(chrt chart.Charter, chrtVals map[string]any, options common.ReleaseOptions, caps *common.Capabilities, skipSchemaValidation bool) (common.Values, error) { + return ToRenderValuesWithSchemaValidationAndPath(chrt, chrtVals, options, caps, skipSchemaValidation, "") +} + +// ToRenderValuesWithSchemaValidationAndPath is like ToRenderValuesWithSchemaValidation but accepts chartDir +// for resolving relative $ref in JSON schemas. +func ToRenderValuesWithSchemaValidationAndPath(chrt chart.Charter, chrtVals map[string]any, options common.ReleaseOptions, caps *common.Capabilities, skipSchemaValidation bool, chartDir string) (common.Values, error) { if caps == nil { caps = common.DefaultCapabilities } @@ -60,7 +66,7 @@ func ToRenderValuesWithSchemaValidation(chrt chart.Charter, chrtVals map[string] } if !skipSchemaValidation { - if err := ValidateAgainstSchema(chrt, vals); err != nil { + if err := ValidateAgainstSchemaWithPath(chrt, vals, chartDir); err != nil { return top, fmt.Errorf("values don't meet the specifications of the schema(s) in the following chart(s):\n%w", err) } } diff --git a/pkg/chart/v2/lint/rules/template.go b/pkg/chart/v2/lint/rules/template.go index 43665aa3a..97a403b2e 100644 --- a/pkg/chart/v2/lint/rules/template.go +++ b/pkg/chart/v2/lint/rules/template.go @@ -128,7 +128,7 @@ func (t *templateLinter) Lint() { return } - valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, t.skipSchemaValidation) + valuesToRender, err := util.ToRenderValuesWithSchemaValidationAndPath(chart, cvals, options, caps, t.skipSchemaValidation, t.linter.ChartDir) if err != nil { t.linter.RunLinterRule(support.ErrorSev, templatesDir, err) return diff --git a/pkg/chart/v2/lint/rules/values.go b/pkg/chart/v2/lint/rules/values.go index d06875320..77caf130b 100644 --- a/pkg/chart/v2/lint/rules/values.go +++ b/pkg/chart/v2/lint/rules/values.go @@ -41,7 +41,7 @@ func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]any, return } - linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, valueOverrides, skipSchemaValidation)) + linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(linter.ChartDir, valueOverrides, skipSchemaValidation)) } func validateValuesFileExistence(valuesPath string) error { @@ -52,7 +52,10 @@ func validateValuesFileExistence(valuesPath string) error { return nil } -func validateValuesFile(valuesPath string, overrides map[string]any, skipSchemaValidation bool) error { +func validateValuesFile(chartDir string, overrides map[string]any, skipSchemaValidation bool) error { + valuesPath := filepath.Join(chartDir, "values.yaml") + schemaPath := filepath.Join(chartDir, "values.schema.json") + values, err := common.ReadValuesFile(valuesPath) if err != nil { return fmt.Errorf("unable to parse YAML: %w", err) @@ -66,8 +69,6 @@ func validateValuesFile(valuesPath string, overrides map[string]any, skipSchemaV coalescedValues := util.CoalesceTables(make(map[string]any, 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 @@ -75,9 +76,9 @@ func validateValuesFile(valuesPath string, overrides map[string]any, skipSchemaV if err != nil { return err } - baseDir := filepath.Dir(schemaPath) + if !skipSchemaValidation { - return util.ValidateAgainstSingleSchema(coalescedValues, schema, baseDir) + return util.ValidateAgainstSingleSchemaWithPath(coalescedValues, schema, schemaPath) } return nil diff --git a/pkg/chart/v2/lint/rules/values_test.go b/pkg/chart/v2/lint/rules/values_test.go index afc544ebd..b40c7b7de 100644 --- a/pkg/chart/v2/lint/rules/values_test.go +++ b/pkg/chart/v2/lint/rules/values_test.go @@ -66,8 +66,7 @@ func TestValidateValuesFileWellFormed(t *testing.T) { not:well[]{}formed ` tmpdir := ensure.TempFile(t, "values.yaml", []byte(badYaml)) - valfile := filepath.Join(tmpdir, "values.yaml") - if err := validateValuesFile(valfile, map[string]any{}, false); err == nil { + if err := validateValuesFile(tmpdir, map[string]any{}, false); err == nil { t.Fatal("expected values file to fail parsing") } } @@ -77,8 +76,7 @@ func TestValidateValuesFileSchema(t *testing.T) { tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) createTestingSchema(t, tmpdir) - valfile := filepath.Join(tmpdir, "values.yaml") - if err := validateValuesFile(valfile, map[string]any{}, false); err != nil { + if err := validateValuesFile(tmpdir, map[string]any{}, false); err != nil { t.Fatalf("Failed validation with %s", err) } } @@ -89,9 +87,7 @@ func TestValidateValuesFileSchemaFailure(t *testing.T) { tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) createTestingSchema(t, tmpdir) - valfile := filepath.Join(tmpdir, "values.yaml") - - err := validateValuesFile(valfile, map[string]any{}, false) + err := validateValuesFile(tmpdir, map[string]any{}, false) if err == nil { t.Fatal("expected values file to fail parsing") } @@ -105,9 +101,7 @@ func TestValidateValuesFileSchemaFailureButWithSkipSchemaValidation(t *testing.T tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) createTestingSchema(t, tmpdir) - valfile := filepath.Join(tmpdir, "values.yaml") - - err := validateValuesFile(valfile, map[string]any{}, true) + err := validateValuesFile(tmpdir, map[string]any{}, true) if err != nil { t.Fatal("expected values file to pass parsing because of skipSchemaValidation") } @@ -121,8 +115,7 @@ func TestValidateValuesFileSchemaOverrides(t *testing.T) { tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) createTestingSchema(t, tmpdir) - valfile := filepath.Join(tmpdir, "values.yaml") - if err := validateValuesFile(valfile, overrides, false); err != nil { + if err := validateValuesFile(tmpdir, overrides, false); err != nil { t.Fatalf("Failed validation with %s", err) } } @@ -157,9 +150,7 @@ func TestValidateValuesFile(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, false) + err := validateValuesFile(tmpdir, tt.overrides, false) switch { case err != nil && tt.errorMessage == "": diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index d36cd9e34..e52bfcc71 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -247,6 +247,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options if err != nil { return nil, err } + client.ChartDir = cp slog.Debug("Chart path", "path", cp) diff --git a/pkg/cmd/install_test.go b/pkg/cmd/install_test.go index 5fa3c1340..3a88a9aa6 100644 --- a/pkg/cmd/install_test.go +++ b/pkg/cmd/install_test.go @@ -231,6 +231,11 @@ func TestInstall(t *testing.T) { 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", }, + { + name: "install with schema file containing $ref", + cmd: "install reftest testdata/testcharts/chart-with-schema-ref", + golden: "output/schema-ref.txt", + }, // Install deprecated chart { name: "install with warning about deprecated chart", diff --git a/pkg/cmd/template_test.go b/pkg/cmd/template_test.go index 5bcccf5d0..6bfa29743 100644 --- a/pkg/cmd/template_test.go +++ b/pkg/cmd/template_test.go @@ -166,6 +166,11 @@ func TestTemplateCmd(t *testing.T) { cmd: fmt.Sprintf("template '%s' -f %s/extra_values.yaml", chartPath, chartPath), golden: "output/template-subchart-cm-set-file.txt", }, + { + name: "template with schema file containing $ref", + cmd: "template reftest testdata/testcharts/chart-with-schema-ref", + golden: "output/template-schema-ref.txt", + }, } runTestCmd(t, tests) } diff --git a/pkg/cmd/testdata/output/schema-ref.txt b/pkg/cmd/testdata/output/schema-ref.txt new file mode 100644 index 000000000..fa6015315 --- /dev/null +++ b/pkg/cmd/testdata/output/schema-ref.txt @@ -0,0 +1,7 @@ +NAME: reftest +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +DESCRIPTION: Install complete +TEST SUITE: None diff --git a/pkg/cmd/testdata/output/template-schema-ref.txt b/pkg/cmd/testdata/output/template-schema-ref.txt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/pkg/cmd/testdata/output/template-schema-ref.txt @@ -0,0 +1 @@ + diff --git a/pkg/cmd/testdata/output/upgrade-schema-ref.txt b/pkg/cmd/testdata/output/upgrade-schema-ref.txt new file mode 100644 index 000000000..cceed6919 --- /dev/null +++ b/pkg/cmd/testdata/output/upgrade-schema-ref.txt @@ -0,0 +1,8 @@ +Release "reftest" has been upgraded. Happy Helming! +NAME: reftest +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 2 +DESCRIPTION: Upgrade complete +TEST SUITE: None diff --git a/pkg/cmd/testdata/testcharts/chart-with-schema-ref b/pkg/cmd/testdata/testcharts/chart-with-schema-ref new file mode 120000 index 000000000..235b87210 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-schema-ref @@ -0,0 +1 @@ +../../../action/testdata/charts/chart-with-schema-ref \ No newline at end of file diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index b71c4ae2d..8aea8be50 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -186,6 +186,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if err != nil { return err } + client.ChartDir = chartPath p := getter.All(settings) vals, err := valueOpts.MergeValues(p) diff --git a/pkg/cmd/upgrade_test.go b/pkg/cmd/upgrade_test.go index 0ae1e3561..0c7ef5671 100644 --- a/pkg/cmd/upgrade_test.go +++ b/pkg/cmd/upgrade_test.go @@ -190,6 +190,12 @@ func TestUpgradeCmd(t *testing.T) { golden: "output/upgrade-uninstalled-with-keep-history.txt", rels: []*release.Release{relWithStatusMock("funny-bunny", 2, ch, rcommon.StatusUninstalled)}, }, + { + name: "upgrade with schema file containing $ref", + cmd: "upgrade reftest testdata/testcharts/chart-with-schema-ref", + golden: "output/upgrade-schema-ref.txt", + rels: []*release.Release{relMock("reftest", 1, ch)}, + }, } runTestCmd(t, tests) } From 6a2d9a8695cc49aac74d8ed207c8848b9cbb5ad2 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Sun, 22 Feb 2026 14:48:07 +0100 Subject: [PATCH 5/8] fix: handle dynamic schema paths in error message trimming The error message trimming logic was hardcoded to remove the prefix "jsonschema validation failed with 'file:///values.schema.json#'", which broke when we switched to using absolute paths for $ref resolution. Signed-off-by: Benoit Tigeot --- pkg/chart/common/util/jsonschema.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/chart/common/util/jsonschema.go b/pkg/chart/common/util/jsonschema.go index e34691f7d..38ec632ae 100644 --- a/pkg/chart/common/util/jsonschema.go +++ b/pkg/chart/common/util/jsonschema.go @@ -236,7 +236,12 @@ func (e JSONSchemaValidationError) Error() string { // 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") + // Remove the "jsonschema validation failed with 'file://...#'" line regardless of the path + if strings.HasPrefix(errStr, "jsonschema validation failed with 'file://") { + if idx := strings.Index(errStr, "#'\n"); idx != -1 { + errStr = errStr[idx+3:] // Skip past "#'\n" + } + } // The extra new line is needed for when there are sub-charts. return errStr + "\n" From 690730d3b9b864b2f8a11519387a963e6990bb89 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Sun, 22 Feb 2026 15:03:04 +0100 Subject: [PATCH 6/8] fix(test): Remove symlink Signed-off-by: Benoit Tigeot --- pkg/cmd/testdata/testcharts/chart-with-schema-ref | 1 - .../testdata/testcharts/chart-with-schema-ref/Chart.yaml | 3 +++ .../testcharts/chart-with-schema-ref/name.schema.json | 4 ++++ .../testcharts/chart-with-schema-ref/values.schema.json | 7 +++++++ .../testdata/testcharts/chart-with-schema-ref/values.yaml | 1 + 5 files changed, 15 insertions(+), 1 deletion(-) delete mode 120000 pkg/cmd/testdata/testcharts/chart-with-schema-ref create mode 100644 pkg/cmd/testdata/testcharts/chart-with-schema-ref/Chart.yaml create mode 100644 pkg/cmd/testdata/testcharts/chart-with-schema-ref/name.schema.json create mode 100644 pkg/cmd/testdata/testcharts/chart-with-schema-ref/values.schema.json create mode 100644 pkg/cmd/testdata/testcharts/chart-with-schema-ref/values.yaml diff --git a/pkg/cmd/testdata/testcharts/chart-with-schema-ref b/pkg/cmd/testdata/testcharts/chart-with-schema-ref deleted file mode 120000 index 235b87210..000000000 --- a/pkg/cmd/testdata/testcharts/chart-with-schema-ref +++ /dev/null @@ -1 +0,0 @@ -../../../action/testdata/charts/chart-with-schema-ref \ No newline at end of file diff --git a/pkg/cmd/testdata/testcharts/chart-with-schema-ref/Chart.yaml b/pkg/cmd/testdata/testcharts/chart-with-schema-ref/Chart.yaml new file mode 100644 index 000000000..c344a04d9 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-schema-ref/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: chart-with-schema-ref +version: 0.1.0 diff --git a/pkg/cmd/testdata/testcharts/chart-with-schema-ref/name.schema.json b/pkg/cmd/testdata/testcharts/chart-with-schema-ref/name.schema.json new file mode 100644 index 000000000..290e9cca5 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-schema-ref/name.schema.json @@ -0,0 +1,4 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string" +} diff --git a/pkg/cmd/testdata/testcharts/chart-with-schema-ref/values.schema.json b/pkg/cmd/testdata/testcharts/chart-with-schema-ref/values.schema.json new file mode 100644 index 000000000..e253c4c7e --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-schema-ref/values.schema.json @@ -0,0 +1,7 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { "$ref": "name.schema.json" } + } +} diff --git a/pkg/cmd/testdata/testcharts/chart-with-schema-ref/values.yaml b/pkg/cmd/testdata/testcharts/chart-with-schema-ref/values.yaml new file mode 100644 index 000000000..0b9fc7e3a --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-schema-ref/values.yaml @@ -0,0 +1 @@ +name: "test" From dd0b8f40babfefa3b38351bc15b9c8a765532b37 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Wed, 25 Feb 2026 00:39:53 +0100 Subject: [PATCH 7/8] fix(schema): limit $ref resolution to lint command Schema validation with $ref cross-file references only works when charts are available as directories on the filesystem. The install, template, and upgrade commands may work with archived charts (.tgz) that are loaded directly into memory without filesystem extraction. This change limits ChartDir setting to the lint command only. The lint command already extracts archived charts to temporary directories before validation, so $ref resolution works for both directory and archived charts when linting. For install/template/upgrade commands: - Main schema validation continues to work - Charts without $ref validate successfully - Charts with $ref will fail .. maybe we could add a warning about $ref not being supported in these commands? Changes: - Remove ChartDir assignment from install and upgrade commands - Update schema validation to use synthetic path when chartDir is empty - Add test for linting archived chart with $ref - Remove install/template/upgrade tests for $ref (not supported) Signed-off-by: Benoit Tigeot --- pkg/action/lint_test.go | 4 +++ .../testdata/charts/chart-with-schema-ref.tgz | Bin 0 -> 397 bytes pkg/chart/common/util/jsonschema.go | 28 +++++++++++++----- pkg/cmd/install.go | 1 - pkg/cmd/install_test.go | 5 ---- pkg/cmd/template_test.go | 5 ---- pkg/cmd/testdata/output/schema-ref.txt | 7 ----- .../testdata/output/template-schema-ref.txt | 1 - .../testdata/output/upgrade-schema-ref.txt | 8 ----- .../chart-with-schema-ref/Chart.yaml | 3 -- .../chart-with-schema-ref/name.schema.json | 4 --- .../chart-with-schema-ref/values.schema.json | 7 ----- .../chart-with-schema-ref/values.yaml | 1 - pkg/cmd/upgrade.go | 1 - pkg/cmd/upgrade_test.go | 6 ---- 15 files changed, 24 insertions(+), 57 deletions(-) create mode 100644 pkg/action/testdata/charts/chart-with-schema-ref.tgz delete mode 100644 pkg/cmd/testdata/output/schema-ref.txt delete mode 100644 pkg/cmd/testdata/output/template-schema-ref.txt delete mode 100644 pkg/cmd/testdata/output/upgrade-schema-ref.txt delete mode 100644 pkg/cmd/testdata/testcharts/chart-with-schema-ref/Chart.yaml delete mode 100644 pkg/cmd/testdata/testcharts/chart-with-schema-ref/name.schema.json delete mode 100644 pkg/cmd/testdata/testcharts/chart-with-schema-ref/values.schema.json delete mode 100644 pkg/cmd/testdata/testcharts/chart-with-schema-ref/values.yaml diff --git a/pkg/action/lint_test.go b/pkg/action/lint_test.go index 63b2f51ae..08329ad6c 100644 --- a/pkg/action/lint_test.go +++ b/pkg/action/lint_test.go @@ -75,6 +75,10 @@ func TestLintChart(t *testing.T) { name: "chart-with-schema-ref", chartPath: "testdata/charts/chart-with-schema-ref", }, + { + name: "archived-chart-with-schema-ref", + chartPath: "testdata/charts/chart-with-schema-ref.tgz", + }, { name: "chart-with-schema-negative", chartPath: "testdata/charts/chart-with-schema-negative", diff --git a/pkg/action/testdata/charts/chart-with-schema-ref.tgz b/pkg/action/testdata/charts/chart-with-schema-ref.tgz new file mode 100644 index 0000000000000000000000000000000000000000..da12c8d095ff48f06bd876c37125ae1fa7121a46 GIT binary patch literal 397 zcmV;80doEyiwFR4HJ)hz1MQYeZi6rohFNn8%k8rG1vV1f-l2OZHGwDrWIL&%DtE6P zQW{lNvH_|n{uWpawr61c&wy|=IfRg$b8s3(ei{kO`tb~)oMu80LQ?|Vg)$KVK7D9W zv@=#}7s^64ZEcaQSE{vt%yZMq^2$Ev_ZY@I0pZt5YKsnXyP3*|`yL|8uH-*weg0FC zrFrJ^&qcU4gMb z250S~R Date: Wed, 25 Feb 2026 01:17:28 +0100 Subject: [PATCH 8/8] fix(schema): enable $ref resolution for directory charts Set ChartDir only for directory-based charts to enable $ref resolution in JSON schemas. Archived charts (.tgz) are loaded into memory without filesystem extraction, so $ref resolution is not supported for them. This fixes the original issue where `helm template .` and `helm install .` failed to validate schemas with relative $ref references. Fixes #31260 Signed-off-by: Benoit Tigeot --- pkg/cmd/install.go | 7 +++++++ pkg/cmd/install_test.go | 5 +++++ pkg/cmd/template_test.go | 5 +++++ pkg/cmd/testdata/output/schema-ref.txt | 7 +++++++ pkg/cmd/testdata/output/template-schema-ref.txt | 1 + pkg/cmd/testdata/output/upgrade-schema-ref.txt | 8 ++++++++ .../testdata/testcharts/chart-with-schema-ref/Chart.yaml | 3 +++ .../testcharts/chart-with-schema-ref/name.schema.json | 4 ++++ .../testcharts/chart-with-schema-ref/values.schema.json | 7 +++++++ .../testdata/testcharts/chart-with-schema-ref/values.yaml | 1 + pkg/cmd/upgrade.go | 7 +++++++ pkg/cmd/upgrade_test.go | 6 ++++++ 12 files changed, 61 insertions(+) create mode 100644 pkg/cmd/testdata/output/schema-ref.txt create mode 100644 pkg/cmd/testdata/output/template-schema-ref.txt create mode 100644 pkg/cmd/testdata/output/upgrade-schema-ref.txt create mode 100644 pkg/cmd/testdata/testcharts/chart-with-schema-ref/Chart.yaml create mode 100644 pkg/cmd/testdata/testcharts/chart-with-schema-ref/name.schema.json create mode 100644 pkg/cmd/testdata/testcharts/chart-with-schema-ref/values.schema.json create mode 100644 pkg/cmd/testdata/testcharts/chart-with-schema-ref/values.yaml diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index d36cd9e34..10473c5d9 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -248,6 +248,13 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options return nil, err } + // Only set ChartDir for directory-based charts to enable $ref resolution. + // Archived charts (.tgz) are loaded into memory without filesystem extraction, + // so $ref resolution is not supported for them. + if fi, err := os.Stat(cp); err == nil && fi.IsDir() { + client.ChartDir = cp + } + slog.Debug("Chart path", "path", cp) p := getter.All(settings) diff --git a/pkg/cmd/install_test.go b/pkg/cmd/install_test.go index 5fa3c1340..3a88a9aa6 100644 --- a/pkg/cmd/install_test.go +++ b/pkg/cmd/install_test.go @@ -231,6 +231,11 @@ func TestInstall(t *testing.T) { 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", }, + { + name: "install with schema file containing $ref", + cmd: "install reftest testdata/testcharts/chart-with-schema-ref", + golden: "output/schema-ref.txt", + }, // Install deprecated chart { name: "install with warning about deprecated chart", diff --git a/pkg/cmd/template_test.go b/pkg/cmd/template_test.go index 5bcccf5d0..6bfa29743 100644 --- a/pkg/cmd/template_test.go +++ b/pkg/cmd/template_test.go @@ -166,6 +166,11 @@ func TestTemplateCmd(t *testing.T) { cmd: fmt.Sprintf("template '%s' -f %s/extra_values.yaml", chartPath, chartPath), golden: "output/template-subchart-cm-set-file.txt", }, + { + name: "template with schema file containing $ref", + cmd: "template reftest testdata/testcharts/chart-with-schema-ref", + golden: "output/template-schema-ref.txt", + }, } runTestCmd(t, tests) } diff --git a/pkg/cmd/testdata/output/schema-ref.txt b/pkg/cmd/testdata/output/schema-ref.txt new file mode 100644 index 000000000..fa6015315 --- /dev/null +++ b/pkg/cmd/testdata/output/schema-ref.txt @@ -0,0 +1,7 @@ +NAME: reftest +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +DESCRIPTION: Install complete +TEST SUITE: None diff --git a/pkg/cmd/testdata/output/template-schema-ref.txt b/pkg/cmd/testdata/output/template-schema-ref.txt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/pkg/cmd/testdata/output/template-schema-ref.txt @@ -0,0 +1 @@ + diff --git a/pkg/cmd/testdata/output/upgrade-schema-ref.txt b/pkg/cmd/testdata/output/upgrade-schema-ref.txt new file mode 100644 index 000000000..cceed6919 --- /dev/null +++ b/pkg/cmd/testdata/output/upgrade-schema-ref.txt @@ -0,0 +1,8 @@ +Release "reftest" has been upgraded. Happy Helming! +NAME: reftest +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 2 +DESCRIPTION: Upgrade complete +TEST SUITE: None diff --git a/pkg/cmd/testdata/testcharts/chart-with-schema-ref/Chart.yaml b/pkg/cmd/testdata/testcharts/chart-with-schema-ref/Chart.yaml new file mode 100644 index 000000000..c344a04d9 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-schema-ref/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: chart-with-schema-ref +version: 0.1.0 diff --git a/pkg/cmd/testdata/testcharts/chart-with-schema-ref/name.schema.json b/pkg/cmd/testdata/testcharts/chart-with-schema-ref/name.schema.json new file mode 100644 index 000000000..290e9cca5 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-schema-ref/name.schema.json @@ -0,0 +1,4 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string" +} diff --git a/pkg/cmd/testdata/testcharts/chart-with-schema-ref/values.schema.json b/pkg/cmd/testdata/testcharts/chart-with-schema-ref/values.schema.json new file mode 100644 index 000000000..e253c4c7e --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-schema-ref/values.schema.json @@ -0,0 +1,7 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { "$ref": "name.schema.json" } + } +} diff --git a/pkg/cmd/testdata/testcharts/chart-with-schema-ref/values.yaml b/pkg/cmd/testdata/testcharts/chart-with-schema-ref/values.yaml new file mode 100644 index 000000000..0b9fc7e3a --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-schema-ref/values.yaml @@ -0,0 +1 @@ +name: "test" diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index b71c4ae2d..386107c52 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -187,6 +187,13 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return err } + // Only set ChartDir for directory-based charts to enable $ref resolution. + // Archived charts (.tgz) are loaded into memory without filesystem extraction, + // so $ref resolution is not supported for them. + if fi, err := os.Stat(chartPath); err == nil && fi.IsDir() { + client.ChartDir = chartPath + } + p := getter.All(settings) vals, err := valueOpts.MergeValues(p) if err != nil { diff --git a/pkg/cmd/upgrade_test.go b/pkg/cmd/upgrade_test.go index 0ae1e3561..0c7ef5671 100644 --- a/pkg/cmd/upgrade_test.go +++ b/pkg/cmd/upgrade_test.go @@ -190,6 +190,12 @@ func TestUpgradeCmd(t *testing.T) { golden: "output/upgrade-uninstalled-with-keep-history.txt", rels: []*release.Release{relWithStatusMock("funny-bunny", 2, ch, rcommon.StatusUninstalled)}, }, + { + name: "upgrade with schema file containing $ref", + cmd: "upgrade reftest testdata/testcharts/chart-with-schema-ref", + golden: "output/upgrade-schema-ref.txt", + rels: []*release.Release{relMock("reftest", 1, ch)}, + }, } runTestCmd(t, tests) }