mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-11-01 02:38:12 +00:00 
			
		
		
		
	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
This commit is contained in:
		| @@ -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") | ||||
| 	} | ||||
|   | ||||
| @@ -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"}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										8
									
								
								test/fixtures/pkg/kubectl/cmd/apply/widget-clientside.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								test/fixtures/pkg/kubectl/cmd/apply/widget-clientside.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| apiVersion: "unit-test.test.com/v1" | ||||
| kind: Widget | ||||
| metadata: | ||||
|   name: "widget" | ||||
|   namespace: "test" | ||||
|   labels: | ||||
|     foo: bar | ||||
| key: "value" | ||||
							
								
								
									
										10
									
								
								test/fixtures/pkg/kubectl/cmd/apply/widget-serverside.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								test/fixtures/pkg/kubectl/cmd/apply/widget-serverside.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -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" | ||||
		Reference in New Issue
	
	Block a user
	 Hakan Baba
					Hakan Baba