Skip to content

Commit

Permalink
Add CEL library with custom healthchecks to runtime
Browse files Browse the repository at this point in the history
Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
  • Loading branch information
matheuscscp committed Jan 28, 2025
1 parent c550303 commit 79a726e
Show file tree
Hide file tree
Showing 15 changed files with 1,589 additions and 2 deletions.
18 changes: 18 additions & 0 deletions runtime/cel/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
Copyright 2025 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// cel provides utilities for evaluating Common Expression Language (CEL) expressions.
package cel
121 changes: 121 additions & 0 deletions runtime/cel/expression.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
Copyright 2025 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package cel

import (
"fmt"

"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/ext"
)

// Expression represents a parsed CEL expression.
type Expression struct {
expr string
prog cel.Program
}

// Option is a function that configures the CEL expression.
type Option func(*options)

type options struct {
compile bool
envOpts []cel.EnvOption
}

// WithCompile specifies that the expression should be compiled,
// which provides stricter checks at parse time, before evaluation.
func WithCompile() Option {
return func(o *options) {
o.compile = true
}
}

// WithStructVariables declares variables of type google.protobuf.Struct.
func WithStructVariables(vars ...string) Option {
return func(o *options) {
for _, v := range vars {
d := cel.Variable(v, cel.ObjectType("google.protobuf.Struct"))
o.envOpts = append(o.envOpts, d)
}
}
}

// NewExpression parses the given CEL expression and returns a new Expression.
func NewExpression(expr string, opts ...Option) (*Expression, error) {
var o options
for _, opt := range opts {
opt(&o)
}

envOpts := append([]cel.EnvOption{
cel.HomogeneousAggregateLiterals(),
cel.EagerlyValidateDeclarations(true),
cel.DefaultUTCTimeZone(true),
cel.CrossTypeNumericComparisons(true),
cel.OptionalTypes(),
ext.Strings(),
ext.Sets(),
ext.Encoders(),
}, o.envOpts...)

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

parse := env.Parse
if o.compile {
parse = env.Compile
}
e, issues := parse(expr)
if issues != nil {
return nil, fmt.Errorf("failed to parse the CEL expression '%s': %s", expr, issues.String())
}

progOpts := []cel.ProgramOption{
cel.EvalOptions(cel.OptOptimize),

// 100 is the kubernetes default:
// https://github.com/kubernetes/kubernetes/blob/3f26d005571dc5903e7cebae33ada67986bc40f3/staging/src/k8s.io/apiserver/pkg/apis/cel/config.go#L33-L35
cel.InterruptCheckFrequency(100),
}

prog, err := env.Program(e, progOpts...)
if err != nil {
return nil, fmt.Errorf("failed to create CEL program: %w", err)
}

return &Expression{
expr: expr,
prog: prog,
}, nil
}

// EvaluateBoolean evaluates the expression with the given data and returns the result as a boolean.
func (e *Expression) EvaluateBoolean(data map[string]any) (bool, error) {
val, _, err := e.prog.Eval(data)
if err != nil {
return false, fmt.Errorf("failed to evaluate the CEL expression '%s': %w", e.expr, err)
}
result, ok := val.(types.Bool)
if !ok {
return false, fmt.Errorf("failed to evaluate CEL expression as boolean: '%s'", e.expr)
}
return bool(result), nil
}
190 changes: 190 additions & 0 deletions runtime/cel/expression_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/*
Copyright 2025 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package cel_test

import (
"testing"

. "github.com/onsi/gomega"

"github.com/fluxcd/pkg/runtime/cel"
)

func TestNewExpression(t *testing.T) {
for _, tt := range []struct {
name string
expr string
opts []cel.Option
err string
}{
{
name: "valid expression",
expr: "foo",
},
{
name: "invalid expression",
expr: "foo.",
err: "failed to parse the CEL expression 'foo.': ERROR: <input>:1:5: Syntax error: no viable alternative at input '.'",
},
{
name: "compilation detects undeclared references",
expr: "foo",
opts: []cel.Option{cel.WithCompile()},
err: "failed to parse the CEL expression 'foo': ERROR: <input>:1:1: undeclared reference to 'foo'",
},
{
name: "compilation detects type errors",
expr: "foo == 'bar'",
opts: []cel.Option{cel.WithCompile(), cel.WithStructVariables("foo")},
err: "failed to parse the CEL expression 'foo == 'bar'': ERROR: <input>:1:5: found no matching overload for '_==_' applied to '(map(string, dyn), string)'",
},
} {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

g := NewWithT(t)

e, err := cel.NewExpression(tt.expr, tt.opts...)

if tt.err != "" {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.err))
g.Expect(e).To(BeNil())
} else {
g.Expect(err).NotTo(HaveOccurred())
g.Expect(e).NotTo(BeNil())
}
})
}
}

func TestExpression_EvaluateBoolean(t *testing.T) {
for _, tt := range []struct {
name string
expr string
opts []cel.Option
data map[string]any
result bool
err string
}{
{
name: "inexistent field",
expr: "foo",
data: map[string]any{},
err: "failed to evaluate the CEL expression 'foo': no such attribute(s): foo",
},
{
name: "boolean field true",
expr: "foo",
data: map[string]any{"foo": true},
result: true,
},
{
name: "boolean field false",
expr: "foo",
data: map[string]any{"foo": false},
result: false,
},
{
name: "nested boolean field true",
expr: "foo.bar",
data: map[string]any{"foo": map[string]any{"bar": true}},
result: true,
},
{
name: "nested boolean field false",
expr: "foo.bar",
data: map[string]any{"foo": map[string]any{"bar": false}},
result: false,
},
{
name: "boolean literal true",
expr: "true",
data: map[string]any{},
result: true,
},
{
name: "boolean literal false",
expr: "false",
data: map[string]any{},
result: false,
},
{
name: "non-boolean literal",
expr: "'some-value'",
data: map[string]any{},
err: "failed to evaluate CEL expression as boolean: ''some-value''",
},
{
name: "non-boolean field",
expr: "foo",
data: map[string]any{"foo": "some-value"},
err: "failed to evaluate CEL expression as boolean: 'foo'",
},
{
name: "nested non-boolean field",
expr: "foo.bar",
data: map[string]any{"foo": map[string]any{"bar": "some-value"}},
err: "failed to evaluate CEL expression as boolean: 'foo.bar'",
},
{
name: "complex expression evaluating true",
expr: "foo && bar",
data: map[string]any{"foo": true, "bar": true},
result: true,
},
{
name: "complex expression evaluating false",
expr: "foo && bar",
data: map[string]any{"foo": true, "bar": false},
result: false,
},
{
name: "compiled expression returning true",
expr: "foo.bar",
opts: []cel.Option{cel.WithCompile(), cel.WithStructVariables("foo")},
data: map[string]any{"foo": map[string]any{"bar": true}},
result: true,
},
{
name: "compiled expression returning false",
expr: "foo.bar",
opts: []cel.Option{cel.WithCompile(), cel.WithStructVariables("foo")},
data: map[string]any{"foo": map[string]any{"bar": false}},
result: false,
},
} {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

g := NewWithT(t)

e, err := cel.NewExpression(tt.expr, tt.opts...)
g.Expect(err).NotTo(HaveOccurred())

result, err := e.EvaluateBoolean(tt.data)

if tt.err != "" {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.err))
} else {
g.Expect(err).NotTo(HaveOccurred())
g.Expect(result).To(Equal(tt.result))
}
})
}
}
Loading

0 comments on commit 79a726e

Please sign in to comment.