This commit is contained in:
Benoit Tigeot 2026-05-21 11:46:33 +02:00 committed by GitHub
commit 79919d1327
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 501 additions and 61 deletions

View file

@ -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

View file

@ -42,7 +42,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 {
@ -53,7 +53,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)
@ -67,8 +70,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
@ -78,7 +79,7 @@ func validateValuesFile(valuesPath string, overrides map[string]any, skipSchemaV
}
if !skipSchemaValidation {
return util.ValidateAgainstSingleSchema(coalescedValues, schema)
return util.ValidateAgainstSingleSchemaWithPath(coalescedValues, schema, schemaPath)
}
return nil

View file

@ -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 == "":

View file

@ -127,6 +127,9 @@ type Install struct {
// Used by helm template to add the release as part of OutputDir path
// OutputDir/<ReleaseName>
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
@ -363,7 +366,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
}

View file

@ -71,6 +71,14 @@ 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: "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",

Binary file not shown.

View file

@ -0,0 +1,3 @@
apiVersion: v2
name: chart-with-schema-ref
version: 0.1.0

View file

@ -0,0 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "string"
}

View file

@ -0,0 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"name": { "$ref": "name.schema.json" }
}
}

View file

@ -0,0 +1 @@
name: "test"

View file

@ -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
@ -296,7 +299,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
}

View file

@ -23,6 +23,8 @@ import (
"fmt"
"log/slog"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
@ -74,14 +76,42 @@ 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
}
// Convert chartDir to absolute path for $ref resolution.
// If chartDir is empty (e.g., chart loaded from .tgz archive), absChartPath
// remains empty and a synthetic path will be used instead.
var absChartPath string
if chartDir != "" {
var err error
absChartPath, err = filepath.Abs(chartDir)
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())
var schemaPath string
if absChartPath != "" {
// Use the chart directory for $ref resolution
schemaPath = filepath.Join(absChartPath, "values.schema.json")
} else {
// No chart directory (e.g., chart loaded from .tgz archive).
// Use a synthetic path - $ref resolution will not work, but main schema validation will.
schemaPath = "/values.schema.json"
}
err := ValidateAgainstSingleSchemaWithPath(values, chrt.Schema(), schemaPath)
if err != nil {
fmt.Fprintf(&sb, "%s:\n", chrt.Name())
sb.WriteString(err.Error())
@ -108,7 +138,15 @@ func ValidateAgainstSchema(ch chart.Charter, values map[string]any) error {
continue
}
if err := ValidateAgainstSchema(subchart, subchartValues); err != nil {
var subchartPath string
if absChartPath != "" {
subchartPath = resolveSubchartDir(
filepath.Join(absChartPath, "charts"),
sub.Name(),
sub.Schema(),
)
}
if err := ValidateAgainstSchemaWithPath(subchart, subchartValues, subchartPath); err != nil {
sb.WriteString(err.Error())
}
}
@ -122,6 +160,12 @@ 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) {
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)
@ -146,12 +190,14 @@ func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte) (reter
compiler := jsonschema.NewCompiler()
compiler.UseLoader(loader)
err = compiler.AddResource("file:///values.schema.json", schema)
schemaURL := "file://" + schemaPath
err = compiler.AddResource(schemaURL, schema)
if err != nil {
return err
}
validator, err := compiler.Compile("file:///values.schema.json")
validator, err := compiler.Compile(schemaURL)
if err != nil {
return err
}
@ -193,6 +239,41 @@ func (l urnLoader) Load(urlStr string) (any, error) {
return jsonschema.UnmarshalJSON(strings.NewReader("true"))
}
// resolveSubchartDir finds the on-disk directory for a subchart under chartsDir.
// Returns "" when no directory can be found, which disables $ref resolution.
func resolveSubchartDir(chartsDir, effectiveName string, schema []byte) string {
// Direct match; handles the common non-aliased case in one syscall.
candidate := filepath.Join(chartsDir, effectiveName)
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
return candidate
}
// The effective name didn't match a directory likely an alias.
// Scan charts/ subdirectories and match by schema content.
if len(schema) == 0 {
return ""
}
entries, err := os.ReadDir(chartsDir)
if err != nil {
return ""
}
for _, e := range entries {
if !e.IsDir() {
continue
}
data, err := os.ReadFile(filepath.Join(chartsDir, e.Name(), "values.schema.json"))
if err != nil {
continue
}
// getAliasDependency shallow-copies the chart, so schema bytes in memory
// are identical to the file originally loaded from this directory.
if bytes.Equal(data, schema) {
return filepath.Join(chartsDir, e.Name())
}
}
return ""
}
// Note, JSONSchemaValidationError is used to wrap the error from the underlying
// validation package so that Helm has a clean interface and the validation package
// could be replaced without changing the Helm SDK API.
@ -209,7 +290,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"

