mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-21 19:01:49 +00:00
Merge pull request #41151 from ahakanbaba/tpr-unit-tests
Automatic merge from submit-queue (batch tested with PRs 41937, 41151, 42092, 40269, 42135) Add a unit test for idempotent applys to the TPR entries. The test 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. The apply operation expects a last-modified-configuration annotation. That is written verbatim in the test/fixture file. References #40841 **What this PR does / why we need it**: Adds one unit test for TPR's using applies. **Which issue this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: fixes # References: https://github.com/kubernetes/features/issues/95 https://github.com/kubernetes/kubernetes/issues/40841#issue-204769102 **Special notes for your reviewer**: I am not super proud of the tpr-entry name. But I feel like we need to call the two objects differently. The one which has Kind:ThirdPartyResource and the one has Kind:Foo. Is the name "ThirdPartyResource" used interchangeably for both ? I used tpr-entry for the Kind:Foo object. Also I !assume! this is testing an idempotent apply because the last-applied-configuration annotation is the same as the object itself. This is the state I see in the logs of kubectl if I do a proper idempotent apply of a third party resource entry. I guess I will know more once I start playing around with apply command that change TPR objects. **Release note**: ```release-note ```
This commit is contained in:
commit
8ceb0c4025
@ -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"
|
Loading…
Reference in New Issue
Block a user