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

[KEP-0009] feat: add expression based assertions #576

Open
wants to merge 31 commits into
base: main
Choose a base branch
from

Conversation

kumar-mallikarjuna
Copy link
Contributor

What this PR does / why we need it:
This PR adds CEL-expression based assertions to TestAsserts. See https://github.com/kudobuilder/kuttl/blob/main/keps/0009-expression-based-assertions.md for more details.

Fixes #562

Copy link
Member

@porridge porridge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, but needs a few changes.

pkg/apis/testharness/v1beta1/test_types.go Outdated Show resolved Hide resolved
pkg/apis/testharness/v1beta1/test_types.go Outdated Show resolved Hide resolved
pkg/test/utils/kubernetes.go Outdated Show resolved Hide resolved
pkg/test/utils/kubernetes.go Outdated Show resolved Hide resolved
pkg/test/utils/kubernetes.go Outdated Show resolved Hide resolved
variables[resourceRef.Id] = referencedResource.Object
}

env, err := cel.NewEnv()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling cel.NewEnv() may not be enough, you probably want to enable a couple of options/libs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eddycharly any hints on what might be useful? 🤔

@kumar-mallikarjuna kumar-mallikarjuna force-pushed the KEP-0009 branch 2 times, most recently from 0fe748e to a85752a Compare December 3, 2024 07:56
pkg/apis/testharness/v1beta1/expression.go Outdated Show resolved Hide resolved
pkg/apis/testharness/v1beta1/expression.go Outdated Show resolved Hide resolved
pkg/apis/testharness/v1beta1/expression.go Outdated Show resolved Hide resolved
pkg/test/step.go Outdated Show resolved Hide resolved
pkg/test/utils/kubernetes.go Outdated Show resolved Hide resolved
pkg/test/utils/kubernetes.go Outdated Show resolved Hide resolved
pkg/test/utils/expression.go Outdated Show resolved Hide resolved
pkg/test/utils/kubernetes.go Outdated Show resolved Hide resolved
pkg/test/utils/kubernetes.go Outdated Show resolved Hide resolved
@kumar-mallikarjuna
Copy link
Contributor Author

Thanks for the quick review @porridge . I'll add the tests tomorrow.

Copy link
Member

@porridge porridge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this looks much better, but some more changes are needed. 🙏🏻
Please see inline.

pkg/test/step.go Outdated Show resolved Hide resolved
pkg/test/expression_integration_test.go Show resolved Hide resolved
pkg/expressions/cel.go Outdated Show resolved Hide resolved
pkg/expressions/cel.go Outdated Show resolved Hide resolved
pkg/expressions/cel.go Outdated Show resolved Hide resolved
pkg/test/step.go Outdated Show resolved Hide resolved
pkg/apis/testharness/v1beta1/expression.go Outdated Show resolved Hide resolved
pkg/expressions/cel.go Outdated Show resolved Hide resolved
pkg/expressions/cel.go Show resolved Hide resolved
pkg/test/expression_integration_test.go Outdated Show resolved Hide resolved
Copy link
Member

@porridge porridge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost there!
Just a bunch of nitpicks for messages and identifier names, and one issue in the test..

pkg/expressions/cel.go Outdated Show resolved Hide resolved
pkg/expressions/cel.go Outdated Show resolved Hide resolved
pkg/expressions/cel.go Outdated Show resolved Hide resolved
pkg/expressions/cel.go Outdated Show resolved Hide resolved
pkg/expressions/cel.go Outdated Show resolved Hide resolved
pkg/test/expression_integration_test.go Outdated Show resolved Hide resolved
pkg/test/expression_integration_test.go Outdated Show resolved Hide resolved
variables[resourceRef.Id] = referencedResource.Object
}

env, err := cel.NewEnv()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eddycharly any hints on what might be useful? 🤔

Copy link
Member

@porridge porridge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more things.

pkg/test/expression_integration_test.go Show resolved Hide resolved
pkg/expressions/cel.go Outdated Show resolved Hide resolved
pkg/expressions/cel.go Outdated Show resolved Hide resolved
pkg/expressions/cel.go Outdated Show resolved Hide resolved
pkg/test/expression_integration_test.go Outdated Show resolved Hide resolved
pkg/test/expression_integration_test.go Outdated Show resolved Hide resolved
Signed-off-by: Kumar Mallikarjuna <[email protected]>
Signed-off-by: Kumar Mallikarjuna <[email protected]>
Signed-off-by: Kumar Mallikarjuna <[email protected]>
Signed-off-by: Kumar Mallikarjuna <[email protected]>
Signed-off-by: Kumar Mallikarjuna <[email protected]>
Signed-off-by: Kumar Mallikarjuna <[email protected]>
Signed-off-by: Kumar Mallikarjuna <[email protected]>
Signed-off-by: Kumar Mallikarjuna <[email protected]>
Signed-off-by: Kumar Mallikarjuna <[email protected]>
Copy link
Member

@porridge porridge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM now, just the debugging leftovers need to go :-)

pkg/test/step.go Outdated Show resolved Hide resolved
pkg/test/expression_integration_test.go Outdated Show resolved Hide resolved
@eddycharly
Copy link
Contributor

