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