Skip to content

Commit 0e2cacf

Browse files
committed
Sort AgentTolerations when creating agent Bundle (#4367)
* Sort AgentTolerations when creating agent Bundle This is a follow-up to: rancher/rancher#52755 , merged in Rancher recently and to avoid the problem when running Fleet standalone. It simply sorts the AgentTolerations when creating the agent Bundle so they don't differ and create later different _Content_ resources. --------- Signed-off-by: Xavi Garcia <[email protected]>
1 parent b52e708 commit 0e2cacf

File tree

2 files changed

+242
-0
lines changed

2 files changed

+242
-0
lines changed

internal/cmd/controller/agentmanagement/controllers/manageagent/manageagent.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"crypto/sha256"
1212
"encoding/json"
1313
"fmt"
14+
"sort"
1415

1516
"github.com/sirupsen/logrus"
1617

@@ -34,6 +35,46 @@ import (
3435
"k8s.io/utils/ptr"
3536
)
3637

38+
func sortTolerations(tols []corev1.Toleration) {
39+
sort.Slice(tols, func(i, j int) bool {
40+
a := tols[i]
41+
b := tols[j]
42+
43+
// 1. Key
44+
if a.Key != b.Key {
45+
return a.Key < b.Key
46+
}
47+
48+
// 2. Value
49+
if a.Value != b.Value {
50+
return a.Value < b.Value
51+
}
52+
53+
// 3. Operator
54+
if a.Operator != b.Operator {
55+
return a.Operator < b.Operator
56+
}
57+
58+
// 4. Effect
59+
if a.Effect != b.Effect {
60+
return a.Effect < b.Effect
61+
}
62+
63+
// 5. TolerationSeconds (nil-safe)
64+
if a.TolerationSeconds == nil && b.TolerationSeconds != nil {
65+
return true // nil sorts first
66+
}
67+
if a.TolerationSeconds != nil && b.TolerationSeconds == nil {
68+
return false
69+
}
70+
if a.TolerationSeconds == nil && b.TolerationSeconds == nil {
71+
return false
72+
}
73+
74+
return *a.TolerationSeconds < *b.TolerationSeconds
75+
})
76+
}
77+
3778
const (
3879
AgentBundleName = "fleet-agent"
3980
)
@@ -286,6 +327,10 @@ func (h *handler) newAgentBundle(ns string, cluster *fleet.Cluster) (runtime.Obj
286327
priorityClassName = scheduling.FleetAgentPriorityClassName
287328
}
288329

330+
if cluster.Spec.AgentTolerations != nil {
331+
sortTolerations(cluster.Spec.AgentTolerations)
332+
}
333+
289334
// Notice we only set the agentScope when it's a non-default agentNamespace. This is for backwards compatibility
290335
// for when we didn't have agent scope before
291336
objs := agent.Manifest(

internal/cmd/controller/agentmanagement/controllers/manageagent/manageagent_test.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
package manageagent
22

33
import (
4+
"reflect"
5+
"strings"
46
"testing"
57

68
"github.com/rancher/wrangler/v3/pkg/generic/fake"
9+
"github.com/rancher/wrangler/v3/pkg/schemes"
710
"go.uber.org/mock/gomock"
811
corev1 "k8s.io/api/core/v1"
912
"k8s.io/apimachinery/pkg/api/resource"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/apimachinery/pkg/runtime"
1015
"k8s.io/utils/ptr"
1116

1217
fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
18+
appsv1 "k8s.io/api/apps/v1"
19+
networkv1 "k8s.io/api/networking/v1"
20+
"sigs.k8s.io/yaml"
21+
22+
"github.com/rancher/fleet/internal/config"
1323
)
1424

1525
func TestOnClusterChangeAffinity(t *testing.T) {
@@ -107,6 +117,85 @@ func TestOnClusterChangeAffinity(t *testing.T) {
107117
}
108118
}
109119

120+
func TestNewAgentBundle_SortsAgentTolerations(t *testing.T) {
121+
// make sure config is set for newAgentBundle
122+
config.Set(config.DefaultConfig())
123+
124+
checkRegisterAddToScheme(t, appsv1.AddToScheme)
125+
checkRegisterAddToScheme(t, networkv1.AddToScheme)
126+
127+
h := &handler{systemNamespace: "fleet-system"}
128+
129+
unsorted := []corev1.Toleration{
130+
{Key: "b", Value: "2", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoExecute},
131+
{Key: "a", Value: "1", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule},
132+
{Key: "a", Value: "1", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoSchedule},
133+
{Key: "a", Value: "0", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoExecute},
134+
}
135+
136+
cluster := &fleet.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "c1"}, Spec: fleet.ClusterSpec{AgentTolerations: unsorted}}
137+
138+
wantUser := []corev1.Toleration{
139+
{Key: "a", Value: "0", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoExecute},
140+
{Key: "a", Value: "1", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule},
141+
{Key: "a", Value: "1", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoSchedule},
142+
{Key: "b", Value: "2", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoExecute},
143+
}
144+
145+
// ensure leader election env is set so NewLeaderElectionOptionsWithPrefix doesn't error
146+
t.Setenv("FLEET_AGENT_ELECTION_LEASE_DURATION", "15s")
147+
t.Setenv("FLEET_AGENT_ELECTION_RENEW_DEADLINE", "10s")
148+
t.Setenv("FLEET_AGENT_ELECTION_RETRY_PERIOD", "2s")
149+
150+
obj, err := h.newAgentBundle("ns", cluster)
151+
if err != nil {
152+
t.Fatalf("unexpected error from newAgentBundle: %v", err)
153+
}
154+
155+
b, ok := obj.(*fleet.Bundle)
156+
if !ok {
157+
t.Fatalf("expected bundle object, got %#v", obj)
158+
}
159+
160+
if len(b.Spec.Resources) == 0 {
161+
t.Fatalf("bundle resources empty")
162+
}
163+
164+
content := b.Spec.Resources[0].Content
165+
docs := strings.Split(content, "\n---\n")
166+
167+
var found bool
168+
for _, d := range docs {
169+
var m map[string]interface{}
170+
if err := yaml.Unmarshal([]byte(d), &m); err != nil {
171+
continue
172+
}
173+
if kind, _ := m["kind"].(string); kind == "Deployment" {
174+
dep := &appsv1.Deployment{}
175+
if err := yaml.Unmarshal([]byte(d), dep); err != nil {
176+
t.Fatalf("failed to unmarshal deployment: %v", err)
177+
}
178+
179+
wantFinal := []corev1.Toleration{
180+
{Key: "node.cloudprovider.kubernetes.io/uninitialized", Operator: corev1.TolerationOpEqual, Value: "true", Effect: corev1.TaintEffectNoSchedule},
181+
{Key: "cattle.io/os", Operator: corev1.TolerationOpEqual, Value: "linux", Effect: corev1.TaintEffectNoSchedule},
182+
}
183+
wantFinal = append(wantFinal, wantUser...)
184+
185+
if !reflect.DeepEqual(dep.Spec.Template.Spec.Tolerations, wantFinal) {
186+
t.Fatalf("deployment tolerations mismatch:\n got: %#v\n want: %#v", dep.Spec.Template.Spec.Tolerations, wantFinal)
187+
}
188+
189+
found = true
190+
break
191+
}
192+
}
193+
194+
if !found {
195+
t.Fatalf("no Deployment found in bundle yaml")
196+
}
197+
}
198+
110199
func TestOnClusterChangeResources(t *testing.T) {
111200
ctrl := gomock.NewController(t)
112201
namespaces := fake.NewMockNonNamespacedControllerInterface[*corev1.Namespace, *corev1.NamespaceList](ctrl)
@@ -328,3 +417,111 @@ func TestOnClusterChangeHostNetwork(t *testing.T) {
328417
})
329418
}
330419
}
420+
421+
// Table-driven tests covering all comparator fields used by sortTolerations
422+
func TestSortTolerations(t *testing.T) {
423+
five := int64(5)
424+
ten := int64(10)
425+
426+
tests := []struct {
427+
name string
428+
in []corev1.Toleration
429+
want []corev1.Toleration
430+
}{
431+
{
432+
name: "basic ordering",
433+
in: []corev1.Toleration{
434+
{Key: "b", Value: "2", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoExecute},
435+
{Key: "a", Value: "1", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule},
436+
{Key: "a", Value: "1", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoSchedule},
437+
{Key: "a", Value: "0", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoExecute},
438+
},
439+
want: []corev1.Toleration{
440+
{Key: "a", Value: "0", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoExecute},
441+
{Key: "a", Value: "1", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule},
442+
{Key: "a", Value: "1", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoSchedule},
443+
{Key: "b", Value: "2", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoExecute},
444+
},
445+
},
446+
{
447+
name: "toleration seconds nil first",
448+
in: []corev1.Toleration{
449+
{Key: "k", Value: "v", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule, TolerationSeconds: &ten},
450+
{Key: "k", Value: "v", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule}, // nil
451+
{Key: "k", Value: "v", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule, TolerationSeconds: &five},
452+
},
453+
want: []corev1.Toleration{
454+
{Key: "k", Value: "v", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule},
455+
{Key: "k", Value: "v", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule, TolerationSeconds: &five},
456+
{Key: "k", Value: "v", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule, TolerationSeconds: &ten},
457+
},
458+
},
459+
{
460+
name: "key ordering",
461+
in: []corev1.Toleration{
462+
{Key: "z", Value: "x", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule},
463+
{Key: "a", Value: "x", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule},
464+
},
465+
want: []corev1.Toleration{
466+
{Key: "a", Value: "x", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule},
467+
{Key: "z", Value: "x", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule},
468+
},
469+
},
470+
{
471+
name: "value ordering",
472+
in: []corev1.Toleration{
473+
{Key: "k", Value: "z", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule},
474+
{Key: "k", Value: "a", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule},
475+
},
476+
want: []corev1.Toleration{
477+
{Key: "k", Value: "a", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule},
478+
{Key: "k", Value: "z", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule},
479+
},
480+
},
481+
{
482+
name: "operator ordering",
483+
in: []corev1.Toleration{
484+
{Key: "k", Value: "v", Operator: "", Effect: corev1.TaintEffectNoSchedule},
485+
{Key: "k", Value: "v", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoSchedule},
486+
{Key: "k", Value: "v", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule},
487+
},
488+
want: []corev1.Toleration{
489+
{Key: "k", Value: "v", Operator: "", Effect: corev1.TaintEffectNoSchedule},
490+
{Key: "k", Value: "v", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule},
491+
{Key: "k", Value: "v", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoSchedule},
492+
},
493+
},
494+
{
495+
name: "effect ordering",
496+
in: []corev1.Toleration{
497+
{Key: "k", Value: "v", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule},
498+
{Key: "k", Value: "v", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoExecute},
499+
{Key: "k", Value: "v", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectPreferNoSchedule},
500+
},
501+
want: []corev1.Toleration{
502+
{Key: "k", Value: "v", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoExecute},
503+
{Key: "k", Value: "v", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule},
504+
{Key: "k", Value: "v", Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectPreferNoSchedule},
505+
},
506+
},
507+
}
508+
509+
for _, tt := range tests {
510+
t.Run(tt.name, func(t *testing.T) {
511+
inCopy := make([]corev1.Toleration, len(tt.in))
512+
copy(inCopy, tt.in)
513+
sortTolerations(inCopy)
514+
if !reflect.DeepEqual(inCopy, tt.want) {
515+
t.Fatalf("%s: got:\n%#v\nwant:\n%#v", tt.name, inCopy, tt.want)
516+
}
517+
})
518+
}
519+
}
520+
521+
func checkRegisterAddToScheme(t *testing.T, f func(*runtime.Scheme) error) {
522+
t.Helper()
523+
err := schemes.Register(f)
524+
if err != nil {
525+
t.Fatalf("failed to add to scheme: %v", err)
526+
}
527+
}

0 commit comments

Comments
 (0)