Skip to content

Commit ec218e0

Browse files
tmshortclaude
andcommitted
Fix manifest ordering inconsistency
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 <[email protected]> Signed-off-by: Todd Short <[email protected]>
1 parent 39cbdbe commit ec218e0

File tree

2 files changed

+60
-4
lines changed

2 files changed

+60
-4
lines changed

internal/operator-controller/applier/phase.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package applier
22

33
import (
4+
"slices"
5+
46
"k8s.io/apimachinery/pkg/runtime/schema"
57

68
ocv1 "github.com/operator-framework/operator-controller/api/v1"
@@ -111,6 +113,57 @@ func init() {
111113
}
112114
}
113115

116+
// Sort objects within the phase deterministically by Group, Version, Kind, Namespace, Name
117+
// to ensure consistent ordering regardless of input order. This is critical for
118+
// Helm-to-Boxcutter migration where the same resources may come from different sources
119+
// (Helm release manifest vs bundle manifest) and need to produce identical phases.
120+
func compareClusterExtensionRevisionObjects(a, b ocv1.ClusterExtensionRevisionObject) int {
121+
aGVK := a.Object.GroupVersionKind()
122+
bGVK := b.Object.GroupVersionKind()
123+
124+
// Compare Group
125+
if aGVK.Group < bGVK.Group {
126+
return -1
127+
} else if aGVK.Group > bGVK.Group {
128+
return 1
129+
}
130+
131+
// Compare Version
132+
if aGVK.Version < bGVK.Version {
133+
return -1
134+
} else if aGVK.Version > bGVK.Version {
135+
return 1
136+
}
137+
138+
// Compare Kind
139+
if aGVK.Kind < bGVK.Kind {
140+
return -1
141+
} else if aGVK.Kind > bGVK.Kind {
142+
return 1
143+
}
144+
145+
// Compare Namespace
146+
aNs := a.Object.GetNamespace()
147+
bNs := b.Object.GetNamespace()
148+
if aNs < bNs {
149+
return -1
150+
} else if aNs > bNs {
151+
return 1
152+
}
153+
154+
// Compare Name
155+
aName := a.Object.GetName()
156+
bName := b.Object.GetName()
157+
if aName < bName {
158+
return -1
159+
}
160+
if aName > bName {
161+
return 1
162+
}
163+
164+
return 0
165+
}
166+
114167
// PhaseSort takes an unsorted list of objects and organizes them into sorted phases.
115168
// Each phase will be applied in order according to DefaultPhaseOrder. Objects
116169
// within a single phase are applied simultaneously.
@@ -125,6 +178,9 @@ func PhaseSort(unsortedObjs []ocv1.ClusterExtensionRevisionObject) []ocv1.Cluste
125178

126179
for _, phaseName := range defaultPhaseOrder {
127180
if objs, ok := phaseMap[phaseName]; ok {
181+
// Sort objects within the phase deterministically
182+
slices.SortFunc(objs, compareClusterExtensionRevisionObjects)
183+
128184
phasesSorted = append(phasesSorted, ocv1.ClusterExtensionRevisionPhase{
129185
Name: string(phaseName),
130186
Objects: objs,

internal/operator-controller/applier/phase_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -262,16 +262,16 @@ func Test_PhaseSort(t *testing.T) {
262262
{
263263
Object: unstructured.Unstructured{
264264
Object: map[string]interface{}{
265-
"apiVersion": "apps/v1",
266-
"kind": "Deployment",
265+
"apiVersion": "v1",
266+
"kind": "ConfigMap",
267267
},
268268
},
269269
},
270270
{
271271
Object: unstructured.Unstructured{
272272
Object: map[string]interface{}{
273-
"apiVersion": "v1",
274-
"kind": "ConfigMap",
273+
"apiVersion": "apps/v1",
274+
"kind": "Deployment",
275275
},
276276
},
277277
},

0 commit comments

Comments
 (0)