Skip to content

Commit 1aa1221

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 1aa1221

13 files changed

Lines changed: 605 additions & 199 deletions

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: 64 additions & 31 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 {
@@ -93,7 +93,7 @@ func (r *SimpleRevisionGenerator) GenerateRevisionFromHelmRelease(
9393
func (r *SimpleRevisionGenerator) GenerateRevision(
9494
ctx context.Context,
9595
bundleFS fs.FS, ext *ocv1.ClusterExtension,
96-
objectLabels, revisionAnnotations map[string]string,
96+
objectLabels, storageLabels map[string]string,
9797
) (*ocv1.ClusterExtensionRevision, error) {
9898
// extract plain manifests
9999
plain, err := r.ManifestProvider.Get(bundleFS, ext)
@@ -133,11 +133,11 @@ func (r *SimpleRevisionGenerator) GenerateRevision(
133133
})
134134
}
135135

136-
if revisionAnnotations == nil {
137-
revisionAnnotations = map[string]string{}
136+
if storageLabels == nil {
137+
storageLabels = map[string]string{}
138138
}
139139

140-
return r.buildClusterExtensionRevision(objs, ext, revisionAnnotations), nil
140+
return r.buildClusterExtensionRevision(objs, ext, storageLabels), nil
141141
}
142142

143143
// sanitizedUnstructured takes an unstructured obj, removes status if present, and returns a sanitized copy containing only the allowed metadata entries set below.
@@ -185,32 +185,47 @@ func sanitizedUnstructured(ctx context.Context, unstr *unstructured.Unstructured
185185
func (r *SimpleRevisionGenerator) buildClusterExtensionRevision(
186186
objects []ocv1.ClusterExtensionRevisionObject,
187187
ext *ocv1.ClusterExtension,
188-
annotations map[string]string,
188+
storageLabels map[string]string,
189189
) *ocv1.ClusterExtensionRevision {
190+
// Merge storage labels with owner label
191+
allLabels := make(map[string]string, len(storageLabels)+1)
192+
allLabels[controllers.ClusterExtensionRevisionOwnerLabel] = ext.Name
193+
for k, v := range storageLabels {
194+
allLabels[k] = v
195+
}
196+
190197
return &ocv1.ClusterExtensionRevision{
191198
ObjectMeta: metav1.ObjectMeta{
192-
Annotations: annotations,
193-
Labels: map[string]string{
194-
controllers.ClusterExtensionRevisionOwnerLabel: ext.Name,
195-
},
199+
Labels: allLabels,
196200
},
197201
Spec: ocv1.ClusterExtensionRevisionSpec{
198-
Phases: PhaseSort(objects),
202+
// Explicitly set LifecycleState to Active. While the CRD has a default,
203+
// being explicit here ensures all code paths are clear and doesn't rely
204+
// on API server defaulting behavior.
205+
LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateActive,
206+
Phases: PhaseSort(objects),
199207
},
200208
}
201209
}
202210

211+
// BoxcutterStorageMigrator migrates ClusterExtensions from Helm-based storage to
212+
// ClusterExtensionRevision storage, enabling upgrades from older operator-controller versions.
203213
type BoxcutterStorageMigrator struct {
204214
ActionClientGetter helmclient.ActionClientGetter
205215
RevisionGenerator ClusterExtensionRevisionGenerator
206216
Client boxcutterStorageMigratorClient
217+
Scheme *runtime.Scheme
207218
}
208219

209220
type boxcutterStorageMigratorClient interface {
210221
List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error
222+
Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error
211223
Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error
224+
Status() client.StatusWriter
212225
}
213226

227+
// Migrate creates a ClusterExtensionRevision from an existing Helm release if no revisions exist yet.
228+
// The migration is idempotent and skipped if revisions already exist or no Helm release is found.
214229
func (m *BoxcutterStorageMigrator) Migrate(ctx context.Context, ext *ocv1.ClusterExtension, objectLabels map[string]string) error {
215230
existingRevisionList := ocv1.ClusterExtensionRevisionList{}
216231
if err := m.Client.List(ctx, &existingRevisionList, client.MatchingLabels{
@@ -242,9 +257,35 @@ func (m *BoxcutterStorageMigrator) Migrate(ctx context.Context, ext *ocv1.Cluste
242257
return err
243258
}
244259

260+
// Set ownerReference for proper garbage collection when the ClusterExtension is deleted.
261+
if err := controllerutil.SetControllerReference(ext, rev, m.Scheme); err != nil {
262+
return fmt.Errorf("set ownerref: %w", err)
263+
}
264+
245265
if err := m.Client.Create(ctx, rev); err != nil {
246266
return err
247267
}
268+
269+
// Re-fetch the created revision to ensure we have the latest Generation from the API server.
270+
fetchedRev := &ocv1.ClusterExtensionRevision{}
271+
if err := m.Client.Get(ctx, client.ObjectKey{Name: rev.Name}, fetchedRev); err != nil {
272+
return fmt.Errorf("fetching created revision: %w", err)
273+
}
274+
275+
// Set Available=Unknown so the revision controller will verify cluster state through probes.
276+
// During migration, ClusterExtension will briefly show as not installed until verification completes.
277+
meta.SetStatusCondition(&fetchedRev.Status.Conditions, metav1.Condition{
278+
Type: ocv1.ClusterExtensionRevisionTypeAvailable,
279+
Status: metav1.ConditionUnknown,
280+
Reason: ocv1.ClusterExtensionRevisionReasonMigrated,
281+
Message: "Migrated from Helm storage, awaiting cluster state verification",
282+
ObservedGeneration: fetchedRev.Generation,
283+
})
284+
285+
if err := m.Client.Status().Update(ctx, fetchedRev); err != nil {
286+
return fmt.Errorf("updating migrated revision status: %w", err)
287+
}
288+
248289
return nil
249290
}
250291

@@ -256,8 +297,8 @@ type Boxcutter struct {
256297
FieldOwner string
257298
}
258299

259-
func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (bool, string, error) {
260-
return bc.apply(ctx, contentFS, ext, objectLabels, revisionAnnotations)
300+
func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, storageLabels map[string]string) (bool, string, error) {
301+
return bc.apply(ctx, contentFS, ext, objectLabels, storageLabels)
261302
}
262303

263304
func (bc *Boxcutter) getObjects(rev *ocv1.ClusterExtensionRevision) []client.Object {
@@ -281,9 +322,9 @@ func (bc *Boxcutter) createOrUpdate(ctx context.Context, obj client.Object) erro
281322
return bc.Client.Patch(ctx, obj, client.Apply, client.FieldOwner(bc.FieldOwner), client.ForceOwnership)
282323
}
283324

284-
func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (bool, string, error) {
325+
func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, storageLabels map[string]string) (bool, string, error) {
285326
// Generate desired revision
286-
desiredRevision, err := bc.RevisionGenerator.GenerateRevision(ctx, contentFS, ext, objectLabels, revisionAnnotations)
327+
desiredRevision, err := bc.RevisionGenerator.GenerateRevision(ctx, contentFS, ext, objectLabels, storageLabels)
287328
if err != nil {
288329
return false, "", err
289330
}
@@ -304,7 +345,6 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
304345
if len(existingRevisions) > 0 {
305346
// try first to update the current revision.
306347
currentRevision = &existingRevisions[len(existingRevisions)-1]
307-
desiredRevision.Spec.Previous = currentRevision.Spec.Previous
308348
desiredRevision.Spec.Revision = currentRevision.Spec.Revision
309349
desiredRevision.Name = currentRevision.Name
310350

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

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

361401
if err := bc.createOrUpdate(ctx, desiredRevision); err != nil {
@@ -380,28 +420,21 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
380420
return true, "", nil
381421
}
382422

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)
423+
// garbageCollectOldRevisions deletes archived revisions beyond ClusterExtensionRevisionRetentionLimit.
424+
// Active revisions are never deleted. revisionList must be sorted oldest to newest.
425+
func (bc *Boxcutter) garbageCollectOldRevisions(ctx context.Context, revisionList []ocv1.ClusterExtensionRevision) error {
389426
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
427+
// Only delete archived revisions that are beyond the limit
428+
if index < len(revisionList)-ClusterExtensionRevisionRetentionLimit && r.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived {
392429
if err := bc.Client.Delete(ctx, &ocv1.ClusterExtensionRevision{
393430
ObjectMeta: metav1.ObjectMeta{
394431
Name: r.Name,
395432
},
396433
}); err != nil && !apierrors.IsNotFound(err) {
397-
return fmt.Errorf("deleting previous archived Revision: %w", err)
434+
return fmt.Errorf("deleting archived revision: %w", err)
398435
}
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()})
402436
}
403437
}
404-
latestRevision.Spec.Previous = trimmedPrevious
405438
return nil
406439
}
407440

0 commit comments

Comments
 (0)