From d1a161e624791237c3bda7e30ea6bf565c64f504 Mon Sep 17 00:00:00 2001 From: Kevin McDermott Date: Wed, 13 Nov 2024 17:41:59 +0000 Subject: [PATCH] More tests. This adds more testing for the CEL evaluation mechanism for resource filtering. Signed-off-by: Kevin McDermott --- internal/server/cel.go | 36 ++++------ internal/server/cel_test.go | 131 ++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 24 deletions(-) create mode 100644 internal/server/cel_test.go diff --git a/internal/server/cel.go b/internal/server/cel.go index 37b836a20..28a680f41 100644 --- a/internal/server/cel.go +++ b/internal/server/cel.go @@ -25,7 +25,6 @@ import ( "strings" "github.com/google/cel-go/cel" - "github.com/google/cel-go/checker/decls" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/common/types/traits" @@ -54,6 +53,9 @@ func newCELProgram(expr string) (cel.Program, error) { if issues != nil && issues.Err() != nil { return nil, fmt.Errorf("expression %v check failed: %w", expr, issues.Err()) } + if checked.OutputType() != types.BoolType { + return nil, fmt.Errorf("invalid expression output type %v", checked.OutputType()) + } prg, err := env.Program(checked, cel.EvalOptions(cel.OptOptimize), cel.InterruptCheckFrequency(100)) if err != nil { @@ -94,26 +96,19 @@ func newCELEvaluator(expr string, req *http.Request) (resourcePredicate, error) return nil, fmt.Errorf("expression %v failed to evaluate: %w", expr, err) } - v, ok := out.(types.Bool) - if !ok { - return nil, fmt.Errorf("expression %q did not return a boolean value", expr) - } - - result := v.Value().(bool) + result := out.Value().(bool) return &result, nil }, nil } func makeCELEnv() (*cel.Env, error) { - mapStrDyn := decls.NewMapType(decls.String, decls.Dyn) return cel.NewEnv( celext.Strings(), notifications(), - cel.Declarations( - decls.NewVar("resource", mapStrDyn), - decls.NewVar("request", mapStrDyn), - )) + cel.Variable("resource", cel.ObjectType("google.protobuf.Struct")), + cel.Variable("request", cel.ObjectType("google.protobuf.Struct")), + ) } func isJSONContent(r *http.Request) bool { @@ -132,17 +127,10 @@ func isJSONContent(r *http.Request) bool { } func notifications() cel.EnvOption { - r, err := types.NewRegistry() - if err != nil { - panic(err) // TODO: Do something better? - } - - return cel.Lib(¬ificationsLib{registry: r}) + return cel.Lib(¬ificationsLib{}) } -type notificationsLib struct { - registry *types.Registry -} +type notificationsLib struct{} // LibraryName implements the SingletonLibrary interface method. func (*notificationsLib) LibraryName() string { @@ -151,13 +139,13 @@ func (*notificationsLib) LibraryName() string { // CompileOptions implements the Library interface method. func (l *notificationsLib) CompileOptions() []cel.EnvOption { - listStrDyn := cel.ListType(cel.DynType) + listDyn := cel.ListType(cel.DynType) opts := []cel.EnvOption{ cel.Function("first", - cel.MemberOverload("first_list", []*cel.Type{listStrDyn}, cel.DynType, + cel.MemberOverload("first_list", []*cel.Type{listDyn}, cel.DynType, cel.UnaryBinding(listFirst))), cel.Function("last", - cel.MemberOverload("last_list", []*cel.Type{listStrDyn}, cel.DynType, + cel.MemberOverload("last_list", []*cel.Type{listDyn}, cel.DynType, cel.UnaryBinding(listLast))), } diff --git a/internal/server/cel_test.go b/internal/server/cel_test.go new file mode 100644 index 000000000..d230f30e0 --- /dev/null +++ b/internal/server/cel_test.go @@ -0,0 +1,131 @@ +package server + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "testing" + + apiv1 "github.com/fluxcd/notification-controller/api/v1" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestValidateCELExpressionValidExpressions(t *testing.T) { + validationTests := []string{ + "true", + "false", + "request.body.value == 'test'", + } + + for _, tt := range validationTests { + t.Run(tt, func(t *testing.T) { + g := NewWithT(t) + g.Expect(ValidateCELExpression(tt)).To(Succeed()) + }) + } +} + +func TestValidateCELExpressionInvalidExpressions(t *testing.T) { + validationTests := []struct { + expression string + wantError string + }{ + { + "'test'", + "invalid expression output type string", + }, + { + "requrest.body.value", + "undeclared reference to 'requrest'", + }, + } + + for _, tt := range validationTests { + t.Run(tt.expression, func(t *testing.T) { + g := NewWithT(t) + g.Expect(ValidateCELExpression(tt.expression)).To(MatchError(ContainSubstring(tt.wantError))) + }) + } +} + +func TestCELEvaluation(t *testing.T) { + evaluationTests := []struct { + expression string + request *http.Request + resource client.Object + wantResult bool + }{ + { + expression: `resource.metadata.name == 'test-resource' && request.body.target.repository == 'hello-world'`, + request: testNewHTTPRequest(t, http.MethodPost, "/test", map[string]any{ + "target": map[string]any{ + "repository": "hello-world", + }, + }), + resource: &apiv1.Receiver{ + TypeMeta: metav1.TypeMeta{ + Kind: apiv1.ReceiverKind, + APIVersion: apiv1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-resource", + }, + }, + wantResult: true, + }, + { + expression: `resource.metadata.name == 'test-resource' && request.body.image.source.split(':').last().startsWith('v')`, + request: testNewHTTPRequest(t, http.MethodPost, "/test", map[string]any{ + "image": map[string]any{ + "source": "hello-world:v1.0.0", + }, + }), + resource: &apiv1.Receiver{ + TypeMeta: metav1.TypeMeta{ + Kind: apiv1.ReceiverKind, + APIVersion: apiv1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-resource", + }, + }, + wantResult: true, + }, + } + + for _, tt := range evaluationTests { + t.Run(tt.expression, func(t *testing.T) { + g := NewWithT(t) + evaluator, err := newCELEvaluator(tt.expression, tt.request) + g.Expect(err).To(Succeed()) + + result, err := evaluator(context.Background(), tt.resource) + g.Expect(err).To(Succeed()) + g.Expect(result).To(Equal(&tt.wantResult)) + }) + } +} + +func testNewHTTPRequest(t *testing.T, method, target string, body map[string]any) *http.Request { + var httpBody io.Reader + g := NewWithT(t) + if body != nil { + b, err := json.Marshal(body) + g.Expect(err).To(Succeed()) + httpBody = bytes.NewReader(b) + } + + req, err := http.NewRequest(method, target, httpBody) + g.Expect(err).To(Succeed()) + + if httpBody != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req + +}