diff --git a/pkg/workspace/assert.go b/pkg/assertions/assert.go similarity index 54% rename from pkg/workspace/assert.go rename to pkg/assertions/assert.go index 03fc2ff2..8147eb30 100644 --- a/pkg/workspace/assert.go +++ b/pkg/assertions/assert.go @@ -1,15 +1,14 @@ // Copyright 2022 VMware, Inc. // SPDX-License-Identifier: Apache-2.0 -package workspace +package assertions import ( "fmt" + "github.com/k14s/starlark-go/starlark" - "github.com/vmware-tanzu/carvel-ytt/pkg/filepos" "github.com/vmware-tanzu/carvel-ytt/pkg/template" "github.com/vmware-tanzu/carvel-ytt/pkg/yamlmeta" - "github.com/vmware-tanzu/carvel-ytt/pkg/yamltemplate" ) // Declare @assert/... annotation names @@ -17,23 +16,28 @@ const ( AnnotationAssertValidate template.AnnotationName = "assert/validate" ) -// ProcessAndRunValidations takes a root Node, and threadName, and traverses the tree checking for assert annotations. -// Validations are processed and executed using the value of the annotated node as the parameter to the assertions. +// ProcessAndRunValidations takes a root Node, and threadName, and validates each Node in the tree. +// Assert annotations are stored on the Node as Validations, which are then executed using the +// value of the annotated node as the parameter to the assertions. // -// When the assertions have violations, the errors are collected and returned in an AssertCheck. -// Otherwise, returns empty AssertCheck and nil. +// When a Node's value is invalid, the errors are collected and returned in an AssertCheck. +// Otherwise, returns empty AssertCheck and nil error. func ProcessAndRunValidations(n yamlmeta.Node, threadName string) (AssertCheck, error) { if n == nil { return AssertCheck{}, nil } + err := yamlmeta.Walk(n, &convertAssertAnnsToValidations{}) + if err != nil { + return AssertCheck{}, err + } - assertionChecker := newAssertChecker(threadName) - err := yamlmeta.Walk(n, assertionChecker) + validationRunner := newValidationRunner(threadName) + err = yamlmeta.Walk(n, validationRunner) if err != nil { return AssertCheck{}, err } - return assertionChecker.chk, nil + return validationRunner.chk, nil } // AssertCheck holds the resulting violations from executing Validations on a node. @@ -59,21 +63,14 @@ func (ac *AssertCheck) HasViolations() bool { return len(ac.Violations) > 0 } -type assertChecker struct { - thread *starlark.Thread - chk AssertCheck -} - -func newAssertChecker(threadName string) *assertChecker { - return &assertChecker{thread: &starlark.Thread{Name: threadName}, chk: AssertCheck{[]error{}}} -} +type convertAssertAnnsToValidations struct{} // Visit if `node` is annotated with `@assert/validate` (AnnotationAssertValidate). -// Checks, validates, and runs the validation Rules, any violations from running the assertions are collected. +// Checks annotation, and stores validation Rules on Node's validations meta. // -// This visitor returns and error if any assertion is not well-formed, +// This visitor returns and error if any assert annotation is not well-formed, // otherwise, returns nil. -func (a *assertChecker) Visit(node yamlmeta.Node) error { +func (a *convertAssertAnnsToValidations) Visit(node yamlmeta.Node) error { nodeAnnotations := template.NewAnnotations(node) if !nodeAnnotations.Has(AnnotationAssertValidate) { return nil @@ -86,12 +83,8 @@ func (a *assertChecker) Visit(node yamlmeta.Node) error { if syntaxErr != nil { return syntaxErr } - for _, rule := range rules { - err := rule.Validate(node, a.thread) - if err != nil { - a.chk.Violations = append(a.chk.Violations, err) - } - } + // store rules in node's validations meta without overriding any existing rules + AddValidations(node, rules) } return nil @@ -123,55 +116,38 @@ func newRulesFromAssertValidateAnnotation(annotation template.NodeAnnotation) ([ return nil, fmt.Errorf("Invalid @%s annotation - expected second item in the 2-tuple to be an assertion function, but was %s (at %s)", AnnotationAssertValidate, ruleTuple[1].Type(), annotation.Position.AsCompactString()) } rules = append(rules, Rule{ - msg: message.GoString(), - assertion: lambda, - position: annotation.Position, + Msg: message.GoString(), + Assertion: lambda, + Position: annotation.Position, }) } return rules, nil } -// A Rule represents an argument to an @assert/validate annotation; -// it contains a string description of what constitutes a valid value, -// and a function that asserts the rule against an actual value. -// One @assert/validate annotation can have multiple Rules. -type Rule struct { - msg string - assertion starlark.Callable - position *filepos.Position +type validationRunner struct { + thread *starlark.Thread + chk AssertCheck } -// Validate runs the assertion from the Rule with the node's value as arguments. -// -// Returns an error if the assertion returns False (not-None), or assert.fail()s. -// Otherwise, returns nil. -func (r Rule) Validate(node yamlmeta.Node, thread *starlark.Thread) error { - var key string - var nodeValue starlark.Value - switch typedNode := node.(type) { - case *yamlmeta.DocumentSet, *yamlmeta.Array, *yamlmeta.Map: - panic(fmt.Sprintf("@%s annotation at %s - not supported on %s at %s", AnnotationAssertValidate, r.position.AsCompactString(), yamlmeta.TypeName(node), node.GetPosition().AsCompactString())) - case *yamlmeta.MapItem: - key = fmt.Sprintf("%q", typedNode.Key) - nodeValue = yamltemplate.NewGoValueWithYAML(typedNode.Value).AsStarlarkValue() - case *yamlmeta.ArrayItem: - key = yamlmeta.TypeName(typedNode) - nodeValue = yamltemplate.NewGoValueWithYAML(typedNode.Value).AsStarlarkValue() - case *yamlmeta.Document: - key = yamlmeta.TypeName(typedNode) - nodeValue = yamltemplate.NewGoValueWithYAML(typedNode.Value).AsStarlarkValue() - } +func newValidationRunner(threadName string) *validationRunner { + return &validationRunner{thread: &starlark.Thread{Name: threadName}, chk: AssertCheck{[]error{}}} +} - result, err := starlark.Call(thread, r.assertion, starlark.Tuple{nodeValue}, []starlark.Tuple{}) - if err != nil { - return fmt.Errorf("%s (%s) requires %q; %s (by %s)", key, node.GetPosition().AsCompactString(), r.msg, err.Error(), r.position.AsCompactString()) +// Visit if `node` is has validations in its meta. +// Runs the validation Rules, any violations from running the assertions are collected. +// +// This visitor stores error(violations) in the validationRunner and returns nil. +func (a *validationRunner) Visit(node yamlmeta.Node) error { + // get rules in node's meta + rules := GetValidations(node) + if rules == nil { + return nil } - - // in order to pass, the assertion must return True or None - if _, ok := result.(starlark.NoneType); !ok { - if !result.Truth() { - return fmt.Errorf("%s (%s) requires %q (by %s)", key, node.GetPosition().AsCompactString(), r.msg, r.position.AsCompactString()) + for _, rule := range rules { + err := rule.Validate(node, a.thread) + if err != nil { + a.chk.Violations = append(a.chk.Violations, err) } } diff --git a/pkg/assertions/validate.go b/pkg/assertions/validate.go new file mode 100644 index 00000000..5b6f730a --- /dev/null +++ b/pkg/assertions/validate.go @@ -0,0 +1,58 @@ +// Copyright 2022 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 + +package assertions + +import ( + "fmt" + + "github.com/k14s/starlark-go/starlark" + "github.com/vmware-tanzu/carvel-ytt/pkg/filepos" + "github.com/vmware-tanzu/carvel-ytt/pkg/yamlmeta" + "github.com/vmware-tanzu/carvel-ytt/pkg/yamltemplate" +) + +// A Rule represents a validation attached to a Node via an annotation; +// it contains a string description of what constitutes a valid value, +// and a function that asserts the rule against an actual value. +type Rule struct { + Msg string + Assertion starlark.Callable + Position *filepos.Position +} + +// Validate runs the assertion from the Rule with the node's value as arguments. +// +// Returns an error if the assertion returns False (not-None), or assert.fail()s. +// Otherwise, returns nil. +func (r Rule) Validate(node yamlmeta.Node, thread *starlark.Thread) error { + var key string + var nodeValue starlark.Value + switch typedNode := node.(type) { + case *yamlmeta.DocumentSet, *yamlmeta.Array, *yamlmeta.Map: + panic(fmt.Sprintf("validation at %s - not supported on %s at %s", r.Position.AsCompactString(), yamlmeta.TypeName(node), node.GetPosition().AsCompactString())) + case *yamlmeta.MapItem: + key = fmt.Sprintf("%q", typedNode.Key) + nodeValue = yamltemplate.NewGoValueWithYAML(typedNode.Value).AsStarlarkValue() + case *yamlmeta.ArrayItem: + key = yamlmeta.TypeName(typedNode) + nodeValue = yamltemplate.NewGoValueWithYAML(typedNode.Value).AsStarlarkValue() + case *yamlmeta.Document: + key = yamlmeta.TypeName(typedNode) + nodeValue = yamltemplate.NewGoValueWithYAML(typedNode.Value).AsStarlarkValue() + } + + result, err := starlark.Call(thread, r.Assertion, starlark.Tuple{nodeValue}, []starlark.Tuple{}) + if err != nil { + return fmt.Errorf("%s (%s) requires %q; %s (by %s)", key, node.GetPosition().AsCompactString(), r.Msg, err.Error(), r.Position.AsCompactString()) + } + + // in order to pass, the assertion must return True or None + if _, ok := result.(starlark.NoneType); !ok { + if !result.Truth() { + return fmt.Errorf("%s (%s) requires %q (by %s)", key, node.GetPosition().AsCompactString(), r.Msg, r.Position.AsCompactString()) + } + } + + return nil +} diff --git a/pkg/assertions/yamlmeta.go b/pkg/assertions/yamlmeta.go new file mode 100644 index 00000000..41d248da --- /dev/null +++ b/pkg/assertions/yamlmeta.go @@ -0,0 +1,29 @@ +// Copyright 2022 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 + +package assertions + +import "github.com/vmware-tanzu/carvel-ytt/pkg/yamlmeta" + +// 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 { + 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) +} + +// GetValidations retrieves validation Rules from node metadata, set previously via SetValidations(). +func GetValidations(node yamlmeta.Node) []Rule { + metas := node.GetMeta("validations") + if rules, ok := metas.([]Rule); ok { + return rules + } + return nil +} diff --git a/pkg/workspace/library_execution.go b/pkg/workspace/library_execution.go index 32c04590..052e1f65 100644 --- a/pkg/workspace/library_execution.go +++ b/pkg/workspace/library_execution.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/k14s/starlark-go/starlark" + "github.com/vmware-tanzu/carvel-ytt/pkg/assertions" "github.com/vmware-tanzu/carvel-ytt/pkg/cmd/ui" "github.com/vmware-tanzu/carvel-ytt/pkg/files" "github.com/vmware-tanzu/carvel-ytt/pkg/template" @@ -100,7 +101,7 @@ func (ll *LibraryExecution) Values(valuesOverlays []*datavalues.Envelope, schema // Returns an error if the arguments to an @assert/validate are invalid, // otherwise, checks the AssertCheck for violations, and returns nil if there are no violations. func (ll *LibraryExecution) validateValues(values *datavalues.Envelope) error { - assertCheck, err := ProcessAndRunValidations(values.Doc, "assert-data-values") + assertCheck, err := assertions.ProcessAndRunValidations(values.Doc, "assert-data-values") if err != nil { return err }