From 2834f5b1d21077e66dac2f51d8593d991affb530 Mon Sep 17 00:00:00 2001 From: Andy Caldwell Date: Wed, 8 Nov 2023 22:36:04 +0000 Subject: [PATCH] Support loading subschemas from the schemas directory in a chart Signed-off-by: Andy Caldwell --- pkg/chart/chart.go | 4 +- pkg/chart/loader/load.go | 3 +- pkg/chartutil/jsonschema.go | 19 +++++-- pkg/chartutil/jsonschema_test.go | 28 ++++++++-- .../testdata/test-values-extra.schema.json | 19 +++++++ .../test-values-with-extra.schema.json | 52 +++++++++++++++++++ pkg/lint/rules/values.go | 38 +++++++++++++- 7 files changed, 153 insertions(+), 10 deletions(-) create mode 100644 pkg/chartutil/testdata/test-values-extra.schema.json create mode 100644 pkg/chartutil/testdata/test-values-with-extra.schema.json diff --git a/pkg/chart/chart.go b/pkg/chart/chart.go index a3bed63a3..8a4861660 100644 --- a/pkg/chart/chart.go +++ b/pkg/chart/chart.go @@ -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"` diff --git a/pkg/chart/loader/load.go b/pkg/chart/loader/load.go index 7cc8878a8..353a562aa 100644 --- a/pkg/chart/loader/load.go +++ b/pkg/chart/loader/load.go @@ -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/"): diff --git a/pkg/chartutil/jsonschema.go b/pkg/chartutil/jsonschema.go index 7b9768fd3..1c26a2c9d 100644 --- a/pkg/chartutil/jsonschema.go +++ b/pkg/chartutil/jsonschema.go @@ -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 } diff --git a/pkg/chartutil/jsonschema_test.go b/pkg/chartutil/jsonschema_test.go index 7610db337..b7188ea62 100644 --- a/pkg/chartutil/jsonschema_test.go +++ b/pkg/chartutil/jsonschema_test.go @@ -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() diff --git a/pkg/chartutil/testdata/test-values-extra.schema.json b/pkg/chartutil/testdata/test-values-extra.schema.json new file mode 100644 index 000000000..24af1027d --- /dev/null +++ b/pkg/chartutil/testdata/test-values-extra.schema.json @@ -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" + } +} diff --git a/pkg/chartutil/testdata/test-values-with-extra.schema.json b/pkg/chartutil/testdata/test-values-with-extra.schema.json new file mode 100644 index 000000000..609d3283b --- /dev/null +++ b/pkg/chartutil/testdata/test-values-with-extra.schema.json @@ -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" +} diff --git a/pkg/lint/rules/values.go b/pkg/lint/rules/values.go index 538d8381b..8661aa197 100644 --- a/pkg/lint/rules/values.go +++ b/pkg/lint/rules/values.go @@ -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) }