View file

@ -20,6 +20,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
@ -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,63 @@ 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)
}
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)
}
absSchemaPath, err := filepath.Abs(schemaPath)
if err != nil {
t.Fatalf("Error getting absolute path: %s", err)
}
if err := ValidateAgainstSingleSchemaWithPath(values, schema, absSchemaPath); 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)
}
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)
}
absSchemaPath, err := filepath.Abs(schemaPath)
if err != nil {
t.Fatalf("Error getting absolute path: %s", err)
}
if err := ValidateAgainstSingleSchemaWithPath(values, schema, absSchemaPath); err != nil {
t.Errorf("Error validating Values against Schema with relative references from subfolder: %s", err)
}
}
@ -389,3 +442,170 @@ func TestValidateAgainstSchema_InvalidSubchartValuesType_NoPanic(t *testing.T) {
t.Fatal("expected an error when subchart values have invalid type, got nil")
}
}
// Test that $ref resolution works for aliased subcharts.
// When a subchart has an alias (e.g., mysql aliased as "database"),
// processDependencyEnabled rewrites Metadata.Name to the alias,
// but the on-disk directory retains the original name (charts/mysql/).
// The schema validator must find the correct directory to resolve $ref.
func TestValidateAgainstSchemaWithPath_AliasedSubchartRef(t *testing.T) {
tmpDir := t.TempDir()
// On-disk layout: charts/mysql/ contains the schema files.
// The directory name is the ORIGINAL chart name, not the alias.
mysqlDir := filepath.Join(tmpDir, "charts", "mysql")
if err := os.MkdirAll(mysqlDir, 0o755); err != nil {
t.Fatal(err)
}
baseSchema := []byte(`{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"port": { "type": "integer", "minimum": 1 }
},
"required": ["port"]
}`)
if err := os.WriteFile(filepath.Join(mysqlDir, "base.schema.json"), baseSchema, 0o644); err != nil {
t.Fatal(err)
}
subchartSchemaBytes := []byte(`{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"config": { "$ref": "./base.schema.json" }
},
"required": ["config"]
}`)
if err := os.WriteFile(filepath.Join(mysqlDir, "values.schema.json"), subchartSchemaBytes, 0o644); err != nil {
t.Fatal(err)
}
// In-memory chart: Metadata.Name is the ALIAS ("database"),
// simulating what processDependencyEnabled does after loading.
subchart := &chart.Chart{
Metadata: &chart.Metadata{Name: "database"},
Schema: subchartSchemaBytes,
}
chrt := &chart.Chart{
Metadata: &chart.Metadata{Name: "testchart"},
}
chrt.AddDependency(subchart)
vals := map[string]any{
"database": map[string]any{
"config": map[string]any{
"port": 3306,
},
},
}
if err := ValidateAgainstSchemaWithPath(chrt, vals, tmpDir); err != nil {
t.Errorf("expected no error for valid values with aliased subchart $ref, got: %s", err)
}
}
// Test that $ref resolution works when multiple aliases point to the same chart.
// Both aliased subcharts should resolve $ref through the single on-disk directory.
func TestValidateAgainstSchemaWithPath_MultipleAliasesSameChart(t *testing.T) {
tmpDir := t.TempDir()
mysqlDir := filepath.Join(tmpDir, "charts", "mysql")
if err := os.MkdirAll(mysqlDir, 0o755); err != nil {
t.Fatal(err)
}
baseSchema := []byte(`{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"port": { "type": "integer", "minimum": 1 }
},
"required": ["port"]
}`)
if err := os.WriteFile(filepath.Join(mysqlDir, "base.schema.json"), baseSchema, 0o644); err != nil {
t.Fatal(err)
}
subchartSchemaBytes := []byte(`{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"config": { "$ref": "./base.schema.json" }
},
"required": ["config"]
}`)
if err := os.WriteFile(filepath.Join(mysqlDir, "values.schema.json"), subchartSchemaBytes, 0o644); err != nil {
t.Fatal(err)
}
// Two aliased subcharts from the same original chart
primary := &chart.Chart{
Metadata: &chart.Metadata{Name: "primary"},
Schema: subchartSchemaBytes,
}
replica := &chart.Chart{
Metadata: &chart.Metadata{Name: "replica"},
Schema: subchartSchemaBytes,
}
chrt := &chart.Chart{
Metadata: &chart.Metadata{Name: "testchart"},
}
chrt.AddDependency(primary)
chrt.AddDependency(replica)
vals := map[string]any{
"primary": map[string]any{
"config": map[string]any{"port": 3306},
},
"replica": map[string]any{
"config": map[string]any{"port": 3307},
},
}
if err := ValidateAgainstSchemaWithPath(chrt, vals, tmpDir); err != nil {
t.Errorf("expected no error for multiple aliases of same chart, got: %s", err)
}
}
// Test that validation proceeds gracefully when an aliased subchart has no
// matching directory on disk (e.g., the subchart is an archived .tgz).
// $ref resolution is disabled but main schema validation still works.
func TestValidateAgainstSchemaWithPath_AliasedSubchartNoDir(t *testing.T) {
tmpDir := t.TempDir()
// Create empty charts/ directory — no subdirectory matching any name
if err := os.MkdirAll(filepath.Join(tmpDir, "charts"), 0o755); err != nil {
t.Fatal(err)
}
// Schema without $ref — validates independently
subchartSchemaBytes := []byte(`{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"port": { "type": "integer" }
},
"required": ["port"]
}`)
subchart := &chart.Chart{
Metadata: &chart.Metadata{Name: "database"},
Schema: subchartSchemaBytes,
}
chrt := &chart.Chart{
Metadata: &chart.Metadata{Name: "testchart"},
}
chrt.AddDependency(subchart)
vals := map[string]any{
"database": map[string]any{
"port": 5432,
},
}
if err := ValidateAgainstSchemaWithPath(chrt, vals, tmpDir); err != nil {
t.Errorf("expected no error when aliased subchart dir missing (graceful fallback), got: %s", err)
}
}

