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 <benoit.tigeot@lifen.fr>

Add test
pull/31274/head
Benoit Tigeot 2 weeks ago
parent 5b43b744b8
commit a2fafd3c5f
No known key found for this signature in database
GPG Key ID: 8E6D4FC8AEBDA62C

@ -75,9 +75,9 @@ func validateValuesFile(valuesPath string, overrides map[string]interface{}, ski
if err != nil { if err != nil {
return err return err
} }
baseDir := filepath.Dir(schemaPath)
if !skipSchemaValidation { if !skipSchemaValidation {
return util.ValidateAgainstSingleSchema(coalescedValues, schema) return util.ValidateAgainstSingleSchema(coalescedValues, schema, baseDir)
} }
return nil return nil

@ -23,6 +23,7 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"net/http" "net/http"
"path/filepath"
"strings" "strings"
"time" "time"
@ -79,8 +80,7 @@ func ValidateAgainstSchema(ch chart.Charter, values map[string]interface{}) erro
} }
var sb strings.Builder var sb strings.Builder
if chrt.Schema() != nil { if chrt.Schema() != nil {
slog.Debug("chart name", "chart-name", chrt.Name()) err := ValidateAgainstSingleSchema(values, chrt.Schema(), chrt.ChartFullPath())
err := ValidateAgainstSingleSchema(values, chrt.Schema())
if err != nil { if err != nil {
sb.WriteString(fmt.Sprintf("%s:\n", chrt.Name())) sb.WriteString(fmt.Sprintf("%s:\n", chrt.Name()))
sb.WriteString(err.Error()) sb.WriteString(err.Error())
@ -107,7 +107,7 @@ func ValidateAgainstSchema(ch chart.Charter, values map[string]interface{}) erro
} }
// ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema // 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() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
reterr = fmt.Errorf("unable to validate schema: %s", r) reterr = fmt.Errorf("unable to validate schema: %s", r)
@ -131,12 +131,13 @@ func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte) (reter
compiler := jsonschema.NewCompiler() compiler := jsonschema.NewCompiler()
compiler.UseLoader(loader) 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 { if err != nil {
return err return err
} }
validator, err := compiler.Compile("file:///values.schema.json") validator, err := compiler.Compile(base)
if err != nil { if err != nil {
return err return err
} }

@ -20,6 +20,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"path/filepath"
"strings" "strings"
"testing" "testing"
@ -37,7 +38,7 @@ func TestValidateAgainstSingleSchema(t *testing.T) {
t.Fatalf("Error reading YAML file: %s", err) 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) t.Errorf("Error validating Values against Schema: %s", err)
} }
} }
@ -53,7 +54,7 @@ func TestValidateAgainstInvalidSingleSchema(t *testing.T) {
} }
var errString string 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") t.Fatalf("Expected an error, but got nil")
} else { } else {
errString = err.Error() errString = err.Error()
@ -77,7 +78,7 @@ func TestValidateAgainstSingleSchemaNegative(t *testing.T) {
} }
var errString string 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") t.Fatalf("Expected an error, but got nil")
} else { } else {
errString = err.Error() errString = err.Error()
@ -177,11 +178,12 @@ func TestValidateAgainstSchemaNegative(t *testing.T) {
errString = err.Error() errString = err.Error()
} }
expectedErrString := `subchart: expectedValidationError := "missing property 'age'"
- at '': missing property 'age' if !strings.Contains(errString, "subchart:") {
` t.Errorf("Error string should contain 'subchart:', got: %s", errString)
if errString != expectedErrString { }
t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) 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() errString = err.Error()
} }
expectedErrString := `subchart: expectedValidationErrors := []string{
- at '/data': no items match contains schema "no items match contains schema",
- at '/data/0': got number, want string "got number, want string",
` }
if errString != expectedErrString { if !strings.Contains(errString, "subchart:") {
t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) 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)
} }
} }

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

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

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

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

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

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

@ -75,9 +75,9 @@ func validateValuesFile(valuesPath string, overrides map[string]interface{}, ski
if err != nil { if err != nil {
return err return err
} }
baseDir := filepath.Dir(schemaPath)
if !skipSchemaValidation { if !skipSchemaValidation {
return util.ValidateAgainstSingleSchema(coalescedValues, schema) return util.ValidateAgainstSingleSchema(coalescedValues, schema, baseDir)
} }
return nil return nil

Loading…
Cancel
Save