Skip to content

Commit d7100ae

Browse files
committed
fix: adopt migrated ClusterObjectSets
Signed-off-by: Todd Short <tshort@redhat.com>
1 parent a61dd75 commit d7100ae

3 files changed

Lines changed: 100 additions & 0 deletions

File tree

‎internal/operator-controller/applier/boxcutter.go‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,17 @@ func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
478478
return true, "", nil
479479
}
480480

481+
// The migration tool creates a successful revision 1 before it creates the
482+
// ClusterExtension. Do not render that same bundle into a second revision:
483+
// claim the pre-existing revision and use it as the initial installed state.
484+
// Comparing the resolved bundle metadata keeps normal upgrades intact.
485+
if revision := migratedSubscriptionRevision(existingRevisions, revisionAnnotations); revision != nil {
486+
if err := bc.adoptRevision(ctx, ext, revision); err != nil {
487+
return false, "", fmt.Errorf("adopting migrated revision %s: %w", revision.Name, err)
488+
}
489+
return true, "", nil
490+
}
491+
481492
// Generate desired revision
482493
desiredRevision, err := bc.RevisionGenerator.GenerateRevision(ctx, contentFS, ext, objectLabels, revisionAnnotations)
483494
if err != nil {
@@ -574,6 +585,46 @@ func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
574585
return true, "", nil
575586
}
576587

588+
func migratedSubscriptionRevision(revisions []ocv1.ClusterObjectSet, desiredAnnotations map[string]string) *ocv1.ClusterObjectSet {
589+
for i := range revisions {
590+
revision := &revisions[i]
591+
if revision.Spec.Revision != 1 ||
592+
revision.Annotations[labels.MigratedFromSubscriptionKey] == "" ||
593+
!meta.IsStatusConditionTrue(revision.Status.Conditions, ocv1.ClusterObjectSetTypeSucceeded) {
594+
continue
595+
}
596+
597+
matchesBundle := true
598+
for _, key := range []string{labels.PackageNameKey, labels.BundleNameKey, labels.BundleVersionKey, labels.BundleReferenceKey, labels.BundleReleaseKey} {
599+
if revision.Annotations[key] != desiredAnnotations[key] {
600+
matchesBundle = false
601+
break
602+
}
603+
}
604+
if matchesBundle {
605+
return revision
606+
}
607+
}
608+
return nil
609+
}
610+
611+
func (bc *Boxcutter) adoptRevision(ctx context.Context, ext *ocv1.ClusterExtension, revision *ocv1.ClusterObjectSet) error {
612+
for _, ref := range revision.OwnerReferences {
613+
if ref.Controller != nil && *ref.Controller && ref.UID != ext.UID {
614+
return fmt.Errorf("revision is already controlled by %s %q", ref.Kind, ref.Name)
615+
}
616+
}
617+
618+
for _, ref := range revision.OwnerReferences {
619+
if ref.Controller != nil && *ref.Controller && ref.UID == ext.UID {
620+
return nil
621+
}
622+
}
623+
624+
revision.OwnerReferences = append(revision.OwnerReferences, *metav1.NewControllerRef(ext, ocv1.SchemeGroupVersion.WithKind(ocv1.ClusterExtensionKind)))
625+
return bc.Client.Update(ctx, revision)
626+
}
627+
577628
// createExternalizedRevision creates a new COS with all objects externalized to Secrets.
578629
// It follows a crash-safe three-step sequence: create Secrets, create COS, patch ownerRefs.
579630
func (bc *Boxcutter) createExternalizedRevision(ctx context.Context, ext *ocv1.ClusterExtension, desiredRevision *ocv1ac.ClusterObjectSetApplyConfiguration, existingRevisions []ocv1.ClusterObjectSet) error {

‎internal/operator-controller/applier/boxcutter_test.go‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,42 @@ func TestBoxcutter_Apply(t *testing.T) {
666666
assert.Equal(t, "test-ext-1", revList.Items[0].Name)
667667
},
668668
},
669+
{
670+
name: "adopts matching migration revision without creating a duplicate",
671+
mockBuilder: func(t *testing.T) applier.ClusterObjectSetGenerator {
672+
ctrl := gomock.NewController(t)
673+
return mockapplier.NewMockClusterObjectSetGenerator(ctrl)
674+
},
675+
existingObjs: []client.Object{
676+
&ocv1.ClusterObjectSet{
677+
ObjectMeta: metav1.ObjectMeta{
678+
Name: "test-ext-1",
679+
Labels: map[string]string{labels.OwnerNameKey: ext.Name},
680+
Annotations: map[string]string{
681+
labels.MigratedFromSubscriptionKey: "test-namespace/test-subscription",
682+
labels.PackageNameKey: "test-package",
683+
labels.BundleNameKey: "test-package.v1.0.0",
684+
labels.BundleVersionKey: "1.0.0",
685+
labels.BundleReferenceKey: "registry.example/test-package@sha256:123",
686+
},
687+
},
688+
Spec: ocv1.ClusterObjectSetSpec{Revision: 1},
689+
Status: ocv1.ClusterObjectSetStatus{Conditions: []metav1.Condition{{
690+
Type: ocv1.ClusterObjectSetTypeSucceeded,
691+
Status: metav1.ConditionTrue,
692+
}}},
693+
},
694+
},
695+
validate: func(t *testing.T, c client.Client) {
696+
revList := &ocv1.ClusterObjectSetList{}
697+
require.NoError(t, c.List(t.Context(), revList, client.MatchingLabels{labels.OwnerNameKey: ext.Name}))
698+
require.Len(t, revList.Items, 1)
699+
assert.Equal(t, "test-ext-1", revList.Items[0].Name)
700+
require.Len(t, revList.Items[0].OwnerReferences, 1)
701+
assert.Equal(t, ext.Name, revList.Items[0].OwnerReferences[0].Name)
702+
assert.Equal(t, ext.UID, revList.Items[0].OwnerReferences[0].UID)
703+
},
704+
},
669705
{
670706
name: "new revision created when objects in new revision are different",
671707
mockBuilder: func(t *testing.T) applier.ClusterObjectSetGenerator {
@@ -1087,6 +1123,13 @@ func TestBoxcutter_Apply(t *testing.T) {
10871123
labels.BundleVersionKey: "1.0.1",
10881124
labels.PackageNameKey: "test-package",
10891125
}
1126+
} else if tc.name == "adopts matching migration revision without creating a duplicate" {
1127+
revisionAnnotations = map[string]string{
1128+
labels.PackageNameKey: "test-package",
1129+
labels.BundleNameKey: "test-package.v1.0.0",
1130+
labels.BundleVersionKey: "1.0.0",
1131+
labels.BundleReferenceKey: "registry.example/test-package@sha256:123",
1132+
}
10901133
}
10911134
completed, status, err := boxcutter.Apply(t.Context(), testFS, ext, nil, revisionAnnotations)
10921135

‎internal/operator-controller/labels/labels.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,10 @@ const (
5151
// that were created during migration from Helm releases. This label is used
5252
// to distinguish migrated revisions from those created by normal Boxcutter operation.
5353
MigratedFromHelmKey = "olm.operatorframework.io/migrated-from-helm"
54+
55+
// MigratedFromSubscriptionKey is the annotation placed on a revision created
56+
// by the OLM v0-to-v1 migration tool. A matching revision 1 is adopted by a
57+
// subsequently-created ClusterExtension instead of being replaced by its
58+
// initial Boxcutter reconciliation.
59+
MigratedFromSubscriptionKey = "olm.operatorframework.io/migrated-from-subscription"
5460
)

0 commit comments

Comments
 (0)