Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data value schema validation #653

Merged
merged 2 commits into from
May 9, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions pkg/assertions/yamlmeta.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,35 @@

package assertions

import "github.com/vmware-tanzu/carvel-ytt/pkg/yamlmeta"
import (
"fmt"

"github.com/vmware-tanzu/carvel-ytt/pkg/yamlmeta"
)

const validations = "validations"

// AddValidations appends validation Rules to node's validations metadata, later retrieved via GetValidations().
func AddValidations(node yamlmeta.Node, rules []Rule) {
metas := node.GetMeta("validations")
if currRules, ok := metas.([]Rule); ok {
metas := node.GetMeta(validations)
if metas != nil {
currRules, ok := metas.([]Rule)
if !ok {
panic(fmt.Sprintf("Unexpected validations in meta : %s %v", node.GetPosition().AsCompactString(), metas))
}
rules = append(currRules, rules...)
}
SetValidations(node, rules)
}

// SetValidations attaches validation Rules to node's metadata, later retrieved via GetValidations().
func SetValidations(node yamlmeta.Node, rules []Rule) {
node.SetMeta("validations", rules)
node.SetMeta(validations, rules)
}

// GetValidations retrieves validation Rules from node metadata, set previously via SetValidations().
func GetValidations(node yamlmeta.Node) []Rule {
metas := node.GetMeta("validations")
metas := node.GetMeta(validations)
if rules, ok := metas.([]Rule); ok {
vmunishwar marked this conversation as resolved.
Show resolved Hide resolved
return rules
}
Expand Down
335 changes: 333 additions & 2 deletions pkg/cmd/template/assert_validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
package template_test

import (
"testing"

cmdtpl "github.com/vmware-tanzu/carvel-ytt/pkg/cmd/template"
"github.com/vmware-tanzu/carvel-ytt/pkg/files"

"testing"
)

func TestAssertValidateOnDataValuesSucceeds(t *testing.T) {
Expand Down Expand Up @@ -371,3 +371,334 @@ values: #@ data.values
assertFails(t, filesToProcess, expectedErr, opts)
})
}

func TestSchemaValidationSucceeds(t *testing.T) {
t.Run("when validations pass using --data-values-inspect", func(t *testing.T) {
opts := cmdtpl.NewOptions()
opts.DataValuesFlags.Inspect = true

schemaYAML := `#@ load("@ytt:assert", "assert")
#@data/values-schema
#@schema/validation ("a non empty data values", lambda v: True if v else assert.fail("data values was empty"))
---
#@schema/validation ("a map with more than 3 elements", lambda v: True if len(v) > 3 else assert.fail("length of map was less than 3"))
my_map:
#@schema/validation ("a non-empty string", lambda v: True if len(v) > 0 else assert.fail("length of string was 0"))
string: server.example.com
#@schema/validation ("an int over 9000", lambda v: True if v > 9000 else assert.fail("int was less than 9000"))
int: 54321
#@schema/validation ("a float less than pi", lambda v: True if v < 3.1415 else assert.fail("float was more than 3.1415"))
float: 2.3
#@schema/validation ("bool evaluating to true", lambda v: v)
bool: true
#@schema/validation ("a null value", lambda v: True if v == None else assert.fail("value was not null"))
#@schema/nullable
nil: ""
#@schema/validation ("an array with 1 or more items", lambda v: True if len(v) >= 1 else assert.fail("array was empty"))
#@schema/default ['abc']
my_array:
#@schema/validation ("a non-empty string", lambda v: True if len(v) > 0 else assert.fail("length of string was 0"))
- abc
`

expected := `my_map:
string: server.example.com
int: 54321
float: 2.3
bool: true
nil: null
my_array:
- abc
`

filesToProcess := files.NewSortedFiles([]*files.File{
files.MustNewFileFromSource(files.NewBytesSource("schema.yml", []byte(schemaYAML))),
})

assertSucceedsDocSet(t, filesToProcess, expected, opts)
})
t.Run("when validations pass on an array item using --data-values-inspect", func(t *testing.T) {
opts := cmdtpl.NewOptions()
opts.DataValuesFlags.Inspect = true
schemaYAML := `#@ load("@ytt:assert", "assert")
#@data/values-schema
---
my_array:
#@schema/validation ("an integer larger than 4", lambda v: True if v > 4 else assert.fail("values less than 5"))
- 5
`
dvYAML := `#@data/values
---
my_array:
- 5
- 6
- 7
`

expected := `my_array:
- 5
- 6
- 7
`

filesToProcess := files.NewSortedFiles([]*files.File{
files.MustNewFileFromSource(files.NewBytesSource("schema.yml", []byte(schemaYAML))),
files.MustNewFileFromSource(files.NewBytesSource("dv.yml", []byte(dvYAML))),
})

assertSucceedsDocSet(t, filesToProcess, expected, opts)
})
t.Run("when validations pass with other optional schema annotations", func(t *testing.T) {
opts := cmdtpl.NewOptions()
opts.DataValuesFlags.Inspect = true
schemaYAML := `#@ load("@ytt:assert", "assert")
#@data/values-schema
---
#@schema/default [5]
my_array:
#@schema/validation ("an integer larger than 4", lambda v: True if v > 4 else assert.fail("values less than 5"))
- 6

#@schema/type any= True
#@schema/validation ("an integer larger than 4", lambda v: True if v > 4 else assert.fail("values less than 5"))
my_map: 5

#@schema/nullable
#@schema/validation ("an integer larger than 4", lambda v: True if v > 4 else assert.fail("values less than 5"))
my_other_map: 5
`
dvYAML := `#@data/values
---
my_other_map: 6
`

expected := `my_array:
- 5
my_map: 5
my_other_map: 6
`

filesToProcess := files.NewSortedFiles([]*files.File{
files.MustNewFileFromSource(files.NewBytesSource("schema.yml", []byte(schemaYAML))),
files.MustNewFileFromSource(files.NewBytesSource("dv.yml", []byte(dvYAML))),
})

assertSucceedsDocSet(t, filesToProcess, expected, opts)
})
t.Run("when validations on library values pass", func(t *testing.T) {
opts := cmdtpl.NewOptions()
configYAML := `
#@ load("@ytt:template", "template")
#@ load("@ytt:library", "library")
---

#@ def additional_vals():
int: 10
#@ end

#@ lib = library.get("lib")
#@ lib2 = lib.with_data_values_schema(additional_vals())
--- #@ template.replace(lib.eval())
--- #@ template.replace(lib2.eval())
`

libSchemaYAML := `#@ load("@ytt:assert", "assert")

#@data/values-schema
---
#@schema/validation ("an integer over 1", lambda v: True if v > 1 else assert.fail("value was less than 1"))
int: 2
`

libConfigYAML := `
#@ load("@ytt:data", "data")
---
values: #@ data.values
`

expected := `values:
int: 2
---
values:
int: 10
`

filesToProcess := files.NewSortedFiles([]*files.File{
files.MustNewFileFromSource(files.NewBytesSource("config.yml", []byte(configYAML))),
files.MustNewFileFromSource(files.NewBytesSource("_ytt_lib/lib/schema.yml", []byte(libSchemaYAML))),
files.MustNewFileFromSource(files.NewBytesSource("_ytt_lib/lib/config.yml", []byte(libConfigYAML))),
})

assertSucceedsDocSet(t, filesToProcess, expected, opts)
})
}

