Skip to content

Commit 4efba59

Browse files
dinhxuanvutimflannagan
authored andcommitted
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]> Upstream-repository: operator-lifecycle-manager Upstream-commit: e3dfbc50dc67a31fef181cc560a537a17e51817b
1 parent b59e734 commit 4efba59

File tree

7 files changed

+229
-4
lines changed

7 files changed

+229
-4
lines changed

staging/operator-lifecycle-manager/pkg/controller/registry/resolver/cache/predicates.go

Lines changed: 40 additions & 0 deletions
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+
}

staging/operator-lifecycle-manager/pkg/controller/registry/resolver/resolver.go

Lines changed: 8 additions & 2 deletions
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.

staging/operator-lifecycle-manager/pkg/controller/registry/resolver/resolver_test.go

Lines changed: 123 additions & 0 deletions
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+
}

staging/operator-lifecycle-manager/pkg/controller/registry/resolver/source_registry.go

Lines changed: 5 additions & 0 deletions
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

vendor/github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/cache/predicates.go

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/resolver.go

Lines changed: 8 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/source_registry.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)