1
16
17 package taint
18
19 import (
20 "io"
21 "net/http"
22 "reflect"
23 "strings"
24 "testing"
25 "time"
26
27 corev1 "k8s.io/api/core/v1"
28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29 "k8s.io/apimachinery/pkg/runtime"
30 "k8s.io/apimachinery/pkg/util/strategicpatch"
31 "k8s.io/cli-runtime/pkg/genericiooptions"
32 "k8s.io/client-go/rest/fake"
33 cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
34 cmdutil "k8s.io/kubectl/pkg/cmd/util"
35 "k8s.io/kubectl/pkg/scheme"
36 )
37
38 func generateNodeAndTaintedNode(oldTaints []corev1.Taint, newTaints []corev1.Taint) (*corev1.Node, *corev1.Node) {
39 var taintedNode *corev1.Node
40
41
42 node := &corev1.Node{
43 ObjectMeta: metav1.ObjectMeta{
44 Name: "node-name",
45 CreationTimestamp: metav1.Time{Time: time.Now()},
46 },
47 Spec: corev1.NodeSpec{
48 Taints: oldTaints,
49 },
50 Status: corev1.NodeStatus{},
51 }
52
53
54 taintedNode = node.DeepCopy()
55 taintedNode.Spec.Taints = newTaints
56
57 return node, taintedNode
58 }
59
60 func equalTaints(taintsA, taintsB []corev1.Taint) bool {
61 if len(taintsA) != len(taintsB) {
62 return false
63 }
64
65 for _, taintA := range taintsA {
66 found := false
67 for _, taintB := range taintsB {
68 if reflect.DeepEqual(taintA, taintB) {
69 found = true
70 break
71 }
72 }
73 if !found {
74 return false
75 }
76 }
77 return true
78 }
79
80 func TestTaint(t *testing.T) {
81 tests := []struct {
82 description string
83 oldTaints []corev1.Taint
84 newTaints []corev1.Taint
85 args []string
86 expectFatal bool
87 expectTaint bool
88 }{
89
90 {
91 description: "taints a node with effect NoSchedule",
92 newTaints: []corev1.Taint{{
93 Key: "foo",
94 Value: "bar",
95 Effect: "NoSchedule",
96 }},
97 args: []string{"node", "node-name", "foo=bar:NoSchedule"},
98 expectFatal: false,
99 expectTaint: true,
100 },
101 {
102 description: "taints a node with effect PreferNoSchedule",
103 newTaints: []corev1.Taint{{
104 Key: "foo",
105 Value: "bar",
106 Effect: "PreferNoSchedule",
107 }},
108 args: []string{"node", "node-name", "foo=bar:PreferNoSchedule"},
109 expectFatal: false,
110 expectTaint: true,
111 },
112 {
113 description: "update an existing taint on the node, change the value from bar to barz",
114 oldTaints: []corev1.Taint{{
115 Key: "foo",
116 Value: "bar",
117 Effect: "NoSchedule",
118 }},
119 newTaints: []corev1.Taint{{
120 Key: "foo",
121 Value: "barz",
122 Effect: "NoSchedule",
123 }},
124 args: []string{"node", "node-name", "foo=barz:NoSchedule", "--overwrite"},
125 expectFatal: false,
126 expectTaint: true,
127 },
128 {
129 description: "taints a node with two taints",
130 newTaints: []corev1.Taint{{
131 Key: "dedicated",
132 Value: "namespaceA",
133 Effect: "NoSchedule",
134 }, {
135 Key: "foo",
136 Value: "bar",
137 Effect: "PreferNoSchedule",
138 }},
139 args: []string{"node", "node-name", "dedicated=namespaceA:NoSchedule", "foo=bar:PreferNoSchedule"},
140 expectFatal: false,
141 expectTaint: true,
142 },
143 {
144 description: "node has two taints with the same key but different effect, remove one of them by indicating exact key and effect",
145 oldTaints: []corev1.Taint{{
146 Key: "dedicated",
147 Value: "namespaceA",
148 Effect: "NoSchedule",
149 }, {
150 Key: "dedicated",
151 Value: "namespaceA",
152 Effect: "PreferNoSchedule",
153 }},
154 newTaints: []corev1.Taint{{
155 Key: "dedicated",
156 Value: "namespaceA",
157 Effect: "PreferNoSchedule",
158 }},
159 args: []string{"node", "node-name", "dedicated:NoSchedule-"},
160 expectFatal: false,
161 expectTaint: true,
162 },
163 {
164 description: "node has two taints with the same key but different effect, remove all of them with wildcard",
165 oldTaints: []corev1.Taint{{
166 Key: "dedicated",
167 Value: "namespaceA",
168 Effect: "NoSchedule",
169 }, {
170 Key: "dedicated",
171 Value: "namespaceA",
172 Effect: "PreferNoSchedule",
173 }},
174 newTaints: []corev1.Taint{},
175 args: []string{"node", "node-name", "dedicated-"},
176 expectFatal: false,
177 expectTaint: true,
178 },
179 {
180 description: "node has two taints, update one of them and remove the other",
181 oldTaints: []corev1.Taint{{
182 Key: "dedicated",
183 Value: "namespaceA",
184 Effect: "NoSchedule",
185 }, {
186 Key: "foo",
187 Value: "bar",
188 Effect: "PreferNoSchedule",
189 }},
190 newTaints: []corev1.Taint{{
191 Key: "foo",
192 Value: "barz",
193 Effect: "PreferNoSchedule",
194 }},
195 args: []string{"node", "node-name", "dedicated:NoSchedule-", "foo=barz:PreferNoSchedule", "--overwrite"},
196 expectFatal: false,
197 expectTaint: true,
198 },
199
200
201 {
202 description: "invalid taint key",
203 args: []string{"node", "node-name", "nospecialchars^@=banana:NoSchedule"},
204 expectFatal: true,
205 expectTaint: false,
206 },
207 {
208 description: "invalid taint effect",
209 args: []string{"node", "node-name", "foo=bar:NoExcute"},
210 expectFatal: true,
211 expectTaint: false,
212 },
213 {
214 description: "duplicated taints with the same key and effect should be rejected",
215 args: []string{"node", "node-name", "foo=bar:NoExcute", "foo=barz:NoExcute"},
216 expectFatal: true,
217 expectTaint: false,
218 },
219 {
220 description: "add and remove taint with same key and effect should be rejected",
221 args: []string{"node", "node-name", "foo=:NoExcute", "foo=:NoExcute-"},
222 expectFatal: true,
223 expectTaint: false,
224 },
225 {
226 description: "can't update existing taint on the node, since 'overwrite' flag is not set",
227 oldTaints: []corev1.Taint{{
228 Key: "foo",
229 Value: "bar",
230 Effect: "NoSchedule",
231 }},
232 newTaints: []corev1.Taint{{
233 Key: "foo",
234 Value: "bar",
235 Effect: "NoSchedule",
236 }},
237 args: []string{"node", "node-name", "foo=bar:NoSchedule"},
238 expectFatal: true,
239 expectTaint: false,
240 },
241 }
242
243 for _, test := range tests {
244 t.Run(test.description, func(t *testing.T) {
245 oldNode, expectNewNode := generateNodeAndTaintedNode(test.oldTaints, test.newTaints)
246 newNode := &corev1.Node{}
247 tainted := false
248 tf := cmdtesting.NewTestFactory()
249 defer tf.Cleanup()
250
251 codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
252 ns := scheme.Codecs.WithoutConversion()
253
254 tf.Client = &fake.RESTClient{
255 NegotiatedSerializer: ns,
256 GroupVersion: corev1.SchemeGroupVersion,
257 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
258 m := &MyReq{req}
259 switch {
260 case m.isFor("GET", "/nodes"):
261 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, oldNode)}, nil
262 case m.isFor("GET", "/nodes/node-name"):
263 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, oldNode)}, nil
264 case m.isFor("PATCH", "/nodes/node-name"):
265 tainted = true
266 data, err := io.ReadAll(req.Body)
267 if err != nil {
268 t.Fatalf("%s: unexpected error: %v", test.description, err)
269 }
270 defer req.Body.Close()
271
272
273 oldJSON, err := runtime.Encode(codec, oldNode)
274 if err != nil {
275 t.Fatalf("%s: unexpected error: %v", test.description, err)
276 }
277 appliedPatch, err := strategicpatch.StrategicMergePatch(oldJSON, data, &corev1.Node{})
278 if err != nil {
279 t.Fatalf("%s: unexpected error: %v", test.description, err)
280 }
281
282
283 if err := runtime.DecodeInto(codec, appliedPatch, newNode); err != nil {
284 t.Fatalf("%s: unexpected error: %v", test.description, err)
285 }
286 if !equalTaints(expectNewNode.Spec.Taints, newNode.Spec.Taints) {
287 t.Fatalf("%s: expected:\n%v\nsaw:\n%v\n", test.description, expectNewNode.Spec.Taints, newNode.Spec.Taints)
288 }
289 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, newNode)}, nil
290 case m.isFor("PUT", "/nodes/node-name"):
291 tainted = true
292 data, err := io.ReadAll(req.Body)
293 if err != nil {
294 t.Fatalf("%s: unexpected error: %v", test.description, err)
295 }
296 defer req.Body.Close()
297 if err := runtime.DecodeInto(codec, data, newNode); err != nil {
298 t.Fatalf("%s: unexpected error: %v", test.description, err)
299 }
300 if !equalTaints(expectNewNode.Spec.Taints, newNode.Spec.Taints) {
301 t.Fatalf("%s: expected:\n%v\nsaw:\n%v\n", test.description, expectNewNode.Spec.Taints, newNode.Spec.Taints)
302 }
303 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, newNode)}, nil
304 default:
305 t.Fatalf("%s: unexpected request: %v %#v\n%#v", test.description, req.Method, req.URL, req)
306 return nil, nil
307 }
308 }),
309 }
310 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
311
312 cmd := NewCmdTaint(tf, genericiooptions.NewTestIOStreamsDiscard())
313
314 sawFatal := false
315 func() {
316 defer func() {
317
318 if r := recover(); r != nil {
319 t.Logf("Recovered: %v", r)
320 }
321
322
323 cmdutil.DefaultBehaviorOnFatal()
324 }()
325 cmdutil.BehaviorOnFatal(func(e string, code int) { sawFatal = true; panic(e) })
326 cmd.SetArgs(test.args)
327 cmd.Execute()
328 }()
329
330 if test.expectFatal {
331 if !sawFatal {
332 t.Fatalf("%s: unexpected non-error", test.description)
333 }
334 }
335
336 if test.expectTaint {
337 if !tainted {
338 t.Fatalf("%s: node not tainted", test.description)
339 }
340 }
341 if !test.expectTaint {
342 if tainted {
343 t.Fatalf("%s: unexpected taint", test.description)
344 }
345 }
346 })
347 }
348 }
349
350 func TestValidateFlags(t *testing.T) {
351 tests := []struct {
352 taintOpts TaintOptions
353 description string
354 expectFatal bool
355 }{
356
357 {
358 taintOpts: TaintOptions{selector: "myLabel=X", all: false},
359 description: "With Selector and without All flag",
360 expectFatal: false,
361 },
362 {
363 taintOpts: TaintOptions{selector: "", all: true},
364 description: "Without selector and All flag",
365 expectFatal: false,
366 },
367 {
368 taintOpts: TaintOptions{selector: "myLabel=X", all: true},
369 description: "With Selector and with All flag",
370 expectFatal: true,
371 },
372 {
373 taintOpts: TaintOptions{selector: "", all: false, resources: []string{"node"}},
374 description: "Without Selector and All flags and if node name is not provided",
375 expectFatal: true,
376 },
377 {
378 taintOpts: TaintOptions{selector: "", all: false, resources: []string{"node", "node-name"}},
379 description: "Without Selector and ALL flags and if node name is provided",
380 expectFatal: false,
381 },
382 }
383 for _, test := range tests {
384 sawFatal := false
385 err := test.taintOpts.validateFlags()
386 if err != nil {
387 sawFatal = true
388 }
389 if test.expectFatal {
390 if !sawFatal {
391 t.Fatalf("%s expected not to fail", test.description)
392 }
393 }
394 }
395 }
396
397 type MyReq struct {
398 Request *http.Request
399 }
400
401 func (m *MyReq) isFor(method string, path string) bool {
402 req := m.Request
403
404 return method == req.Method && (req.URL.Path == path ||
405 req.URL.Path == strings.Join([]string{"/api/v1", path}, "") ||
406 req.URL.Path == strings.Join([]string{"/apis/extensions/v1beta1", path}, "") ||
407 req.URL.Path == strings.Join([]string{"/apis/batch/v1", path}, ""))
408 }
409
View as plain text