1
16
17 package kube
18
19 import (
20 "bytes"
21 "io"
22 "net/http"
23 "strings"
24 "testing"
25
26 v1 "k8s.io/api/core/v1"
27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28 "k8s.io/apimachinery/pkg/runtime"
29 "k8s.io/cli-runtime/pkg/resource"
30 "k8s.io/client-go/kubernetes/scheme"
31 "k8s.io/client-go/rest/fake"
32 cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
33 )
34
35 var unstructuredSerializer = resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer
36 var codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
37
38 func objBody(obj runtime.Object) io.ReadCloser {
39 return io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj))))
40 }
41
42 func newPod(name string) v1.Pod {
43 return newPodWithStatus(name, v1.PodStatus{}, "")
44 }
45
46 func newPodWithStatus(name string, status v1.PodStatus, namespace string) v1.Pod {
47 ns := v1.NamespaceDefault
48 if namespace != "" {
49 ns = namespace
50 }
51 return v1.Pod{
52 ObjectMeta: metav1.ObjectMeta{
53 Name: name,
54 Namespace: ns,
55 SelfLink: "/api/v1/namespaces/default/pods/" + name,
56 },
57 Spec: v1.PodSpec{
58 Containers: []v1.Container{{
59 Name: "app:v4",
60 Image: "abc/app:v4",
61 Ports: []v1.ContainerPort{{Name: "http", ContainerPort: 80}},
62 }},
63 },
64 Status: status,
65 }
66 }
67
68 func newPodList(names ...string) v1.PodList {
69 var list v1.PodList
70 for _, name := range names {
71 list.Items = append(list.Items, newPod(name))
72 }
73 return list
74 }
75
76 func notFoundBody() *metav1.Status {
77 return &metav1.Status{
78 Code: http.StatusNotFound,
79 Status: metav1.StatusFailure,
80 Reason: metav1.StatusReasonNotFound,
81 Message: " \"\" not found",
82 Details: &metav1.StatusDetails{},
83 }
84 }
85
86 func newResponse(code int, obj runtime.Object) (*http.Response, error) {
87 header := http.Header{}
88 header.Set("Content-Type", runtime.ContentTypeJSON)
89 body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj))))
90 return &http.Response{StatusCode: code, Header: header, Body: body}, nil
91 }
92
93 func newTestClient(t *testing.T) *Client {
94 testFactory := cmdtesting.NewTestFactory()
95 t.Cleanup(testFactory.Cleanup)
96
97 return &Client{
98 Factory: testFactory.WithNamespace("default"),
99 Log: nopLogger,
100 }
101 }
102
103 func TestUpdate(t *testing.T) {
104 listA := newPodList("starfish", "otter", "squid")
105 listB := newPodList("starfish", "otter", "dolphin")
106 listC := newPodList("starfish", "otter", "dolphin")
107 listB.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
108 listC.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
109
110 var actions []string
111
112 c := newTestClient(t)
113 c.Factory.(*cmdtesting.TestFactory).UnstructuredClient = &fake.RESTClient{
114 NegotiatedSerializer: unstructuredSerializer,
115 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
116 p, m := req.URL.Path, req.Method
117 actions = append(actions, p+":"+m)
118 t.Logf("got request %s %s", p, m)
119 switch {
120 case p == "/namespaces/default/pods/starfish" && m == "GET":
121 return newResponse(200, &listA.Items[0])
122 case p == "/namespaces/default/pods/otter" && m == "GET":
123 return newResponse(200, &listA.Items[1])
124 case p == "/namespaces/default/pods/otter" && m == "PATCH":
125 data, err := io.ReadAll(req.Body)
126 if err != nil {
127 t.Fatalf("could not dump request: %s", err)
128 }
129 req.Body.Close()
130 expected := `{}`
131 if string(data) != expected {
132 t.Errorf("expected patch\n%s\ngot\n%s", expected, string(data))
133 }
134 return newResponse(200, &listB.Items[0])
135 case p == "/namespaces/default/pods/dolphin" && m == "GET":
136 return newResponse(404, notFoundBody())
137 case p == "/namespaces/default/pods/starfish" && m == "PATCH":
138 data, err := io.ReadAll(req.Body)
139 if err != nil {
140 t.Fatalf("could not dump request: %s", err)
141 }
142 req.Body.Close()
143 expected := `{"spec":{"$setElementOrder/containers":[{"name":"app:v4"}],"containers":[{"$setElementOrder/ports":[{"containerPort":443}],"name":"app:v4","ports":[{"containerPort":443,"name":"https"},{"$patch":"delete","containerPort":80}]}]}}`
144 if string(data) != expected {
145 t.Errorf("expected patch\n%s\ngot\n%s", expected, string(data))
146 }
147 return newResponse(200, &listB.Items[0])
148 case p == "/namespaces/default/pods" && m == "POST":
149 return newResponse(200, &listB.Items[1])
150 case p == "/namespaces/default/pods/squid" && m == "DELETE":
151 return newResponse(200, &listB.Items[1])
152 case p == "/namespaces/default/pods/squid" && m == "GET":
153 return newResponse(200, &listB.Items[2])
154 default:
155 t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
156 return nil, nil
157 }
158 }),
159 }
160 first, err := c.Build(objBody(&listA), false)
161 if err != nil {
162 t.Fatal(err)
163 }
164 second, err := c.Build(objBody(&listB), false)
165 if err != nil {
166 t.Fatal(err)
167 }
168
169 result, err := c.Update(first, second, false)
170 if err != nil {
171 t.Fatal(err)
172 }
173
174 if len(result.Created) != 1 {
175 t.Errorf("expected 1 resource created, got %d", len(result.Created))
176 }
177 if len(result.Updated) != 2 {
178 t.Errorf("expected 2 resource updated, got %d", len(result.Updated))
179 }
180 if len(result.Deleted) != 1 {
181 t.Errorf("expected 1 resource deleted, got %d", len(result.Deleted))
182 }
183
184
185
186
187
188
189
190
191
192
193
194 expectedActions := []string{
195 "/namespaces/default/pods/starfish:GET",
196 "/namespaces/default/pods/starfish:GET",
197 "/namespaces/default/pods/starfish:PATCH",
198 "/namespaces/default/pods/otter:GET",
199 "/namespaces/default/pods/otter:GET",
200 "/namespaces/default/pods/otter:GET",
201 "/namespaces/default/pods/dolphin:GET",
202 "/namespaces/default/pods:POST",
203 "/namespaces/default/pods/squid:GET",
204 "/namespaces/default/pods/squid:DELETE",
205 }
206 if len(expectedActions) != len(actions) {
207 t.Fatalf("unexpected number of requests, expected %d, got %d", len(expectedActions), len(actions))
208 }
209 for k, v := range expectedActions {
210 if actions[k] != v {
211 t.Errorf("expected %s request got %s", v, actions[k])
212 }
213 }
214 }
215
216 func TestBuild(t *testing.T) {
217 tests := []struct {
218 name string
219 namespace string
220 reader io.Reader
221 count int
222 err bool
223 }{
224 {
225 name: "Valid input",
226 namespace: "test",
227 reader: strings.NewReader(guestbookManifest),
228 count: 6,
229 }, {
230 name: "Valid input, deploying resources into different namespaces",
231 namespace: "test",
232 reader: strings.NewReader(namespacedGuestbookManifest),
233 count: 1,
234 },
235 }
236
237 c := newTestClient(t)
238 for _, tt := range tests {
239 t.Run(tt.name, func(t *testing.T) {
240
241 infos, err := c.Build(tt.reader, false)
242 if err != nil && !tt.err {
243 t.Errorf("Got error message when no error should have occurred: %v", err)
244 } else if err != nil && strings.Contains(err.Error(), "--validate=false") {
245 t.Error("error message was not scrubbed")
246 }
247
248 if len(infos) != tt.count {
249 t.Errorf("expected %d result objects, got %d", tt.count, len(infos))
250 }
251 })
252 }
253 }
254
255 func TestBuildTable(t *testing.T) {
256 tests := []struct {
257 name string
258 namespace string
259 reader io.Reader
260 count int
261 err bool
262 }{
263 {
264 name: "Valid input",
265 namespace: "test",
266 reader: strings.NewReader(guestbookManifest),
267 count: 6,
268 }, {
269 name: "Valid input, deploying resources into different namespaces",
270 namespace: "test",
271 reader: strings.NewReader(namespacedGuestbookManifest),
272 count: 1,
273 },
274 }
275
276 c := newTestClient(t)
277 for _, tt := range tests {
278 t.Run(tt.name, func(t *testing.T) {
279
280 infos, err := c.BuildTable(tt.reader, false)
281 if err != nil && !tt.err {
282 t.Errorf("Got error message when no error should have occurred: %v", err)
283 } else if err != nil && strings.Contains(err.Error(), "--validate=false") {
284 t.Error("error message was not scrubbed")
285 }
286
287 if len(infos) != tt.count {
288 t.Errorf("expected %d result objects, got %d", tt.count, len(infos))
289 }
290 })
291 }
292 }
293
294 func TestPerform(t *testing.T) {
295 tests := []struct {
296 name string
297 reader io.Reader
298 count int
299 err bool
300 errMessage string
301 }{
302 {
303 name: "Valid input",
304 reader: strings.NewReader(guestbookManifest),
305 count: 6,
306 }, {
307 name: "Empty manifests",
308 reader: strings.NewReader(""),
309 err: true,
310 errMessage: "no objects visited",
311 },
312 }
313
314 for _, tt := range tests {
315 t.Run(tt.name, func(t *testing.T) {
316 results := []*resource.Info{}
317
318 fn := func(info *resource.Info) error {
319 results = append(results, info)
320 return nil
321 }
322
323 c := newTestClient(t)
324 infos, err := c.Build(tt.reader, false)
325 if err != nil && err.Error() != tt.errMessage {
326 t.Errorf("Error while building manifests: %v", err)
327 }
328
329 err = perform(infos, fn)
330 if (err != nil) != tt.err {
331 t.Errorf("expected error: %v, got %v", tt.err, err)
332 }
333 if err != nil && err.Error() != tt.errMessage {
334 t.Errorf("expected error message: %v, got %v", tt.errMessage, err)
335 }
336
337 if len(results) != tt.count {
338 t.Errorf("expected %d result objects, got %d", tt.count, len(results))
339 }
340 })
341 }
342 }
343
344 func TestReal(t *testing.T) {
345 t.Skip("This is a live test, comment this line to run")
346 c := New(nil)
347 resources, err := c.Build(strings.NewReader(guestbookManifest), false)
348 if err != nil {
349 t.Fatal(err)
350 }
351 if _, err := c.Create(resources); err != nil {
352 t.Fatal(err)
353 }
354
355 testSvcEndpointManifest := testServiceManifest + "\n---\n" + testEndpointManifest
356 c = New(nil)
357 resources, err = c.Build(strings.NewReader(testSvcEndpointManifest), false)
358 if err != nil {
359 t.Fatal(err)
360 }
361 if _, err := c.Create(resources); err != nil {
362 t.Fatal(err)
363 }
364
365 resources, err = c.Build(strings.NewReader(testEndpointManifest), false)
366 if err != nil {
367 t.Fatal(err)
368 }
369
370 if _, errs := c.Delete(resources); errs != nil {
371 t.Fatal(errs)
372 }
373
374 resources, err = c.Build(strings.NewReader(testSvcEndpointManifest), false)
375 if err != nil {
376 t.Fatal(err)
377 }
378
379 if _, errs := c.Delete(resources); errs != nil {
380 t.Fatal(errs)
381 }
382 }
383
384 const testServiceManifest = `
385 kind: Service
386 apiVersion: v1
387 metadata:
388 name: my-service
389 spec:
390 selector:
391 app: myapp
392 ports:
393 - port: 80
394 protocol: TCP
395 targetPort: 9376
396 `
397
398 const testEndpointManifest = `
399 kind: Endpoints
400 apiVersion: v1
401 metadata:
402 name: my-service
403 subsets:
404 - addresses:
405 - ip: "1.2.3.4"
406 ports:
407 - port: 9376
408 `
409
410 const guestbookManifest = `
411 apiVersion: v1
412 kind: Service
413 metadata:
414 name: redis-master
415 labels:
416 app: redis
417 tier: backend
418 role: master
419 spec:
420 ports:
421 - port: 6379
422 targetPort: 6379
423 selector:
424 app: redis
425 tier: backend
426 role: master
427 ---
428 apiVersion: extensions/v1beta1
429 kind: Deployment
430 metadata:
431 name: redis-master
432 spec:
433 replicas: 1
434 template:
435 metadata:
436 labels:
437 app: redis
438 role: master
439 tier: backend
440 spec:
441 containers:
442 - name: master
443 image: registry.k8s.io/redis:e2e # or just image: redis
444 resources:
445 requests:
446 cpu: 100m
447 memory: 100Mi
448 ports:
449 - containerPort: 6379
450 ---
451 apiVersion: v1
452 kind: Service
453 metadata:
454 name: redis-slave
455 labels:
456 app: redis
457 tier: backend
458 role: slave
459 spec:
460 ports:
461 # the port that this service should serve on
462 - port: 6379
463 selector:
464 app: redis
465 tier: backend
466 role: slave
467 ---
468 apiVersion: extensions/v1beta1
469 kind: Deployment
470 metadata:
471 name: redis-slave
472 spec:
473 replicas: 2
474 template:
475 metadata:
476 labels:
477 app: redis
478 role: slave
479 tier: backend
480 spec:
481 containers:
482 - name: slave
483 image: gcr.io/google_samples/gb-redisslave:v1
484 resources:
485 requests:
486 cpu: 100m
487 memory: 100Mi
488 env:
489 - name: GET_HOSTS_FROM
490 value: dns
491 ports:
492 - containerPort: 6379
493 ---
494 apiVersion: v1
495 kind: Service
496 metadata:
497 name: frontend
498 labels:
499 app: guestbook
500 tier: frontend
501 spec:
502 ports:
503 - port: 80
504 selector:
505 app: guestbook
506 tier: frontend
507 ---
508 apiVersion: extensions/v1beta1
509 kind: Deployment
510 metadata:
511 name: frontend
512 spec:
513 replicas: 3
514 template:
515 metadata:
516 labels:
517 app: guestbook
518 tier: frontend
519 spec:
520 containers:
521 - name: php-redis
522 image: gcr.io/google-samples/gb-frontend:v4
523 resources:
524 requests:
525 cpu: 100m
526 memory: 100Mi
527 env:
528 - name: GET_HOSTS_FROM
529 value: dns
530 ports:
531 - containerPort: 80
532 `
533
534 const namespacedGuestbookManifest = `
535 apiVersion: extensions/v1beta1
536 kind: Deployment
537 metadata:
538 name: frontend
539 namespace: guestbook
540 spec:
541 replicas: 3
542 template:
543 metadata:
544 labels:
545 app: guestbook
546 tier: frontend
547 spec:
548 containers:
549 - name: php-redis
550 image: gcr.io/google-samples/gb-frontend:v4
551 resources:
552 requests:
553 cpu: 100m
554 memory: 100Mi
555 env:
556 - name: GET_HOSTS_FROM
557 value: dns
558 ports:
559 - containerPort: 80
560 `
561
View as plain text