1/*
2Copyright 2018 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package wait
18
19import (
20	"io/ioutil"
21	"strings"
22	"testing"
23	"time"
24
25	"github.com/davecgh/go-spew/spew"
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/watch"
34	"k8s.io/cli-runtime/pkg/genericclioptions"
35	"k8s.io/cli-runtime/pkg/printers"
36	"k8s.io/cli-runtime/pkg/resource"
37	dynamicfakeclient "k8s.io/client-go/dynamic/fake"
38	clienttesting "k8s.io/client-go/testing"
39)
40
41func newUnstructuredList(items ...*unstructured.Unstructured) *unstructured.UnstructuredList {
42	list := &unstructured.UnstructuredList{}
43	for i := range items {
44		list.Items = append(list.Items, *items[i])
45	}
46	return list
47}
48
49func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured {
50	return &unstructured.Unstructured{
51		Object: map[string]interface{}{
52			"apiVersion": apiVersion,
53			"kind":       kind,
54			"metadata": map[string]interface{}{
55				"namespace": namespace,
56				"name":      name,
57				"uid":       "some-UID-value",
58			},
59		},
60	}
61}
62
63func newUnstructuredStatus(status *metav1.Status) runtime.Unstructured {
64	obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(status)
65	if err != nil {
66		panic(err)
67	}
68	return &unstructured.Unstructured{
69		Object: obj,
70	}
71}
72
73func addCondition(in *unstructured.Unstructured, name, status string) *unstructured.Unstructured {
74	conditions, _, _ := unstructured.NestedSlice(in.Object, "status", "conditions")
75	conditions = append(conditions, map[string]interface{}{
76		"type":   name,
77		"status": status,
78	})
79	unstructured.SetNestedSlice(in.Object, conditions, "status", "conditions")
80	return in
81}
82
83func TestWaitForDeletion(t *testing.T) {
84	scheme := runtime.NewScheme()
85
86	tests := []struct {
87		name       string
88		infos      []*resource.Info
89		fakeClient func() *dynamicfakeclient.FakeDynamicClient
90		timeout    time.Duration
91		uidMap     UIDMap
92
93		expectedErr     string
94		validateActions func(t *testing.T, actions []clienttesting.Action)
95	}{
96		{
97			name: "missing on get",
98			infos: []*resource.Info{
99				{
100					Mapping: &meta.RESTMapping{
101						Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
102					},
103					Name:      "name-foo",
104					Namespace: "ns-foo",
105				},
106			},
107			fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
108				return dynamicfakeclient.NewSimpleDynamicClient(scheme)
109			},
110			timeout: 10 * time.Second,
111
112			validateActions: func(t *testing.T, actions []clienttesting.Action) {
113				if len(actions) != 1 {
114					t.Fatal(spew.Sdump(actions))
115				}
116				if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" {
117					t.Error(spew.Sdump(actions))
118				}
119			},
120		},
121		{
122			name:  "handles no infos",
123			infos: []*resource.Info{},
124			fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
125				return dynamicfakeclient.NewSimpleDynamicClient(scheme)
126			},
127			timeout:     10 * time.Second,
128			expectedErr: errNoMatchingResources.Error(),
129
130			validateActions: func(t *testing.T, actions []clienttesting.Action) {
131				if len(actions) != 0 {
132					t.Fatal(spew.Sdump(actions))
133				}
134			},
135		},
136		{
137			name: "uid conflict on get",
138			infos: []*resource.Info{
139				{
140					Mapping: &meta.RESTMapping{
141						Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
142					},
143					Name:      "name-foo",
144					Namespace: "ns-foo",
145				},
146			},
147			fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
148				fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
149				fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
150					return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
151				})
152				count := 0
153				fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
154					if count == 0 {
155						count++
156						fakeWatch := watch.NewRaceFreeFake()
157						go func() {
158							time.Sleep(100 * time.Millisecond)
159							fakeWatch.Stop()
160						}()
161						return true, fakeWatch, nil
162					}
163					fakeWatch := watch.NewRaceFreeFake()
164					return true, fakeWatch, nil
165				})
166				return fakeClient
167			},
168			timeout: 10 * time.Second,
169			uidMap: UIDMap{
170				ResourceLocation{Namespace: "ns-foo", Name: "name-foo"}:                                                                               types.UID("some-UID-value"),
171				ResourceLocation{GroupResource: schema.GroupResource{Group: "group", Resource: "theresource"}, Namespace: "ns-foo", Name: "name-foo"}: types.UID("some-nonmatching-UID-value"),
172			},
173
174			validateActions: func(t *testing.T, actions []clienttesting.Action) {
175				if len(actions) != 1 {
176					t.Fatal(spew.Sdump(actions))
177				}
178				if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" {
179					t.Error(spew.Sdump(actions))
180				}
181			},
182		},
183		{
184			name: "times out",
185			infos: []*resource.Info{
186				{
187					Mapping: &meta.RESTMapping{
188						Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
189					},
190					Name:      "name-foo",
191					Namespace: "ns-foo",
192				},
193			},
194			fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
195				fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
196				fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
197					return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
198				})
199				return fakeClient
200			},
201			timeout: 1 * time.Second,
202
203			expectedErr: "timed out waiting for the condition on theresource/name-foo",
204			validateActions: func(t *testing.T, actions []clienttesting.Action) {
205				if len(actions) != 2 {
206					t.Fatal(spew.Sdump(actions))
207				}
208				if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" {
209					t.Error(spew.Sdump(actions))
210				}
211				if !actions[1].Matches("watch", "theresource") {
212					t.Error(spew.Sdump(actions))
213				}
214			},
215		},
216		{
217			name: "handles watch close out",
218			infos: []*resource.Info{
219				{
220					Mapping: &meta.RESTMapping{
221						Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
222					},
223					Name:      "name-foo",
224					Namespace: "ns-foo",
225				},
226			},
227			fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
228				fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
229				fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
230					unstructuredObj := newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")
231					unstructuredObj.SetResourceVersion("123")
232					unstructuredList := newUnstructuredList(unstructuredObj)
233					unstructuredList.SetResourceVersion("234")
234					return true, unstructuredList, nil
235				})
236				count := 0
237				fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
238					if count == 0 {
239						count++
240						fakeWatch := watch.NewRaceFreeFake()
241						go func() {
242							time.Sleep(100 * time.Millisecond)
243							fakeWatch.Stop()
244						}()
245						return true, fakeWatch, nil
246					}
247					fakeWatch := watch.NewRaceFreeFake()
248					return true, fakeWatch, nil
249				})
250				return fakeClient
251			},
252			timeout: 3 * time.Second,
253
254			expectedErr: "timed out waiting for the condition on theresource/name-foo",
255			validateActions: func(t *testing.T, actions []clienttesting.Action) {
256				if len(actions) != 4 {
257					t.Fatal(spew.Sdump(actions))
258				}
259				if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" {
260					t.Error(spew.Sdump(actions))
261				}
262				if !actions[1].Matches("watch", "theresource") || actions[1].(clienttesting.WatchAction).GetWatchRestrictions().ResourceVersion != "234" {
263					t.Error(spew.Sdump(actions))
264				}
265				if !actions[2].Matches("list", "theresource") || actions[2].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" {
266					t.Error(spew.Sdump(actions))
267				}
268				if !actions[3].Matches("watch", "theresource") || actions[3].(clienttesting.WatchAction).GetWatchRestrictions().ResourceVersion != "234" {
269					t.Error(spew.Sdump(actions))
270				}
271			},
272		},
273		{
274			name: "handles watch delete",
275			infos: []*resource.Info{
276				{
277					Mapping: &meta.RESTMapping{
278						Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
279					},
280					Name:      "name-foo",
281					Namespace: "ns-foo",
282				},
283			},
284			fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
285				fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
286				fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
287					return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
288				})
289				fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
290					fakeWatch := watch.NewRaceFreeFake()
291					fakeWatch.Action(watch.Deleted, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"))
292					return true, fakeWatch, nil
293				})
294				return fakeClient
295			},
296			timeout: 10 * time.Second,
297
298			validateActions: func(t *testing.T, actions []clienttesting.Action) {
299				if len(actions) != 2 {
300					t.Fatal(spew.Sdump(actions))
301				}
302				if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" {
303					t.Error(spew.Sdump(actions))
304				}
305				if !actions[1].Matches("watch", "theresource") {
306					t.Error(spew.Sdump(actions))
307				}
308			},
309		},
310		{
311			name: "handles watch delete multiple",
312			infos: []*resource.Info{
313				{
314					Mapping: &meta.RESTMapping{
315						Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource-1"},
316					},
317					Name:      "name-foo-1",
318					Namespace: "ns-foo",
319				},
320				{
321					Mapping: &meta.RESTMapping{
322						Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource-2"},
323					},
324					Name:      "name-foo-2",
325					Namespace: "ns-foo",
326				},
327			},
328			fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
329				fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
330				fakeClient.PrependReactor("get", "theresource-1", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
331					return true, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo-1"), nil
332				})
333				fakeClient.PrependReactor("get", "theresource-2", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
334					return true, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo-2"), nil
335				})
336				fakeClient.PrependWatchReactor("theresource-1", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
337					fakeWatch := watch.NewRaceFreeFake()
338					fakeWatch.Action(watch.Deleted, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo-1"))
339					return true, fakeWatch, nil
340				})
341				fakeClient.PrependWatchReactor("theresource-2", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
342					fakeWatch := watch.NewRaceFreeFake()
343					fakeWatch.Action(watch.Deleted, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo-2"))
344					return true, fakeWatch, nil
345				})
346				return fakeClient
347			},
348			timeout: 10 * time.Second,
349
350			validateActions: func(t *testing.T, actions []clienttesting.Action) {
351				if len(actions) != 2 {
352					t.Fatal(spew.Sdump(actions))
353				}
354				if !actions[0].Matches("list", "theresource-1") {
355					t.Error(spew.Sdump(actions))
356				}
357				if !actions[1].Matches("list", "theresource-2") {
358					t.Error(spew.Sdump(actions))
359				}
360			},
361		},
362		{
363			name: "ignores watch error",
364			infos: []*resource.Info{
365				{
366					Mapping: &meta.RESTMapping{
367						Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
368					},
369					Name:      "name-foo",
370					Namespace: "ns-foo",
371				},
372			},
373			fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
374				fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
375				fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
376					return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
377				})
378				count := 0
379				fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
380					fakeWatch := watch.NewRaceFreeFake()
381					if count == 0 {
382						fakeWatch.Error(newUnstructuredStatus(&metav1.Status{
383							TypeMeta: metav1.TypeMeta{Kind: "Status", APIVersion: "v1"},
384							Status:   "Failure",
385							Code:     500,
386							Message:  "Bad",
387						}))
388						fakeWatch.Stop()
389					} else {
390						fakeWatch.Action(watch.Deleted, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"))
391					}
392					count++
393					return true, fakeWatch, nil
394				})
395				return fakeClient
396			},
397			timeout: 10 * time.Second,
398
399			validateActions: func(t *testing.T, actions []clienttesting.Action) {
400				if len(actions) != 4 {
401					t.Fatal(spew.Sdump(actions))
402				}
403				if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" {
404					t.Error(spew.Sdump(actions))
405				}
406				if !actions[1].Matches("watch", "theresource") {
407					t.Error(spew.Sdump(actions))
408				}
409				if !actions[2].Matches("list", "theresource") || actions[2].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" {
410					t.Error(spew.Sdump(actions))
411				}
412				if !actions[3].Matches("watch", "theresource") {
413					t.Error(spew.Sdump(actions))
414				}
415			},
416		},
417	}
418
419	for _, test := range tests {
420		t.Run(test.name, func(t *testing.T) {
421			fakeClient := test.fakeClient()
422			o := &WaitOptions{
423				ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(test.infos...),
424				UIDMap:         test.uidMap,
425				DynamicClient:  fakeClient,
426				Timeout:        test.timeout,
427
428				Printer:     printers.NewDiscardingPrinter(),
429				ConditionFn: IsDeleted,
430				IOStreams:   genericclioptions.NewTestIOStreamsDiscard(),
431			}
432			err := o.RunWait()
433			switch {
434			case err == nil && len(test.expectedErr) == 0:
435			case err != nil && len(test.expectedErr) == 0:
436				t.Fatal(err)
437			case err == nil && len(test.expectedErr) != 0:
438				t.Fatalf("missing: %q", test.expectedErr)
439			case err != nil && len(test.expectedErr) != 0:
440				if !strings.Contains(err.Error(), test.expectedErr) {
441					t.Fatalf("expected %q, got %q", test.expectedErr, err.Error())
442				}
443			}
444
445			test.validateActions(t, fakeClient.Actions())
446		})
447	}
448}
449
450func TestWaitForCondition(t *testing.T) {
451	scheme := runtime.NewScheme()
452
453	tests := []struct {
454		name       string
455		infos      []*resource.Info
456		fakeClient func() *dynamicfakeclient.FakeDynamicClient
457		timeout    time.Duration
458
459		expectedErr     string
460		validateActions func(t *testing.T, actions []clienttesting.Action)
461	}{
462		{
463			name: "present on get",
464			infos: []*resource.Info{
465				{
466					Mapping: &meta.RESTMapping{
467						Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
468					},
469					Name:      "name-foo",
470					Namespace: "ns-foo",
471				},
472			},
473			fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
474				fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
475				fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
476					return true, newUnstructuredList(addCondition(
477						newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
478						"the-condition", "status-value",
479					)), nil
480				})
481				return fakeClient
482			},
483			timeout: 10 * time.Second,
484
485			validateActions: func(t *testing.T, actions []clienttesting.Action) {
486				if len(actions) != 1 {
487					t.Fatal(spew.Sdump(actions))
488				}
489				if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" {
490					t.Error(spew.Sdump(actions))
491				}
492			},
493		},
494		{
495			name:  "handles no infos",
496			infos: []*resource.Info{},
497			fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
498				return dynamicfakeclient.NewSimpleDynamicClient(scheme)
499			},
500			timeout:     10 * time.Second,
501			expectedErr: errNoMatchingResources.Error(),
502
503			validateActions: func(t *testing.T, actions []clienttesting.Action) {
504				if len(actions) != 0 {
505					t.Fatal(spew.Sdump(actions))
506				}
507			},
508		},
509		{
510			name: "handles empty object name",
511			infos: []*resource.Info{
512				{
513					Mapping: &meta.RESTMapping{
514						Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
515					},
516					Namespace: "ns-foo",
517				},
518			},
519			fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
520				return dynamicfakeclient.NewSimpleDynamicClient(scheme)
521			},
522			timeout:     10 * time.Second,
523			expectedErr: "resource name must be provided",
524
525			validateActions: func(t *testing.T, actions []clienttesting.Action) {
526				if len(actions) != 0 {
527					t.Fatal(spew.Sdump(actions))
528				}
529			},
530		},
531		{
532			name: "times out",
533			infos: []*resource.Info{
534				{
535					Mapping: &meta.RESTMapping{
536						Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
537					},
538					Name:      "name-foo",
539					Namespace: "ns-foo",
540				},
541			},
542			fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
543				fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
544				fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
545					return true, addCondition(
546						newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
547						"some-other-condition", "status-value",
548					), nil
549				})
550				return fakeClient
551			},
552			timeout: 1 * time.Second,
553
554			expectedErr: "timed out waiting for the condition on theresource/name-foo",
555			validateActions: func(t *testing.T, actions []clienttesting.Action) {
556				if len(actions) != 2 {
557					t.Fatal(spew.Sdump(actions))
558				}
559				if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" {
560					t.Error(spew.Sdump(actions))
561				}
562				if !actions[1].Matches("watch", "theresource") {
563					t.Error(spew.Sdump(actions))
564				}
565			},
566		},
567		{
568			name: "handles watch close out",
569			infos: []*resource.Info{
570				{
571					Mapping: &meta.RESTMapping{
572						Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
573					},
574					Name:      "name-foo",
575					Namespace: "ns-foo",
576				},
577			},
578			fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
579				fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
580				fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
581					unstructuredObj := newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")
582					unstructuredObj.SetResourceVersion("123")
583					unstructuredList := newUnstructuredList(unstructuredObj)
584					unstructuredList.SetResourceVersion("234")
585					return true, unstructuredList, nil
586				})
587				count := 0
588				fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
589					if count == 0 {
590						count++
591						fakeWatch := watch.NewRaceFreeFake()
592						go func() {
593							time.Sleep(100 * time.Millisecond)
594							fakeWatch.Stop()
595						}()
596						return true, fakeWatch, nil
597					}
598					fakeWatch := watch.NewRaceFreeFake()
599					return true, fakeWatch, nil
600				})
601				return fakeClient
602			},
603			timeout: 3 * time.Second,
604
605			expectedErr: "timed out waiting for the condition on theresource/name-foo",
606			validateActions: func(t *testing.T, actions []clienttesting.Action) {
607				if len(actions) != 4 {
608					t.Fatal(spew.Sdump(actions))
609				}
610				if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" {
611					t.Error(spew.Sdump(actions))
612				}
613				if !actions[1].Matches("watch", "theresource") || actions[1].(clienttesting.WatchAction).GetWatchRestrictions().ResourceVersion != "234" {
614					t.Error(spew.Sdump(actions))
615				}
616				if !actions[2].Matches("list", "theresource") || actions[2].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" {
617					t.Error(spew.Sdump(actions))
618				}
619				if !actions[3].Matches("watch", "theresource") || actions[3].(clienttesting.WatchAction).GetWatchRestrictions().ResourceVersion != "234" {
620					t.Error(spew.Sdump(actions))
621				}
622			},
623		},
624		{
625			name: "handles watch condition change",
626			infos: []*resource.Info{
627				{
628					Mapping: &meta.RESTMapping{
629						Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
630					},
631					Name:      "name-foo",
632					Namespace: "ns-foo",
633				},
634			},
635			fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
636				fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
637				fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
638					return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
639				})
640				fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
641					fakeWatch := watch.NewRaceFreeFake()
642					fakeWatch.Action(watch.Modified, addCondition(
643						newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
644						"the-condition", "status-value",
645					))
646					return true, fakeWatch, nil
647				})
648				return fakeClient
649			},
650			timeout: 10 * time.Second,
651
652			validateActions: func(t *testing.T, actions []clienttesting.Action) {
653				if len(actions) != 2 {
654					t.Fatal(spew.Sdump(actions))
655				}
656				if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" {
657					t.Error(spew.Sdump(actions))
658				}
659				if !actions[1].Matches("watch", "theresource") {
660					t.Error(spew.Sdump(actions))
661				}
662			},
663		},
664		{
665			name: "handles watch created",
666			infos: []*resource.Info{
667				{
668					Mapping: &meta.RESTMapping{
669						Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
670					},
671					Name:      "name-foo",
672					Namespace: "ns-foo",
673				},
674			},
675			fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
676				fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
677				fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
678					fakeWatch := watch.NewRaceFreeFake()
679					fakeWatch.Action(watch.Added, addCondition(
680						newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
681						"the-condition", "status-value",
682					))
683					return true, fakeWatch, nil
684				})
685				return fakeClient
686			},
687			timeout: 10 * time.Second,
688
689			validateActions: func(t *testing.T, actions []clienttesting.Action) {
690				if len(actions) != 2 {
691					t.Fatal(spew.Sdump(actions))
692				}
693				if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" {
694					t.Error(spew.Sdump(actions))
695				}
696				if !actions[1].Matches("watch", "theresource") {
697					t.Error(spew.Sdump(actions))
698				}
699			},
700		},
701		{
702			name: "ignores watch error",
703			infos: []*resource.Info{
704				{
705					Mapping: &meta.RESTMapping{
706						Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
707					},
708					Name:      "name-foo",
709					Namespace: "ns-foo",
710				},
711			},
712			fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
713				fakeClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
714				fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
715					return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
716				})
717				count := 0
718				fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
719					fakeWatch := watch.NewRaceFreeFake()
720					if count == 0 {
721						fakeWatch.Error(newUnstructuredStatus(&metav1.Status{
722							TypeMeta: metav1.TypeMeta{Kind: "Status", APIVersion: "v1"},
723							Status:   "Failure",
724							Code:     500,
725							Message:  "Bad",
726						}))
727						fakeWatch.Stop()
728					} else {
729						fakeWatch.Action(watch.Modified, addCondition(
730							newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
731							"the-condition", "status-value",
732						))
733					}
734					count++
735					return true, fakeWatch, nil
736				})
737				return fakeClient
738			},
739			timeout: 10 * time.Second,
740
741			validateActions: func(t *testing.T, actions []clienttesting.Action) {
742				if len(actions) != 4 {
743					t.Fatal(spew.Sdump(actions))
744				}
745				if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" {
746					t.Error(spew.Sdump(actions))
747				}
748				if !actions[1].Matches("watch", "theresource") {
749					t.Error(spew.Sdump(actions))
750				}
751				if !actions[2].Matches("list", "theresource") || actions[2].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=name-foo" {
752					t.Error(spew.Sdump(actions))
753				}
754				if !actions[3].Matches("watch", "theresource") {
755					t.Error(spew.Sdump(actions))
756				}
757			},
758		},
759	}
760
761	for _, test := range tests {
762		t.Run(test.name, func(t *testing.T) {
763			fakeClient := test.fakeClient()
764			o := &WaitOptions{
765				ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(test.infos...),
766				DynamicClient:  fakeClient,
767				Timeout:        test.timeout,
768
769				Printer:     printers.NewDiscardingPrinter(),
770				ConditionFn: ConditionalWait{conditionName: "the-condition", conditionStatus: "status-value", errOut: ioutil.Discard}.IsConditionMet,
771				IOStreams:   genericclioptions.NewTestIOStreamsDiscard(),
772			}
773			err := o.RunWait()
774			switch {
775			case err == nil && len(test.expectedErr) == 0:
776			case err != nil && len(test.expectedErr) == 0:
777				t.Fatal(err)
778			case err == nil && len(test.expectedErr) != 0:
779				t.Fatalf("missing: %q", test.expectedErr)
780			case err != nil && len(test.expectedErr) != 0:
781				if !strings.Contains(err.Error(), test.expectedErr) {
782					t.Fatalf("expected %q, got %q", test.expectedErr, err.Error())
783				}
784			}
785
786			test.validateActions(t, fakeClient.Actions())
787		})
788	}
789}
790