Support loading subschemas from the schemas directory in a chart

Signed-off-by: Andy Caldwell <andycaldwell@microsoft.com>
pull/12553/head
Andy Caldwell 8 months ago
parent d70e293e5f
commit 2834f5b1d2
No known key found for this signature in database
GPG Key ID: D4204541AC1D228D

@ -46,8 +46,10 @@ type Chart struct {
Templates []*File `json:"templates"`
// Values are default config for this chart.
Values map[string]interface{} `json:"values"`
// Schema is an optional JSON schema for imposing structure on Values
// Schema is an optional JSON schema imposing structure on the chart values
Schema []byte `json:"schema"`
// Additional schemas are optional JSON schemas that can be referenced from the root schema
ExtraSchemas [][]byte `json:"extra_schemas"`
// Files are miscellaneous files in a chart archive,
// e.g. README, LICENSE, etc.
Files []*File `json:"files"`

@ -137,7 +137,8 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
if c.Metadata.APIVersion == chart.APIVersionV1 {
c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data})
}
case strings.HasPrefix(f.Name, "schemas/"):
c.ExtraSchemas = append(c.ExtraSchemas, f.Data)
case strings.HasPrefix(f.Name, "templates/"):
c.Templates = append(c.Templates, &chart.File{Name: f.Name, Data: f.Data})
case strings.HasPrefix(f.Name, "charts/"):

@ -32,7 +32,7 @@ import (
func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) error {
var sb strings.Builder
if chrt.Schema != nil {
err := ValidateAgainstSingleSchema(values, chrt.Schema)
err := ValidateAgainstSingleSchema(values, chrt.Schema, chrt.ExtraSchemas)
if err != nil {
sb.WriteString(fmt.Sprintf("%s:\n", chrt.Name()))
sb.WriteString(err.Error())
@ -55,7 +55,7 @@ func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) err
}
// ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema
func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) (reterr error) {
func ValidateAgainstSingleSchema(values Values, schemaJSON []byte, extraSchemas [][]byte) (reterr error) {
defer func() {
if r := recover(); r != nil {
reterr = fmt.Errorf("unable to validate schema: %s", r)
@ -73,10 +73,21 @@ func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) (reterr error
if bytes.Equal(valuesJSON, []byte("null")) {
valuesJSON = []byte("{}")
}
schemaLoader := gojsonschema.NewBytesLoader(schemaJSON)
extraSchemasLoader := gojsonschema.NewSchemaLoader()
for _, extraSchema := range extraSchemas {
extraLoader := gojsonschema.NewBytesLoader(extraSchema)
extraSchemasLoader.AddSchemas(extraLoader)
}
rootSchemaLoader := gojsonschema.NewBytesLoader(schemaJSON)
rootSchema, err := extraSchemasLoader.Compile(rootSchemaLoader)
if err != nil {
return fmt.Errorf("enable to compile schema: %s", err)
}
valuesLoader := gojsonschema.NewBytesLoader(valuesJSON)
result, err := gojsonschema.Validate(schemaLoader, valuesLoader)
result, err := rootSchema.Validate(valuesLoader)
if err != nil {
return err
}

@ -33,7 +33,29 @@ 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, make([][]byte, 0)); err != nil {
t.Errorf("Error validating Values against Schema: %s", err)
}
}
func TestValidateAgainstSchemaWithExtras(t *testing.T) {
values, err := ReadValuesFile("./testdata/test-values.yaml")
if err != nil {
t.Fatalf("Error reading YAML file: %s", err)
}
schema, err := os.ReadFile("./testdata/test-values-with-extra.schema.json")
if err != nil {
t.Fatalf("Error reading YAML file: %s", err)
}
extra, err := os.ReadFile("./testdata/test-values-extra.schema.json")
if err != nil {
t.Fatalf("Error reading YAML file: %s", err)
}
extraSchemas := make([][]byte, 1)
extraSchemas[0] = extra
if err := ValidateAgainstSingleSchema(values, schema, extraSchemas); err != nil {
t.Errorf("Error validating Values against Schema: %s", err)
}
}
@ -49,7 +71,7 @@ func TestValidateAgainstInvalidSingleSchema(t *testing.T) {
}
var errString string
if err := ValidateAgainstSingleSchema(values, schema); err == nil {
if err := ValidateAgainstSingleSchema(values, schema, make([][]byte, 0)); err == nil {
t.Fatalf("Expected an error, but got nil")
} else {
errString = err.Error()
@ -73,7 +95,7 @@ func TestValidateAgainstSingleSchemaNegative(t *testing.T) {
}
var errString string
if err := ValidateAgainstSingleSchema(values, schema); err == nil {
if err := ValidateAgainstSingleSchema(values, schema, make([][]byte, 0)); err == nil {
t.Fatalf("Expected an error, but got nil")
} else {
errString = err.Error()

@ -0,0 +1,19 @@
{
"$id": "https://helm.sh/schemas/addresses",
"type": "array",
"description": "List of addresses",
"items": {
"properties": {
"city": {
"type": "string"
},
"number": {
"type": "number"
},
"street": {
"type": "string"
}
},
"type": "object"
}
}

@ -0,0 +1,52 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"addresses": {
"$ref": "https://helm.sh/schemas/addresses"
},
"age": {
"description": "Age",
"minimum": 0,
"type": "integer"
},
"employmentInfo": {
"properties": {
"salary": {
"minimum": 0,
"type": "number"
},
"title": {
"type": "string"
}
},
"required": [
"salary"
],
"type": "object"
},
"firstname": {
"description": "First name",
"type": "string"
},
"lastname": {
"type": "string"
},
"likesCoffee": {
"type": "boolean"
},
"phoneNumbers": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"firstname",
"lastname",
"addresses",
"employmentInfo"
],
"title": "Values",
"type": "object"
}

@ -18,6 +18,7 @@ package rules
import (
"os"
"io/fs"
"path/filepath"
"github.com/pkg/errors"
@ -76,11 +77,46 @@ func validateValuesFile(valuesPath string, overrides map[string]interface{}) err
ext := filepath.Ext(valuesPath)
schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json"
schema, err := os.ReadFile(schemaPath)
// Check for zero-length _before_ checking for errors, since we want schema files to be optional
if len(schema) == 0 {
return nil
}
if err != nil {
return err
}
return chartutil.ValidateAgainstSingleSchema(coalescedValues, schema)
extraSchemas := make([][]byte, 0)
schemasPath := filepath.Dir(valuesPath) + "/schemas"
err = filepath.WalkDir(schemasPath, func(path string, dentry fs.DirEntry, err error) error {
if path == schemasPath && dentry == nil && err != nil {
// The "schemas" folder could not be opened if it doesn't exist, treat it as empty
if os.IsNotExist(err) {
return fs.SkipAll
} else {
return err
}
}
if err != nil {
return err
}
if !dentry.IsDir() {
schema, err = os.ReadFile(path)
if err != nil {
return err
}
extraSchemas = append(extraSchemas, schema)
}
return nil
})
if err != nil {
return err
}
return chartutil.ValidateAgainstSingleSchema(coalescedValues, schema, extraSchemas)
}

Loading…
Cancel
Save