1 package komega
2
3 import (
4 "testing"
5
6 . "github.com/onsi/gomega"
7 appsv1 "k8s.io/api/apps/v1"
8 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
10 "sigs.k8s.io/controller-runtime/pkg/client"
11 )
12
13 func TestEqualObjectMatcher(t *testing.T) {
14 cases := []struct {
15 name string
16 original client.Object
17 modified client.Object
18 options []EqualObjectOption
19 want bool
20 }{
21 {
22 name: "succeed with equal objects",
23 original: &appsv1.Deployment{
24 ObjectMeta: metav1.ObjectMeta{
25 Name: "test",
26 },
27 },
28 modified: &appsv1.Deployment{
29 ObjectMeta: metav1.ObjectMeta{
30 Name: "test",
31 },
32 },
33 want: true,
34 },
35 {
36 name: "fail with non equal objects",
37 original: &appsv1.Deployment{
38 ObjectMeta: metav1.ObjectMeta{
39 Name: "test",
40 },
41 },
42 modified: &appsv1.Deployment{
43 ObjectMeta: metav1.ObjectMeta{
44 Name: "somethingelse",
45 },
46 },
47 want: false,
48 },
49 {
50 name: "succeeds if ignored fields do not match",
51 original: &appsv1.Deployment{
52 ObjectMeta: metav1.ObjectMeta{
53 Name: "test",
54 Labels: map[string]string{"somelabel": "somevalue"},
55 OwnerReferences: []metav1.OwnerReference{{
56 Name: "controller",
57 }},
58 },
59 },
60 modified: &appsv1.Deployment{
61 ObjectMeta: metav1.ObjectMeta{
62 Name: "somethingelse",
63 Labels: map[string]string{"somelabel": "anothervalue"},
64 OwnerReferences: []metav1.OwnerReference{{
65 Name: "another",
66 }},
67 },
68 },
69 want: true,
70 options: []EqualObjectOption{
71 IgnorePaths{
72 "ObjectMeta.Name",
73 "ObjectMeta.CreationTimestamp",
74 "ObjectMeta.Labels.somelabel",
75 "ObjectMeta.OwnerReferences[0].Name",
76 "Spec.Template.ObjectMeta",
77 },
78 },
79 },
80 {
81 name: "succeeds if ignored fields in json notation do not match",
82 original: &appsv1.Deployment{
83 ObjectMeta: metav1.ObjectMeta{
84 Name: "test",
85 Labels: map[string]string{"somelabel": "somevalue"},
86 OwnerReferences: []metav1.OwnerReference{{
87 Name: "controller",
88 }},
89 },
90 },
91 modified: &appsv1.Deployment{
92 ObjectMeta: metav1.ObjectMeta{
93 Name: "somethingelse",
94 Labels: map[string]string{"somelabel": "anothervalue"},
95 OwnerReferences: []metav1.OwnerReference{{
96 Name: "another",
97 }},
98 },
99 },
100 want: true,
101 options: []EqualObjectOption{
102 IgnorePaths{
103 "metadata.name",
104 "metadata.creationTimestamp",
105 "metadata.labels.somelabel",
106 "metadata.ownerReferences[0].name",
107 "spec.template.metadata",
108 },
109 },
110 },
111 {
112 name: "succeeds if all allowed fields match, and some others do not",
113 original: &appsv1.Deployment{
114 ObjectMeta: metav1.ObjectMeta{
115 Name: "test",
116 Namespace: "default",
117 },
118 },
119 modified: &appsv1.Deployment{
120 ObjectMeta: metav1.ObjectMeta{
121 Name: "test",
122 Namespace: "special",
123 },
124 },
125 want: true,
126 options: []EqualObjectOption{
127 MatchPaths{
128 "ObjectMeta.Name",
129 },
130 },
131 },
132 {
133 name: "works with unstructured.Unstructured",
134 original: &unstructured.Unstructured{
135 Object: map[string]interface{}{
136 "metadata": map[string]interface{}{
137 "name": "something",
138 "namespace": "test",
139 },
140 },
141 },
142 modified: &unstructured.Unstructured{
143 Object: map[string]interface{}{
144 "metadata": map[string]interface{}{
145 "name": "somethingelse",
146 "namespace": "test",
147 },
148 },
149 },
150 want: true,
151 options: []EqualObjectOption{
152 IgnorePaths{
153 "metadata.name",
154 },
155 },
156 },
157
158
159 {
160 name: "Equal field (spec) both in original and in modified",
161 original: &unstructured.Unstructured{
162 Object: map[string]interface{}{
163 "spec": map[string]interface{}{
164 "foo": "bar",
165 },
166 },
167 },
168 modified: &unstructured.Unstructured{
169 Object: map[string]interface{}{
170 "spec": map[string]interface{}{
171 "foo": "bar",
172 },
173 },
174 },
175 want: true,
176 },
177
178 {
179 name: "Equal nested field both in original and in modified",
180 original: &unstructured.Unstructured{
181 Object: map[string]interface{}{
182 "spec": map[string]interface{}{
183 "template": map[string]interface{}{
184 "spec": map[string]interface{}{
185 "A": "A",
186 },
187 },
188 },
189 },
190 },
191 modified: &unstructured.Unstructured{
192 Object: map[string]interface{}{
193 "spec": map[string]interface{}{
194 "template": map[string]interface{}{
195 "spec": map[string]interface{}{
196 "A": "A",
197 },
198 },
199 },
200 },
201 },
202 want: true,
203 },
204
205
206 {
207 name: "Unequal field both in original and in modified",
208 original: &unstructured.Unstructured{
209 Object: map[string]interface{}{
210 "spec": map[string]interface{}{
211 "foo": "bar-changed",
212 },
213 },
214 },
215 modified: &unstructured.Unstructured{
216 Object: map[string]interface{}{
217 "spec": map[string]interface{}{
218 "foo": "bar",
219 },
220 },
221 },
222 want: false,
223 },
224 {
225 name: "Unequal nested field both in original and modified",
226 original: &unstructured.Unstructured{
227 Object: map[string]interface{}{
228 "spec": map[string]interface{}{
229 "template": map[string]interface{}{
230 "spec": map[string]interface{}{
231 "A": "A-Changed",
232 },
233 },
234 },
235 },
236 },
237 modified: &unstructured.Unstructured{
238 Object: map[string]interface{}{
239 "spec": map[string]interface{}{
240 "template": map[string]interface{}{
241 "spec": map[string]interface{}{
242 "A": "A",
243 },
244 },
245 },
246 },
247 },
248 want: false,
249 },
250
251 {
252 name: "Value of type map with different values",
253 original: &unstructured.Unstructured{
254 Object: map[string]interface{}{
255 "spec": map[string]interface{}{
256 "map": map[string]string{
257 "A": "A-changed",
258 "B": "B",
259
260 },
261 },
262 },
263 },
264 modified: &unstructured.Unstructured{
265 Object: map[string]interface{}{
266 "spec": map[string]interface{}{
267 "map": map[string]string{
268 "A": "A",
269
270 "C": "C",
271 },
272 },
273 },
274 },
275 want: false,
276 },
277
278 {
279 name: "Value of type Array or Slice with same length but different values",
280 original: &unstructured.Unstructured{
281 Object: map[string]interface{}{
282 "spec": map[string]interface{}{
283 "slice": []string{
284 "D",
285 "C",
286 "B",
287 },
288 },
289 },
290 },
291 modified: &unstructured.Unstructured{
292 Object: map[string]interface{}{
293 "spec": map[string]interface{}{
294 "slice": []string{
295 "A",
296 "B",
297 "C",
298 },
299 },
300 },
301 },
302 want: false,
303 },
304
305
306 {
307 name: "Creation timestamp set to empty value on both original and modified",
308 original: &unstructured.Unstructured{
309 Object: map[string]interface{}{
310 "spec": map[string]interface{}{
311 "A": "A",
312 },
313 "metadata": map[string]interface{}{
314 "selfLink": "foo",
315 "creationTimestamp": metav1.Time{},
316 },
317 },
318 },
319 modified: &unstructured.Unstructured{
320 Object: map[string]interface{}{
321 "spec": map[string]interface{}{
322 "A": "A",
323 },
324 "metadata": map[string]interface{}{
325 "selfLink": "foo",
326 "creationTimestamp": metav1.Time{},
327 },
328 },
329 },
330 want: true,
331 },
332
333
334 {
335 name: "Field only in modified",
336 original: &unstructured.Unstructured{
337 Object: map[string]interface{}{},
338 },
339 modified: &unstructured.Unstructured{
340 Object: map[string]interface{}{
341 "spec": map[string]interface{}{
342 "foo": "bar",
343 },
344 },
345 },
346 want: false,
347 },
348 {
349 name: "Nested field only in modified",
350 original: &unstructured.Unstructured{
351 Object: map[string]interface{}{},
352 },
353 modified: &unstructured.Unstructured{
354 Object: map[string]interface{}{
355 "spec": map[string]interface{}{
356 "template": map[string]interface{}{
357 "spec": map[string]interface{}{
358 "A": "A",
359 },
360 },
361 },
362 },
363 },
364 want: false,
365 },
366 {
367 name: "Creation timestamp exists on modified but not on original",
368 original: &unstructured.Unstructured{
369 Object: map[string]interface{}{
370 "spec": map[string]interface{}{
371 "A": "A",
372 },
373 },
374 },
375 modified: &unstructured.Unstructured{
376 Object: map[string]interface{}{
377 "spec": map[string]interface{}{
378 "A": "A",
379 },
380 "metadata": map[string]interface{}{
381 "selfLink": "foo",
382 "creationTimestamp": "2021-11-03T11:05:17Z",
383 },
384 },
385 },
386 want: false,
387 },
388
389
390 {
391 name: "Field only in original",
392 original: &unstructured.Unstructured{
393 Object: map[string]interface{}{
394 "spec": map[string]interface{}{
395 "foo": "bar",
396 },
397 },
398 },
399 modified: &unstructured.Unstructured{
400 Object: map[string]interface{}{},
401 },
402 want: false,
403 },
404 {
405 name: "Nested field only in original",
406 original: &unstructured.Unstructured{
407 Object: map[string]interface{}{
408 "spec": map[string]interface{}{
409 "template": map[string]interface{}{
410 "spec": map[string]interface{}{
411 "A": "A",
412 },
413 },
414 },
415 },
416 },
417 modified: &unstructured.Unstructured{
418 Object: map[string]interface{}{},
419 },
420 want: false,
421 },
422 {
423 name: "Creation timestamp exists on original but not on modified",
424 original: &unstructured.Unstructured{
425 Object: map[string]interface{}{
426 "spec": map[string]interface{}{
427 "A": "A",
428 },
429 "metadata": map[string]interface{}{
430 "selfLink": "foo",
431 "creationTimestamp": "2021-11-03T11:05:17Z",
432 },
433 },
434 },
435 modified: &unstructured.Unstructured{
436 Object: map[string]interface{}{
437 "spec": map[string]interface{}{
438 "A": "A",
439 },
440 },
441 },
442
443 want: false,
444 },
445
446
447 {
448 name: "Unequal Metadata fields computed by the system or in status",
449 original: &unstructured.Unstructured{
450 Object: map[string]interface{}{},
451 },
452 modified: &unstructured.Unstructured{
453 Object: map[string]interface{}{
454 "metadata": map[string]interface{}{
455 "selfLink": "foo",
456 "uid": "foo",
457 "resourceVersion": "foo",
458 "generation": "foo",
459 "managedFields": "foo",
460 },
461 "status": map[string]interface{}{
462 "foo": "bar",
463 },
464 },
465 },
466 want: false,
467 },
468 {
469 name: "Unequal labels and annotations",
470 original: &unstructured.Unstructured{
471 Object: map[string]interface{}{},
472 },
473 modified: &unstructured.Unstructured{
474 Object: map[string]interface{}{
475 "metadata": map[string]interface{}{
476 "labels": map[string]interface{}{
477 "foo": "bar",
478 },
479 "annotations": map[string]interface{}{
480 "foo": "bar",
481 },
482 },
483 },
484 },
485 want: false,
486 },
487
488
489 {
490 name: "Unequal metadata fields ignored by IgnorePaths MatchOption",
491 original: &unstructured.Unstructured{
492 Object: map[string]interface{}{
493 "metadata": map[string]interface{}{
494 "name": "test",
495 },
496 },
497 },
498 modified: &unstructured.Unstructured{
499 Object: map[string]interface{}{
500 "metadata": map[string]interface{}{
501 "name": "test",
502 "selfLink": "foo",
503 "uid": "foo",
504 "resourceVersion": "foo",
505 "generation": "foo",
506 "managedFields": "foo",
507 },
508 },
509 },
510 options: []EqualObjectOption{IgnoreAutogeneratedMetadata},
511 want: true,
512 },
513 {
514 name: "Unequal labels and annotations ignored by IgnorePaths MatchOption",
515 original: &unstructured.Unstructured{
516 Object: map[string]interface{}{
517 "metadata": map[string]interface{}{
518 "name": "test",
519 },
520 },
521 },
522 modified: &unstructured.Unstructured{
523 Object: map[string]interface{}{
524 "metadata": map[string]interface{}{
525 "name": "test",
526 "labels": map[string]interface{}{
527 "foo": "bar",
528 },
529 "annotations": map[string]interface{}{
530 "foo": "bar",
531 },
532 },
533 },
534 },
535 options: []EqualObjectOption{IgnorePaths{"metadata.labels", "metadata.annotations"}},
536 want: true,
537 },
538 {
539 name: "Ignore fields are not compared",
540 original: &unstructured.Unstructured{
541 Object: map[string]interface{}{
542 "spec": map[string]interface{}{},
543 },
544 },
545 modified: &unstructured.Unstructured{
546 Object: map[string]interface{}{
547 "spec": map[string]interface{}{
548 "controlPlaneEndpoint": map[string]interface{}{
549 "host": "",
550 "port": 0,
551 },
552 },
553 },
554 },
555 options: []EqualObjectOption{IgnorePaths{"spec.controlPlaneEndpoint"}},
556 want: true,
557 },
558 {
559 name: "Not-ignored fields are still compared",
560 original: &unstructured.Unstructured{
561 Object: map[string]interface{}{
562 "metadata": map[string]interface{}{
563 "annotations": map[string]interface{}{},
564 },
565 },
566 },
567 modified: &unstructured.Unstructured{
568 Object: map[string]interface{}{
569 "metadata": map[string]interface{}{
570 "annotations": map[string]interface{}{
571 "ignored": "somevalue",
572 "superflous": "shouldcausefailure",
573 },
574 },
575 },
576 },
577 options: []EqualObjectOption{IgnorePaths{"metadata.annotations.ignored"}},
578 want: false,
579 },
580
581
582 {
583 name: "Unequal metadata fields not compared by setting MatchPaths MatchOption",
584 original: &unstructured.Unstructured{
585 Object: map[string]interface{}{
586 "spec": map[string]interface{}{
587 "A": "A",
588 },
589 },
590 },
591 modified: &unstructured.Unstructured{
592 Object: map[string]interface{}{
593 "spec": map[string]interface{}{
594 "A": "A",
595 },
596 "metadata": map[string]interface{}{
597 "selfLink": "foo",
598 "uid": "foo",
599 },
600 },
601 },
602 options: []EqualObjectOption{MatchPaths{"spec"}},
603 want: true,
604 },
605
606
607 {
608 name: "No changes",
609 original: &unstructured.Unstructured{
610 Object: map[string]interface{}{
611 "spec": map[string]interface{}{
612 "A": "A",
613 "B": "B",
614 "C": "C",
615 },
616 },
617 },
618 modified: &unstructured.Unstructured{
619 Object: map[string]interface{}{
620 "spec": map[string]interface{}{
621 "A": "A",
622 "B": "B",
623 },
624 },
625 },
626 want: false,
627 },
628 {
629 name: "Many changes",
630 original: &unstructured.Unstructured{
631 Object: map[string]interface{}{
632 "spec": map[string]interface{}{
633 "A": "A",
634
635 "C": "C",
636 },
637 },
638 },
639 modified: &unstructured.Unstructured{
640 Object: map[string]interface{}{
641 "spec": map[string]interface{}{
642 "A": "A",
643 "B": "B",
644 },
645 },
646 },
647 want: false,
648 },
649 }
650
651 for _, c := range cases {
652 t.Run(c.name, func(t *testing.T) {
653 g := NewWithT(t)
654 m := EqualObject(c.original, c.options...)
655 success, _ := m.Match(c.modified)
656 if !success {
657 t.Log(m.FailureMessage(c.modified))
658 }
659 g.Expect(success).To(Equal(c.want))
660 })
661 }
662 }
663
View as plain text