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 24, 2025
1 parent c550303 commit 575c4eb
Show file tree
Hide file tree
Showing 11 changed files with 892 additions and 0 deletions.
66 changes: 66 additions & 0 deletions runtime/cel/boolean_expr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
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"
)

// EvaluateBooleanExpr evaluates a boolean expression with the given data.
func EvaluateBooleanExpr(rawExpr string, data map[string]any) (bool, error) {
var envOptions = []cel.EnvOption{
cel.HomogeneousAggregateLiterals(),
cel.EagerlyValidateDeclarations(true),
cel.DefaultUTCTimeZone(true),
cel.CrossTypeNumericComparisons(true),
cel.OptionalTypes(),
ext.Strings(ext.StringsVersion(2)),
ext.Sets(),
ext.Encoders(),
}

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

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

prog, err := env.Program(expr, cel.EvalOptions(cel.OptOptimize))
if err != nil {
return false, fmt.Errorf("failed to create CEL program: %w", err)
}

val, _, err := prog.Eval(data)
if err != nil {
return false, fmt.Errorf("failed to evaluate the CEL expression: %w", err)
}

result, ok := val.(types.Bool)
if !ok {
return false, fmt.Errorf("failed to evaluate CEL expression as boolean: %s", rawExpr)
}

return bool(result), nil
}
129 changes: 129 additions & 0 deletions runtime/cel/boolean_expr_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
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 TestEvaluateBooleanExpr(t *testing.T) {
for _, tt := range []struct {
name string
expr string
data map[string]any
result bool
err string
}{
{
name: "invalid expression",
expr: "foo.",
err: "failed to parse the CEL expression",
},
{
name: "inexistent field",
expr: "foo",
data: map[string]any{},
err: "failed to evaluate the CEL expression: 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",
},
{
name: "non-boolean field",
expr: "foo",
data: map[string]any{"foo": "some-value"},
err: "failed to evaluate CEL expression as boolean",
},
{
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",
},
{
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,
},
} {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

g := NewWithT(t)

result, err := cel.EvaluateBooleanExpr(tt.expr, 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))
}
})
}
}
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
87 changes: 87 additions & 0 deletions runtime/cel/object_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
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/fluxcd/cli-utils/pkg/kstatus/status"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

"github.com/fluxcd/pkg/apis/kustomize"
)

// EvaluateObjectStatus evaluates the health status of a custom resource object
// according to the rules defined in RFC 0009:
//
// First we check if the object has the field status.observedGeneration. If it does,
// and the value is different from metadata.generation, we return the status InProgress.
//
// Then we evaluate the healthcheck expressions in the following order:
// - InProgress: if true, return status InProgress
// - Failed: if true, return status Failed
// - Current: if true, return status Current
//
// If none of the expressions are true, we return status InProgress.
func EvaluateObjectStatus(exprs *kustomize.HealthCheckExpressions, u *unstructured.Unstructured) (*status.Result, error) {
// exprs.Current is a required field.
if exprs.Current == "" {
return nil, fmt.Errorf("expression Current not specified")
}

unsObj := u.UnstructuredContent()

// Check if the object has the field status.observedGeneration
// and if it differs from metadata.generation, in which case we
// return status InProgress.
observedGeneration, ok, err := unstructured.NestedInt64(unsObj, "status", "observedGeneration")
if err != nil {
return nil, err
}
if ok {
generation, ok, err := unstructured.NestedInt64(unsObj, "metadata", "generation")
if err != nil {
return nil, err
}
if ok && observedGeneration != generation {
return &status.Result{Status: status.InProgressStatus}, nil
}
}

// Evaluate the healthcheck expressions.
for _, e := range []struct {
expr string
status status.Status
}{
// This order is defined in RFC 0009.
{expr: exprs.InProgress, status: status.InProgressStatus},
{expr: exprs.Failed, status: status.FailedStatus},
{expr: exprs.Current, status: status.CurrentStatus},
} {
if e.expr != "" {
result, err := EvaluateBooleanExpr(e.expr, unsObj)
if err != nil {
return nil, err
}
if result {
return &status.Result{Status: e.status}, nil
}
}
}

return &status.Result{Status: status.InProgressStatus}, nil
}
Loading

0 comments on commit 575c4eb

Please sign in to comment.