1
16
17 package apply
18
19 import (
20 "bytes"
21 "encoding/json"
22 "errors"
23 "fmt"
24 "io"
25 "net/http"
26 "os"
27 "path/filepath"
28 "strings"
29 "testing"
30 "time"
31
32 "github.com/google/go-cmp/cmp"
33 "github.com/spf13/cobra"
34 "github.com/stretchr/testify/assert"
35 "github.com/stretchr/testify/require"
36 appsv1 "k8s.io/api/apps/v1"
37 corev1 "k8s.io/api/core/v1"
38 apierrors "k8s.io/apimachinery/pkg/api/errors"
39 "k8s.io/apimachinery/pkg/api/meta"
40 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
41 "k8s.io/apimachinery/pkg/runtime"
42 "k8s.io/apimachinery/pkg/runtime/schema"
43 "k8s.io/apimachinery/pkg/types"
44 "k8s.io/apimachinery/pkg/util/sets"
45 sptest "k8s.io/apimachinery/pkg/util/strategicpatch/testing"
46 "k8s.io/cli-runtime/pkg/genericclioptions"
47 "k8s.io/cli-runtime/pkg/genericiooptions"
48 "k8s.io/cli-runtime/pkg/resource"
49 dynamicfakeclient "k8s.io/client-go/dynamic/fake"
50 openapiclient "k8s.io/client-go/openapi"
51 "k8s.io/client-go/openapi/openapitest"
52 restclient "k8s.io/client-go/rest"
53 "k8s.io/client-go/rest/fake"
54 testing2 "k8s.io/client-go/testing"
55 "k8s.io/client-go/tools/clientcmd"
56 clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
57 "k8s.io/client-go/util/csaupgrade"
58 cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
59 cmdutil "k8s.io/kubectl/pkg/cmd/util"
60 "k8s.io/kubectl/pkg/scheme"
61 "k8s.io/kubectl/pkg/util/openapi"
62 utilpointer "k8s.io/utils/pointer"
63 "k8s.io/utils/strings/slices"
64 "sigs.k8s.io/yaml"
65 )
66
67 var (
68 fakeSchema = sptest.Fake{Path: filepath.Join("..", "..", "..", "testdata", "openapi", "swagger.json")}
69 fakeOpenAPIV3Legacy = sptest.OpenAPIV3Getter{Path: filepath.Join("..", "..", "..", "testdata", "openapi", "v3", "api", "v1.json")}
70 fakeOpenAPIV3AppsV1 = sptest.OpenAPIV3Getter{Path: filepath.Join("..", "..", "..", "testdata", "openapi", "v3", "apis", "apps", "v1.json")}
71 testingOpenAPISchemas = []testOpenAPISchema{AlwaysErrorsOpenAPISchema, FakeOpenAPISchema}
72
73 AlwaysErrorsOpenAPISchema = testOpenAPISchema{
74 OpenAPISchemaFn: func() (openapi.Resources, error) {
75 return nil, errors.New("cannot get openapi spec")
76 },
77 OpenAPIV3ClientFunc: func() (openapiclient.Client, error) {
78 return nil, errors.New("cannot get openapiv3 client")
79 },
80 }
81 FakeOpenAPISchema = testOpenAPISchema{
82 OpenAPISchemaFn: func() (openapi.Resources, error) {
83 s, err := fakeSchema.OpenAPISchema()
84 if err != nil {
85 return nil, err
86 }
87 return openapi.NewOpenAPIData(s)
88 },
89 OpenAPIV3ClientFunc: func() (openapiclient.Client, error) {
90 c := openapitest.NewFakeClient()
91 c.PathsMap["api/v1"] = openapitest.FakeGroupVersion{GVSpec: fakeOpenAPIV3Legacy.SchemaBytesOrDie()}
92 c.PathsMap["apis/apps/v1"] = openapitest.FakeGroupVersion{GVSpec: fakeOpenAPIV3AppsV1.SchemaBytesOrDie()}
93 return c, nil
94 },
95 }
96 AlwaysPanicSchema = testOpenAPISchema{
97 OpenAPISchemaFn: func() (openapi.Resources, error) {
98 panic("error, openAPIV2 should not be called")
99 },
100 OpenAPIV3ClientFunc: func() (openapiclient.Client, error) {
101 return &OpenAPIV3ClientAlwaysPanic{}, nil
102 },
103 }
104
105 codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
106 )
107
108 type OpenAPIV3ClientAlwaysPanic struct{}
109
110 func (o *OpenAPIV3ClientAlwaysPanic) Paths() (map[string]openapiclient.GroupVersion, error) {
111 panic("Cannot get paths")
112 }
113
114 func noopOpenAPIV3Patch(t *testing.T, f func(t *testing.T)) {
115 f(t)
116 }
117 func disableOpenAPIV3Patch(t *testing.T, f func(t *testing.T)) {
118 cmdtesting.WithAlphaEnvsDisabled([]cmdutil.FeatureGate{cmdutil.OpenAPIV3Patch}, t, f)
119 }
120
121 var applyFeatureToggles = []func(*testing.T, func(t *testing.T)){noopOpenAPIV3Patch, disableOpenAPIV3Patch}
122
123 type testOpenAPISchema struct {
124 OpenAPISchemaFn func() (openapi.Resources, error)
125 OpenAPIV3ClientFunc func() (openapiclient.Client, error)
126 }
127
128 func TestApplyExtraArgsFail(t *testing.T) {
129 f := cmdtesting.NewTestFactory()
130 defer f.Cleanup()
131
132 cmd := &cobra.Command{}
133 flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard())
134 flags.AddFlags(cmd)
135 _, err := flags.ToOptions(f, cmd, "kubectl", []string{"rc"})
136 require.EqualError(t, err, "Unexpected args: [rc]\nSee ' -h' for help and examples")
137 }
138
139 func TestAlphaEnablement(t *testing.T) {
140 alphas := map[cmdutil.FeatureGate]string{
141 cmdutil.ApplySet: "applyset",
142 }
143 for feature, flag := range alphas {
144 f := cmdtesting.NewTestFactory()
145 defer f.Cleanup()
146
147 cmd := &cobra.Command{}
148 flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard())
149 flags.AddFlags(cmd)
150 require.Nil(t, cmd.Flags().Lookup(flag), "flag %q should not be registered without the %q feature enabled", flag, feature)
151
152 cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{feature}, t, func(t *testing.T) {
153 cmd := &cobra.Command{}
154 flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard())
155 flags.AddFlags(cmd)
156 require.NotNil(t, cmd.Flags().Lookup(flag), "flag %q should be registered with the %q feature enabled", flag, feature)
157 })
158 }
159 }
160
161 func TestApplyFlagValidation(t *testing.T) {
162 tests := []struct {
163 args [][]string
164 enableAlphas []cmdutil.FeatureGate
165 expectedErr string
166 }{
167 {
168 args: [][]string{
169 {"force-conflicts", "true"},
170 },
171 expectedErr: "--force-conflicts only works with --server-side",
172 },
173 {
174 args: [][]string{
175 {"server-side", "true"},
176 {"dry-run", "client"},
177 },
178 expectedErr: "--dry-run=client doesn't work with --server-side (did you mean --dry-run=server instead?)",
179 },
180 {
181 args: [][]string{
182 {"force", "true"},
183 {"server-side", "true"},
184 },
185 expectedErr: "--force cannot be used with --server-side",
186 },
187 {
188 args: [][]string{
189 {"force", "true"},
190 {"dry-run", "server"},
191 },
192 expectedErr: "--dry-run=server cannot be used with --force",
193 },
194 {
195 args: [][]string{
196 {"all", "true"},
197 {"selector", "unused"},
198 },
199 expectedErr: "cannot set --all and --selector at the same time",
200 },
201 {
202 args: [][]string{
203 {"force", "true"},
204 {"prune", "true"},
205 {"all", "true"},
206 },
207 expectedErr: "--force cannot be used with --prune",
208 },
209 {
210 args: [][]string{
211 {"prune", "true"},
212 {"force", "true"},
213 {"applyset", "mySecret"},
214 {"namespace", "myNs"},
215 },
216 enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet},
217 expectedErr: "--force cannot be used with --prune",
218 },
219 {
220 args: [][]string{
221 {"server-side", "true"},
222 {"prune", "true"},
223 {"all", "true"},
224 },
225 expectedErr: "--prune is in alpha and doesn't currently work on objects created by server-side apply",
226 },
227 {
228 args: [][]string{
229 {"prune", "true"},
230 },
231 expectedErr: "all resources selected for prune without explicitly passing --all. To prune all resources, pass the --all flag. If you did not mean to prune all resources, specify a label selector",
232 },
233 {
234 args: [][]string{
235 {"prune", "false"},
236 {"applyset", "mySecret"},
237 {"namespace", "myNs"},
238 },
239 enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet},
240 expectedErr: "--applyset requires --prune",
241 },
242 {
243 args: [][]string{
244 {"prune", "true"},
245 {"applyset", "mySecret"},
246 {"selector", "foo=bar"},
247 {"namespace", "myNs"},
248 },
249 enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet},
250 expectedErr: "--selector is incompatible with --applyset",
251 },
252 {
253 args: [][]string{
254 {"prune", "true"},
255 {"applyset", "mySecret"},
256 {"namespace", "myNs"},
257 {"all", "true"},
258 },
259 enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet},
260 expectedErr: "--all is incompatible with --applyset",
261 },
262 {
263 args: [][]string{
264 {"prune", "true"},
265 {"applyset", "mySecret"},
266 {"namespace", "myNs"},
267 {"prune-allowlist", "core/v1/ConfigMap"},
268 },
269 enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet},
270 expectedErr: "--prune-allowlist is incompatible with --applyset",
271 },
272 }
273
274 for i, test := range tests {
275 t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
276 f := cmdtesting.NewTestFactory()
277 defer f.Cleanup()
278 f.Client = &fake.RESTClient{}
279 f.UnstructuredClient = f.Client
280 cmdtesting.WithAlphaEnvs(test.enableAlphas, t, func(t *testing.T) {
281 cmd := &cobra.Command{}
282 flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard())
283 flags.AddFlags(cmd)
284 cmd.Flags().Set("filename", "unused")
285 for _, arg := range test.args {
286 if arg[0] == "namespace" {
287 f.WithNamespace(arg[1])
288 } else {
289 cmd.Flags().Set(arg[0], arg[1])
290 }
291 }
292 o, err := flags.ToOptions(f, cmd, "kubectl", []string{})
293 if err != nil {
294 t.Fatalf("unexpected error creating apply options: %s", err)
295 }
296 err = o.Validate()
297 if err == nil {
298 t.Fatalf("missing expected error for case %d with args %+v", i, test.args)
299 }
300 if test.expectedErr != err.Error() {
301 t.Errorf("expected error %s, got %s", test.expectedErr, err)
302 }
303 })
304 })
305 }
306 }
307
308 const (
309 filenameCM = "../../../testdata/apply/cm.yaml"
310 filenameRC = "../../../testdata/apply/rc.yaml"
311 filenameRCArgs = "../../../testdata/apply/rc-args.yaml"
312 filenameRCLastAppliedArgs = "../../../testdata/apply/rc-lastapplied-args.yaml"
313 filenameRCNoAnnotation = "../../../testdata/apply/rc-no-annotation.yaml"
314 filenameRCLASTAPPLIED = "../../../testdata/apply/rc-lastapplied.yaml"
315 filenameRCManagedFieldsLA = "../../../testdata/apply/rc-managedfields-lastapplied.yaml"
316 filenameSVC = "../../../testdata/apply/service.yaml"
317 filenameRCSVC = "../../../testdata/apply/rc-service.yaml"
318 filenameNoExistRC = "../../../testdata/apply/rc-noexist.yaml"
319 filenameRCPatchTest = "../../../testdata/apply/patch.json"
320 dirName = "../../../testdata/apply/testdir"
321 filenameRCJSON = "../../../testdata/apply/rc.json"
322 filenamePodGeneratedName = "../../../testdata/apply/pod-generated-name.yaml"
323
324 filenameWidgetClientside = "../../../testdata/apply/widget-clientside.yaml"
325 filenameWidgetServerside = "../../../testdata/apply/widget-serverside.yaml"
326 filenameDeployObjServerside = "../../../testdata/apply/deploy-serverside.yaml"
327 filenameDeployObjClientside = "../../../testdata/apply/deploy-clientside.yaml"
328 filenameApplySetCR = "../../../testdata/apply/applyset-cr.yaml"
329 filenameApplySetCRD = "../../../testdata/apply/applysets-crd.yaml"
330 )
331
332 func readConfigMapList(t *testing.T, filename string) [][]byte {
333 data := readBytesFromFile(t, filename)
334 cmList := corev1.ConfigMapList{}
335 if err := runtime.DecodeInto(codec, data, &cmList); err != nil {
336 t.Fatal(err)
337 }
338
339 var listCmBytes [][]byte
340
341 for _, cm := range cmList.Items {
342 cmBytes, err := runtime.Encode(codec, &cm)
343 if err != nil {
344 t.Fatal(err)
345 }
346 listCmBytes = append(listCmBytes, cmBytes)
347 }
348
349 return listCmBytes
350 }
351
352 func readBytesFromFile(t *testing.T, filename string) []byte {
353 file, err := os.Open(filename)
354 if err != nil {
355 t.Fatal(err)
356 }
357 defer file.Close()
358
359 data, err := io.ReadAll(file)
360 if err != nil {
361 t.Fatal(err)
362 }
363
364 return data
365 }
366
367 func readReplicationController(t *testing.T, filenameRC string) (string, []byte) {
368 rcObj := readReplicationControllerFromFile(t, filenameRC)
369 metaAccessor, err := meta.Accessor(rcObj)
370 if err != nil {
371 t.Fatal(err)
372 }
373 rcBytes, err := runtime.Encode(codec, rcObj)
374 if err != nil {
375 t.Fatal(err)
376 }
377
378 return metaAccessor.GetName(), rcBytes
379 }
380
381 func readReplicationControllerFromFile(t *testing.T, filename string) *corev1.ReplicationController {
382 data := readBytesFromFile(t, filename)
383 rc := corev1.ReplicationController{}
384 if err := runtime.DecodeInto(codec, data, &rc); err != nil {
385 t.Fatal(err)
386 }
387
388 return &rc
389 }
390
391 func readUnstructuredFromFile(t *testing.T, filename string) *unstructured.Unstructured {
392 data := readBytesFromFile(t, filename)
393 unst := unstructured.Unstructured{}
394 if err := runtime.DecodeInto(codec, data, &unst); err != nil {
395 t.Fatal(err)
396 }
397 return &unst
398 }
399
400 func readServiceFromFile(t *testing.T, filename string) *corev1.Service {
401 data := readBytesFromFile(t, filename)
402 svc := corev1.Service{}
403 if err := runtime.DecodeInto(codec, data, &svc); err != nil {
404 t.Fatal(err)
405 }
406
407 return &svc
408 }
409
410 func annotateRuntimeObject(t *testing.T, originalObj, currentObj runtime.Object, kind string) (string, []byte) {
411 originalAccessor, err := meta.Accessor(originalObj)
412 if err != nil {
413 t.Fatal(err)
414 }
415
416
417
418
419
420
421
422 originalLabels := originalAccessor.GetLabels()
423 originalLabels["DELETE_ME"] = "DELETE_ME"
424 originalAccessor.SetLabels(originalLabels)
425 original, err := runtime.Encode(unstructured.NewJSONFallbackEncoder(codec), originalObj)
426 if err != nil {
427 t.Fatal(err)
428 }
429
430 currentAccessor, err := meta.Accessor(currentObj)
431 if err != nil {
432 t.Fatal(err)
433 }
434
435 currentAnnotations := currentAccessor.GetAnnotations()
436 if currentAnnotations == nil {
437 currentAnnotations = make(map[string]string)
438 }
439 currentAnnotations[corev1.LastAppliedConfigAnnotation] = string(original)
440 currentAccessor.SetAnnotations(currentAnnotations)
441 current, err := runtime.Encode(unstructured.NewJSONFallbackEncoder(codec), currentObj)
442 if err != nil {
443 t.Fatal(err)
444 }
445
446 return currentAccessor.GetName(), current
447 }
448
449 func readAndAnnotateReplicationController(t *testing.T, filename string) (string, []byte) {
450 rc1 := readReplicationControllerFromFile(t, filename)
451 rc2 := readReplicationControllerFromFile(t, filename)
452 return annotateRuntimeObject(t, rc1, rc2, "ReplicationController")
453 }
454
455 func readAndAnnotateService(t *testing.T, filename string) (string, []byte) {
456 svc1 := readServiceFromFile(t, filename)
457 svc2 := readServiceFromFile(t, filename)
458 return annotateRuntimeObject(t, svc1, svc2, "Service")
459 }
460
461 func readAndAnnotateUnstructured(t *testing.T, filename string) (string, []byte) {
462 obj1 := readUnstructuredFromFile(t, filename)
463 obj2 := readUnstructuredFromFile(t, filename)
464 return annotateRuntimeObject(t, obj1, obj2, "Widget")
465 }
466
467 func validatePatchApplication(t *testing.T, req *http.Request, patchType types.PatchType) {
468 if got, wanted := req.Header.Get("Content-Type"), string(patchType); got != wanted {
469 t.Fatalf("unexpected content-type expected: %s but actual %s\n", wanted, got)
470 }
471
472 patch, err := io.ReadAll(req.Body)
473 if err != nil {
474 t.Fatal(err)
475 }
476
477 patchMap := map[string]interface{}{}
478 if err := json.Unmarshal(patch, &patchMap); err != nil {
479 t.Fatal(err)
480 }
481
482 annotationsMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"})
483 if _, ok := annotationsMap[corev1.LastAppliedConfigAnnotation]; !ok {
484 t.Fatalf("patch does not contain annotation:\n%s\n", patch)
485 }
486
487 labelMap := walkMapPath(t, patchMap, []string{"metadata", "labels"})
488 if deleteMe, ok := labelMap["DELETE_ME"]; !ok || deleteMe != nil {
489 t.Fatalf("patch does not remove deleted key: DELETE_ME:\n%s\n", patch)
490 }
491 }
492
493 func walkMapPath(t *testing.T, start map[string]interface{}, path []string) map[string]interface{} {
494 finish := start
495 for i := 0; i < len(path); i++ {
496 var ok bool
497 finish, ok = finish[path[i]].(map[string]interface{})
498 if !ok {
499 t.Fatalf("key:%s of path:%v not found in map:%v", path[i], path, start)
500 }
501 }
502
503 return finish
504 }
505
506 func TestRunApplyPrintsValidObjectList(t *testing.T) {
507 cmdtesting.InitTestErrorHandler(t)
508 configMapList := readConfigMapList(t, filenameCM)
509 pathCM := "/namespaces/test/configmaps"
510
511 tf := cmdtesting.NewTestFactory().WithNamespace("test")
512 defer tf.Cleanup()
513
514 tf.UnstructuredClient = &fake.RESTClient{
515 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
516 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
517 switch p, m := req.URL.Path, req.Method; {
518 case strings.HasPrefix(p, pathCM) && m == "GET":
519 fallthrough
520 case strings.HasPrefix(p, pathCM) && m == "PATCH":
521 var body io.ReadCloser
522
523 switch p {
524 case pathCM + "/test0":
525 body = io.NopCloser(bytes.NewReader(configMapList[0]))
526 case pathCM + "/test1":
527 body = io.NopCloser(bytes.NewReader(configMapList[1]))
528 default:
529 t.Errorf("unexpected request to %s", p)
530 }
531
532 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
533 default:
534 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
535 return nil, nil
536 }
537 }),
538 }
539 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
540
541 ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams()
542 cmd := NewCmdApply("kubectl", tf, ioStreams)
543 cmd.Flags().Set("filename", filenameCM)
544 cmd.Flags().Set("output", "json")
545 cmd.Flags().Set("dry-run", "client")
546 cmd.Run(cmd, []string{})
547
548
549 cmList := corev1.List{}
550 if err := runtime.DecodeInto(codec, buf.Bytes(), &cmList); err != nil {
551 t.Fatal(err)
552 }
553
554 if len(cmList.Items) != 2 {
555 t.Fatalf("Expected 2 items in the result; got %d", len(cmList.Items))
556 }
557 if !strings.Contains(string(cmList.Items[0].Raw), "key1") {
558 t.Fatalf("Did not get first ConfigMap at the first position")
559 }
560 if !strings.Contains(string(cmList.Items[1].Raw), "key2") {
561 t.Fatalf("Did not get second ConfigMap at the second position")
562 }
563 }
564
565 func TestRunApplyViewLastApplied(t *testing.T) {
566 _, rcBytesWithConfig := readReplicationController(t, filenameRCLASTAPPLIED)
567 _, rcBytesWithArgs := readReplicationController(t, filenameRCLastAppliedArgs)
568 nameRC, rcBytes := readReplicationController(t, filenameRC)
569 pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
570
571 tests := []struct {
572 name, nameRC, pathRC, filePath, outputFormat, expectedErr, expectedOut, selector string
573 args []string
574 respBytes []byte
575 }{
576 {
577 name: "view with file",
578 filePath: filenameRC,
579 outputFormat: "",
580 expectedErr: "",
581 expectedOut: "test: 1234\n",
582 selector: "",
583 args: []string{},
584 respBytes: rcBytesWithConfig,
585 },
586 {
587 name: "test with file include `%s` in arguments",
588 filePath: filenameRCArgs,
589 outputFormat: "",
590 expectedErr: "",
591 expectedOut: "args: -random_flag=%s@domain.com\n",
592 selector: "",
593 args: []string{},
594 respBytes: rcBytesWithArgs,
595 },
596 {
597 name: "view with file json format",
598 filePath: filenameRC,
599 outputFormat: "json",
600 expectedErr: "",
601 expectedOut: "{\n \"test\": 1234\n}\n",
602 selector: "",
603 args: []string{},
604 respBytes: rcBytesWithConfig,
605 },
606 {
607 name: "view resource/name invalid format",
608 filePath: "",
609 outputFormat: "wide",
610 expectedErr: "error: Unexpected -o output mode: wide, the flag 'output' must be one of yaml|json\nSee 'view-last-applied -h' for help and examples",
611 expectedOut: "",
612 selector: "",
613 args: []string{"replicationcontroller", "test-rc"},
614 respBytes: rcBytesWithConfig,
615 },
616 {
617 name: "view resource with label",
618 filePath: "",
619 outputFormat: "",
620 expectedErr: "",
621 expectedOut: "test: 1234\n",
622 selector: "name=test-rc",
623 args: []string{"replicationcontroller"},
624 respBytes: rcBytesWithConfig,
625 },
626 {
627 name: "view resource without annotations",
628 filePath: "",
629 outputFormat: "",
630 expectedErr: "error: no last-applied-configuration annotation found on resource: test-rc",
631 expectedOut: "",
632 selector: "",
633 args: []string{"replicationcontroller", "test-rc"},
634 respBytes: rcBytes,
635 },
636 {
637 name: "view resource no match",
638 filePath: "",
639 outputFormat: "",
640 expectedErr: "Error from server (NotFound): the server could not find the requested resource (get replicationcontrollers no-match)",
641 expectedOut: "",
642 selector: "",
643 args: []string{"replicationcontroller", "no-match"},
644 respBytes: nil,
645 },
646 }
647 for _, test := range tests {
648 t.Run(test.name, func(t *testing.T) {
649 tf := cmdtesting.NewTestFactory().WithNamespace("test")
650 defer tf.Cleanup()
651
652 tf.UnstructuredClient = &fake.RESTClient{
653 GroupVersion: schema.GroupVersion{Version: "v1"},
654 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
655 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
656 switch p, m := req.URL.Path, req.Method; {
657 case p == pathRC && m == "GET":
658 bodyRC := io.NopCloser(bytes.NewReader(test.respBytes))
659 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
660 case p == "/namespaces/test/replicationcontrollers" && m == "GET":
661 bodyRC := io.NopCloser(bytes.NewReader(test.respBytes))
662 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
663 case p == "/namespaces/test/replicationcontrollers/no-match" && m == "GET":
664 return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Pod{})}, nil
665 case p == "/api/v1/namespaces/test" && m == "GET":
666 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Namespace{})}, nil
667 default:
668 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
669 return nil, nil
670 }
671 }),
672 }
673 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
674
675 cmdutil.BehaviorOnFatal(func(str string, code int) {
676 if str != test.expectedErr {
677 t.Errorf("%s: unexpected error: %s\nexpected: %s", test.name, str, test.expectedErr)
678 }
679 })
680
681 ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams()
682 cmd := NewCmdApplyViewLastApplied(tf, ioStreams)
683 if test.filePath != "" {
684 cmd.Flags().Set("filename", test.filePath)
685 }
686 if test.outputFormat != "" {
687 cmd.Flags().Set("output", test.outputFormat)
688 }
689 if test.selector != "" {
690 cmd.Flags().Set("selector", test.selector)
691 }
692
693 cmd.Run(cmd, test.args)
694 if buf.String() != test.expectedOut {
695 t.Fatalf("%s: unexpected output: %s\nexpected: %s", test.name, buf.String(), test.expectedOut)
696 }
697 })
698 }
699 }
700
701 func TestApplyObjectWithoutAnnotation(t *testing.T) {
702 cmdtesting.InitTestErrorHandler(t)
703 nameRC, rcBytes := readReplicationController(t, filenameRC)
704 pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
705
706 tf := cmdtesting.NewTestFactory().WithNamespace("test")
707 defer tf.Cleanup()
708
709 tf.UnstructuredClient = &fake.RESTClient{
710 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
711 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
712 switch p, m := req.URL.Path, req.Method; {
713 case p == pathRC && m == "GET":
714 bodyRC := io.NopCloser(bytes.NewReader(rcBytes))
715 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
716 case p == pathRC && m == "PATCH":
717 bodyRC := io.NopCloser(bytes.NewReader(rcBytes))
718 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
719 default:
720 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
721 return nil, nil
722 }
723 }),
724 }
725 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
726 tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc
727
728 ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
729 cmd := NewCmdApply("kubectl", tf, ioStreams)
730 cmd.Flags().Set("filename", filenameRC)
731 cmd.Flags().Set("output", "name")
732 cmd.Run(cmd, []string{})
733
734
735 expectRC := "replicationcontroller/" + nameRC + "\n"
736 expectWarning := fmt.Sprintf(warningNoLastAppliedConfigAnnotation, "replicationcontrollers/test-rc", corev1.LastAppliedConfigAnnotation, "kubectl")
737 if errBuf.String() != expectWarning {
738 t.Fatalf("unexpected non-warning: %s\nexpected: %s", errBuf.String(), expectWarning)
739 }
740 if buf.String() != expectRC {
741 t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
742 }
743 }
744
745 func TestOpenAPIV3PatchFeatureFlag(t *testing.T) {
746
747
748
749
750 cmdtesting.InitTestErrorHandler(t)
751 nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
752 pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
753
754 t.Run("test apply when a local object is specified - openapi v2 smp", func(t *testing.T) {
755 disableOpenAPIV3Patch(t, func(t *testing.T) {
756 tf := cmdtesting.NewTestFactory().WithNamespace("test")
757 defer tf.Cleanup()
758
759 tf.UnstructuredClient = &fake.RESTClient{
760 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
761 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
762 switch p, m := req.URL.Path, req.Method; {
763 case p == pathRC && m == "GET":
764 bodyRC := io.NopCloser(bytes.NewReader(currentRC))
765 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
766 case p == pathRC && m == "PATCH":
767 validatePatchApplication(t, req, types.StrategicMergePatchType)
768 bodyRC := io.NopCloser(bytes.NewReader(currentRC))
769 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
770 default:
771 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
772 return nil, nil
773 }
774 }),
775 }
776 tf.OpenAPISchemaFunc = FakeOpenAPISchema.OpenAPISchemaFn
777 tf.OpenAPIV3ClientFunc = AlwaysPanicSchema.OpenAPIV3ClientFunc
778 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
779
780 ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
781 cmd := NewCmdApply("kubectl", tf, ioStreams)
782 cmd.Flags().Set("filename", filenameRC)
783 cmd.Flags().Set("output", "name")
784 cmd.Run(cmd, []string{})
785
786
787 expectRC := "replicationcontroller/" + nameRC + "\n"
788 if buf.String() != expectRC {
789 t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
790 }
791 if errBuf.String() != "" {
792 t.Fatalf("unexpected error output: %s", errBuf.String())
793 }
794 })
795 })
796
797 }
798
799 func TestOpenAPIV3DoesNotLoadV2(t *testing.T) {
800 cmdtesting.InitTestErrorHandler(t)
801 nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
802 pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
803
804 t.Run("test apply when a local object is specified - openapi v3 smp", func(t *testing.T) {
805 tf := cmdtesting.NewTestFactory().WithNamespace("test")
806 defer tf.Cleanup()
807
808 tf.UnstructuredClient = &fake.RESTClient{
809 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
810 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
811 switch p, m := req.URL.Path, req.Method; {
812 case p == pathRC && m == "GET":
813 bodyRC := io.NopCloser(bytes.NewReader(currentRC))
814 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
815 case p == pathRC && m == "PATCH":
816 validatePatchApplication(t, req, types.StrategicMergePatchType)
817 bodyRC := io.NopCloser(bytes.NewReader(currentRC))
818 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
819 default:
820 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
821 return nil, nil
822 }
823 }),
824 }
825 tf.OpenAPISchemaFunc = AlwaysPanicSchema.OpenAPISchemaFn
826 tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc
827 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
828
829 ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
830 cmd := NewCmdApply("kubectl", tf, ioStreams)
831 cmd.Flags().Set("filename", filenameRC)
832 cmd.Flags().Set("output", "name")
833 cmd.Run(cmd, []string{})
834
835
836 expectRC := "replicationcontroller/" + nameRC + "\n"
837 if buf.String() != expectRC {
838 t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
839 }
840 if errBuf.String() != "" {
841 t.Fatalf("unexpected error output: %s", errBuf.String())
842 }
843 })
844
845 }
846
847 func TestApplyObject(t *testing.T) {
848 cmdtesting.InitTestErrorHandler(t)
849 nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
850 pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
851
852 for _, testingOpenAPISchema := range testingOpenAPISchemas {
853 for _, openAPIFeatureToggle := range applyFeatureToggles {
854 t.Run("test apply when a local object is specified - openapi v3 smp", func(t *testing.T) {
855 openAPIFeatureToggle(t, func(t *testing.T) {
856 tf := cmdtesting.NewTestFactory().WithNamespace("test")
857
858 defer tf.Cleanup()
859
860 tf.UnstructuredClient = &fake.RESTClient{
861 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
862 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
863 switch p, m := req.URL.Path, req.Method; {
864 case p == pathRC && m == "GET":
865 bodyRC := io.NopCloser(bytes.NewReader(currentRC))
866 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
867 case p == pathRC && m == "PATCH":
868 validatePatchApplication(t, req, types.StrategicMergePatchType)
869 bodyRC := io.NopCloser(bytes.NewReader(currentRC))
870 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
871 default:
872 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
873 return nil, nil
874 }
875 }),
876 }
877 tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
878 tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
879 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
880
881 ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
882 cmd := NewCmdApply("kubectl", tf, ioStreams)
883 cmd.Flags().Set("filename", filenameRC)
884 cmd.Flags().Set("output", "name")
885 cmd.Run(cmd, []string{})
886
887
888 expectRC := "replicationcontroller/" + nameRC + "\n"
889 if buf.String() != expectRC {
890 t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
891 }
892 if errBuf.String() != "" {
893 t.Fatalf("unexpected error output: %s", errBuf.String())
894 }
895 })
896 })
897 }
898 }
899 }
900
901 func TestApplyPruneObjects(t *testing.T) {
902 cmdtesting.InitTestErrorHandler(t)
903 nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
904 pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
905
906 for _, testingOpenAPISchema := range testingOpenAPISchemas {
907 for _, openAPIFeatureToggle := range applyFeatureToggles {
908
909 t.Run("test apply returns correct output", func(t *testing.T) {
910 openAPIFeatureToggle(t, func(t *testing.T) {
911 tf := cmdtesting.NewTestFactory().WithNamespace("test")
912 defer tf.Cleanup()
913
914 tf.UnstructuredClient = &fake.RESTClient{
915 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
916 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
917 switch p, m := req.URL.Path, req.Method; {
918 case p == pathRC && m == "GET":
919 bodyRC := io.NopCloser(bytes.NewReader(currentRC))
920 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
921 case p == pathRC && m == "PATCH":
922 validatePatchApplication(t, req, types.StrategicMergePatchType)
923 bodyRC := io.NopCloser(bytes.NewReader(currentRC))
924 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
925 default:
926 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
927 return nil, nil
928 }
929 }),
930 }
931 tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
932 tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
933 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
934
935 ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
936 cmd := NewCmdApply("kubectl", tf, ioStreams)
937 cmd.Flags().Set("filename", filenameRC)
938 cmd.Flags().Set("prune", "true")
939 cmd.Flags().Set("namespace", "test")
940 cmd.Flags().Set("output", "yaml")
941 cmd.Flags().Set("all", "true")
942 cmd.Run(cmd, []string{})
943
944 if !strings.Contains(buf.String(), "test-rc") {
945 t.Fatalf("unexpected output: %s\nexpected to contain: %s", buf.String(), "test-rc")
946 }
947 if errBuf.String() != "" {
948 t.Fatalf("unexpected error output: %s", errBuf.String())
949 }
950 })
951 })
952 }
953 }
954 }
955
956 func TestApplyPruneObjectsWithAllowlist(t *testing.T) {
957 cmdtesting.InitTestErrorHandler(t)
958
959
960 rc := readUnstructuredFromFile(t, filenameRC)
961 err := setLastAppliedConfigAnnotation(rc)
962 if err != nil {
963 t.Fatal(err)
964 }
965
966
967 rc2 := &unstructured.Unstructured{
968 Object: map[string]interface{}{
969 "kind": "ReplicationController",
970 "apiVersion": "v1",
971 "metadata": map[string]interface{}{
972 "name": "test-rc2",
973 "namespace": "test",
974 "uid": "uid-rc2",
975 },
976 },
977 }
978 err = setLastAppliedConfigAnnotation(rc2)
979 if err != nil {
980 t.Fatal(err)
981 }
982
983
984 cm := &unstructured.Unstructured{
985 Object: map[string]interface{}{
986 "kind": "ConfigMap",
987 "apiVersion": "v1",
988 "metadata": map[string]interface{}{
989 "name": "test-cm",
990 "namespace": "test",
991 "uid": "uid-cm",
992 },
993 },
994 }
995 err = setLastAppliedConfigAnnotation(cm)
996 if err != nil {
997 t.Fatal(err)
998 }
999
1000
1001 ns := &unstructured.Unstructured{
1002 Object: map[string]interface{}{
1003 "kind": "Namespace",
1004 "apiVersion": "v1",
1005 "metadata": map[string]interface{}{
1006 "name": "test-apply",
1007 "uid": "uid-ns",
1008 },
1009 },
1010 }
1011 err = setLastAppliedConfigAnnotation(ns)
1012 if err != nil {
1013 t.Fatal(err)
1014 }
1015
1016
1017 cmNoUID := &unstructured.Unstructured{
1018 Object: map[string]interface{}{
1019 "kind": "ConfigMap",
1020 "apiVersion": "v1",
1021 "metadata": map[string]interface{}{
1022 "name": "test-cm-nouid",
1023 "namespace": "test",
1024 },
1025 },
1026 }
1027 err = setLastAppliedConfigAnnotation(cmNoUID)
1028 if err != nil {
1029 t.Fatal(err)
1030 }
1031
1032
1033 cmNoLastApplied := &unstructured.Unstructured{
1034 Object: map[string]interface{}{
1035 "kind": "ConfigMap",
1036 "apiVersion": "v1",
1037 "metadata": map[string]interface{}{
1038 "name": "test-cm-nolastapplied",
1039 "namespace": "test",
1040 "uid": "uid-cm-nolastapplied",
1041 },
1042 },
1043 }
1044
1045 testCases := map[string]struct {
1046 currentResources []runtime.Object
1047 pruneAllowlist []string
1048 namespace string
1049 expectedPrunedResources []string
1050 expectedOutputs []string
1051 }{
1052 "prune without namespace and allowlist should delete resources that are not in the specified file": {
1053 currentResources: []runtime.Object{rc, rc2, cm, ns},
1054 expectedPrunedResources: []string{"test/test-cm", "test/test-rc2", "/test-apply"},
1055 expectedOutputs: []string{
1056 "replicationcontroller/test-rc unchanged",
1057 "configmap/test-cm pruned",
1058 "replicationcontroller/test-rc2 pruned",
1059 "namespace/test-apply pruned",
1060 },
1061 },
1062
1063
1064 "prune with namespace and without allowlist should delete resources that are not in the specified file": {
1065 currentResources: []runtime.Object{rc, rc2, cm, ns},
1066 namespace: "test",
1067 expectedPrunedResources: []string{"test/test-cm", "test/test-rc2", "/test-apply"},
1068 expectedOutputs: []string{
1069 "replicationcontroller/test-rc unchanged",
1070 "configmap/test-cm pruned",
1071 "replicationcontroller/test-rc2 pruned",
1072 "namespace/test-apply pruned",
1073 },
1074 },
1075
1076 "prune with namespace and allowlist should delete all matching resources": {
1077 currentResources: []runtime.Object{rc, cm, ns},
1078 pruneAllowlist: []string{"core/v1/ConfigMap", "core/v1/Namespace"},
1079 namespace: "test",
1080 expectedPrunedResources: []string{"test/test-cm", "/test-apply"},
1081 expectedOutputs: []string{
1082 "replicationcontroller/test-rc unchanged",
1083 "configmap/test-cm pruned",
1084 "namespace/test-apply pruned",
1085 },
1086 },
1087 "prune with allowlist should delete only matching resources": {
1088 currentResources: []runtime.Object{rc, rc2, cm},
1089 pruneAllowlist: []string{"core/v1/ConfigMap"},
1090 namespace: "test",
1091 expectedPrunedResources: []string{"test/test-cm"},
1092 expectedOutputs: []string{
1093 "replicationcontroller/test-rc unchanged",
1094 "configmap/test-cm pruned",
1095 },
1096 },
1097 "prune with allowlist specifying the same resource type multiple times should not fail": {
1098 currentResources: []runtime.Object{rc, rc2, cm},
1099 pruneAllowlist: []string{"core/v1/ConfigMap", "core/v1/ConfigMap"},
1100 namespace: "test",
1101 expectedPrunedResources: []string{"test/test-cm"},
1102 expectedOutputs: []string{
1103 "replicationcontroller/test-rc unchanged",
1104 "configmap/test-cm pruned",
1105 },
1106 },
1107 "prune with allowlist should not delete resources that exist in the specified file": {
1108 currentResources: []runtime.Object{rc, rc2, cm},
1109 pruneAllowlist: []string{"core/v1/ReplicationController"},
1110 namespace: "test",
1111 expectedPrunedResources: []string{"test/test-rc2"},
1112 expectedOutputs: []string{
1113 "replicationcontroller/test-rc unchanged",
1114 "replicationcontroller/test-rc2 pruned",
1115 },
1116 },
1117 "prune with allowlist specifying multiple resource types should delete matching resources": {
1118 currentResources: []runtime.Object{rc, rc2, cm},
1119 pruneAllowlist: []string{"core/v1/ConfigMap", "core/v1/ReplicationController"},
1120 namespace: "test",
1121 expectedPrunedResources: []string{"test/test-cm", "test/test-rc2"},
1122 expectedOutputs: []string{
1123 "replicationcontroller/test-rc unchanged",
1124 "configmap/test-cm pruned",
1125 "replicationcontroller/test-rc2 pruned",
1126 },
1127 },
1128 "prune should not delete resources that are missing a UID": {
1129 currentResources: []runtime.Object{rc, cm, cmNoUID},
1130 expectedPrunedResources: []string{"test/test-cm"},
1131 expectedOutputs: []string{
1132 "replicationcontroller/test-rc unchanged",
1133 "configmap/test-cm pruned",
1134 },
1135 },
1136 "prune should not delete resources that are missing the last applied config annotation": {
1137 currentResources: []runtime.Object{rc, cm, cmNoLastApplied},
1138 expectedPrunedResources: []string{"test/test-cm"},
1139 expectedOutputs: []string{
1140 "replicationcontroller/test-rc unchanged",
1141 "configmap/test-cm pruned",
1142 },
1143 },
1144 }
1145
1146 for testCaseName, tc := range testCases {
1147 for _, testingOpenAPISchema := range testingOpenAPISchemas {
1148 t.Run(testCaseName, func(t *testing.T) {
1149 tf := cmdtesting.NewTestFactory().WithNamespace("test")
1150 defer tf.Cleanup()
1151
1152 tf.UnstructuredClient = &fake.RESTClient{
1153 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
1154 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
1155 switch p, m := req.URL.Path, req.Method; {
1156 case p == "/namespaces/test/replicationcontrollers/test-rc" && m == "GET":
1157 encoded := runtime.EncodeOrDie(unstructured.UnstructuredJSONScheme, rc)
1158 bodyRC := io.NopCloser(strings.NewReader(encoded))
1159 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
1160 case p == "/namespaces/test/replicationcontrollers/test-rc" && m == "PATCH":
1161 encoded := runtime.EncodeOrDie(unstructured.UnstructuredJSONScheme, rc)
1162 bodyRC := io.NopCloser(strings.NewReader(encoded))
1163 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
1164 default:
1165 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
1166 return nil, nil
1167 }
1168 }),
1169 }
1170 tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
1171 tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
1172 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
1173
1174 for _, resource := range tc.currentResources {
1175 if err := tf.FakeDynamicClient.Tracker().Add(resource); err != nil {
1176 t.Fatal(err)
1177 }
1178 }
1179
1180 ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
1181 cmd := NewCmdApply("kubectl", tf, ioStreams)
1182 cmd.Flags().Set("filename", filenameRC)
1183 cmd.Flags().Set("prune", "true")
1184 cmd.Flags().Set("namespace", tc.namespace)
1185 cmd.Flags().Set("all", "true")
1186 for _, allow := range tc.pruneAllowlist {
1187 cmd.Flags().Set("prune-allowlist", allow)
1188 }
1189 cmd.Run(cmd, []string{})
1190
1191 if errBuf.String() != "" {
1192 t.Fatalf("unexpected error output: %s", errBuf.String())
1193 }
1194
1195 actualOutput := buf.String()
1196 for _, expectedOutput := range tc.expectedOutputs {
1197 if !strings.Contains(actualOutput, expectedOutput) {
1198 t.Fatalf("expected output to contain %q, but it did not. Actual Output:\n%s", expectedOutput, actualOutput)
1199 }
1200 }
1201
1202 var prunedResources []string
1203 for _, action := range tf.FakeDynamicClient.Actions() {
1204 if action.GetVerb() == "delete" {
1205 deleteAction := action.(testing2.DeleteAction)
1206 prunedResources = append(prunedResources, deleteAction.GetNamespace()+"/"+deleteAction.GetName())
1207 }
1208 }
1209
1210
1211 for _, resource := range prunedResources {
1212 if !slices.Contains(tc.expectedPrunedResources, resource) {
1213 t.Fatalf("expected %s not to be pruned, but it was", resource)
1214 }
1215 }
1216
1217
1218 for _, resource := range tc.expectedPrunedResources {
1219 if !slices.Contains(prunedResources, resource) {
1220 t.Fatalf("expected %s to be pruned, but it was not", resource)
1221 }
1222 }
1223
1224 })
1225 }
1226 }
1227 }
1228
1229 func setLastAppliedConfigAnnotation(obj runtime.Object) error {
1230 accessor, err := meta.Accessor(obj)
1231 if err != nil {
1232 return err
1233 }
1234 annotations := accessor.GetAnnotations()
1235 if annotations == nil {
1236 annotations = make(map[string]string)
1237 accessor.SetAnnotations(annotations)
1238 }
1239 annotations[corev1.LastAppliedConfigAnnotation] = runtime.EncodeOrDie(unstructured.NewJSONFallbackEncoder(codec), obj)
1240 accessor.SetAnnotations(annotations)
1241 return nil
1242 }
1243
1244
1245
1246 func TestApplyCSAMigration(t *testing.T) {
1247 cmdtesting.InitTestErrorHandler(t)
1248 nameRC, rcWithManagedFields := readAndAnnotateReplicationController(t, filenameRCManagedFieldsLA)
1249 pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
1250
1251 for _, openAPIFeatureToggle := range applyFeatureToggles {
1252 openAPIFeatureToggle(t, func(t *testing.T) {
1253 tf := cmdtesting.NewTestFactory().WithNamespace("test")
1254 defer tf.Cleanup()
1255
1256
1257
1258
1259
1260 postPatchObj := &unstructured.Unstructured{}
1261 err := json.Unmarshal(rcWithManagedFields, &postPatchObj.Object)
1262 require.NoError(t, err)
1263
1264 expectedPatch, err := csaupgrade.UpgradeManagedFieldsPatch(postPatchObj, sets.New(FieldManagerClientSideApply), "kubectl")
1265 require.NoError(t, err)
1266
1267 err = csaupgrade.UpgradeManagedFields(postPatchObj, sets.New("kubectl-client-side-apply"), "kubectl")
1268 require.NoError(t, err)
1269
1270 postPatchData, err := json.Marshal(postPatchObj)
1271 require.NoError(t, err)
1272
1273 patches := 0
1274 targetPatches := 2
1275 applies := 0
1276
1277 tf.UnstructuredClient = &fake.RESTClient{
1278 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
1279 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
1280 switch p, m := req.URL.Path, req.Method; {
1281 case p == pathRC && m == "GET":
1282
1283
1284 if patches < targetPatches {
1285 bodyRC := io.NopCloser(bytes.NewReader(rcWithManagedFields))
1286 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
1287 }
1288
1289 t.Fatalf("should not do a fetch in serverside-apply")
1290 return nil, nil
1291 case p == pathRC && m == "PATCH":
1292 if got := req.Header.Get("Content-Type"); got == string(types.ApplyPatchType) {
1293 defer func() {
1294 applies += 1
1295 }()
1296
1297 switch applies {
1298 case 0:
1299
1300
1301 bodyRC := io.NopCloser(bytes.NewReader(rcWithManagedFields))
1302 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
1303 case 1:
1304
1305
1306
1307
1308
1309
1310 bodyRC := io.NopCloser(bytes.NewReader(rcWithManagedFields))
1311 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
1312 case 2, 3:
1313
1314
1315 bodyRC := io.NopCloser(bytes.NewReader(postPatchData))
1316 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
1317 default:
1318 require.Fail(t, "sent more apply requests than expected")
1319 return &http.Response{StatusCode: http.StatusBadRequest, Header: cmdtesting.DefaultHeader()}, nil
1320 }
1321 } else if got == string(types.JSONPatchType) {
1322 defer func() {
1323 patches += 1
1324 }()
1325
1326
1327 body, err := io.ReadAll(req.Body)
1328 require.NoError(t, err)
1329 require.Equal(t, expectedPatch, body)
1330
1331 switch patches {
1332 case targetPatches - 1:
1333 bodyRC := io.NopCloser(bytes.NewReader(postPatchData))
1334 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
1335 default:
1336
1337 return &http.Response{StatusCode: http.StatusConflict, Header: cmdtesting.DefaultHeader()}, nil
1338
1339 }
1340 } else {
1341 t.Fatalf("unexpected content-type: %s\n", got)
1342 return nil, nil
1343 }
1344
1345 default:
1346 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
1347 return nil, nil
1348 }
1349 }),
1350 }
1351 tf.OpenAPISchemaFunc = FakeOpenAPISchema.OpenAPISchemaFn
1352 tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc
1353 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
1354
1355 ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
1356 cmd := NewCmdApply("kubectl", tf, ioStreams)
1357 cmd.Flags().Set("filename", filenameRC)
1358 cmd.Flags().Set("output", "yaml")
1359 cmd.Flags().Set("server-side", "true")
1360 cmd.Flags().Set("show-managed-fields", "true")
1361 cmd.Run(cmd, []string{})
1362
1363
1364 require.Equal(t, targetPatches, patches, "should retry as many times as a conflict was returned")
1365 require.Equal(t, 3, applies, "should perform specified # of apply calls upon migration")
1366 require.Empty(t, errBuf.String())
1367
1368
1369
1370
1371 rc := &corev1.ReplicationController{}
1372 if err := runtime.DecodeInto(codec, buf.Bytes(), rc); err != nil {
1373 t.Fatal(err)
1374 }
1375
1376 upgradedRC := rc.DeepCopyObject()
1377 err = csaupgrade.UpgradeManagedFields(upgradedRC, sets.New("kubectl-client-side-apply"), "kubectl")
1378 require.NoError(t, err)
1379 require.NotEmpty(t, rc.ManagedFields)
1380 require.Equal(t, rc, upgradedRC, "upgrading should be no-op in future")
1381
1382
1383
1384 ioStreams, _, _, errBuf = genericiooptions.NewTestIOStreams()
1385 cmd = NewCmdApply("kubectl", tf, ioStreams)
1386 cmd.Flags().Set("filename", filenameRC)
1387 cmd.Flags().Set("output", "yaml")
1388 cmd.Flags().Set("server-side", "true")
1389 cmd.Flags().Set("show-managed-fields", "true")
1390 cmd.Run(cmd, []string{})
1391
1392 require.Empty(t, errBuf)
1393 require.Equal(t, 4, applies, "only a single call to server-side apply should have been performed")
1394 require.Equal(t, targetPatches, patches, "no more json patches should have been needed")
1395 })
1396 }
1397 }
1398
1399 func TestApplyObjectOutput(t *testing.T) {
1400 cmdtesting.InitTestErrorHandler(t)
1401 nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
1402 pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
1403
1404
1405 postPatchObj := &unstructured.Unstructured{}
1406 if err := json.Unmarshal(currentRC, &postPatchObj.Object); err != nil {
1407 t.Fatal(err)
1408 }
1409 postPatchLabels := postPatchObj.GetLabels()
1410 if postPatchLabels == nil {
1411 postPatchLabels = map[string]string{}
1412 }
1413 postPatchLabels["post-patch"] = "value"
1414 postPatchObj.SetLabels(postPatchLabels)
1415 postPatchData, err := json.Marshal(postPatchObj)
1416 if err != nil {
1417 t.Fatal(err)
1418 }
1419
1420 for _, testingOpenAPISchema := range testingOpenAPISchemas {
1421 for _, openAPIFeatureToggle := range applyFeatureToggles {
1422 t.Run("test apply returns correct output", func(t *testing.T) {
1423 openAPIFeatureToggle(t, func(t *testing.T) {
1424 tf := cmdtesting.NewTestFactory().WithNamespace("test")
1425 defer tf.Cleanup()
1426
1427 tf.UnstructuredClient = &fake.RESTClient{
1428 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
1429 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
1430 switch p, m := req.URL.Path, req.Method; {
1431 case p == pathRC && m == "GET":
1432 bodyRC := io.NopCloser(bytes.NewReader(currentRC))
1433 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
1434 case p == pathRC && m == "PATCH":
1435 validatePatchApplication(t, req, types.StrategicMergePatchType)
1436 bodyRC := io.NopCloser(bytes.NewReader(postPatchData))
1437 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
1438 default:
1439 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
1440 return nil, nil
1441 }
1442 }),
1443 }
1444 tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
1445 tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
1446 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
1447
1448 ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
1449 cmd := NewCmdApply("kubectl", tf, ioStreams)
1450 cmd.Flags().Set("filename", filenameRC)
1451 cmd.Flags().Set("output", "yaml")
1452 cmd.Run(cmd, []string{})
1453
1454 if !strings.Contains(buf.String(), "test-rc") {
1455 t.Fatalf("unexpected output: %s\nexpected to contain: %s", buf.String(), "test-rc")
1456 }
1457 if !strings.Contains(buf.String(), "post-patch: value") {
1458 t.Fatalf("unexpected output: %s\nexpected to contain: %s", buf.String(), "post-patch: value")
1459 }
1460 if errBuf.String() != "" {
1461 t.Fatalf("unexpected error output: %s", errBuf.String())
1462 }
1463 })
1464 })
1465 }
1466 }
1467 }
1468
1469 func TestApplyRetry(t *testing.T) {
1470 cmdtesting.InitTestErrorHandler(t)
1471 nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
1472 pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
1473
1474 for _, testingOpenAPISchema := range testingOpenAPISchemas {
1475 for _, openAPIFeatureToggle := range applyFeatureToggles {
1476
1477 t.Run("test apply retries on conflict error", func(t *testing.T) {
1478 openAPIFeatureToggle(t, func(t *testing.T) {
1479 firstPatch := true
1480 retry := false
1481 getCount := 0
1482 tf := cmdtesting.NewTestFactory().WithNamespace("test")
1483 defer tf.Cleanup()
1484
1485 tf.UnstructuredClient = &fake.RESTClient{
1486 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
1487 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
1488 switch p, m := req.URL.Path, req.Method; {
1489 case p == pathRC && m == "GET":
1490 getCount++
1491 bodyRC := io.NopCloser(bytes.NewReader(currentRC))
1492 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
1493 case p == pathRC && m == "PATCH":
1494 if firstPatch {
1495 firstPatch = false
1496 statusErr := apierrors.NewConflict(schema.GroupResource{Group: "", Resource: "rc"}, "test-rc", fmt.Errorf("the object has been modified. Please apply at first"))
1497 bodyBytes, _ := json.Marshal(statusErr)
1498 bodyErr := io.NopCloser(bytes.NewReader(bodyBytes))
1499 return &http.Response{StatusCode: http.StatusConflict, Header: cmdtesting.DefaultHeader(), Body: bodyErr}, nil
1500 }
1501 retry = true
1502 validatePatchApplication(t, req, types.StrategicMergePatchType)
1503 bodyRC := io.NopCloser(bytes.NewReader(currentRC))
1504 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
1505 default:
1506 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
1507 return nil, nil
1508 }
1509 }),
1510 }
1511 tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
1512 tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
1513 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
1514
1515 ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
1516 cmd := NewCmdApply("kubectl", tf, ioStreams)
1517 cmd.Flags().Set("filename", filenameRC)
1518 cmd.Flags().Set("output", "name")
1519 cmd.Run(cmd, []string{})
1520
1521 if !retry || getCount != 2 {
1522 t.Fatalf("apply didn't retry when get conflict error")
1523 }
1524
1525
1526 expectRC := "replicationcontroller/" + nameRC + "\n"
1527 if buf.String() != expectRC {
1528 t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
1529 }
1530 if errBuf.String() != "" {
1531 t.Fatalf("unexpected error output: %s", errBuf.String())
1532 }
1533 })
1534 })
1535 }
1536 }
1537 }
1538
1539 func TestApplyNonExistObject(t *testing.T) {
1540 nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
1541 pathRC := "/namespaces/test/replicationcontrollers"
1542 pathNameRC := pathRC + "/" + nameRC
1543
1544 tf := cmdtesting.NewTestFactory().WithNamespace("test")
1545 defer tf.Cleanup()
1546
1547 tf.UnstructuredClient = &fake.RESTClient{
1548 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
1549 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
1550 switch p, m := req.URL.Path, req.Method; {
1551 case p == "/api/v1/namespaces/test" && m == "GET":
1552 return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(nil))}, nil
1553 case p == pathNameRC && m == "GET":
1554 return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(nil))}, nil
1555 case p == pathRC && m == "POST":
1556 bodyRC := io.NopCloser(bytes.NewReader(currentRC))
1557 return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
1558 default:
1559 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
1560 return nil, nil
1561 }
1562 }),
1563 }
1564 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
1565
1566 ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams()
1567 cmd := NewCmdApply("kubectl", tf, ioStreams)
1568 cmd.Flags().Set("filename", filenameRC)
1569 cmd.Flags().Set("output", "name")
1570 cmd.Run(cmd, []string{})
1571
1572
1573 expectRC := "replicationcontroller/" + nameRC + "\n"
1574 if buf.String() != expectRC {
1575 t.Errorf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
1576 }
1577 }
1578
1579 func TestApplyEmptyPatch(t *testing.T) {
1580 cmdtesting.InitTestErrorHandler(t)
1581 nameRC, _ := readAndAnnotateReplicationController(t, filenameRC)
1582 pathRC := "/namespaces/test/replicationcontrollers"
1583 pathNameRC := pathRC + "/" + nameRC
1584
1585 verifyPost := false
1586
1587 var body []byte
1588
1589 tf := cmdtesting.NewTestFactory().WithNamespace("test")
1590 defer tf.Cleanup()
1591
1592 tf.UnstructuredClient = &fake.RESTClient{
1593 GroupVersion: schema.GroupVersion{Version: "v1"},
1594 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
1595 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
1596 switch p, m := req.URL.Path, req.Method; {
1597 case p == "/api/v1/namespaces/test" && m == "GET":
1598 return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(nil))}, nil
1599 case p == pathNameRC && m == "GET":
1600 if body == nil {
1601 return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(nil))}, nil
1602 }
1603 bodyRC := io.NopCloser(bytes.NewReader(body))
1604 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
1605 case p == pathRC && m == "POST":
1606 body, _ = io.ReadAll(req.Body)
1607 verifyPost = true
1608 bodyRC := io.NopCloser(bytes.NewReader(body))
1609 return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
1610 default:
1611 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
1612 return nil, nil
1613 }
1614 }),
1615 }
1616 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
1617
1618
1619 ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams()
1620 cmd := NewCmdApply("kubectl", tf, ioStreams)
1621 cmd.Flags().Set("filename", filenameRC)
1622 cmd.Flags().Set("output", "name")
1623 cmd.Run(cmd, []string{})
1624
1625 expectRC := "replicationcontroller/" + nameRC + "\n"
1626 if buf.String() != expectRC {
1627 t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
1628 }
1629 if !verifyPost {
1630 t.Fatal("No server-side post call detected")
1631 }
1632
1633
1634 ioStreams, _, buf, _ = genericiooptions.NewTestIOStreams()
1635 cmd = NewCmdApply("kubectl", tf, ioStreams)
1636 cmd.Flags().Set("filename", filenameRC)
1637 cmd.Flags().Set("output", "name")
1638 cmd.Run(cmd, []string{})
1639
1640 if buf.String() != expectRC {
1641 t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
1642 }
1643 }
1644
1645 func TestApplyMultipleObjectsAsList(t *testing.T) {
1646 testApplyMultipleObjects(t, true)
1647 }
1648
1649 func TestApplyMultipleObjectsAsFiles(t *testing.T) {
1650 testApplyMultipleObjects(t, false)
1651 }
1652
1653 func testApplyMultipleObjects(t *testing.T, asList bool) {
1654 nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
1655 pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
1656
1657 nameSVC, currentSVC := readAndAnnotateService(t, filenameSVC)
1658 pathSVC := "/namespaces/test/services/" + nameSVC
1659
1660 for _, testingOpenAPISchema := range testingOpenAPISchemas {
1661 t.Run("test apply on multiple objects", func(t *testing.T) {
1662 tf := cmdtesting.NewTestFactory().WithNamespace("test")
1663 defer tf.Cleanup()
1664
1665 tf.UnstructuredClient = &fake.RESTClient{
1666 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
1667 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
1668 switch p, m := req.URL.Path, req.Method; {
1669 case p == pathRC && m == "GET":
1670 bodyRC := io.NopCloser(bytes.NewReader(currentRC))
1671 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
1672 case p == pathRC && m == "PATCH":
1673 validatePatchApplication(t, req, types.StrategicMergePatchType)
1674 bodyRC := io.NopCloser(bytes.NewReader(currentRC))
1675 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
1676 case p == pathSVC && m == "GET":
1677 bodySVC := io.NopCloser(bytes.NewReader(currentSVC))
1678 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodySVC}, nil
1679 case p == pathSVC && m == "PATCH":
1680 validatePatchApplication(t, req, types.StrategicMergePatchType)
1681 bodySVC := io.NopCloser(bytes.NewReader(currentSVC))
1682 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodySVC}, nil
1683 default:
1684 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
1685 return nil, nil
1686 }
1687 }),
1688 }
1689 tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
1690 tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
1691 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
1692
1693 ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
1694 cmd := NewCmdApply("kubectl", tf, ioStreams)
1695 if asList {
1696 cmd.Flags().Set("filename", filenameRCSVC)
1697 } else {
1698 cmd.Flags().Set("filename", filenameRC)
1699 cmd.Flags().Set("filename", filenameSVC)
1700 }
1701 cmd.Flags().Set("output", "name")
1702
1703 cmd.Run(cmd, []string{})
1704
1705
1706 expectRC := "replicationcontroller/" + nameRC + "\n"
1707 expectSVC := "service/" + nameSVC + "\n"
1708
1709 expectOne := expectRC + expectSVC
1710 expectTwo := expectSVC + expectRC
1711 if buf.String() != expectOne && buf.String() != expectTwo {
1712 t.Fatalf("unexpected output: %s\nexpected: %s OR %s", buf.String(), expectOne, expectTwo)
1713 }
1714 if errBuf.String() != "" {
1715 t.Fatalf("unexpected error output: %s", errBuf.String())
1716 }
1717 })
1718 }
1719 }
1720
1721 func readDeploymentFromFile(t *testing.T, file string) []byte {
1722 raw := readBytesFromFile(t, file)
1723 obj := &appsv1.Deployment{}
1724 if err := runtime.DecodeInto(codec, raw, obj); err != nil {
1725 t.Fatal(err)
1726 }
1727 objJSON, err := runtime.Encode(codec, obj)
1728 if err != nil {
1729 t.Fatal(err)
1730 }
1731 return objJSON
1732 }
1733
1734 func TestApplyNULLPreservation(t *testing.T) {
1735 cmdtesting.InitTestErrorHandler(t)
1736 deploymentName := "nginx-deployment"
1737 deploymentPath := "/namespaces/test/deployments/" + deploymentName
1738
1739 verifiedPatch := false
1740 deploymentBytes := readDeploymentFromFile(t, filenameDeployObjServerside)
1741
1742 for _, testingOpenAPISchema := range testingOpenAPISchemas {
1743 for _, openAPIFeatureToggle := range applyFeatureToggles {
1744
1745 t.Run("test apply preserves NULL fields", func(t *testing.T) {
1746 openAPIFeatureToggle(t, func(t *testing.T) {
1747 tf := cmdtesting.NewTestFactory().WithNamespace("test")
1748 defer tf.Cleanup()
1749
1750 tf.UnstructuredClient = &fake.RESTClient{
1751 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
1752 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
1753 switch p, m := req.URL.Path, req.Method; {
1754 case p == deploymentPath && m == "GET":
1755 body := io.NopCloser(bytes.NewReader(deploymentBytes))
1756 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
1757 case p == deploymentPath && m == "PATCH":
1758 patch, err := io.ReadAll(req.Body)
1759 if err != nil {
1760 t.Fatal(err)
1761 }
1762
1763 patchMap := map[string]interface{}{}
1764 if err := json.Unmarshal(patch, &patchMap); err != nil {
1765 t.Fatal(err)
1766 }
1767 annotationMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"})
1768 if _, ok := annotationMap[corev1.LastAppliedConfigAnnotation]; !ok {
1769 t.Fatalf("patch does not contain annotation:\n%s\n", patch)
1770 }
1771 strategy := walkMapPath(t, patchMap, []string{"spec", "strategy"})
1772 if value, ok := strategy["rollingUpdate"]; !ok || value != nil {
1773 t.Fatalf("patch did not retain null value in key: rollingUpdate:\n%s\n", patch)
1774 }
1775 verifiedPatch = true
1776
1777
1778
1779
1780 body := io.NopCloser(bytes.NewReader(deploymentBytes))
1781 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
1782 default:
1783 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
1784 return nil, nil
1785 }
1786 }),
1787 }
1788 tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
1789 tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
1790 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
1791
1792 ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
1793 cmd := NewCmdApply("kubectl", tf, ioStreams)
1794 cmd.Flags().Set("filename", filenameDeployObjClientside)
1795 cmd.Flags().Set("output", "name")
1796
1797 cmd.Run(cmd, []string{})
1798
1799 expected := "deployment.apps/" + deploymentName + "\n"
1800 if buf.String() != expected {
1801 t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
1802 }
1803 if errBuf.String() != "" {
1804 t.Fatalf("unexpected error output: %s", errBuf.String())
1805 }
1806 if !verifiedPatch {
1807 t.Fatal("No server-side patch call detected")
1808 }
1809 })
1810 })
1811 }
1812 }
1813 }
1814
1815
1816 func TestUnstructuredApply(t *testing.T) {
1817 cmdtesting.InitTestErrorHandler(t)
1818 name, curr := readAndAnnotateUnstructured(t, filenameWidgetClientside)
1819 path := "/namespaces/test/widgets/" + name
1820
1821 verifiedPatch := false
1822
1823 for _, testingOpenAPISchema := range testingOpenAPISchemas {
1824 for _, openAPIFeatureToggle := range applyFeatureToggles {
1825
1826 t.Run("test apply works correctly with unstructured objects", func(t *testing.T) {
1827 openAPIFeatureToggle(t, func(t *testing.T) {
1828 tf := cmdtesting.NewTestFactory().WithNamespace("test")
1829 defer tf.Cleanup()
1830
1831 tf.UnstructuredClient = &fake.RESTClient{
1832 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
1833 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
1834 switch p, m := req.URL.Path, req.Method; {
1835 case p == path && m == "GET":
1836 body := io.NopCloser(bytes.NewReader(curr))
1837 return &http.Response{
1838 StatusCode: http.StatusOK,
1839 Header: cmdtesting.DefaultHeader(),
1840 Body: body}, nil
1841 case p == path && m == "PATCH":
1842 validatePatchApplication(t, req, types.MergePatchType)
1843 verifiedPatch = true
1844
1845 body := io.NopCloser(bytes.NewReader(curr))
1846 return &http.Response{
1847 StatusCode: http.StatusOK,
1848 Header: cmdtesting.DefaultHeader(),
1849 Body: body}, nil
1850 default:
1851 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
1852 return nil, nil
1853 }
1854 }),
1855 }
1856 tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
1857 tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
1858 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
1859
1860 ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
1861 cmd := NewCmdApply("kubectl", tf, ioStreams)
1862 cmd.Flags().Set("filename", filenameWidgetClientside)
1863 cmd.Flags().Set("output", "name")
1864 cmd.Run(cmd, []string{})
1865
1866 expected := "widget.unit-test.test.com/" + name + "\n"
1867 if buf.String() != expected {
1868 t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
1869 }
1870 if errBuf.String() != "" {
1871 t.Fatalf("unexpected error output: %s", errBuf.String())
1872 }
1873 if !verifiedPatch {
1874 t.Fatal("No server-side patch call detected")
1875 }
1876 })
1877 })
1878 }
1879 }
1880 }
1881
1882
1883 func TestUnstructuredIdempotentApply(t *testing.T) {
1884 cmdtesting.InitTestErrorHandler(t)
1885
1886 serversideObject := readUnstructuredFromFile(t, filenameWidgetServerside)
1887 serversideData, err := runtime.Encode(unstructured.NewJSONFallbackEncoder(codec), serversideObject)
1888 if err != nil {
1889 t.Fatal(err)
1890 }
1891 path := "/namespaces/test/widgets/widget"
1892
1893 for _, testingOpenAPISchema := range testingOpenAPISchemas {
1894 for _, openAPIFeatureToggle := range applyFeatureToggles {
1895
1896 t.Run("test repeated apply operations on an unstructured object", func(t *testing.T) {
1897 openAPIFeatureToggle(t, func(t *testing.T) {
1898
1899 tf := cmdtesting.NewTestFactory().WithNamespace("test")
1900 defer tf.Cleanup()
1901
1902 tf.UnstructuredClient = &fake.RESTClient{
1903 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
1904 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
1905 switch p, m := req.URL.Path, req.Method; {
1906 case p == path && m == "GET":
1907 body := io.NopCloser(bytes.NewReader(serversideData))
1908 return &http.Response{
1909 StatusCode: http.StatusOK,
1910 Header: cmdtesting.DefaultHeader(),
1911 Body: body}, nil
1912 case p == path && m == "PATCH":
1913
1914
1915 patch, err := io.ReadAll(req.Body)
1916 if err != nil {
1917 t.Fatal(err)
1918 }
1919 t.Fatalf("Unexpected Patch: %s", patch)
1920 return nil, nil
1921 default:
1922 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
1923 return nil, nil
1924 }
1925 }),
1926 }
1927 tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
1928 tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
1929 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
1930
1931 ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
1932 cmd := NewCmdApply("kubectl", tf, ioStreams)
1933 cmd.Flags().Set("filename", filenameWidgetClientside)
1934 cmd.Flags().Set("output", "name")
1935 cmd.Run(cmd, []string{})
1936
1937 expected := "widget.unit-test.test.com/widget\n"
1938 if buf.String() != expected {
1939 t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
1940 }
1941 if errBuf.String() != "" {
1942 t.Fatalf("unexpected error output: %s", errBuf.String())
1943 }
1944 })
1945 })
1946 }
1947 }
1948 }
1949
1950 func TestRunApplySetLastApplied(t *testing.T) {
1951 cmdtesting.InitTestErrorHandler(t)
1952 nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
1953 pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
1954
1955 noExistRC, _ := readAndAnnotateReplicationController(t, filenameNoExistRC)
1956 noExistPath := "/namespaces/test/replicationcontrollers/" + noExistRC
1957
1958 noAnnotationName, noAnnotationRC := readReplicationController(t, filenameRCNoAnnotation)
1959 noAnnotationPath := "/namespaces/test/replicationcontrollers/" + noAnnotationName
1960
1961 tests := []struct {
1962 name, nameRC, pathRC, filePath, expectedErr, expectedOut, output string
1963 }{
1964 {
1965 name: "set with exist object",
1966 filePath: filenameRC,
1967 expectedErr: "",
1968 expectedOut: "replicationcontroller/test-rc\n",
1969 output: "name",
1970 },
1971 {
1972 name: "set with no-exist object",
1973 filePath: filenameNoExistRC,
1974 expectedErr: "Error from server (NotFound): the server could not find the requested resource (get replicationcontrollers no-exist)",
1975 expectedOut: "",
1976 output: "name",
1977 },
1978 {
1979 name: "set for the annotation does not exist on the live object",
1980 filePath: filenameRCNoAnnotation,
1981 expectedErr: "error: no last-applied-configuration annotation found on resource: no-annotation, to create the annotation, run the command with --create-annotation",
1982 expectedOut: "",
1983 output: "name",
1984 },
1985 {
1986 name: "set with exist object output json",
1987 filePath: filenameRCJSON,
1988 expectedErr: "",
1989 expectedOut: "replicationcontroller/test-rc\n",
1990 output: "name",
1991 },
1992 {
1993 name: "set test for a directory of files",
1994 filePath: dirName,
1995 expectedErr: "",
1996 expectedOut: "replicationcontroller/test-rc\nreplicationcontroller/test-rc\n",
1997 output: "name",
1998 },
1999 }
2000 for _, test := range tests {
2001 t.Run(test.name, func(t *testing.T) {
2002 tf := cmdtesting.NewTestFactory().WithNamespace("test")
2003 defer tf.Cleanup()
2004
2005 tf.UnstructuredClient = &fake.RESTClient{
2006 GroupVersion: schema.GroupVersion{Version: "v1"},
2007 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
2008 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
2009 switch p, m := req.URL.Path, req.Method; {
2010 case p == pathRC && m == "GET":
2011 bodyRC := io.NopCloser(bytes.NewReader(currentRC))
2012 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
2013 case p == noAnnotationPath && m == "GET":
2014 bodyRC := io.NopCloser(bytes.NewReader(noAnnotationRC))
2015 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
2016 case p == noExistPath && m == "GET":
2017 return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Pod{})}, nil
2018 case p == pathRC && m == "PATCH":
2019 checkPatchString(t, req)
2020 bodyRC := io.NopCloser(bytes.NewReader(currentRC))
2021 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
2022 case p == "/api/v1/namespaces/test" && m == "GET":
2023 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Namespace{})}, nil
2024 default:
2025 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
2026 return nil, nil
2027 }
2028 }),
2029 }
2030 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
2031
2032 cmdutil.BehaviorOnFatal(func(str string, code int) {
2033 if str != test.expectedErr {
2034 t.Errorf("%s: unexpected error: %s\nexpected: %s", test.name, str, test.expectedErr)
2035 }
2036 })
2037
2038 ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams()
2039 cmd := NewCmdApplySetLastApplied(tf, ioStreams)
2040 cmd.Flags().Set("filename", test.filePath)
2041 cmd.Flags().Set("output", test.output)
2042 cmd.Run(cmd, []string{})
2043
2044 if buf.String() != test.expectedOut {
2045 t.Fatalf("%s: unexpected output: %s\nexpected: %s", test.name, buf.String(), test.expectedOut)
2046 }
2047 })
2048 }
2049 cmdutil.BehaviorOnFatal(func(str string, code int) {})
2050 }
2051
2052 func checkPatchString(t *testing.T, req *http.Request) {
2053 checkString := string(readBytesFromFile(t, filenameRCPatchTest))
2054 patch, err := io.ReadAll(req.Body)
2055 if err != nil {
2056 t.Fatal(err)
2057 }
2058
2059 patchMap := map[string]interface{}{}
2060 if err := json.Unmarshal(patch, &patchMap); err != nil {
2061 t.Fatal(err)
2062 }
2063
2064 annotationsMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"})
2065 if _, ok := annotationsMap[corev1.LastAppliedConfigAnnotation]; !ok {
2066 t.Fatalf("patch does not contain annotation:\n%s\n", patch)
2067 }
2068
2069 resultString := annotationsMap["kubectl.kubernetes.io/last-applied-configuration"]
2070 if resultString != checkString {
2071 t.Fatalf("patch annotation is not correct, expect:%s\n but got:%s\n", checkString, resultString)
2072 }
2073 }
2074
2075 func TestForceApply(t *testing.T) {
2076 cmdtesting.InitTestErrorHandler(t)
2077 scheme := runtime.NewScheme()
2078 nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
2079 pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
2080 pathRCList := "/namespaces/test/replicationcontrollers"
2081 expected := map[string]int{
2082 "getOk": 6,
2083 "getNotFound": 1,
2084 "getList": 0,
2085 "patch": 6,
2086 "delete": 1,
2087 "post": 1,
2088 }
2089
2090 for _, testingOpenAPISchema := range testingOpenAPISchemas {
2091 for _, openAPIFeatureToggle := range applyFeatureToggles {
2092
2093 t.Run("test apply with --force", func(t *testing.T) {
2094 openAPIFeatureToggle(t, func(t *testing.T) {
2095 deleted := false
2096 isScaledDownToZero := false
2097 counts := map[string]int{}
2098 tf := cmdtesting.NewTestFactory().WithNamespace("test")
2099 defer tf.Cleanup()
2100
2101 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
2102 tf.UnstructuredClient = &fake.RESTClient{
2103 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
2104 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
2105 switch p, m := req.URL.Path, req.Method; {
2106 case strings.HasSuffix(p, pathRC) && m == "GET":
2107 if deleted {
2108 counts["getNotFound"]++
2109 return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte{}))}, nil
2110 }
2111 counts["getOk"]++
2112 var bodyRC io.ReadCloser
2113 if isScaledDownToZero {
2114 rcObj := readReplicationControllerFromFile(t, filenameRC)
2115 rcObj.Spec.Replicas = utilpointer.Int32Ptr(0)
2116 rcBytes, err := runtime.Encode(codec, rcObj)
2117 if err != nil {
2118 t.Fatal(err)
2119 }
2120 bodyRC = io.NopCloser(bytes.NewReader(rcBytes))
2121 } else {
2122 bodyRC = io.NopCloser(bytes.NewReader(currentRC))
2123 }
2124 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
2125 case strings.HasSuffix(p, pathRCList) && m == "GET":
2126 counts["getList"]++
2127 rcObj := readUnstructuredFromFile(t, filenameRC)
2128 list := &unstructured.UnstructuredList{
2129 Object: map[string]interface{}{
2130 "apiVersion": "v1",
2131 "kind": "ReplicationControllerList",
2132 },
2133 Items: []unstructured.Unstructured{*rcObj},
2134 }
2135 listBytes, err := runtime.Encode(codec, list)
2136 if err != nil {
2137 t.Fatal(err)
2138 }
2139 bodyRCList := io.NopCloser(bytes.NewReader(listBytes))
2140 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRCList}, nil
2141 case strings.HasSuffix(p, pathRC) && m == "PATCH":
2142 counts["patch"]++
2143 if counts["patch"] <= 6 {
2144 statusErr := apierrors.NewConflict(schema.GroupResource{Group: "", Resource: "rc"}, "test-rc", fmt.Errorf("the object has been modified. Please apply at first"))
2145 bodyBytes, _ := json.Marshal(statusErr)
2146 bodyErr := io.NopCloser(bytes.NewReader(bodyBytes))
2147 return &http.Response{StatusCode: http.StatusConflict, Header: cmdtesting.DefaultHeader(), Body: bodyErr}, nil
2148 }
2149 t.Fatalf("unexpected request: %#v after %v tries\n%#v", req.URL, counts["patch"], req)
2150 return nil, nil
2151 case strings.HasSuffix(p, pathRC) && m == "DELETE":
2152 counts["delete"]++
2153 deleted = true
2154 bodyRC := io.NopCloser(bytes.NewReader(currentRC))
2155 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
2156 case strings.HasSuffix(p, pathRC) && m == "PUT":
2157 counts["put"]++
2158 bodyRC := io.NopCloser(bytes.NewReader(currentRC))
2159 isScaledDownToZero = true
2160 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
2161 case strings.HasSuffix(p, pathRCList) && m == "POST":
2162 counts["post"]++
2163 deleted = false
2164 isScaledDownToZero = false
2165 bodyRC := io.NopCloser(bytes.NewReader(currentRC))
2166 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
2167 default:
2168 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
2169 return nil, nil
2170 }
2171 }),
2172 }
2173 fakeDynamicClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
2174 tf.FakeDynamicClient = fakeDynamicClient
2175 tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
2176 tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
2177 tf.Client = tf.UnstructuredClient
2178 tf.ClientConfigVal = &restclient.Config{}
2179
2180 ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
2181 cmd := NewCmdApply("kubectl", tf, ioStreams)
2182 cmd.Flags().Set("filename", filenameRC)
2183 cmd.Flags().Set("output", "name")
2184 cmd.Flags().Set("force", "true")
2185 cmd.Run(cmd, []string{})
2186
2187 for method, exp := range expected {
2188 if exp != counts[method] {
2189 t.Errorf("Unexpected amount of %q API calls, wanted %v got %v", method, exp, counts[method])
2190 }
2191 }
2192
2193 if expected := "replicationcontroller/" + nameRC + "\n"; buf.String() != expected {
2194 t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
2195 }
2196 if errBuf.String() != "" {
2197 t.Fatalf("unexpected error output: %s", errBuf.String())
2198 }
2199 })
2200 })
2201 }
2202 }
2203 }
2204
2205 func TestDontAllowForceApplyWithServerDryRun(t *testing.T) {
2206 expectedError := "error: --dry-run=server cannot be used with --force"
2207
2208 cmdutil.BehaviorOnFatal(func(str string, code int) {
2209 panic(str)
2210 })
2211 defer func() {
2212 actualError := recover()
2213 if expectedError != actualError {
2214 t.Fatalf(`expected error "%s", but got "%s"`, expectedError, actualError)
2215 }
2216 }()
2217
2218 tf := cmdtesting.NewTestFactory().WithNamespace("test")
2219 defer tf.Cleanup()
2220
2221 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
2222
2223 ioStreams, _, _, _ := genericiooptions.NewTestIOStreams()
2224 cmd := NewCmdApply("kubectl", tf, ioStreams)
2225 cmd.Flags().Set("filename", filenameRC)
2226 cmd.Flags().Set("dry-run", "server")
2227 cmd.Flags().Set("force", "true")
2228 cmd.Run(cmd, []string{})
2229
2230 t.Fatalf(`expected error "%s"`, expectedError)
2231 }
2232
2233 func TestDontAllowForceApplyWithServerSide(t *testing.T) {
2234 expectedError := "error: --force cannot be used with --server-side"
2235
2236 cmdutil.BehaviorOnFatal(func(str string, code int) {
2237 panic(str)
2238 })
2239 defer func() {
2240 actualError := recover()
2241 if expectedError != actualError {
2242 t.Fatalf(`expected error "%s", but got "%s"`, expectedError, actualError)
2243 }
2244 }()
2245
2246 tf := cmdtesting.NewTestFactory().WithNamespace("test")
2247 defer tf.Cleanup()
2248
2249 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
2250
2251 ioStreams, _, _, _ := genericiooptions.NewTestIOStreams()
2252 cmd := NewCmdApply("kubectl", tf, ioStreams)
2253 cmd.Flags().Set("filename", filenameRC)
2254 cmd.Flags().Set("server-side", "true")
2255 cmd.Flags().Set("force", "true")
2256 cmd.Run(cmd, []string{})
2257
2258 t.Fatalf(`expected error "%s"`, expectedError)
2259 }
2260
2261 func TestDontAllowApplyWithPodGeneratedName(t *testing.T) {
2262 expectedError := "error: from testing-: cannot use generate name with apply"
2263 cmdutil.BehaviorOnFatal(func(str string, code int) {
2264 if str != expectedError {
2265 t.Fatalf(`expected error "%s", but got "%s"`, expectedError, str)
2266 }
2267 })
2268
2269 tf := cmdtesting.NewTestFactory().WithNamespace("test")
2270 defer tf.Cleanup()
2271 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
2272
2273 ioStreams, _, _, _ := genericiooptions.NewTestIOStreams()
2274 cmd := NewCmdApply("kubectl", tf, ioStreams)
2275 cmd.Flags().Set("filename", filenamePodGeneratedName)
2276 cmd.Flags().Set("dry-run", "client")
2277 cmd.Run(cmd, []string{})
2278 }
2279
2280 func TestApplySetParentValidation(t *testing.T) {
2281 for name, test := range map[string]struct {
2282 applysetFlag string
2283 namespaceFlag string
2284 setup func(*testing.T, *cmdtesting.TestFactory)
2285 expectParentKind string
2286 expectBlankParentNs bool
2287 expectErr string
2288 }{
2289 "parent type must be valid": {
2290 applysetFlag: "doesnotexist/thename",
2291 expectErr: "invalid parent reference \"doesnotexist/thename\": no matches for /, Resource=doesnotexist",
2292 },
2293 "parent name must be present": {
2294 applysetFlag: "secret/",
2295 expectErr: "invalid parent reference \"secret/\": name cannot be blank",
2296 },
2297 "configmap parents are valid": {
2298 applysetFlag: "configmap/thename",
2299 namespaceFlag: "mynamespace",
2300 expectParentKind: "ConfigMap",
2301 },
2302 "secret parents are valid": {
2303 applysetFlag: "secret/thename",
2304 namespaceFlag: "mynamespace",
2305 expectParentKind: "Secret",
2306 },
2307 "plural resource works": {
2308 applysetFlag: "secrets/thename",
2309 namespaceFlag: "mynamespace",
2310 expectParentKind: "Secret",
2311 },
2312 "other namespaced builtin parents types are correctly parsed but invalid": {
2313 applysetFlag: "deployments.apps/thename",
2314 expectParentKind: "Deployment",
2315 expectErr: "[namespace is required to use namespace-scoped ApplySet, resource \"apps/v1, Resource=deployments\" is not permitted as an ApplySet parent]",
2316 },
2317 "namespaced builtin parents with multi-segment groups are correctly parsed but invalid": {
2318 applysetFlag: "priorityclasses.scheduling.k8s.io/thename",
2319 expectParentKind: "PriorityClass",
2320 expectErr: "resource \"scheduling.k8s.io/v1alpha1, Resource=priorityclasses\" is not permitted as an ApplySet parent",
2321 },
2322 "non-namespaced builtin types are correctly parsed but invalid": {
2323 applysetFlag: "namespaces/thename",
2324 expectParentKind: "Namespace",
2325 namespaceFlag: "somenamespace",
2326 expectBlankParentNs: true,
2327 expectErr: "resource \"/v1, Resource=namespaces\" is not permitted as an ApplySet parent",
2328 },
2329 "parent namespace should use the value of the namespace flag": {
2330 applysetFlag: "mysecret",
2331 namespaceFlag: "mynamespace",
2332 expectParentKind: "Secret",
2333 },
2334 "parent namespace should not use the default namespace from ClientConfig": {
2335 applysetFlag: "mysecret",
2336 setup: func(t *testing.T, f *cmdtesting.TestFactory) {
2337
2338
2339 ns, overridden, err := f.ToRawKubeConfigLoader().Namespace()
2340 require.NoError(t, err)
2341 require.Falsef(t, overridden, "namespace unexpectedly overridden")
2342 require.Equal(t, "default", ns)
2343 },
2344 expectBlankParentNs: true,
2345 expectParentKind: "Secret",
2346 expectErr: "namespace is required to use namespace-scoped ApplySet",
2347 },
2348 "parent namespace should not use the default namespace from the user's kubeconfig": {
2349 applysetFlag: "mysecret",
2350 setup: func(t *testing.T, f *cmdtesting.TestFactory) {
2351 kubeConfig := clientcmdapi.NewConfig()
2352 kubeConfig.CurrentContext = "default"
2353 kubeConfig.Contexts["default"] = &clientcmdapi.Context{Namespace: "bar"}
2354 clientConfig := clientcmd.NewDefaultClientConfig(*kubeConfig, &clientcmd.ConfigOverrides{
2355 ClusterDefaults: clientcmdapi.Cluster{Server: "http://localhost:8080"}})
2356 f.WithClientConfig(clientConfig)
2357 },
2358 expectBlankParentNs: true,
2359 expectParentKind: "Secret",
2360 expectErr: "namespace is required to use namespace-scoped ApplySet",
2361 },
2362 } {
2363 t.Run(name, func(t *testing.T) {
2364 cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) {
2365 cmd := &cobra.Command{}
2366 flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard())
2367 flags.AddFlags(cmd)
2368 cmd.Flags().Set("filename", filenameRC)
2369 cmd.Flags().Set("applyset", test.applysetFlag)
2370 cmd.Flags().Set("prune", "true")
2371 f := cmdtesting.NewTestFactory()
2372 defer f.Cleanup()
2373 setUpClientsForApplySetWithSSA(t, f)
2374
2375 var expectedParentNs string
2376 if test.namespaceFlag != "" {
2377 f.WithNamespace(test.namespaceFlag)
2378 if !test.expectBlankParentNs {
2379 expectedParentNs = test.namespaceFlag
2380 }
2381 }
2382
2383 if test.setup != nil {
2384 test.setup(t, f)
2385 }
2386
2387 o, err := flags.ToOptions(f, cmd, "kubectl", []string{})
2388 if test.expectErr == "" {
2389 require.NoError(t, err, "ToOptions error")
2390 } else if err != nil {
2391 require.EqualError(t, err, test.expectErr)
2392 return
2393 }
2394
2395 assert.Equal(t, expectedParentNs, o.ApplySet.parentRef.Namespace)
2396 assert.Equal(t, test.expectParentKind, o.ApplySet.parentRef.GroupVersionKind.Kind)
2397
2398 err = o.Validate()
2399 if test.expectErr != "" {
2400 require.EqualError(t, err, test.expectErr)
2401 } else {
2402 require.NoError(t, err, "Validate error")
2403 }
2404 })
2405 })
2406 }
2407 }
2408
2409 func setUpClientsForApplySetWithSSA(t *testing.T, tf *cmdtesting.TestFactory, objects ...runtime.Object) {
2410 listMapping := map[schema.GroupVersionResource]string{
2411 {Group: "", Version: "v1", Resource: "services"}: "ServiceList",
2412 {Group: "", Version: "v1", Resource: "replicationcontrollers"}: "ReplicationControllerList",
2413 {Group: "apiextensions.k8s.io", Version: "v1", Resource: "customresourcedefinitions"}: "CustomResourceDefinitionList",
2414 }
2415 fakeDynamicClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), listMapping, objects...)
2416 tf.FakeDynamicClient = fakeDynamicClient
2417
2418 tf.UnstructuredClient = &fake.RESTClient{
2419 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
2420 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
2421 tokens := strings.Split(strings.TrimPrefix(req.URL.Path, "/"), "/")
2422 var gvr schema.GroupVersionResource
2423 var name, namespace string
2424
2425 if len(tokens) == 4 && tokens[0] == "namespaces" {
2426 namespace = tokens[1]
2427 name = tokens[3]
2428 gvr = schema.GroupVersionResource{Version: "v1", Resource: tokens[2]}
2429 } else if len(tokens) == 2 && tokens[0] == "applysets" {
2430 gvr = schema.GroupVersionResource{Group: "company.com", Version: "v1", Resource: tokens[0]}
2431 name = tokens[1]
2432 } else {
2433 t.Fatalf("unexpected request: path segments %v: request: \n%#v", tokens, req)
2434 return nil, nil
2435 }
2436
2437 switch req.Method {
2438 case "GET":
2439 obj, err := fakeDynamicClient.Tracker().Get(gvr, namespace, name)
2440 if err == nil {
2441 objJson, err := json.Marshal(obj)
2442 require.NoError(t, err)
2443 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.BytesBody(objJson)}, nil
2444 } else if apierrors.IsNotFound(err) {
2445 return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader()}, nil
2446 } else {
2447 t.Fatalf("error getting object: %v", err)
2448 }
2449 case "PATCH":
2450 require.Equal(t, string(types.ApplyPatchType), req.Header.Get("Content-Type"), "received patch request with unexpected patch type")
2451
2452 var existing *unstructured.Unstructured
2453 existingObj, err := fakeDynamicClient.Tracker().Get(gvr, namespace, name)
2454 if err != nil {
2455 if !apierrors.IsNotFound(err) {
2456 t.Fatalf("error getting object: %v", err)
2457 }
2458 } else {
2459 existing = existingObj.(*unstructured.Unstructured)
2460 }
2461
2462 data, err := io.ReadAll(req.Body)
2463 require.NoError(t, err)
2464
2465 patch := &unstructured.Unstructured{}
2466 err = runtime.DecodeInto(codec, data, patch)
2467 require.NoError(t, err)
2468
2469 var returnData []byte
2470 if existing == nil {
2471 patch.SetUID("a-static-fake-uid")
2472 err := fakeDynamicClient.Tracker().Create(gvr, patch, namespace)
2473 require.NoError(t, err, "error creating object")
2474
2475 returnData, err = json.Marshal(patch)
2476 require.NoError(t, err, "error marshalling response: %v", err)
2477 } else {
2478 uid := existing.GetUID()
2479 patch.DeepCopyInto(existing)
2480 existing.SetUID(uid)
2481
2482 err = fakeDynamicClient.Tracker().Update(gvr, existing, namespace)
2483 require.NoError(t, err, "error updating object")
2484
2485 returnData, err = json.Marshal(existing)
2486 require.NoError(t, err, "error marshalling response")
2487 }
2488 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(returnData))}, nil
2489
2490 default:
2491 t.Fatalf("unexpected request: %s\n%#v", req.URL.Path, req)
2492 return nil, nil
2493 }
2494 return nil, nil
2495 }),
2496 }
2497 tf.Client = tf.UnstructuredClient
2498 }
2499
2500 func TestLoadObjects(t *testing.T) {
2501 f := cmdtesting.NewTestFactory().WithNamespace("test")
2502 defer f.Cleanup()
2503 f.Client = &fake.RESTClient{}
2504 f.UnstructuredClient = f.Client
2505
2506 testFiles := []string{"testdata/prune/simple/manifest1", "testdata/prune/simple/manifest2"}
2507 for _, testFile := range testFiles {
2508 t.Run(testFile, func(t *testing.T) {
2509 cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) {
2510
2511 cmd := &cobra.Command{}
2512 flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard())
2513 flags.AddFlags(cmd)
2514 cmd.Flags().Set("filename", testFile+".yaml")
2515 cmd.Flags().Set("applyset", filepath.Base(filepath.Dir(testFile)))
2516 cmd.Flags().Set("prune", "true")
2517
2518 o, err := flags.ToOptions(f, cmd, "kubectl", []string{})
2519 if err != nil {
2520 t.Fatalf("unexpected error creating apply options: %v", err)
2521 }
2522
2523 err = o.Validate()
2524 if err != nil {
2525 t.Fatalf("unexpected error from validate: %v", err)
2526 }
2527
2528 resources, err := o.GetObjects()
2529 if err != nil {
2530 t.Fatalf("GetObjects gave unexpected error %v", err)
2531 }
2532
2533 var objectYAMLs []string
2534 for _, obj := range resources {
2535 y, err := yaml.Marshal(obj.Object)
2536 if err != nil {
2537 t.Fatalf("error marshaling object: %v", err)
2538 }
2539 objectYAMLs = append(objectYAMLs, string(y))
2540 }
2541 got := strings.Join(objectYAMLs, "\n---\n\n")
2542
2543 p := testFile + "-expected-getobjects.yaml"
2544 wantBytes, err := os.ReadFile(p)
2545 if err != nil {
2546 t.Fatalf("error reading file %q: %v", p, err)
2547 }
2548 want := string(wantBytes)
2549 if diff := cmp.Diff(want, got); diff != "" {
2550 t.Errorf("GetObjects returned unexpected diff (-want +got):\n%s", diff)
2551 }
2552 })
2553 })
2554 }
2555 }
2556
2557 func TestApplySetParentManagement(t *testing.T) {
2558 nameParentSecret := "my-set"
2559 tf := cmdtesting.NewTestFactory().WithNamespace("test")
2560 defer tf.Cleanup()
2561
2562 replicationController := readUnstructuredFromFile(t, filenameRC)
2563 setUpClientsForApplySetWithSSA(t, tf, replicationController)
2564 failDeletes := false
2565 tf.FakeDynamicClient.PrependReactor("delete", "*", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
2566 if failDeletes {
2567 return true, nil, fmt.Errorf("an error on the server (\"\") has prevented the request from succeeding")
2568 }
2569 return false, nil, nil
2570 })
2571 cmdutil.BehaviorOnFatal(func(s string, i int) {
2572 if failDeletes && s == `error: pruning ReplicationController test/test-rc: an error on the server ("") has prevented the request from succeeding` {
2573 t.Logf("got expected error %q", s)
2574 } else {
2575 t.Fatalf("unexpected exit %d: %s", i, s)
2576 }
2577 })
2578 defer cmdutil.DefaultBehaviorOnFatal()
2579
2580
2581
2582 ioStreams, _, outbuff, errbuff := genericiooptions.NewTestIOStreams()
2583 cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) {
2584 cmd := NewCmdApply("kubectl", tf, ioStreams)
2585 cmd.Flags().Set("filename", filenameRC)
2586 cmd.Flags().Set("server-side", "true")
2587 cmd.Flags().Set("applyset", nameParentSecret)
2588 cmd.Flags().Set("prune", "true")
2589 cmd.Run(cmd, []string{})
2590 })
2591 assert.Equal(t, "replicationcontroller/test-rc serverside-applied\n", outbuff.String())
2592 assert.Equal(t, "", errbuff.String())
2593
2594 createdSecret, err := tf.FakeDynamicClient.Tracker().Get(schema.GroupVersionResource{Resource: "secrets", Version: "v1"}, "test", nameParentSecret)
2595 require.NoError(t, err)
2596 createSecretYaml, err := yaml.Marshal(createdSecret)
2597 require.NoError(t, err)
2598 require.Equal(t, `apiVersion: v1
2599 kind: Secret
2600 metadata:
2601 annotations:
2602 applyset.kubernetes.io/additional-namespaces: ""
2603 applyset.kubernetes.io/contains-group-kinds: ReplicationController
2604 applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$
2605 creationTimestamp: null
2606 labels:
2607 applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1
2608 name: my-set
2609 namespace: test
2610 uid: a-static-fake-uid
2611 `, string(createSecretYaml))
2612
2613
2614 outbuff.Reset()
2615 errbuff.Reset()
2616 cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) {
2617 cmd := NewCmdApply("kubectl", tf, ioStreams)
2618 cmd.Flags().Set("filename", filenameRC)
2619 cmd.Flags().Set("filename", filenameSVC)
2620 cmd.Flags().Set("server-side", "true")
2621 cmd.Flags().Set("applyset", nameParentSecret)
2622 cmd.Flags().Set("prune", "true")
2623 cmd.Run(cmd, []string{})
2624 })
2625 assert.Equal(t, "replicationcontroller/test-rc serverside-applied\nservice/test-service serverside-applied\n", outbuff.String())
2626 assert.Equal(t, "", errbuff.String())
2627
2628 updatedSecret, err := tf.FakeDynamicClient.Tracker().Get(schema.GroupVersionResource{Resource: "secrets", Version: "v1"}, "test", nameParentSecret)
2629 require.NoError(t, err)
2630 updatedSecretYaml, err := yaml.Marshal(updatedSecret)
2631 require.NoError(t, err)
2632 require.Equal(t, `apiVersion: v1
2633 kind: Secret
2634 metadata:
2635 annotations:
2636 applyset.kubernetes.io/additional-namespaces: ""
2637 applyset.kubernetes.io/contains-group-kinds: ReplicationController,Service
2638 applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$
2639 creationTimestamp: null
2640 labels:
2641 applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1
2642 name: my-set
2643 namespace: test
2644 uid: a-static-fake-uid
2645 `, string(updatedSecretYaml))
2646
2647
2648
2649 failDeletes = true
2650 outbuff.Reset()
2651 errbuff.Reset()
2652 cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) {
2653 cmd := NewCmdApply("kubectl", tf, ioStreams)
2654 cmd.Flags().Set("filename", filenameSVC)
2655 cmd.Flags().Set("server-side", "true")
2656 cmd.Flags().Set("applyset", nameParentSecret)
2657 cmd.Flags().Set("prune", "true")
2658 cmd.Run(cmd, []string{})
2659 })
2660 assert.Equal(t, "service/test-service serverside-applied\n", outbuff.String())
2661 assert.Equal(t, "", errbuff.String())
2662
2663 updatedSecret, err = tf.FakeDynamicClient.Tracker().Get(schema.GroupVersionResource{Resource: "secrets", Version: "v1"}, "test", nameParentSecret)
2664 require.NoError(t, err)
2665 updatedSecretYaml, err = yaml.Marshal(updatedSecret)
2666 require.NoError(t, err)
2667 require.Equal(t, `apiVersion: v1
2668 kind: Secret
2669 metadata:
2670 annotations:
2671 applyset.kubernetes.io/additional-namespaces: ""
2672 applyset.kubernetes.io/contains-group-kinds: ReplicationController,Service
2673 applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$
2674 creationTimestamp: null
2675 labels:
2676 applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1
2677 name: my-set
2678 namespace: test
2679 uid: a-static-fake-uid
2680 `, string(updatedSecretYaml))
2681
2682
2683 failDeletes = false
2684
2685 outbuff.Reset()
2686 errbuff.Reset()
2687 cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) {
2688 cmd := NewCmdApply("kubectl", tf, ioStreams)
2689 cmd.Flags().Set("filename", filenameSVC)
2690 cmd.Flags().Set("server-side", "true")
2691 cmd.Flags().Set("applyset", nameParentSecret)
2692 cmd.Flags().Set("prune", "true")
2693 cmd.Run(cmd, []string{})
2694 })
2695 assert.Equal(t, "service/test-service serverside-applied\nreplicationcontroller/test-rc pruned\n", outbuff.String())
2696 assert.Equal(t, "", errbuff.String())
2697
2698 updatedSecret, err = tf.FakeDynamicClient.Tracker().Get(schema.GroupVersionResource{Resource: "secrets", Version: "v1"}, "test", nameParentSecret)
2699 require.NoError(t, err)
2700 updatedSecretYaml, err = yaml.Marshal(updatedSecret)
2701 require.NoError(t, err)
2702 require.Equal(t, `apiVersion: v1
2703 kind: Secret
2704 metadata:
2705 annotations:
2706 applyset.kubernetes.io/additional-namespaces: ""
2707 applyset.kubernetes.io/contains-group-kinds: Service
2708 applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$
2709 creationTimestamp: null
2710 labels:
2711 applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1
2712 name: my-set
2713 namespace: test
2714 uid: a-static-fake-uid
2715 `, string(updatedSecretYaml))
2716 }
2717
2718 func TestApplySetInvalidLiveParent(t *testing.T) {
2719 nameParentSecret := "my-set"
2720 tf := cmdtesting.NewTestFactory().WithNamespace("test")
2721 defer tf.Cleanup()
2722
2723 type testCase struct {
2724 gksAnnotation string
2725 toolingAnnotation string
2726 idLabel string
2727 expectErr string
2728 }
2729 validIDLabel := "applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1"
2730 validToolingAnnotation := "kubectl/v1.27.0"
2731 validGksAnnotation := "Deployment.apps,Namespace,Secret"
2732
2733 for name, test := range map[string]testCase{
2734 "group-resources annotation is required": {
2735 gksAnnotation: "",
2736 toolingAnnotation: validToolingAnnotation,
2737 idLabel: validIDLabel,
2738 expectErr: "error: parsing ApplySet annotation on \"secrets./my-set\": kubectl requires the \"applyset.kubernetes.io/contains-group-kinds\" annotation to be set on all ApplySet parent objects",
2739 },
2740 "group-resources annotation should not contain invalid resources": {
2741 gksAnnotation: "does-not-exist",
2742 toolingAnnotation: validToolingAnnotation,
2743 idLabel: validIDLabel,
2744 expectErr: "error: parsing ApplySet annotation on \"secrets./my-set\": could not find mapping for kind in \"applyset.kubernetes.io/contains-group-kinds\" annotation: no matches for kind \"does-not-exist\" in group \"\"",
2745 },
2746 "tooling annotation is required": {
2747 gksAnnotation: validGksAnnotation,
2748 toolingAnnotation: "",
2749 idLabel: validIDLabel,
2750 expectErr: "error: ApplySet parent object \"secrets./my-set\" already exists and is missing required annotation \"applyset.kubernetes.io/tooling\"",
2751 },
2752 "tooling annotation must have kubectl prefix": {
2753 gksAnnotation: validGksAnnotation,
2754 toolingAnnotation: "helm/v3",
2755 idLabel: validIDLabel,
2756 expectErr: "error: ApplySet parent object \"secrets./my-set\" already exists and is managed by tooling \"helm\" instead of \"kubectl\"",
2757 },
2758 "tooling annotation with invalid prefix with one segment can be parsed": {
2759 gksAnnotation: validGksAnnotation,
2760 toolingAnnotation: "helm",
2761 idLabel: validIDLabel,
2762 expectErr: "error: ApplySet parent object \"secrets./my-set\" already exists and is managed by tooling \"helm\" instead of \"kubectl\"",
2763 },
2764 "tooling annotation with invalid prefix with many segments can be parsed": {
2765 gksAnnotation: validGksAnnotation,
2766 toolingAnnotation: "example.com/tool/why/v1",
2767 idLabel: validIDLabel,
2768 expectErr: "error: ApplySet parent object \"secrets./my-set\" already exists and is managed by tooling \"example.com/tool/why\" instead of \"kubectl\"",
2769 },
2770 "ID label is required": {
2771 gksAnnotation: validGksAnnotation,
2772 toolingAnnotation: validToolingAnnotation,
2773 idLabel: "",
2774 expectErr: "error: ApplySet parent object \"secrets./my-set\" exists and does not have required label applyset.kubernetes.io/id",
2775 },
2776 "ID label must match the ApplySet's real ID": {
2777 gksAnnotation: validGksAnnotation,
2778 toolingAnnotation: validToolingAnnotation,
2779 idLabel: "somethingelse",
2780 expectErr: fmt.Sprintf("error: ApplySet parent object \"secrets./my-set\" exists and has incorrect value for label \"applyset.kubernetes.io/id\" (got: somethingelse, want: %s)", validIDLabel),
2781 },
2782 } {
2783 t.Run(name, func(t *testing.T) {
2784 require.NotEmpty(t, test.expectErr, "invalid test case")
2785 cmdutil.BehaviorOnFatal(func(s string, i int) {
2786 assert.Equal(t, test.expectErr, s)
2787 })
2788 defer cmdutil.DefaultBehaviorOnFatal()
2789 secret := &unstructured.Unstructured{}
2790 secret.SetKind("Secret")
2791 secret.SetAPIVersion("v1")
2792 secret.SetName(nameParentSecret)
2793 secret.SetNamespace("test")
2794 annotations := make(map[string]string)
2795 labels := make(map[string]string)
2796 if test.gksAnnotation != "" {
2797 annotations[ApplySetGKsAnnotation] = test.gksAnnotation
2798 }
2799 if test.toolingAnnotation != "" {
2800 annotations[ApplySetToolingAnnotation] = test.toolingAnnotation
2801 }
2802 if test.idLabel != "" {
2803 labels[ApplySetParentIDLabel] = test.idLabel
2804 }
2805 secret.SetAnnotations(annotations)
2806 secret.SetLabels(labels)
2807 setUpClientsForApplySetWithSSA(t, tf, secret)
2808
2809 cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) {
2810 ioStreams, _, _, _ := genericiooptions.NewTestIOStreams()
2811 cmd := NewCmdApply("kubectl", tf, ioStreams)
2812 cmd.Flags().Set("filename", filenameSVC)
2813 cmd.Flags().Set("server-side", "true")
2814 cmd.Flags().Set("applyset", nameParentSecret)
2815 cmd.Flags().Set("prune", "true")
2816 cmd.Run(cmd, []string{})
2817 })
2818 })
2819 }
2820 }
2821
2822 func TestApplySet_ClusterScopedCustomResourceParent(t *testing.T) {
2823 tf := cmdtesting.NewTestFactory()
2824 defer tf.Cleanup()
2825
2826 replicationController := readUnstructuredFromFile(t, filenameRC)
2827 crd := readUnstructuredFromFile(t, filenameApplySetCRD)
2828 cr := readUnstructuredFromFile(t, filenameApplySetCR)
2829 setUpClientsForApplySetWithSSA(t, tf, replicationController, crd)
2830
2831 ioStreams, _, outbuff, errbuff := genericiooptions.NewTestIOStreams()
2832 cmdutil.BehaviorOnFatal(func(s string, i int) {
2833 require.Equal(t, "error: custom resource ApplySet parents cannot be created automatically", s)
2834 })
2835 defer cmdutil.DefaultBehaviorOnFatal()
2836
2837
2838 cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) {
2839 cmd := NewCmdApply("kubectl", tf, ioStreams)
2840 cmd.Flags().Set("filename", filenameRC)
2841 cmd.Flags().Set("server-side", "true")
2842 cmd.Flags().Set("applyset", fmt.Sprintf("applysets.company.com/my-set"))
2843 cmd.Flags().Set("prune", "true")
2844 cmd.Run(cmd, []string{})
2845 })
2846 cmdtesting.InitTestErrorHandler(t)
2847
2848
2849 require.NoError(t, tf.FakeDynamicClient.Tracker().Add(cr))
2850 cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) {
2851 cmd := NewCmdApply("kubectl", tf, ioStreams)
2852 cmd.Flags().Set("filename", filenameRC)
2853 cmd.Flags().Set("server-side", "true")
2854 cmd.Flags().Set("applyset", fmt.Sprintf("applysets.company.com/my-set"))
2855 cmd.Flags().Set("prune", "true")
2856 cmd.Run(cmd, []string{})
2857 })
2858 assert.Equal(t, "replicationcontroller/test-rc serverside-applied\n", outbuff.String())
2859 assert.Equal(t, "", errbuff.String())
2860
2861 updatedCR, err := tf.FakeDynamicClient.Tracker().Get(schema.GroupVersionResource{Resource: "applysets", Version: "v1", Group: "company.com"}, "", "my-set")
2862 require.NoError(t, err)
2863 updatedCRYaml, err := yaml.Marshal(updatedCR)
2864 require.NoError(t, err)
2865 require.Equal(t, `apiVersion: company.com/v1
2866 kind: ApplySet
2867 metadata:
2868 annotations:
2869 applyset.kubernetes.io/additional-namespaces: test
2870 applyset.kubernetes.io/contains-group-kinds: ReplicationController
2871 applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$
2872 creationTimestamp: null
2873 labels:
2874 applyset.kubernetes.io/id: applyset-rhp1a-HVAVT_dFgyEygyA1BEB82HPp2o10UiFTpqtAs-v1
2875 name: my-set
2876 `, string(updatedCRYaml))
2877 }
2878
2879 func TestApplyWithPruneV2(t *testing.T) {
2880 testdirs := []string{"testdata/prune/simple"}
2881 for _, testdir := range testdirs {
2882 t.Run(testdir, func(t *testing.T) {
2883 tf := cmdtesting.NewTestFactory().WithNamespace("test")
2884 defer tf.Cleanup()
2885
2886 codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
2887
2888 scheme := runtime.NewScheme()
2889
2890 listMapping := map[schema.GroupVersionResource]string{
2891 {Group: "", Version: "v1", Resource: "namespaces"}: "NamespaceList",
2892 }
2893
2894 fakeDynamicClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
2895 tf.FakeDynamicClient = fakeDynamicClient
2896
2897 tf.UnstructuredClient = &fake.RESTClient{
2898 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
2899 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
2900 method := req.Method
2901
2902 tokens := strings.Split(strings.TrimPrefix(req.URL.Path, "/"), "/")
2903
2904 if len(tokens) == 2 && tokens[0] == "namespaces" && method == "GET" {
2905 name := tokens[1]
2906 gvr := schema.GroupVersionResource{Version: "v1", Resource: "namespaces"}
2907 ns, err := fakeDynamicClient.Tracker().Get(gvr, "", name)
2908 if err != nil {
2909 if apierrors.IsNotFound(err) {
2910 return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader()}, nil
2911 }
2912 t.Fatalf("error getting object: %v", err)
2913 }
2914 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, ns)}, nil
2915 }
2916
2917 if len(tokens) == 4 && tokens[0] == "namespaces" && tokens[2] == "secrets" && method == "GET" {
2918 namespace := tokens[1]
2919 name := tokens[3]
2920 gvr := schema.GroupVersionResource{Version: "v1", Resource: "secrets"}
2921 obj, err := fakeDynamicClient.Tracker().Get(gvr, namespace, name)
2922 if err != nil {
2923 if apierrors.IsNotFound(err) {
2924 return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader()}, nil
2925 }
2926 t.Fatalf("error getting object: %v", err)
2927 }
2928 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj)}, nil
2929 }
2930
2931 if len(tokens) == 4 && tokens[0] == "namespaces" && tokens[2] == "secrets" && method == "PATCH" {
2932 namespace := tokens[1]
2933 name := tokens[3]
2934 gvr := schema.GroupVersionResource{Version: "v1", Resource: "secrets"}
2935 var existing *unstructured.Unstructured
2936 existingObj, err := fakeDynamicClient.Tracker().Get(gvr, namespace, name)
2937 if err != nil {
2938 if !apierrors.IsNotFound(err) {
2939 t.Fatalf("error getting object: %v", err)
2940 }
2941 } else {
2942 existing = existingObj.(*unstructured.Unstructured)
2943 }
2944
2945 data, err := io.ReadAll(req.Body)
2946 if err != nil {
2947 t.Fatalf("unexpected error: %v", err)
2948 }
2949
2950 patch := &unstructured.Unstructured{}
2951 if err := runtime.DecodeInto(codec, data, patch); err != nil {
2952 t.Fatalf("unexpected error: %v", err)
2953 }
2954
2955 var returnData []byte
2956 if existing == nil {
2957 uid := types.UID(fmt.Sprintf("%v", time.Now().UnixNano()))
2958 patch.SetUID(uid)
2959
2960 if err := fakeDynamicClient.Tracker().Create(gvr, patch, namespace); err != nil {
2961 t.Fatalf("error creating object: %v", err)
2962 }
2963
2964 b, err := json.Marshal(patch)
2965 if err != nil {
2966 t.Fatalf("error marshalling response: %v", err)
2967 }
2968 returnData = b
2969 } else {
2970 patch.DeepCopyInto(existing)
2971 if err := fakeDynamicClient.Tracker().Update(gvr, existing, namespace); err != nil {
2972 t.Fatalf("error updating object: %v", err)
2973 }
2974 b, err := json.Marshal(existing)
2975 if err != nil {
2976 t.Fatalf("error marshalling response: %v", err)
2977 }
2978 returnData = b
2979 }
2980
2981 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(returnData))}, nil
2982 }
2983
2984 if len(tokens) == 1 && tokens[0] == "namespaces" && method == "POST" {
2985 data, err := io.ReadAll(req.Body)
2986 if err != nil {
2987 t.Fatalf("unexpected error: %v", err)
2988 }
2989
2990 u := &unstructured.Unstructured{}
2991 if err := runtime.DecodeInto(codec, data, u); err != nil {
2992 t.Fatalf("unexpected error: %v", err)
2993 }
2994
2995 name := u.GetName()
2996 ns := u.GetNamespace()
2997 gvr := schema.GroupVersionResource{Version: "v1", Resource: "namespaces"}
2998
2999 existing, err := fakeDynamicClient.Tracker().Get(gvr, ns, name)
3000 if err != nil {
3001 if apierrors.IsNotFound(err) {
3002 existing = nil
3003 } else {
3004 t.Fatalf("error fetching object: %v", err)
3005 }
3006 }
3007
3008 if existing != nil {
3009 return &http.Response{StatusCode: http.StatusConflict, Header: cmdtesting.DefaultHeader()}, nil
3010 }
3011
3012 uid := types.UID(fmt.Sprintf("%v", time.Now().UnixNano()))
3013 u.SetUID(uid)
3014
3015 if err := fakeDynamicClient.Tracker().Create(gvr, u, ns); err != nil {
3016 t.Fatalf("error creating object: %v", err)
3017 }
3018
3019 body := cmdtesting.ObjBody(codec, u)
3020
3021 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
3022 }
3023
3024 t.Fatalf("unexpected request: %v %v\n%#v", req.Method, req.URL, req)
3025 return nil, nil
3026 }),
3027 }
3028
3029 tf.Client = tf.UnstructuredClient
3030 tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc
3031
3032 cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) {
3033 manifests := []string{"manifest1", "manifest2"}
3034 for _, manifest := range manifests {
3035 t.Logf("applying manifest %v", manifest)
3036
3037 cmd := &cobra.Command{}
3038 flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard())
3039 flags.AddFlags(cmd)
3040 cmd.Flags().Set("filename", filepath.Join(testdir, manifest+".yaml"))
3041 cmd.Flags().Set("applyset", filepath.Base(testdir))
3042 cmd.Flags().Set("prune", "true")
3043 cmd.Flags().Set("validate", "false")
3044
3045 o, err := flags.ToOptions(tf, cmd, "kubectl", []string{})
3046 if err != nil {
3047 t.Fatalf("unexpected error creating apply options: %v", err)
3048 }
3049
3050 err = o.Validate()
3051 if err != nil {
3052 t.Fatalf("unexpected error from validate: %v", err)
3053 }
3054
3055 var unifiedOutput bytes.Buffer
3056 o.Out = &unifiedOutput
3057 o.ErrOut = &unifiedOutput
3058
3059 if err := o.Run(); err != nil {
3060 t.Errorf("error running apply: %v", err)
3061 }
3062
3063 got := unifiedOutput.String()
3064
3065 p := filepath.Join(testdir, manifest+"-expected-apply.txt")
3066 wantBytes, err := os.ReadFile(p)
3067 if err != nil {
3068 t.Fatalf("error reading file %q: %v", p, err)
3069 }
3070 want := string(wantBytes)
3071 if diff := cmp.Diff(want, got); diff != "" {
3072 t.Errorf("apply output has unexpected diff (-want +got):\n%s", diff)
3073 }
3074 }
3075 })
3076 })
3077 }
3078 }
3079
3080 func TestApplySetUpdateConflictsAreRetried(t *testing.T) {
3081 nameParentSecret := "my-set"
3082 pathSecret := "/namespaces/test/secrets/" + nameParentSecret
3083 secretYaml := `apiVersion: v1
3084 kind: Secret
3085 metadata:
3086 annotations:
3087 applyset.kubernetes.io/additional-namespaces: ""
3088 applyset.kubernetes.io/contains-group-resources: replicationcontrollers
3089 applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$
3090 creationTimestamp: null
3091 labels:
3092 applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1
3093 name: my-set
3094 namespace: test
3095 `
3096 tf := cmdtesting.NewTestFactory().WithNamespace("test")
3097 defer tf.Cleanup()
3098
3099 applyReturnedConflict := false
3100 appliedWithConflictsForced := false
3101 tf.Client = &fake.RESTClient{
3102 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
3103 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
3104 if req.Method == "GET" && req.URL.Path == pathSecret {
3105 data, err := yaml.YAMLToJSON([]byte(secretYaml))
3106 require.NoError(t, err)
3107 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(data))}, nil
3108 }
3109
3110 contentType := req.Header.Get("Content-Type")
3111 forceConflicts := req.URL.Query().Get("force") == "true"
3112 if req.Method == "PATCH" && contentType == string(types.ApplyPatchType) {
3113
3114 if req.URL.Path == pathSecret {
3115 if !forceConflicts {
3116 applyReturnedConflict = true
3117 return &http.Response{StatusCode: http.StatusConflict, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(strings.NewReader("Apply failed with 1 conflict: conflict with \"other\": .metadata.annotations.applyset.kubernetes.io/contains-group-resources"))}, nil
3118 }
3119 appliedWithConflictsForced = true
3120 }
3121 data, err := io.ReadAll(req.Body)
3122 require.NoError(t, err)
3123 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(data))}, nil
3124 }
3125 t.Fatalf("unexpected request to %s\n%#v", req.URL.Path, req)
3126 return nil, nil
3127 }),
3128 }
3129 tf.UnstructuredClient = tf.Client
3130
3131 ioStreams, _, outbuff, errbuff := genericiooptions.NewTestIOStreams()
3132 cmdutil.BehaviorOnFatal(fatalNoExit(t, ioStreams))
3133 defer cmdutil.DefaultBehaviorOnFatal()
3134
3135 cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) {
3136 cmd := NewCmdApply("kubectl", tf, ioStreams)
3137 cmd.Flags().Set("filename", filenameRC)
3138 cmd.Flags().Set("server-side", "true")
3139 cmd.Flags().Set("applyset", nameParentSecret)
3140 cmd.Flags().Set("prune", "true")
3141 cmd.Run(cmd, []string{})
3142 })
3143 assert.Equal(t, "replicationcontroller/test-rc serverside-applied\n", outbuff.String())
3144 assert.Equal(t, "", errbuff.String())
3145 assert.Truef(t, applyReturnedConflict, "test did not simulate a conflict scenario")
3146 assert.Truef(t, appliedWithConflictsForced, "conflicts were never forced")
3147 }
3148
3149 func TestApplyWithPruneV2Fail(t *testing.T) {
3150 tf := cmdtesting.NewTestFactory().WithNamespace("test")
3151 defer tf.Cleanup()
3152
3153 codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
3154
3155 scheme := runtime.NewScheme()
3156
3157 listMapping := map[schema.GroupVersionResource]string{
3158 {Group: "", Version: "v1", Resource: "namespaces"}: "NamespaceList",
3159 }
3160
3161 fakeDynamicClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
3162 tf.FakeDynamicClient = fakeDynamicClient
3163
3164 failDelete := false
3165 fakeDynamicClient.PrependReactor("delete", "*", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
3166 if failDelete {
3167 return true, nil, fmt.Errorf("an error on the server (\"\") has prevented the request from succeeding")
3168 }
3169 return false, nil, nil
3170 })
3171
3172 tf.UnstructuredClient = &fake.RESTClient{
3173 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
3174 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
3175 method := req.Method
3176
3177 tokens := strings.Split(strings.TrimPrefix(req.URL.Path, "/"), "/")
3178
3179 if len(tokens) == 2 && tokens[0] == "namespaces" && method == "GET" {
3180 name := tokens[1]
3181 gvr := schema.GroupVersionResource{Version: "v1", Resource: "namespaces"}
3182 ns, err := fakeDynamicClient.Tracker().Get(gvr, "", name)
3183 if err != nil {
3184 if apierrors.IsNotFound(err) {
3185 return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader()}, nil
3186 }
3187 t.Fatalf("error getting object: %v", err)
3188 }
3189 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, ns)}, nil
3190 }
3191
3192 if len(tokens) == 4 && tokens[0] == "namespaces" && tokens[2] == "secrets" && method == "GET" {
3193 namespace := tokens[1]
3194 name := tokens[3]
3195 gvr := schema.GroupVersionResource{Version: "v1", Resource: "secrets"}
3196 obj, err := fakeDynamicClient.Tracker().Get(gvr, namespace, name)
3197 if err != nil {
3198 if apierrors.IsNotFound(err) {
3199 return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader()}, nil
3200 }
3201 t.Fatalf("error getting object: %v", err)
3202 }
3203 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj)}, nil
3204 }
3205
3206 if len(tokens) == 4 && tokens[0] == "namespaces" && tokens[2] == "secrets" && method == "PATCH" {
3207 namespace := tokens[1]
3208 name := tokens[3]
3209 gvr := schema.GroupVersionResource{Version: "v1", Resource: "secrets"}
3210 var existing *unstructured.Unstructured
3211 existingObj, err := fakeDynamicClient.Tracker().Get(gvr, namespace, name)
3212 if err != nil {
3213 if !apierrors.IsNotFound(err) {
3214 t.Fatalf("error getting object: %v", err)
3215 }
3216 } else {
3217 existing = existingObj.(*unstructured.Unstructured)
3218 }
3219
3220 data, err := io.ReadAll(req.Body)
3221 if err != nil {
3222 t.Fatalf("unexpected error: %v", err)
3223 }
3224
3225 patch := &unstructured.Unstructured{}
3226 if err := runtime.DecodeInto(codec, data, patch); err != nil {
3227 t.Fatalf("unexpected error: %v", err)
3228 }
3229
3230 var returnData []byte
3231 if existing == nil {
3232 uid := types.UID(fmt.Sprintf("%v", time.Now().UnixNano()))
3233 patch.SetUID(uid)
3234
3235 if err := fakeDynamicClient.Tracker().Create(gvr, patch, namespace); err != nil {
3236 t.Fatalf("error creating object: %v", err)
3237 }
3238
3239 b, err := json.Marshal(patch)
3240 if err != nil {
3241 t.Fatalf("error marshalling response: %v", err)
3242 }
3243 returnData = b
3244 } else {
3245 patch.DeepCopyInto(existing)
3246 if err := fakeDynamicClient.Tracker().Update(gvr, existing, namespace); err != nil {
3247 t.Fatalf("error updating object: %v", err)
3248 }
3249 b, err := json.Marshal(existing)
3250 if err != nil {
3251 t.Fatalf("error marshalling response: %v", err)
3252 }
3253 returnData = b
3254 }
3255
3256 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(returnData))}, nil
3257 }
3258
3259 if len(tokens) == 1 && tokens[0] == "namespaces" && method == "POST" {
3260 data, err := io.ReadAll(req.Body)
3261 if err != nil {
3262 t.Fatalf("unexpected error: %v", err)
3263 }
3264
3265 u := &unstructured.Unstructured{}
3266 if err := runtime.DecodeInto(codec, data, u); err != nil {
3267 t.Fatalf("unexpected error: %v", err)
3268 }
3269
3270 name := u.GetName()
3271 ns := u.GetNamespace()
3272 gvr := schema.GroupVersionResource{Version: "v1", Resource: "namespaces"}
3273
3274 existing, err := fakeDynamicClient.Tracker().Get(gvr, ns, name)
3275 if err != nil {
3276 if apierrors.IsNotFound(err) {
3277 existing = nil
3278 } else {
3279 t.Fatalf("error fetching object: %v", err)
3280 }
3281 }
3282
3283 if existing != nil {
3284 return &http.Response{StatusCode: http.StatusConflict, Header: cmdtesting.DefaultHeader()}, nil
3285 }
3286
3287 uid := types.UID(fmt.Sprintf("%v", time.Now().UnixNano()))
3288 u.SetUID(uid)
3289
3290 if err := fakeDynamicClient.Tracker().Create(gvr, u, ns); err != nil {
3291 t.Fatalf("error creating object: %v", err)
3292 }
3293
3294 body := cmdtesting.ObjBody(codec, u)
3295
3296 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
3297 }
3298
3299 t.Fatalf("unexpected request: %v %v\n%#v", req.Method, req.URL, req)
3300 return nil, nil
3301 }),
3302 }
3303
3304 tf.Client = tf.UnstructuredClient
3305 tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc
3306
3307 testdirs := []string{"testdata/prune/simple"}
3308 for _, testdir := range testdirs {
3309 t.Run(testdir, func(t *testing.T) {
3310 cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) {
3311 manifests := []string{"manifest1", "manifest2"}
3312 for i, manifest := range manifests {
3313 if i != 0 {
3314 t.Logf("will inject failures into future delete operations")
3315 failDelete = true
3316 }
3317 t.Logf("applying manifest %v", manifest)
3318
3319 var unifiedOutput bytes.Buffer
3320 ioStreams := genericiooptions.IOStreams{
3321 ErrOut: &unifiedOutput,
3322 Out: &unifiedOutput,
3323 In: bytes.NewBufferString(""),
3324 }
3325 cmdutil.BehaviorOnFatal(fatalNoExit(t, ioStreams))
3326 defer cmdutil.DefaultBehaviorOnFatal()
3327
3328 rootCmd := &cobra.Command{
3329 Use: "kubectl",
3330 }
3331 kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag().WithDiscoveryBurst(300).WithDiscoveryQPS(50.0)
3332 kubeConfigFlags.AddFlags(rootCmd.PersistentFlags())
3333
3334 applyCmd := NewCmdApply("kubectl", tf, ioStreams)
3335 rootCmd.AddCommand(applyCmd)
3336
3337 rootCmd.SetArgs([]string{
3338 "apply",
3339 "--filename=" + filepath.Join(testdir, manifest+".yaml"),
3340 "--applyset=" + filepath.Base(testdir),
3341 "--namespace=default",
3342 "--prune=true",
3343 "--validate=false",
3344 })
3345 if err := rootCmd.Execute(); err != nil {
3346 t.Errorf("error running apply command: %v", err)
3347 }
3348
3349 got := unifiedOutput.String()
3350
3351 p := filepath.Join(testdir, "scenarios", "error-on-apply", manifest+"-expected-apply.txt")
3352 wantBytes, err := os.ReadFile(p)
3353 if err != nil {
3354 t.Fatalf("error reading file %q: %v", p, err)
3355 }
3356 want := string(wantBytes)
3357 if diff := cmp.Diff(want, got); diff != "" {
3358 t.Errorf("apply output has unexpected diff (-want +got):\n%s", diff)
3359 }
3360 }
3361 })
3362 })
3363 }
3364 }
3365
3366
3367
3368 func fatalNoExit(t *testing.T, ioStreams genericiooptions.IOStreams) func(msg string, code int) {
3369 return func(msg string, code int) {
3370 if len(msg) > 0 {
3371
3372 if !strings.HasSuffix(msg, "\n") {
3373 msg += "\n"
3374 }
3375 fmt.Fprint(ioStreams.ErrOut, msg)
3376 }
3377 }
3378 }
3379
3380 func TestApplySetDryRun(t *testing.T) {
3381 cmdtesting.InitTestErrorHandler(t)
3382 nameRC, rc := readReplicationController(t, filenameRC)
3383 pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
3384 nameParentSecret := "my-set"
3385 pathSecret := "/namespaces/test/secrets/" + nameParentSecret
3386
3387 tf := cmdtesting.NewTestFactory().WithNamespace("test")
3388 defer tf.Cleanup()
3389
3390
3391
3392 serverSideData := map[string][]byte{
3393 pathRC: rc,
3394 }
3395 fakeDryRunClient := func(t *testing.T, allowPatch bool) *fake.RESTClient {
3396 return &fake.RESTClient{
3397 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
3398 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
3399 if req.Method == "GET" {
3400 data, ok := serverSideData[req.URL.Path]
3401 if !ok {
3402 return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(nil))}, nil
3403 }
3404 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(data))}, nil
3405 }
3406 if req.Method == "PATCH" && allowPatch && req.URL.Query().Get("dryRun") == "All" {
3407 data, err := io.ReadAll(req.Body)
3408 require.NoError(t, err)
3409 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(data))}, nil
3410 }
3411
3412 t.Fatalf("unexpected request: to %s\n%#v", req.URL.Path, req)
3413 return nil, nil
3414 }),
3415 }
3416 }
3417
3418 t.Run("server side dry run", func(t *testing.T) {
3419 ioStreams, _, outbuff, _ := genericiooptions.NewTestIOStreams()
3420 tf.Client = fakeDryRunClient(t, true)
3421 tf.UnstructuredClient = tf.Client
3422 cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) {
3423 cmd := NewCmdApply("kubectl", tf, ioStreams)
3424 cmd.Flags().Set("filename", filenameRC)
3425 cmd.Flags().Set("server-side", "true")
3426 cmd.Flags().Set("applyset", nameParentSecret)
3427 cmd.Flags().Set("prune", "true")
3428 cmd.Flags().Set("dry-run", "server")
3429 cmd.Run(cmd, []string{})
3430 })
3431 assert.Equal(t, "replicationcontroller/test-rc serverside-applied (server dry run)\n", outbuff.String())
3432 assert.Equal(t, len(serverSideData), 1, "unexpected creation")
3433 require.Nil(t, serverSideData[pathSecret], "secret was created")
3434 })
3435
3436 t.Run("client side dry run", func(t *testing.T) {
3437 ioStreams, _, outbuff, _ := genericiooptions.NewTestIOStreams()
3438 tf.Client = fakeDryRunClient(t, false)
3439 tf.UnstructuredClient = tf.Client
3440 cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) {
3441 cmd := NewCmdApply("kubectl", tf, ioStreams)
3442 cmd.Flags().Set("filename", filenameRC)
3443 cmd.Flags().Set("applyset", nameParentSecret)
3444 cmd.Flags().Set("prune", "true")
3445 cmd.Flags().Set("dry-run", "client")
3446 cmd.Run(cmd, []string{})
3447 })
3448 assert.Equal(t, "replicationcontroller/test-rc configured (dry run)\n", outbuff.String())
3449 assert.Equal(t, len(serverSideData), 1, "unexpected creation")
3450 require.Nil(t, serverSideData[pathSecret], "secret was created")
3451 })
3452 }
3453
View as plain text