Support loading subschemas from the schemas directory in a chart

Signed-off-by: Andy Caldwell <andycaldwell@microsoft.com>
pull/12553/head
Andy Caldwell 2 years 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"` Templates []*File `json:"templates"`
// Values are default config for this chart. // Values are default config for this chart.
Values map[string]interface{} `json:"values"` 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"` 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, // Files are miscellaneous files in a chart archive,
// e.g. README, LICENSE, etc. // e.g. README, LICENSE, etc.
Files []*File `json:"files"` Files []*File `json:"files"`

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

@ -32,7 +32,7 @@ import (
func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) error { func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) error {
var sb strings.Builder var sb strings.Builder
if chrt.Schema != nil { if chrt.Schema != nil {
err := ValidateAgainstSingleSchema(values, chrt.Schema) err := ValidateAgainstSingleSchema(values, chrt.Schema, chrt.ExtraSchemas)
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())
@ -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 // 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() { 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)
@ -73,10 +73,21 @@ func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) (reterr error
if bytes.Equal(valuesJSON, []byte("null")) { if bytes.Equal(valuesJSON, []byte("null")) {
valuesJSON = []byte("{}") 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) valuesLoader := gojsonschema.NewBytesLoader(valuesJSON)
result, err := gojsonschema.Validate(schemaLoader, valuesLoader) result, err := rootSchema.Validate(valuesLoader)
if err != nil { if err != nil {
return err return err
} }

@ -33,7 +33,29 @@ 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, 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) t.Errorf("Error validating Values against Schema: %s", err)
} }
} }
@ -49,7 +71,7 @@ func TestValidateAgainstInvalidSingleSchema(t *testing.T) {
} }
var errString string 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") t.Fatalf("Expected an error, but got nil")
} else { } else {
errString = err.Error() errString = err.Error()
@ -73,7 +95,7 @@ func TestValidateAgainstSingleSchemaNegative(t *testing.T) {
} }
var errString string 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") t.Fatalf("Expected an error, but got nil")
} else { } else {
errString = err.Error() 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 ( import (
"os" "os"
"io/fs"
"path/filepath" "path/filepath"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -76,11 +77,46 @@ func validateValuesFile(valuesPath string, overrides map[string]interface{}) err
ext := filepath.Ext(valuesPath) ext := filepath.Ext(valuesPath)
schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json" schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json"
schema, err := os.ReadFile(schemaPath) 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 { if len(schema) == 0 {
return nil return nil
} }
if err != nil { if err != nil {
return err 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