1
2
3
4 package status
5
6 import (
7 "bytes"
8 "context"
9 "encoding/json"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/spf13/cobra"
15 "github.com/stretchr/testify/assert"
16 "k8s.io/apimachinery/pkg/runtime/schema"
17 cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
18 cmdutil "k8s.io/kubectl/pkg/cmd/util"
19 "sigs.k8s.io/cli-utils/pkg/apply/poller"
20 "sigs.k8s.io/cli-utils/pkg/inventory"
21 "sigs.k8s.io/cli-utils/pkg/kstatus/polling"
22 pollevent "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
23 "sigs.k8s.io/cli-utils/pkg/kstatus/status"
24 "sigs.k8s.io/cli-utils/pkg/manifestreader"
25 "sigs.k8s.io/cli-utils/pkg/object"
26 )
27
28 var (
29 inventoryTemplate = `
30 kind: ConfigMap
31 apiVersion: v1
32 metadata:
33 labels:
34 cli-utils.sigs.k8s.io/inventory-id: test
35 name: foo
36 namespace: default
37 `
38 depObject = object.ObjMetadata{
39 Name: "foo",
40 Namespace: "default",
41 GroupKind: schema.GroupKind{
42 Group: "apps",
43 Kind: "Deployment",
44 },
45 }
46
47 stsObject = object.ObjMetadata{
48 Name: "bar",
49 Namespace: "default",
50 GroupKind: schema.GroupKind{
51 Group: "apps",
52 Kind: "StatefulSet",
53 },
54 }
55 )
56
57 type fakePoller struct {
58 events []pollevent.Event
59 }
60
61 func (f *fakePoller) Poll(ctx context.Context, _ object.ObjMetadataSet,
62 _ polling.PollOptions) <-chan pollevent.Event {
63 eventChannel := make(chan pollevent.Event)
64 go func() {
65 defer close(eventChannel)
66 for _, e := range f.events {
67 eventChannel <- e
68 }
69 <-ctx.Done()
70 }()
71 return eventChannel
72 }
73
74 func TestCommand(t *testing.T) {
75 testCases := map[string]struct {
76 pollUntil string
77 printer string
78 timeout time.Duration
79 input string
80 inventory object.ObjMetadataSet
81 events []pollevent.Event
82 expectedErrMsg string
83 expectedOutput string
84 }{
85 "no inventory template": {
86 pollUntil: "known",
87 printer: "events",
88 input: "",
89 expectedErrMsg: "Package uninitialized. Please run \"init\" command.",
90 },
91 "no inventory in live state": {
92 pollUntil: "known",
93 printer: "events",
94 input: inventoryTemplate,
95 expectedOutput: "no resources found in the inventory\n",
96 },
97 "wait for all known": {
98 pollUntil: "known",
99 printer: "events",
100 input: inventoryTemplate,
101 inventory: object.ObjMetadataSet{
102 depObject,
103 stsObject,
104 },
105 events: []pollevent.Event{
106 {
107 Type: pollevent.ResourceUpdateEvent,
108 Resource: &pollevent.ResourceStatus{
109 Identifier: depObject,
110 Status: status.InProgressStatus,
111 Message: "inProgress",
112 },
113 },
114 {
115 Type: pollevent.ResourceUpdateEvent,
116 Resource: &pollevent.ResourceStatus{
117 Identifier: stsObject,
118 Status: status.CurrentStatus,
119 Message: "current",
120 },
121 },
122 },
123 expectedOutput: `
124 foo/deployment.apps/default/foo is InProgress: inProgress
125 foo/statefulset.apps/default/bar is Current: current
126 `,
127 },
128 "wait for all current": {
129 pollUntil: "current",
130 printer: "events",
131 input: inventoryTemplate,
132 inventory: object.ObjMetadataSet{
133 depObject,
134 stsObject,
135 },
136 events: []pollevent.Event{
137 {
138 Type: pollevent.ResourceUpdateEvent,
139 Resource: &pollevent.ResourceStatus{
140 Identifier: depObject,
141 Status: status.InProgressStatus,
142 Message: "inProgress",
143 },
144 },
145 {
146 Type: pollevent.ResourceUpdateEvent,
147 Resource: &pollevent.ResourceStatus{
148 Identifier: stsObject,
149 Status: status.InProgressStatus,
150 Message: "inProgress",
151 },
152 },
153 {
154 Type: pollevent.ResourceUpdateEvent,
155 Resource: &pollevent.ResourceStatus{
156 Identifier: stsObject,
157 Status: status.CurrentStatus,
158 Message: "current",
159 },
160 },
161 {
162 Type: pollevent.ResourceUpdateEvent,
163 Resource: &pollevent.ResourceStatus{
164 Identifier: depObject,
165 Status: status.CurrentStatus,
166 Message: "current",
167 },
168 },
169 },
170 expectedOutput: `
171 foo/deployment.apps/default/foo is InProgress: inProgress
172 foo/statefulset.apps/default/bar is InProgress: inProgress
173 foo/statefulset.apps/default/bar is Current: current
174 foo/deployment.apps/default/foo is Current: current
175 `,
176 },
177 "wait for all deleted": {
178 pollUntil: "deleted",
179 printer: "events",
180 input: inventoryTemplate,
181 inventory: object.ObjMetadataSet{
182 depObject,
183 stsObject,
184 },
185 events: []pollevent.Event{
186 {
187 Type: pollevent.ResourceUpdateEvent,
188 Resource: &pollevent.ResourceStatus{
189 Identifier: stsObject,
190 Status: status.NotFoundStatus,
191 Message: "notFound",
192 },
193 },
194 {
195 Type: pollevent.ResourceUpdateEvent,
196 Resource: &pollevent.ResourceStatus{
197 Identifier: depObject,
198 Status: status.NotFoundStatus,
199 Message: "notFound",
200 },
201 },
202 },
203 expectedOutput: `
204 foo/statefulset.apps/default/bar is NotFound: notFound
205 foo/deployment.apps/default/foo is NotFound: notFound
206 `,
207 },
208 "forever with timeout": {
209 pollUntil: "forever",
210 printer: "events",
211 timeout: 2 * time.Second,
212 input: inventoryTemplate,
213 inventory: object.ObjMetadataSet{
214 depObject,
215 stsObject,
216 },
217 events: []pollevent.Event{
218 {
219 Type: pollevent.ResourceUpdateEvent,
220 Resource: &pollevent.ResourceStatus{
221 Identifier: stsObject,
222 Status: status.InProgressStatus,
223 Message: "inProgress",
224 },
225 },
226 {
227 Type: pollevent.ResourceUpdateEvent,
228 Resource: &pollevent.ResourceStatus{
229 Identifier: depObject,
230 Status: status.InProgressStatus,
231 Message: "inProgress",
232 },
233 },
234 },
235 expectedOutput: `
236 foo/statefulset.apps/default/bar is InProgress: inProgress
237 foo/deployment.apps/default/foo is InProgress: inProgress
238 `,
239 },
240 }
241
242 jsonTestCases := map[string]struct {
243 pollUntil string
244 printer string
245 timeout time.Duration
246 input string
247 inventory object.ObjMetadataSet
248 events []pollevent.Event
249 expectedErrMsg string
250 expectedOutput []map[string]interface{}
251 }{
252 "wait for all known json": {
253 pollUntil: "known",
254 printer: "json",
255 input: inventoryTemplate,
256 inventory: object.ObjMetadataSet{
257 depObject,
258 stsObject,
259 },
260 events: []pollevent.Event{
261 {
262 Type: pollevent.ResourceUpdateEvent,
263 Resource: &pollevent.ResourceStatus{
264 Identifier: depObject,
265 Status: status.InProgressStatus,
266 Message: "inProgress",
267 },
268 },
269 {
270 Type: pollevent.ResourceUpdateEvent,
271 Resource: &pollevent.ResourceStatus{
272 Identifier: stsObject,
273 Status: status.CurrentStatus,
274 Message: "current",
275 },
276 },
277 },
278 expectedOutput: []map[string]interface{}{
279 {
280 "group": "apps",
281 "kind": "Deployment",
282 "namespace": "default",
283 "name": "foo",
284 "timestamp": "",
285 "type": "status",
286 "inventory-name": "foo",
287 "status": "InProgress",
288 "message": "inProgress",
289 },
290 {
291 "group": "apps",
292 "kind": "StatefulSet",
293 "namespace": "default",
294 "name": "bar",
295 "timestamp": "",
296 "type": "status",
297 "inventory-name": "foo",
298 "status": "Current",
299 "message": "current",
300 },
301 },
302 },
303 "wait for all current json": {
304 pollUntil: "current",
305 printer: "json",
306 input: inventoryTemplate,
307 inventory: object.ObjMetadataSet{
308 depObject,
309 stsObject,
310 },
311 events: []pollevent.Event{
312 {
313 Type: pollevent.ResourceUpdateEvent,
314 Resource: &pollevent.ResourceStatus{
315 Identifier: depObject,
316 Status: status.InProgressStatus,
317 Message: "inProgress",
318 },
319 },
320 {
321 Type: pollevent.ResourceUpdateEvent,
322 Resource: &pollevent.ResourceStatus{
323 Identifier: stsObject,
324 Status: status.InProgressStatus,
325 Message: "inProgress",
326 },
327 },
328 {
329 Type: pollevent.ResourceUpdateEvent,
330 Resource: &pollevent.ResourceStatus{
331 Identifier: stsObject,
332 Status: status.CurrentStatus,
333 Message: "current",
334 },
335 },
336 {
337 Type: pollevent.ResourceUpdateEvent,
338 Resource: &pollevent.ResourceStatus{
339 Identifier: depObject,
340 Status: status.CurrentStatus,
341 Message: "current",
342 },
343 },
344 },
345 expectedOutput: []map[string]interface{}{
346 {
347 "group": "apps",
348 "kind": "Deployment",
349 "namespace": "default",
350 "name": "foo",
351 "timestamp": "",
352 "type": "status",
353 "inventory-name": "foo",
354 "status": "InProgress",
355 "message": "inProgress",
356 },
357 {
358 "group": "apps",
359 "kind": "StatefulSet",
360 "namespace": "default",
361 "name": "bar",
362 "timestamp": "",
363 "type": "status",
364 "inventory-name": "foo",
365 "status": "InProgress",
366 "message": "inProgress",
367 },
368 {
369 "group": "apps",
370 "kind": "StatefulSet",
371 "namespace": "default",
372 "name": "bar",
373 "timestamp": "",
374 "type": "status",
375 "inventory-name": "foo",
376 "status": "Current",
377 "message": "current",
378 },
379 {
380 "group": "apps",
381 "kind": "Deployment",
382 "namespace": "default",
383 "name": "foo",
384 "timestamp": "",
385 "type": "status",
386 "inventory-name": "foo",
387 "status": "Current",
388 "message": "current",
389 },
390 },
391 },
392 "wait for all deleted json": {
393 pollUntil: "deleted",
394 printer: "json",
395 input: inventoryTemplate,
396 inventory: object.ObjMetadataSet{
397 depObject,
398 stsObject,
399 },
400 events: []pollevent.Event{
401 {
402 Type: pollevent.ResourceUpdateEvent,
403 Resource: &pollevent.ResourceStatus{
404 Identifier: stsObject,
405 Status: status.NotFoundStatus,
406 Message: "notFound",
407 },
408 },
409 {
410 Type: pollevent.ResourceUpdateEvent,
411 Resource: &pollevent.ResourceStatus{
412 Identifier: depObject,
413 Status: status.NotFoundStatus,
414 Message: "notFound",
415 },
416 },
417 },
418 expectedOutput: []map[string]interface{}{
419 {
420 "group": "apps",
421 "kind": "StatefulSet",
422 "namespace": "default",
423 "name": "bar",
424 "timestamp": "",
425 "type": "status",
426 "inventory-name": "foo",
427 "status": "NotFound",
428 "message": "notFound",
429 },
430 {
431 "group": "apps",
432 "kind": "Deployment",
433 "namespace": "default",
434 "name": "foo",
435 "timestamp": "",
436 "type": "status",
437 "inventory-name": "foo",
438 "status": "NotFound",
439 "message": "notFound",
440 },
441 },
442 },
443 "forever with timeout json": {
444 pollUntil: "forever",
445 printer: "json",
446 timeout: 2 * time.Second,
447 input: inventoryTemplate,
448 inventory: object.ObjMetadataSet{
449 depObject,
450 stsObject,
451 },
452 events: []pollevent.Event{
453 {
454 Type: pollevent.ResourceUpdateEvent,
455 Resource: &pollevent.ResourceStatus{
456 Identifier: stsObject,
457 Status: status.InProgressStatus,
458 Message: "inProgress",
459 },
460 },
461 {
462 Type: pollevent.ResourceUpdateEvent,
463 Resource: &pollevent.ResourceStatus{
464 Identifier: depObject,
465 Status: status.InProgressStatus,
466 Message: "inProgress",
467 },
468 },
469 },
470 expectedOutput: []map[string]interface{}{
471 {
472 "group": "apps",
473 "kind": "StatefulSet",
474 "namespace": "default",
475 "name": "bar",
476 "timestamp": "",
477 "type": "status",
478 "inventory-name": "foo",
479 "status": "InProgress",
480 "message": "inProgress",
481 },
482 {
483 "group": "apps",
484 "kind": "Deployment",
485 "namespace": "default",
486 "name": "foo",
487 "timestamp": "",
488 "type": "status",
489 "inventory-name": "foo",
490 "status": "InProgress",
491 "message": "inProgress",
492 },
493 },
494 },
495 }
496
497 for tn, tc := range testCases {
498 t.Run(tn, func(t *testing.T) {
499 tf := cmdtesting.NewTestFactory().WithNamespace("namespace")
500 defer tf.Cleanup()
501
502 loader := manifestreader.NewFakeLoader(tf, tc.inventory)
503 runner := &Runner{
504 factory: tf,
505 invFactory: inventory.FakeClientFactory(tc.inventory),
506 loader: NewInventoryLoader(loader),
507 PollerFactoryFunc: func(c cmdutil.Factory) (poller.Poller, error) {
508 return &fakePoller{tc.events}, nil
509 },
510
511 pollUntil: tc.pollUntil,
512 output: tc.printer,
513 timeout: tc.timeout,
514 invType: Local,
515 }
516
517 cmd := &cobra.Command{
518 PreRunE: runner.preRunE,
519 RunE: runner.runE,
520 }
521 cmd.SetIn(strings.NewReader(tc.input))
522 var buf bytes.Buffer
523 cmd.SetOut(&buf)
524 cmd.SetArgs([]string{})
525
526 err := cmd.Execute()
527
528 if tc.expectedErrMsg != "" {
529 if !assert.Error(t, err) {
530 t.FailNow()
531 }
532 assert.Contains(t, err.Error(), tc.expectedErrMsg)
533 return
534 }
535
536 assert.NoError(t, err)
537 assert.Equal(t, strings.TrimSpace(buf.String()), strings.TrimSpace(tc.expectedOutput))
538 })
539 }
540
541 for tn, tc := range jsonTestCases {
542 t.Run(tn, func(t *testing.T) {
543 tf := cmdtesting.NewTestFactory().WithNamespace("namespace")
544 defer tf.Cleanup()
545
546 loader := manifestreader.NewFakeLoader(tf, tc.inventory)
547 runner := &Runner{
548 factory: tf,
549 invFactory: inventory.FakeClientFactory(tc.inventory),
550 loader: NewInventoryLoader(loader),
551 PollerFactoryFunc: func(c cmdutil.Factory) (poller.Poller, error) {
552 return &fakePoller{tc.events}, nil
553 },
554
555 pollUntil: tc.pollUntil,
556 output: tc.printer,
557 timeout: tc.timeout,
558 invType: Local,
559 }
560
561 cmd := &cobra.Command{
562 RunE: runner.runE,
563 }
564 cmd.SetIn(strings.NewReader(tc.input))
565 var buf bytes.Buffer
566 cmd.SetOut(&buf)
567 cmd.SetArgs([]string{})
568
569 err := cmd.Execute()
570 if tc.expectedErrMsg != "" {
571 if !assert.Error(t, err) {
572 t.FailNow()
573 }
574 assert.Contains(t, err.Error(), tc.expectedErrMsg)
575 return
576 }
577
578 assert.NoError(t, err)
579 actual := strings.Split(buf.String(), "\n")
580 assertOutput(t, tc.expectedOutput, actual)
581 })
582 }
583 }
584
585
586 func assertOutput(t *testing.T, expectedOutput []map[string]interface{}, actual []string) bool {
587 for i, expectedMap := range expectedOutput {
588 if len(expectedMap) == 0 {
589 return assert.Empty(t, actual[i])
590 }
591
592 var m map[string]interface{}
593 err := json.Unmarshal([]byte(actual[i]), &m)
594 if !assert.NoError(t, err) {
595 return false
596 }
597
598 if _, found := expectedMap["timestamp"]; found {
599 if _, ok := m["timestamp"]; ok {
600 delete(expectedMap, "timestamp")
601 delete(m, "timestamp")
602 } else {
603 t.Error("expected to find key 'timestamp', but didn't")
604 return false
605 }
606 }
607 if !assert.Equal(t, expectedMap, m) {
608 return false
609 }
610 }
611 return true
612 }
613
View as plain text