diff --git a/operators/constellation-node-operator/controllers/nodeversion_controller.go b/operators/constellation-node-operator/controllers/nodeversion_controller.go index ff706c702f..276448f14d 100644 --- a/operators/constellation-node-operator/controllers/nodeversion_controller.go +++ b/operators/constellation-node-operator/controllers/nodeversion_controller.go @@ -11,6 +11,7 @@ import ( "encoding/json" "errors" "reflect" + "slices" "strings" "time" @@ -388,6 +389,10 @@ func (r *NodeVersionReconciler) tryStartClusterVersionUpgrade(ctx context.Contex func (r *NodeVersionReconciler) pairDonorsAndHeirs(ctx context.Context, controller metav1.Object, outdatedNodes []corev1.Node, mintNodes []mintNode) []replacementPair { logr := log.FromContext(ctx) var pairs []replacementPair + + // Prioritize control-plane nodes, which need to be upgraded first starting with k8s v1.31.0. + sortByControlPlanes(outdatedNodes) + for _, mintNode := range mintNodes { var foundReplacement bool // find outdated node in the same group @@ -943,3 +948,16 @@ type newNodeConfig struct { scalingGroupByID map[string]updatev1alpha1.ScalingGroup newNodesBudget int } + +func sortByControlPlanes(nodes []corev1.Node) { + slices.SortStableFunc(nodes, func(a, b corev1.Node) int { + _, aControlPlane := a.Labels["node-role.kubernetes.io/control-plane"] + _, bControlPlane := b.Labels["node-role.kubernetes.io/control-plane"] + if aControlPlane == bControlPlane { + return 0 + } else if aControlPlane { + return -1 + } + return 1 + }) +} diff --git a/operators/constellation-node-operator/controllers/nodeversion_controller_test.go b/operators/constellation-node-operator/controllers/nodeversion_controller_test.go index c9ae880421..5146c4cdcf 100644 --- a/operators/constellation-node-operator/controllers/nodeversion_controller_test.go +++ b/operators/constellation-node-operator/controllers/nodeversion_controller_test.go @@ -9,6 +9,7 @@ package controllers import ( "context" "errors" + "fmt" "sync" "testing" @@ -891,3 +892,42 @@ func (*unimplementedNodeReplacer) CreateNode(_ context.Context, _ string) (nodeN func (*unimplementedNodeReplacer) DeleteNode(_ context.Context, _ string) error { panic("unimplemented") } + +func TestSortByControlPlane(t *testing.T) { + w1 := newNode("w1", false) + w2 := newNode("w2", false) + cp1 := newNode("cp1", true) + cp2 := newNode("cp2", true) + + for i, tc := range []struct { + input []corev1.Node + expected []string + }{ + {input: []corev1.Node{w1, w2, cp1, cp2}, expected: []string{"cp1", "cp2", "w1", "w2"}}, + {input: []corev1.Node{w2, cp1, cp2, w1}, expected: []string{"cp1", "cp2", "w2", "w1"}}, + {input: []corev1.Node{cp2, w1, cp1, w2}, expected: []string{"cp2", "cp1", "w1", "w2"}}, + {input: []corev1.Node{cp1, cp2, w1, w2}, expected: []string{"cp1", "cp2", "w1", "w2"}}, + } { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + sortByControlPlanes(tc.input) + var actual []string + for _, node := range tc.input { + actual = append(actual, node.Name) + } + assert.Equal(t, tc.expected, actual) + }) + } +} + +func newNode(name string, controlPlane bool) corev1.Node { + node := corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: make(map[string]string), + }, + } + if controlPlane { + node.Labels["node-role.kubernetes.io/control-plane"] = "" + } + return node +}