-
Notifications
You must be signed in to change notification settings - Fork 73
🐛 OCPBUGS-78455: fix(Boxcutter-Collision): Fix collision detection bypass after ClusterExtension upgrade #2578
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 | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -142,7 +142,7 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, cer | |||||||||||||||||||||||
| return c.delete(ctx, cer) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| phases, opts, err := c.buildBoxcutterPhases(ctx, cer) | ||||||||||||||||||||||||
| phases, opts, siblingRevisionNames, err := c.buildBoxcutterPhases(ctx, cer) | ||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||
| setRetryingConditions(cer, err.Error()) | ||||||||||||||||||||||||
| return ctrl.Result{}, fmt.Errorf("converting to boxcutter revision: %v", err) | ||||||||||||||||||||||||
|
|
@@ -210,6 +210,11 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, cer | |||||||||||||||||||||||
| if ores.Action() == machinery.ActionCollision { | ||||||||||||||||||||||||
| collidingObjs = append(collidingObjs, ores.String()) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| if ores.Action() == machinery.ActionProgressed { | ||||||||||||||||||||||||
| if controllerRef := getForeignRevisionController(ores.Object(), siblingRevisionNames); controllerRef != nil { | ||||||||||||||||||||||||
| collidingObjs = append(collidingObjs, ores.String()+fmt.Sprintf("Conflicting Owner: %s\n", controllerRef.String())) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
| collidingObjs = append(collidingObjs, ores.String()+fmt.Sprintf("Conflicting Owner: %s\n", controllerRef.String())) | |
| collidingObjs = append(collidingObjs, ores.String()+fmt.Sprintf("\nConflicting Owner: %s", controllerRef.String())) |
Copilot
AI
Mar 20, 2026
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.
getForeignRevisionController returns &ref where ref is a copy of the slice element. This forces an escape allocation and can be confusing to readers expecting a pointer into the original ownerReferences slice. Returning &refs[i] (or copying the fields you need into a new struct explicitly) would be clearer and avoid the extra allocation.
| ref := refs[i] | |
| if ref.Controller != nil && *ref.Controller && | |
| ref.Kind == ocv1.ClusterExtensionRevisionKind && | |
| ref.APIVersion == ocv1.GroupVersion.String() { | |
| if !siblingRevisionNames.Has(ref.Name) { | |
| return &ref | |
| if refs[i].Controller != nil && *refs[i].Controller && | |
| refs[i].Kind == ocv1.ClusterExtensionRevisionKind && | |
| refs[i].APIVersion == ocv1.GroupVersion.String() { | |
| if !siblingRevisionNames.Has(refs[i].Name) { | |
| return &refs[i] |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -1111,7 +1111,7 @@ func newTestClusterExtensionRevision(t *testing.T, revisionName string, ext *ocv | |||||
| labels.ServiceAccountNamespaceKey: ext.Spec.Namespace, | ||||||
| }, | ||||||
| Labels: map[string]string{ | ||||||
| labels.OwnerNameKey: "test-ext", | ||||||
| labels.OwnerNameKey: ext.Name, | ||||||
| }, | ||||||
| }, | ||||||
| Spec: ocv1.ClusterExtensionRevisionSpec{ | ||||||
|
|
@@ -1344,6 +1344,258 @@ func (m *mockTrackingCache) Free(ctx context.Context, user client.Object) error | |||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| func Test_ClusterExtensionRevisionReconciler_Reconcile_ForeignRevisionCollision(t *testing.T) { | ||||||
| testScheme := newScheme(t) | ||||||
|
|
||||||
| for _, tc := range []struct { | ||||||
| name string | ||||||
| reconcilingRevisionName string | ||||||
| existingObjs func() []client.Object | ||||||
| revisionResult machinery.RevisionResult | ||||||
| expectCollision bool | ||||||
| }{ | ||||||
| { | ||||||
| name: "ActionProgressed with foreign controller ownerRef is treated as collision", | ||||||
| reconcilingRevisionName: "ext-B-1", | ||||||
| existingObjs: func() []client.Object { | ||||||
| extA := &ocv1.ClusterExtension{ | ||||||
| ObjectMeta: metav1.ObjectMeta{Name: "ext-A", UID: "ext-A-uid"}, | ||||||
| Spec: ocv1.ClusterExtensionSpec{ | ||||||
| Namespace: "ns-a", | ||||||
| ServiceAccount: ocv1.ServiceAccountReference{Name: "sa-a"}, | ||||||
| Source: ocv1.SourceConfig{ | ||||||
| SourceType: ocv1.SourceTypeCatalog, | ||||||
| Catalog: &ocv1.CatalogFilter{PackageName: "pkg"}, | ||||||
| }, | ||||||
| }, | ||||||
| } | ||||||
| extB := &ocv1.ClusterExtension{ | ||||||
| ObjectMeta: metav1.ObjectMeta{Name: "ext-B", UID: "ext-B-uid"}, | ||||||
| Spec: ocv1.ClusterExtensionSpec{ | ||||||
| Namespace: "ns-b", | ||||||
| ServiceAccount: ocv1.ServiceAccountReference{Name: "sa-b"}, | ||||||
| Source: ocv1.SourceConfig{ | ||||||
| SourceType: ocv1.SourceTypeCatalog, | ||||||
| Catalog: &ocv1.CatalogFilter{PackageName: "pkg"}, | ||||||
| }, | ||||||
| }, | ||||||
| } | ||||||
| // CER from ext-A's upgrade (revision 2) - this is the "foreign" owner | ||||||
| cerA2 := newTestClusterExtensionRevision(t, "ext-A-2", extA, testScheme) | ||||||
|
|
||||||
| // CER from ext-B (revision 1) - the one being reconciled | ||||||
| cerB1 := newTestClusterExtensionRevision(t, "ext-B-1", extB, testScheme) | ||||||
|
|
||||||
| return []client.Object{extA, extB, cerA2, cerB1} | ||||||
| }, | ||||||
| revisionResult: mockRevisionResult{ | ||||||
| phases: []machinery.PhaseResult{ | ||||||
| mockPhaseResult{ | ||||||
| name: "everything", | ||||||
| objects: []machinery.ObjectResult{ | ||||||
| mockObjectResult{ | ||||||
| action: machinery.ActionProgressed, | ||||||
| object: func() machinery.Object { | ||||||
| obj := &unstructured.Unstructured{ | ||||||
| Object: map[string]interface{}{ | ||||||
| "apiVersion": "apiextensions.k8s.io/v1", | ||||||
| "kind": "CustomResourceDefinition", | ||||||
| "metadata": map[string]interface{}{ | ||||||
| "name": "widgets.example.com", | ||||||
| "ownerReferences": []interface{}{ | ||||||
| map[string]interface{}{ | ||||||
| "apiVersion": ocv1.GroupVersion.String(), | ||||||
| "kind": ocv1.ClusterExtensionRevisionKind, | ||||||
| "name": "ext-A-2", | ||||||
| "uid": "ext-A-2", | ||||||
| "controller": true, | ||||||
| "blockOwnerDeletion": true, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| } | ||||||
| return obj | ||||||
| }(), | ||||||
| probes: machinerytypes.ProbeResultContainer{ | ||||||
| boxcutter.ProgressProbeType: { | ||||||
| Status: machinerytypes.ProbeStatusTrue, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| expectCollision: true, | ||||||
| }, | ||||||
| { | ||||||
| name: "ActionProgressed with sibling controller ownerRef is NOT a collision", | ||||||
| reconcilingRevisionName: "ext-A-1", | ||||||
| existingObjs: func() []client.Object { | ||||||
| extA := &ocv1.ClusterExtension{ | ||||||
| ObjectMeta: metav1.ObjectMeta{Name: "ext-A", UID: "ext-A-uid"}, | ||||||
| Spec: ocv1.ClusterExtensionSpec{ | ||||||
| Namespace: "ns-a", | ||||||
| ServiceAccount: ocv1.ServiceAccountReference{Name: "sa-a"}, | ||||||
| Source: ocv1.SourceConfig{ | ||||||
| SourceType: ocv1.SourceTypeCatalog, | ||||||
| Catalog: &ocv1.CatalogFilter{PackageName: "pkg"}, | ||||||
| }, | ||||||
| }, | ||||||
| } | ||||||
| // CER from ext-A revision 1 (being reconciled, older) | ||||||
| cerA1 := newTestClusterExtensionRevision(t, "ext-A-1", extA, testScheme) | ||||||
| // CER from ext-A revision 2 (newer, already progressed) | ||||||
| cerA2 := newTestClusterExtensionRevision(t, "ext-A-2", extA, testScheme) | ||||||
|
|
||||||
| return []client.Object{extA, cerA1, cerA2} | ||||||
| }, | ||||||
| revisionResult: mockRevisionResult{ | ||||||
| phases: []machinery.PhaseResult{ | ||||||
| mockPhaseResult{ | ||||||
| name: "everything", | ||||||
| objects: []machinery.ObjectResult{ | ||||||
| mockObjectResult{ | ||||||
| action: machinery.ActionProgressed, | ||||||
| object: func() machinery.Object { | ||||||
| obj := &unstructured.Unstructured{ | ||||||
| Object: map[string]interface{}{ | ||||||
| "apiVersion": "apiextensions.k8s.io/v1", | ||||||
| "kind": "CustomResourceDefinition", | ||||||
| "metadata": map[string]interface{}{ | ||||||
| "name": "widgets.example.com", | ||||||
| "ownerReferences": []interface{}{ | ||||||
| map[string]interface{}{ | ||||||
| "apiVersion": ocv1.GroupVersion.String(), | ||||||
| "kind": ocv1.ClusterExtensionRevisionKind, | ||||||
| "name": "ext-A-2", | ||||||
| "uid": "ext-A-2", | ||||||
| "controller": true, | ||||||
| "blockOwnerDeletion": true, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| } | ||||||
| return obj | ||||||
| }(), | ||||||
| probes: machinerytypes.ProbeResultContainer{ | ||||||
| boxcutter.ProgressProbeType: { | ||||||
| Status: machinerytypes.ProbeStatusTrue, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| expectCollision: false, | ||||||
| }, | ||||||
| { | ||||||
| name: "ActionProgressed with non-CER controller ownerRef is NOT a collision", | ||||||
| reconcilingRevisionName: "ext-B-1", | ||||||
| existingObjs: func() []client.Object { | ||||||
| extB := &ocv1.ClusterExtension{ | ||||||
| ObjectMeta: metav1.ObjectMeta{Name: "ext-B", UID: "ext-B-uid"}, | ||||||
| Spec: ocv1.ClusterExtensionSpec{ | ||||||
| Namespace: "ns-b", | ||||||
| ServiceAccount: ocv1.ServiceAccountReference{Name: "sa-b"}, | ||||||
| Source: ocv1.SourceConfig{ | ||||||
| SourceType: ocv1.SourceTypeCatalog, | ||||||
| Catalog: &ocv1.CatalogFilter{PackageName: "pkg"}, | ||||||
| }, | ||||||
| }, | ||||||
| } | ||||||
| cerB1 := newTestClusterExtensionRevision(t, "ext-B-1", extB, testScheme) | ||||||
|
|
||||||
| return []client.Object{extB, cerB1} | ||||||
| }, | ||||||
| revisionResult: mockRevisionResult{ | ||||||
| phases: []machinery.PhaseResult{ | ||||||
| mockPhaseResult{ | ||||||
| name: "everything", | ||||||
| objects: []machinery.ObjectResult{ | ||||||
| mockObjectResult{ | ||||||
| action: machinery.ActionProgressed, | ||||||
| object: func() machinery.Object { | ||||||
| obj := &unstructured.Unstructured{ | ||||||
| Object: map[string]interface{}{ | ||||||
| "apiVersion": "v1", | ||||||
| "kind": "ConfigMap", | ||||||
| "metadata": map[string]interface{}{ | ||||||
| "name": "some-cm", | ||||||
| "namespace": "default", | ||||||
| "ownerReferences": []interface{}{ | ||||||
| map[string]interface{}{ | ||||||
| "apiVersion": "apps/v1", | ||||||
| "kind": "Deployment", | ||||||
| "name": "some-deployment", | ||||||
| "uid": "deploy-uid", | ||||||
| "controller": true, | ||||||
| "blockOwnerDeletion": true, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| } | ||||||
| return obj | ||||||
| }(), | ||||||
| probes: machinerytypes.ProbeResultContainer{ | ||||||
| boxcutter.ProgressProbeType: { | ||||||
| Status: machinerytypes.ProbeStatusTrue, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| expectCollision: false, | ||||||
| }, | ||||||
| } { | ||||||
| t.Run(tc.name, func(t *testing.T) { | ||||||
| testClient := fake.NewClientBuilder(). | ||||||
| WithScheme(testScheme). | ||||||
| WithStatusSubresource(&ocv1.ClusterExtensionRevision{}). | ||||||
| WithObjects(tc.existingObjs()...). | ||||||
| Build() | ||||||
|
|
||||||
| mockEngine := &mockRevisionEngine{ | ||||||
| reconcile: func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionReconcileOption) (machinery.RevisionResult, error) { | ||||||
| return tc.revisionResult, nil | ||||||
| }, | ||||||
| } | ||||||
| result, err := (&controllers.ClusterExtensionRevisionReconciler{ | ||||||
| Client: testClient, | ||||||
| RevisionEngineFactory: &mockRevisionEngineFactory{engine: mockEngine}, | ||||||
| TrackingCache: &mockTrackingCache{client: testClient}, | ||||||
| }).Reconcile(t.Context(), ctrl.Request{ | ||||||
| NamespacedName: types.NamespacedName{ | ||||||
| Name: tc.reconcilingRevisionName, | ||||||
| }, | ||||||
| }) | ||||||
|
|
||||||
| if tc.expectCollision { | ||||||
| require.Equal(t, ctrl.Result{RequeueAfter: 10 * time.Second}, result) | ||||||
| require.NoError(t, err) | ||||||
|
|
||||||
| rev := &ocv1.ClusterExtensionRevision{} | ||||||
| require.NoError(t, testClient.Get(t.Context(), client.ObjectKey{Name: tc.reconcilingRevisionName}, rev)) | ||||||
| cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.TypeProgressing) | ||||||
|
||||||
| cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.TypeProgressing) | |
| cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing) |
Uh oh!
There was an error while loading. Please reload this page.