kubectl/pkg/cmd/apply/apply_test.go

3450 lines
127 KiB
Go

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