From 1cc30bdfb204c3696cda9c0aab6afe259efe488b Mon Sep 17 00:00:00 2001 From: Hakan Baba Date: Wed, 8 Feb 2017 11:04:18 -0800 Subject: [PATCH] Add a unit test for applies and idempotent applys to the TPR entries. The tests in apply_test follows the general pattern of other tests. We load from a file in test/fixtures and mock the API server in the function closure in the HttpClient call. In PATCH request rount-tripper we check that the kubectl apply implementation worked as expected. References #40841 --- pkg/kubectl/cmd/apply_test.go | 170 ++++++++++++++++++ pkg/kubectl/cmd/testing/fake.go | 16 ++ .../kubectl/cmd/apply/widget-clientside.yaml | 8 + .../kubectl/cmd/apply/widget-serverside.yaml | 10 ++ 4 files changed, 204 insertions(+) create mode 100644 test/fixtures/pkg/kubectl/cmd/apply/widget-clientside.yaml create mode 100644 test/fixtures/pkg/kubectl/cmd/apply/widget-serverside.yaml diff --git a/pkg/kubectl/cmd/apply_test.go b/pkg/kubectl/cmd/apply_test.go index fe2ca8e7c07..3aac10a12c9 100644 --- a/pkg/kubectl/cmd/apply_test.go +++ b/pkg/kubectl/cmd/apply_test.go @@ -29,6 +29,7 @@ import ( kubeerr "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/client-go/rest/fake" @@ -68,6 +69,9 @@ const ( filenameRCPatchTest = "../../../test/fixtures/pkg/kubectl/cmd/apply/patch.json" dirName = "../../../test/fixtures/pkg/kubectl/cmd/apply/testdir" filenameRCJSON = "../../../test/fixtures/pkg/kubectl/cmd/apply/rc.json" + + filenameWidgetClientside = "../../../test/fixtures/pkg/kubectl/cmd/apply/widget-clientside.yaml" + filenameWidgetServerside = "../../../test/fixtures/pkg/kubectl/cmd/apply/widget-serverside.yaml" ) func readBytesFromFile(t *testing.T, filename string) []byte { @@ -109,6 +113,15 @@ func readReplicationControllerFromFile(t *testing.T, filename string) *api.Repli return &rc } +func readUnstructuredFromFile(t *testing.T, filename string) *unstructured.Unstructured { + data := readBytesFromFile(t, filename) + unst := unstructured.Unstructured{} + if err := runtime.DecodeInto(testapi.Default.Codec(), data, &unst); err != nil { + t.Fatal(err) + } + return &unst +} + func readServiceFromFile(t *testing.T, filename string) *api.Service { data := readBytesFromFile(t, filename) svc := api.Service{} @@ -125,6 +138,12 @@ func annotateRuntimeObject(t *testing.T, originalObj, currentObj runtime.Object, 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) @@ -164,6 +183,12 @@ func readAndAnnotateService(t *testing.T, filename string) (string, []byte) { 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) { patch, err := ioutil.ReadAll(req.Body) if err != nil { @@ -655,7 +680,152 @@ func TestApplyNULLPreservation(t *testing.T) { if buf.String() != expected { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected) } + if !verifiedPatch { + t.Fatal("No server-side patch call detected") + } +} +// TestUnstructuredApply checks apply operations on an unstructured object +func TestUnstructuredApply(t *testing.T) { + initTestErrorHandler(t) + name, curr := readAndAnnotateUnstructured(t, filenameWidgetClientside) + path := "/namespaces/test/widgets/" + name + + verifiedPatch := false + + f, tf, _, _ := cmdtesting.NewAPIFactory() + tf.Printer = &testPrinter{} + tf.UnstructuredClient = &fake.RESTClient{ + APIRegistry: api.Registry, + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + switch p, m := req.URL.Path, req.Method; { + case p == path && m == "GET": + body := ioutil.NopCloser(bytes.NewReader(curr)) + return &http.Response{ + StatusCode: 200, + Header: defaultHeader(), + Body: body}, nil + case p == path && m == "PATCH": + contentType := req.Header.Get("Content-Type") + if contentType != "application/merge-patch+json" { + t.Fatalf("Unexpected Content-Type: %s", contentType) + } + validatePatchApplication(t, req) + verifiedPatch = true + + body := ioutil.NopCloser(bytes.NewReader(curr)) + return &http.Response{ + StatusCode: 200, + Header: defaultHeader(), + Body: body}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + + tf.Namespace = "test" + buf := bytes.NewBuffer([]byte{}) + errBuf := bytes.NewBuffer([]byte{}) + + cmd := NewCmdApply(f, buf, errBuf) + cmd.Flags().Set("filename", filenameWidgetClientside) + cmd.Flags().Set("output", "name") + cmd.Run(cmd, []string{}) + + expected := "widget/" + name + "\n" + if buf.String() != expected { + t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected) + } + if !verifiedPatch { + t.Fatal("No server-side patch call detected") + } +} + +// TestUnstructuredIdempotentApply checks repeated apply operation on an unstructured object +func TestUnstructuredIdempotentApply(t *testing.T) { + initTestErrorHandler(t) + + serversideObject := readUnstructuredFromFile(t, filenameWidgetServerside) + serversideData, err := runtime.Encode(testapi.Default.Codec(), serversideObject) + if err != nil { + t.Fatal(err) + } + path := "/namespaces/test/widgets/widget" + + verifiedPatch := false + + f, tf, _, _ := cmdtesting.NewAPIFactory() + tf.Printer = &testPrinter{} + tf.UnstructuredClient = &fake.RESTClient{ + APIRegistry: api.Registry, + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + switch p, m := req.URL.Path, req.Method; { + case p == path && m == "GET": + body := ioutil.NopCloser(bytes.NewReader(serversideData)) + return &http.Response{ + StatusCode: 200, + Header: defaultHeader(), + Body: body}, nil + case p == path && m == "PATCH": + // In idempotent updates, kubectl sends a logically empty + // request body with the PATCH request. + // Should look like this: + // Request Body: {"metadata":{"annotations":{}}} + + patch, err := ioutil.ReadAll(req.Body) + if err != nil { + t.Fatal(err) + } + + contentType := req.Header.Get("Content-Type") + if contentType != "application/merge-patch+json" { + t.Fatalf("Unexpected Content-Type: %s", contentType) + } + + patchMap := map[string]interface{}{} + if err := json.Unmarshal(patch, &patchMap); err != nil { + t.Fatal(err) + } + if len(patchMap) != 1 { + t.Fatalf("Unexpected Patch. Has more than 1 entry. path: %s", patch) + } + + annotationsMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"}) + if len(annotationsMap) != 0 { + t.Fatalf("Unexpected Patch. Found unexpected annotation: %s", patch) + } + + verifiedPatch = true + + body := ioutil.NopCloser(bytes.NewReader(serversideData)) + return &http.Response{ + StatusCode: 200, + Header: defaultHeader(), + Body: body}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + + tf.Namespace = "test" + buf := bytes.NewBuffer([]byte{}) + errBuf := bytes.NewBuffer([]byte{}) + + cmd := NewCmdApply(f, buf, errBuf) + cmd.Flags().Set("filename", filenameWidgetClientside) + cmd.Flags().Set("output", "name") + cmd.Run(cmd, []string{}) + + expected := "widget/widget\n" + if buf.String() != expected { + t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected) + } if !verifiedPatch { t.Fatal("No server-side patch call detected") } diff --git a/pkg/kubectl/cmd/testing/fake.go b/pkg/kubectl/cmd/testing/fake.go index fc2e174af25..62520948057 100644 --- a/pkg/kubectl/cmd/testing/fake.go +++ b/pkg/kubectl/cmd/testing/fake.go @@ -761,5 +761,21 @@ func testDynamicResources() []*discovery.APIGroupResources { }, }, }, + { + Group: metav1.APIGroup{ + Name: "unit-test.test.com", + Versions: []metav1.GroupVersionForDiscovery{ + {GroupVersion: "unit-test.test.com/v1", Version: "v1"}, + }, + PreferredVersion: metav1.GroupVersionForDiscovery{ + GroupVersion: "unit-test.test.com/v1", + Version: "v1"}, + }, + VersionedResources: map[string][]metav1.APIResource{ + "v1": { + {Name: "widgets", Namespaced: true, Kind: "Widget"}, + }, + }, + }, } } diff --git a/test/fixtures/pkg/kubectl/cmd/apply/widget-clientside.yaml b/test/fixtures/pkg/kubectl/cmd/apply/widget-clientside.yaml new file mode 100644 index 00000000000..4688160fe22 --- /dev/null +++ b/test/fixtures/pkg/kubectl/cmd/apply/widget-clientside.yaml @@ -0,0 +1,8 @@ +apiVersion: "unit-test.test.com/v1" +kind: Widget +metadata: + name: "widget" + namespace: "test" + labels: + foo: bar +key: "value" \ No newline at end of file diff --git a/test/fixtures/pkg/kubectl/cmd/apply/widget-serverside.yaml b/test/fixtures/pkg/kubectl/cmd/apply/widget-serverside.yaml new file mode 100644 index 00000000000..9fab75a8421 --- /dev/null +++ b/test/fixtures/pkg/kubectl/cmd/apply/widget-serverside.yaml @@ -0,0 +1,10 @@ +apiVersion: "unit-test.test.com/v1" +kind: Widget +metadata: + name: "widget" + namespace: "test" + annotations: + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"unit-test.test.com/v1\",\"key\":\"value\",\"kind\":\"Widget\",\"metadata\":{\"annotations\":{},\"labels\":{\"foo\":\"bar\"},\"name\":\"widget\",\"namespace\":\"test\"}}\n" + labels: + foo: bar +key: "value" \ No newline at end of file