1
16
17 package internal_test
18
19 import (
20 "fmt"
21 "reflect"
22 "testing"
23
24 apiequality "k8s.io/apimachinery/pkg/api/equality"
25 apierrors "k8s.io/apimachinery/pkg/api/errors"
26 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
27 "k8s.io/apimachinery/pkg/runtime"
28 "k8s.io/apimachinery/pkg/runtime/schema"
29 "k8s.io/apimachinery/pkg/util/managedfields/internal"
30 "k8s.io/apimachinery/pkg/util/managedfields/managedfieldstest"
31 yamlutil "k8s.io/apimachinery/pkg/util/yaml"
32 "sigs.k8s.io/structured-merge-diff/v4/fieldpath"
33 "sigs.k8s.io/structured-merge-diff/v4/merge"
34 "sigs.k8s.io/yaml"
35 )
36
37 type testArgs struct {
38 lastApplied []byte
39 original []byte
40 applied []byte
41 fieldManager string
42 expectConflictSet *fieldpath.Set
43 }
44
45
46
47
48 func TestApplyUsingLastAppliedAnnotation(t *testing.T) {
49 f := managedfieldstest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("apps/v1", "Deployment"))
50
51 tests := []testArgs{
52 {
53 fieldManager: "kubectl",
54 lastApplied: []byte(`
55 apiVersion: apps/v1
56 kind: Deployment
57 metadata:
58 name: my-deployment
59 spec:
60 replicas: 3
61 selector:
62 matchLabels:
63 app: my-app
64 template:
65 metadata:
66 labels:
67 app: my-app
68 spec:
69 containers:
70 - name: my-c
71 image: my-image-v1
72 - name: my-c2
73 image: my-image2
74 `),
75 original: []byte(`
76 apiVersion: apps/v1
77 kind: Deployment
78 metadata:
79 name: my-deployment
80 labels:
81 app: my-app # missing from last-applied
82 spec:
83 replicas: 100 # does not match last-applied
84 selector:
85 matchLabels:
86 app: my-app
87 template:
88 metadata:
89 labels:
90 app: my-app
91 spec:
92 containers:
93 - name: my-c
94 image: my-image-v2 # does no match last-applied
95 # note that second container in last-applied is missing
96 `),
97 applied: []byte(`
98 # test conflicts due to fields not allowed by last-applied
99
100 apiVersion: apps/v1
101 kind: Deployment
102 metadata:
103 name: my-deployment
104 labels:
105 app: my-new-label # NOT allowed: update label
106 spec:
107 replicas: 333 # NOT allowed: update replicas
108 selector:
109 matchLabels:
110 app: my-new-label # allowed: update label
111 template:
112 metadata:
113 labels:
114 app: my-new-label # allowed: update-label
115 spec:
116 containers:
117 - name: my-c
118 image: my-image-new # NOT allowed: update image
119 `),
120 expectConflictSet: fieldpath.NewSet(
121 fieldpath.MakePathOrDie("metadata", "labels", "app"),
122 fieldpath.MakePathOrDie("spec", "replicas"),
123 fieldpath.MakePathOrDie("spec", "template", "spec", "containers", fieldpath.KeyByFields("name", "my-c"), "image"),
124 ),
125 },
126 {
127 fieldManager: "kubectl",
128 lastApplied: []byte(`
129 apiVersion: apps/v1
130 kind: Deployment
131 metadata:
132 name: my-deployment
133 labels:
134 app: my-app
135 spec:
136 replicas: 3
137 selector:
138 matchLabels:
139 app: my-app
140 template:
141 metadata:
142 labels:
143 app: my-app
144 spec:
145 containers:
146 - name: my-c
147 image: my-image
148 `),
149 original: []byte(`
150 apiVersion: apps/v1
151 kind: Deployment
152 metadata:
153 name: my-deployment
154 labels:
155 app: my-app
156 spec:
157 replicas: 100 # does not match last applied
158 selector:
159 matchLabels:
160 app: my-app
161 template:
162 metadata:
163 labels:
164 app: my-app
165 spec:
166 containers:
167 - name: my-c
168 image: my-image
169 `),
170 applied: []byte(`
171 apiVersion: apps/v1
172 kind: Deployment
173 metadata:
174 name: my-deployment
175 labels:
176 app: my-new-label
177 spec:
178 replicas: 3 # expect conflict
179 template:
180 metadata:
181 labels:
182 app: my-app
183 spec:
184 containers:
185 - name: my-c
186 image: my-image
187 `),
188 expectConflictSet: fieldpath.NewSet(
189 fieldpath.MakePathOrDie("spec", "replicas"),
190 ),
191 },
192 {
193 fieldManager: "kubectl",
194 original: []byte(`
195 apiVersion: apps/v1
196 kind: Deployment
197 metadata:
198 name: my-deployment
199 labels:
200 app: my-app
201 spec:
202 replicas: 100
203 selector:
204 matchLabels:
205 app: my-app
206 template:
207 metadata:
208 labels:
209 app: my-app
210 spec:
211 containers:
212 - name: my-c
213 image: my-image
214 `),
215 applied: []byte(`
216 # applied object matches original
217
218 apiVersion: apps/v1
219 kind: Deployment
220 metadata:
221 name: my-deployment
222 labels:
223 app: my-app
224 spec:
225 replicas: 100
226 selector:
227 matchLabels:
228 app: my-app
229 template:
230 metadata:
231 labels:
232 app: my-app
233 spec:
234 containers:
235 - name: my-c
236 image: my-image
237 `),
238 },
239 {
240 fieldManager: "kubectl",
241 original: []byte(`
242 apiVersion: apps/v1
243 kind: Deployment
244 metadata:
245 name: my-deployment
246 labels:
247 app: my-app
248 spec:
249 replicas: 3
250 selector:
251 matchLabels:
252 app: my-app
253 template:
254 metadata:
255 labels:
256 app: my-app
257 spec:
258 containers:
259 - name: my-c
260 image: my-image
261 `),
262 applied: []byte(`
263 # test allowed update with no conflicts
264
265 apiVersion: apps/v1
266 kind: Deployment
267 metadata:
268 name: my-deployment
269 labels:
270 app: my-new-label # update label
271 spec:
272 replicas: 333 # update replicas
273 selector:
274 matchLabels:
275 app: my-new-label # update label
276 template:
277 metadata:
278 labels:
279 app: my-new-label # update-label
280 spec:
281 containers:
282 - name: my-c
283 image: my-image
284 `),
285 },
286 {
287 fieldManager: "not_kubectl",
288 lastApplied: []byte(`
289 # expect conflicts because field manager is NOT kubectl
290
291 apiVersion: apps/v1
292 kind: Deployment
293 metadata:
294 name: my-deployment
295 labels:
296 app: my-app
297 spec:
298 replicas: 3
299 selector:
300 matchLabels:
301 app: my-app
302 template:
303 metadata:
304 labels:
305 app: my-app
306 spec:
307 containers:
308 - name: my-c
309 image: my-image-v1
310 `),
311 original: []byte(`
312 apiVersion: apps/v1
313 kind: Deployment
314 metadata:
315 name: my-deployment
316 labels:
317 app: my-app
318 spec:
319 replicas: 100 # does not match last-applied
320 selector:
321 matchLabels:
322 app: my-app
323 template:
324 metadata:
325 labels:
326 app: my-app
327 spec:
328 containers:
329 - name: my-c
330 image: my-image-v2 # does no match last-applied
331 `),
332 applied: []byte(`
333 # test conflicts due to fields not allowed by last-applied
334
335 apiVersion: apps/v1
336 kind: Deployment
337 metadata:
338 name: my-deployment
339 labels:
340 app: my-new-label # update label
341 spec:
342 replicas: 333 # update replicas
343 selector:
344 matchLabels:
345 app: my-new-label # update label
346 template:
347 metadata:
348 labels:
349 app: my-new-label # update-label
350 spec:
351 containers:
352 - name: my-c
353 image: my-image-new # update image
354 `),
355 expectConflictSet: fieldpath.NewSet(
356 fieldpath.MakePathOrDie("metadata", "labels", "app"),
357 fieldpath.MakePathOrDie("spec", "replicas"),
358 fieldpath.MakePathOrDie("spec", "selector"),
359 fieldpath.MakePathOrDie("spec", "template", "metadata", "labels", "app"),
360 fieldpath.MakePathOrDie("spec", "template", "spec", "containers", fieldpath.KeyByFields("name", "my-c"), "image"),
361 ),
362 },
363 {
364 fieldManager: "kubectl",
365 original: []byte(`
366 apiVersion: apps/v1
367 kind: Deployment
368 metadata:
369 name: my-deployment
370 labels:
371 app: my-app
372 spec:
373 replicas: 3
374 selector:
375 matchLabels:
376 app: my-app
377 template:
378 metadata:
379 labels:
380 app: my-app
381 spec:
382 containers:
383 - name: my-c
384 image: my-image
385 `),
386 applied: []byte(`
387 # test allowed update with no conflicts
388
389 apiVersion: apps/v1
390 kind: Deployment
391 metadata:
392 name: my-deployment
393 labels:
394 app: my-new-label
395 spec:
396 replicas: 3
397 selector:
398 matchLabels:
399 app: my-app
400 template:
401 metadata:
402 labels:
403 app: my-app
404 spec:
405 containers:
406 - name: my-c
407 image: my-new-image # update image
408 `),
409 },
410 {
411 fieldManager: "not_kubectl",
412 original: []byte(`
413 apiVersion: apps/v1
414 kind: Deployment
415 metadata:
416 name: my-deployment
417 labels:
418 app: my-app
419 spec:
420 replicas: 100
421 selector:
422 matchLabels:
423 app: my-app
424 template:
425 metadata:
426 labels:
427 app: my-app
428 spec:
429 containers:
430 - name: my-c
431 image: my-image
432 `),
433 applied: []byte(`
434
435 # expect changes to fail because field manager is not kubectl
436
437 apiVersion: apps/v1
438 kind: Deployment
439 metadata:
440 name: my-deployment
441 labels:
442 app: my-new-label # update label
443 spec:
444 replicas: 3 # update replicas
445 selector:
446 matchLabels:
447 app: my-app
448 template:
449 metadata:
450 labels:
451 app: my-app
452 spec:
453 containers:
454 - name: my-c
455 image: my-new-image # update image
456 `),
457 expectConflictSet: fieldpath.NewSet(
458 fieldpath.MakePathOrDie("metadata", "labels", "app"),
459 fieldpath.MakePathOrDie("spec", "replicas"),
460 fieldpath.MakePathOrDie("spec", "template", "spec", "containers", fieldpath.KeyByFields("name", "my-c"), "image"),
461 ),
462 },
463 {
464 fieldManager: "kubectl",
465 original: []byte(`
466 apiVersion: apps/v1
467 kind: Deployment
468 metadata:
469 name: my-deployment
470 spec:
471 replicas: 3
472 `),
473 applied: []byte(`
474 apiVersion: apps/v1
475 kind: Deployment
476 metadata:
477 name: my-deployment
478 spec:
479 replicas: 100 # update replicas
480 `),
481 },
482 {
483 fieldManager: "kubectl",
484 lastApplied: []byte(`
485 apiVersion: extensions/v1beta1
486 kind: Deployment
487 metadata:
488 name: my-deployment
489 spec:
490 replicas: 3
491 `),
492 original: []byte(`
493 apiVersion: apps/v1 # expect conflict due to apiVersion mismatch with last-applied
494 kind: Deployment
495 metadata:
496 name: my-deployment
497 spec:
498 replicas: 3
499 `),
500 applied: []byte(`
501 apiVersion: apps/v1
502 kind: Deployment
503 metadata:
504 name: my-deployment
505 spec:
506 replicas: 100 # update replicas
507 `),
508 expectConflictSet: fieldpath.NewSet(
509 fieldpath.MakePathOrDie("spec", "replicas"),
510 ),
511 },
512 {
513 fieldManager: "kubectl",
514 lastApplied: []byte(`
515 apiVerison: foo
516 kind: bar
517 spec: expect conflict due to invalid object
518 `),
519 original: []byte(`
520 apiVersion: apps/v1
521 kind: Deployment
522 metadata:
523 name: my-deployment
524 spec:
525 replicas: 3
526 `),
527 applied: []byte(`
528 apiVersion: apps/v1
529 kind: Deployment
530 metadata:
531 name: my-deployment
532 spec:
533 replicas: 100 # update replicas
534 `),
535 expectConflictSet: fieldpath.NewSet(
536 fieldpath.MakePathOrDie("spec", "replicas"),
537 ),
538 },
539 {
540 fieldManager: "kubectl",
541
542 lastApplied: []byte{},
543 original: []byte(`
544 apiVersion: apps/v1
545 kind: Deployment
546 metadata:
547 name: my-deployment
548 spec:
549 replicas: 3
550 `),
551 applied: []byte(`
552 apiVersion: apps/v1
553 kind: Deployment
554 metadata:
555 name: my-deployment
556 spec:
557 replicas: 100 # update replicas
558 `),
559 expectConflictSet: fieldpath.NewSet(
560 fieldpath.MakePathOrDie("spec", "replicas"),
561 ),
562 },
563 }
564
565 testConflicts(t, f, tests)
566 }
567
568 func TestServiceApply(t *testing.T) {
569 f := managedfieldstest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "Service"))
570
571 tests := []testArgs{
572 {
573 fieldManager: "kubectl",
574 original: []byte(`
575 apiVersion: v1
576 kind: Service
577 metadata:
578 name: test
579 spec:
580 ports:
581 - name: https
582 port: 443
583 protocol: TCP
584 targetPort: 8443
585 selector:
586 old: test
587 `),
588 applied: []byte(`
589 # All accepted while using the same field manager
590
591 apiVersion: v1
592 kind: Service
593 metadata:
594 name: test
595 spec:
596 ports:
597 - name: https
598 port: 443
599 protocol: TCP
600 targetPort: 8444
601 selector:
602 new: test
603 `),
604 },
605 {
606 fieldManager: "kubectl",
607 original: []byte(`
608 apiVersion: v1
609 kind: Service
610 metadata:
611 name: test
612 spec:
613 ports:
614 - name: https
615 port: 443
616 protocol: TCP
617 targetPort: 8443
618 selector:
619 old: test
620 `),
621 applied: []byte(`
622 # Allowed to remove selectors while using the same field manager
623
624 apiVersion: v1
625 kind: Service
626 metadata:
627 name: test
628 spec:
629 ports:
630 - name: https
631 port: 443
632 protocol: TCP
633 targetPort: 8444
634 selector: {}
635 `),
636 },
637 {
638 fieldManager: "not_kubectl",
639 original: []byte(`
640 apiVersion: v1
641 kind: Service
642 metadata:
643 name: test
644 spec:
645 ports:
646 - name: https
647 port: 443
648 protocol: TCP # TODO: issue - this is a defaulted field, should not be required in a new spec
649 targetPort: 8443
650 selector:
651 old: test
652 `),
653 applied: []byte(`
654 # test selector update not allowed by last-applied
655
656 apiVersion: v1
657 kind: Service
658 metadata:
659 name: test
660 spec:
661 ports:
662 - name: https
663 port: 443
664 protocol: TCP
665 targetPort: 8444
666 selector:
667 new: test
668 `),
669 expectConflictSet: fieldpath.NewSet(
670 fieldpath.MakePathOrDie("spec", "selector"),
671 fieldpath.MakePathOrDie("spec", "ports", fieldpath.KeyByFields("port", 443, "protocol", "TCP"), "targetPort"),
672 ),
673 },
674 }
675
676 testConflicts(t, f, tests)
677 }
678
679 func TestReplicationControllerApply(t *testing.T) {
680 f := managedfieldstest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "ReplicationController"))
681
682 tests := []testArgs{
683 {
684 fieldManager: "kubectl",
685 original: []byte(`
686 apiVersion: v1
687 kind: ReplicationController
688 metadata:
689 name: test
690 spec:
691 replicas: 0
692 selector:
693 old: test
694 `),
695 applied: []byte(`
696 # All accepted while using the same field manager
697
698 apiVersion: v1
699 kind: ReplicationController
700 metadata:
701 name: test
702 spec:
703 replicas: 3
704 selector:
705 new: test
706 `),
707 },
708 {
709 fieldManager: "not_kubectl",
710 original: []byte(`
711 apiVersion: v1
712 kind: ReplicationController
713 metadata:
714 name: test
715 spec:
716 replicas: 0
717 selector:
718 old: test
719 `),
720 applied: []byte(`
721 # test selector update not allowed by last-applied
722
723 apiVersion: v1
724 kind: ReplicationController
725 metadata:
726 name: test
727 spec:
728 replicas: 3
729 selector:
730 new: test
731 `),
732 expectConflictSet: fieldpath.NewSet(
733 fieldpath.MakePathOrDie("spec", "selector"),
734 fieldpath.MakePathOrDie("spec", "replicas"),
735 ),
736 },
737 }
738
739 testConflicts(t, f, tests)
740 }
741
742 func TestPodApply(t *testing.T) {
743 f := managedfieldstest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "Pod"))
744
745 tests := []testArgs{
746 {
747 fieldManager: "kubectl",
748 original: []byte(`
749 apiVersion: v1
750 kind: Pod
751 metadata:
752 name: test
753 namespace: test
754 spec:
755 containers:
756 - args:
757 - -v=2
758 command:
759 - controller
760 image: some.registry/app:latest
761 name: doJob
762 nodeName: definetlyControlPlane
763 nodeSelector:
764 node-role.kubernetes.io/master: ""
765 `),
766 applied: []byte(`
767 # All accepted while using the same field manager
768
769 apiVersion: v1
770 kind: Pod
771 metadata:
772 name: test
773 namespace: test
774 spec:
775 containers:
776 - args:
777 - -v=2
778 command:
779 - controller
780 image: some.registry/app:latest
781 name: doJob
782 nodeSelector:
783 node-role.kubernetes.io/worker: ""
784 `),
785 },
786 {
787 fieldManager: "not_kubectl",
788 original: []byte(`
789 apiVersion: v1
790 kind: Pod
791 metadata:
792 name: test
793 namespace: test
794 spec:
795 containers:
796 - args:
797 - -v=2
798 command:
799 - controller
800 image: some.registry/app:latest
801 name: doJob
802 nodeName: definetlyControlPlane
803 nodeSelector:
804 node-role.kubernetes.io/master: ""
805 `),
806 applied: []byte(`
807 # test selector update not allowed by last-applied
808
809 apiVersion: v1
810 kind: Pod
811 metadata:
812 name: test
813 namespace: test
814 spec:
815 containers:
816 - args:
817 - -v=2
818 command:
819 - controller
820 image: some.registry/app:latest
821 name: doJob
822 nodeName: definetlyControlPlane
823 nodeSelector:
824 node-role.kubernetes.io/master: ""
825 otherNodeType: ""
826 `),
827 expectConflictSet: fieldpath.NewSet(
828 fieldpath.MakePathOrDie("spec", "nodeSelector"),
829 ),
830 },
831 {
832 fieldManager: "not_kubectl",
833 original: []byte(`
834 apiVersion: v1
835 kind: Pod
836 metadata:
837 name: test
838 namespace: test
839 spec:
840 containers:
841 - args:
842 - -v=2
843 command:
844 - controller
845 image: some.registry/app:latest
846 name: doJob
847 nodeName: definetlyControlPlane
848 nodeSelector:
849 node-role.kubernetes.io/master: ""
850 `),
851 applied: []byte(`
852 # purging selector not allowed for different manager
853
854 apiVersion: v1
855 kind: Pod
856 metadata:
857 name: test
858 namespace: test
859 spec:
860 containers:
861 - args:
862 - -v=2
863 command:
864 - controller
865 image: some.registry/app:latest
866 name: doJob
867 nodeName: another
868 nodeSelector: {}
869 `),
870 expectConflictSet: fieldpath.NewSet(
871 fieldpath.MakePathOrDie("spec", "nodeSelector"),
872 fieldpath.MakePathOrDie("spec", "nodeName"),
873 ),
874 },
875 {
876 fieldManager: "kubectl",
877 original: []byte(`
878 apiVersion: v1
879 kind: Pod
880 metadata:
881 name: test
882 namespace: test
883 spec:
884 containers:
885 - args:
886 - -v=2
887 command:
888 - controller
889 image: some.registry/app:latest
890 name: doJob
891 nodeName: definetlyControlPlane
892 nodeSelector:
893 node-role.kubernetes.io/master: ""
894 `),
895 applied: []byte(`
896 # same manager could purge nodeSelector
897
898 apiVersion: v1
899 kind: Pod
900 metadata:
901 name: test
902 namespace: test
903 spec:
904 containers:
905 - args:
906 - -v=2
907 command:
908 - controller
909 image: some.registry/app:latest
910 name: doJob
911 nodeName: another
912 nodeSelector: {}
913 `),
914 },
915 }
916
917 testConflicts(t, f, tests)
918 }
919
920 func testConflicts(t *testing.T, f managedfieldstest.TestFieldManager, tests []testArgs) {
921 for i, test := range tests {
922 t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
923 f.Reset()
924
925 originalObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
926 if err := yaml.Unmarshal(test.original, &originalObj.Object); err != nil {
927 t.Errorf("error decoding YAML: %v", err)
928 }
929
930 if test.lastApplied == nil {
931 test.lastApplied = test.original
932 }
933 if err := setLastAppliedFromEncoded(originalObj, test.lastApplied); err != nil {
934 t.Errorf("failed to set last applied: %v", err)
935 }
936
937 if err := f.Update(originalObj, "test_client_side_apply"); err != nil {
938 t.Errorf("failed to apply object: %v", err)
939 }
940
941 appliedObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
942 if err := yaml.Unmarshal(test.applied, &appliedObj.Object); err != nil {
943 t.Errorf("error decoding YAML: %v", err)
944 }
945
946 err := f.Apply(appliedObj, test.fieldManager, false)
947
948 if test.expectConflictSet == nil {
949 if err != nil {
950 t.Errorf("expected no error but got %v", err)
951 }
952 } else {
953 if err == nil || !apierrors.IsConflict(err) {
954 t.Errorf("expected to get conflicts but got %v", err)
955 }
956
957 expectedConflicts := merge.Conflicts{}
958 test.expectConflictSet.Iterate(func(p fieldpath.Path) {
959 expectedConflicts = append(expectedConflicts, merge.Conflict{
960 Manager: fmt.Sprintf(`{"manager":"test_client_side_apply","operation":"Update","apiVersion":"%s"}`, f.APIVersion()),
961 Path: p,
962 })
963 })
964 expectedConflictErr := internal.NewConflictError(expectedConflicts)
965 if !reflect.DeepEqual(expectedConflictErr, err) {
966 t.Errorf("expected to get\n%+v\nbut got\n%+v", expectedConflictErr, err)
967 }
968
969
970 err = f.Apply(appliedObj, test.fieldManager, true)
971 if err != nil {
972 t.Errorf("unexpected error during force ownership apply: %v", err)
973 }
974
975 }
976
977
978 if !apiequality.Semantic.DeepDerivative(appliedObj, f.Live()) {
979 t.Errorf("expected equal resource: \n%#v, got: \n%#v", appliedObj, f.Live())
980 }
981 })
982 }
983 }
984
985 func yamlToJSON(y []byte) (string, error) {
986 obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
987 if err := yaml.Unmarshal(y, &obj.Object); err != nil {
988 return "", fmt.Errorf("error decoding YAML: %v", err)
989 }
990 serialization, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj)
991 if err != nil {
992 return "", fmt.Errorf("error encoding object: %v", err)
993 }
994 json, err := yamlutil.ToJSON(serialization)
995 if err != nil {
996 return "", fmt.Errorf("error converting to json: %v", err)
997 }
998 return string(json), nil
999 }
1000
1001 func setLastAppliedFromEncoded(obj runtime.Object, lastApplied []byte) error {
1002 lastAppliedJSON, err := yamlToJSON(lastApplied)
1003 if err != nil {
1004 return err
1005 }
1006 return internal.SetLastApplied(obj, lastAppliedJSON)
1007 }
1008
View as plain text