Skip to content

Commit c47c401

Browse files
authored
Add conditions CEL lib for easier status condition checks (#116)
Signed-off-by: Ben Perry <[email protected]>
1 parent 92b6585 commit c47c401

File tree

2 files changed

+206
-0
lines changed

2 files changed

+206
-0
lines changed

pkg/cel/library/conditions.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package library
2+
3+
import (
4+
"github.com/google/cel-go/cel"
5+
"github.com/google/cel-go/common/types"
6+
"github.com/google/cel-go/common/types/ref"
7+
"github.com/google/cel-go/common/types/traits"
8+
)
9+
10+
// ConditionsLib defines the CEL library for checking status conditions.
11+
//
12+
// hasConditions
13+
//
14+
// Checks if a status map has any conditions set
15+
//
16+
// hasConditions(<map>) <bool>
17+
//
18+
// Takes a single map argument, checks for a "conditions" key, and returns true if it is
19+
// a list containing any elements. Otherwise returns false.
20+
//
21+
// Examples:
22+
//
23+
// hasConditions({'conditions': [{'type': 'Ready', 'status': 'False'}]}) // returns true
24+
//
25+
// hasConditions(object.status) && object.status.conditions.exists(c, c.type == "Ready" && c.status == "True")
26+
27+
func ConditionsLib() cel.EnvOption {
28+
return cel.Lib(conditionsLib)
29+
}
30+
31+
var conditionsLib = &conditionsLibType{}
32+
33+
type conditionsLibType struct{}
34+
35+
func (*conditionsLibType) LibraryName() string {
36+
return "open-cluster-management.conditions"
37+
}
38+
39+
func (j *conditionsLibType) CompileOptions() []cel.EnvOption {
40+
options := []cel.EnvOption{
41+
cel.Function("hasConditions", cel.Overload(
42+
"status_has_conditions",
43+
[]*cel.Type{cel.DynType},
44+
cel.BoolType,
45+
cel.UnaryBinding(hasConditions),
46+
)),
47+
}
48+
return options
49+
}
50+
51+
func (*conditionsLibType) ProgramOptions() []cel.ProgramOption {
52+
return []cel.ProgramOption{}
53+
}
54+
55+
func hasConditions(value ref.Val) ref.Val {
56+
status, ok := value.(traits.Mapper)
57+
if !ok {
58+
return types.Bool(false)
59+
}
60+
61+
conditions, ok := status.Find(types.String("conditions"))
62+
if !ok {
63+
return types.Bool(false)
64+
}
65+
66+
if lister, ok := conditions.(traits.Lister); !ok || lister.Size() == types.Int(0) {
67+
return types.Bool(false)
68+
}
69+
70+
return types.Bool(true)
71+
}

pkg/cel/library/conditions_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package library
2+
3+
import (
4+
"regexp"
5+
"testing"
6+
7+
"github.com/google/cel-go/cel"
8+
"github.com/google/cel-go/common/types"
9+
"github.com/google/cel-go/common/types/ref"
10+
"github.com/stretchr/testify/require"
11+
"k8s.io/apimachinery/pkg/util/sets"
12+
)
13+
14+
func TestConditionsLib(t *testing.T) {
15+
trueVal := types.Bool(true)
16+
falseVal := types.Bool(false)
17+
18+
cases := []struct {
19+
name string
20+
expr string
21+
expectValue ref.Val
22+
expectedCompileErrs []string
23+
expectedRuntimeErr string
24+
}{
25+
{
26+
name: "null status",
27+
expr: `hasConditions(null)`,
28+
expectValue: falseVal,
29+
},
30+
{
31+
name: "missing conditions",
32+
expr: `hasConditions({"some": "value"})`,
33+
expectValue: falseVal,
34+
},
35+
{
36+
name: "null conditions",
37+
expr: `hasConditions({"conditions": null})`,
38+
expectValue: falseVal,
39+
},
40+
{
41+
name: "empty conditions",
42+
expr: `hasConditions({"conditions": []})`,
43+
expectValue: falseVal,
44+
},
45+
{
46+
name: "invalid type",
47+
expr: `hasConditions({"conditions": {}})`,
48+
expectValue: falseVal,
49+
},
50+
{
51+
name: "invalid object",
52+
expr: `hasConditions('{"conditions": []}')`,
53+
expectValue: falseVal,
54+
},
55+
{
56+
name: "valid conditions",
57+
expr: `hasConditions({"conditions": [{"type": "Ready", "status": "True"}]})`,
58+
expectValue: trueVal,
59+
},
60+
}
61+
62+
for _, c := range cases {
63+
t.Run(c.name, func(t *testing.T) {
64+
testConditions(t, c.expr, c.expectValue, c.expectedRuntimeErr, c.expectedCompileErrs)
65+
})
66+
}
67+
}
68+
69+
func testConditions(t *testing.T, expr string, expectValue ref.Val, expectRuntimeErrPattern string, expectCompileErrs []string) {
70+
env, err := cel.NewEnv(
71+
ConditionsLib(),
72+
)
73+
if err != nil {
74+
t.Fatalf("%v", err)
75+
}
76+
77+
compiled, issues := env.Compile(expr)
78+
79+
if len(expectCompileErrs) > 0 {
80+
missingCompileErrs := []string{}
81+
matchedCompileErrs := sets.New[int]()
82+
for _, expectedCompileErr := range expectCompileErrs {
83+
compiledPattern, err := regexp.Compile(expectedCompileErr)
84+
if err != nil {
85+
t.Fatalf("failed to compile expected err regex: %v", err)
86+
}
87+
88+
didMatch := false
89+
90+
for i, compileError := range issues.Errors() {
91+
if compiledPattern.Match([]byte(compileError.Message)) {
92+
didMatch = true
93+
matchedCompileErrs.Insert(i)
94+
}
95+
}
96+
97+
if !didMatch {
98+
missingCompileErrs = append(missingCompileErrs, expectedCompileErr)
99+
} else if len(matchedCompileErrs) != len(issues.Errors()) {
100+
unmatchedErrs := []cel.Error{}
101+
for i, issue := range issues.Errors() {
102+
if !matchedCompileErrs.Has(i) {
103+
unmatchedErrs = append(unmatchedErrs, *issue)
104+
}
105+
}
106+
require.Empty(t, unmatchedErrs, "unexpected compilation errors")
107+
}
108+
}
109+
110+
require.Empty(t, missingCompileErrs, "expected compilation errors")
111+
return
112+
} else if len(issues.Errors()) > 0 {
113+
t.Fatalf("%v", issues.Errors())
114+
}
115+
116+
prog, err := env.Program(compiled)
117+
if err != nil {
118+
t.Fatalf("%v", err)
119+
}
120+
res, _, err := prog.Eval(map[string]interface{}{})
121+
if len(expectRuntimeErrPattern) > 0 {
122+
if err == nil {
123+
t.Fatalf("no runtime error thrown. Expected: %v", expectRuntimeErrPattern)
124+
} else if expectRuntimeErrPattern != err.Error() {
125+
t.Fatalf("unexpected err: %v", err)
126+
}
127+
} else if err != nil {
128+
t.Fatalf("%v", err)
129+
} else if expectValue != nil {
130+
converted := res.Equal(expectValue).Value().(bool)
131+
require.True(t, converted, "expectation not equal to output")
132+
} else {
133+
t.Fatal("expected result must not be nil")
134+
}
135+
}

0 commit comments

Comments
 (0)