Skip to content

Commit c067348

Browse files
(chore): Use label-based cache for revision lookups instead of explicit chains
Instead of tracking revision history through Spec.Previous fields, we now find related revisions using labels and the controller-runtime cache. This is more efficient and works better with controller-runtime's caching layer. To support this change, the Helm-to-Boxcutter migration now sets ownerReferences on migrated revisions, ensuring they work identically to newly created revisions. Assisted-by: Cursor
1 parent b3f85d5 commit c067348

File tree

5 files changed

+375
-25
lines changed

5 files changed

+375
-25
lines changed

cmd/operator-controller/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,7 @@ func setupBoxcutter(
586586
ceReconciler.RevisionStatesGetter = &controllers.BoxcutterRevisionStatesGetter{Reader: mgr.GetClient()}
587587
ceReconciler.StorageMigrator = &applier.BoxcutterStorageMigrator{
588588
Client: mgr.GetClient(),
589+
Scheme: mgr.GetScheme(),
589590
ActionClientGetter: acg,
590591
RevisionGenerator: rg,
591592
}

internal/operator-controller/applier/boxcutter.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ func (r *SimpleRevisionGenerator) GenerateRevisionFromHelmRelease(
8484
})
8585
rev.Name = fmt.Sprintf("%s-1", ext.Name)
8686
rev.Spec.Revision = 1
87+
// Explicitly set LifecycleState to Active to ensure migrated revision is recognized as active
88+
// even before CRD default is applied by the API server
89+
rev.Spec.LifecycleState = ocv1.ClusterExtensionRevisionLifecycleStateActive
8790
return rev, nil
8891
}
8992

@@ -140,30 +143,56 @@ func (r *SimpleRevisionGenerator) buildClusterExtensionRevision(
140143
ext *ocv1.ClusterExtension,
141144
annotations map[string]string,
142145
) *ocv1.ClusterExtensionRevision {
146+
revisionLabels := map[string]string{
147+
controllers.ClusterExtensionRevisionOwnerLabel: ext.Name,
148+
}
149+
// Package name must be in labels for BoxcutterRevisionStatesGetter to find it
150+
if pkg, ok := annotations[labels.PackageNameKey]; ok {
151+
revisionLabels[labels.PackageNameKey] = pkg
152+
}
153+
143154
return &ocv1.ClusterExtensionRevision{
144155
ObjectMeta: metav1.ObjectMeta{
145156
Annotations: annotations,
146-
Labels: map[string]string{
147-
controllers.ClusterExtensionRevisionOwnerLabel: ext.Name,
148-
},
157+
Labels: revisionLabels,
149158
},
150159
Spec: ocv1.ClusterExtensionRevisionSpec{
151160
Phases: PhaseSort(objects),
152161
},
153162
}
154163
}
155164

165+
// BoxcutterStorageMigrator migrates ClusterExtensions from Helm-based storage to Boxcutter's
166+
// ClusterExtensionRevision storage.
167+
//
168+
// Scenario: Given a ClusterExtension, determine if migration from Helm storage is needed
169+
// - When ClusterExtensionRevisions already exist, no migration needed (return early)
170+
// - When a Helm release exists but no revisions, create ClusterExtensionRevision from Helm release
171+
// - When no Helm release exists and no revisions, this is a fresh install (no migration needed)
172+
//
173+
// The migrated revision includes:
174+
// - Owner label (olm.operatorframework.io/owner) for label-based lookups
175+
// - OwnerReference to parent ClusterExtension for proper garbage collection
176+
// - All objects from the Helm release manifest
177+
//
178+
// This ensures migrated revisions have the same structure as revisions created by normal
179+
// Boxcutter flow, enabling successful upgrades after migration.
156180
type BoxcutterStorageMigrator struct {
157181
ActionClientGetter helmclient.ActionClientGetter
158182
RevisionGenerator ClusterExtensionRevisionGenerator
159183
Client boxcutterStorageMigratorClient
184+
Scheme *runtime.Scheme // Required for setting ownerReferences on migrated revisions
160185
}
161186

162187
type boxcutterStorageMigratorClient interface {
163188
List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error
164189
Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error
165190
}
166191

