/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apply import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "os" "path/filepath" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" sptest "k8s.io/apimachinery/pkg/util/strategicpatch/testing" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" dynamicfakeclient "k8s.io/client-go/dynamic/fake" openapiclient "k8s.io/client-go/openapi" "k8s.io/client-go/openapi/openapitest" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" testing2 "k8s.io/client-go/testing" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" "k8s.io/client-go/util/csaupgrade" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/openapi" utilpointer "k8s.io/utils/pointer" "k8s.io/utils/strings/slices" "sigs.k8s.io/yaml" ) var ( fakeSchema = sptest.Fake{Path: filepath.Join("..", "..", "..", "testdata", "openapi", "swagger.json")} fakeOpenAPIV3Legacy = sptest.OpenAPIV3Getter{Path: filepath.Join("..", "..", "..", "testdata", "openapi", "v3", "api", "v1.json")} fakeOpenAPIV3AppsV1 = sptest.OpenAPIV3Getter{Path: filepath.Join("..", "..", "..", "testdata", "openapi", "v3", "apis", "apps", "v1.json")} testingOpenAPISchemas = []testOpenAPISchema{AlwaysErrorsOpenAPISchema, FakeOpenAPISchema} AlwaysErrorsOpenAPISchema = testOpenAPISchema{ OpenAPISchemaFn: func() (openapi.Resources, error) { return nil, errors.New("cannot get openapi spec") }, OpenAPIV3ClientFunc: func() (openapiclient.Client, error) { return nil, errors.New("cannot get openapiv3 client") }, } FakeOpenAPISchema = testOpenAPISchema{ OpenAPISchemaFn: func() (openapi.Resources, error) { s, err := fakeSchema.OpenAPISchema() if err != nil { return nil, err } return openapi.NewOpenAPIData(s) }, OpenAPIV3ClientFunc: func() (openapiclient.Client, error) { c := openapitest.NewFakeClient() c.PathsMap["api/v1"] = openapitest.FakeGroupVersion{GVSpec: fakeOpenAPIV3Legacy.SchemaBytesOrDie()} c.PathsMap["apis/apps/v1"] = openapitest.FakeGroupVersion{GVSpec: fakeOpenAPIV3AppsV1.SchemaBytesOrDie()} return c, nil }, } AlwaysPanicSchema = testOpenAPISchema{ OpenAPISchemaFn: func() (openapi.Resources, error) { panic("error, openAPIV2 should not be called") }, OpenAPIV3ClientFunc: func() (openapiclient.Client, error) { return &OpenAPIV3ClientAlwaysPanic{}, nil }, } codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) ) type OpenAPIV3ClientAlwaysPanic struct{} func (o *OpenAPIV3ClientAlwaysPanic) Paths() (map[string]openapiclient.GroupVersion, error) { panic("Cannot get paths") } func noopOpenAPIV3Patch(t *testing.T, f func(t *testing.T)) { f(t) } func disableOpenAPIV3Patch(t *testing.T, f func(t *testing.T)) { cmdtesting.WithAlphaEnvsDisabled([]cmdutil.FeatureGate{cmdutil.OpenAPIV3Patch}, t, f) } var applyFeatureToggles = []func(*testing.T, func(t *testing.T)){noopOpenAPIV3Patch, disableOpenAPIV3Patch} type testOpenAPISchema struct { OpenAPISchemaFn func() (openapi.Resources, error) OpenAPIV3ClientFunc func() (openapiclient.Client, error) } func TestApplyExtraArgsFail(t *testing.T) { f := cmdtesting.NewTestFactory() defer f.Cleanup() cmd := &cobra.Command{} flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard()) flags.AddFlags(cmd) _, err := flags.ToOptions(f, cmd, "kubectl", []string{"rc"}) require.EqualError(t, err, "Unexpected args: [rc]\nSee ' -h' for help and examples") } func TestAlphaEnablement(t *testing.T) { alphas := map[cmdutil.FeatureGate]string{ cmdutil.ApplySet: "applyset", } for feature, flag := range alphas { f := cmdtesting.NewTestFactory() defer f.Cleanup() cmd := &cobra.Command{} flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard()) flags.AddFlags(cmd) require.Nil(t, cmd.Flags().Lookup(flag), "flag %q should not be registered without the %q feature enabled", flag, feature) cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{feature}, t, func(t *testing.T) { cmd := &cobra.Command{} flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard()) flags.AddFlags(cmd) require.NotNil(t, cmd.Flags().Lookup(flag), "flag %q should be registered with the %q feature enabled", flag, feature) }) } } func TestApplyFlagValidation(t *testing.T) { tests := []struct { args [][]string enableAlphas []cmdutil.FeatureGate expectedErr string }{ { args: [][]string{ {"force-conflicts", "true"}, }, expectedErr: "--force-conflicts only works with --server-side", }, { args: [][]string{ {"server-side", "true"}, {"dry-run", "client"}, }, expectedErr: "--dry-run=client doesn't work with --server-side (did you mean --dry-run=server instead?)", }, { args: [][]string{ {"force", "true"}, {"server-side", "true"}, }, expectedErr: "--force cannot be used with --server-side", }, { args: [][]string{ {"force", "true"}, {"dry-run", "server"}, }, expectedErr: "--dry-run=server cannot be used with --force", }, { args: [][]string{ {"all", "true"}, {"selector", "unused"}, }, expectedErr: "cannot set --all and --selector at the same time", }, { args: [][]string{ {"force", "true"}, {"prune", "true"}, {"all", "true"}, }, expectedErr: "--force cannot be used with --prune", }, { args: [][]string{ {"prune", "true"}, {"force", "true"}, {"applyset", "mySecret"}, {"namespace", "myNs"}, }, enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet}, expectedErr: "--force cannot be used with --prune", }, { args: [][]string{ {"server-side", "true"}, {"prune", "true"}, {"all", "true"}, }, expectedErr: "--prune is in alpha and doesn't currently work on objects created by server-side apply", }, { args: [][]string{ {"prune", "true"}, }, 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", }, { args: [][]string{ {"prune", "false"}, {"applyset", "mySecret"}, {"namespace", "myNs"}, }, enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet}, expectedErr: "--applyset requires --prune", }, { args: [][]string{ {"prune", "true"}, {"applyset", "mySecret"}, {"selector", "foo=bar"}, {"namespace", "myNs"}, }, enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet}, expectedErr: "--selector is incompatible with --applyset", }, { args: [][]string{ {"prune", "true"}, {"applyset", "mySecret"}, {"namespace", "myNs"}, {"all", "true"}, }, enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet}, expectedErr: "--all is incompatible with --applyset", }, { args: [][]string{ {"prune", "true"}, {"applyset", "mySecret"}, {"namespace", "myNs"}, {"prune-allowlist", "core/v1/ConfigMap"}, }, enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet}, expectedErr: "--prune-allowlist is incompatible with --applyset", }, } for i, test := range tests { t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { f := cmdtesting.NewTestFactory() defer f.Cleanup() f.Client = &fake.RESTClient{} f.UnstructuredClient = f.Client cmdtesting.WithAlphaEnvs(test.enableAlphas, t, func(t *testing.T) { cmd := &cobra.Command{} flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard()) flags.AddFlags(cmd) cmd.Flags().Set("filename", "unused") for _, arg := range test.args { if arg[0] == "namespace" { f.WithNamespace(arg[1]) } else { cmd.Flags().Set(arg[0], arg[1]) } } o, err := flags.ToOptions(f, cmd, "kubectl", []string{}) if err != nil { t.Fatalf("unexpected error creating apply options: %s", err) } err = o.Validate() if err == nil { t.Fatalf("missing expected error for case %d with args %+v", i, test.args) } if test.expectedErr != err.Error() { t.Errorf("expected error %s, got %s", test.expectedErr, err) } }) }) } } const ( filenameCM = "../../../testdata/apply/cm.yaml" filenameRC = "../../../testdata/apply/rc.yaml" filenameRCArgs = "../../../testdata/apply/rc-args.yaml" filenameRCLastAppliedArgs = "../../../testdata/apply/rc-lastapplied-args.yaml" filenameRCNoAnnotation = "../../../testdata/apply/rc-no-annotation.yaml" filenameRCLASTAPPLIED = "../../../testdata/apply/rc-lastapplied.yaml" filenameRCManagedFieldsLA = "../../../testdata/apply/rc-managedfields-lastapplied.yaml" filenameSVC = "../../../testdata/apply/service.yaml" filenameRCSVC = "../../../testdata/apply/rc-service.yaml" filenameNoExistRC = "../../../testdata/apply/rc-noexist.yaml" filenameRCPatchTest = "../../../testdata/apply/patch.json" dirName = "../../../testdata/apply/testdir" filenameRCJSON = "../../../testdata/apply/rc.json" filenamePodGeneratedName = "../../../testdata/apply/pod-generated-name.yaml" filenameWidgetClientside = "../../../testdata/apply/widget-clientside.yaml" filenameWidgetServerside = "../../../testdata/apply/widget-serverside.yaml" filenameDeployObjServerside = "../../../testdata/apply/deploy-serverside.yaml" filenameDeployObjClientside = "../../../testdata/apply/deploy-clientside.yaml" filenameApplySetCR = "../../../testdata/apply/applyset-cr.yaml" filenameApplySetCRD = "../../../testdata/apply/applysets-crd.yaml" ) func readConfigMapList(t *testing.T, filename string) [][]byte { data := readBytesFromFile(t, filename) cmList := corev1.ConfigMapList{} if err := runtime.DecodeInto(codec, data, &cmList); err != nil { t.Fatal(err) } var listCmBytes [][]byte for _, cm := range cmList.Items { cmBytes, err := runtime.Encode(codec, &cm) if err != nil { t.Fatal(err) } listCmBytes = append(listCmBytes, cmBytes) } return listCmBytes } func readBytesFromFile(t *testing.T, filename string) []byte { file, err := os.Open(filename) if err != nil { t.Fatal(err) } defer file.Close() data, err := io.ReadAll(file) if err != nil { t.Fatal(err) } return data } func readReplicationController(t *testing.T, filenameRC string) (string, []byte) { rcObj := readReplicationControllerFromFile(t, filenameRC) metaAccessor, err := meta.Accessor(rcObj) if err != nil { t.Fatal(err) } rcBytes, err := runtime.Encode(codec, rcObj) if err != nil { t.Fatal(err) } return metaAccessor.GetName(), rcBytes } func readReplicationControllerFromFile(t *testing.T, filename string) *corev1.ReplicationController { data := readBytesFromFile(t, filename) rc := corev1.ReplicationController{} if err := runtime.DecodeInto(codec, data, &rc); err != nil { t.Fatal(err) } return &rc } func readUnstructuredFromFile(t *testing.T, filename string) *unstructured.Unstructured { data := readBytesFromFile(t, filename) unst := unstructured.Unstructured{} if err := runtime.DecodeInto(codec, data, &unst); err != nil { t.Fatal(err) } return &unst } func readServiceFromFile(t *testing.T, filename string) *corev1.Service { data := readBytesFromFile(t, filename) svc := corev1.Service{} if err := runtime.DecodeInto(codec, data, &svc); err != nil { t.Fatal(err) } return &svc } func annotateRuntimeObject(t *testing.T, originalObj, currentObj runtime.Object, kind string) (string, []byte) { originalAccessor, err := meta.Accessor(originalObj) if err != nil { t.Fatal(err) } // The return value of this function is used in the body of the GET // request in the unit tests. Here we are adding a misc label to the object. // In tests, the validatePatchApplication() gets called in PATCH request // handler in fake round tripper. validatePatchApplication call // checks that this DELETE_ME label was deleted by the apply implementation in // kubectl. originalLabels := originalAccessor.GetLabels() originalLabels["DELETE_ME"] = "DELETE_ME" originalAccessor.SetLabels(originalLabels) original, err := runtime.Encode(unstructured.NewJSONFallbackEncoder(codec), originalObj) if err != nil { t.Fatal(err) } currentAccessor, err := meta.Accessor(currentObj) if err != nil { t.Fatal(err) } currentAnnotations := currentAccessor.GetAnnotations() if currentAnnotations == nil { currentAnnotations = make(map[string]string) } currentAnnotations[corev1.LastAppliedConfigAnnotation] = string(original) currentAccessor.SetAnnotations(currentAnnotations) current, err := runtime.Encode(unstructured.NewJSONFallbackEncoder(codec), currentObj) if err != nil { t.Fatal(err) } return currentAccessor.GetName(), current } func readAndAnnotateReplicationController(t *testing.T, filename string) (string, []byte) { rc1 := readReplicationControllerFromFile(t, filename) rc2 := readReplicationControllerFromFile(t, filename) return annotateRuntimeObject(t, rc1, rc2, "ReplicationController") } func readAndAnnotateService(t *testing.T, filename string) (string, []byte) { svc1 := readServiceFromFile(t, filename) svc2 := readServiceFromFile(t, filename) return annotateRuntimeObject(t, svc1, svc2, "Service") } func readAndAnnotateUnstructured(t *testing.T, filename string) (string, []byte) { obj1 := readUnstructuredFromFile(t, filename) obj2 := readUnstructuredFromFile(t, filename) return annotateRuntimeObject(t, obj1, obj2, "Widget") } func validatePatchApplication(t *testing.T, req *http.Request, patchType types.PatchType) { if got, wanted := req.Header.Get("Content-Type"), string(patchType); got != wanted { t.Fatalf("unexpected content-type expected: %s but actual %s\n", wanted, got) } patch, err := io.ReadAll(req.Body) if err != nil { t.Fatal(err) } patchMap := map[string]interface{}{} if err := json.Unmarshal(patch, &patchMap); err != nil { t.Fatal(err) } annotationsMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"}) if _, ok := annotationsMap[corev1.LastAppliedConfigAnnotation]; !ok { t.Fatalf("patch does not contain annotation:\n%s\n", patch) } labelMap := walkMapPath(t, patchMap, []string{"metadata", "labels"}) if deleteMe, ok := labelMap["DELETE_ME"]; !ok || deleteMe != nil { t.Fatalf("patch does not remove deleted key: DELETE_ME:\n%s\n", patch) } } func walkMapPath(t *testing.T, start map[string]interface{}, path []string) map[string]interface{} { finish := start for i := 0; i < len(path); i++ { var ok bool finish, ok = finish[path[i]].(map[string]interface{}) if !ok { t.Fatalf("key:%s of path:%v not found in map:%v", path[i], path, start) } } return finish } func TestRunApplyPrintsValidObjectList(t *testing.T) { cmdtesting.InitTestErrorHandler(t) configMapList := readConfigMapList(t, filenameCM) pathCM := "/namespaces/test/configmaps" tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case strings.HasPrefix(p, pathCM) && m == "GET": fallthrough case strings.HasPrefix(p, pathCM) && m == "PATCH": var body io.ReadCloser switch p { case pathCM + "/test0": body = io.NopCloser(bytes.NewReader(configMapList[0])) case pathCM + "/test1": body = io.NopCloser(bytes.NewReader(configMapList[1])) default: t.Errorf("unexpected request to %s", p) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameCM) cmd.Flags().Set("output", "json") cmd.Flags().Set("dry-run", "client") cmd.Run(cmd, []string{}) // ensure that returned list can be unmarshaled back into a configmap list cmList := corev1.List{} if err := runtime.DecodeInto(codec, buf.Bytes(), &cmList); err != nil { t.Fatal(err) } if len(cmList.Items) != 2 { t.Fatalf("Expected 2 items in the result; got %d", len(cmList.Items)) } if !strings.Contains(string(cmList.Items[0].Raw), "key1") { t.Fatalf("Did not get first ConfigMap at the first position") } if !strings.Contains(string(cmList.Items[1].Raw), "key2") { t.Fatalf("Did not get second ConfigMap at the second position") } } func TestRunApplyViewLastApplied(t *testing.T) { _, rcBytesWithConfig := readReplicationController(t, filenameRCLASTAPPLIED) _, rcBytesWithArgs := readReplicationController(t, filenameRCLastAppliedArgs) nameRC, rcBytes := readReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC tests := []struct { name, nameRC, pathRC, filePath, outputFormat, expectedErr, expectedOut, selector string args []string respBytes []byte }{ { name: "view with file", filePath: filenameRC, outputFormat: "", expectedErr: "", expectedOut: "test: 1234\n", selector: "", args: []string{}, respBytes: rcBytesWithConfig, }, { name: "test with file include `%s` in arguments", filePath: filenameRCArgs, outputFormat: "", expectedErr: "", expectedOut: "args: -random_flag=%s@domain.com\n", selector: "", args: []string{}, respBytes: rcBytesWithArgs, }, { name: "view with file json format", filePath: filenameRC, outputFormat: "json", expectedErr: "", expectedOut: "{\n \"test\": 1234\n}\n", selector: "", args: []string{}, respBytes: rcBytesWithConfig, }, { name: "view resource/name invalid format", filePath: "", outputFormat: "wide", 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", expectedOut: "", selector: "", args: []string{"replicationcontroller", "test-rc"}, respBytes: rcBytesWithConfig, }, { name: "view resource with label", filePath: "", outputFormat: "", expectedErr: "", expectedOut: "test: 1234\n", selector: "name=test-rc", args: []string{"replicationcontroller"}, respBytes: rcBytesWithConfig, }, { name: "view resource without annotations", filePath: "", outputFormat: "", expectedErr: "error: no last-applied-configuration annotation found on resource: test-rc", expectedOut: "", selector: "", args: []string{"replicationcontroller", "test-rc"}, respBytes: rcBytes, }, { name: "view resource no match", filePath: "", outputFormat: "", expectedErr: "Error from server (NotFound): the server could not find the requested resource (get replicationcontrollers no-match)", expectedOut: "", selector: "", args: []string{"replicationcontroller", "no-match"}, respBytes: nil, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: "v1"}, NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(test.respBytes)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == "/namespaces/test/replicationcontrollers" && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(test.respBytes)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == "/namespaces/test/replicationcontrollers/no-match" && m == "GET": return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Pod{})}, nil case p == "/api/v1/namespaces/test" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Namespace{})}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() cmdutil.BehaviorOnFatal(func(str string, code int) { if str != test.expectedErr { t.Errorf("%s: unexpected error: %s\nexpected: %s", test.name, str, test.expectedErr) } }) ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdApplyViewLastApplied(tf, ioStreams) if test.filePath != "" { cmd.Flags().Set("filename", test.filePath) } if test.outputFormat != "" { cmd.Flags().Set("output", test.outputFormat) } if test.selector != "" { cmd.Flags().Set("selector", test.selector) } cmd.Run(cmd, test.args) if buf.String() != test.expectedOut { t.Fatalf("%s: unexpected output: %s\nexpected: %s", test.name, buf.String(), test.expectedOut) } }) } } func TestApplyObjectWithoutAnnotation(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, rcBytes := readReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(rcBytes)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathRC && m == "PATCH": bodyRC := io.NopCloser(bytes.NewReader(rcBytes)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) // uses the name from the file, not the response expectRC := "replicationcontroller/" + nameRC + "\n" expectWarning := fmt.Sprintf(warningNoLastAppliedConfigAnnotation, "replicationcontrollers/test-rc", corev1.LastAppliedConfigAnnotation, "kubectl") if errBuf.String() != expectWarning { t.Fatalf("unexpected non-warning: %s\nexpected: %s", errBuf.String(), expectWarning) } if buf.String() != expectRC { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC) } } func TestOpenAPIV3PatchFeatureFlag(t *testing.T) { // OpenAPIV3 smp apply is on by default. // Test that users can disable it to use OpenAPI V2 smp // An OpenAPI V3 root that always panics is used to ensure // the v3 code path is never exercised when the feature is disabled cmdtesting.InitTestErrorHandler(t) nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC t.Run("test apply when a local object is specified - openapi v2 smp", func(t *testing.T) { disableOpenAPIV3Patch(t, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathRC && m == "PATCH": validatePatchApplication(t, req, types.StrategicMergePatchType) bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = FakeOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = AlwaysPanicSchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) // uses the name from the file, not the response expectRC := "replicationcontroller/" + nameRC + "\n" if buf.String() != expectRC { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC) } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } }) }) } func TestOpenAPIV3DoesNotLoadV2(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC t.Run("test apply when a local object is specified - openapi v3 smp", func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathRC && m == "PATCH": validatePatchApplication(t, req, types.StrategicMergePatchType) bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = AlwaysPanicSchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) // uses the name from the file, not the response expectRC := "replicationcontroller/" + nameRC + "\n" if buf.String() != expectRC { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC) } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } }) } func TestApplyObject(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, openAPIFeatureToggle := range applyFeatureToggles { t.Run("test apply when a local object is specified - openapi v3 smp", func(t *testing.T) { openAPIFeatureToggle(t, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathRC && m == "PATCH": validatePatchApplication(t, req, types.StrategicMergePatchType) bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) // uses the name from the file, not the response expectRC := "replicationcontroller/" + nameRC + "\n" if buf.String() != expectRC { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC) } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } }) }) } } } func TestApplyPruneObjects(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, openAPIFeatureToggle := range applyFeatureToggles { t.Run("test apply returns correct output", func(t *testing.T) { openAPIFeatureToggle(t, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathRC && m == "PATCH": validatePatchApplication(t, req, types.StrategicMergePatchType) bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("prune", "true") cmd.Flags().Set("namespace", "test") cmd.Flags().Set("output", "yaml") cmd.Flags().Set("all", "true") cmd.Run(cmd, []string{}) if !strings.Contains(buf.String(), "test-rc") { t.Fatalf("unexpected output: %s\nexpected to contain: %s", buf.String(), "test-rc") } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } }) }) } } } func TestApplyPruneObjectsWithAllowlist(t *testing.T) { cmdtesting.InitTestErrorHandler(t) // Read ReplicationController from the file we will use to apply. This one will not be pruned because it exists in the file. rc := readUnstructuredFromFile(t, filenameRC) err := setLastAppliedConfigAnnotation(rc) if err != nil { t.Fatal(err) } // Create another ReplicationController that can be pruned rc2 := &unstructured.Unstructured{ Object: map[string]interface{}{ "kind": "ReplicationController", "apiVersion": "v1", "metadata": map[string]interface{}{ "name": "test-rc2", "namespace": "test", "uid": "uid-rc2", }, }, } err = setLastAppliedConfigAnnotation(rc2) if err != nil { t.Fatal(err) } // Create a ConfigMap that can be pruned cm := &unstructured.Unstructured{ Object: map[string]interface{}{ "kind": "ConfigMap", "apiVersion": "v1", "metadata": map[string]interface{}{ "name": "test-cm", "namespace": "test", "uid": "uid-cm", }, }, } err = setLastAppliedConfigAnnotation(cm) if err != nil { t.Fatal(err) } // Create Namespace that can be pruned ns := &unstructured.Unstructured{ Object: map[string]interface{}{ "kind": "Namespace", "apiVersion": "v1", "metadata": map[string]interface{}{ "name": "test-apply", "uid": "uid-ns", }, }, } err = setLastAppliedConfigAnnotation(ns) if err != nil { t.Fatal(err) } // Create a ConfigMap without a UID. Resources without a UID will not be pruned. cmNoUID := &unstructured.Unstructured{ Object: map[string]interface{}{ "kind": "ConfigMap", "apiVersion": "v1", "metadata": map[string]interface{}{ "name": "test-cm-nouid", "namespace": "test", }, }, } err = setLastAppliedConfigAnnotation(cmNoUID) if err != nil { t.Fatal(err) } // Create a ConfigMap without a last applied annotation. Resources without a last applied annotation will not be pruned. cmNoLastApplied := &unstructured.Unstructured{ Object: map[string]interface{}{ "kind": "ConfigMap", "apiVersion": "v1", "metadata": map[string]interface{}{ "name": "test-cm-nolastapplied", "namespace": "test", "uid": "uid-cm-nolastapplied", }, }, } testCases := map[string]struct { currentResources []runtime.Object pruneAllowlist []string namespace string expectedPrunedResources []string expectedOutputs []string }{ "prune without namespace and allowlist should delete resources that are not in the specified file": { currentResources: []runtime.Object{rc, rc2, cm, ns}, expectedPrunedResources: []string{"test/test-cm", "test/test-rc2", "/test-apply"}, expectedOutputs: []string{ "replicationcontroller/test-rc unchanged", "configmap/test-cm pruned", "replicationcontroller/test-rc2 pruned", "namespace/test-apply pruned", }, }, // Deprecated: kubectl apply will no longer prune non-namespaced resources by default when used with the --namespace flag in a future release // namespace is a non-namespaced resource and will not be pruned in the future "prune with namespace and without allowlist should delete resources that are not in the specified file": { currentResources: []runtime.Object{rc, rc2, cm, ns}, namespace: "test", expectedPrunedResources: []string{"test/test-cm", "test/test-rc2", "/test-apply"}, expectedOutputs: []string{ "replicationcontroller/test-rc unchanged", "configmap/test-cm pruned", "replicationcontroller/test-rc2 pruned", "namespace/test-apply pruned", }, }, // Even namespace is a non-namespaced resource, it will be pruned if specified in pruneAllowList in the future "prune with namespace and allowlist should delete all matching resources": { currentResources: []runtime.Object{rc, cm, ns}, pruneAllowlist: []string{"core/v1/ConfigMap", "core/v1/Namespace"}, namespace: "test", expectedPrunedResources: []string{"test/test-cm", "/test-apply"}, expectedOutputs: []string{ "replicationcontroller/test-rc unchanged", "configmap/test-cm pruned", "namespace/test-apply pruned", }, }, "prune with allowlist should delete only matching resources": { currentResources: []runtime.Object{rc, rc2, cm}, pruneAllowlist: []string{"core/v1/ConfigMap"}, namespace: "test", expectedPrunedResources: []string{"test/test-cm"}, expectedOutputs: []string{ "replicationcontroller/test-rc unchanged", "configmap/test-cm pruned", }, }, "prune with allowlist specifying the same resource type multiple times should not fail": { currentResources: []runtime.Object{rc, rc2, cm}, pruneAllowlist: []string{"core/v1/ConfigMap", "core/v1/ConfigMap"}, namespace: "test", expectedPrunedResources: []string{"test/test-cm"}, expectedOutputs: []string{ "replicationcontroller/test-rc unchanged", "configmap/test-cm pruned", }, }, "prune with allowlist should not delete resources that exist in the specified file": { currentResources: []runtime.Object{rc, rc2, cm}, pruneAllowlist: []string{"core/v1/ReplicationController"}, namespace: "test", expectedPrunedResources: []string{"test/test-rc2"}, expectedOutputs: []string{ "replicationcontroller/test-rc unchanged", "replicationcontroller/test-rc2 pruned", }, }, "prune with allowlist specifying multiple resource types should delete matching resources": { currentResources: []runtime.Object{rc, rc2, cm}, pruneAllowlist: []string{"core/v1/ConfigMap", "core/v1/ReplicationController"}, namespace: "test", expectedPrunedResources: []string{"test/test-cm", "test/test-rc2"}, expectedOutputs: []string{ "replicationcontroller/test-rc unchanged", "configmap/test-cm pruned", "replicationcontroller/test-rc2 pruned", }, }, "prune should not delete resources that are missing a UID": { currentResources: []runtime.Object{rc, cm, cmNoUID}, expectedPrunedResources: []string{"test/test-cm"}, expectedOutputs: []string{ "replicationcontroller/test-rc unchanged", "configmap/test-cm pruned", }, }, "prune should not delete resources that are missing the last applied config annotation": { currentResources: []runtime.Object{rc, cm, cmNoLastApplied}, expectedPrunedResources: []string{"test/test-cm"}, expectedOutputs: []string{ "replicationcontroller/test-rc unchanged", "configmap/test-cm pruned", }, }, } for testCaseName, tc := range testCases { for _, testingOpenAPISchema := range testingOpenAPISchemas { t.Run(testCaseName, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/replicationcontrollers/test-rc" && m == "GET": encoded := runtime.EncodeOrDie(unstructured.UnstructuredJSONScheme, rc) bodyRC := io.NopCloser(strings.NewReader(encoded)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == "/namespaces/test/replicationcontrollers/test-rc" && m == "PATCH": encoded := runtime.EncodeOrDie(unstructured.UnstructuredJSONScheme, rc) bodyRC := io.NopCloser(strings.NewReader(encoded)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() for _, resource := range tc.currentResources { if err := tf.FakeDynamicClient.Tracker().Add(resource); err != nil { t.Fatal(err) } } ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("prune", "true") cmd.Flags().Set("namespace", tc.namespace) cmd.Flags().Set("all", "true") for _, allow := range tc.pruneAllowlist { cmd.Flags().Set("prune-allowlist", allow) } cmd.Run(cmd, []string{}) if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } actualOutput := buf.String() for _, expectedOutput := range tc.expectedOutputs { if !strings.Contains(actualOutput, expectedOutput) { t.Fatalf("expected output to contain %q, but it did not. Actual Output:\n%s", expectedOutput, actualOutput) } } var prunedResources []string for _, action := range tf.FakeDynamicClient.Actions() { if action.GetVerb() == "delete" { deleteAction := action.(testing2.DeleteAction) prunedResources = append(prunedResources, deleteAction.GetNamespace()+"/"+deleteAction.GetName()) } } // Make sure nothing unexpected was pruned for _, resource := range prunedResources { if !slices.Contains(tc.expectedPrunedResources, resource) { t.Fatalf("expected %s not to be pruned, but it was", resource) } } // Make sure everything that was expected to be pruned was pruned for _, resource := range tc.expectedPrunedResources { if !slices.Contains(prunedResources, resource) { t.Fatalf("expected %s to be pruned, but it was not", resource) } } }) } } } func setLastAppliedConfigAnnotation(obj runtime.Object) error { accessor, err := meta.Accessor(obj) if err != nil { return err } annotations := accessor.GetAnnotations() if annotations == nil { annotations = make(map[string]string) accessor.SetAnnotations(annotations) } annotations[corev1.LastAppliedConfigAnnotation] = runtime.EncodeOrDie(unstructured.NewJSONFallbackEncoder(codec), obj) accessor.SetAnnotations(annotations) return nil } // Tests that apply of object in need of CSA migration results in a call // to patch it. func TestApplyCSAMigration(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, rcWithManagedFields := readAndAnnotateReplicationController(t, filenameRCManagedFieldsLA) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC for _, openAPIFeatureToggle := range applyFeatureToggles { openAPIFeatureToggle(t, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() // The object after patch should be equivalent to the output of // csaupgrade.UpgradeManagedFields // // Parse object into unstructured, apply patch postPatchObj := &unstructured.Unstructured{} err := json.Unmarshal(rcWithManagedFields, &postPatchObj.Object) require.NoError(t, err) expectedPatch, err := csaupgrade.UpgradeManagedFieldsPatch(postPatchObj, sets.New(FieldManagerClientSideApply), "kubectl") require.NoError(t, err) err = csaupgrade.UpgradeManagedFields(postPatchObj, sets.New("kubectl-client-side-apply"), "kubectl") require.NoError(t, err) postPatchData, err := json.Marshal(postPatchObj) require.NoError(t, err) patches := 0 targetPatches := 2 applies := 0 tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": // During retry loop for patch fetch is performed. // keep returning the unchanged data if patches < targetPatches { bodyRC := io.NopCloser(bytes.NewReader(rcWithManagedFields)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil } t.Fatalf("should not do a fetch in serverside-apply") return nil, nil case p == pathRC && m == "PATCH": if got := req.Header.Get("Content-Type"); got == string(types.ApplyPatchType) { defer func() { applies += 1 }() switch applies { case 0: // initial apply. // Just return the same object but with managed fields bodyRC := io.NopCloser(bytes.NewReader(rcWithManagedFields)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case 1: // Second apply should include only last apply annotation unmodified // Return new object // NOTE: on a real server this would also modify the managed fields // just return the same object unmodified. It is not so important // for this test for the last-applied to appear in new field // manager response, only that the client asks the server to do it bodyRC := io.NopCloser(bytes.NewReader(rcWithManagedFields)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case 2, 3: // Before the last apply we have formed our JSONPAtch so it // should reply now with the upgraded object bodyRC := io.NopCloser(bytes.NewReader(postPatchData)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: require.Fail(t, "sent more apply requests than expected") return &http.Response{StatusCode: http.StatusBadRequest, Header: cmdtesting.DefaultHeader()}, nil } } else if got == string(types.JSONPatchType) { defer func() { patches += 1 }() // Require that the patch is equal to what is expected body, err := io.ReadAll(req.Body) require.NoError(t, err) require.Equal(t, expectedPatch, body) switch patches { case targetPatches - 1: bodyRC := io.NopCloser(bytes.NewReader(postPatchData)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: // Return conflict until the client has retried enough times return &http.Response{StatusCode: http.StatusConflict, Header: cmdtesting.DefaultHeader()}, nil } } else { t.Fatalf("unexpected content-type: %s\n", got) return nil, nil } default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = FakeOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "yaml") cmd.Flags().Set("server-side", "true") cmd.Flags().Set("show-managed-fields", "true") cmd.Run(cmd, []string{}) // JSONPatch should have been attempted exactly the given number of times require.Equal(t, targetPatches, patches, "should retry as many times as a conflict was returned") require.Equal(t, 3, applies, "should perform specified # of apply calls upon migration") require.Empty(t, errBuf.String()) // ensure that in the future there will be no migrations necessary // (by showing migration is a no-op) rc := &corev1.ReplicationController{} if err := runtime.DecodeInto(codec, buf.Bytes(), rc); err != nil { t.Fatal(err) } upgradedRC := rc.DeepCopyObject() err = csaupgrade.UpgradeManagedFields(upgradedRC, sets.New("kubectl-client-side-apply"), "kubectl") require.NoError(t, err) require.NotEmpty(t, rc.ManagedFields) require.Equal(t, rc, upgradedRC, "upgrading should be no-op in future") // Apply the upgraded object. // Expect only a single PATCH call to apiserver ioStreams, _, _, errBuf = genericiooptions.NewTestIOStreams() cmd = NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "yaml") cmd.Flags().Set("server-side", "true") cmd.Flags().Set("show-managed-fields", "true") cmd.Run(cmd, []string{}) require.Empty(t, errBuf) require.Equal(t, 4, applies, "only a single call to server-side apply should have been performed") require.Equal(t, targetPatches, patches, "no more json patches should have been needed") }) } } func TestApplyObjectOutput(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC // Add some extra data to the post-patch object postPatchObj := &unstructured.Unstructured{} if err := json.Unmarshal(currentRC, &postPatchObj.Object); err != nil { t.Fatal(err) } postPatchLabels := postPatchObj.GetLabels() if postPatchLabels == nil { postPatchLabels = map[string]string{} } postPatchLabels["post-patch"] = "value" postPatchObj.SetLabels(postPatchLabels) postPatchData, err := json.Marshal(postPatchObj) if err != nil { t.Fatal(err) } for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, openAPIFeatureToggle := range applyFeatureToggles { t.Run("test apply returns correct output", func(t *testing.T) { openAPIFeatureToggle(t, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathRC && m == "PATCH": validatePatchApplication(t, req, types.StrategicMergePatchType) bodyRC := io.NopCloser(bytes.NewReader(postPatchData)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "yaml") cmd.Run(cmd, []string{}) if !strings.Contains(buf.String(), "test-rc") { t.Fatalf("unexpected output: %s\nexpected to contain: %s", buf.String(), "test-rc") } if !strings.Contains(buf.String(), "post-patch: value") { t.Fatalf("unexpected output: %s\nexpected to contain: %s", buf.String(), "post-patch: value") } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } }) }) } } } func TestApplyRetry(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, openAPIFeatureToggle := range applyFeatureToggles { t.Run("test apply retries on conflict error", func(t *testing.T) { openAPIFeatureToggle(t, func(t *testing.T) { firstPatch := true retry := false getCount := 0 tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": getCount++ bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathRC && m == "PATCH": if firstPatch { firstPatch = false statusErr := apierrors.NewConflict(schema.GroupResource{Group: "", Resource: "rc"}, "test-rc", fmt.Errorf("the object has been modified. Please apply at first")) bodyBytes, _ := json.Marshal(statusErr) bodyErr := io.NopCloser(bytes.NewReader(bodyBytes)) return &http.Response{StatusCode: http.StatusConflict, Header: cmdtesting.DefaultHeader(), Body: bodyErr}, nil } retry = true validatePatchApplication(t, req, types.StrategicMergePatchType) bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) if !retry || getCount != 2 { t.Fatalf("apply didn't retry when get conflict error") } // uses the name from the file, not the response expectRC := "replicationcontroller/" + nameRC + "\n" if buf.String() != expectRC { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC) } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } }) }) } } } func TestApplyNonExistObject(t *testing.T) { nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers" pathNameRC := pathRC + "/" + nameRC tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/api/v1/namespaces/test" && m == "GET": return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(nil))}, nil case p == pathNameRC && m == "GET": return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(nil))}, nil case p == pathRC && m == "POST": bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) // uses the name from the file, not the response expectRC := "replicationcontroller/" + nameRC + "\n" if buf.String() != expectRC { t.Errorf("unexpected output: %s\nexpected: %s", buf.String(), expectRC) } } func TestApplyEmptyPatch(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, _ := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers" pathNameRC := pathRC + "/" + nameRC verifyPost := false var body []byte tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: "v1"}, NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/api/v1/namespaces/test" && m == "GET": return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(nil))}, nil case p == pathNameRC && m == "GET": if body == nil { return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(nil))}, nil } bodyRC := io.NopCloser(bytes.NewReader(body)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathRC && m == "POST": body, _ = io.ReadAll(req.Body) verifyPost = true bodyRC := io.NopCloser(bytes.NewReader(body)) return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() // 1. apply non exist object ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) expectRC := "replicationcontroller/" + nameRC + "\n" if buf.String() != expectRC { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC) } if !verifyPost { t.Fatal("No server-side post call detected") } // 2. test apply already exist object, will not send empty patch request ioStreams, _, buf, _ = genericiooptions.NewTestIOStreams() cmd = NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) if buf.String() != expectRC { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC) } } func TestApplyMultipleObjectsAsList(t *testing.T) { testApplyMultipleObjects(t, true) } func TestApplyMultipleObjectsAsFiles(t *testing.T) { testApplyMultipleObjects(t, false) } func testApplyMultipleObjects(t *testing.T, asList bool) { nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC nameSVC, currentSVC := readAndAnnotateService(t, filenameSVC) pathSVC := "/namespaces/test/services/" + nameSVC for _, testingOpenAPISchema := range testingOpenAPISchemas { t.Run("test apply on multiple objects", func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathRC && m == "PATCH": validatePatchApplication(t, req, types.StrategicMergePatchType) bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathSVC && m == "GET": bodySVC := io.NopCloser(bytes.NewReader(currentSVC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodySVC}, nil case p == pathSVC && m == "PATCH": validatePatchApplication(t, req, types.StrategicMergePatchType) bodySVC := io.NopCloser(bytes.NewReader(currentSVC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodySVC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) if asList { cmd.Flags().Set("filename", filenameRCSVC) } else { cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("filename", filenameSVC) } cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) // Names should come from the REST response, NOT the files expectRC := "replicationcontroller/" + nameRC + "\n" expectSVC := "service/" + nameSVC + "\n" // Test both possible orders since output is non-deterministic. expectOne := expectRC + expectSVC expectTwo := expectSVC + expectRC if buf.String() != expectOne && buf.String() != expectTwo { t.Fatalf("unexpected output: %s\nexpected: %s OR %s", buf.String(), expectOne, expectTwo) } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } }) } } func readDeploymentFromFile(t *testing.T, file string) []byte { raw := readBytesFromFile(t, file) obj := &appsv1.Deployment{} if err := runtime.DecodeInto(codec, raw, obj); err != nil { t.Fatal(err) } objJSON, err := runtime.Encode(codec, obj) if err != nil { t.Fatal(err) } return objJSON } func TestApplyNULLPreservation(t *testing.T) { cmdtesting.InitTestErrorHandler(t) deploymentName := "nginx-deployment" deploymentPath := "/namespaces/test/deployments/" + deploymentName verifiedPatch := false deploymentBytes := readDeploymentFromFile(t, filenameDeployObjServerside) for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, openAPIFeatureToggle := range applyFeatureToggles { t.Run("test apply preserves NULL fields", func(t *testing.T) { openAPIFeatureToggle(t, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == deploymentPath && m == "GET": body := io.NopCloser(bytes.NewReader(deploymentBytes)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil case p == deploymentPath && m == "PATCH": patch, err := io.ReadAll(req.Body) if err != nil { t.Fatal(err) } patchMap := map[string]interface{}{} if err := json.Unmarshal(patch, &patchMap); err != nil { t.Fatal(err) } annotationMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"}) if _, ok := annotationMap[corev1.LastAppliedConfigAnnotation]; !ok { t.Fatalf("patch does not contain annotation:\n%s\n", patch) } strategy := walkMapPath(t, patchMap, []string{"spec", "strategy"}) if value, ok := strategy["rollingUpdate"]; !ok || value != nil { t.Fatalf("patch did not retain null value in key: rollingUpdate:\n%s\n", patch) } verifiedPatch = true // The real API server would had returned the patched object but Kubectl // is ignoring the actual return object. // TODO: Make this match actual server behavior by returning the patched object. body := io.NopCloser(bytes.NewReader(deploymentBytes)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameDeployObjClientside) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) expected := "deployment.apps/" + deploymentName + "\n" if buf.String() != expected { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected) } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } if !verifiedPatch { t.Fatal("No server-side patch call detected") } }) }) } } } // TestUnstructuredApply checks apply operations on an unstructured object func TestUnstructuredApply(t *testing.T) { cmdtesting.InitTestErrorHandler(t) name, curr := readAndAnnotateUnstructured(t, filenameWidgetClientside) path := "/namespaces/test/widgets/" + name verifiedPatch := false for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, openAPIFeatureToggle := range applyFeatureToggles { t.Run("test apply works correctly with unstructured objects", func(t *testing.T) { openAPIFeatureToggle(t, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == path && m == "GET": body := io.NopCloser(bytes.NewReader(curr)) return &http.Response{ StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil case p == path && m == "PATCH": validatePatchApplication(t, req, types.MergePatchType) verifiedPatch = true body := io.NopCloser(bytes.NewReader(curr)) return &http.Response{ StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameWidgetClientside) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) expected := "widget.unit-test.test.com/" + name + "\n" if buf.String() != expected { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected) } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } if !verifiedPatch { t.Fatal("No server-side patch call detected") } }) }) } } } // TestUnstructuredIdempotentApply checks repeated apply operation on an unstructured object func TestUnstructuredIdempotentApply(t *testing.T) { cmdtesting.InitTestErrorHandler(t) serversideObject := readUnstructuredFromFile(t, filenameWidgetServerside) serversideData, err := runtime.Encode(unstructured.NewJSONFallbackEncoder(codec), serversideObject) if err != nil { t.Fatal(err) } path := "/namespaces/test/widgets/widget" for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, openAPIFeatureToggle := range applyFeatureToggles { t.Run("test repeated apply operations on an unstructured object", func(t *testing.T) { openAPIFeatureToggle(t, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == path && m == "GET": body := io.NopCloser(bytes.NewReader(serversideData)) return &http.Response{ StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil case p == path && m == "PATCH": // In idempotent updates, kubectl will resolve to an empty patch and not send anything to the server // Thus, if we reach this branch, kubectl is unnecessarily sending a patch. patch, err := io.ReadAll(req.Body) if err != nil { t.Fatal(err) } t.Fatalf("Unexpected Patch: %s", patch) return nil, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameWidgetClientside) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) expected := "widget.unit-test.test.com/widget\n" if buf.String() != expected { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected) } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } }) }) } } } func TestRunApplySetLastApplied(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC noExistRC, _ := readAndAnnotateReplicationController(t, filenameNoExistRC) noExistPath := "/namespaces/test/replicationcontrollers/" + noExistRC noAnnotationName, noAnnotationRC := readReplicationController(t, filenameRCNoAnnotation) noAnnotationPath := "/namespaces/test/replicationcontrollers/" + noAnnotationName tests := []struct { name, nameRC, pathRC, filePath, expectedErr, expectedOut, output string }{ { name: "set with exist object", filePath: filenameRC, expectedErr: "", expectedOut: "replicationcontroller/test-rc\n", output: "name", }, { name: "set with no-exist object", filePath: filenameNoExistRC, expectedErr: "Error from server (NotFound): the server could not find the requested resource (get replicationcontrollers no-exist)", expectedOut: "", output: "name", }, { name: "set for the annotation does not exist on the live object", filePath: filenameRCNoAnnotation, expectedErr: "error: no last-applied-configuration annotation found on resource: no-annotation, to create the annotation, run the command with --create-annotation", expectedOut: "", output: "name", }, { name: "set with exist object output json", filePath: filenameRCJSON, expectedErr: "", expectedOut: "replicationcontroller/test-rc\n", output: "name", }, { name: "set test for a directory of files", filePath: dirName, expectedErr: "", expectedOut: "replicationcontroller/test-rc\nreplicationcontroller/test-rc\n", output: "name", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: "v1"}, NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == noAnnotationPath && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(noAnnotationRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == noExistPath && m == "GET": return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Pod{})}, nil case p == pathRC && m == "PATCH": checkPatchString(t, req) bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == "/api/v1/namespaces/test" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Namespace{})}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() cmdutil.BehaviorOnFatal(func(str string, code int) { if str != test.expectedErr { t.Errorf("%s: unexpected error: %s\nexpected: %s", test.name, str, test.expectedErr) } }) ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdApplySetLastApplied(tf, ioStreams) cmd.Flags().Set("filename", test.filePath) cmd.Flags().Set("output", test.output) cmd.Run(cmd, []string{}) if buf.String() != test.expectedOut { t.Fatalf("%s: unexpected output: %s\nexpected: %s", test.name, buf.String(), test.expectedOut) } }) } cmdutil.BehaviorOnFatal(func(str string, code int) {}) } func checkPatchString(t *testing.T, req *http.Request) { checkString := string(readBytesFromFile(t, filenameRCPatchTest)) patch, err := io.ReadAll(req.Body) if err != nil { t.Fatal(err) } patchMap := map[string]interface{}{} if err := json.Unmarshal(patch, &patchMap); err != nil { t.Fatal(err) } annotationsMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"}) if _, ok := annotationsMap[corev1.LastAppliedConfigAnnotation]; !ok { t.Fatalf("patch does not contain annotation:\n%s\n", patch) } resultString := annotationsMap["kubectl.kubernetes.io/last-applied-configuration"] if resultString != checkString { t.Fatalf("patch annotation is not correct, expect:%s\n but got:%s\n", checkString, resultString) } } func TestForceApply(t *testing.T) { cmdtesting.InitTestErrorHandler(t) scheme := runtime.NewScheme() nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC pathRCList := "/namespaces/test/replicationcontrollers" expected := map[string]int{ "getOk": 6, "getNotFound": 1, "getList": 0, "patch": 6, "delete": 1, "post": 1, } for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, openAPIFeatureToggle := range applyFeatureToggles { t.Run("test apply with --force", func(t *testing.T) { openAPIFeatureToggle(t, func(t *testing.T) { deleted := false isScaledDownToZero := false counts := map[string]int{} tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.ClientConfigVal = cmdtesting.DefaultClientConfig() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case strings.HasSuffix(p, pathRC) && m == "GET": if deleted { counts["getNotFound"]++ return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte{}))}, nil } counts["getOk"]++ var bodyRC io.ReadCloser if isScaledDownToZero { rcObj := readReplicationControllerFromFile(t, filenameRC) rcObj.Spec.Replicas = utilpointer.Int32Ptr(0) rcBytes, err := runtime.Encode(codec, rcObj) if err != nil { t.Fatal(err) } bodyRC = io.NopCloser(bytes.NewReader(rcBytes)) } else { bodyRC = io.NopCloser(bytes.NewReader(currentRC)) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case strings.HasSuffix(p, pathRCList) && m == "GET": counts["getList"]++ rcObj := readUnstructuredFromFile(t, filenameRC) list := &unstructured.UnstructuredList{ Object: map[string]interface{}{ "apiVersion": "v1", "kind": "ReplicationControllerList", }, Items: []unstructured.Unstructured{*rcObj}, } listBytes, err := runtime.Encode(codec, list) if err != nil { t.Fatal(err) } bodyRCList := io.NopCloser(bytes.NewReader(listBytes)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRCList}, nil case strings.HasSuffix(p, pathRC) && m == "PATCH": counts["patch"]++ if counts["patch"] <= 6 { statusErr := apierrors.NewConflict(schema.GroupResource{Group: "", Resource: "rc"}, "test-rc", fmt.Errorf("the object has been modified. Please apply at first")) bodyBytes, _ := json.Marshal(statusErr) bodyErr := io.NopCloser(bytes.NewReader(bodyBytes)) return &http.Response{StatusCode: http.StatusConflict, Header: cmdtesting.DefaultHeader(), Body: bodyErr}, nil } t.Fatalf("unexpected request: %#v after %v tries\n%#v", req.URL, counts["patch"], req) return nil, nil case strings.HasSuffix(p, pathRC) && m == "DELETE": counts["delete"]++ deleted = true bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case strings.HasSuffix(p, pathRC) && m == "PUT": counts["put"]++ bodyRC := io.NopCloser(bytes.NewReader(currentRC)) isScaledDownToZero = true return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case strings.HasSuffix(p, pathRCList) && m == "POST": counts["post"]++ deleted = false isScaledDownToZero = false bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } fakeDynamicClient := dynamicfakeclient.NewSimpleDynamicClient(scheme) tf.FakeDynamicClient = fakeDynamicClient tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.Client = tf.UnstructuredClient tf.ClientConfigVal = &restclient.Config{} ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "name") cmd.Flags().Set("force", "true") cmd.Run(cmd, []string{}) for method, exp := range expected { if exp != counts[method] { t.Errorf("Unexpected amount of %q API calls, wanted %v got %v", method, exp, counts[method]) } } if expected := "replicationcontroller/" + nameRC + "\n"; buf.String() != expected { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected) } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } }) }) } } } func TestDontAllowForceApplyWithServerDryRun(t *testing.T) { expectedError := "error: --dry-run=server cannot be used with --force" cmdutil.BehaviorOnFatal(func(str string, code int) { panic(str) }) defer func() { actualError := recover() if expectedError != actualError { t.Fatalf(`expected error "%s", but got "%s"`, expectedError, actualError) } }() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("dry-run", "server") cmd.Flags().Set("force", "true") cmd.Run(cmd, []string{}) t.Fatalf(`expected error "%s"`, expectedError) } func TestDontAllowForceApplyWithServerSide(t *testing.T) { expectedError := "error: --force cannot be used with --server-side" cmdutil.BehaviorOnFatal(func(str string, code int) { panic(str) }) defer func() { actualError := recover() if expectedError != actualError { t.Fatalf(`expected error "%s", but got "%s"`, expectedError, actualError) } }() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("force", "true") cmd.Run(cmd, []string{}) t.Fatalf(`expected error "%s"`, expectedError) } func TestDontAllowApplyWithPodGeneratedName(t *testing.T) { expectedError := "error: from testing-: cannot use generate name with apply" cmdutil.BehaviorOnFatal(func(str string, code int) { if str != expectedError { t.Fatalf(`expected error "%s", but got "%s"`, expectedError, str) } }) tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenamePodGeneratedName) cmd.Flags().Set("dry-run", "client") cmd.Run(cmd, []string{}) } func TestApplySetParentValidation(t *testing.T) { for name, test := range map[string]struct { applysetFlag string namespaceFlag string setup func(*testing.T, *cmdtesting.TestFactory) expectParentKind string expectBlankParentNs bool expectErr string }{ "parent type must be valid": { applysetFlag: "doesnotexist/thename", expectErr: "invalid parent reference \"doesnotexist/thename\": no matches for /, Resource=doesnotexist", }, "parent name must be present": { applysetFlag: "secret/", expectErr: "invalid parent reference \"secret/\": name cannot be blank", }, "configmap parents are valid": { applysetFlag: "configmap/thename", namespaceFlag: "mynamespace", expectParentKind: "ConfigMap", }, "secret parents are valid": { applysetFlag: "secret/thename", namespaceFlag: "mynamespace", expectParentKind: "Secret", }, "plural resource works": { applysetFlag: "secrets/thename", namespaceFlag: "mynamespace", expectParentKind: "Secret", }, "other namespaced builtin parents types are correctly parsed but invalid": { applysetFlag: "deployments.apps/thename", expectParentKind: "Deployment", expectErr: "[namespace is required to use namespace-scoped ApplySet, resource \"apps/v1, Resource=deployments\" is not permitted as an ApplySet parent]", }, "namespaced builtin parents with multi-segment groups are correctly parsed but invalid": { applysetFlag: "priorityclasses.scheduling.k8s.io/thename", expectParentKind: "PriorityClass", expectErr: "resource \"scheduling.k8s.io/v1alpha1, Resource=priorityclasses\" is not permitted as an ApplySet parent", }, "non-namespaced builtin types are correctly parsed but invalid": { applysetFlag: "namespaces/thename", expectParentKind: "Namespace", namespaceFlag: "somenamespace", expectBlankParentNs: true, expectErr: "resource \"/v1, Resource=namespaces\" is not permitted as an ApplySet parent", }, "parent namespace should use the value of the namespace flag": { applysetFlag: "mysecret", namespaceFlag: "mynamespace", expectParentKind: "Secret", }, "parent namespace should not use the default namespace from ClientConfig": { applysetFlag: "mysecret", setup: func(t *testing.T, f *cmdtesting.TestFactory) { // by default, the value "default" is used for the namespace // make sure this assumption still holds ns, overridden, err := f.ToRawKubeConfigLoader().Namespace() require.NoError(t, err) require.Falsef(t, overridden, "namespace unexpectedly overridden") require.Equal(t, "default", ns) }, expectBlankParentNs: true, expectParentKind: "Secret", expectErr: "namespace is required to use namespace-scoped ApplySet", }, "parent namespace should not use the default namespace from the user's kubeconfig": { applysetFlag: "mysecret", setup: func(t *testing.T, f *cmdtesting.TestFactory) { kubeConfig := clientcmdapi.NewConfig() kubeConfig.CurrentContext = "default" kubeConfig.Contexts["default"] = &clientcmdapi.Context{Namespace: "bar"} clientConfig := clientcmd.NewDefaultClientConfig(*kubeConfig, &clientcmd.ConfigOverrides{ ClusterDefaults: clientcmdapi.Cluster{Server: "http://localhost:8080"}}) f.WithClientConfig(clientConfig) }, expectBlankParentNs: true, expectParentKind: "Secret", expectErr: "namespace is required to use namespace-scoped ApplySet", }, } { t.Run(name, func(t *testing.T) { cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := &cobra.Command{} flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard()) flags.AddFlags(cmd) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("applyset", test.applysetFlag) cmd.Flags().Set("prune", "true") f := cmdtesting.NewTestFactory() defer f.Cleanup() setUpClientsForApplySetWithSSA(t, f) var expectedParentNs string if test.namespaceFlag != "" { f.WithNamespace(test.namespaceFlag) if !test.expectBlankParentNs { expectedParentNs = test.namespaceFlag } } if test.setup != nil { test.setup(t, f) } o, err := flags.ToOptions(f, cmd, "kubectl", []string{}) if test.expectErr == "" { require.NoError(t, err, "ToOptions error") } else if err != nil { require.EqualError(t, err, test.expectErr) return } assert.Equal(t, expectedParentNs, o.ApplySet.parentRef.Namespace) assert.Equal(t, test.expectParentKind, o.ApplySet.parentRef.GroupVersionKind.Kind) err = o.Validate() if test.expectErr != "" { require.EqualError(t, err, test.expectErr) } else { require.NoError(t, err, "Validate error") } }) }) } } func setUpClientsForApplySetWithSSA(t *testing.T, tf *cmdtesting.TestFactory, objects ...runtime.Object) { listMapping := map[schema.GroupVersionResource]string{ {Group: "", Version: "v1", Resource: "services"}: "ServiceList", {Group: "", Version: "v1", Resource: "replicationcontrollers"}: "ReplicationControllerList", {Group: "apiextensions.k8s.io", Version: "v1", Resource: "customresourcedefinitions"}: "CustomResourceDefinitionList", } fakeDynamicClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), listMapping, objects...) tf.FakeDynamicClient = fakeDynamicClient tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { tokens := strings.Split(strings.TrimPrefix(req.URL.Path, "/"), "/") var gvr schema.GroupVersionResource var name, namespace string if len(tokens) == 4 && tokens[0] == "namespaces" { // e.g. namespaces/my-ns/secrets/my-secret namespace = tokens[1] name = tokens[3] gvr = schema.GroupVersionResource{Version: "v1", Resource: tokens[2]} } else if len(tokens) == 2 && tokens[0] == "applysets" { gvr = schema.GroupVersionResource{Group: "company.com", Version: "v1", Resource: tokens[0]} name = tokens[1] } else { t.Fatalf("unexpected request: path segments %v: request: \n%#v", tokens, req) return nil, nil } switch req.Method { case "GET": obj, err := fakeDynamicClient.Tracker().Get(gvr, namespace, name) if err == nil { objJson, err := json.Marshal(obj) require.NoError(t, err) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.BytesBody(objJson)}, nil } else if apierrors.IsNotFound(err) { return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader()}, nil } else { t.Fatalf("error getting object: %v", err) } case "PATCH": require.Equal(t, string(types.ApplyPatchType), req.Header.Get("Content-Type"), "received patch request with unexpected patch type") var existing *unstructured.Unstructured existingObj, err := fakeDynamicClient.Tracker().Get(gvr, namespace, name) if err != nil { if !apierrors.IsNotFound(err) { t.Fatalf("error getting object: %v", err) } } else { existing = existingObj.(*unstructured.Unstructured) } data, err := io.ReadAll(req.Body) require.NoError(t, err) patch := &unstructured.Unstructured{} err = runtime.DecodeInto(codec, data, patch) require.NoError(t, err) var returnData []byte if existing == nil { patch.SetUID("a-static-fake-uid") err := fakeDynamicClient.Tracker().Create(gvr, patch, namespace) require.NoError(t, err, "error creating object") returnData, err = json.Marshal(patch) require.NoError(t, err, "error marshalling response: %v", err) } else { uid := existing.GetUID() patch.DeepCopyInto(existing) existing.SetUID(uid) err = fakeDynamicClient.Tracker().Update(gvr, existing, namespace) require.NoError(t, err, "error updating object") returnData, err = json.Marshal(existing) require.NoError(t, err, "error marshalling response") } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(returnData))}, nil default: t.Fatalf("unexpected request: %s\n%#v", req.URL.Path, req) return nil, nil } return nil, nil }), } tf.Client = tf.UnstructuredClient } func TestLoadObjects(t *testing.T) { f := cmdtesting.NewTestFactory().WithNamespace("test") defer f.Cleanup() f.Client = &fake.RESTClient{} f.UnstructuredClient = f.Client testFiles := []string{"testdata/prune/simple/manifest1", "testdata/prune/simple/manifest2"} for _, testFile := range testFiles { t.Run(testFile, func(t *testing.T) { cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := &cobra.Command{} flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard()) flags.AddFlags(cmd) cmd.Flags().Set("filename", testFile+".yaml") cmd.Flags().Set("applyset", filepath.Base(filepath.Dir(testFile))) cmd.Flags().Set("prune", "true") o, err := flags.ToOptions(f, cmd, "kubectl", []string{}) if err != nil { t.Fatalf("unexpected error creating apply options: %v", err) } err = o.Validate() if err != nil { t.Fatalf("unexpected error from validate: %v", err) } resources, err := o.GetObjects() if err != nil { t.Fatalf("GetObjects gave unexpected error %v", err) } var objectYAMLs []string for _, obj := range resources { y, err := yaml.Marshal(obj.Object) if err != nil { t.Fatalf("error marshaling object: %v", err) } objectYAMLs = append(objectYAMLs, string(y)) } got := strings.Join(objectYAMLs, "\n---\n\n") p := testFile + "-expected-getobjects.yaml" wantBytes, err := os.ReadFile(p) if err != nil { t.Fatalf("error reading file %q: %v", p, err) } want := string(wantBytes) if diff := cmp.Diff(want, got); diff != "" { t.Errorf("GetObjects returned unexpected diff (-want +got):\n%s", diff) } }) }) } } func TestApplySetParentManagement(t *testing.T) { nameParentSecret := "my-set" tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() replicationController := readUnstructuredFromFile(t, filenameRC) setUpClientsForApplySetWithSSA(t, tf, replicationController) failDeletes := false tf.FakeDynamicClient.PrependReactor("delete", "*", func(action testing2.Action) (handled bool, ret runtime.Object, err error) { if failDeletes { return true, nil, fmt.Errorf("an error on the server (\"\") has prevented the request from succeeding") } return false, nil, nil }) cmdutil.BehaviorOnFatal(func(s string, i int) { if failDeletes && s == `error: pruning ReplicationController test/test-rc: an error on the server ("") has prevented the request from succeeding` { t.Logf("got expected error %q", s) } else { t.Fatalf("unexpected exit %d: %s", i, s) } }) defer cmdutil.DefaultBehaviorOnFatal() // Initially, the rc 'exists' server side but the svc and applyset secret do not // This should 'update' the rc and create the secret ioStreams, _, outbuff, errbuff := genericiooptions.NewTestIOStreams() cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("applyset", nameParentSecret) cmd.Flags().Set("prune", "true") cmd.Run(cmd, []string{}) }) assert.Equal(t, "replicationcontroller/test-rc serverside-applied\n", outbuff.String()) assert.Equal(t, "", errbuff.String()) createdSecret, err := tf.FakeDynamicClient.Tracker().Get(schema.GroupVersionResource{Resource: "secrets", Version: "v1"}, "test", nameParentSecret) require.NoError(t, err) createSecretYaml, err := yaml.Marshal(createdSecret) require.NoError(t, err) require.Equal(t, `apiVersion: v1 kind: Secret metadata: annotations: applyset.kubernetes.io/additional-namespaces: "" applyset.kubernetes.io/contains-group-kinds: ReplicationController applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$ creationTimestamp: null labels: applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1 name: my-set namespace: test uid: a-static-fake-uid `, string(createSecretYaml)) // Next, do an apply that creates a second resource, the svc, and updates the applyset secret outbuff.Reset() errbuff.Reset() cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("filename", filenameSVC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("applyset", nameParentSecret) cmd.Flags().Set("prune", "true") cmd.Run(cmd, []string{}) }) assert.Equal(t, "replicationcontroller/test-rc serverside-applied\nservice/test-service serverside-applied\n", outbuff.String()) assert.Equal(t, "", errbuff.String()) updatedSecret, err := tf.FakeDynamicClient.Tracker().Get(schema.GroupVersionResource{Resource: "secrets", Version: "v1"}, "test", nameParentSecret) require.NoError(t, err) updatedSecretYaml, err := yaml.Marshal(updatedSecret) require.NoError(t, err) require.Equal(t, `apiVersion: v1 kind: Secret metadata: annotations: applyset.kubernetes.io/additional-namespaces: "" applyset.kubernetes.io/contains-group-kinds: ReplicationController,Service applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$ creationTimestamp: null labels: applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1 name: my-set namespace: test uid: a-static-fake-uid `, string(updatedSecretYaml)) // Next, do an apply that attempts to remove the rc from the set, but pruning fails // Both types remain in the ApplySet failDeletes = true outbuff.Reset() errbuff.Reset() cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameSVC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("applyset", nameParentSecret) cmd.Flags().Set("prune", "true") cmd.Run(cmd, []string{}) }) assert.Equal(t, "service/test-service serverside-applied\n", outbuff.String()) assert.Equal(t, "", errbuff.String()) updatedSecret, err = tf.FakeDynamicClient.Tracker().Get(schema.GroupVersionResource{Resource: "secrets", Version: "v1"}, "test", nameParentSecret) require.NoError(t, err) updatedSecretYaml, err = yaml.Marshal(updatedSecret) require.NoError(t, err) require.Equal(t, `apiVersion: v1 kind: Secret metadata: annotations: applyset.kubernetes.io/additional-namespaces: "" applyset.kubernetes.io/contains-group-kinds: ReplicationController,Service applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$ creationTimestamp: null labels: applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1 name: my-set namespace: test uid: a-static-fake-uid `, string(updatedSecretYaml)) // Finally, do an apply that successfully removes the rc and updates the set failDeletes = false outbuff.Reset() errbuff.Reset() cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameSVC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("applyset", nameParentSecret) cmd.Flags().Set("prune", "true") cmd.Run(cmd, []string{}) }) assert.Equal(t, "service/test-service serverside-applied\nreplicationcontroller/test-rc pruned\n", outbuff.String()) assert.Equal(t, "", errbuff.String()) updatedSecret, err = tf.FakeDynamicClient.Tracker().Get(schema.GroupVersionResource{Resource: "secrets", Version: "v1"}, "test", nameParentSecret) require.NoError(t, err) updatedSecretYaml, err = yaml.Marshal(updatedSecret) require.NoError(t, err) require.Equal(t, `apiVersion: v1 kind: Secret metadata: annotations: applyset.kubernetes.io/additional-namespaces: "" applyset.kubernetes.io/contains-group-kinds: Service applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$ creationTimestamp: null labels: applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1 name: my-set namespace: test uid: a-static-fake-uid `, string(updatedSecretYaml)) } func TestApplySetInvalidLiveParent(t *testing.T) { nameParentSecret := "my-set" tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() type testCase struct { gksAnnotation string toolingAnnotation string idLabel string expectErr string } validIDLabel := "applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1" validToolingAnnotation := "kubectl/v1.27.0" validGksAnnotation := "Deployment.apps,Namespace,Secret" for name, test := range map[string]testCase{ "group-resources annotation is required": { gksAnnotation: "", toolingAnnotation: validToolingAnnotation, idLabel: validIDLabel, 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", }, "group-resources annotation should not contain invalid resources": { gksAnnotation: "does-not-exist", toolingAnnotation: validToolingAnnotation, idLabel: validIDLabel, 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 \"\"", }, "tooling annotation is required": { gksAnnotation: validGksAnnotation, toolingAnnotation: "", idLabel: validIDLabel, expectErr: "error: ApplySet parent object \"secrets./my-set\" already exists and is missing required annotation \"applyset.kubernetes.io/tooling\"", }, "tooling annotation must have kubectl prefix": { gksAnnotation: validGksAnnotation, toolingAnnotation: "helm/v3", idLabel: validIDLabel, expectErr: "error: ApplySet parent object \"secrets./my-set\" already exists and is managed by tooling \"helm\" instead of \"kubectl\"", }, "tooling annotation with invalid prefix with one segment can be parsed": { gksAnnotation: validGksAnnotation, toolingAnnotation: "helm", idLabel: validIDLabel, expectErr: "error: ApplySet parent object \"secrets./my-set\" already exists and is managed by tooling \"helm\" instead of \"kubectl\"", }, "tooling annotation with invalid prefix with many segments can be parsed": { gksAnnotation: validGksAnnotation, toolingAnnotation: "example.com/tool/why/v1", idLabel: validIDLabel, expectErr: "error: ApplySet parent object \"secrets./my-set\" already exists and is managed by tooling \"example.com/tool/why\" instead of \"kubectl\"", }, "ID label is required": { gksAnnotation: validGksAnnotation, toolingAnnotation: validToolingAnnotation, idLabel: "", expectErr: "error: ApplySet parent object \"secrets./my-set\" exists and does not have required label applyset.kubernetes.io/id", }, "ID label must match the ApplySet's real ID": { gksAnnotation: validGksAnnotation, toolingAnnotation: validToolingAnnotation, idLabel: "somethingelse", 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), }, } { t.Run(name, func(t *testing.T) { require.NotEmpty(t, test.expectErr, "invalid test case") cmdutil.BehaviorOnFatal(func(s string, i int) { assert.Equal(t, test.expectErr, s) }) defer cmdutil.DefaultBehaviorOnFatal() secret := &unstructured.Unstructured{} secret.SetKind("Secret") secret.SetAPIVersion("v1") secret.SetName(nameParentSecret) secret.SetNamespace("test") annotations := make(map[string]string) labels := make(map[string]string) if test.gksAnnotation != "" { annotations[ApplySetGKsAnnotation] = test.gksAnnotation } if test.toolingAnnotation != "" { annotations[ApplySetToolingAnnotation] = test.toolingAnnotation } if test.idLabel != "" { labels[ApplySetParentIDLabel] = test.idLabel } secret.SetAnnotations(annotations) secret.SetLabels(labels) setUpClientsForApplySetWithSSA(t, tf, secret) cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameSVC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("applyset", nameParentSecret) cmd.Flags().Set("prune", "true") cmd.Run(cmd, []string{}) }) }) } } func TestApplySet_ClusterScopedCustomResourceParent(t *testing.T) { tf := cmdtesting.NewTestFactory() defer tf.Cleanup() replicationController := readUnstructuredFromFile(t, filenameRC) crd := readUnstructuredFromFile(t, filenameApplySetCRD) cr := readUnstructuredFromFile(t, filenameApplySetCR) setUpClientsForApplySetWithSSA(t, tf, replicationController, crd) ioStreams, _, outbuff, errbuff := genericiooptions.NewTestIOStreams() cmdutil.BehaviorOnFatal(func(s string, i int) { require.Equal(t, "error: custom resource ApplySet parents cannot be created automatically", s) }) defer cmdutil.DefaultBehaviorOnFatal() // Initially, the rc 'exists' server side the parent CR does not. This should fail. cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("applyset", fmt.Sprintf("applysets.company.com/my-set")) cmd.Flags().Set("prune", "true") cmd.Run(cmd, []string{}) }) cmdtesting.InitTestErrorHandler(t) // Simulate creating the CR parent out of band require.NoError(t, tf.FakeDynamicClient.Tracker().Add(cr)) cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("applyset", fmt.Sprintf("applysets.company.com/my-set")) cmd.Flags().Set("prune", "true") cmd.Run(cmd, []string{}) }) assert.Equal(t, "replicationcontroller/test-rc serverside-applied\n", outbuff.String()) assert.Equal(t, "", errbuff.String()) updatedCR, err := tf.FakeDynamicClient.Tracker().Get(schema.GroupVersionResource{Resource: "applysets", Version: "v1", Group: "company.com"}, "", "my-set") require.NoError(t, err) updatedCRYaml, err := yaml.Marshal(updatedCR) require.NoError(t, err) require.Equal(t, `apiVersion: company.com/v1 kind: ApplySet metadata: annotations: applyset.kubernetes.io/additional-namespaces: test applyset.kubernetes.io/contains-group-kinds: ReplicationController applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$ creationTimestamp: null labels: applyset.kubernetes.io/id: applyset-rhp1a-HVAVT_dFgyEygyA1BEB82HPp2o10UiFTpqtAs-v1 name: my-set `, string(updatedCRYaml)) } func TestApplyWithPruneV2(t *testing.T) { testdirs := []string{"testdata/prune/simple"} for _, testdir := range testdirs { t.Run(testdir, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) scheme := runtime.NewScheme() listMapping := map[schema.GroupVersionResource]string{ {Group: "", Version: "v1", Resource: "namespaces"}: "NamespaceList", } fakeDynamicClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) tf.FakeDynamicClient = fakeDynamicClient tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { method := req.Method tokens := strings.Split(strings.TrimPrefix(req.URL.Path, "/"), "/") if len(tokens) == 2 && tokens[0] == "namespaces" && method == "GET" { name := tokens[1] gvr := schema.GroupVersionResource{Version: "v1", Resource: "namespaces"} ns, err := fakeDynamicClient.Tracker().Get(gvr, "", name) if err != nil { if apierrors.IsNotFound(err) { return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader()}, nil } t.Fatalf("error getting object: %v", err) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, ns)}, nil } if len(tokens) == 4 && tokens[0] == "namespaces" && tokens[2] == "secrets" && method == "GET" { namespace := tokens[1] name := tokens[3] gvr := schema.GroupVersionResource{Version: "v1", Resource: "secrets"} obj, err := fakeDynamicClient.Tracker().Get(gvr, namespace, name) if err != nil { if apierrors.IsNotFound(err) { return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader()}, nil } t.Fatalf("error getting object: %v", err) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj)}, nil } if len(tokens) == 4 && tokens[0] == "namespaces" && tokens[2] == "secrets" && method == "PATCH" { namespace := tokens[1] name := tokens[3] gvr := schema.GroupVersionResource{Version: "v1", Resource: "secrets"} var existing *unstructured.Unstructured existingObj, err := fakeDynamicClient.Tracker().Get(gvr, namespace, name) if err != nil { if !apierrors.IsNotFound(err) { t.Fatalf("error getting object: %v", err) } } else { existing = existingObj.(*unstructured.Unstructured) } data, err := io.ReadAll(req.Body) if err != nil { t.Fatalf("unexpected error: %v", err) } patch := &unstructured.Unstructured{} if err := runtime.DecodeInto(codec, data, patch); err != nil { t.Fatalf("unexpected error: %v", err) } var returnData []byte if existing == nil { uid := types.UID(fmt.Sprintf("%v", time.Now().UnixNano())) patch.SetUID(uid) if err := fakeDynamicClient.Tracker().Create(gvr, patch, namespace); err != nil { t.Fatalf("error creating object: %v", err) } b, err := json.Marshal(patch) if err != nil { t.Fatalf("error marshalling response: %v", err) } returnData = b } else { patch.DeepCopyInto(existing) if err := fakeDynamicClient.Tracker().Update(gvr, existing, namespace); err != nil { t.Fatalf("error updating object: %v", err) } b, err := json.Marshal(existing) if err != nil { t.Fatalf("error marshalling response: %v", err) } returnData = b } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(returnData))}, nil } if len(tokens) == 1 && tokens[0] == "namespaces" && method == "POST" { data, err := io.ReadAll(req.Body) if err != nil { t.Fatalf("unexpected error: %v", err) } u := &unstructured.Unstructured{} if err := runtime.DecodeInto(codec, data, u); err != nil { t.Fatalf("unexpected error: %v", err) } name := u.GetName() ns := u.GetNamespace() gvr := schema.GroupVersionResource{Version: "v1", Resource: "namespaces"} existing, err := fakeDynamicClient.Tracker().Get(gvr, ns, name) if err != nil { if apierrors.IsNotFound(err) { existing = nil } else { t.Fatalf("error fetching object: %v", err) } } if existing != nil { return &http.Response{StatusCode: http.StatusConflict, Header: cmdtesting.DefaultHeader()}, nil } uid := types.UID(fmt.Sprintf("%v", time.Now().UnixNano())) u.SetUID(uid) if err := fakeDynamicClient.Tracker().Create(gvr, u, ns); err != nil { t.Fatalf("error creating object: %v", err) } body := cmdtesting.ObjBody(codec, u) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil } t.Fatalf("unexpected request: %v %v\n%#v", req.Method, req.URL, req) return nil, nil }), } tf.Client = tf.UnstructuredClient tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { manifests := []string{"manifest1", "manifest2"} for _, manifest := range manifests { t.Logf("applying manifest %v", manifest) cmd := &cobra.Command{} flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard()) flags.AddFlags(cmd) cmd.Flags().Set("filename", filepath.Join(testdir, manifest+".yaml")) cmd.Flags().Set("applyset", filepath.Base(testdir)) cmd.Flags().Set("prune", "true") cmd.Flags().Set("validate", "false") o, err := flags.ToOptions(tf, cmd, "kubectl", []string{}) if err != nil { t.Fatalf("unexpected error creating apply options: %v", err) } err = o.Validate() if err != nil { t.Fatalf("unexpected error from validate: %v", err) } var unifiedOutput bytes.Buffer o.Out = &unifiedOutput o.ErrOut = &unifiedOutput if err := o.Run(); err != nil { t.Errorf("error running apply: %v", err) } got := unifiedOutput.String() p := filepath.Join(testdir, manifest+"-expected-apply.txt") wantBytes, err := os.ReadFile(p) if err != nil { t.Fatalf("error reading file %q: %v", p, err) } want := string(wantBytes) if diff := cmp.Diff(want, got); diff != "" { t.Errorf("apply output has unexpected diff (-want +got):\n%s", diff) } } }) }) } } func TestApplySetUpdateConflictsAreRetried(t *testing.T) { nameParentSecret := "my-set" pathSecret := "/namespaces/test/secrets/" + nameParentSecret secretYaml := `apiVersion: v1 kind: Secret metadata: annotations: applyset.kubernetes.io/additional-namespaces: "" applyset.kubernetes.io/contains-group-resources: replicationcontrollers applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$ creationTimestamp: null labels: applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1 name: my-set namespace: test ` tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() applyReturnedConflict := false appliedWithConflictsForced := false tf.Client = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { if req.Method == "GET" && req.URL.Path == pathSecret { data, err := yaml.YAMLToJSON([]byte(secretYaml)) require.NoError(t, err) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(data))}, nil } contentType := req.Header.Get("Content-Type") forceConflicts := req.URL.Query().Get("force") == "true" if req.Method == "PATCH" && contentType == string(types.ApplyPatchType) { // make the ApplySet secret SSA request fail unless conflicts are forced if req.URL.Path == pathSecret { if !forceConflicts { applyReturnedConflict = true 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 } appliedWithConflictsForced = true } data, err := io.ReadAll(req.Body) require.NoError(t, err) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(data))}, nil } t.Fatalf("unexpected request to %s\n%#v", req.URL.Path, req) return nil, nil }), } tf.UnstructuredClient = tf.Client ioStreams, _, outbuff, errbuff := genericiooptions.NewTestIOStreams() cmdutil.BehaviorOnFatal(fatalNoExit(t, ioStreams)) defer cmdutil.DefaultBehaviorOnFatal() cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("applyset", nameParentSecret) cmd.Flags().Set("prune", "true") cmd.Run(cmd, []string{}) }) assert.Equal(t, "replicationcontroller/test-rc serverside-applied\n", outbuff.String()) assert.Equal(t, "", errbuff.String()) assert.Truef(t, applyReturnedConflict, "test did not simulate a conflict scenario") assert.Truef(t, appliedWithConflictsForced, "conflicts were never forced") } func TestApplyWithPruneV2Fail(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) scheme := runtime.NewScheme() listMapping := map[schema.GroupVersionResource]string{ {Group: "", Version: "v1", Resource: "namespaces"}: "NamespaceList", } fakeDynamicClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) tf.FakeDynamicClient = fakeDynamicClient failDelete := false fakeDynamicClient.PrependReactor("delete", "*", func(action testing2.Action) (handled bool, ret runtime.Object, err error) { if failDelete { return true, nil, fmt.Errorf("an error on the server (\"\") has prevented the request from succeeding") } return false, nil, nil }) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { method := req.Method tokens := strings.Split(strings.TrimPrefix(req.URL.Path, "/"), "/") if len(tokens) == 2 && tokens[0] == "namespaces" && method == "GET" { name := tokens[1] gvr := schema.GroupVersionResource{Version: "v1", Resource: "namespaces"} ns, err := fakeDynamicClient.Tracker().Get(gvr, "", name) if err != nil { if apierrors.IsNotFound(err) { return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader()}, nil } t.Fatalf("error getting object: %v", err) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, ns)}, nil } if len(tokens) == 4 && tokens[0] == "namespaces" && tokens[2] == "secrets" && method == "GET" { namespace := tokens[1] name := tokens[3] gvr := schema.GroupVersionResource{Version: "v1", Resource: "secrets"} obj, err := fakeDynamicClient.Tracker().Get(gvr, namespace, name) if err != nil { if apierrors.IsNotFound(err) { return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader()}, nil } t.Fatalf("error getting object: %v", err) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj)}, nil } if len(tokens) == 4 && tokens[0] == "namespaces" && tokens[2] == "secrets" && method == "PATCH" { namespace := tokens[1] name := tokens[3] gvr := schema.GroupVersionResource{Version: "v1", Resource: "secrets"} var existing *unstructured.Unstructured existingObj, err := fakeDynamicClient.Tracker().Get(gvr, namespace, name) if err != nil { if !apierrors.IsNotFound(err) { t.Fatalf("error getting object: %v", err) } } else { existing = existingObj.(*unstructured.Unstructured) } data, err := io.ReadAll(req.Body) if err != nil { t.Fatalf("unexpected error: %v", err) } patch := &unstructured.Unstructured{} if err := runtime.DecodeInto(codec, data, patch); err != nil { t.Fatalf("unexpected error: %v", err) } var returnData []byte if existing == nil { uid := types.UID(fmt.Sprintf("%v", time.Now().UnixNano())) patch.SetUID(uid) if err := fakeDynamicClient.Tracker().Create(gvr, patch, namespace); err != nil { t.Fatalf("error creating object: %v", err) } b, err := json.Marshal(patch) if err != nil { t.Fatalf("error marshalling response: %v", err) } returnData = b } else { patch.DeepCopyInto(existing) if err := fakeDynamicClient.Tracker().Update(gvr, existing, namespace); err != nil { t.Fatalf("error updating object: %v", err) } b, err := json.Marshal(existing) if err != nil { t.Fatalf("error marshalling response: %v", err) } returnData = b } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(returnData))}, nil } if len(tokens) == 1 && tokens[0] == "namespaces" && method == "POST" { data, err := io.ReadAll(req.Body) if err != nil { t.Fatalf("unexpected error: %v", err) } u := &unstructured.Unstructured{} if err := runtime.DecodeInto(codec, data, u); err != nil { t.Fatalf("unexpected error: %v", err) } name := u.GetName() ns := u.GetNamespace() gvr := schema.GroupVersionResource{Version: "v1", Resource: "namespaces"} existing, err := fakeDynamicClient.Tracker().Get(gvr, ns, name) if err != nil { if apierrors.IsNotFound(err) { existing = nil } else { t.Fatalf("error fetching object: %v", err) } } if existing != nil { return &http.Response{StatusCode: http.StatusConflict, Header: cmdtesting.DefaultHeader()}, nil } uid := types.UID(fmt.Sprintf("%v", time.Now().UnixNano())) u.SetUID(uid) if err := fakeDynamicClient.Tracker().Create(gvr, u, ns); err != nil { t.Fatalf("error creating object: %v", err) } body := cmdtesting.ObjBody(codec, u) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil } t.Fatalf("unexpected request: %v %v\n%#v", req.Method, req.URL, req) return nil, nil }), } tf.Client = tf.UnstructuredClient tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc testdirs := []string{"testdata/prune/simple"} for _, testdir := range testdirs { t.Run(testdir, func(t *testing.T) { cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { manifests := []string{"manifest1", "manifest2"} for i, manifest := range manifests { if i != 0 { t.Logf("will inject failures into future delete operations") failDelete = true } t.Logf("applying manifest %v", manifest) var unifiedOutput bytes.Buffer ioStreams := genericiooptions.IOStreams{ ErrOut: &unifiedOutput, Out: &unifiedOutput, In: bytes.NewBufferString(""), } cmdutil.BehaviorOnFatal(fatalNoExit(t, ioStreams)) defer cmdutil.DefaultBehaviorOnFatal() rootCmd := &cobra.Command{ Use: "kubectl", } kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag().WithDiscoveryBurst(300).WithDiscoveryQPS(50.0) kubeConfigFlags.AddFlags(rootCmd.PersistentFlags()) applyCmd := NewCmdApply("kubectl", tf, ioStreams) rootCmd.AddCommand(applyCmd) rootCmd.SetArgs([]string{ "apply", "--filename=" + filepath.Join(testdir, manifest+".yaml"), "--applyset=" + filepath.Base(testdir), "--namespace=default", "--prune=true", "--validate=false", }) if err := rootCmd.Execute(); err != nil { t.Errorf("error running apply command: %v", err) } got := unifiedOutput.String() p := filepath.Join(testdir, "scenarios", "error-on-apply", manifest+"-expected-apply.txt") wantBytes, err := os.ReadFile(p) if err != nil { t.Fatalf("error reading file %q: %v", p, err) } want := string(wantBytes) if diff := cmp.Diff(want, got); diff != "" { t.Errorf("apply output has unexpected diff (-want +got):\n%s", diff) } } }) }) } } // fatalNoExit is a handler that replaces the default cmdutil.BehaviorOnFatal, // that still prints as expected, but does not call os.Exit (which terminates our tests) func fatalNoExit(t *testing.T, ioStreams genericiooptions.IOStreams) func(msg string, code int) { return func(msg string, code int) { if len(msg) > 0 { // add newline if needed if !strings.HasSuffix(msg, "\n") { msg += "\n" } fmt.Fprint(ioStreams.ErrOut, msg) } } } func TestApplySetDryRun(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, rc := readReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC nameParentSecret := "my-set" pathSecret := "/namespaces/test/secrets/" + nameParentSecret tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() // Scenario: the rc 'exists' server side but the applyset secret does not // In dry run mode, non-dry run patch requests should not be made, and the secret should not be created serverSideData := map[string][]byte{ pathRC: rc, } fakeDryRunClient := func(t *testing.T, allowPatch bool) *fake.RESTClient { return &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { if req.Method == "GET" { data, ok := serverSideData[req.URL.Path] if !ok { return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(nil))}, nil } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(data))}, nil } if req.Method == "PATCH" && allowPatch && req.URL.Query().Get("dryRun") == "All" { data, err := io.ReadAll(req.Body) require.NoError(t, err) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(data))}, nil } t.Fatalf("unexpected request: to %s\n%#v", req.URL.Path, req) return nil, nil }), } } t.Run("server side dry run", func(t *testing.T) { ioStreams, _, outbuff, _ := genericiooptions.NewTestIOStreams() tf.Client = fakeDryRunClient(t, true) tf.UnstructuredClient = tf.Client cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("applyset", nameParentSecret) cmd.Flags().Set("prune", "true") cmd.Flags().Set("dry-run", "server") cmd.Run(cmd, []string{}) }) assert.Equal(t, "replicationcontroller/test-rc serverside-applied (server dry run)\n", outbuff.String()) assert.Equal(t, len(serverSideData), 1, "unexpected creation") require.Nil(t, serverSideData[pathSecret], "secret was created") }) t.Run("client side dry run", func(t *testing.T) { ioStreams, _, outbuff, _ := genericiooptions.NewTestIOStreams() tf.Client = fakeDryRunClient(t, false) tf.UnstructuredClient = tf.Client cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("applyset", nameParentSecret) cmd.Flags().Set("prune", "true") cmd.Flags().Set("dry-run", "client") cmd.Run(cmd, []string{}) }) assert.Equal(t, "replicationcontroller/test-rc configured (dry run)\n", outbuff.String()) assert.Equal(t, len(serverSideData), 1, "unexpected creation") require.Nil(t, serverSideData[pathSecret], "secret was created") }) }