View file

@ -0,0 +1,10 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"required": ["name"]
}

View file

@ -0,0 +1,3 @@
user:
name: "John Doe"
age: 30

View file

@ -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"]
}

View file

@ -0,0 +1,10 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"config": {
"type": "string"
}
},
"required": ["config"]
}

View file

@ -0,0 +1,3 @@
appConfig:
config: "production"
replicas: 3

View file

@ -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"]
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -42,7 +42,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 {
@ -53,7 +53,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)
@ -67,8 +70,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
@ -78,7 +79,7 @@ func validateValuesFile(valuesPath string, overrides map[string]any, skipSchemaV
}
if !skipSchemaValidation {
return util.ValidateAgainstSingleSchema(coalescedValues, schema)
return util.ValidateAgainstSingleSchemaWithPath(coalescedValues, schema, schemaPath)
}
return nil

View file

@ -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 == "":

View file

@ -269,6 +269,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)

View file

@ -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",

View file

@ -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)
}

View file

@ -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

View file

@ -0,0 +1 @@

View file

@ -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

View file

@ -0,0 +1,3 @@
apiVersion: v2
name: chart-with-schema-ref
version: 0.1.0

View file

@ -0,0 +1,4 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "string"
}

View file

@ -0,0 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"name": { "$ref": "name.schema.json" }
}
}

View file

@ -0,0 +1 @@
name: "test"

View file

@ -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 {

View file

@ -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)
}