func TestSchemaValidationFails(t *testing.T) {
t.Run("when validations fail", func(t *testing.T) {
opts := cmdtpl.NewOptions()
schemaYAML := `#@ load("@ytt:assert", "assert")

#@data/values-schema
---
#@schema/validation ("a non-empty string", lambda v: True if len(v) > 0 else assert.fail("length of string was 0"))
string: ""
#@schema/validation ("an int over 9000", lambda v: True if v > 9000 else assert.fail("int was less than 9000"))
int: 5432
#@schema/validation ("a float less than pi", lambda v: True if v < 3.1415 else assert.fail("float was more than 3.1415"))
float: 21.3
#@schema/validation ("bool evaluating to true", lambda v: v)
bool: false
#@schema/validation ("a null value", lambda v: True if v == None else assert.fail("value was not null"))
nil: ""
#@schema/validation ("an array with 1 or more items", lambda v: True if len(v) >= 1 else assert.fail("array was empty"))
my_array:
#@schema/validation ("a non-empty string", lambda v: True if len(v) > 0 else assert.fail("length of string was 0"))
- abc
`

expectedErr := `One or more data values were invalid:
- "string" (schema.yml:6) requires "a non-empty string"; assert.fail: fail: length of string was 0 (by schema.yml:5)
- "int" (schema.yml:8) requires "an int over 9000"; assert.fail: fail: int was less than 9000 (by schema.yml:7)
- "float" (schema.yml:10) requires "a float less than pi"; assert.fail: fail: float was more than 3.1415 (by schema.yml:9)
- "bool" (schema.yml:12) requires "bool evaluating to true" (by schema.yml:11)
- "nil" (schema.yml:14) requires "a null value"; assert.fail: fail: value was not null (by schema.yml:13)
- "my_array" (schema.yml:16) requires "an array with 1 or more items"; assert.fail: fail: array was empty (by schema.yml:15)`

filesToProcess := files.NewSortedFiles([]*files.File{
files.MustNewFileFromSource(files.NewBytesSource("schema.yml", []byte(schemaYAML))),
})

assertFails(t, filesToProcess, expectedErr, opts)
})
t.Run("when validations fail on an array item with data values overlays", func(t *testing.T) {
opts := cmdtpl.NewOptions()

schemaYAML := `#@ load("@ytt:assert", "assert")
#@data/values-schema
---
my_array:
#@schema/validation ("an integer larger than 4", lambda v: True if v > 4 else assert.fail("values less than 5"))
- 5
`
dvYAML := `#@data/values
---
my_array:
- 5
- 6
- 7
- 1
- 2
- 3
`

expectedErr := `One or more data values were invalid:
- array item (dv.yml:7) requires "an integer larger than 4"; assert.fail: fail: values less than 5 (by schema.yml:5)
- array item (dv.yml:8) requires "an integer larger than 4"; assert.fail: fail: values less than 5 (by schema.yml:5)
- array item (dv.yml:9) requires "an integer larger than 4"; assert.fail: fail: values less than 5 (by schema.yml:5)`

filesToProcess := files.NewSortedFiles([]*files.File{
files.MustNewFileFromSource(files.NewBytesSource("schema.yml", []byte(schemaYAML))),
files.MustNewFileFromSource(files.NewBytesSource("dv.yml", []byte(dvYAML))),
})

assertFails(t, filesToProcess, expectedErr, opts)
})
t.Run("when validations on a document fail", func(t *testing.T) {
opts := cmdtpl.NewOptions()
schemaYAML := `#@ load("@ytt:assert", "assert")
#@data/values-schema
#@schema/validation ("need more than 1 data value", lambda v: True if len(v) > 1 else assert.fail("less than 1 data values present"))
---
my_map: "abc"
`

expectedErr := `One or more data values were invalid:
- document (schema.yml:4) requires "need more than 1 data value"; assert.fail: fail: less than 1 data values present (by schema.yml:3)
`

filesToProcess := files.NewSortedFiles([]*files.File{
files.MustNewFileFromSource(files.NewBytesSource("schema.yml", []byte(schemaYAML))),
})

assertFails(t, filesToProcess, expectedErr, opts)
})
t.Run("when validations fail with other optional schema annotations", func(t *testing.T) {
opts := cmdtpl.NewOptions()
opts.DataValuesFlags.Inspect = true
schemaYAML := `#@ load("@ytt:assert", "assert")
#@data/values-schema
---
#@schema/default [3]
my_array:
#@schema/validation ("an integer larger than 4", lambda v: True if v > 4 else assert.fail("values less than 5"))
- 6

#@schema/type any= True
#@schema/validation ("an integer larger than 4", lambda v: True if v > 4 else assert.fail("values less than 5"))
my_map: 3

#@schema/nullable
#@schema/validation ("an integer larger than 4", lambda v: True if v > 4 else assert.fail("values less than 5"))
my_other_map: 5
`
dvYAML := `#@data/values
---
my_other_map: 3
`

expectedErr := `One or more data values were invalid:
- array item (schema.yml:5) requires "an integer larger than 4"; assert.fail: fail: values less than 5 (by schema.yml:6)
- "my_map" (schema.yml:11) requires "an integer larger than 4"; assert.fail: fail: values less than 5 (by schema.yml:10)
- "my_other_map" (dv.yml:3) requires "an integer larger than 4"; assert.fail: fail: values less than 5 (by schema.yml:14)
`

filesToProcess := files.NewSortedFiles([]*files.File{
files.MustNewFileFromSource(files.NewBytesSource("schema.yml", []byte(schemaYAML))),
files.MustNewFileFromSource(files.NewBytesSource("dv.yml", []byte(dvYAML))),
})

assertFails(t, filesToProcess, expectedErr, opts)
})
t.Run("when validations on library schema fail", func(t *testing.T) {
opts := cmdtpl.NewOptions()
configYAML := `
#@ load("@ytt:template", "template")
#@ load("@ytt:library", "library")
---

#@ lib = library.get("lib")
--- #@ template.replace(lib.eval())
`

libSchemaYAML := `#@ load("@ytt:assert", "assert")

#@data/values-schema
---
#@schema/validation ("an integer over 1", lambda v: True if v > 1 else assert.fail("value was less than 1"))
int: 1
`

libConfigYAML := `
#@ load("@ytt:data", "data")
---
values: #@ data.values
`

expectedErr := `- library.eval: Evaluating library 'lib': One or more data values were invalid:
in <toplevel>
config.yml:7 | --- #@ template.replace(lib.eval())

reason:
- "int" (_ytt_lib/lib/schema.yml:6) requires "an integer over 1"; assert.fail: fail: value was less than 1 (by _ytt_lib/lib/schema.yml:5)
`

filesToProcess := files.NewSortedFiles([]*files.File{
files.MustNewFileFromSource(files.NewBytesSource("config.yml", []byte(configYAML))),
files.MustNewFileFromSource(files.NewBytesSource("_ytt_lib/lib/schema.yml", []byte(libSchemaYAML))),
files.MustNewFileFromSource(files.NewBytesSource("_ytt_lib/lib/config.yml", []byte(libConfigYAML))),
})

assertFails(t, filesToProcess, expectedErr, opts)
})

}
Loading