eddycharly commented Jan 8, 2025

Shouldn't you verify that the program return type is a boolean ?

Something like:

...
if !ast.OutputType().IsExactType(types.BoolType) ...
...

return nil, fmt.Errorf("failed to load resource reference(s): %w", errors.Join(errs...))
}

env, err := cel.NewEnv()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you extending the env somewhere ? Looks like you will want to register a bunch of CEL libs to enable features.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any suggestions on what libs are a must-have @eddycharly ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like this is a good start IMHO. it enables important language features, register comme libs and kube specific ones.

	return cel.NewEnv(
		// configure env
		cel.HomogeneousAggregateLiterals(),
		cel.EagerlyValidateDeclarations(true),
		cel.DefaultUTCTimeZone(true),
		cel.CrossTypeNumericComparisons(true),
		// register common libs
		cel.OptionalTypes(),
		ext.Bindings(),
		ext.Encoders(),
		ext.Lists(),
		ext.Math(),
		ext.Protos(),
		ext.Sets(),
		ext.Strings(),
		// register kubernetes libs
		library.CIDR(),
		library.Format(),
		library.IP(),
		library.Lists(),
		library.Regex(),
		library.URLs(),
	)

}

for _, resourceRef := range resourceRefs {
env, err = env.Extend(cel.Variable(resourceRef.Ref, cel.DynType))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to use a type provider instead of DynType.

}

func evaluateExpression(expr string,
programs map[string]cel.Program,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why create a map to lookup the program corresponding to an expression ? 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eddycharly programs are compiled early at YAML loading time, so here we just access them. We use a map in order to mention the problematic expression in the error message in case it fails.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get it, if programs are compiled at load time you shouldn't even reach evaluateExpression right ?
I didn't look at it deeply but i would expect the focus is on the expression path, not the expression itself.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An expression can compile OK (and its good to do it ASAP to catch syntax errors, etc), but evaluation can fail later (depending on resources in the cluster). An error message about the evaluation should mention the expression itself, so the user knows which expression was the problematic one.

@@ -157,6 +157,11 @@ type TestAssert struct {
Collectors []*TestCollector `json:"collectors,omitempty"`
// Commands is a set of commands to be run as assertions for the current step
Commands []TestAssertCommand `json:"commands,omitempty"`

ResourceRefs []TestResourceRef `json:"resourceRefs,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should consider two TestResourceRef with the same id an error.

Comment on lines +65 to +86
var anyExprErrors, allExprErrors []error
for _, expr := range assertAny {
if err := evaluateExpression(expr.CELExpression, programs, variables); err != nil {
anyExprErrors = append(anyExprErrors, err)
}
}

for _, expr := range assertAll {
if err := evaluateExpression(expr.CELExpression, programs, variables); err != nil {
allExprErrors = append(allExprErrors, err)
}
}

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

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

return errs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would make sense to return early here instead of evaluating all expressions before making a decision.

Comment on lines +425 to +436
variables := make(map[string]interface{})
for _, resourceRef := range s.Assert.ResourceRefs {
if resourceRef.Namespace == "" {
resourceRef.Namespace = namespace
}
namespacedName, referencedResource := resourceRef.BuildResourceReference()
if err := client.Get(context.TODO(), namespacedName, referencedResource); err != nil {
return []error{fmt.Errorf("failed to get referenced resource '%v': %w", namespacedName, err)}
}

variables[resourceRef.Ref] = referencedResource.Object
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be lazy loaded.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eddycharly you mean as a performance improvement? Or do you see a correctness issue?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somehow both. If the first condition is met in any why bother evaluating other conditions ?
I can imagine cases where the first condition returns true but the second condition throws an error. Returning early makes the evaluation succeed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(this may or may not be what you want though)

Copy link
Member

@porridge porridge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay in reviewing.

I think @eddycharly made a bunch of great suggestions. I know too little about CEL to judge with certainty, but I don't think any of these are really blocking. Perhaps we can do them as followup improvements, as the code looks OK in general, and this review is already taking waaaay longer than I expected. And it would feel good to get something usable merged already even if it's not perfect from the start. Unless you feel like addressing at least some of them straight away.

However one thing I completely forgot to mention earlier is documentation. Please at least update the TestAssert schema description and throw in a really short one-pager about this feature somewhere under docs/testing (probably a new file and link to it where appropriate). We can iterate on improving it later.

@kumar-mallikarjuna
Copy link
Contributor Author

I think @eddycharly made a bunch of great suggestions. I know too little about CEL to judge with certainty, but I don't think any of these are really blocking. Perhaps we can do them as followup improvements, as the code looks OK in general, and this review is already taking waaaay longer than I expected. And it would feel good to get something usable merged already even if it's not perfect from the start. Unless you feel like addressing at least some of them straight away.

Thanks for the suggestions, @eddycharly ! I'd vote to merge this as well and include improvements in a separate PR.

However one thing I completely forgot to mention earlier is documentation. Please at least update the TestAssert schema description and throw in a really short one-pager about this feature somewhere under docs/testing (probably a new file and link to it where appropriate). We can iterate on improving it later.

Let me add this tomorrow during my day.

Thanks again @porridge , @eddycharly for the thorough review! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add CEL expression support
3 participants