1
2
3
4 package task
5
6 import (
7 "fmt"
8 "strings"
9 "sync"
10 "testing"
11
12 "github.com/stretchr/testify/assert"
13 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
14 "k8s.io/apimachinery/pkg/runtime/schema"
15 "k8s.io/apimachinery/pkg/types"
16 "k8s.io/cli-runtime/pkg/resource"
17 "k8s.io/client-go/discovery"
18 "k8s.io/client-go/dynamic"
19 "sigs.k8s.io/cli-utils/pkg/apply/cache"
20 "sigs.k8s.io/cli-utils/pkg/apply/event"
21 "sigs.k8s.io/cli-utils/pkg/apply/taskrunner"
22 "sigs.k8s.io/cli-utils/pkg/common"
23 "sigs.k8s.io/cli-utils/pkg/object"
24 "sigs.k8s.io/cli-utils/pkg/testutil"
25 )
26
27 type resourceInfo struct {
28 group string
29 apiVersion string
30 kind string
31 name string
32 namespace string
33 uid types.UID
34 generation int64
35 }
36
37
38
39
40 func TestApplyTask_BasicAppliedObjects(t *testing.T) {
41 testCases := map[string]struct {
42 applied []resourceInfo
43 }{
44 "apply single namespaced resource": {
45 applied: []resourceInfo{
46 {
47 group: "apps",
48 apiVersion: "apps/v1",
49 kind: "Deployment",
50 name: "foo",
51 namespace: "default",
52 uid: types.UID("my-uid"),
53 generation: int64(42),
54 },
55 },
56 },
57 "apply multiple clusterscoped resources": {
58 applied: []resourceInfo{
59 {
60 group: "custom.io",
61 apiVersion: "custom.io/v1beta1",
62 kind: "Custom",
63 name: "bar",
64 uid: types.UID("uid-1"),
65 generation: int64(32),
66 },
67 {
68 group: "custom2.io",
69 apiVersion: "custom2.io/v1",
70 kind: "Custom2",
71 name: "foo",
72 uid: types.UID("uid-2"),
73 generation: int64(1),
74 },
75 },
76 },
77 }
78
79 for tn, tc := range testCases {
80 t.Run(tn, func(t *testing.T) {
81 eventChannel := make(chan event.Event)
82 defer close(eventChannel)
83 resourceCache := cache.NewResourceCacheMap()
84 taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache)
85
86 objs := toUnstructureds(tc.applied)
87
88 oldAO := applyOptionsFactoryFunc
89 applyOptionsFactoryFunc = func(string, chan<- event.Event, common.ServerSideOptions, common.DryRunStrategy,
90 dynamic.Interface, discovery.OpenAPISchemaInterface) applyOptions {
91 return &fakeApplyOptions{}
92 }
93 defer func() { applyOptionsFactoryFunc = oldAO }()
94
95 restMapper := testutil.NewFakeRESTMapper(schema.GroupVersionKind{
96 Group: "apps",
97 Version: "v1",
98 Kind: "Deployment",
99 }, schema.GroupVersionKind{
100 Group: "anothercustom.io",
101 Version: "v2",
102 Kind: "AnotherCustom",
103 })
104
105 applyTask := &ApplyTask{
106 Objects: objs,
107 Mapper: restMapper,
108 InfoHelper: &fakeInfoHelper{},
109 }
110
111 applyTask.Start(taskContext)
112 <-taskContext.TaskChannel()
113
114
115
116 expectedIDs := object.UnstructuredSetToObjMetadataSet(objs)
117 actual := taskContext.InventoryManager().SuccessfulApplies()
118 if !actual.Equal(expectedIDs) {
119 t.Errorf("expected (%s) inventory resources, got (%s)", expectedIDs, actual)
120 }
121
122 im := taskContext.InventoryManager()
123
124 for _, id := range expectedIDs {
125 assert.Falsef(t, im.IsFailedApply(id), "ApplyTask should NOT mark object as failed: %s", id)
126 assert.Falsef(t, im.IsSkippedApply(id), "ApplyTask should NOT mark object as skipped: %s", id)
127 }
128 })
129 }
130 }
131
132 func TestApplyTask_FetchGeneration(t *testing.T) {
133 testCases := map[string]struct {
134 rss []resourceInfo
135 }{
136 "single namespaced resource": {
137 rss: []resourceInfo{
138 {
139 group: "apps",
140 apiVersion: "apps/v1",
141 kind: "Deployment",
142 name: "foo",
143 namespace: "default",
144 uid: types.UID("my-uid"),
145 generation: int64(42),
146 },
147 },
148 },
149 "multiple clusterscoped resources": {
150 rss: []resourceInfo{
151 {
152 group: "custom.io",
153 apiVersion: "custom.io/v1beta1",
154 kind: "Custom",
155 name: "bar",
156 uid: types.UID("uid-1"),
157 generation: int64(32),
158 },
159 {
160 group: "custom2.io",
161 apiVersion: "custom2.io/v1",
162 kind: "Custom2",
163 name: "foo",
164 uid: types.UID("uid-2"),
165 generation: int64(1),
166 },
167 },
168 },
169 }
170
171 for tn, tc := range testCases {
172 t.Run(tn, func(t *testing.T) {
173 eventChannel := make(chan event.Event)
174 defer close(eventChannel)
175 resourceCache := cache.NewResourceCacheMap()
176 taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache)
177
178 objs := toUnstructureds(tc.rss)
179
180 oldAO := applyOptionsFactoryFunc
181 applyOptionsFactoryFunc = func(string, chan<- event.Event, common.ServerSideOptions, common.DryRunStrategy,
182 dynamic.Interface, discovery.OpenAPISchemaInterface) applyOptions {
183 return &fakeApplyOptions{}
184 }
185 defer func() { applyOptionsFactoryFunc = oldAO }()
186 applyTask := &ApplyTask{
187 Objects: objs,
188 InfoHelper: &fakeInfoHelper{},
189 }
190 applyTask.Start(taskContext)
191
192 <-taskContext.TaskChannel()
193
194 for _, info := range tc.rss {
195 id := object.ObjMetadata{
196 GroupKind: schema.GroupKind{
197 Group: info.group,
198 Kind: info.kind,
199 },
200 Name: info.name,
201 Namespace: info.namespace,
202 }
203 uid, _ := taskContext.InventoryManager().AppliedResourceUID(id)
204 assert.Equal(t, info.uid, uid)
205
206 gen, _ := taskContext.InventoryManager().AppliedGeneration(id)
207 assert.Equal(t, info.generation, gen)
208 }
209 })
210 }
211 }
212
213 func TestApplyTask_DryRun(t *testing.T) {
214 testCases := map[string]struct {
215 objs []*unstructured.Unstructured
216 expectedObjects []object.ObjMetadata
217 expectedEvents []event.Event
218 }{
219 "simple dry run": {
220 objs: []*unstructured.Unstructured{
221 toUnstructured(map[string]interface{}{
222 "apiVersion": "apps/v1",
223 "kind": "Deployment",
224 "metadata": map[string]interface{}{
225 "name": "foo",
226 "namespace": "default",
227 },
228 }),
229 },
230 expectedObjects: []object.ObjMetadata{
231 {
232 GroupKind: schema.GroupKind{
233 Group: "apps",
234 Kind: "Deployment",
235 },
236 Name: "foo",
237 Namespace: "default",
238 },
239 },
240 expectedEvents: []event.Event{},
241 },
242 "dry run with CRD and CR": {
243 objs: []*unstructured.Unstructured{
244 toUnstructured(map[string]interface{}{
245 "apiVersion": "apiextensions.k8s.io/v1",
246 "kind": "CustomResourceDefinition",
247 "metadata": map[string]interface{}{
248 "name": "foo",
249 },
250 "spec": map[string]interface{}{
251 "group": "custom.io",
252 "names": map[string]interface{}{
253 "kind": "Custom",
254 },
255 "versions": []interface{}{
256 map[string]interface{}{
257 "name": "v1alpha1",
258 },
259 },
260 },
261 }),
262 toUnstructured(map[string]interface{}{
263 "apiVersion": "custom.io/v1alpha1",
264 "kind": "Custom",
265 "metadata": map[string]interface{}{
266 "name": "bar",
267 },
268 }),
269 },
270 expectedObjects: []object.ObjMetadata{
271 {
272 GroupKind: schema.GroupKind{
273 Group: "custom.io",
274 Kind: "Custom",
275 },
276 Name: "bar",
277 },
278 },
279 expectedEvents: []event.Event{},
280 },
281 }
282
283 for tn, tc := range testCases {
284 for i := range common.Strategies {
285 drs := common.Strategies[i]
286 t.Run(tn, func(t *testing.T) {
287 eventChannel := make(chan event.Event)
288 resourceCache := cache.NewResourceCacheMap()
289 taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache)
290
291 restMapper := testutil.NewFakeRESTMapper(schema.GroupVersionKind{
292 Group: "apps",
293 Version: "v1",
294 Kind: "Deployment",
295 }, schema.GroupVersionKind{
296 Group: "anothercustom.io",
297 Version: "v2",
298 Kind: "AnotherCustom",
299 })
300
301 ao := &fakeApplyOptions{}
302 oldAO := applyOptionsFactoryFunc
303 applyOptionsFactoryFunc = func(string, chan<- event.Event, common.ServerSideOptions, common.DryRunStrategy,
304 dynamic.Interface, discovery.OpenAPISchemaInterface) applyOptions {
305 return ao
306 }
307 defer func() { applyOptionsFactoryFunc = oldAO }()
308
309 applyTask := &ApplyTask{
310 Objects: tc.objs,
311 InfoHelper: &fakeInfoHelper{},
312 Mapper: restMapper,
313 DryRunStrategy: drs,
314 }
315
316 var events []event.Event
317 var wg sync.WaitGroup
318 wg.Add(1)
319 go func() {
320 defer wg.Done()
321 for msg := range eventChannel {
322 events = append(events, msg)
323 }
324 }()
325
326 applyTask.Start(taskContext)
327 <-taskContext.TaskChannel()
328 close(eventChannel)
329 wg.Wait()
330
331 assert.Equal(t, len(tc.expectedObjects), len(ao.objects))
332 for i, obj := range ao.objects {
333 actual, err := object.InfoToObjMeta(obj)
334 if err != nil {
335 continue
336 }
337 assert.Equal(t, tc.expectedObjects[i], actual)
338 }
339
340 assert.Equal(t, len(tc.expectedEvents), len(events))
341 for i, e := range events {
342 assert.Equal(t, tc.expectedEvents[i].Type, e.Type)
343 }
344 })
345 }
346 }
347 }
348
349 func TestApplyTaskWithError(t *testing.T) {
350 testCases := map[string]struct {
351 objs []*unstructured.Unstructured
352 expectedObjects object.ObjMetadataSet
353 expectedEvents []event.Event
354 expectedSkipped object.ObjMetadataSet
355 expectedFailed object.ObjMetadataSet
356 }{
357 "some resources have apply error": {
358 objs: []*unstructured.Unstructured{
359 toUnstructured(map[string]interface{}{
360 "apiVersion": "apiextensions.k8s.io/v1",
361 "kind": "CustomResourceDefinition",
362 "metadata": map[string]interface{}{
363 "name": "foo",
364 },
365 "spec": map[string]interface{}{
366 "group": "anothercustom.io",
367 "names": map[string]interface{}{
368 "kind": "AnotherCustom",
369 },
370 "versions": []interface{}{
371 map[string]interface{}{
372 "name": "v2",
373 },
374 },
375 },
376 }),
377 toUnstructured(map[string]interface{}{
378 "apiVersion": "anothercustom.io/v2",
379 "kind": "AnotherCustom",
380 "metadata": map[string]interface{}{
381 "name": "bar",
382 "namespace": "barbar",
383 },
384 }),
385 toUnstructured(map[string]interface{}{
386 "apiVersion": "anothercustom.io/v2",
387 "kind": "AnotherCustom",
388 "metadata": map[string]interface{}{
389 "name": "bar-with-failure",
390 "namespace": "barbar",
391 },
392 }),
393 },
394 expectedObjects: object.ObjMetadataSet{
395 {
396 GroupKind: schema.GroupKind{
397 Group: "apiextensions.k8s.io",
398 Kind: "CustomResourceDefinition",
399 },
400 Name: "foo",
401 },
402 {
403 GroupKind: schema.GroupKind{
404 Group: "anothercustom.io",
405 Kind: "AnotherCustom",
406 },
407 Name: "bar",
408 Namespace: "barbar",
409 },
410 },
411 expectedEvents: []event.Event{
412 {
413 Type: event.ApplyType,
414 ApplyEvent: event.ApplyEvent{
415 Status: event.ApplyFailed,
416 Error: fmt.Errorf("expected apply error"),
417 },
418 },
419 },
420 expectedFailed: object.ObjMetadataSet{
421 {
422 GroupKind: schema.GroupKind{
423 Group: "anothercustom.io",
424 Kind: "AnotherCustom",
425 },
426 Name: "bar-with-failure",
427 Namespace: "barbar",
428 },
429 },
430 },
431 }
432
433 for tn, tc := range testCases {
434 drs := common.DryRunNone
435 t.Run(tn, func(t *testing.T) {
436 eventChannel := make(chan event.Event)
437 resourceCache := cache.NewResourceCacheMap()
438 taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache)
439
440 restMapper := testutil.NewFakeRESTMapper(schema.GroupVersionKind{
441 Group: "apps",
442 Version: "v1",
443 Kind: "Deployment",
444 }, schema.GroupVersionKind{
445 Group: "anothercustom.io",
446 Version: "v2",
447 Kind: "AnotherCustom",
448 })
449
450 ao := &fakeApplyOptions{}
451 oldAO := applyOptionsFactoryFunc
452 applyOptionsFactoryFunc = func(string, chan<- event.Event, common.ServerSideOptions, common.DryRunStrategy,
453 dynamic.Interface, discovery.OpenAPISchemaInterface) applyOptions {
454 return ao
455 }
456 defer func() { applyOptionsFactoryFunc = oldAO }()
457
458 applyTask := &ApplyTask{
459 Objects: tc.objs,
460 InfoHelper: &fakeInfoHelper{},
461 Mapper: restMapper,
462 DryRunStrategy: drs,
463 }
464
465 var events []event.Event
466 var wg sync.WaitGroup
467 wg.Add(1)
468 go func() {
469 defer wg.Done()
470 for msg := range eventChannel {
471 events = append(events, msg)
472 }
473 }()
474
475 applyTask.Start(taskContext)
476 <-taskContext.TaskChannel()
477 close(eventChannel)
478 wg.Wait()
479
480 assert.Equal(t, len(tc.expectedObjects), len(ao.passedObjects))
481 for i, obj := range ao.passedObjects {
482 actual, err := object.InfoToObjMeta(obj)
483 if err != nil {
484 continue
485 }
486 assert.Equal(t, tc.expectedObjects[i], actual)
487 }
488
489 assert.Equal(t, len(tc.expectedEvents), len(events))
490 for i, e := range events {
491 assert.Equal(t, tc.expectedEvents[i].Type, e.Type)
492 assert.Equal(t, tc.expectedEvents[i].ApplyEvent.Error.Error(), e.ApplyEvent.Error.Error())
493 }
494
495 applyIds := object.UnstructuredSetToObjMetadataSet(tc.objs)
496
497 im := taskContext.InventoryManager()
498
499
500 for _, id := range tc.expectedFailed {
501 assert.Truef(t, im.IsFailedApply(id), "ApplyTask should mark object as failed: %s", id)
502 }
503 for _, id := range applyIds.Diff(tc.expectedFailed) {
504 assert.Falsef(t, im.IsFailedApply(id), "ApplyTask should NOT mark object as failed: %s", id)
505 }
506
507 for _, id := range tc.expectedSkipped {
508 assert.Truef(t, im.IsSkippedApply(id), "ApplyTask should mark object as skipped: %s", id)
509 }
510 for _, id := range applyIds.Diff(tc.expectedSkipped) {
511 assert.Falsef(t, im.IsSkippedApply(id), "ApplyTask should NOT mark object as skipped: %s", id)
512 }
513 })
514 }
515 }
516
517 func toUnstructured(obj map[string]interface{}) *unstructured.Unstructured {
518 return &unstructured.Unstructured{
519 Object: obj,
520 }
521 }
522
523 func toUnstructureds(rss []resourceInfo) []*unstructured.Unstructured {
524 var objs []*unstructured.Unstructured
525
526 for _, rs := range rss {
527 objs = append(objs, &unstructured.Unstructured{
528 Object: map[string]interface{}{
529 "apiVersion": rs.apiVersion,
530 "kind": rs.kind,
531 "metadata": map[string]interface{}{
532 "name": rs.name,
533 "namespace": rs.namespace,
534 "uid": string(rs.uid),
535 "generation": rs.generation,
536 "annotations": map[string]interface{}{
537 "config.k8s.io/owning-inventory": "id",
538 },
539 },
540 },
541 })
542 }
543 return objs
544 }
545
546 type fakeApplyOptions struct {
547 objects []*resource.Info
548 passedObjects []*resource.Info
549 }
550
551 func (f *fakeApplyOptions) Run() error {
552 var err error
553 for _, obj := range f.objects {
554 if strings.Contains(obj.Name, "failure") {
555 err = fmt.Errorf("expected apply error")
556 } else {
557 f.passedObjects = append(f.passedObjects, obj)
558 }
559 }
560 return err
561 }
562
563 func (f *fakeApplyOptions) SetObjects(objects []*resource.Info) {
564 f.objects = objects
565 }
566
567 type fakeInfoHelper struct{}
568
569 func (f *fakeInfoHelper) UpdateInfo(*resource.Info) error {
570 return nil
571 }
572
573 func (f *fakeInfoHelper) BuildInfos(objs []*unstructured.Unstructured) ([]*resource.Info, error) {
574 return object.UnstructuredsToInfos(objs)
575 }
576
577 func (f *fakeInfoHelper) BuildInfo(obj *unstructured.Unstructured) (*resource.Info, error) {
578 return object.UnstructuredToInfo(obj)
579 }
580
View as plain text