Skip to content

Commit f40e8e5

Browse files
committed
Fix resolution error if inner entry doesn't provide a required API.
For bundle-to-bundle dependencies (as opposed to subscription-to-bundle "dependencies"), candidates were filtered first to only those that satisfy the constraint in question, then sorted in channel order. Since the relative order of entries within a channel is currently determined by replaces-distance from the channel head, sorting would fail if an entry in the middle of a channel's replaces chain did not satisfy the constraint. For example, assume an operator "bar" has three available versions within a single channel: bar-1, bar-2, and bar-3. If bar-1 and bar-3 provide the API "Bar", but bar-2 does not, then resolution will fail for any operator that requires the API "Bar", because bar-1 and bar-3 are not connected by update edges without bar-2. Now, all (package, channel, catalog) combinations satisfying a constraint are enumerated and sorted before filtering to only those entries that satisfy the constraint. Signed-off-by: Ben Luddy <[email protected]>
1 parent b044fdc commit f40e8e5

File tree

6 files changed

+127
-7
lines changed

6 files changed

+127
-7
lines changed

pkg/controller/registry/resolver/cache.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,12 @@ func WithLabel(label string) OperatorPredicate {
514514
})
515515
}
516516

517+
func WithCatalog(key registry.CatalogKey) OperatorPredicate {
518+
return OperatorPredicateFunc(func(o *Operator) bool {
519+
return key.Equal(o.SourceInfo().Catalog)
520+
})
521+
}
522+
517523
func ProvidingAPI(api opregistry.APIKey) OperatorPredicate {
518524
return OperatorPredicateFunc(func(o *Operator) bool {
519525
for _, p := range o.Properties() {
@@ -611,6 +617,12 @@ func True() OperatorPredicate {
611617
})
612618
}
613619

620+
func False() OperatorPredicate {
621+
return OperatorPredicateFunc(func(*Operator) bool {
622+
return false
623+
})
624+
}
625+
614626
func CountingPredicate(p OperatorPredicate, n *int) OperatorPredicate {
615627
return OperatorPredicateFunc(func(o *Operator) bool {
616628
if p.Test(o) {

pkg/controller/registry/resolver/resolver.go

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -339,15 +339,38 @@ func (r *SatResolver) getBundleInstallables(catalog registry.CatalogKey, predica
339339
}
340340

341341
for _, d := range dependencyPredicates {
342-
// errors ignored; this will build an empty/unsatisfiable dependency if no candidates are found
343-
candidateBundles, _ := AtLeast(1, namespacedCache.FindPreferred(&bundle.sourceInfo.Catalog, d))
344-
sortedBundles, err := r.sortBundles(candidateBundles)
342+
sourcePredicate := False()
343+
// Build a filter matching all (catalog,
344+
// package, channel) combinations that contain
345+
// at least one candidate bundle, even if only
346+
// a subset of those bundles actually satisfy
347+
// the dependency.
348+
sources := map[OperatorSourceInfo]struct{}{}
349+
for _, b := range namespacedCache.Find(d) {
350+
si := b.SourceInfo()
351+
352+
if _, ok := sources[*si]; ok {
353+
// Predicate already covers this source.
354+
continue
355+
}
356+
sources[*si] = struct{}{}
357+
358+
sourcePredicate = Or(sourcePredicate, And(
359+
WithPackage(si.Package),
360+
WithChannel(si.Channel),
361+
WithCatalog(si.Catalog),
362+
))
363+
}
364+
sortedBundles, err := r.sortBundles(namespacedCache.FindPreferred(&bundle.sourceInfo.Catalog, sourcePredicate))
345365
if err != nil {
346366
errs = append(errs, err)
347367
continue
348368
}
349369
bundleDependencies := make([]solver.Identifier, 0)
350-
for _, b := range sortedBundles {
370+
// The dependency predicate is applied here
371+
// (after sorting) to remove all bundles that
372+
// don't satisfy the dependency.
373+
for _, b := range Filter(sortedBundles, d) {
351374
src := b.SourceInfo()
352375
if src == nil {
353376
err := fmt.Errorf("unable to resolve the source of bundle %s, invalid cache", bundle.Identifier())
@@ -360,7 +383,6 @@ func (r *SatResolver) getBundleInstallables(catalog registry.CatalogKey, predica
360383
bundleDependencies = append(bundleDependencies, i.Identifier())
361384
bundleStack = append(bundleStack, b)
362385
}
363-
364386
bundleInstallable.AddDependency(bundleDependencies)
365387
}
366388

@@ -469,6 +491,20 @@ func (r *SatResolver) newSnapshotForNamespace(namespace string, subs []*v1alpha1
469491
op.sourceInfo = &OperatorSourceInfo{
470492
Catalog: existingOperatorCatalog,
471493
}
494+
// Try to determine source package name from properties and add to SourceInfo.
495+
for _, p := range op.properties {
496+
if p.Type != opregistry.PackageType {
497+
continue
498+
}
499+
var pp opregistry.PackageProperty
500+
err := json.Unmarshal([]byte(p.Value), &pp)
501+
if err != nil {
502+
r.log.Warnf("failed to unmarshal package property of csv %q: %w", csv.Name, err)
503+
continue
504+
}
505+
op.sourceInfo.Package = pp.PackageName
506+
}
507+
472508
standaloneOperators = append(standaloneOperators, op)
473509

474510
// all standalone operators are mandatory

pkg/controller/registry/resolver/resolver_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,6 +1319,8 @@ func genOperator(name, version, replaces, pkg, channel, catalogName, catalogName
13191319
Namespace: catalogNamespace,
13201320
},
13211321
DefaultChannel: defaultChannel != "" && channel == defaultChannel,
1322+
Package: pkg,
1323+
Channel: channel,
13221324
},
13231325
providedAPIs: providedAPIs,
13241326
requiredAPIs: requiredAPIs,

pkg/controller/registry/resolver/step_resolver_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -855,7 +855,7 @@ func TestResolver(t *testing.T) {
855855
steps, lookups, subs, err := resolver.ResolveSteps(namespace, nil)
856856
if tt.out.solverError == nil {
857857
if tt.out.errAssert == nil {
858-
assert.Nil(t, err)
858+
assert.NoError(t, err)
859859
} else {
860860
tt.out.errAssert(t, err)
861861
}

pkg/controller/registry/resolver/util_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ func bundle(name, pkg, channel, replaces string, providedCRDs, requiredCRDs, pro
329329
RequiredApis: apiSetToGVK(requiredCRDs, requiredAPIServices),
330330
Replaces: replaces,
331331
Dependencies: apiSetToDependencies(requiredCRDs, requiredAPIServices),
332-
Properties: append(apiSetToProperties(providedCRDs, providedAPIServices, false),
332+
Properties: append(apiSetToProperties(providedCRDs, providedAPIServices, false),
333333
packageNameToProperty(pkg, "0.0.0"),
334334
),
335335
}

test/e2e/subscription_e2e_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,76 @@ var _ = Describe("Subscription", func() {
5555
TearDown(testNamespace)
5656
})
5757

58+
When("an entry in the middle of a channel does not provide a required GVK", func() {
59+
var (
60+
teardown func()
61+
)
62+
63+
BeforeEach(func() {
64+
teardown = func() {}
65+
66+
packages := []registry.PackageManifest{
67+
{
68+
PackageName: "dependency",
69+
Channels: []registry.PackageChannel{
70+
{Name: "channel-dependency", CurrentCSVName: "csv-dependency-3"},
71+
},
72+
DefaultChannelName: "channel-dependency",
73+
},
74+
{
75+
PackageName: "root",
76+
Channels: []registry.PackageChannel{
77+
{Name: "channel-root", CurrentCSVName: "csv-root"},
78+
},
79+
DefaultChannelName: "channel-root",
80+
},
81+
}
82+
83+
crds := []apiextensions.CustomResourceDefinition{newCRD(genName("crd-"))}
84+
csvs := []operatorsv1alpha1.ClusterServiceVersion{
85+
newCSV("csv-dependency-1", testNamespace, "", semver.MustParse("1.0.0"), crds, nil, nil),
86+
newCSV("csv-dependency-2", testNamespace, "csv-dependency-1", semver.MustParse("2.0.0"), nil, nil, nil),
87+
newCSV("csv-dependency-3", testNamespace, "csv-dependency-2", semver.MustParse("3.0.0"), crds, nil, nil),
88+
newCSV("csv-root", testNamespace, "", semver.MustParse("1.0.0"), nil, crds, nil),
89+
}
90+
91+
_, teardown = createInternalCatalogSource(ctx.Ctx().KubeClient(), ctx.Ctx().OperatorClient(), "test-catalog", testNamespace, packages, crds, csvs)
92+
93+
createSubscriptionForCatalog(ctx.Ctx().OperatorClient(), testNamespace, "test-subscription", "test-catalog", "root", "channel-root", "", operatorsv1alpha1.ApprovalAutomatic)
94+
})
95+
96+
AfterEach(func() {
97+
teardown()
98+
})
99+
100+
It("should create a Subscription for the latest entry providing the required GVK", func() {
101+
Eventually(func() ([]operatorsv1alpha1.Subscription, error) {
102+
var list operatorsv1alpha1.SubscriptionList
103+
if err := ctx.Ctx().Client().List(context.TODO(), &list); err != nil {
104+
return nil, err
105+
}
106+
return list.Items, nil
107+
}).Should(ContainElement(WithTransform(
108+
func(in operatorsv1alpha1.Subscription) operatorsv1alpha1.SubscriptionSpec {
109+
return operatorsv1alpha1.SubscriptionSpec{
110+
CatalogSource: in.Spec.CatalogSource,
111+
CatalogSourceNamespace: in.Spec.CatalogSourceNamespace,
112+
Package: in.Spec.Package,
113+
Channel: in.Spec.Channel,
114+
StartingCSV: in.Spec.StartingCSV,
115+
}
116+
},
117+
Equal(operatorsv1alpha1.SubscriptionSpec{
118+
CatalogSource: "test-catalog",
119+
CatalogSourceNamespace: testNamespace,
120+
Package: "dependency",
121+
Channel: "channel-dependency",
122+
StartingCSV: "csv-dependency-3",
123+
}),
124+
)))
125+
})
126+
})
127+
58128
// I. Creating a new subscription
59129
// A. If package is not installed, creating a subscription should install latest version
60130
It("creation if not installed", func() {

0 commit comments

Comments
 (0)