192+
// Migrate performs a one-time migration from Helm storage to ClusterExtensionRevision storage.
193+
//
194+
// This enables upgrades from older operator-controller versions that used Helm for state management.
195+
// The migration is idempotent - it checks for existing revisions before attempting to migrate.
167196
func (m *BoxcutterStorageMigrator) Migrate(ctx context.Context, ext *ocv1.ClusterExtension, objectLabels map[string]string) error {
168197
existingRevisionList := ocv1.ClusterExtensionRevisionList{}
169198
if err := m.Client.List(ctx, &existingRevisionList, client.MatchingLabels{
@@ -195,6 +224,12 @@ func (m *BoxcutterStorageMigrator) Migrate(ctx context.Context, ext *ocv1.Cluste
195224
return err
196225
}
197226

227+
// Set ownerReference to ensure proper lifecycle management and garbage collection.
228+
// This makes the migrated revision consistent with revisions created by normal Boxcutter flow.
229+
if err := controllerutil.SetControllerReference(ext, rev, m.Scheme); err != nil {
230+
return fmt.Errorf("set ownerref: %w", err)
231+
}
232+
198233
if err := m.Client.Create(ctx, rev); err != nil {
199234
return err
200235
}

internal/operator-controller/applier/boxcutter_test.go

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,13 @@ func Test_SimpleRevisionGenerator_GenerateRevisionFromHelmRelease(t *testing.T)
6969
"olm.operatorframework.io/package-name": "my-package",
7070
},
7171
Labels: map[string]string{
72-
"olm.operatorframework.io/owner": "test-123",
72+
"olm.operatorframework.io/owner": "test-123",
73+
"olm.operatorframework.io/package-name": "my-package",
7374
},
7475
},
7576
Spec: ocv1.ClusterExtensionRevisionSpec{
76-
Revision: 1,
77+
LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateActive,
78+
Revision: 1,
7779
Phases: []ocv1.ClusterExtensionRevisionPhase{
7880
{
7981
Name: "deploy",
@@ -758,6 +760,100 @@ func TestBoxcutter_Apply(t *testing.T) {
758760
assert.Contains(t, latest.Spec.Previous, ocv1.ClusterExtensionRevisionPrevious{Name: "rev-4"})
759761
},
760762
},
763+
{
764+
name: "annotation-only update (same phases, different annotations)",
765+
mockBuilder: &mockBundleRevisionBuilder{
766+
makeRevisionFunc: func(bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (*ocv1.ClusterExtensionRevision, error) {
767+
// Return revision with UPDATED annotations but SAME phases
768+
revisionLabels := map[string]string{
769+
controllers.ClusterExtensionRevisionOwnerLabel: ext.Name,
770+
}
771+
if pkg, ok := revisionAnnotations[labels.PackageNameKey]; ok {
772+
revisionLabels[labels.PackageNameKey] = pkg
773+
}
774+
return &ocv1.ClusterExtensionRevision{
775+
ObjectMeta: metav1.ObjectMeta{
776+
Annotations: revisionAnnotations,
777+
Labels: revisionLabels,
778+
},
779+
Spec: ocv1.ClusterExtensionRevisionSpec{
780+
Phases: []ocv1.ClusterExtensionRevisionPhase{
781+
{
782+
Name: string(applier.PhaseDeploy),
783+
Objects: []ocv1.ClusterExtensionRevisionObject{
784+
{
785+
Object: unstructured.Unstructured{
786+
Object: map[string]interface{}{
787+
"apiVersion": "v1",
788+
"kind": "ConfigMap",
789+
"metadata": map[string]interface{}{
790+
"name": "test-cm",
791+
},
792+
},
793+
},
794+
},
795+
},
796+
},
797+
},
798+
},
799+
}, nil
800+
},
801+
},
802+
existingObjs: []client.Object{
803+
ext,
804+
&ocv1.ClusterExtensionRevision{
805+
ObjectMeta: metav1.ObjectMeta{
806+
Name: "test-ext-1",
807+
Annotations: map[string]string{
808+
labels.BundleVersionKey: "1.0.0",
809+
labels.PackageNameKey: "test-package",
810+
},
811+
Labels: map[string]string{
812+
controllers.ClusterExtensionRevisionOwnerLabel: ext.Name,
813+
labels.PackageNameKey: "test-package",
814+
},
815+
},
816+
Spec: ocv1.ClusterExtensionRevisionSpec{
817+
Revision: 1,
818+
Phases: []ocv1.ClusterExtensionRevisionPhase{
819+
{
820+
Name: string(applier.PhaseDeploy),
821+
Objects: []ocv1.ClusterExtensionRevisionObject{
822+
{
823+
Object: unstructured.Unstructured{
824+
Object: map[string]interface{}{
825+
"apiVersion": "v1",
826+
"kind": "ConfigMap",
827+
"metadata": map[string]interface{}{
828+
"name": "test-cm",
829+
},
830+
},
831+
},
832+
},
833+
},
834+
},
835+
},
836+
},
837+
},
838+
},
839+
validate: func(t *testing.T, c client.Client) {
840+
revList := &ocv1.ClusterExtensionRevisionList{}
841+
err := c.List(context.Background(), revList, client.MatchingLabels{controllers.ClusterExtensionRevisionOwnerLabel: ext.Name})
842+
require.NoError(t, err)
843+
// Should still be only 1 revision (in-place update, not new revision)
844+
require.Len(t, revList.Items, 1)
845+
846+
rev := revList.Items[0]
847+
assert.Equal(t, "test-ext-1", rev.Name)
848+
assert.Equal(t, int64(1), rev.Spec.Revision)
849+
// Verify annotations were updated
850+
assert.Equal(t, "1.0.1", rev.Annotations[labels.BundleVersionKey])
851+
assert.Equal(t, "test-package", rev.Annotations[labels.PackageNameKey])
852+
// Verify labels still have package name
853+
assert.Equal(t, ext.Name, rev.Labels[controllers.ClusterExtensionRevisionOwnerLabel])
854+
assert.Equal(t, "test-package", rev.Labels[labels.PackageNameKey])
855+
},
856+
},
761857
}
762858

763859
for _, tc := range testCases {
@@ -780,7 +876,15 @@ func TestBoxcutter_Apply(t *testing.T) {
780876
testFS := fstest.MapFS{}
781877

782878
// Execute
783-
installSucceeded, installStatus, err := boxcutter.Apply(t.Context(), testFS, ext, nil, nil)
879+
revisionAnnotations := map[string]string{}
880+
if tc.name == "annotation-only update (same phases, different annotations)" {
881+
// For annotation-only update test, pass NEW annotations
882+
revisionAnnotations = map[string]string{
883+
labels.BundleVersionKey: "1.0.1",
884+
labels.PackageNameKey: "test-package",
885+
}
886+
}
887+
installSucceeded, installStatus, err := boxcutter.Apply(t.Context(), testFS, ext, nil, revisionAnnotations)
784888

785889
// Assert
786890
if tc.expectedErr != "" {
@@ -808,13 +912,17 @@ func TestBoxcutter_Apply(t *testing.T) {
808912

809913
func TestBoxcutterStorageMigrator(t *testing.T) {
810914
t.Run("creates revision", func(t *testing.T) {
915+
testScheme := runtime.NewScheme()
916+
require.NoError(t, ocv1.AddToScheme(testScheme))
917+
811918
brb := &mockBundleRevisionBuilder{}
812919
mag := &mockActionGetter{}
813920
client := &clientMock{}
814921
sm := &applier.BoxcutterStorageMigrator{
815922
RevisionGenerator: brb,
816923
ActionClientGetter: mag,
817924
Client: client,
925+
Scheme: testScheme,
818926
}
819927

820928
ext := &ocv1.ClusterExtension{
@@ -836,13 +944,17 @@ func TestBoxcutterStorageMigrator(t *testing.T) {
836944
})
837945

838946
t.Run("does not create revision when revisions exist", func(t *testing.T) {
947+
testScheme := runtime.NewScheme()
948+
require.NoError(t, ocv1.AddToScheme(testScheme))
949+
839950
brb := &mockBundleRevisionBuilder{}
840951
mag := &mockActionGetter{}
841952
client := &clientMock{}
842953
sm := &applier.BoxcutterStorageMigrator{
843954
RevisionGenerator: brb,
844955
ActionClientGetter: mag,
845956
Client: client,
957+
Scheme: testScheme,
846958
}
847959

848960
ext := &ocv1.ClusterExtension{
@@ -866,6 +978,9 @@ func TestBoxcutterStorageMigrator(t *testing.T) {
866978
})
867979

868980
t.Run("does not create revision when no helm release", func(t *testing.T) {
981+
testScheme := runtime.NewScheme()
982+
require.NoError(t, ocv1.AddToScheme(testScheme))
983+
869984
brb := &mockBundleRevisionBuilder{}
870985
mag := &mockActionGetter{
871986
getClientErr: driver.ErrReleaseNotFound,
@@ -875,6 +990,7 @@ func TestBoxcutterStorageMigrator(t *testing.T) {
875990
RevisionGenerator: brb,
876991
ActionClientGetter: mag,
877992
Client: client,
993+
Scheme: testScheme,
878994
}
879995

880996
ext := &ocv1.ClusterExtension{
@@ -905,7 +1021,15 @@ func (m *mockBundleRevisionBuilder) GenerateRevisionFromHelmRelease(
9051021
helmRelease *release.Release, ext *ocv1.ClusterExtension,
9061022
objectLabels map[string]string,
9071023
) (*ocv1.ClusterExtensionRevision, error) {
908-
return nil, nil
1024+
return &ocv1.ClusterExtensionRevision{
1025+
ObjectMeta: metav1.ObjectMeta{
1026+
Name: "test-revision",
1027+
Labels: map[string]string{
1028+
controllers.ClusterExtensionRevisionOwnerLabel: ext.Name,
1029+
},
1030+
},
1031+
Spec: ocv1.ClusterExtensionRevisionSpec{},
1032+
}, nil
9091033
}
9101034

9111035
type clientMock struct {

0 commit comments

Comments
 (0)