From 48e05e132461559549dcb776efe12a2571bb446c Mon Sep 17 00:00:00 2001 From: Todd Short Date: Tue, 11 Nov 2025 15:28:34 -0500 Subject: [PATCH] Fix Boxcutter manifest ordering inconsistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue: Manifest ordering inconsistency: CRDs from Helm release manifest and bundle manifest appeared in different orders, causing PhaseSort to produce different phase structures even though they contained the same objects. Solution: Added deterministic sorting in PhaseSort (phase.go): - Sort objects within each phase by Group, Version, Kind, Namespace, Name - Ensures consistent phase structure regardless of input order - Critical for comparing revisions from different sources 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Signed-off-by: Todd Short --- internal/operator-controller/applier/phase.go | 23 + .../operator-controller/applier/phase_test.go | 600 +++++++++++++++++- 2 files changed, 620 insertions(+), 3 deletions(-) diff --git a/internal/operator-controller/applier/phase.go b/internal/operator-controller/applier/phase.go index 9ae31db6a7..6baa396cfc 100644 --- a/internal/operator-controller/applier/phase.go +++ b/internal/operator-controller/applier/phase.go @@ -1,6 +1,9 @@ package applier import ( + "cmp" + "slices" + "k8s.io/apimachinery/pkg/runtime/schema" ocv1 "github.com/operator-framework/operator-controller/api/v1" @@ -111,6 +114,23 @@ func init() { } } +// Sort objects within the phase deterministically by Group, Version, Kind, Namespace, Name +// to ensure consistent ordering regardless of input order. This is critical for +// Helm-to-Boxcutter migration where the same resources may come from different sources +// (Helm release manifest vs bundle manifest) and need to produce identical phases. +func compareClusterExtensionRevisionObjects(a, b ocv1.ClusterExtensionRevisionObject) int { + aGVK := a.Object.GroupVersionKind() + bGVK := b.Object.GroupVersionKind() + + return cmp.Or( + cmp.Compare(aGVK.Group, bGVK.Group), + cmp.Compare(aGVK.Version, bGVK.Version), + cmp.Compare(aGVK.Kind, bGVK.Kind), + cmp.Compare(a.Object.GetNamespace(), b.Object.GetNamespace()), + cmp.Compare(a.Object.GetName(), b.Object.GetName()), + ) +} + // PhaseSort takes an unsorted list of objects and organizes them into sorted phases. // Each phase will be applied in order according to DefaultPhaseOrder. Objects // within a single phase are applied simultaneously. @@ -125,6 +145,9 @@ func PhaseSort(unsortedObjs []ocv1.ClusterExtensionRevisionObject) []ocv1.Cluste for _, phaseName := range defaultPhaseOrder { if objs, ok := phaseMap[phaseName]; ok { + // Sort objects within the phase deterministically + slices.SortFunc(objs, compareClusterExtensionRevisionObjects) + phasesSorted = append(phasesSorted, ocv1.ClusterExtensionRevisionPhase{ Name: string(phaseName), Objects: objs, diff --git a/internal/operator-controller/applier/phase_test.go b/internal/operator-controller/applier/phase_test.go index 3f2d85d0b1..6c0fe8fb32 100644 --- a/internal/operator-controller/applier/phase_test.go +++ b/internal/operator-controller/applier/phase_test.go @@ -259,6 +259,14 @@ func Test_PhaseSort(t *testing.T) { { Name: string(applier.PhaseDeploy), Objects: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + }, + }, + }, { Object: unstructured.Unstructured{ Object: map[string]interface{}{ @@ -267,11 +275,64 @@ func Test_PhaseSort(t *testing.T) { }, }, }, + }, + }, + }, + }, + { + name: "no objects", + objs: []v1.ClusterExtensionRevisionObject{}, + want: []v1.ClusterExtensionRevisionPhase{}, + }, + { + name: "sort by group within same phase", + objs: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "test", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "test", + }, + }, + }, + }, + }, + want: []v1.ClusterExtensionRevisionPhase{ + { + Name: string(applier.PhaseDeploy), + Objects: []v1.ClusterExtensionRevisionObject{ { Object: unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "test", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "test", + }, }, }, }, @@ -280,9 +341,542 @@ func Test_PhaseSort(t *testing.T) { }, }, { - name: "no objects", - objs: []v1.ClusterExtensionRevisionObject{}, - want: []v1.ClusterExtensionRevisionPhase{}, + name: "sort by version within same group and phase", + objs: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "batch/v1", + "kind": "Job", + "metadata": map[string]interface{}{ + "name": "test", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "batch/v1beta1", + "kind": "CronJob", + "metadata": map[string]interface{}{ + "name": "test", + }, + }, + }, + }, + }, + want: []v1.ClusterExtensionRevisionPhase{ + { + Name: string(applier.PhaseDeploy), + Objects: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "batch/v1", + "kind": "Job", + "metadata": map[string]interface{}{ + "name": "test", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "batch/v1beta1", + "kind": "CronJob", + "metadata": map[string]interface{}{ + "name": "test", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "sort by kind within same group, version, and phase", + objs: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Service", + "metadata": map[string]interface{}{ + "name": "test", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "test", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "name": "test", + }, + }, + }, + }, + }, + want: []v1.ClusterExtensionRevisionPhase{ + { + Name: string(applier.PhaseDeploy), + Objects: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "test", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "name": "test", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Service", + "metadata": map[string]interface{}{ + "name": "test", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "sort by namespace within same GVK and phase", + objs: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "test", + "namespace": "zebra", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "test", + "namespace": "alpha", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "test", + "namespace": "beta", + }, + }, + }, + }, + }, + want: []v1.ClusterExtensionRevisionPhase{ + { + Name: string(applier.PhaseDeploy), + Objects: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "test", + "namespace": "alpha", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "test", + "namespace": "beta", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "test", + "namespace": "zebra", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "sort by name within same GVK, namespace, and phase", + objs: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "zoo", + "namespace": "default", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "apple", + "namespace": "default", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "banana", + "namespace": "default", + }, + }, + }, + }, + }, + want: []v1.ClusterExtensionRevisionPhase{ + { + Name: string(applier.PhaseDeploy), + Objects: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "apple", + "namespace": "default", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "banana", + "namespace": "default", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "zoo", + "namespace": "default", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "comprehensive sorting - all dimensions", + objs: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "app-z", + "namespace": "prod", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "name": "secret-b", + "namespace": "prod", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "name": "secret-a", + "namespace": "prod", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "config", + "namespace": "dev", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "app-a", + "namespace": "prod", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "config", + "namespace": "prod", + }, + }, + }, + }, + }, + want: []v1.ClusterExtensionRevisionPhase{ + { + Name: string(applier.PhaseDeploy), + Objects: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "config", + "namespace": "dev", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "config", + "namespace": "prod", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "name": "secret-a", + "namespace": "prod", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "name": "secret-b", + "namespace": "prod", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "app-a", + "namespace": "prod", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "app-z", + "namespace": "prod", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "cluster-scoped vs namespaced resources - empty namespace sorts first", + objs: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": map[string]interface{}{ + "name": "admin", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": map[string]interface{}{ + "name": "viewer", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": map[string]interface{}{ + "name": "admin", + "namespace": "default", + }, + }, + }, + }, + }, + want: []v1.ClusterExtensionRevisionPhase{ + { + Name: string(applier.PhaseRBAC), + Objects: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": map[string]interface{}{ + "name": "admin", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": map[string]interface{}{ + "name": "viewer", + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": map[string]interface{}{ + "name": "admin", + "namespace": "default", + }, + }, + }, + }, + }, + }, + }, }, } { t.Run(tt.name, func(t *testing.T) {