Skip to content

Commit e11d566

Browse files
ecordellawgreene
authored andcommitted
fix(resolver): Transferring API Ownership
Problem: In an upgrade where v1 of Operator A provides an API and v1 of Operator B depends on the API, where the ownership of the API is transferred from Operator A to Operator B during the upgrade, OLM may run into a situation where Operator B is updated first. This causes OLM to fail to calculate the generation because multiple operator provide the same API. Solution: Add and remove updates as a set to prevent the situation described above.
1 parent fa4b13b commit e11d566

File tree

3 files changed

+105
-5
lines changed

3 files changed

+105
-5
lines changed

pkg/controller/registry/resolver/evolver.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,13 @@ func (e *NamespaceGenerationEvolver) Evolve(add map[OperatorSourceInfo]struct{})
5151
}
5252

5353
func (e *NamespaceGenerationEvolver) checkForUpdates() error {
54+
// maps the old operator identifier to the new operator
55+
updates := EmptyOperatorSet()
56+
5457
// take a snapshot of the current generation so that we don't update the same operator twice in one resolution
55-
for _, op := range e.gen.Operators().Snapshot() {
58+
snapshot := e.gen.Operators().Snapshot()
59+
60+
for _, op := range snapshot {
5661
// only check for updates if we have sourceinfo
5762
if op.SourceInfo() == &ExistingOperator {
5863
continue
@@ -68,15 +73,21 @@ func (e *NamespaceGenerationEvolver) checkForUpdates() error {
6873
return errors.Wrap(err, "error parsing bundle")
6974
}
7075
o.SetReplaces(op.Identifier())
76+
updates[op.Identifier()] = o
77+
}
7178

72-
// Remove the old operator and the APIs it provides before adding the new operator to the generation.
73-
e.gen.RemoveOperator(op)
79+
// remove any operators we found updates for
80+
for old := range updates {
81+
e.gen.RemoveOperator(e.gen.Operators().Snapshot()[old])
82+
}
7483

75-
// Add the new operator and the APIs it provides to the generation.
76-
if err := e.gen.AddOperator(o); err != nil {
84+
// add the new operators we found
85+
for _, new := range updates {
86+
if err := e.gen.AddOperator(new); err != nil {
7787
return errors.Wrap(err, "error calculating generation changes due to new bundle")
7888
}
7989
}
90+
8091
return nil
8192
}
8293

pkg/controller/registry/resolver/evolver_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,46 @@ func TestNamespaceGenerationEvolver(t *testing.T) {
411411
"original"),
412412
),
413413
},
414+
{
415+
name: "OwnershipTransfer",
416+
fields: fields{
417+
querier: NewFakeSourceQuerier(map[CatalogKey][]*api.Bundle{
418+
CatalogKey{"catsrc", "catsrc-namespace"}: {
419+
bundle("depender.v2", "depender", "channel", "depender.v1",
420+
APISet{opregistry.APIKey{"g", "v", "k", "ks"}: {}}, EmptyAPISet(), EmptyAPISet(), EmptyAPISet()),
421+
bundle("original.v2", "o", "c", "original", EmptyAPISet(), EmptyAPISet(), EmptyAPISet(), EmptyAPISet()),
422+
},
423+
}),
424+
gen: NewGenerationFromOperators(
425+
NewFakeOperatorSurface("original", "o", "c", "", "catsrc", "", []opregistry.APIKey{{"g", "v", "k", "ks"}}, nil, nil, nil),
426+
NewFakeOperatorSurface("depender.v1", "depender", "channel", "", "catsrc", "", nil, []opregistry.APIKey{{"g", "v", "k", "ks"}}, nil, nil),
427+
),
428+
},
429+
args: args{},
430+
wantGen: NewGenerationFromOperators(
431+
NewFakeOperatorSurface("original.v2", "o", "c", "original", "catsrc", "", nil, nil, nil, nil),
432+
NewFakeOperatorSurface("depender.v2", "depender", "channel", "depender.v1", "catsrc", "", []opregistry.APIKey{{"g", "v", "k", "ks"}}, nil, nil, nil),
433+
),
434+
},
435+
{
436+
name: "PicksOlderProvider",
437+
fields: fields{
438+
querier: NewFakeSourceQuerier(map[CatalogKey][]*api.Bundle{
439+
CatalogKey{"catsrc", "catsrc-namespace"}: {
440+
bundle("original", "o", "c", "", APISet{opregistry.APIKey{"g", "v", "k", "ks"}: {}}, EmptyAPISet(), EmptyAPISet(), EmptyAPISet()),
441+
bundle("original.v2", "o", "c", "original", EmptyAPISet(), EmptyAPISet(), EmptyAPISet(), EmptyAPISet()),
442+
},
443+
}),
444+
gen: NewGenerationFromOperators(
445+
NewFakeOperatorSurface("depender.v1", "depender", "channel", "", "catsrc", "", nil, []opregistry.APIKey{{"g", "v", "k", "ks"}}, nil, nil),
446+
),
447+
},
448+
args: args{},
449+
wantGen: NewGenerationFromOperators(
450+
NewFakeOperatorSurface("original", "o", "c", "", "catsrc", "", []opregistry.APIKey{{"g", "v", "k", "ks"}},nil, nil, nil),
451+
NewFakeOperatorSurface("depender.v1", "depender", "channel", "", "catsrc", "", nil,[]opregistry.APIKey{{"g", "v", "k", "ks"}}, nil, nil),
452+
),
453+
},
414454
}
415455
for _, tt := range tests {
416456
t.Run(tt.name, func(t *testing.T) {

pkg/controller/registry/resolver/resolver_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,55 @@ func TestNamespaceResolver(t *testing.T) {
409409
},
410410
},
411411
},
412+
{
413+
// This test verifies that ownership of an api can be migrated between two operators
414+
name: "OwnedAPITransfer",
415+
clusterState: []runtime.Object{
416+
existingSub(namespace, "a.v1", "a", "alpha", catalog),
417+
existingOperator(namespace, "a.v1", "a", "alpha", "", Provides1, nil, nil, nil),
418+
existingSub(namespace, "b.v1", "b", "alpha", catalog),
419+
existingOperator(namespace, "b.v1", "b", "alpha", "", nil, Requires1, nil, nil),
420+
},
421+
querier: NewFakeSourceQuerier(map[CatalogKey][]*api.Bundle{
422+
catalog: {
423+
bundle("a.v2", "a", "alpha", "a.v1", nil, nil, nil, nil),
424+
bundle("b.v2", "b", "alpha", "b.v1", Provides1, nil, nil, nil),
425+
},
426+
}),
427+
out: out{
428+
steps: [][]*v1alpha1.Step{
429+
bundleSteps(bundle("a.v2", "a", "alpha", "a.v1", nil, nil, nil, nil), namespace, "", catalog),
430+
bundleSteps(bundle("b.v2", "b", "alpha", "b.v1", Provides1, nil, nil, nil), namespace, "", catalog),
431+
},
432+
subs: []*v1alpha1.Subscription{
433+
updatedSub(namespace, "a.v2", "a", "alpha", catalog),
434+
updatedSub(namespace, "b.v2", "b", "alpha", catalog),
435+
},
436+
},
437+
},
438+
{
439+
name: "PicksOlderProvider",
440+
clusterState: []runtime.Object{
441+
newSub(namespace, "b", "alpha", catalog),
442+
},
443+
querier: NewFakeSourceQuerier(map[CatalogKey][]*api.Bundle{
444+
catalog: {
445+
bundle("a.v1", "a", "alpha", "", Provides1, nil, nil, nil),
446+
bundle("a.v2", "a", "alpha", "a.v1", nil, nil, nil, nil),
447+
bundle("b.v1", "b", "alpha", "", nil, Requires1, nil, nil),
448+
},
449+
}),
450+
out: out{
451+
steps: [][]*v1alpha1.Step{
452+
bundleSteps(bundle("a.v1", "a", "alpha", "", Provides1, nil, nil, nil), namespace, "", catalog),
453+
bundleSteps(bundle("b.v1", "b", "alpha", "", nil, Requires1, nil, nil), namespace, "", catalog),
454+
subSteps(namespace, "a.v1", "a", "alpha", catalog),
455+
},
456+
subs: []*v1alpha1.Subscription{
457+
updatedSub(namespace, "b.v1", "b", "alpha", catalog),
458+
},
459+
},
460+
},
412461
{
413462
name: "InstalledSub/UpdateInHead/SkipRange",
414463
clusterState: []runtime.Object{

0 commit comments

Comments
 (0)