Skip to content

Commit 472e8a2

Browse files
(chore): Use label-based cache for revision lookups instead of explicit chains (#2315)
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 fc65e52 commit 472e8a2

File tree

13 files changed

+590
-178
lines changed

13 files changed

+590
-178
lines changed

api/v1/clusterextensionrevision_types.go

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package v1
1919
import (
2020
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2121
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
22-
"k8s.io/apimachinery/pkg/types"
2322
)
2423

2524
const (
@@ -40,6 +39,7 @@ const (
4039
ClusterExtensionRevisionReasonIncomplete = "Incomplete"
4140
ClusterExtensionRevisionReasonProgressing = "Progressing"
4241
ClusterExtensionRevisionReasonArchived = "Archived"
42+
ClusterExtensionRevisionReasonMigrated = "Migrated"
4343
)
4444

4545
// ClusterExtensionRevisionSpec defines the desired state of ClusterExtensionRevision.
@@ -66,10 +66,6 @@ type ClusterExtensionRevisionSpec struct {
6666
// +listMapKey=name
6767
// +optional
6868
Phases []ClusterExtensionRevisionPhase `json:"phases,omitempty"`
69-
// Previous references previous revisions that objects can be adopted from.
70-
//
71-
// +kubebuilder:validation:XValidation:rule="self == oldSelf", message="previous is immutable"
72-
Previous []ClusterExtensionRevisionPrevious `json:"previous,omitempty"`
7369
}
7470

7571
// ClusterExtensionRevisionLifecycleState specifies the lifecycle state of the ClusterExtensionRevision.
@@ -130,13 +126,6 @@ const (
130126
CollisionProtectionNone CollisionProtection = "None"
131127
)
132128

133-
type ClusterExtensionRevisionPrevious struct {
134-
// +kubebuilder:validation:Required
135-
Name string `json:"name"`
136-
// +kubebuilder:validation:Required
137-
UID types.UID `json:"uid"`
138-
}
139-
140129
// ClusterExtensionRevisionStatus defines the observed state of a ClusterExtensionRevision.
141130
type ClusterExtensionRevisionStatus struct {
142131
// +listType=map

api/v1/zz_generated.deepcopy.go

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

cmd/operator-controller/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,7 @@ func setupBoxcutter(
583583
ceReconciler.RevisionStatesGetter = &controllers.BoxcutterRevisionStatesGetter{Reader: mgr.GetClient()}
584584
ceReconciler.StorageMigrator = &applier.BoxcutterStorageMigrator{
585585
Client: mgr.GetClient(),
586+
Scheme: mgr.GetScheme(),
586587
ActionClientGetter: acg,
587588
RevisionGenerator: rg,
588589
}

helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -111,27 +111,6 @@ spec:
111111
x-kubernetes-validations:
112112
- message: phases is immutable
113113
rule: self == oldSelf || oldSelf.size() == 0
114-
previous:
115-
description: Previous references previous revisions that objects can
116-
be adopted from.
117-
items:
118-
properties:
119-
name:
120-
type: string
121-
uid:
122-
description: |-
123-
UID is a type that holds unique ID values, including UUIDs. Because we
124-
don't ONLY use UUIDs, this is an alias to string. Being a type captures
125-
intent and helps make sure that UIDs and names do not get conflated.
126-
type: string
127-
required:
128-
- name
129-
- uid
130-
type: object
131-
type: array
132-
x-kubernetes-validations:
133-
- message: previous is immutable
134-
rule: self == oldSelf
135114
revision:
136115
description: |-
137116
Revision is a sequence number representing a specific revision of the ClusterExtension instance.

internal/operator-controller/applier/boxcutter.go

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import (
3131
)
3232

3333
const (
34-
ClusterExtensionRevisionPreviousLimit = 5
34+
ClusterExtensionRevisionRetentionLimit = 5
3535
)
3636

3737
type ClusterExtensionRevisionGenerator interface {
@@ -187,22 +187,33 @@ func (r *SimpleRevisionGenerator) buildClusterExtensionRevision(
187187
},
188188
},
189189
Spec: ocv1.ClusterExtensionRevisionSpec{
190-
Phases: PhaseSort(objects),
190+
// Explicitly set LifecycleState to Active. While the CRD has a default,
191+
// being explicit here ensures all code paths are clear and doesn't rely
192+
// on API server defaulting behavior.
193+
LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateActive,
194+
Phases: PhaseSort(objects),
191195
},
192196
}
193197
}
194198

199+
// BoxcutterStorageMigrator migrates ClusterExtensions from Helm-based storage to
200+
// ClusterExtensionRevision storage, enabling upgrades from older operator-controller versions.
195201
type BoxcutterStorageMigrator struct {
196202
ActionClientGetter helmclient.ActionClientGetter
197203
RevisionGenerator ClusterExtensionRevisionGenerator
198204
Client boxcutterStorageMigratorClient
205+
Scheme *runtime.Scheme
199206
}
200207

201208
type boxcutterStorageMigratorClient interface {
202209
List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error
210+
Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error
203211
Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error
212+
Status() client.StatusWriter
204213
}
205214

215+
// Migrate creates a ClusterExtensionRevision from an existing Helm release if no revisions exist yet.
216+
// The migration is idempotent and skipped if revisions already exist or no Helm release is found.
206217
func (m *BoxcutterStorageMigrator) Migrate(ctx context.Context, ext *ocv1.ClusterExtension, objectLabels map[string]string) error {
207218
existingRevisionList := ocv1.ClusterExtensionRevisionList{}
208219
if err := m.Client.List(ctx, &existingRevisionList, client.MatchingLabels{
@@ -234,9 +245,34 @@ func (m *BoxcutterStorageMigrator) Migrate(ctx context.Context, ext *ocv1.Cluste
234245
return err
235246
}
236247

248+
// Set ownerReference for proper garbage collection when the ClusterExtension is deleted.
249+
if err := controllerutil.SetControllerReference(ext, rev, m.Scheme); err != nil {
250+
return fmt.Errorf("set ownerref: %w", err)
251+
}
252+
237253
if err := m.Client.Create(ctx, rev); err != nil {
238254
return err
239255
}
256+
257+
// Re-fetch to get server-managed fields like Generation
258+
if err := m.Client.Get(ctx, client.ObjectKeyFromObject(rev), rev); err != nil {
259+
return fmt.Errorf("getting created revision: %w", err)
260+
}
261+
262+
// Set Available=Unknown so the revision controller will verify cluster state through probes.
263+
// During migration, ClusterExtension will briefly show as not installed until verification completes.
264+
meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
265+
Type: ocv1.ClusterExtensionRevisionTypeAvailable,
266+
Status: metav1.ConditionUnknown,
267+
Reason: ocv1.ClusterExtensionRevisionReasonMigrated,
268+
Message: "Migrated from Helm storage, awaiting cluster state verification",
269+
ObservedGeneration: rev.Generation,
270+
})
271+
272+
if err := m.Client.Status().Update(ctx, rev); err != nil {
273+
return fmt.Errorf("updating migrated revision status: %w", err)
274+
}
275+
240276
return nil
241277
}
242278

@@ -296,7 +332,6 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
296332
if len(existingRevisions) > 0 {
297333
// try first to update the current revision.
298334
currentRevision = &existingRevisions[len(existingRevisions)-1]
299-
desiredRevision.Spec.Previous = currentRevision.Spec.Previous
300335
desiredRevision.Spec.Revision = currentRevision.Spec.Revision
301336
desiredRevision.Name = currentRevision.Name
302337

@@ -346,8 +381,8 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
346381
desiredRevision.Name = fmt.Sprintf("%s-%d", ext.Name, revisionNumber)
347382
desiredRevision.Spec.Revision = revisionNumber
348383

349-
if err = bc.setPreviousRevisions(ctx, desiredRevision, prevRevisions); err != nil {
350-
return false, "", fmt.Errorf("garbage collecting old Revisions: %w", err)
384+
if err = bc.garbageCollectOldRevisions(ctx, prevRevisions); err != nil {
385+
return false, "", fmt.Errorf("garbage collecting old revisions: %w", err)
351386
}
352387

353388
if err := bc.createOrUpdate(ctx, desiredRevision); err != nil {
@@ -372,28 +407,21 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
372407
return true, "", nil
373408
}
374409

375-
// setPreviousRevisions populates spec.previous of latestRevision, trimming the list of previous _archived_ revisions down to
376-
// ClusterExtensionRevisionPreviousLimit or to the first _active_ revision and deletes trimmed revisions from the cluster.
377-
// NOTE: revisionList must be sorted in chronographical order, from oldest to latest.
378-
func (bc *Boxcutter) setPreviousRevisions(ctx context.Context, latestRevision *ocv1.ClusterExtensionRevision, revisionList []ocv1.ClusterExtensionRevision) error {
379-
// Pre-allocate with capacity limit to reduce allocations
380-
trimmedPrevious := make([]ocv1.ClusterExtensionRevisionPrevious, 0, ClusterExtensionRevisionPreviousLimit)
410+
// garbageCollectOldRevisions deletes archived revisions beyond ClusterExtensionRevisionRetentionLimit.
411+
// Active revisions are never deleted. revisionList must be sorted oldest to newest.
412+
func (bc *Boxcutter) garbageCollectOldRevisions(ctx context.Context, revisionList []ocv1.ClusterExtensionRevision) error {
381413
for index, r := range revisionList {
382-
if index < len(revisionList)-ClusterExtensionRevisionPreviousLimit && r.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived {
383-
// Delete oldest CREs from the cluster and list to reach ClusterExtensionRevisionPreviousLimit or latest active revision
414+
// Only delete archived revisions that are beyond the limit
415+
if index < len(revisionList)-ClusterExtensionRevisionRetentionLimit && r.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived {
384416
if err := bc.Client.Delete(ctx, &ocv1.ClusterExtensionRevision{
385417
ObjectMeta: metav1.ObjectMeta{
386418
Name: r.Name,
387419
},
388420
}); err != nil && !apierrors.IsNotFound(err) {
389-
return fmt.Errorf("deleting previous archived Revision: %w", err)
421+
return fmt.Errorf("deleting archived revision: %w", err)
390422
}
391-
} else {
392-
// All revisions within the limit or still active are preserved
393-
trimmedPrevious = append(trimmedPrevious, ocv1.ClusterExtensionRevisionPrevious{Name: r.Name, UID: r.GetUID()})
394423
}
395424
}
396-
latestRevision.Spec.Previous = trimmedPrevious
397425
return nil
398426
}
399427

0 commit comments

Comments
 (0)