Skip to content

Commit

Permalink
chore: pre-build environment and program for expressions
Browse files Browse the repository at this point in the history
Signed-off-by: Kumar Mallikarjuna <[email protected]>
  • Loading branch information
kumar-mallikarjuna committed Dec 3, 2024
1 parent 5dcbef9 commit a7e5287
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 46 deletions.
15 changes: 15 additions & 0 deletions pkg/apis/testharness/v1beta1/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"strings"

"github.com/google/cel-go/cel"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
Expand Down Expand Up @@ -57,3 +58,17 @@ func (t *TestResourceRef) String() string {
t.Ref,
)
}

func (t *Assertion) BuildProgram(env *cel.Env) (cel.Program, error) {
ast, issues := env.Compile(t.CELExpression)
if issues != nil && issues.Err() != nil {
return nil, fmt.Errorf("type-check error: %s", issues.Err())
}

prg, err := env.Program(ast)
if err != nil {
return nil, fmt.Errorf("program construction error: %w", err)
}

return prg, nil
}
8 changes: 3 additions & 5 deletions pkg/apis/testharness/v1beta1/test_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import (
const KubeconfigLoadingEager = "Eager"
const KubeconfigLoadingLazy = "Lazy"

type CELExpression string

// Create embedded struct to implement custom DeepCopyInto method
type RestConfig struct {
RC *rest.Config
Expand Down Expand Up @@ -157,8 +155,8 @@ type TestAssert struct {

ResourceRefs []TestResourceRef `json:"resourceRefs,omitempty"`

AssertAny []Assertion `json:"assertAny,omitempty"`
AssertAll []Assertion `json:"assertAll,omitempty"`
AssertAny []*Assertion `json:"assertAny,omitempty"`
AssertAll []*Assertion `json:"assertAll,omitempty"`
}

// TestAssertCommand an assertion based on the result of the execution of a command
Expand Down Expand Up @@ -238,7 +236,7 @@ type TestResourceRef struct {
}

type Assertion struct {
CELExpression CELExpression `json:"celExpr,omitempty"`
CELExpression string `json:"celExpr,omitempty"`
}

// DefaultKINDContext defines the default kind context to use.
Expand Down
20 changes: 16 additions & 4 deletions pkg/apis/testharness/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 33 additions & 2 deletions pkg/test/step.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"testing"
"time"

"github.com/google/cel-go/cel"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -45,6 +46,8 @@ type Step struct {
Step *harness.TestStep
Assert *harness.TestAssert

Programs map[string]cel.Program

Asserts []client.Object
Apply []client.Object
Errors []client.Object
Expand Down Expand Up @@ -416,9 +419,14 @@ func (s *Step) CheckAssertExpressions(
ctx context.Context,
resourceRefs []harness.TestResourceRef,
assertAny,
assertAll []harness.Assertion,
assertAll []*harness.Assertion,
) []error {
return testutils.RunAssertExpressions(ctx, s.Logger, resourceRefs, assertAny, assertAll, s.Kubeconfig)
client, err := s.Client(false)
if err != nil {
return []error{err}
}

return testutils.RunAssertExpressions(ctx, s.Logger, client, s.Programs, resourceRefs, assertAny, assertAll)
}

// Check checks if the resources defined in Asserts and Errors are in the correct state.
Expand Down Expand Up @@ -525,6 +533,8 @@ func (s *Step) String() string {
// if seen, mark a test immediately failed.
// - All other YAML files are considered resources to create.
func (s *Step) LoadYAML(file string) error {
s.Programs = make(map[string]cel.Program)

skipFile, objects, err := s.loadOrSkipFile(file)
if skipFile || err != nil {
return err
Expand Down Expand Up @@ -554,6 +564,27 @@ func (s *Step) LoadYAML(file string) error {
if len(errs) > 0 {
return fmt.Errorf("failed to load TestAssert object from %s: %w", file, errors.Join(errs...))
}

var assertions []*harness.Assertion
assertions = append(assertions, s.Assert.AssertAny...)
assertions = append(assertions, s.Assert.AssertAll...)

env, err := testutils.BuildEnv(s.Assert.ResourceRefs)
if err != nil {
return fmt.Errorf("failed to load TestAssert object from %s: %w", file, err)
}

for _, assertion := range assertions {
if prg, err := assertion.BuildProgram(env); err != nil {
errs = append(errs, err)
} else {
s.Programs[assertion.CELExpression] = prg
}
}

if len(errs) > 0 {
return fmt.Errorf("failed to load TestAssert object from %s: %w", file, errors.Join(errs...))
}
} else {
asserts = append(asserts, obj)
}
Expand Down
25 changes: 25 additions & 0 deletions pkg/test/utils/expression.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package utils

import (
"fmt"

"github.com/google/cel-go/cel"

"github.com/kudobuilder/kuttl/pkg/apis/testharness/v1beta1"
)

func BuildEnv(resourceRefs []v1beta1.TestResourceRef) (*cel.Env, error) {
env, err := cel.NewEnv()
if err != nil {
return nil, fmt.Errorf("failed to create environment: %w", err)
}

for _, resourceRef := range resourceRefs {
env, err = env.Extend(cel.Variable(resourceRef.Ref, cel.DynType))
if err != nil {
return nil, fmt.Errorf("failed to add resource parameter '%v' to environment: %w", resourceRef.Ref, err)
}
}

return env, nil
}
63 changes: 29 additions & 34 deletions pkg/test/utils/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -1114,7 +1114,7 @@ func RunCommand(ctx context.Context, namespace string, cmd harness.Command, cwd

kuttlENV := make(map[string]string)
kuttlENV["NAMESPACE"] = namespace
kuttlENV["KUBECONFIG"] = kubeconfigPath(actualDir, kubeconfigOverride)
kuttlENV["KUBECONFIG"] = KubeconfigPath(actualDir, kubeconfigOverride)
kuttlENV["PATH"] = fmt.Sprintf("%s/bin/:%s", actualDir, os.Getenv("PATH"))

// by default testsuite timeout is the command timeout
Expand Down Expand Up @@ -1183,7 +1183,7 @@ func RunCommand(ctx context.Context, namespace string, cmd harness.Command, cwd
return nil, nil
}

func kubeconfigPath(actualDir, override string) string {
func KubeconfigPath(actualDir, override string) string {
if override != "" {
if filepath.IsAbs(override) {
return override
Expand Down Expand Up @@ -1223,27 +1223,17 @@ func RunAssertCommands(ctx context.Context, logger Logger, namespace string, com
func RunAssertExpressions(
ctx context.Context,
logger Logger,
cl client.Client,
programs map[string]cel.Program,
resourceRefs []harness.TestResourceRef,
assertAny,
assertAll []harness.Assertion,
kubeconfigOverride string,
assertAll []*harness.Assertion,
) []error {
errs := []error{}
if len(assertAny) == 0 && len(assertAll) == 0 {
return errs
}

actualDir, err := os.Getwd()
if err != nil {
return []error{fmt.Errorf("failed to get current working director: %w", err)}
}

kubeconfig := kubeconfigPath(actualDir, kubeconfigOverride)
cl, err := NewClient(kubeconfig, "")(false)
if err != nil {
return []error{fmt.Errorf("failed to construct client: %w", err)}
}

variables := make(map[string]interface{})
for _, resourceRef := range resourceRefs {
namespacedName, referencedResource := resourceRef.BuildResourceReference()
Expand All @@ -1258,40 +1248,45 @@ func RunAssertExpressions(
variables[resourceRef.Ref] = referencedResource.Object
}

env, err := cel.NewEnv()
if err != nil {
return []error{fmt.Errorf("failed to create environment: %w", err)}
}

for k := range variables {
env, err = env.Extend(cel.Variable(k, cel.DynType))
var anyExpressionsEvaluation, allExpressionsEvaluation []error
for _, expr := range assertAny {
prg, ok := programs[expr.CELExpression]
if !ok {
return []error{fmt.Errorf("couldn't find pre-built program for expression: %v", expr.CELExpression)}
}
out, _, err := prg.Eval(variables)
if err != nil {
return []error{fmt.Errorf("failed to add resource parameter '%v' to environment: %w", k, err)}
return []error{fmt.Errorf("failed to evaluate program: %w", err)}
}
}

for _, expr := range assertAny {
ast, issues := env.Compile(string(expr.CELExpression))
if issues != nil && issues.Err() != nil {
return []error{fmt.Errorf("type-check error: %s", issues.Err())}
if out.Value() != true {
anyExpressionsEvaluation = append(anyExpressionsEvaluation, fmt.Errorf("expression '%v' evaluated to '%v'", expr.CELExpression, out.Value()))
}
}

prg, err := env.Program(ast)
if err != nil {
return []error{fmt.Errorf("program construction error: %w", err)}
for _, expr := range assertAll {
prg, ok := programs[expr.CELExpression]
if !ok {
return []error{fmt.Errorf("couldn't find pre-built program for expression: %v", expr.CELExpression)}
}

out, _, err := prg.Eval(variables)
if err != nil {
return []error{fmt.Errorf("failed to evaluate program: %w", err)}
}

logger.Logf("expression '%v' evaluated to '%v'", expr, out.Value())
if out.Value() != true {
errs = append(errs, fmt.Errorf("failed validation, expression '%v' evaluated to '%v'", expr, out.Value()))
allExpressionsEvaluation = append(allExpressionsEvaluation, fmt.Errorf("expression '%v' evaluated to '%v'", expr.CELExpression, out.Value()))
}
}

if len(assertAny) != 0 && len(anyExpressionsEvaluation) == len(assertAny) {
errs = append(errs, fmt.Errorf("no expression evaluated to true: %w", errors.Join(anyExpressionsEvaluation...)))
}

if len(allExpressionsEvaluation) != len(assertAll) {
errs = append(errs, fmt.Errorf("not all expressions evaluated to true: %w", errors.Join(allExpressionsEvaluation...)))
}

return errs
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/test/utils/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func TestKubeconfigPath(t *testing.T) {
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
result := kubeconfigPath(tt.path, tt.override)
result := KubeconfigPath(tt.path, tt.override)
assert.Equal(t, tt.expected, result)
})
}
Expand Down

0 comments on commit a7e5287

Please sign in to comment.