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" |   revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053" | ||||||
|   version = "v1.3.0" |   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]] | [[projects]] | ||||||
|   digest = "1:340553b2fdaab7d53e63fd40f8ed82203bdd3274253055bdb80a46828482ef81" |   digest = "1:340553b2fdaab7d53e63fd40f8ed82203bdd3274253055bdb80a46828482ef81" | ||||||
|   name = "github.com/xenolf/lego" |   name = "github.com/xenolf/lego" | ||||||
|  | @ -1676,6 +1700,7 @@ | ||||||
|     "github.com/spf13/pflag", |     "github.com/spf13/pflag", | ||||||
|     "github.com/stretchr/testify/assert", |     "github.com/stretchr/testify/assert", | ||||||
|     "github.com/stretchr/testify/suite", |     "github.com/stretchr/testify/suite", | ||||||
|  |     "github.com/xeipuuv/gojsonschema", | ||||||
|     "golang.org/x/crypto/openpgp", |     "golang.org/x/crypto/openpgp", | ||||||
|     "golang.org/x/crypto/openpgp/clearsign", |     "golang.org/x/crypto/openpgp/clearsign", | ||||||
|     "golang.org/x/crypto/openpgp/errors", |     "golang.org/x/crypto/openpgp/errors", | ||||||
|  |  | ||||||
|  | @ -107,3 +107,6 @@ | ||||||
|   go-tests = true |   go-tests = true | ||||||
|   unused-packages = 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, | 			wantError: true, | ||||||
| 			golden:    "output/install-chart-bad-type.txt", | 			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) | 	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 |   LICENSE             # OPTIONAL: A plain text file containing the license for the chart | ||||||
|   README.md           # OPTIONAL: A human-readable README file |   README.md           # OPTIONAL: A human-readable README file | ||||||
|   values.yaml         # The default configuration values for this chart |   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. |   charts/             # A directory containing any charts upon which this chart depends. | ||||||
|   templates/          # A directory of templates that, when combined with values, |   templates/          # A directory of templates that, when combined with values, | ||||||
|                       # will generate valid Kubernetes manifest files. |                       # 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. | 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 | ### 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. | standard references that will help you out. | ||||||
| 
 | 
 | ||||||
