1
16
17 package wait
18
19 import (
20 "io"
21 "strings"
22 "testing"
23 "time"
24
25 "github.com/stretchr/testify/require"
26
27 "k8s.io/apimachinery/pkg/api/meta"
28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
30 "k8s.io/apimachinery/pkg/runtime"
31 "k8s.io/apimachinery/pkg/runtime/schema"
32 "k8s.io/apimachinery/pkg/types"
33 "k8s.io/apimachinery/pkg/util/yaml"
34 "k8s.io/apimachinery/pkg/watch"
35 "k8s.io/cli-runtime/pkg/genericclioptions"
36 "k8s.io/cli-runtime/pkg/genericiooptions"
37 "k8s.io/cli-runtime/pkg/printers"
38 "k8s.io/cli-runtime/pkg/resource"
39 dynamicfakeclient "k8s.io/client-go/dynamic/fake"
40 clienttesting "k8s.io/client-go/testing"
41 )
42
43 const (
44 None string = ""
45 podYAML string = `
46 apiVersion: v1
47 kind: Pod
48 metadata:
49 creationTimestamp: "1998-10-21T18:39:43Z"
50 generateName: foo-b6699dcfb-
51 labels:
52 app: nginx
53 pod-template-hash: b6699dcfb
54 name: foo-b6699dcfb-rnv7t
55 namespace: default
56 ownerReferences:
57 - apiVersion: apps/v1
58 blockOwnerDeletion: true
59 controller: true
60 kind: ReplicaSet
61 name: foo-b6699dcfb
62 uid: 8fc1088c-15d5-4a8c-8502-4dfcedef97b8
63 resourceVersion: "14203463"
64 uid: e2cc99fa-5a28-44da-b880-4dded28882ef
65 spec:
66 containers:
67 - image: nginx
68 imagePullPolicy: IfNotPresent
69 name: nginx
70 ports:
71 - containerPort: 80
72 protocol: TCP
73 resources:
74 limits:
75 cpu: 500m
76 memory: 128Mi
77 requests:
78 cpu: 250m
79 memory: 64Mi
80 terminationMessagePath: /dev/termination-log
81 terminationMessagePolicy: File
82 volumeMounts:
83 - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
84 name: kube-api-access-s64k4
85 readOnly: true
86 dnsPolicy: ClusterFirst
87 enableServiceLinks: true
88 nodeName: knode0
89 preemptionPolicy: PreemptLowerPriority
90 priority: 0
91 restartPolicy: Always
92 schedulerName: default-scheduler
93 securityContext: {}
94 serviceAccount: default
95 serviceAccountName: default
96 terminationGracePeriodSeconds: 30
97 tolerations:
98 - effect: NoExecute
99 key: node.kubernetes.io/not-ready
100 operator: Exists
101 tolerationSeconds: 300
102 - effect: NoExecute
103 key: node.kubernetes.io/unreachable
104 operator: Exists
105 tolerationSeconds: 300
106 volumes:
107 - name: kube-api-access-s64k4
108 projected:
109 defaultMode: 420
110 sources:
111 - serviceAccountToken:
112 expirationSeconds: 3607
113 path: token
114 - configMap:
115 items:
116 - key: ca.crt
117 path: ca.crt
118 name: kube-root-ca.crt
119 status:
120 conditions:
121 - lastProbeTime: null
122 lastTransitionTime: "1998-10-21T18:39:37Z"
123 status: "True"
124 type: Initialized
125 - lastProbeTime: null
126 lastTransitionTime: "1998-10-21T18:39:42Z"
127 status: "True"
128 type: Ready
129 - lastProbeTime: null
130 lastTransitionTime: "1998-10-21T18:39:42Z"
131 status: "True"
132 type: ContainersReady
133 - lastProbeTime: null
134 lastTransitionTime: "1998-10-21T18:39:37Z"
135 status: "True"
136 type: PodScheduled
137 containerStatuses:
138 - containerID: containerd://e35792ba1d6e9a56629659b35dbdb93dacaa0a413962ee04775319f5438e493c
139 image: docker.io/library/nginx:latest
140 imageID: docker.io/library/nginx@sha256:644a70516a26004c97d0d85c7fe1d0c3a67ea8ab7ddf4aff193d9f301670cf36
141 lastState: {}
142 name: nginx
143 ready: true
144 restartCount: 0
145 started: true
146 state:
147 running:
148 startedAt: "1998-10-21T18:39:41Z"
149 hostIP: 192.168.0.22
150 phase: Running
151 podIP: 10.42.1.203
152 podIPs:
153 - ip: 10.42.1.203
154 qosClass: Burstable
155 startTime: "1998-10-21T18:39:37Z"
156 `
157 )
158
159 func newUnstructuredList(items ...*unstructured.Unstructured) *unstructured.UnstructuredList {
160 list := &unstructured.UnstructuredList{}
161 for i := range items {
162 list.Items = append(list.Items, *items[i])
163 }
164 return list
165 }
166
167 func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured {
168 return &unstructured.Unstructured{
169 Object: map[string]interface{}{
170 "apiVersion": apiVersion,
171 "kind": kind,
172 "metadata": map[string]interface{}{
173 "namespace": namespace,
174 "name": name,
175 "uid": "some-UID-value",
176 },
177 },
178 }
179 }
180
181 func newUnstructuredWithGeneration(apiVersion, kind, namespace, name string, generation int64) *unstructured.Unstructured {
182 return &unstructured.Unstructured{
183 Object: map[string]interface{}{
184 "apiVersion": apiVersion,
185 "kind": kind,
186 "metadata": map[string]interface{}{
187 "namespace": namespace,
188 "name": name,
189 "uid": "some-UID-value",
190 "generation": generation,
191 },
192 },
193 }
194 }
195
196 func newUnstructuredStatus(status *metav1.Status) runtime.Unstructured {
197 obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(status)
198 if err != nil {
199 panic(err)
200 }
201 return &unstructured.Unstructured{
202 Object: obj,
203 }
204 }
205
206 func addCondition(in *unstructured.Unstructured, name, status string) *unstructured.Unstructured {
207 conditions, _, _ := unstructured.NestedSlice(in.Object, "status", "conditions")
208 conditions = append(conditions, map[string]interface{}{
209 "type": name,
210 "status": status,
211 })
212 unstructured.SetNestedSlice(in.Object, conditions, "status", "conditions")
213 return in
214 }
215
216 func addConditionWithObservedGeneration(in *unstructured.Unstructured, name, status string, observedGeneration int64) *unstructured.Unstructured {
217 conditions, _, _ := unstructured.NestedSlice(in.Object, "status", "conditions")
218 conditions = append(conditions, map[string]interface{}{
219 "type": name,
220 "status": status,
221 "observedGeneration": observedGeneration,
222 })
223 unstructured.SetNestedSlice(in.Object, conditions, "status", "conditions")
224 return in
225 }
226
227
228
229 func createUnstructured(t *testing.T, config string) *unstructured.Unstructured {
230 t.Helper()
231 result := map[string]interface{}{}
232
233 require.False(t, strings.Contains(config, "\t"), "Yaml %s cannot contain tabs", config)
234 require.NoError(t, yaml.Unmarshal([]byte(config), &result), "Could not parse config:\n\n%s\n", config)
235
236 return &unstructured.Unstructured{
237 Object: result,
238 }
239 }
240
241 func TestWaitForDeletion(t *testing.T) {
242 scheme := runtime.NewScheme()
243 listMapping := map[schema.GroupVersionResource]string{
244 {Group: "group", Version: "version", Resource: "theresource"}: "TheKindList",
245 {Group: "group", Version: "version", Resource: "theresource-1"}: "TheKindList",
246 {Group: "group", Version: "version", Resource: "theresource-2"}: "TheKindList",
247 }
248
249 tests := []struct {
250 name string
251 infos []*resource.Info
252 fakeClient func() *dynamicfakeclient.FakeDynamicClient
253 timeout time.Duration
254 uidMap UIDMap
255
256 expectedErr string
257 }{
258 {
259 name: "missing on get",
260 infos: []*resource.Info{
261 {
262 Mapping: &meta.RESTMapping{
263 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
264 },
265 Name: "name-foo",
266 Namespace: "ns-foo",
267 },
268 },
269 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
270 return dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
271 },
272 timeout: 10 * time.Second,
273 },
274 {
275 name: "handles no infos",
276 infos: []*resource.Info{},
277 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
278 return dynamicfakeclient.NewSimpleDynamicClient(scheme)
279 },
280 timeout: 10 * time.Second,
281 expectedErr: errNoMatchingResources.Error(),
282 },
283 {
284 name: "uid conflict on get",
285 infos: []*resource.Info{
286 {
287 Mapping: &meta.RESTMapping{
288 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
289 },
290 Name: "name-foo",
291 Namespace: "ns-foo",
292 },
293 },
294 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
295 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
296 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
297 return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
298 })
299 count := 0
300 fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
301 if count == 0 {
302 count++
303 fakeWatch := watch.NewRaceFreeFake()
304 go func() {
305 time.Sleep(1 * time.Second)
306 fakeWatch.Stop()
307 }()
308 return true, fakeWatch, nil
309 }
310 fakeWatch := watch.NewRaceFreeFake()
311 return true, fakeWatch, nil
312 })
313 return fakeClient
314 },
315 timeout: 10 * time.Second,
316 uidMap: UIDMap{
317 ResourceLocation{Namespace: "ns-foo", Name: "name-foo"}: types.UID("some-UID-value"),
318 ResourceLocation{GroupResource: schema.GroupResource{Group: "group", Resource: "theresource"}, Namespace: "ns-foo", Name: "name-foo"}: types.UID("some-nonmatching-UID-value"),
319 },
320 },
321 {
322 name: "times out",
323 infos: []*resource.Info{
324 {
325 Mapping: &meta.RESTMapping{
326 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
327 },
328 Name: "name-foo",
329 Namespace: "ns-foo",
330 },
331 },
332 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
333 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
334 fakeClient.PrependReactor("get", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
335 return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
336 })
337 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
338 return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
339 })
340 return fakeClient
341 },
342 timeout: 1 * time.Second,
343
344 expectedErr: "timed out waiting for the condition on theresource/name-foo",
345 },
346 {
347 name: "delete for existing resource with no timeout",
348 infos: []*resource.Info{
349 {
350 Mapping: &meta.RESTMapping{
351 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
352 },
353 Name: "name-foo",
354 Namespace: "ns-foo",
355 },
356 },
357 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
358 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
359 fakeClient.PrependReactor("get", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
360 return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
361 })
362 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
363 return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
364 })
365 return fakeClient
366 },
367 timeout: 0 * time.Second,
368
369 expectedErr: "condition not met for theresource/name-foo",
370 },
371 {
372 name: "delete for nonexisting resource with no timeout",
373 infos: []*resource.Info{
374 {
375 Mapping: &meta.RESTMapping{
376 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thenonexistentresource"},
377 },
378 Name: "name-foo",
379 Namespace: "ns-foo",
380 },
381 },
382 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
383 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
384 fakeClient.PrependReactor("get", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
385 return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
386 })
387 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
388 return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
389 })
390 return fakeClient
391 },
392 timeout: 0 * time.Second,
393
394 expectedErr: "",
395 },
396 {
397 name: "handles watch close out",
398 infos: []*resource.Info{
399 {
400 Mapping: &meta.RESTMapping{
401 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
402 },
403 Name: "name-foo",
404 Namespace: "ns-foo",
405 },
406 },
407 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
408 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
409 fakeClient.PrependReactor("get", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
410 unstructuredObj := newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")
411 unstructuredObj.SetResourceVersion("123")
412 unstructuredList := newUnstructuredList(unstructuredObj)
413 unstructuredList.SetResourceVersion("234")
414 return true, unstructuredList, nil
415 })
416 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
417 unstructuredObj := newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")
418 unstructuredObj.SetResourceVersion("123")
419 unstructuredList := newUnstructuredList(unstructuredObj)
420 unstructuredList.SetResourceVersion("234")
421 return true, unstructuredList, nil
422 })
423 count := 0
424 fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
425 if count == 0 {
426 count++
427 fakeWatch := watch.NewRaceFreeFake()
428 go func() {
429 time.Sleep(1 * time.Second)
430 fakeWatch.Stop()
431 }()
432 return true, fakeWatch, nil
433 }
434 fakeWatch := watch.NewRaceFreeFake()
435 return true, fakeWatch, nil
436 })
437 return fakeClient
438 },
439 timeout: 3 * time.Second,
440
441 expectedErr: "timed out waiting for the condition on theresource/name-foo",
442 },
443 {
444 name: "handles watch delete",
445 infos: []*resource.Info{
446 {
447 Mapping: &meta.RESTMapping{
448 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
449 },
450 Name: "name-foo",
451 Namespace: "ns-foo",
452 },
453 },
454 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
455 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
456 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
457 return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
458 })
459 fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
460 fakeWatch := watch.NewRaceFreeFake()
461 fakeWatch.Action(watch.Deleted, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"))
462 return true, fakeWatch, nil
463 })
464 return fakeClient
465 },
466 timeout: 10 * time.Second,
467 },
468 {
469 name: "handles watch delete multiple",
470 infos: []*resource.Info{
471 {
472 Mapping: &meta.RESTMapping{
473 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource-1"},
474 },
475 Name: "name-foo-1",
476 Namespace: "ns-foo",
477 },
478 {
479 Mapping: &meta.RESTMapping{
480 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource-2"},
481 },
482 Name: "name-foo-2",
483 Namespace: "ns-foo",
484 },
485 },
486 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
487 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
488 fakeClient.PrependReactor("get", "theresource-1", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
489 return true, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo-1"), nil
490 })
491 fakeClient.PrependReactor("get", "theresource-2", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
492 return true, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo-2"), nil
493 })
494 fakeClient.PrependWatchReactor("theresource-1", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
495 fakeWatch := watch.NewRaceFreeFake()
496 fakeWatch.Action(watch.Deleted, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo-1"))
497 return true, fakeWatch, nil
498 })
499 fakeClient.PrependWatchReactor("theresource-2", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
500 fakeWatch := watch.NewRaceFreeFake()
501 fakeWatch.Action(watch.Deleted, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo-2"))
502 return true, fakeWatch, nil
503 })
504 return fakeClient
505 },
506 timeout: 10 * time.Second,
507 },
508 {
509 name: "ignores watch error",
510 infos: []*resource.Info{
511 {
512 Mapping: &meta.RESTMapping{
513 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
514 },
515 Name: "name-foo",
516 Namespace: "ns-foo",
517 },
518 },
519 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
520 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
521 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
522 return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
523 })
524 count := 0
525 fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
526 fakeWatch := watch.NewRaceFreeFake()
527 if count == 0 {
528 fakeWatch.Error(newUnstructuredStatus(&metav1.Status{
529 TypeMeta: metav1.TypeMeta{Kind: "Status", APIVersion: "v1"},
530 Status: "Failure",
531 Code: 500,
532 Message: "Bad",
533 }))
534 fakeWatch.Stop()
535 } else {
536 fakeWatch.Action(watch.Deleted, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"))
537 }
538 count++
539 return true, fakeWatch, nil
540 })
541 return fakeClient
542 },
543 timeout: 10 * time.Second,
544 },
545 }
546
547 for _, test := range tests {
548 t.Run(test.name, func(t *testing.T) {
549 fakeClient := test.fakeClient()
550 o := &WaitOptions{
551 ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(test.infos...),
552 UIDMap: test.uidMap,
553 DynamicClient: fakeClient,
554 Timeout: test.timeout,
555
556 Printer: printers.NewDiscardingPrinter(),
557 ConditionFn: IsDeleted,
558 IOStreams: genericiooptions.NewTestIOStreamsDiscard(),
559 }
560 err := o.RunWait()
561 switch {
562 case err == nil && len(test.expectedErr) == 0:
563 case err != nil && len(test.expectedErr) == 0:
564 t.Fatal(err)
565 case err == nil && len(test.expectedErr) != 0:
566 t.Fatalf("missing: %q", test.expectedErr)
567 case err != nil && len(test.expectedErr) != 0:
568 if !strings.Contains(err.Error(), test.expectedErr) {
569 t.Fatalf("expected %q, got %q", test.expectedErr, err.Error())
570 }
571 }
572 })
573 }
574 }
575
576 func TestWaitForCondition(t *testing.T) {
577 scheme := runtime.NewScheme()
578 listMapping := map[schema.GroupVersionResource]string{
579 {Group: "group", Version: "version", Resource: "theresource"}: "TheKindList",
580 }
581
582 tests := []struct {
583 name string
584 infos []*resource.Info
585 fakeClient func() *dynamicfakeclient.FakeDynamicClient
586 timeout time.Duration
587
588 expectedErr string
589 }{
590 {
591 name: "present on get",
592 infos: []*resource.Info{
593 {
594 Mapping: &meta.RESTMapping{
595 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
596 },
597 Name: "name-foo",
598 Namespace: "ns-foo",
599 },
600 },
601 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
602 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
603 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
604 return true, newUnstructuredList(addCondition(
605 newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
606 "the-condition", "status-value",
607 )), nil
608 })
609 return fakeClient
610 },
611 timeout: 10 * time.Second,
612 },
613 {
614 name: "handles no infos",
615 infos: []*resource.Info{},
616 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
617 return dynamicfakeclient.NewSimpleDynamicClient(scheme)
618 },
619 timeout: 10 * time.Second,
620 expectedErr: errNoMatchingResources.Error(),
621 },
622 {
623 name: "handles empty object name",
624 infos: []*resource.Info{
625 {
626 Mapping: &meta.RESTMapping{
627 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
628 },
629 Namespace: "ns-foo",
630 },
631 },
632 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
633 return dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
634 },
635 timeout: 10 * time.Second,
636 expectedErr: "resource name must be provided",
637 },
638 {
639 name: "times out",
640 infos: []*resource.Info{
641 {
642 Mapping: &meta.RESTMapping{
643 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
644 },
645 Name: "name-foo",
646 Namespace: "ns-foo",
647 },
648 },
649 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
650 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
651 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
652 return true, addCondition(
653 newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
654 "some-other-condition", "status-value",
655 ), nil
656 })
657 return fakeClient
658 },
659 timeout: 1 * time.Second,
660
661 expectedErr: `theresource.group "name-foo" not found`,
662 },
663 {
664 name: "for nonexisting resource with no timeout",
665 infos: []*resource.Info{
666 {
667 Mapping: &meta.RESTMapping{
668 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thenonexistingresource"},
669 },
670 Name: "name-foo",
671 Namespace: "ns-foo",
672 },
673 },
674 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
675 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
676 fakeClient.PrependReactor("get", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
677 return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
678 })
679 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
680 return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
681 })
682 return fakeClient
683 },
684 timeout: 0 * time.Second,
685
686 expectedErr: "thenonexistingresource.group \"name-foo\" not found",
687 },
688 {
689 name: "for existing resource with no timeout",
690 infos: []*resource.Info{
691 {
692 Mapping: &meta.RESTMapping{
693 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
694 },
695 Name: "name-foo",
696 Namespace: "ns-foo",
697 },
698 },
699 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
700 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
701 fakeClient.PrependReactor("get", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
702 return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
703 })
704 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
705 return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
706 })
707 return fakeClient
708 },
709 timeout: 0 * time.Second,
710
711 expectedErr: "condition not met for theresource/name-foo",
712 },
713 {
714 name: "handles watch close out",
715 infos: []*resource.Info{
716 {
717 Mapping: &meta.RESTMapping{
718 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
719 },
720 Name: "name-foo",
721 Namespace: "ns-foo",
722 },
723 },
724 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
725 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
726 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
727 unstructuredObj := newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")
728 unstructuredObj.SetResourceVersion("123")
729 unstructuredList := newUnstructuredList(unstructuredObj)
730 unstructuredList.SetResourceVersion("234")
731 return true, unstructuredList, nil
732 })
733 count := 0
734 fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
735 if count == 0 {
736 count++
737 fakeWatch := watch.NewRaceFreeFake()
738 go func() {
739 time.Sleep(1 * time.Second)
740 fakeWatch.Stop()
741 }()
742 return true, fakeWatch, nil
743 }
744 fakeWatch := watch.NewRaceFreeFake()
745 return true, fakeWatch, nil
746 })
747 return fakeClient
748 },
749 timeout: 3 * time.Second,
750
751 expectedErr: "timed out waiting for the condition on theresource/name-foo",
752 },
753 {
754 name: "handles watch condition change",
755 infos: []*resource.Info{
756 {
757 Mapping: &meta.RESTMapping{
758 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
759 },
760 Name: "name-foo",
761 Namespace: "ns-foo",
762 },
763 },
764 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
765 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
766 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
767 return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
768 })
769 fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
770 fakeWatch := watch.NewRaceFreeFake()
771 fakeWatch.Action(watch.Modified, addCondition(
772 newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
773 "the-condition", "status-value",
774 ))
775 return true, fakeWatch, nil
776 })
777 return fakeClient
778 },
779 timeout: 10 * time.Second,
780 },
781 {
782 name: "handles watch created",
783 infos: []*resource.Info{
784 {
785 Mapping: &meta.RESTMapping{
786 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
787 },
788 Name: "name-foo",
789 Namespace: "ns-foo",
790 },
791 },
792 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
793 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
794 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
795 return true, newUnstructuredList(addCondition(
796 newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
797 "the-condition", "status-value",
798 )), nil
799 })
800 return fakeClient
801 },
802 timeout: 1 * time.Second,
803 },
804 {
805 name: "ignores watch error",
806 infos: []*resource.Info{
807 {
808 Mapping: &meta.RESTMapping{
809 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
810 },
811 Name: "name-foo",
812 Namespace: "ns-foo",
813 },
814 },
815 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
816 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
817 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
818 return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
819 })
820 count := 0
821 fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
822 fakeWatch := watch.NewRaceFreeFake()
823 if count == 0 {
824 fakeWatch.Error(newUnstructuredStatus(&metav1.Status{
825 TypeMeta: metav1.TypeMeta{Kind: "Status", APIVersion: "v1"},
826 Status: "Failure",
827 Code: 500,
828 Message: "Bad",
829 }))
830 fakeWatch.Stop()
831 } else {
832 fakeWatch.Action(watch.Modified, addCondition(
833 newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
834 "the-condition", "status-value",
835 ))
836 }
837 count++
838 return true, fakeWatch, nil
839 })
840 return fakeClient
841 },
842 timeout: 10 * time.Second,
843 },
844 {
845 name: "times out due to stale .status.conditions[0].observedGeneration",
846 infos: []*resource.Info{
847 {
848 Mapping: &meta.RESTMapping{
849 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
850 },
851 Name: "name-foo",
852 Namespace: "ns-foo",
853 },
854 },
855 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
856 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
857 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
858 return true, addConditionWithObservedGeneration(
859 newUnstructuredWithGeneration("group/version", "TheKind", "ns-foo", "name-foo", 2),
860 "the-condition", "status-value", 1,
861 ), nil
862 })
863 return fakeClient
864 },
865 timeout: 1 * time.Second,
866
867 expectedErr: `theresource.group "name-foo" not found`,
868 },
869 {
870 name: "handles watch .status.conditions[0].observedGeneration change",
871 infos: []*resource.Info{
872 {
873 Mapping: &meta.RESTMapping{
874 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
875 },
876 Name: "name-foo",
877 Namespace: "ns-foo",
878 },
879 },
880 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
881 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
882 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
883 return true, newUnstructuredList(addConditionWithObservedGeneration(newUnstructuredWithGeneration("group/version", "TheKind", "ns-foo", "name-foo", 2), "the-condition", "status-value", 1)), nil
884 })
885 fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
886 fakeWatch := watch.NewRaceFreeFake()
887 fakeWatch.Action(watch.Modified, addConditionWithObservedGeneration(
888 newUnstructuredWithGeneration("group/version", "TheKind", "ns-foo", "name-foo", 2),
889 "the-condition", "status-value", 2,
890 ))
891 return true, fakeWatch, nil
892 })
893 return fakeClient
894 },
895 timeout: 10 * time.Second,
896 },
897 {
898 name: "times out due to stale .status.observedGeneration",
899 infos: []*resource.Info{
900 {
901 Mapping: &meta.RESTMapping{
902 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
903 },
904 Name: "name-foo",
905 Namespace: "ns-foo",
906 },
907 },
908 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
909 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
910 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
911 instance := addCondition(
912 newUnstructuredWithGeneration("group/version", "TheKind", "ns-foo", "name-foo", 2),
913 "the-condition", "status-value")
914 unstructured.SetNestedField(instance.Object, int64(1), "status", "observedGeneration")
915 return true, instance, nil
916 })
917 return fakeClient
918 },
919 timeout: 1 * time.Second,
920
921 expectedErr: `theresource.group "name-foo" not found`,
922 },
923 {
924 name: "handles watch .status.observedGeneration change",
925 infos: []*resource.Info{
926 {
927 Mapping: &meta.RESTMapping{
928 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
929 },
930 Name: "name-foo",
931 Namespace: "ns-foo",
932 },
933 },
934 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
935 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
936 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
937 instance := addCondition(
938 newUnstructuredWithGeneration("group/version", "TheKind", "ns-foo", "name-foo", 2),
939 "the-condition", "status-value")
940 unstructured.SetNestedField(instance.Object, int64(1), "status", "observedGeneration")
941 return true, newUnstructuredList(instance), nil
942 })
943 fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
944 instance := addCondition(
945 newUnstructuredWithGeneration("group/version", "TheKind", "ns-foo", "name-foo", 2),
946 "the-condition", "status-value")
947 unstructured.SetNestedField(instance.Object, int64(2), "status", "observedGeneration")
948 fakeWatch := watch.NewRaceFreeFake()
949 fakeWatch.Action(watch.Modified, instance)
950 return true, fakeWatch, nil
951 })
952 return fakeClient
953 },
954 timeout: 10 * time.Second,
955 },
956 }
957
958 for _, test := range tests {
959 t.Run(test.name, func(t *testing.T) {
960 fakeClient := test.fakeClient()
961 o := &WaitOptions{
962 ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(test.infos...),
963 DynamicClient: fakeClient,
964 Timeout: test.timeout,
965
966 Printer: printers.NewDiscardingPrinter(),
967 ConditionFn: ConditionalWait{conditionName: "the-condition", conditionStatus: "status-value", errOut: io.Discard}.IsConditionMet,
968 IOStreams: genericiooptions.NewTestIOStreamsDiscard(),
969 }
970 err := o.RunWait()
971 switch {
972 case err == nil && len(test.expectedErr) == 0:
973 case err != nil && len(test.expectedErr) == 0:
974 t.Fatal(err)
975 case err == nil && len(test.expectedErr) != 0:
976 t.Fatalf("missing: %q", test.expectedErr)
977 case err != nil && len(test.expectedErr) != 0:
978 if !strings.Contains(err.Error(), test.expectedErr) {
979 t.Fatalf("expected %q, got %q", test.expectedErr, err.Error())
980 }
981 }
982 })
983 }
984 }
985
986 func TestWaitForDeletionIgnoreNotFound(t *testing.T) {
987 scheme := runtime.NewScheme()
988 listMapping := map[schema.GroupVersionResource]string{
989 {Group: "group", Version: "version", Resource: "theresource"}: "TheKindList",
990 }
991 infos := []*resource.Info{}
992 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
993
994 o := &WaitOptions{
995 ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(infos...),
996 DynamicClient: fakeClient,
997 Printer: printers.NewDiscardingPrinter(),
998 ConditionFn: IsDeleted,
999 IOStreams: genericiooptions.NewTestIOStreamsDiscard(),
1000 ForCondition: "delete",
1001 }
1002 err := o.RunWait()
1003 if err != nil {
1004 t.Fatalf("unexpected error: %v", err)
1005 }
1006 }
1007
1008
1009
1010
1011 func TestWaitForDifferentJSONPathExpression(t *testing.T) {
1012 scheme := runtime.NewScheme()
1013 listMapping := map[schema.GroupVersionResource]string{
1014 {Group: "group", Version: "version", Resource: "theresource"}: "TheKindList",
1015 }
1016 listReactionfunc := func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
1017 return true, newUnstructuredList(createUnstructured(t, podYAML)), nil
1018 }
1019 infos := []*resource.Info{
1020 {
1021 Mapping: &meta.RESTMapping{
1022 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
1023 },
1024 Name: "foo-b6699dcfb-rnv7t",
1025 Namespace: "default",
1026 },
1027 }
1028
1029 tests := []struct {
1030 name string
1031 fakeClient func() *dynamicfakeclient.FakeDynamicClient
1032 jsonPathExp string
1033 jsonPathValue string
1034 matchAnyValue bool
1035
1036 expectedErr string
1037 }{
1038 {
1039 name: "JSONPath entry not exist",
1040 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1041 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1042 fakeClient.PrependReactor("list", "theresource", listReactionfunc)
1043 return fakeClient
1044 },
1045 jsonPathExp: "{.foo.bar}",
1046 jsonPathValue: "baz",
1047 matchAnyValue: false,
1048
1049 expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t",
1050 },
1051 {
1052 name: "compare boolean JSONPath entry",
1053 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1054 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1055 fakeClient.PrependReactor("list", "theresource", listReactionfunc)
1056 return fakeClient
1057 },
1058 jsonPathExp: "{.status.containerStatuses[0].ready}",
1059 jsonPathValue: "true",
1060 matchAnyValue: false,
1061
1062 expectedErr: None,
1063 },
1064 {
1065 name: "compare boolean JSONPath entry wrong value",
1066 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1067 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1068 fakeClient.PrependReactor("list", "theresource", listReactionfunc)
1069 return fakeClient
1070 },
1071 jsonPathExp: "{.status.containerStatuses[0].ready}",
1072 jsonPathValue: "false",
1073 matchAnyValue: false,
1074
1075 expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t",
1076 },
1077 {
1078 name: "compare integer JSONPath entry",
1079 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1080 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1081 fakeClient.PrependReactor("list", "theresource", listReactionfunc)
1082 return fakeClient
1083 },
1084 jsonPathExp: "{.spec.containers[0].ports[0].containerPort}",
1085 jsonPathValue: "80",
1086 matchAnyValue: false,
1087
1088 expectedErr: None,
1089 },
1090 {
1091 name: "compare integer JSONPath entry wrong value",
1092 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1093 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1094 fakeClient.PrependReactor("list", "theresource", listReactionfunc)
1095 return fakeClient
1096 },
1097 jsonPathExp: "{.spec.containers[0].ports[0].containerPort}",
1098 jsonPathValue: "81",
1099 matchAnyValue: false,
1100
1101 expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t",
1102 },
1103 {
1104 name: "compare string JSONPath entry",
1105 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1106 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1107 fakeClient.PrependReactor("list", "theresource", listReactionfunc)
1108 return fakeClient
1109 },
1110 jsonPathExp: "{.spec.nodeName}",
1111 jsonPathValue: "knode0",
1112 matchAnyValue: false,
1113
1114 expectedErr: None,
1115 },
1116 {
1117 name: "matches literal value of JSONPath entry without value condition",
1118 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1119 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1120 fakeClient.PrependReactor("list", "theresource", listReactionfunc)
1121 return fakeClient
1122 },
1123 jsonPathExp: "{.spec.nodeName}",
1124 jsonPathValue: "",
1125 matchAnyValue: true,
1126
1127 expectedErr: None,
1128 },
1129 {
1130 name: "matches complex types map[string]interface{} without value condition",
1131 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1132 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1133 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
1134 return true, newUnstructuredList(createUnstructured(t, podYAML)), nil
1135 })
1136 return fakeClient
1137 },
1138 jsonPathExp: "{.spec}",
1139 jsonPathValue: "",
1140 matchAnyValue: true,
1141
1142 expectedErr: None,
1143 },
1144
1145 {
1146 name: "compare string JSONPath entry wrong value",
1147 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1148 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1149 fakeClient.PrependReactor("list", "theresource", listReactionfunc)
1150 return fakeClient
1151 },
1152 jsonPathExp: "{.spec.nodeName}",
1153 jsonPathValue: "kmaster",
1154 matchAnyValue: false,
1155
1156 expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t",
1157 },
1158 {
1159 name: "matches more than one value",
1160 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1161 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1162 fakeClient.PrependReactor("list", "theresource", listReactionfunc)
1163 return fakeClient
1164 },
1165 jsonPathExp: "{.status.conditions[*]}",
1166 jsonPathValue: "foo",
1167 matchAnyValue: false,
1168
1169 expectedErr: "given jsonpath expression matches more than one value",
1170 },
1171 {
1172 name: "matches more than one value without value condition",
1173 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1174 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1175 fakeClient.PrependReactor("list", "theresource", listReactionfunc)
1176 return fakeClient
1177 },
1178 jsonPathExp: "{.status.conditions[*]}",
1179 jsonPathValue: "",
1180 matchAnyValue: true,
1181
1182 expectedErr: "given jsonpath expression matches more than one value",
1183 },
1184 {
1185 name: "matches more than one list",
1186 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1187 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1188 fakeClient.PrependReactor("list", "theresource", listReactionfunc)
1189 return fakeClient
1190 },
1191 jsonPathExp: "{range .status.conditions[*]}[{.status}] {end}",
1192 jsonPathValue: "foo",
1193 matchAnyValue: false,
1194
1195 expectedErr: "given jsonpath expression matches more than one list",
1196 },
1197 {
1198 name: "matches more than one list without value condition",
1199 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1200 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1201 fakeClient.PrependReactor("list", "theresource", listReactionfunc)
1202 return fakeClient
1203 },
1204 jsonPathExp: "{range .status.conditions[*]}[{.status}] {end}",
1205 jsonPathValue: "",
1206 matchAnyValue: true,
1207
1208 expectedErr: "given jsonpath expression matches more than one list",
1209 },
1210 {
1211 name: "unsupported type []interface{}",
1212 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1213 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1214 fakeClient.PrependReactor("list", "theresource", listReactionfunc)
1215 return fakeClient
1216 },
1217 jsonPathExp: "{.status.conditions}",
1218 jsonPathValue: "True",
1219 matchAnyValue: false,
1220
1221 expectedErr: "jsonpath leads to a nested object or list which is not supported",
1222 },
1223 {
1224 name: "unsupported type map[string]interface{}",
1225 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1226 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1227 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
1228 return true, newUnstructuredList(createUnstructured(t, podYAML)), nil
1229 })
1230 return fakeClient
1231 },
1232 jsonPathExp: "{.spec}",
1233 jsonPathValue: "foo",
1234 matchAnyValue: false,
1235
1236 expectedErr: "jsonpath leads to a nested object or list which is not supported",
1237 },
1238 }
1239 for _, test := range tests {
1240 t.Run(test.name, func(t *testing.T) {
1241 fakeClient := test.fakeClient()
1242 j, _ := newJSONPathParser(test.jsonPathExp)
1243 o := &WaitOptions{
1244 ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(infos...),
1245 DynamicClient: fakeClient,
1246 Timeout: 1 * time.Second,
1247
1248 Printer: printers.NewDiscardingPrinter(),
1249 ConditionFn: JSONPathWait{
1250 matchAnyValue: test.matchAnyValue,
1251 jsonPathValue: test.jsonPathValue,
1252 jsonPathParser: j,
1253 errOut: io.Discard}.IsJSONPathConditionMet,
1254 IOStreams: genericiooptions.NewTestIOStreamsDiscard(),
1255 }
1256
1257 err := o.RunWait()
1258
1259 switch {
1260 case err == nil && len(test.expectedErr) == 0:
1261 case err != nil && len(test.expectedErr) == 0:
1262 t.Fatal(err)
1263 case err == nil && len(test.expectedErr) != 0:
1264 t.Fatalf("missing: %q", test.expectedErr)
1265 case err != nil && len(test.expectedErr) != 0:
1266 if !strings.Contains(err.Error(), test.expectedErr) {
1267 t.Fatalf("expected %q, got %q", test.expectedErr, err.Error())
1268 }
1269 }
1270 })
1271 }
1272 }
1273
1274
1275
1276 func TestWaitForJSONPathCondition(t *testing.T) {
1277 scheme := runtime.NewScheme()
1278 listMapping := map[schema.GroupVersionResource]string{
1279 {Group: "group", Version: "version", Resource: "theresource"}: "TheKindList",
1280 }
1281
1282 tests := []struct {
1283 name string
1284 infos []*resource.Info
1285 fakeClient func() *dynamicfakeclient.FakeDynamicClient
1286 timeout time.Duration
1287 jsonPathExp string
1288 jsonPathValue string
1289
1290 expectedErr string
1291 }{
1292 {
1293 name: "present on get",
1294 infos: []*resource.Info{
1295 {
1296 Mapping: &meta.RESTMapping{
1297 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
1298 },
1299 Name: "foo-b6699dcfb-rnv7t",
1300 Namespace: "default",
1301 },
1302 },
1303 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1304 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1305 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
1306 return true, newUnstructuredList(
1307 createUnstructured(t, podYAML)), nil
1308 })
1309 return fakeClient
1310 },
1311 timeout: 3 * time.Second,
1312 jsonPathExp: "{.metadata.name}",
1313 jsonPathValue: "foo-b6699dcfb-rnv7t",
1314
1315 expectedErr: None,
1316 },
1317 {
1318 name: "handles no infos",
1319 infos: []*resource.Info{},
1320 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1321 return dynamicfakeclient.NewSimpleDynamicClient(scheme)
1322 },
1323 timeout: 10 * time.Second,
1324 expectedErr: errNoMatchingResources.Error(),
1325 },
1326 {
1327 name: "handles empty object name",
1328 infos: []*resource.Info{
1329 {
1330 Mapping: &meta.RESTMapping{
1331 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
1332 },
1333 Namespace: "default",
1334 },
1335 },
1336 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1337 return dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1338 },
1339 timeout: 10 * time.Second,
1340
1341 expectedErr: "resource name must be provided",
1342 },
1343 {
1344 name: "times out",
1345 infos: []*resource.Info{
1346 {
1347 Mapping: &meta.RESTMapping{
1348 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
1349 },
1350 Name: "foo-b6699dcfb-rnv7t",
1351 Namespace: "default",
1352 },
1353 },
1354 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1355 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1356 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
1357 return true, createUnstructured(t, podYAML), nil
1358 })
1359 return fakeClient
1360 },
1361 timeout: 1 * time.Second,
1362
1363 expectedErr: `theresource.group "foo-b6699dcfb-rnv7t" not found`,
1364 },
1365 {
1366 name: "handles watch close out",
1367 infos: []*resource.Info{
1368 {
1369 Mapping: &meta.RESTMapping{
1370 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
1371 },
1372 Name: "foo-b6699dcfb-rnv7t",
1373 Namespace: "default",
1374 },
1375 },
1376 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1377 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1378 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
1379 unstructuredObj := createUnstructured(t, podYAML)
1380 unstructuredObj.SetResourceVersion("123")
1381 unstructuredList := newUnstructuredList(unstructuredObj)
1382 unstructuredList.SetResourceVersion("234")
1383 return true, unstructuredList, nil
1384 })
1385 count := 0
1386 fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
1387 if count == 0 {
1388 count++
1389 fakeWatch := watch.NewRaceFreeFake()
1390 go func() {
1391 time.Sleep(1 * time.Second)
1392 fakeWatch.Stop()
1393 }()
1394 return true, fakeWatch, nil
1395 }
1396 fakeWatch := watch.NewRaceFreeFake()
1397 return true, fakeWatch, nil
1398 })
1399 return fakeClient
1400 },
1401 timeout: 3 * time.Second,
1402 jsonPathExp: "{.metadata.name}",
1403 jsonPathValue: "foo",
1404
1405 expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t",
1406 },
1407 {
1408 name: "handles watch condition change",
1409 infos: []*resource.Info{
1410 {
1411 Mapping: &meta.RESTMapping{
1412 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
1413 },
1414 Name: "foo-b6699dcfb-rnv7t",
1415 Namespace: "default",
1416 },
1417 },
1418 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1419 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1420 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
1421 unstructuredObj := createUnstructured(t, podYAML)
1422 unstructuredObj.SetName("foo")
1423 return true, newUnstructuredList(unstructuredObj), nil
1424 })
1425 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
1426 unstructuredObj := createUnstructured(t, podYAML)
1427 return true, newUnstructuredList(unstructuredObj), nil
1428 })
1429 return fakeClient
1430 },
1431 timeout: 10 * time.Second,
1432 jsonPathExp: "{.metadata.name}",
1433 jsonPathValue: "foo-b6699dcfb-rnv7t",
1434
1435 expectedErr: None,
1436 },
1437 {
1438 name: "handles watch created",
1439 infos: []*resource.Info{
1440 {
1441 Mapping: &meta.RESTMapping{
1442 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
1443 },
1444 Name: "foo-b6699dcfb-rnv7t",
1445 Namespace: "default",
1446 },
1447 },
1448 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1449 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1450 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
1451 return true, newUnstructuredList(
1452 createUnstructured(t, podYAML)), nil
1453 })
1454 return fakeClient
1455 },
1456 timeout: 1 * time.Second,
1457 jsonPathExp: "{.spec.containers[0].image}",
1458 jsonPathValue: "nginx",
1459
1460 expectedErr: None,
1461 },
1462 {
1463 name: "ignores watch error",
1464 infos: []*resource.Info{
1465 {
1466 Mapping: &meta.RESTMapping{
1467 Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
1468 },
1469 Name: "foo-b6699dcfb-rnv7t",
1470 Namespace: "default",
1471 },
1472 },
1473 fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
1474 fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
1475 fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
1476 return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "default", "foo-b6699dcfb-rnv7t")), nil
1477 })
1478 count := 0
1479 fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
1480 fakeWatch := watch.NewRaceFreeFake()
1481 if count == 0 {
1482 fakeWatch.Error(newUnstructuredStatus(&metav1.Status{
1483 TypeMeta: metav1.TypeMeta{Kind: "Status", APIVersion: "v1"},
1484 Status: "Failure",
1485 Code: 500,
1486 Message: "Bad",
1487 }))
1488 fakeWatch.Stop()
1489 } else {
1490 fakeWatch.Action(watch.Modified, createUnstructured(t, podYAML))
1491 }
1492 count++
1493 return true, fakeWatch, nil
1494 })
1495 return fakeClient
1496 },
1497 timeout: 10 * time.Second,
1498 jsonPathExp: "{.metadata.name}",
1499 jsonPathValue: "foo-b6699dcfb-rnv7t",
1500
1501 expectedErr: None,
1502 },
1503 }
1504 for _, test := range tests {
1505 t.Run(test.name, func(t *testing.T) {
1506 fakeClient := test.fakeClient()
1507 j, _ := newJSONPathParser(test.jsonPathExp)
1508 o := &WaitOptions{
1509 ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(test.infos...),
1510 DynamicClient: fakeClient,
1511 Timeout: test.timeout,
1512
1513 Printer: printers.NewDiscardingPrinter(),
1514 ConditionFn: JSONPathWait{
1515 jsonPathValue: test.jsonPathValue,
1516 jsonPathParser: j, errOut: io.Discard}.IsJSONPathConditionMet,
1517 IOStreams: genericiooptions.NewTestIOStreamsDiscard(),
1518 }
1519
1520 err := o.RunWait()
1521
1522 switch {
1523 case err == nil && len(test.expectedErr) == 0:
1524 case err != nil && len(test.expectedErr) == 0:
1525 t.Fatal(err)
1526 case err == nil && len(test.expectedErr) != 0:
1527 t.Fatalf("missing: %q", test.expectedErr)
1528 case err != nil && len(test.expectedErr) != 0:
1529 if !strings.Contains(err.Error(), test.expectedErr) {
1530 t.Fatalf("expected %q, got %q", test.expectedErr, err.Error())
1531 }
1532 }
1533 })
1534 }
1535 }
1536
1537
1538 func TestConditionFuncFor(t *testing.T) {
1539 tests := []struct {
1540 name string
1541 condition string
1542 expectedErr string
1543 }{
1544 {
1545 name: "jsonpath missing JSONPath expression",
1546 condition: "jsonpath=",
1547 expectedErr: "jsonpath expression cannot be empty",
1548 },
1549 {
1550 name: "jsonpath check for condition without value",
1551 condition: "jsonpath={.metadata.name}",
1552 expectedErr: None,
1553 },
1554 {
1555 name: "jsonpath check for condition without value relaxed parsing",
1556 condition: "jsonpath=abc",
1557 expectedErr: None,
1558 },
1559 {
1560 name: "jsonpath check for expression and value",
1561 condition: "jsonpath={.metadata.name}=foo-b6699dcfb-rnv7t",
1562 expectedErr: None,
1563 },
1564 {
1565 name: "jsonpath check for expression and value relaxed parsing",
1566 condition: "jsonpath=.metadata.name=foo-b6699dcfb-rnv7t",
1567 expectedErr: None,
1568 },
1569 {
1570 name: "jsonpath selecting based on condition",
1571 condition: `jsonpath={.status.containerStatuses[?(@.name=="foo")].ready}=True`,
1572 expectedErr: None,
1573 },
1574 {
1575 name: "jsonpath selecting based on condition relaxed parsing",
1576 condition: "jsonpath=status.conditions[?(@.type==\"Available\")].status=True",
1577 expectedErr: None,
1578 },
1579 {
1580 name: "jsonpath selecting based on condition without value",
1581 condition: `jsonpath={.status.containerStatuses[?(@.name=="foo")].ready}`,
1582 expectedErr: None,
1583 },
1584 {
1585 name: "jsonpath selecting based on condition without value relaxed parsing",
1586 condition: `jsonpath=.status.containerStatuses[?(@.name=="foo")].ready`,
1587 expectedErr: None,
1588 },
1589 {
1590 name: "jsonpath invalid expression with repeated '='",
1591 condition: "jsonpath={.metadata.name}='test=wrong'",
1592 expectedErr: "jsonpath wait format must be --for=jsonpath='{.status.readyReplicas}'=3 or --for=jsonpath='{.status.readyReplicas}'",
1593 },
1594 {
1595 name: "jsonpath undefined value after '='",
1596 condition: "jsonpath={.metadata.name}=",
1597 expectedErr: "jsonpath wait has to have a value after equal sign",
1598 },
1599 {
1600 name: "jsonpath complex expressions not supported",
1601 condition: "jsonpath={.status.conditions[?(@.type==\"Failed\"||@.type==\"Complete\")].status}=True",
1602 expectedErr: "unrecognized character in action: U+007C '|'",
1603 },
1604 {
1605 name: "jsonpath invalid expression",
1606 condition: "jsonpath={=True",
1607 expectedErr: "unexpected path string, expected a 'name1.name2' or '.name1.name2' or '{name1.name2}' or " +
1608 "'{.name1.name2}'",
1609 },
1610 {
1611 name: "condition delete",
1612 condition: "delete",
1613 expectedErr: None,
1614 },
1615 {
1616 name: "condition true",
1617 condition: "condition=hello",
1618 expectedErr: None,
1619 },
1620 {
1621 name: "condition with value",
1622 condition: "condition=hello=world",
1623 expectedErr: None,
1624 },
1625 {
1626 name: "unrecognized condition",
1627 condition: "cond=invalid",
1628 expectedErr: "unrecognized condition: \"cond=invalid\"",
1629 },
1630 }
1631 for _, test := range tests {
1632 t.Run(test.name, func(t *testing.T) {
1633 _, err := conditionFuncFor(test.condition, io.Discard)
1634 switch {
1635 case err == nil && test.expectedErr != None:
1636 t.Fatalf("expected error %q, got nil", test.expectedErr)
1637 case err != nil && test.expectedErr == None:
1638 t.Fatalf("expected no error, got %q", err)
1639 case err != nil && test.expectedErr != None:
1640 if !strings.Contains(err.Error(), test.expectedErr) {
1641 t.Fatalf("expected error %q, got %q", test.expectedErr, err.Error())
1642 }
1643 }
1644 })
1645 }
1646 }
1647
View as plain text