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