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

store assert/vaildate rules in Node's metas #654

Merged
merged 3 commits into from
Apr 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
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