Skip to content

Commit 5161d30

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 cbdb84c commit 5161d30

File tree

13 files changed

+565
-174
lines changed

13 files changed

+565
-174
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
@@ -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
}

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: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import (
3232
)
3333

3434
const (
35-
ClusterExtensionRevisionPreviousLimit = 5
35+
ClusterExtensionRevisionRetentionLimit = 5
3636
)
3737

3838
type ClusterExtensionRevisionGenerator interface {
@@ -195,22 +195,33 @@ func (r *SimpleRevisionGenerator) buildClusterExtensionRevision(
195195
},
196196
},
197197
Spec: ocv1.ClusterExtensionRevisionSpec{
198-
Phases: PhaseSort(objects),
198+
// Explicitly set LifecycleState to Active. While the CRD has a default,
199+
// being explicit here ensures all code paths are clear and doesn't rely
200+
// on API server defaulting behavior.
201+
LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateActive,
202+
Phases: PhaseSort(objects),
199203
},
200204
}
201205
}
202206

207+
// BoxcutterStorageMigrator migrates ClusterExtensions from Helm-based storage to
208+
// ClusterExtensionRevision storage, enabling upgrades from older operator-controller versions.
203209
type BoxcutterStorageMigrator struct {
204210
ActionClientGetter helmclient.ActionClientGetter
205211
RevisionGenerator ClusterExtensionRevisionGenerator
206212
Client boxcutterStorageMigratorClient
213+
Scheme *runtime.Scheme
207214
}
208215

209216
type boxcutterStorageMigratorClient interface {
210217
List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error
218+
Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error
211219
Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error
220+
Status() client.StatusWriter
212221
}
213222

223+
// Migrate creates a ClusterExtensionRevision from an existing Helm release if no revisions exist yet.
224+
// The migration is idempotent and skipped if revisions already exist or no Helm release is found.
214225
func (m *BoxcutterStorageMigrator) Migrate(ctx context.Context, ext *ocv1.ClusterExtension, objectLabels map[string]string) error {
215226
existingRevisionList := ocv1.ClusterExtensionRevisionList{}
216227
if err := m.Client.List(ctx, &existingRevisionList, client.MatchingLabels{
@@ -242,9 +253,29 @@ func (m *BoxcutterStorageMigrator) Migrate(ctx context.Context, ext *ocv1.Cluste
242253
return err
243254
}
244255

256+
// Set ownerReference for proper garbage collection when the ClusterExtension is deleted.
257+
if err := controllerutil.SetControllerReference(ext, rev, m.Scheme); err != nil {
258+
return fmt.Errorf("set ownerref: %w", err)
259+
}
260+
245261
if err := m.Client.Create(ctx, rev); err != nil {
246262
return err
247263
}
264+
265+
// Set Available=Unknown so the revision controller will verify cluster state through probes.
266+
// During migration, ClusterExtension will briefly show as not installed until verification completes.
267+
meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
268+
Type: ocv1.ClusterExtensionRevisionTypeAvailable,
269+
Status: metav1.ConditionUnknown,
270+
Reason: ocv1.ClusterExtensionRevisionReasonMigrated,
271+
Message: "Migrated from Helm storage, awaiting cluster state verification",
272+
ObservedGeneration: rev.Generation,
273+
})
274+
275+
if err := m.Client.Status().Update(ctx, rev); err != nil {
276+
return fmt.Errorf("updating migrated revision status: %w", err)
277+
}
278+
248279
return nil
249280
}
250281

@@ -304,7 +335,6 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
304335
if len(existingRevisions) > 0 {
305336
// try first to update the current revision.
306337
currentRevision = &existingRevisions[len(existingRevisions)-1]
307-
desiredRevision.Spec.Previous = currentRevision.Spec.Previous
308338
desiredRevision.Spec.Revision = currentRevision.Spec.Revision
309339
desiredRevision.Name = currentRevision.Name
310340

@@ -354,8 +384,8 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
354384
desiredRevision.Name = fmt.Sprintf("%s-%d", ext.Name, revisionNumber)
355385
desiredRevision.Spec.Revision = revisionNumber
356386

357-
if err = bc.setPreviousRevisions(ctx, desiredRevision, prevRevisions); err != nil {
358-
return false, "", fmt.Errorf("garbage collecting old Revisions: %w", err)
387+
if err = bc.garbageCollectOldRevisions(ctx, prevRevisions); err != nil {
388+
return false, "", fmt.Errorf("garbage collecting old revisions: %w", err)
359389
}
360390

361391
if err := bc.createOrUpdate(ctx, desiredRevision); err != nil {
@@ -380,28 +410,21 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
380410
return true, "", nil
381411
}
382412

383-
// setPreviousRevisions populates spec.previous of latestRevision, trimming the list of previous _archived_ revisions down to
384-
// ClusterExtensionRevisionPreviousLimit or to the first _active_ revision and deletes trimmed revisions from the cluster.
385-
// NOTE: revisionList must be sorted in chronographical order, from oldest to latest.
386-
func (bc *Boxcutter) setPreviousRevisions(ctx context.Context, latestRevision *ocv1.ClusterExtensionRevision, revisionList []ocv1.ClusterExtensionRevision) error {
387-
// Pre-allocate with capacity limit to reduce allocations
388-
trimmedPrevious := make([]ocv1.ClusterExtensionRevisionPrevious, 0, ClusterExtensionRevisionPreviousLimit)
413+
// garbageCollectOldRevisions deletes archived revisions beyond ClusterExtensionRevisionRetentionLimit.
414+
// Active revisions are never deleted. revisionList must be sorted oldest to newest.
415+
func (bc *Boxcutter) garbageCollectOldRevisions(ctx context.Context, revisionList []ocv1.ClusterExtensionRevision) error {
389416
for index, r := range revisionList {
390-
if index < len(revisionList)-ClusterExtensionRevisionPreviousLimit && r.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived {
391-
// Delete oldest CREs from the cluster and list to reach ClusterExtensionRevisionPreviousLimit or latest active revision
417+
// Only delete archived revisions that are beyond the limit
418+
if index < len(revisionList)-ClusterExtensionRevisionRetentionLimit && r.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived {
392419
if err := bc.Client.Delete(ctx, &ocv1.ClusterExtensionRevision{
393420
ObjectMeta: metav1.ObjectMeta{
394421
Name: r.Name,
395422
},
396423
}); err != nil && !apierrors.IsNotFound(err) {
397-
return fmt.Errorf("deleting previous archived Revision: %w", err)
424+
return fmt.Errorf("deleting archived revision: %w", err)
398425
}
399-
} else {
400-
// All revisions within the limit or still active are preserved
401-
trimmedPrevious = append(trimmedPrevious, ocv1.ClusterExtensionRevisionPrevious{Name: r.Name, UID: r.GetUID()})
402426
}
403427
}
404-
latestRevision.Spec.Previous = trimmedPrevious
405428
return nil
406429
}
407430

0 commit comments

Comments
 (0)