From b343b572a5c2d4f2f60d3de1af63b46e822583d2 Mon Sep 17 00:00:00 2001 From: Garrett Cheadle Date: Mon, 25 Apr 2022 15:27:07 -0700 Subject: [PATCH] create assertion top level package --- pkg/{workspace => assertions}/assert.go | 73 +++++++++++------------- pkg/{yamlmeta => assertions}/validate.go | 30 +++++----- pkg/assertions/yamlmeta.go | 23 ++++++++ pkg/workspace/library_execution.go | 3 +- pkg/yamlmeta/convert.go | 6 -- 5 files changed, 72 insertions(+), 63 deletions(-) rename pkg/{workspace => assertions}/assert.go (71%) rename pkg/{yamlmeta => assertions}/validate.go (57%) create mode 100644 pkg/assertions/yamlmeta.go diff --git a/pkg/workspace/assert.go b/pkg/assertions/assert.go similarity index 71% rename from pkg/workspace/assert.go rename to pkg/assertions/assert.go index 2830be98..213eb729 100644 --- a/pkg/workspace/assert.go +++ b/pkg/assertions/assert.go @@ -1,11 +1,10 @@ // 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/template" "github.com/vmware-tanzu/carvel-ytt/pkg/yamlmeta" @@ -16,28 +15,28 @@ const ( AnnotationAssertValidate template.AnnotationName = "assert/validate" ) -// TODO: update go docs -// 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, &assertGetter{}) + err := yamlmeta.Walk(n, &convertAssertAnnsToValidations{}) if err != nil { return AssertCheck{}, err } - assertionRunner := newAssertRunner(threadName) - err = yamlmeta.Walk(n, assertionRunner) + validationRunner := newValidationRunner(threadName) + err = yamlmeta.Walk(n, validationRunner) if err != nil { return AssertCheck{}, err } - return assertionRunner.chk, nil + return validationRunner.chk, nil } // AssertCheck holds the resulting violations from executing Validations on a node. @@ -63,10 +62,14 @@ func (ac *AssertCheck) HasViolations() bool { return len(ac.Violations) > 0 } -type assertGetter struct {} +type convertAssertAnnsToValidations struct{} -// TODO: update go docs -func (a *assertGetter) Visit(node yamlmeta.Node) error { +// Visit if `node` is annotated with `@assert/validate` (AnnotationAssertValidate). +// Checks annotation, and stores validation Rules on Node's validations meta. +// +// This visitor returns and error if any assert annotation is not well-formed, +// otherwise, returns nil. +func (a *convertAssertAnnsToValidations) Visit(node yamlmeta.Node) error { nodeAnnotations := template.NewAnnotations(node) if !nodeAnnotations.Has(AnnotationAssertValidate) { return nil @@ -79,16 +82,15 @@ func (a *assertGetter) Visit(node yamlmeta.Node) error { if syntaxErr != nil { return syntaxErr } - //store rules in node's meta - // TODO: not overriding any metas previously set - node.SetMeta("validations", rules) + // store rules in node's validations meta without overriding any existing rules + AddValidations(node, rules) } return nil } -func newRulesFromAssertValidateAnnotation(annotation template.NodeAnnotation) ([]yamlmeta.Rule, error) { - var rules []yamlmeta.Rule +func newRulesFromAssertValidateAnnotation(annotation template.NodeAnnotation) ([]Rule, error) { + var rules []Rule if len(annotation.Kwargs) != 0 { return nil, fmt.Errorf("Invalid @%s annotation - expected @%s to have 2-tuple as argument(s), but found keyword argument (by %s)", AnnotationAssertValidate, AnnotationAssertValidate, annotation.Position.AsCompactString()) @@ -112,7 +114,7 @@ func newRulesFromAssertValidateAnnotation(annotation template.NodeAnnotation) ([ if !ok { 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, yamlmeta.Rule{ + rules = append(rules, Rule{ Msg: message.GoString(), Assertion: lambda, Position: annotation.Position, @@ -122,35 +124,25 @@ func newRulesFromAssertValidateAnnotation(annotation template.NodeAnnotation) ([ return rules, nil } - -type assertRunner struct { +type validationRunner struct { thread *starlark.Thread chk AssertCheck } -func newAssertRunner(threadName string) *assertRunner { - return &assertRunner{thread: &starlark.Thread{Name: threadName}, chk: AssertCheck{[]error{}}} +func newValidationRunner(threadName string) *validationRunner { + return &validationRunner{thread: &starlark.Thread{Name: threadName}, chk: AssertCheck{[]error{}}} } -// TODO: update go docs -// Visit if `node` is annotated with `@assert/validate` (AnnotationAssertValidate). -// Checks, validates, and runs the validation Rules, any violations from running the assertions are collected. +// Visit if `node` is has validations in its meta. +// Runs the validation Rules, any violations from running the assertions are collected. // -// This visitor returns and error if any assertion is not well-formed, -// otherwise, returns nil. -func (a *assertRunner) Visit(node yamlmeta.Node) error { - - //get rules in node's meta - metas := node.GetMeta("validations") - if metas == nil { - return nil - } - - rules, ok := metas.([]yamlmeta.Rule) - if !ok { +// 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 } - for _, rule := range rules { err := rule.Validate(node, a.thread) if err != nil { @@ -160,4 +152,3 @@ func (a *assertRunner) Visit(node yamlmeta.Node) error { return nil } - diff --git a/pkg/yamlmeta/validate.go b/pkg/assertions/validate.go similarity index 57% rename from pkg/yamlmeta/validate.go rename to pkg/assertions/validate.go index 483a2d2d..fa707896 100644 --- a/pkg/yamlmeta/validate.go +++ b/pkg/assertions/validate.go @@ -1,16 +1,17 @@ -package yamlmeta +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 an argument to an @assert/validate annotation; +// 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. -// One @assert/validate annotation can have multiple Rules. type Rule struct { Msg string Assertion starlark.Callable @@ -21,22 +22,21 @@ type Rule struct { // // Returns an error if the assertion returns False (not-None), or assert.fail()s. // Otherwise, returns nil. -func (r Rule) Validate(node Node, thread *starlark.Thread) error { +func (r Rule) Validate(node yamlmeta.Node, thread *starlark.Thread) error { var key string var nodeValue starlark.Value switch typedNode := node.(type) { - case *DocumentSet, *Array, *Map: - // TODO: is this the correct place for as specific of an error - panic(fmt.Sprintf("@%s annotation at %s - not supported on %s at %s", "assert/validate", r.Position.AsCompactString(), TypeName(node), node.GetPosition().AsCompactString())) - case *MapItem: + 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 = NewGoValueFromAST(typedNode.Value).AsStarlarkValue() - case *ArrayItem: - key = TypeName(typedNode) - nodeValue = NewGoValueFromAST(typedNode.Value).AsStarlarkValue() - case *Document: - key = TypeName(typedNode) - nodeValue = NewGoValueFromAST(typedNode.Value).AsStarlarkValue() + 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{}) diff --git a/pkg/assertions/yamlmeta.go b/pkg/assertions/yamlmeta.go new file mode 100644 index 00000000..542f88ab --- /dev/null +++ b/pkg/assertions/yamlmeta.go @@ -0,0 +1,23 @@ +package assertions + +import "github.com/vmware-tanzu/carvel-ytt/pkg/yamlmeta" + +func AddValidations(node yamlmeta.Node, rules []Rule) { + metas := node.GetMeta("validations") + if currRules, ok := metas.([]Rule); ok { + rules = append(currRules, rules...) + } + SetValidations(node, rules) +} + +func SetValidations(node yamlmeta.Node, rules []Rule) { + node.SetMeta("validations", rules) +} + +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 } diff --git a/pkg/yamlmeta/convert.go b/pkg/yamlmeta/convert.go index df341c79..73399b59 100644 --- a/pkg/yamlmeta/convert.go +++ b/pkg/yamlmeta/convert.go @@ -8,7 +8,6 @@ import ( "github.com/vmware-tanzu/carvel-ytt/pkg/filepos" "github.com/vmware-tanzu/carvel-ytt/pkg/orderedmap" - tplcore "github.com/vmware-tanzu/carvel-ytt/pkg/template/core" "github.com/vmware-tanzu/carvel-ytt/pkg/yamlmeta/internal/yaml.v2" ) @@ -28,11 +27,6 @@ func NewGoFromAST(val interface{}) interface{} { return convertToGo(val) } -//TODO: how else can we provide the RIGHT type of value -func NewGoValueFromAST(val interface{}) tplcore.GoValue { - return tplcore.NewGoValue(convertToGo(val)) -} - func convertToLowYAML(val interface{}) interface{} { switch typedVal := val.(type) { case map[interface{}]interface{}: