wait_test.go 28 KB

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