Skip to content

Commit 574293b

Browse files
committed
Add unit test for generic constraint and add some context on CEL library
Add unit test cases for generic constraint using CEL to ensure it works properly in the resolver. Add some context for the custom library for semver comparison in CEL. Signed-off-by: Vu Dinh <[email protected]>
1 parent 9c3b424 commit 574293b

File tree

4 files changed

+159
-18
lines changed

4 files changed

+159
-18
lines changed

Diff for: pkg/controller/registry/resolver/cache/predicates.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ func (ep *evaluatorPredicate) Test(entry *Entry) bool {
357357
return ok
358358
}
359359

360-
func EvaluatorPredicate(provider constraints.EvaluatorProvider, rule, message string) (Predicate, error) {
360+
func EvaluatorPredicate(provider constraints.EvaluatorProvider, rule string, message string) (Predicate, error) {
361361
eval, err := provider.Evaluator(rule)
362362
if err != nil {
363363
return nil, err

Diff for: pkg/controller/registry/resolver/constraints/cel.go

+31-17
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,23 @@ type celEvaluatorProvider struct {
4343
}
4444

4545
type celEvaluator struct {
46-
p cel.Program
46+
program cel.Program
4747
}
4848

49+
/*
50+
This section of code is for custom library for semver comparison in CEL
51+
The code is inspired by https://github.com/google/cel-go/blob/master/cel/cel_test.go#L46
52+
53+
The semver_compare is wrriten based on `Compare` function in https://github.com/blang/semver
54+
particularly in https://github.com/blang/semver/blob/master/semver.go#L125
55+
56+
Example:
57+
`semver_compare(v1, v2)` is equivalent of `v1.Compare(v2)` in blang/semver library
58+
59+
The result is `semver_compare` is an integer just like `Compare`. So, the CEL
60+
expression `semver_compare(v1, v2) == 0` is equivalent v1.Compare(v2) == 0. In
61+
the other words, it checks if v1 is equal to v2 in term of semver comparision.
62+
*/
4963
type semverLib struct{}
5064

5165
func (semverLib) CompileOptions() []cel.EnvOption {
@@ -69,8 +83,21 @@ func (semverLib) ProgramOptions() []cel.ProgramOption {
6983
}
7084
}
7185

86+
func semverCompare(val1, val2 ref.Val) ref.Val {
87+
v1, err := semver.ParseTolerant(fmt.Sprint(val1.Value()))
88+
if err != nil {
89+
return types.ValOrErr(val1, "unable to parse '%v' to semver format", val1.Value())
90+
}
91+
92+
v2, err := semver.ParseTolerant(fmt.Sprint(val2.Value()))
93+
if err != nil {
94+
return types.ValOrErr(val2, "unable to parse '%v' to semver format", val2.Value())
95+
}
96+
return types.Int(v1.Compare(v2))
97+
}
98+
7299
func (e celEvaluator) Evaluate(env map[string]interface{}) (bool, error) {
73-
result, _, err := e.p.Eval(env)
100+
result, _, err := e.program.Eval(env)
74101
if err != nil {
75102
return false, err
76103
}
@@ -92,22 +119,9 @@ func (e *celEvaluatorProvider) Evaluator(rule string) (Evaluator, error) {
92119
return nil, fmt.Errorf("cel expressions must have type Bool")
93120
}
94121

95-
p, err := e.env.Program(ast)
122+
prog, err := e.env.Program(ast)
96123
if err != nil {
97124
return nil, err
98125
}
99-
return celEvaluator{p: p}, nil
100-
}
101-
102-
func semverCompare(val1, val2 ref.Val) ref.Val {
103-
v1, err := semver.ParseTolerant(fmt.Sprint(val1.Value()))
104-
if err != nil {
105-
return types.ValOrErr(val1, "unable to parse '%v' to semver format", val1.Value())
106-
}
107-
108-
v2, err := semver.ParseTolerant(fmt.Sprint(val2.Value()))
109-
if err != nil {
110-
return types.ValOrErr(val2, "unable to parse '%v' to semver format", val2.Value())
111-
}
112-
return types.Int(v1.Compare(v2))
126+
return celEvaluator{program: prog}, nil
113127
}

Diff for: pkg/controller/registry/resolver/resolver_test.go

+122
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/operator-framework/api/pkg/operators/v1alpha1"
1919
listersv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1alpha1"
2020
"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/cache"
21+
"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/constraints"
2122
"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/solver"
2223
"github.com/operator-framework/operator-registry/pkg/api"
2324
opregistry "github.com/operator-framework/operator-registry/pkg/registry"
@@ -2066,3 +2067,124 @@ func TestNewOperatorFromCSV(t *testing.T) {
20662067
})
20672068
}
20682069
}
2070+
2071+
func TestSolveOperators_GenericConstraint(t *testing.T) {
2072+
Provides1 := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}}
2073+
namespace := "olm"
2074+
catalog := cache.SourceKey{Name: "community", Namespace: namespace}
2075+
2076+
deps1 := []*api.Dependency{
2077+
{
2078+
Type: "olm.constraint",
2079+
Value: `{"message":"gvk-constraint",
2080+
"cel":{"rule":"properties.exists(p, p.type == 'olm.gvk' && p.value == {'group': 'g', 'version': 'v', 'kind': 'k'})"}}`,
2081+
},
2082+
}
2083+
deps2 := []*api.Dependency{
2084+
{
2085+
Type: "olm.constraint",
2086+
Value: `{"message":"gvk2-constraint",
2087+
"cel":{"rule":"properties.exists(p, p.type == 'olm.gvk' && p.value == {'group': 'g2', 'version': 'v', 'kind': 'k'})"}}`,
2088+
},
2089+
}
2090+
deps3 := []*api.Dependency{
2091+
{
2092+
Type: "olm.constraint",
2093+
Value: `{"message":"package-constraint",
2094+
"cel":{"rule":"properties.exists(p, p.type == 'olm.package' && p.value.packageName == 'packageB' && (semver_compare(p.value.version, '1.0.1') == 0))"}}`,
2095+
},
2096+
}
2097+
2098+
tests := []struct {
2099+
name string
2100+
isErr bool
2101+
subs []*v1alpha1.Subscription
2102+
catalog cache.Source
2103+
expected cache.OperatorSet
2104+
message string
2105+
}{
2106+
{
2107+
// generic constraint for satisfiable gvk dependency
2108+
name: "Generic Constraint/Satisfiable GVK Dependency",
2109+
isErr: false,
2110+
subs: []*v1alpha1.Subscription{
2111+
newSub(namespace, "packageA", "stable", catalog),
2112+
},
2113+
catalog: &cache.Snapshot{
2114+
Entries: []*cache.Entry{
2115+
genOperator("opA.v1.0.0", "1.0.0", "", "packageA", "stable", catalog.Name, catalog.Namespace, nil, nil, deps1, "", false),
2116+
genOperator("opB.v1.0.0", "1.0.0", "", "packageB", "stable", catalog.Name, catalog.Namespace, nil, Provides1, nil, "stable", false),
2117+
},
2118+
},
2119+
expected: cache.OperatorSet{
2120+
"opA.v1.0.0": genOperator("opA.v1.0.0", "1.0.0", "", "packageA", "stable", catalog.Name, catalog.Namespace, nil, nil, deps1, "", false),
2121+
"opB.v1.0.0": genOperator("opB.v1.0.0", "1.0.0", "", "packageB", "stable", catalog.Name, catalog.Namespace, nil, Provides1, nil, "stable", false),
2122+
},
2123+
},
2124+
{
2125+
// generic constraint for NotSatisfiable gvk dependency
2126+
name: "Generic Constraint/NotSatisfiable GVK Dependency",
2127+
isErr: true,
2128+
subs: []*v1alpha1.Subscription{
2129+
newSub(namespace, "packageA", "stable", catalog),
2130+
},
2131+
catalog: &cache.Snapshot{
2132+
Entries: []*cache.Entry{
2133+
genOperator("opA.v1.0.0", "1.0.0", "", "packageA", "stable", catalog.Name, catalog.Namespace, nil, nil, deps2, "", false),
2134+
genOperator("opB.v1.0.0", "1.0.0", "", "packageB", "stable", catalog.Name, catalog.Namespace, nil, Provides1, nil, "", false),
2135+
},
2136+
},
2137+
// unable to find satisfiable gvk dependency
2138+
// resolve into nothing
2139+
expected: cache.OperatorSet{},
2140+
message: "gvk2-constraint",
2141+
},
2142+
{
2143+
// generic constraint for package constraint
2144+
name: "Generic Constraint/Satisfiable Package Dependency",
2145+
isErr: false,
2146+
subs: []*v1alpha1.Subscription{
2147+
newSub(namespace, "packageA", "stable", catalog),
2148+
},
2149+
catalog: &cache.Snapshot{
2150+
Entries: []*cache.Entry{
2151+
genOperator("opA.v1.0.0", "1.0.0", "", "packageA", "stable", catalog.Name, catalog.Namespace, nil, nil, deps3, "", false),
2152+
genOperator("opB.v1.0.0", "1.0.0", "", "packageB", "stable", catalog.Name, catalog.Namespace, nil, nil, nil, "", false),
2153+
genOperator("opB.v1.0.1", "1.0.1", "opB.v1.0.0", "packageB", "stable", catalog.Name, catalog.Namespace, nil, nil, nil, "stable", false),
2154+
genOperator("opB.v1.0.2", "1.0.2", "opB.v1.0.1", "packageB", "stable", catalog.Name, catalog.Namespace, nil, nil, nil, "stable", false),
2155+
},
2156+
},
2157+
expected: cache.OperatorSet{
2158+
"opA.v1.0.0": genOperator("opA.v1.0.1", "1.0.1", "", "packageA", "stable", catalog.Name, catalog.Namespace, nil, nil, deps3, "", false),
2159+
"opB.v1.0.1": genOperator("opB.v1.0.1", "1.0.1", "opB.v1.0.0", "packageB", "stable", catalog.Name, catalog.Namespace, nil, nil, nil, "stable", false),
2160+
},
2161+
},
2162+
}
2163+
2164+
for _, tt := range tests {
2165+
t.Run(tt.name, func(t *testing.T) {
2166+
var err error
2167+
var operators cache.OperatorSet
2168+
satResolver := SatResolver{
2169+
cache: cache.New(cache.StaticSourceProvider{
2170+
catalog: tt.catalog,
2171+
}),
2172+
log: logrus.New(),
2173+
evaluatorProvider: constraints.NewCelEvaluatorProvider(),
2174+
}
2175+
2176+
operators, err = satResolver.SolveOperators([]string{namespace}, nil, tt.subs)
2177+
if tt.isErr {
2178+
assert.Error(t, err)
2179+
assert.Contains(t, err.Error(), tt.message)
2180+
} else {
2181+
assert.NoError(t, err)
2182+
for k := range tt.expected {
2183+
require.NotNil(t, operators[k])
2184+
assert.EqualValues(t, k, operators[k].Name)
2185+
}
2186+
}
2187+
assert.Equal(t, len(tt.expected), len(operators))
2188+
})
2189+
}
2190+
}

Diff for: pkg/controller/registry/resolver/source_registry.go

+5
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,11 @@ func legacyDependenciesToProperties(dependencies []*api.Dependency) ([]*api.Prop
233233
Type: "olm.label.required",
234234
Value: dependency.Value,
235235
})
236+
case "olm.constraint":
237+
result = append(result, &api.Property{
238+
Type: "olm.constraint",
239+
Value: dependency.Value,
240+
})
236241
}
237242
}
238243
return result, nil

0 commit comments

Comments
 (0)