Skip to content

Commit 48e05e1

Browse files
tmshortclaude
andcommitted
Fix Boxcutter 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 <noreply@anthropic.com> Signed-off-by: Todd Short <tshort@redhat.com>
1 parent 6ef62de commit 48e05e1

File tree

2 files changed

+620
-3
lines changed

2 files changed

+620
-3
lines changed

internal/operator-controller/applier/phase.go

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

33
import (
4+
"cmp"
5+
"slices"
6+
47
"k8s.io/apimachinery/pkg/runtime/schema"
58

69
ocv1 "github.com/operator-framework/operator-controller/api/v1"
@@ -111,6 +114,23 @@ func init() {
111114
}
112115
}
113116

117+
// Sort objects within the phase deterministically by Group, Version, Kind, Namespace, Name
118+
// to ensure consistent ordering regardless of input order. This is critical for
119+
// Helm-to-Boxcutter migration where the same resources may come from different sources
120+
// (Helm release manifest vs bundle manifest) and need to produce identical phases.
121+
func compareClusterExtensionRevisionObjects(a, b ocv1.ClusterExtensionRevisionObject) int {
122+
aGVK := a.Object.GroupVersionKind()
123+
bGVK := b.Object.GroupVersionKind()
124+
125+
return cmp.Or(
126+
cmp.Compare(aGVK.Group, bGVK.Group),
127+
cmp.Compare(aGVK.Version, bGVK.Version),
128+
cmp.Compare(aGVK.Kind, bGVK.Kind),
129+
cmp.Compare(a.Object.GetNamespace(), b.Object.GetNamespace()),
130+
cmp.Compare(a.Object.GetName(), b.Object.GetName()),
131+
)
132+
}
133+
114134
// PhaseSort takes an unsorted list of objects and organizes them into sorted phases.
115135
// Each phase will be applied in order according to DefaultPhaseOrder. Objects
116136
// within a single phase are applied simultaneously.
@@ -125,6 +145,9 @@ func PhaseSort(unsortedObjs []ocv1.ClusterExtensionRevisionObject) []ocv1.Cluste
125145

126146
for _, phaseName := range defaultPhaseOrder {
127147
if objs, ok := phaseMap[phaseName]; ok {
148+
// Sort objects within the phase deterministically
149+
slices.SortFunc(objs, compareClusterExtensionRevisionObjects)
150+
128151
phasesSorted = append(phasesSorted, ocv1.ClusterExtensionRevisionPhase{
129152
Name: string(phaseName),
130153
Objects: objs,

0 commit comments

Comments
 (0)