...

Source file src/k8s.io/kubectl/pkg/cmd/apply/apply_test.go

Documentation: k8s.io/kubectl/pkg/cmd/apply

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

View as plain text