-
Notifications
You must be signed in to change notification settings - Fork 68
✨ (Valid for Boxcutter Runtime Only) switched to label-based revision lookups — faster, safer, and no orphans left behind. #2315
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -84,6 +84,9 @@ func (r *SimpleRevisionGenerator) GenerateRevisionFromHelmRelease( | |
| }) | ||
| rev.Name = fmt.Sprintf("%s-1", ext.Name) | ||
| rev.Spec.Revision = 1 | ||
| // Explicitly set LifecycleState to Active to ensure migrated revision is recognized as active | ||
| // even before CRD default is applied by the API server | ||
| rev.Spec.LifecycleState = ocv1.ClusterExtensionRevisionLifecycleStateActive | ||
| return rev, nil | ||
| } | ||
|
|
||
|
|
@@ -140,30 +143,57 @@ func (r *SimpleRevisionGenerator) buildClusterExtensionRevision( | |
| ext *ocv1.ClusterExtension, | ||
| annotations map[string]string, | ||
| ) *ocv1.ClusterExtensionRevision { | ||
| revisionLabels := map[string]string{ | ||
| controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, | ||
| } | ||
| // Package name must be in labels for BoxcutterRevisionStatesGetter to find it | ||
| if pkg, ok := annotations[labels.PackageNameKey]; ok { | ||
| revisionLabels[labels.PackageNameKey] = pkg | ||
| } | ||
|
|
||
| return &ocv1.ClusterExtensionRevision{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Annotations: annotations, | ||
| Labels: map[string]string{ | ||
| controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, | ||
| }, | ||
| Labels: revisionLabels, | ||
| }, | ||
| Spec: ocv1.ClusterExtensionRevisionSpec{ | ||
| Phases: PhaseSort(objects), | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // BoxcutterStorageMigrator migrates ClusterExtensions from Helm-based storage to Boxcutter's | ||
| // ClusterExtensionRevision storage. | ||
| // | ||
| // Scenario: Given a ClusterExtension, determine if migration from Helm storage is needed | ||
| // - When ClusterExtensionRevisions already exist, no migration needed (return early) | ||
| // - When a Helm release exists but no revisions, create ClusterExtensionRevision from Helm release | ||
| // - When no Helm release exists and no revisions, this is a fresh install (no migration needed) | ||
| // | ||
| // The migrated revision includes: | ||
| // - Owner label (olm.operatorframework.io/owner) for label-based lookups | ||
| // - OwnerReference to parent ClusterExtension for proper garbage collection | ||
| // - All objects from the Helm release manifest | ||
| // | ||
| // This ensures migrated revisions have the same structure as revisions created by normal | ||
| // Boxcutter flow, enabling successful upgrades after migration. | ||
| type BoxcutterStorageMigrator struct { | ||
| ActionClientGetter helmclient.ActionClientGetter | ||
| RevisionGenerator ClusterExtensionRevisionGenerator | ||
| Client boxcutterStorageMigratorClient | ||
| Scheme *runtime.Scheme // Required for setting ownerReferences on migrated revisions | ||
| } | ||
|
|
||
| type boxcutterStorageMigratorClient interface { | ||
| List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error | ||
| Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error | ||
| Status() client.StatusWriter | ||
| } | ||
|
|
||
| // Migrate performs a one-time migration from Helm storage to ClusterExtensionRevision storage. | ||
| // | ||
| // This enables upgrades from older operator-controller versions that used Helm for state management. | ||
| // The migration is idempotent - it checks for existing revisions before attempting to migrate. | ||
| func (m *BoxcutterStorageMigrator) Migrate(ctx context.Context, ext *ocv1.ClusterExtension, objectLabels map[string]string) error { | ||
| existingRevisionList := ocv1.ClusterExtensionRevisionList{} | ||
| if err := m.Client.List(ctx, &existingRevisionList, client.MatchingLabels{ | ||
|
|
@@ -195,9 +225,39 @@ func (m *BoxcutterStorageMigrator) Migrate(ctx context.Context, ext *ocv1.Cluste | |
| return err | ||
| } | ||
|
|
||
| // Set ownerReference to ensure proper lifecycle management and garbage collection. | ||
| // This makes the migrated revision consistent with revisions created by normal Boxcutter flow. | ||
| if err := controllerutil.SetControllerReference(ext, rev, m.Scheme); err != nil { | ||
| return fmt.Errorf("set ownerref: %w", err) | ||
| } | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @tmshort That should address the JIRA you added: https://issues.redhat.com/browse/OPRUN-4038 Once this PR is merged, I plan to extend the test coverage to validate the orphan scenario, ensuring we’re not leaving any orphans behind. If we identify any, we’ll handle them properly in a follow-up PR. |
||
|
|
||
| if err := m.Client.Create(ctx, rev); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Mark the migrated revision as succeeded since the Helm release was already deployed. | ||
| // Without Succeeded=True, BoxcutterRevisionStatesGetter treats it as RollingOut instead of Installed, | ||
| // which breaks resolution and prevents upgrades. | ||
| // Note: After Create(), rev.Generation is populated by the API server (typically 1 for new objects) | ||
| meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ | ||
| Type: ocv1.ClusterExtensionRevisionTypeAvailable, | ||
| Status: metav1.ConditionTrue, | ||
| Reason: ocv1.ClusterExtensionRevisionReasonAvailable, | ||
| Message: "Migrated from Helm storage", | ||
| ObservedGeneration: rev.Generation, | ||
| }) | ||
| meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ | ||
| Type: ocv1.ClusterExtensionRevisionTypeSucceeded, | ||
| Status: metav1.ConditionTrue, | ||
| Reason: ocv1.ClusterExtensionRevisionReasonRolloutSuccess, | ||
| Message: "Migrated from Helm storage", | ||
| ObservedGeneration: rev.Generation, | ||
camilamacedo86 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }) | ||
|
|
||
| if err := m.Client.Status().Update(ctx, rev); err != nil { | ||
| return fmt.Errorf("updating migrated revision status: %w", err) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
|
|
@@ -270,6 +330,12 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust | |
| case err == nil: | ||
| // inplace patch was successful, no changes in phases | ||
| state = StateUnchanged | ||
| // Re-fetch the revision to get updated metadata and status after the patch | ||
| updated := &ocv1.ClusterExtensionRevision{} | ||
| if err := bc.Client.Get(ctx, client.ObjectKey{Name: currentRevision.Name}, updated); err != nil { | ||
| return false, "", fmt.Errorf("fetching updated revision: %w", err) | ||
| } | ||
| currentRevision = updated | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After in-place patch, |
||
| default: | ||
| return false, "", fmt.Errorf("patching %s Revision: %w", desiredRevision.Name, err) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why:
Packagefromrev.Labels["package-name"]in BoxcutterRevisionStatesGetter