mirror of https://github.com/helm/helm.git
				
				
				
			Feat/schema validation (#5350)
* Add the Schema type and a function to read it
* Added a function to read a schema from a file
* Check that values.yaml matches schema
This commit uses the gojsonschema package to validate a values.yaml file
against a corresponding values.schema.yaml file.
* Add functionality to generate a schema from a values.yaml
* Add Schema to Chart and loader
* Clean up implementation in chartutil
* Add tests for helm install with schema
* Add schema validation to helm lint
* Clean up "matchSchema"
* Modify error output
* Add documentation
* Fix a linter issue
* Fix a test that broke during a rebase
* Clean up documentation
* Specify JSONSchema spec
Since JSONSchema is still in a draft state as of this commit, we need to
specify a particular version of the JSONSchema spec
* Switch to using builtin functionality for file extensions
* Switch to using a third-party library for JSON conversion
* Use the constants from the gojsonschema package
* Updates to unit tests
* Minor change to avoid string cast
* Remove JSON Schema generation
* Change Schema type from map[string]interface{} to []byte
* Convert all Schema YAML to JSON
* Fix some tests that were broken by a rebase
* Fix up YAML/JSON conversions
* This checks subcharts for schema validation
The final coalesced values for a given chart will be validated against
that chart's schema, as well as any dependent subchart's schema
* Add unit tests for ValidateAgainstSchema
* Remove nonessential test files
* Remove a misleading unit test
The TestReadSchema unit test was simply testing the ReadValues function,
which is already being validated in the TestReadValues unit test
* Update documentation to reflect changes to subchart schemas
			
			
This commit is contained in:
		
							parent
							
								
									3dd1765491
								
							
						
					
					
						commit
						ffff0e8c33
					
				|  | @ -835,6 +835,30 @@ | |||
|   revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053" | ||||
|   version = "v1.3.0" | ||||
| 
 | ||||
| [[projects]] | ||||
|   branch = "master" | ||||
|   digest = "1:f4e5276a3b356f4692107047fd2890f2fe534f4feeb6b1fd2f6dfbd87f1ccf54" | ||||
|   name = "github.com/xeipuuv/gojsonpointer" | ||||
|   packages = ["."] | ||||
|   pruneopts = "UT" | ||||
|   revision = "4e3ac2762d5f479393488629ee9370b50873b3a6" | ||||
| 
 | ||||
| [[projects]] | ||||
|   branch = "master" | ||||
|   digest = "1:dc6a6c28ca45d38cfce9f7cb61681ee38c5b99ec1425339bfc1e1a7ba769c807" | ||||
|   name = "github.com/xeipuuv/gojsonreference" | ||||
|   packages = ["."] | ||||
|   pruneopts = "UT" | ||||
|   revision = "bd5ef7bd5415a7ac448318e64f11a24cd21e594b" | ||||
| 
 | ||||
| [[projects]] | ||||
|   digest = "1:1c898ea6c30c16e8d55fdb6fe44c4bee5f9b7d68aa260cfdfc3024491dcc7bea" | ||||
|   name = "github.com/xeipuuv/gojsonschema" | ||||
|   packages = ["."] | ||||
|   pruneopts = "UT" | ||||
|   revision = "f971f3cd73b2899de6923801c147f075263e0c50" | ||||
|   version = "v1.1.0" | ||||
| 
 | ||||
| [[projects]] | ||||
|   digest = "1:340553b2fdaab7d53e63fd40f8ed82203bdd3274253055bdb80a46828482ef81" | ||||
|   name = "github.com/xenolf/lego" | ||||
|  | @ -1676,6 +1700,7 @@ | |||
|     "github.com/spf13/pflag", | ||||
|     "github.com/stretchr/testify/assert", | ||||
|     "github.com/stretchr/testify/suite", | ||||
|     "github.com/xeipuuv/gojsonschema", | ||||
|     "golang.org/x/crypto/openpgp", | ||||
|     "golang.org/x/crypto/openpgp/clearsign", | ||||
|     "golang.org/x/crypto/openpgp/errors", | ||||
|  |  | |||
|  | @ -107,3 +107,6 @@ | |||
|   go-tests = true | ||||
|   unused-packages = true | ||||
| 
 | ||||
| [[constraint]] | ||||
|   name = "github.com/xeipuuv/gojsonschema" | ||||
|   version = "1.1.0" | ||||
|  |  | |||
|  | @ -136,6 +136,53 @@ func TestInstall(t *testing.T) { | |||
| 			wantError: true, | ||||
| 			golden:    "output/install-chart-bad-type.txt", | ||||
| 		}, | ||||
| 		// Install, values from yaml, schematized
 | ||||
| 		{ | ||||
| 			name:   "install with schema file", | ||||
| 			cmd:    "install schema testdata/testcharts/chart-with-schema", | ||||
| 			golden: "output/schema.txt", | ||||
| 		}, | ||||
| 		// Install, values from yaml, schematized with errors
 | ||||
| 		{ | ||||
| 			name:      "install with schema file, with errors", | ||||
| 			cmd:       "install schema testdata/testcharts/chart-with-schema-negative", | ||||
| 			wantError: true, | ||||
| 			golden:    "output/schema-negative.txt", | ||||
| 		}, | ||||
| 		// Install, values from yaml, extra values from yaml, schematized with errors
 | ||||
| 		{ | ||||
| 			name:      "install with schema file, extra values from yaml, with errors", | ||||
| 			cmd:       "install schema testdata/testcharts/chart-with-schema -f testdata/testcharts/chart-with-schema/extra-values.yaml", | ||||
| 			wantError: true, | ||||
| 			golden:    "output/schema-negative.txt", | ||||
| 		}, | ||||
| 		// Install, values from yaml, extra values from cli, schematized with errors
 | ||||
| 		{ | ||||
| 			name:      "install with schema file, extra values from cli, with errors", | ||||
| 			cmd:       "install schema testdata/testcharts/chart-with-schema --set age=-5", | ||||
| 			wantError: true, | ||||
| 			golden:    "output/schema-negative-cli.txt", | ||||
| 		}, | ||||
| 		// Install with subchart, values from yaml, schematized with errors
 | ||||
| 		{ | ||||
| 			name:      "install with schema file and schematized subchart, with errors", | ||||
| 			cmd:       "install schema testdata/testcharts/chart-with-schema-and-subchart", | ||||
| 			wantError: true, | ||||
| 			golden:    "output/subchart-schema-negative.txt", | ||||
| 		}, | ||||
| 		// Install with subchart, values from yaml, extra values from cli, schematized with errors
 | ||||
| 		{ | ||||
| 			name:   "install with schema file and schematized subchart, extra values from cli", | ||||
| 			cmd:    "install schema testdata/testcharts/chart-with-schema-and-subchart --set lastname=doe --set subchart-with-schema.age=25", | ||||
| 			golden: "output/subchart-schema-cli.txt", | ||||
| 		}, | ||||
| 		// Install with subchart, values from yaml, extra values from cli, schematized with errors
 | ||||
| 		{ | ||||
| 			name:      "install with schema file and schematized subchart, extra values from cli, with errors", | ||||
| 			cmd:       "install schema testdata/testcharts/chart-with-schema-and-subchart --set lastname=doe --set subchart-with-schema.age=-25", | ||||
| 			wantError: true, | ||||
| 			golden:    "output/subchart-schema-cli-negative.txt", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	runTestActionCmd(t, tests) | ||||
|  |  | |||
|  | @ -0,0 +1,4 @@ | |||
| Error: values don't meet the specifications of the schema(s) in the following chart(s): | ||||
| empty: | ||||
| - age: Must be greater than or equal to 0/1 | ||||
| 
 | ||||
|  | @ -0,0 +1,5 @@ | |||
| Error: values don't meet the specifications of the schema(s) in the following chart(s): | ||||
| empty: | ||||
| - (root): employmentInfo is required | ||||
| - age: Must be greater than or equal to 0/1 | ||||
| 
 | ||||
|  | @ -0,0 +1,5 @@ | |||
| NAME: schema | ||||
| LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC | ||||
| NAMESPACE: default | ||||
| STATUS: deployed | ||||
| 
 | ||||
|  | @ -0,0 +1,4 @@ | |||
| Error: values don't meet the specifications of the schema(s) in the following chart(s): | ||||
| subchart-with-schema: | ||||
| - age: Must be greater than or equal to 0/1 | ||||
| 
 | ||||
|  | @ -0,0 +1,5 @@ | |||
| NAME: schema | ||||
| LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC | ||||
| NAMESPACE: default | ||||
| STATUS: deployed | ||||
| 
 | ||||
|  | @ -0,0 +1,6 @@ | |||
| Error: values don't meet the specifications of the schema(s) in the following chart(s): | ||||
| chart-without-schema: | ||||
| - (root): lastname is required | ||||
| subchart-with-schema: | ||||
| - (root): age is required | ||||
| 
 | ||||
|  | @ -0,0 +1,6 @@ | |||
| apiVersion: v1 | ||||
| name: chart-without-schema | ||||
| description: A Helm chart for Kubernetes | ||||
| type: application | ||||
| version: 0.1.0 | ||||
| appVersion: 0.1.0 | ||||
|  | @ -0,0 +1,6 @@ | |||
| apiVersion: v1 | ||||
| name: subchart-with-schema | ||||
| description: A Helm chart for Kubernetes | ||||
| type: application | ||||
| version: 0.1.0 | ||||
| appVersion: 0.1.0 | ||||
|  | @ -0,0 +1 @@ | |||
| # This file is intentionally blank | ||||
|  | @ -0,0 +1,15 @@ | |||
| { | ||||
|   "$schema": "http://json-schema.org/draft-07/schema#", | ||||
|   "title": "Values", | ||||
|   "type": "object", | ||||
|   "properties": { | ||||
|     "age": { | ||||
|       "description": "Age", | ||||
|       "minimum": 0, | ||||
|       "type": "integer" | ||||
|     } | ||||
|   }, | ||||
|   "required": [ | ||||
|     "age" | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										1
									
								
								cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/templates/empty.yaml
								
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										1
									
								
								cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/templates/empty.yaml
								
								
								
									vendored
								
								
									Normal file
								
							|  | @ -0,0 +1 @@ | |||
| # This file is intentionally blank | ||||
							
								
								
									
										18
									
								
								cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/values.schema.json
								
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										18
									
								
								cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/values.schema.json
								
								
								
									vendored
								
								
									Normal file
								
							|  | @ -0,0 +1,18 @@ | |||
| { | ||||
|   "$schema": "http://json-schema.org/draft-07/schema#", | ||||
|   "title": "Values", | ||||
|   "type": "object", | ||||
|   "properties": { | ||||
|     "firstname": { | ||||
|       "description": "First name", | ||||
|       "type": "string" | ||||
|     }, | ||||
|     "lastname": { | ||||
|       "type": "string" | ||||
|     } | ||||
|   }, | ||||
|   "required": [ | ||||
|     "firstname", | ||||
|     "lastname" | ||||
|   ] | ||||
| } | ||||
|  | @ -0,0 +1 @@ | |||
| firstname: "John" | ||||
|  | @ -0,0 +1,7 @@ | |||
| apiVersion: v1 | ||||
| description: Empty testing chart | ||||
| home: https://k8s.io/helm | ||||
| name: empty | ||||
| sources: | ||||
| - https://github.com/kubernetes/helm | ||||
| version: 0.1.0 | ||||
|  | @ -0,0 +1 @@ | |||
| # This file is intentionally blank | ||||
|  | @ -0,0 +1,67 @@ | |||
| { | ||||
|   "$schema": "http://json-schema.org/draft-07/schema#", | ||||
|   "properties": { | ||||
|     "addresses": { | ||||
|       "description": "List of addresses", | ||||
|       "items": { | ||||
|         "properties": { | ||||
|           "city": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "number": { | ||||
|             "type": "number" | ||||
|           }, | ||||
|           "street": { | ||||
|             "type": "string" | ||||
|           } | ||||
|         }, | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "type": "array" | ||||
|     }, | ||||
|     "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" | ||||
| } | ||||
|  | @ -0,0 +1,14 @@ | |||
| firstname: John | ||||
| lastname: Doe | ||||
| age: -5 | ||||
| likesCoffee: true | ||||
| addresses: | ||||
|   - city: Springfield | ||||
|     street: Main | ||||
|     number: 12345 | ||||
|   - city: New York | ||||
|     street: Broadway | ||||
|     number: 67890 | ||||
| phoneNumbers: | ||||
|   - "(888) 888-8888" | ||||
|   - "(555) 555-5555" | ||||
|  | @ -0,0 +1,7 @@ | |||
| apiVersion: v1 | ||||
| description: Empty testing chart | ||||
| home: https://k8s.io/helm | ||||
| name: empty | ||||
| sources: | ||||
| - https://github.com/kubernetes/helm | ||||
| version: 0.1.0 | ||||
|  | @ -0,0 +1,2 @@ | |||
| age: -5 | ||||
| employmentInfo: null | ||||
|  | @ -0,0 +1 @@ | |||
| # This file is intentionally blank | ||||
|  | @ -0,0 +1,67 @@ | |||
| { | ||||
|   "$schema": "http://json-schema.org/draft-07/schema#", | ||||
|   "properties": { | ||||
|     "addresses": { | ||||
|       "description": "List of addresses", | ||||
|       "items": { | ||||
|         "properties": { | ||||
|           "city": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "number": { | ||||
|             "type": "number" | ||||
|           }, | ||||
|           "street": { | ||||
|             "type": "string" | ||||
|           } | ||||
|         }, | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "type": "array" | ||||
|     }, | ||||
|     "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" | ||||
| } | ||||
|  | @ -0,0 +1,17 @@ | |||
| firstname: John | ||||
| lastname: Doe | ||||
| age: 25 | ||||
| likesCoffee: true | ||||
| employmentInfo: | ||||
|   title: Software Developer | ||||
|   salary: 100000 | ||||
| addresses: | ||||
|   - city: Springfield | ||||
|     street: Main | ||||
|     number: 12345 | ||||
|   - city: New York | ||||
|     street: Broadway | ||||
|     number: 67890 | ||||
| phoneNumbers: | ||||
|   - "(888) 888-8888" | ||||
|   - "(555) 555-5555" | ||||
|  | @ -26,6 +26,7 @@ wordpress/ | |||
|   LICENSE             # OPTIONAL: A plain text file containing the license for the chart | ||||
|   README.md           # OPTIONAL: A human-readable README file | ||||
|   values.yaml         # The default configuration values for this chart | ||||
|   values.schema.json  # OPTIONAL: A JSON Schema for imposing a structure on the values.yaml file | ||||
|   charts/             # A directory containing any charts upon which this chart depends. | ||||
|   templates/          # A directory of templates that, when combined with values, | ||||
|                       # will generate valid Kubernetes manifest files. | ||||
|  | @ -763,14 +764,98 @@ parent chart. | |||
| 
 | ||||
| Also, global variables of parent charts take precedence over the global variables from subcharts. | ||||
| 
 | ||||
| ### Schema Files | ||||
| 
 | ||||
| Sometimes, a chart maintainer might want to define a structure on their values. | ||||
| This can be done by defining a schema in the `values.schema.json` file. A | ||||
| schema is represented as a [JSON Schema](https://json-schema.org/). | ||||
| It might look something like this: | ||||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "$schema": "http://json-schema.org/draft-07/schema#", | ||||
|   "properties": { | ||||
|     "image": { | ||||
|       "description": "Container Image", | ||||
|       "properties": { | ||||
|         "repo": { | ||||
|           "type": "string" | ||||
|         }, | ||||
|         "tag": { | ||||
|           "type": "string" | ||||
|         } | ||||
|       }, | ||||
|       "type": "object" | ||||
|     }, | ||||
|     "name": { | ||||
|       "description": "Service name", | ||||
|       "type": "string" | ||||
|     }, | ||||
|     "port": { | ||||
|       "description": "Port", | ||||
|       "minimum": 0, | ||||
|       "type": "integer" | ||||
|     }, | ||||
|     "protocol": { | ||||
|       "type": "string" | ||||
|     } | ||||
|   }, | ||||
|   "required": [ | ||||
|     "protocol", | ||||
|     "port" | ||||
|   ], | ||||
|   "title": "Values", | ||||
|   "type": "object" | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| This schema will be applied to the values to validate it. Validation occurs | ||||
| when any of the following commands are invoked: | ||||
| 
 | ||||
| * `helm install` | ||||
| * `helm upgrade` | ||||
| * `helm lint` | ||||
| * `helm template` | ||||
| 
 | ||||
| An example of a | ||||
| `values.yaml` file that meets the requirements of this schema might look | ||||
| something like this: | ||||
| 
 | ||||
| ```yaml | ||||
| name: frontend | ||||
| protocol: https | ||||
| port: 443 | ||||
| ``` | ||||
| 
 | ||||
| Note that the schema is applied to the final `.Values` object, and not just to | ||||
| the `values.yaml` file. This means that the following `yaml` file is valid, | ||||
| given that the chart is installed with the appropriate `--set` option shown | ||||
| below. | ||||
| 
 | ||||
| ```yaml | ||||
| name: frontend | ||||
| protocol: https | ||||
| ``` | ||||
| 
 | ||||
| ```` | ||||
| helm install --set port=443 | ||||
| ```` | ||||
| 
 | ||||
| Furthermore, the final `.Values` object is checked against *all* subchart | ||||
| schemas. This means that restrictions on a subchart can't be circumvented by a | ||||
| parent chart. This also works backwards - if a subchart has a requirement that | ||||
| is not met in the subchart's `values.yaml` file, the parent chart *must* | ||||
| satisfy those restrictions in order to be valid. | ||||
| 
 | ||||
| ### References | ||||
| 
 | ||||
| When it comes to writing templates and values files, there are several | ||||
| When it comes to writing templates, values, and schema files, there are several | ||||
| standard references that will help you out. | ||||
| 
 | ||||
| - [Go templates](https://godoc.org/text/template) | ||||
| - [Extra template functions](https://godoc.org/github.com/Masterminds/sprig) | ||||
| - [The YAML format](http://yaml.org/spec/) | ||||
| - [JSON Schema](https://json-schema.org/) | ||||
| 
 | ||||
| ## Using Helm to Manage Charts | ||||
| 
 | ||||
|  |  | |||
|  | @ -29,6 +29,8 @@ var ( | |||
| 	invalidArchivedChartPath     = "../../cmd/helm/testdata/testcharts/invalidcompressedchart0.1.0.tgz" | ||||
| 	chartDirPath                 = "../../cmd/helm/testdata/testcharts/decompressedchart/" | ||||
| 	chartMissingManifest         = "../../cmd/helm/testdata/testcharts/chart-missing-manifest" | ||||
| 	chartSchema                  = "../../cmd/helm/testdata/testcharts/chart-with-schema" | ||||
| 	chartSchemaNegative          = "../../cmd/helm/testdata/testcharts/chart-with-schema-negative" | ||||
| ) | ||||
| 
 | ||||
| func TestLintChart(t *testing.T) { | ||||
|  | @ -47,4 +49,10 @@ func TestLintChart(t *testing.T) { | |||
| 	if _, err := lintChart(chartMissingManifest, values, namespace, strict); err == nil { | ||||
| 		t.Error("Expected a chart parsing error") | ||||
| 	} | ||||
| 	if _, err := lintChart(chartSchema, values, namespace, strict); err != nil { | ||||
| 		t.Error(err) | ||||
| 	} | ||||
| 	if _, err := lintChart(chartSchemaNegative, values, namespace, strict); err != nil { | ||||
| 		t.Error(err) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -31,6 +31,8 @@ type Chart struct { | |||
| 	RawValues []byte | ||||
| 	// Values are default config for this template.
 | ||||
| 	Values map[string]interface{} | ||||
| 	// Schema is an optional JSON schema for imposing structure on Values
 | ||||
| 	Schema []byte | ||||
| 	// Files are miscellaneous files in a chart archive,
 | ||||
| 	// e.g. README, LICENSE, etc.
 | ||||
| 	Files []*File | ||||
|  |  | |||
|  | @ -90,6 +90,8 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { | |||
| 				return c, errors.Wrap(err, "cannot load values.yaml") | ||||
| 			} | ||||
| 			c.RawValues = f.Data | ||||
| 		case f.Name == "values.schema.json": | ||||
| 			c.Schema = 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/"): | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ limitations under the License. | |||
| package loader | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"helm.sh/helm/pkg/chart" | ||||
|  | @ -78,6 +79,10 @@ icon: https://example.com/64x64.png | |||
| 			Name: "values.yaml", | ||||
| 			Data: []byte("var: some values"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name: "values.schema.json", | ||||
| 			Data: []byte("type: Values"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name: "templates/deployment.yaml", | ||||
| 			Data: []byte("some deployment"), | ||||
|  | @ -101,6 +106,10 @@ icon: https://example.com/64x64.png | |||
| 		t.Error("Expected chart values to be populated with default values") | ||||
| 	} | ||||
| 
 | ||||
| 	if !bytes.Equal(c.Schema, []byte("type: Values")) { | ||||
| 		t.Error("Expected chart schema to be populated with default values") | ||||
| 	} | ||||
| 
 | ||||
| 	if len(c.Templates) != 2 { | ||||
| 		t.Errorf("Expected number of templates == 2, got %d", len(c.Templates)) | ||||
| 	} | ||||
|  |  | |||
|  | @ -0,0 +1,14 @@ | |||
| firstname: John | ||||
| lastname: Doe | ||||
| age: -5 | ||||
| likesCoffee: true | ||||
| addresses: | ||||
|   - city: Springfield | ||||
|     street: Main | ||||
|     number: 12345 | ||||
|   - city: New York | ||||
|     street: Broadway | ||||
|     number: 67890 | ||||
| phoneNumbers: | ||||
|   - "(888) 888-8888" | ||||
|   - "(555) 555-5555" | ||||
|  | @ -0,0 +1,67 @@ | |||
| { | ||||
|   "$schema": "http://json-schema.org/draft-07/schema#", | ||||
|   "properties": { | ||||
|     "addresses": { | ||||
|       "description": "List of addresses", | ||||
|       "items": { | ||||
|         "properties": { | ||||
|           "city": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "number": { | ||||
|             "type": "number" | ||||
|           }, | ||||
|           "street": { | ||||
|             "type": "string" | ||||
|           } | ||||
|         }, | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "type": "array" | ||||
|     }, | ||||
|     "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" | ||||
| } | ||||
|  | @ -0,0 +1,17 @@ | |||
| firstname: John | ||||
| lastname: Doe | ||||
| age: 25 | ||||
| likesCoffee: true | ||||
| employmentInfo: | ||||
|   title: Software Developer | ||||
|   salary: 100000 | ||||
| addresses: | ||||
|   - city: Springfield | ||||
|     street: Main | ||||
|     number: 12345 | ||||
|   - city: New York | ||||
|     street: Broadway | ||||
|     number: 67890 | ||||
| phoneNumbers: | ||||
|   - "(888) 888-8888" | ||||
|   - "(555) 555-5555" | ||||
|  | @ -17,6 +17,7 @@ limitations under the License. | |||
| package chartutil | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
|  | @ -25,6 +26,7 @@ import ( | |||
| 
 | ||||
| 	"github.com/ghodss/yaml" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"github.com/xeipuuv/gojsonschema" | ||||
| 
 | ||||
| 	"helm.sh/helm/pkg/chart" | ||||
| ) | ||||
|  | @ -133,6 +135,64 @@ func ReadValuesFile(filename string) (Values, error) { | |||
| 	return ReadValues(data) | ||||
| } | ||||
| 
 | ||||
| // ValidateAgainstSchema checks that values does not violate the structure laid out in schema
 | ||||
| func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) error { | ||||
| 	var sb strings.Builder | ||||
| 	if chrt.Schema != nil { | ||||
| 		err := ValidateAgainstSingleSchema(values, chrt.Schema) | ||||
| 		if err != nil { | ||||
| 			sb.WriteString(fmt.Sprintf("%s:\n", chrt.Name())) | ||||
| 			sb.WriteString(err.Error()) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// For each dependency, recurively call this function with the coalesced values
 | ||||
| 	for _, subchrt := range chrt.Dependencies() { | ||||
| 		subchrtValues := values[subchrt.Name()].(map[string]interface{}) | ||||
| 		if err := ValidateAgainstSchema(subchrt, subchrtValues); err != nil { | ||||
| 			sb.WriteString(err.Error()) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if sb.Len() > 0 { | ||||
| 		return errors.New(sb.String()) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema
 | ||||
| func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) error { | ||||
| 	valuesData, err := yaml.Marshal(values) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	valuesJSON, err := yaml.YAMLToJSON(valuesData) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if bytes.Equal(valuesJSON, []byte("null")) { | ||||
| 		valuesJSON = []byte("{}") | ||||
| 	} | ||||
| 	schemaLoader := gojsonschema.NewBytesLoader(schemaJSON) | ||||
| 	valuesLoader := gojsonschema.NewBytesLoader(valuesJSON) | ||||
| 
 | ||||
| 	result, err := gojsonschema.Validate(schemaLoader, valuesLoader) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if !result.Valid() { | ||||
| 		var sb strings.Builder | ||||
| 		for _, desc := range result.Errors() { | ||||
| 			sb.WriteString(fmt.Sprintf("- %s\n", desc)) | ||||
| 		} | ||||
| 		return errors.New(sb.String()) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // CoalesceValues coalesces all of the values in a chart (and its subcharts).
 | ||||
| //
 | ||||
| // Values are coalesced together using the following rules:
 | ||||
|  | @ -331,6 +391,11 @@ func ToRenderValues(chrt *chart.Chart, chrtVals map[string]interface{}, options | |||
| 		return top, err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := ValidateAgainstSchema(chrt, vals); err != nil { | ||||
| 		errFmt := "values don't meet the specifications of the schema(s) in the following chart(s):\n%s" | ||||
| 		return top, fmt.Errorf(errFmt, err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	top["Values"] = vals | ||||
| 	return top, nil | ||||
| } | ||||
|  |  | |||
|  | @ -20,6 +20,7 @@ import ( | |||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"testing" | ||||
| 	"text/template" | ||||
| 
 | ||||
|  | @ -160,6 +161,125 @@ func TestReadValuesFile(t *testing.T) { | |||
| 	matchValues(t, data) | ||||
| } | ||||
| 
 | ||||
| func TestValidateAgainstSingleSchema(t *testing.T) { | ||||
| 	values, err := ReadValuesFile("./testdata/test-values.yaml") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Error reading YAML file: %s", err) | ||||
| 	} | ||||
| 	schema, err := ioutil.ReadFile("./testdata/test-values.schema.json") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Error reading YAML file: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := ValidateAgainstSingleSchema(values, schema); err != nil { | ||||
| 		t.Errorf("Error validating Values against Schema: %s", err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestValidateAgainstSingleSchemaNegative(t *testing.T) { | ||||
| 	values, err := ReadValuesFile("./testdata/test-values-negative.yaml") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Error reading YAML file: %s", err) | ||||
| 	} | ||||
| 	schema, err := ioutil.ReadFile("./testdata/test-values.schema.json") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Error reading YAML file: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	var errString string | ||||
| 	if err := ValidateAgainstSingleSchema(values, schema); err == nil { | ||||
| 		t.Fatalf("Expected an error, but got nil") | ||||
| 	} else { | ||||
| 		errString = err.Error() | ||||
| 	} | ||||
| 
 | ||||
| 	expectedErrString := `- (root): employmentInfo is required | ||||
| - age: Must be greater than or equal to 0/1 | ||||
| ` | ||||
| 	if errString != expectedErrString { | ||||
| 		t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| const subchrtSchema = `{ | ||||
|   "$schema": "http://json-schema.org/draft-07/schema#", | ||||
|   "title": "Values", | ||||
|   "type": "object", | ||||
|   "properties": { | ||||
|     "age": { | ||||
|       "description": "Age", | ||||
|       "minimum": 0, | ||||
|       "type": "integer" | ||||
|     } | ||||
|   }, | ||||
|   "required": [ | ||||
|     "age" | ||||
|   ] | ||||
| } | ||||
| ` | ||||
| 
 | ||||
| func TestValidateAgainstSchema(t *testing.T) { | ||||
| 	subchrtJSON := []byte(subchrtSchema) | ||||
| 	subchrt := &chart.Chart{ | ||||
| 		Metadata: &chart.Metadata{ | ||||
| 			Name: "subchrt", | ||||
| 		}, | ||||
| 		Schema: subchrtJSON, | ||||
| 	} | ||||
| 	chrt := &chart.Chart{ | ||||
| 		Metadata: &chart.Metadata{ | ||||
| 			Name: "chrt", | ||||
| 		}, | ||||
| 	} | ||||
| 	chrt.AddDependency(subchrt) | ||||
| 
 | ||||
| 	vals := map[string]interface{}{ | ||||
| 		"name": "John", | ||||
| 		"subchrt": map[string]interface{}{ | ||||
| 			"age": 25, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	if err := ValidateAgainstSchema(chrt, vals); err != nil { | ||||
| 		t.Errorf("Error validating Values against Schema: %s", err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestValidateAgainstSchemaNegative(t *testing.T) { | ||||
| 	subchrtJSON := []byte(subchrtSchema) | ||||
| 	subchrt := &chart.Chart{ | ||||
| 		Metadata: &chart.Metadata{ | ||||
| 			Name: "subchrt", | ||||
| 		}, | ||||
| 		Schema: subchrtJSON, | ||||
| 	} | ||||
| 	chrt := &chart.Chart{ | ||||
| 		Metadata: &chart.Metadata{ | ||||
| 			Name: "chrt", | ||||
| 		}, | ||||
| 	} | ||||
| 	chrt.AddDependency(subchrt) | ||||
| 
 | ||||
| 	vals := map[string]interface{}{ | ||||
| 		"name":    "John", | ||||
| 		"subchrt": map[string]interface{}{}, | ||||
| 	} | ||||
| 
 | ||||
| 	var errString string | ||||
| 	if err := ValidateAgainstSchema(chrt, vals); err == nil { | ||||
| 		t.Fatalf("Expected an error, but got nil") | ||||
| 	} else { | ||||
| 		errString = err.Error() | ||||
| 	} | ||||
| 
 | ||||
| 	expectedErrString := `subchrt: | ||||
| - (root): age is required | ||||
| ` | ||||
| 	if errString != expectedErrString { | ||||
| 		t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func ExampleValues() { | ||||
| 	doc := ` | ||||
| title: "Moby Dick" | ||||
|  | @ -399,6 +519,7 @@ func TestCoalesceTables(t *testing.T) { | |||
| 		t.Errorf("Expected boat string, got %v", dst["boat"]) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestPathValue(t *testing.T) { | ||||
| 	doc := ` | ||||
| title: "Moby Dick" | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ limitations under the License. | |||
| package rules | ||||
| 
 | ||||
| import ( | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 
 | ||||
|  | @ -48,6 +49,19 @@ func validateValuesFileExistence(valuesPath string) error { | |||
| } | ||||
| 
 | ||||
| func validateValuesFile(valuesPath string) error { | ||||
| 	_, err := chartutil.ReadValuesFile(valuesPath) | ||||
| 	return errors.Wrap(err, "unable to parse YAML") | ||||
| 	values, err := chartutil.ReadValuesFile(valuesPath) | ||||
| 	if err != nil { | ||||
| 		return errors.Wrap(err, "unable to parse YAML") | ||||
| 	} | ||||
| 
 | ||||
| 	ext := filepath.Ext(valuesPath) | ||||
| 	schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json" | ||||
| 	schema, err := ioutil.ReadFile(schemaPath) | ||||
| 	if len(schema) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return chartutil.ValidateAgainstSingleSchema(values, schema) | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue