Skip to content

Commit

Permalink
Merge pull request #659 from vmware-tanzu/assert-with-metas-preplay
Browse files Browse the repository at this point in the history
  • Loading branch information
pivotaljohn authored Apr 29, 2022
2 parents b11b9ea + 67456b7 commit a79548f
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 67 deletions.
108 changes: 42 additions & 66 deletions pkg/workspace/assert.go → pkg/assertions/assert.go
Original file line number Diff line number Diff line change
@@ -1,39 +1,43 @@
// 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
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.
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
}

Expand Down
58 changes: 58 additions & 0 deletions pkg/assertions/validate.go
Original file line number Diff line number Diff line change
@@ -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
}
29 changes: 29 additions & 0 deletions pkg/assertions/yamlmeta.go
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 2 additions & 1 deletion pkg/workspace/library_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down

0 comments on commit a79548f

Please sign in to comment.