@@ -32,7 +32,7 @@ import (
3232)
3333
3434const (
35- ClusterExtensionRevisionPreviousLimit = 5
35+ ClusterExtensionRevisionRetentionLimit = 5
3636)
3737
3838type ClusterExtensionRevisionGenerator interface {
@@ -93,7 +93,7 @@ func (r *SimpleRevisionGenerator) GenerateRevisionFromHelmRelease(
9393func (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
185185func (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.
203213type BoxcutterStorageMigrator struct {
204214 ActionClientGetter helmclient.ActionClientGetter
205215 RevisionGenerator ClusterExtensionRevisionGenerator
206216 Client boxcutterStorageMigratorClient
217+ Scheme * runtime.Scheme
207218}
208219
209220type 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.
214229func (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
263304func (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
@@ -317,6 +357,14 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
317357 case err == nil :
318358 // inplace patch was successful, no changes in phases
319359 state = StateUnchanged
360+ // Re-fetch the revision to ensure we have the latest labels (storage metadata) and
361+ // server-managed fields (resourceVersion, generation) after the in-place patch.
362+ // This is important for subsequent logic that may depend on these updated values.
363+ updated := & ocv1.ClusterExtensionRevision {}
364+ if err := bc .Client .Get (ctx , client.ObjectKey {Name : currentRevision .Name }, updated ); err != nil {
365+ return false , "" , fmt .Errorf ("fetching updated revision: %w" , err )
366+ }
367+ currentRevision = updated
320368 default :
321369 return false , "" , fmt .Errorf ("patching %s Revision: %w" , desiredRevision .Name , err )
322370 }
@@ -354,8 +402,8 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
354402 desiredRevision .Name = fmt .Sprintf ("%s-%d" , ext .Name , revisionNumber )
355403 desiredRevision .Spec .Revision = revisionNumber
356404
357- if err = bc .setPreviousRevisions (ctx , desiredRevision , prevRevisions ); err != nil {
358- return false , "" , fmt .Errorf ("garbage collecting old Revisions : %w" , err )
405+ if err = bc .garbageCollectOldRevisions (ctx , prevRevisions ); err != nil {
406+ return false , "" , fmt .Errorf ("garbage collecting old revisions : %w" , err )
359407 }
360408
361409 if err := bc .createOrUpdate (ctx , desiredRevision ); err != nil {
@@ -380,28 +428,21 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
380428 return true , "" , nil
381429}
382430
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 )
431+ // garbageCollectOldRevisions deletes archived revisions beyond ClusterExtensionRevisionRetentionLimit.
432+ // Active revisions are never deleted. revisionList must be sorted oldest to newest.
433+ func (bc * Boxcutter ) garbageCollectOldRevisions (ctx context.Context , revisionList []ocv1.ClusterExtensionRevision ) error {
389434 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
435+ // Only delete archived revisions that are beyond the limit
436+ if index < len ( revisionList ) - ClusterExtensionRevisionRetentionLimit && r . Spec . LifecycleState == ocv1 . ClusterExtensionRevisionLifecycleStateArchived {
392437 if err := bc .Client .Delete (ctx , & ocv1.ClusterExtensionRevision {
393438 ObjectMeta : metav1.ObjectMeta {
394439 Name : r .Name ,
395440 },
396441 }); err != nil && ! apierrors .IsNotFound (err ) {
397- return fmt .Errorf ("deleting previous archived Revision : %w" , err )
442+ return fmt .Errorf ("deleting archived revision : %w" , err )
398443 }
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 ()})
402444 }
403445 }
404- latestRevision .Spec .Previous = trimmedPrevious
405446 return nil
406447}
407448
0 commit comments