| - [Go templates](https://godoc.org/text/template) | - [Go templates](https://godoc.org/text/template) | ||||||
| - [Extra template functions](https://godoc.org/github.com/Masterminds/sprig) | - [Extra template functions](https://godoc.org/github.com/Masterminds/sprig) | ||||||
| - [The YAML format](http://yaml.org/spec/) | - [The YAML format](http://yaml.org/spec/) | ||||||
|  | - [JSON Schema](https://json-schema.org/) | ||||||
| 
 | 
 | ||||||
| ## Using Helm to Manage Charts | ## Using Helm to Manage Charts | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -29,6 +29,8 @@ var ( | ||||||
| 	invalidArchivedChartPath     = "../../cmd/helm/testdata/testcharts/invalidcompressedchart0.1.0.tgz" | 	invalidArchivedChartPath     = "../../cmd/helm/testdata/testcharts/invalidcompressedchart0.1.0.tgz" | ||||||
| 	chartDirPath                 = "../../cmd/helm/testdata/testcharts/decompressedchart/" | 	chartDirPath                 = "../../cmd/helm/testdata/testcharts/decompressedchart/" | ||||||
| 	chartMissingManifest         = "../../cmd/helm/testdata/testcharts/chart-missing-manifest" | 	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) { | func TestLintChart(t *testing.T) { | ||||||
|  | @ -47,4 +49,10 @@ func TestLintChart(t *testing.T) { | ||||||
| 	if _, err := lintChart(chartMissingManifest, values, namespace, strict); err == nil { | 	if _, err := lintChart(chartMissingManifest, values, namespace, strict); err == nil { | ||||||
| 		t.Error("Expected a chart parsing error") | 		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 | 	RawValues []byte | ||||||
| 	// Values are default config for this template.
 | 	// Values are default config for this template.
 | ||||||
| 	Values map[string]interface{} | 	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,
 | 	// Files are miscellaneous files in a chart archive,
 | ||||||
| 	// e.g. README, LICENSE, etc.
 | 	// e.g. README, LICENSE, etc.
 | ||||||
| 	Files []*File | 	Files []*File | ||||||
|  |  | ||||||
|  | @ -90,6 +90,8 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { | ||||||
| 				return c, errors.Wrap(err, "cannot load values.yaml") | 				return c, errors.Wrap(err, "cannot load values.yaml") | ||||||
| 			} | 			} | ||||||
| 			c.RawValues = f.Data | 			c.RawValues = f.Data | ||||||
|  | 		case f.Name == "values.schema.json": | ||||||
|  | 			c.Schema = 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/"): | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ limitations under the License. | ||||||
| package loader | package loader | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"helm.sh/helm/pkg/chart" | 	"helm.sh/helm/pkg/chart" | ||||||
|  | @ -78,6 +79,10 @@ icon: https://example.com/64x64.png | ||||||
| 			Name: "values.yaml", | 			Name: "values.yaml", | ||||||
| 			Data: []byte("var: some values"), | 			Data: []byte("var: some values"), | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Name: "values.schema.json", | ||||||
|  | 			Data: []byte("type: Values"), | ||||||
|  | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Name: "templates/deployment.yaml", | 			Name: "templates/deployment.yaml", | ||||||
| 			Data: []byte("some deployment"), | 			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") | 		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 { | 	if len(c.Templates) != 2 { | ||||||
| 		t.Errorf("Expected number of templates == 2, got %d", len(c.Templates)) | 		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 | package chartutil | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
|  | @ -25,6 +26,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/ghodss/yaml" | 	"github.com/ghodss/yaml" | ||||||
| 	"github.com/pkg/errors" | 	"github.com/pkg/errors" | ||||||
|  | 	"github.com/xeipuuv/gojsonschema" | ||||||
| 
 | 
 | ||||||
| 	"helm.sh/helm/pkg/chart" | 	"helm.sh/helm/pkg/chart" | ||||||
| ) | ) | ||||||
|  | @ -133,6 +135,64 @@ func ReadValuesFile(filename string) (Values, error) { | ||||||
| 	return ReadValues(data) | 	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).
 | // CoalesceValues coalesces all of the values in a chart (and its subcharts).
 | ||||||
| //
 | //
 | ||||||
| // Values are coalesced together using the following rules:
 | // 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 | 		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 | 	top["Values"] = vals | ||||||
| 	return top, nil | 	return top, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -20,6 +20,7 @@ import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"text/template" | 	"text/template" | ||||||
| 
 | 
 | ||||||
|  | @ -160,6 +161,125 @@ func TestReadValuesFile(t *testing.T) { | ||||||
| 	matchValues(t, data) | 	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() { | func ExampleValues() { | ||||||
| 	doc := ` | 	doc := ` | ||||||
| title: "Moby Dick" | title: "Moby Dick" | ||||||
|  | @ -399,6 +519,7 @@ func TestCoalesceTables(t *testing.T) { | ||||||
| 		t.Errorf("Expected boat string, got %v", dst["boat"]) | 		t.Errorf("Expected boat string, got %v", dst["boat"]) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
| func TestPathValue(t *testing.T) { | func TestPathValue(t *testing.T) { | ||||||
| 	doc := ` | 	doc := ` | ||||||
| title: "Moby Dick" | title: "Moby Dick" | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ limitations under the License. | ||||||
| package rules | package rules | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"io/ioutil" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 
 | 
 | ||||||
|  | @ -48,6 +49,19 @@ func validateValuesFileExistence(valuesPath string) error { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func validateValuesFile(valuesPath string) error { | func validateValuesFile(valuesPath string) error { | ||||||
| 	_, err := chartutil.ReadValuesFile(valuesPath) | 	values, err := chartutil.ReadValuesFile(valuesPath) | ||||||
| 	return errors.Wrap(err, "unable to parse YAML") | 	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