Skip to content

Commit e3dfbc5

Browse files
authored
feat(resolver): support generic constraint using CEL (#2506)
* feat(resolver): support generic constraint using CEL Introduce the new type of constraint that uses CEL (Common Expression Language) as an expression language to define the constraint in a generic way. This type of constraint will allow operator authors to specify a dependency on any arbitrary properties in the bundle. This constraint also supports logical operator such as AND and OR in the CEL expression. Signed-off-by: Vu Dinh <[email protected]> * 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]> * Use constraints library from api with simplified CEL library Signed-off-by: Vu Dinh <[email protected]> * Rebase against master to use property converter interface Signed-off-by: Vu Dinh <[email protected]>
1 parent 7a9e1af commit e3dfbc5

File tree

4 files changed

+176
-2
lines changed

4 files changed

+176
-2
lines changed

pkg/controller/registry/resolver/cache/predicates.go

+40
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"fmt"
77

88
"github.com/blang/semver/v4"
9+
10+
"github.com/operator-framework/api/pkg/constraints"
911
opregistry "github.com/operator-framework/operator-registry/pkg/registry"
1012
)
1113

@@ -354,3 +356,41 @@ func (c countingPredicate) String() string {
354356
func CountingPredicate(p Predicate, n *int) Predicate {
355357
return countingPredicate{p: p, n: n}
356358
}
359+
360+
type celPredicate struct {
361+
program constraints.CelProgram
362+
rule string
363+
message string
364+
}
365+
366+
func (cp *celPredicate) Test(entry *Entry) bool {
367+
props := make([]map[string]interface{}, len(entry.Properties))
368+
for i, p := range entry.Properties {
369+
var v interface{}
370+
if err := json.Unmarshal([]byte(p.Value), &v); err != nil {
371+
continue
372+
}
373+
props[i] = map[string]interface{}{
374+
"type": p.Type,
375+
"value": v,
376+
}
377+
}
378+
379+
ok, err := cp.program.Evaluate(map[string]interface{}{"properties": props})
380+
if err != nil {
381+
return false
382+
}
383+
return ok
384+
}
385+
386+
func CreateCelPredicate(env *constraints.CelEnvironment, rule string, message string) (Predicate, error) {
387+
prog, err := env.Validate(rule)
388+
if err != nil {
389+
return nil, err
390+
}
391+
return &celPredicate{program: prog, rule: rule, message: message}, nil
392+
}
393+
394+
func (cp *celPredicate) String() string {
395+
return fmt.Sprintf("with constraint: %q and message: %q", cp.rule, cp.message)
396+
}

pkg/controller/registry/resolver/resolver.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ func NewDefaultSatResolver(rcp cache.SourceProvider, catsrcLister v1alpha1lister
3535
return &SatResolver{
3636
cache: cache.New(rcp, cache.WithLogger(logger), cache.WithCatalogSourceLister(catsrcLister)),
3737
log: logger,
38-
pc: &predicateConverter{},
38+
pc: &predicateConverter{
39+
celEnv: constraints.NewCelEnvironment(),
40+
},
3941
}
4042
}
4143

@@ -737,7 +739,9 @@ func sortChannel(bundles []*cache.Entry) ([]*cache.Entry, error) {
737739
}
738740

739741
// predicateConverter configures olm.constraint value -> predicate conversion for the resolver.
740-
type predicateConverter struct{}
742+
type predicateConverter struct {
743+
celEnv *constraints.CelEnvironment
744+
}
741745

742746
// convertDependencyProperties converts all known constraint properties to predicates.
743747
func (pc *predicateConverter) convertDependencyProperties(properties []*api.Property) ([]cache.Predicate, error) {
@@ -814,6 +818,8 @@ func (pc *predicateConverter) convertConstraints(constraints ...constraints.Cons
814818
case constraint.None != nil:
815819
subs, perr := pc.convertConstraints(constraint.None.Constraints...)
816820
preds[i], err = cache.Not(subs...), perr
821+
case constraint.Cel != nil:
822+
preds[i], err = cache.CreateCelPredicate(pc.celEnv, constraint.Cel.Rule, constraint.Message)
817823
default:
818824
// Unknown constraint types are handled by constraints.Parse(),
819825
// but parsed constraints may be empty.

pkg/controller/registry/resolver/resolver_test.go

+123
Original file line numberDiff line numberDiff line change
@@ -2380,3 +2380,126 @@ func TestNewOperatorFromCSV(t *testing.T) {
23802380
})
23812381
}
23822382
}
2383+
2384+
func TestSolveOperators_GenericConstraint(t *testing.T) {
2385+
Provides1 := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}}
2386+
namespace := "olm"
2387+
catalog := cache.SourceKey{Name: "community", Namespace: namespace}
2388+
2389+
deps1 := []*api.Dependency{
2390+
{
2391+
Type: "olm.constraint",
2392+
Value: `{"message":"gvk-constraint",
2393+
"cel":{"rule":"properties.exists(p, p.type == 'olm.gvk' && p.value == {'group': 'g', 'version': 'v', 'kind': 'k'})"}}`,
2394+
},
2395+
}
2396+
deps2 := []*api.Dependency{
2397+
{
2398+
Type: "olm.constraint",
2399+
Value: `{"message":"gvk2-constraint",
2400+
"cel":{"rule":"properties.exists(p, p.type == 'olm.gvk' && p.value == {'group': 'g2', 'version': 'v', 'kind': 'k'})"}}`,
2401+
},
2402+
}
2403+
deps3 := []*api.Dependency{
2404+
{
2405+
Type: "olm.constraint",
2406+
Value: `{"message":"package-constraint",
2407+
"cel":{"rule":"properties.exists(p, p.type == 'olm.package' && p.value.packageName == 'packageB' && (semver_compare(p.value.version, '1.0.1') == 0))"}}`,
2408+
},
2409+
}
2410+
2411+
tests := []struct {
2412+
name string
2413+
isErr bool
2414+
subs []*v1alpha1.Subscription
2415+
catalog cache.Source
2416+
expected cache.OperatorSet
2417+
message string
2418+
}{
2419+
{
2420+
// generic constraint for satisfiable gvk dependency
2421+
name: "Generic Constraint/Satisfiable GVK Dependency",
2422+
isErr: false,
2423+
subs: []*v1alpha1.Subscription{
2424+
newSub(namespace, "packageA", "stable", catalog),
2425+
},
2426+
catalog: &cache.Snapshot{
2427+
Entries: []*cache.Entry{
2428+
genOperator("opA.v1.0.0", "1.0.0", "", "packageA", "stable", catalog.Name, catalog.Namespace, nil, nil, deps1, "", false),
2429+
genOperator("opB.v1.0.0", "1.0.0", "", "packageB", "stable", catalog.Name, catalog.Namespace, nil, Provides1, nil, "stable", false),
2430+
},
2431+
},
2432+
expected: cache.OperatorSet{
2433+
"opA.v1.0.0": genOperator("opA.v1.0.0", "1.0.0", "", "packageA", "stable", catalog.Name, catalog.Namespace, nil, nil, deps1, "", false),
2434+
"opB.v1.0.0": genOperator("opB.v1.0.0", "1.0.0", "", "packageB", "stable", catalog.Name, catalog.Namespace, nil, Provides1, nil, "stable", false),
2435+
},
2436+
},
2437+
{
2438+
// generic constraint for NotSatisfiable gvk dependency
2439+
name: "Generic Constraint/NotSatisfiable GVK Dependency",
2440+
isErr: true,
2441+
subs: []*v1alpha1.Subscription{
2442+
newSub(namespace, "packageA", "stable", catalog),
2443+
},
2444+
catalog: &cache.Snapshot{
2445+
Entries: []*cache.Entry{
2446+
genOperator("opA.v1.0.0", "1.0.0", "", "packageA", "stable", catalog.Name, catalog.Namespace, nil, nil, deps2, "", false),
2447+
genOperator("opB.v1.0.0", "1.0.0", "", "packageB", "stable", catalog.Name, catalog.Namespace, nil, Provides1, nil, "", false),
2448+
},
2449+
},
2450+
// unable to find satisfiable gvk dependency
2451+
// resolve into nothing
2452+
expected: cache.OperatorSet{},
2453+
message: "gvk2-constraint",
2454+
},
2455+
{
2456+
// generic constraint for package constraint
2457+
name: "Generic Constraint/Satisfiable Package Dependency",
2458+
isErr: false,
2459+
subs: []*v1alpha1.Subscription{
2460+
newSub(namespace, "packageA", "stable", catalog),
2461+
},
2462+
catalog: &cache.Snapshot{
2463+
Entries: []*cache.Entry{
2464+
genOperator("opA.v1.0.0", "1.0.0", "", "packageA", "stable", catalog.Name, catalog.Namespace, nil, nil, deps3, "", false),
2465+
genOperator("opB.v1.0.0", "1.0.0", "", "packageB", "stable", catalog.Name, catalog.Namespace, nil, nil, nil, "", false),
2466+
genOperator("opB.v1.0.1", "1.0.1", "opB.v1.0.0", "packageB", "stable", catalog.Name, catalog.Namespace, nil, nil, nil, "stable", false),
2467+
genOperator("opB.v1.0.2", "1.0.2", "opB.v1.0.1", "packageB", "stable", catalog.Name, catalog.Namespace, nil, nil, nil, "stable", false),
2468+
},
2469+
},
2470+
expected: cache.OperatorSet{
2471+
"opA.v1.0.0": genOperator("opA.v1.0.1", "1.0.1", "", "packageA", "stable", catalog.Name, catalog.Namespace, nil, nil, deps3, "", false),
2472+
"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),
2473+
},
2474+
},
2475+
}
2476+
2477+
for _, tt := range tests {
2478+
t.Run(tt.name, func(t *testing.T) {
2479+
var err error
2480+
var operators cache.OperatorSet
2481+
satResolver := SatResolver{
2482+
cache: cache.New(cache.StaticSourceProvider{
2483+
catalog: tt.catalog,
2484+
}),
2485+
log: logrus.New(),
2486+
pc: &predicateConverter{
2487+
celEnv: constraints.NewCelEnvironment(),
2488+
},
2489+
}
2490+
2491+
operators, err = satResolver.SolveOperators([]string{namespace}, nil, tt.subs)
2492+
if tt.isErr {
2493+
assert.Error(t, err)
2494+
assert.Contains(t, err.Error(), tt.message)
2495+
} else {
2496+
assert.NoError(t, err)
2497+
for k := range tt.expected {
2498+
require.NotNil(t, operators[k])
2499+
assert.EqualValues(t, k, operators[k].Name)
2500+
}
2501+
}
2502+
assert.Equal(t, len(tt.expected), len(operators))
2503+
})
2504+
}
2505+
}

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)