From c8b1037a58ab6ddc3a8b237938eca2f6336abb73 Mon Sep 17 00:00:00 2001 From: Ben Luddy Date: Mon, 16 Sep 2024 10:10:49 -0400 Subject: [PATCH] Add test for unintended changes to dynamic client response handling. The goal is to increase confidence that a change to the dynamic client does not unintentionally introduce subtle changes to objects returned by dynamic clients in existing programs. --- .../k8s.io/client-go/dynamic/golden_test.go | 157 ++++++++++++++++++ .../TestGoldenResponse/.gitattributes | 2 + .../testdata/TestGoldenResponse/create | 1 + .../dynamic/testdata/TestGoldenResponse/get | 1 + .../dynamic/testdata/TestGoldenResponse/list | 1 + .../TestGoldenResponse/responses/events | 17 ++ .../TestGoldenResponse/responses/list | 10 ++ .../TestGoldenResponse/responses/nonlist | 10 ++ .../testdata/TestGoldenResponse/update | 1 + .../testdata/TestGoldenResponse/updatestatus | 1 + .../dynamic/testdata/TestGoldenResponse/watch | 1 + 11 files changed, 202 insertions(+) create mode 100644 staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/.gitattributes create mode 100644 staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/create create mode 100644 staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/get create mode 100644 staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/list create mode 100644 staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/responses/events create mode 100644 staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/responses/list create mode 100644 staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/responses/nonlist create mode 100644 staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/update create mode 100644 staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/updatestatus create mode 100644 staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/watch diff --git a/staging/src/k8s.io/client-go/dynamic/golden_test.go b/staging/src/k8s.io/client-go/dynamic/golden_test.go index 2ae6dfc6a38..e9f94e9e34d 100644 --- a/staging/src/k8s.io/client-go/dynamic/golden_test.go +++ b/staging/src/k8s.io/client-go/dynamic/golden_test.go @@ -17,7 +17,9 @@ limitations under the License. package dynamic_test import ( + "bufio" "context" + "encoding/json" "fmt" "net" "net/http" @@ -32,6 +34,7 @@ import ( "github.com/google/go-cmp/cmp" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "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/client-go/dynamic" @@ -246,3 +249,157 @@ func TestGoldenRequest(t *testing.T) { }) } } + +type RoundTripperFunc func(*http.Request) (*http.Response, error) + +func (f RoundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { + return f(r) +} + +// TestGoldenResponse tests that the objects returned from dynamic client methods, given a fixed +// HTTP response, are not changed unintentionally by changes to the client. +func TestGoldenResponse(t *testing.T) { + for _, tc := range []struct { + name string + response string // name of fixture containing a serialized HTTP1.1 response + do func(t *testing.T, client dynamic.ResourceInterface) interface{} + }{ + { + name: "create", + response: "nonlist", + do: func(t *testing.T, client dynamic.ResourceInterface) interface{} { + got, err := client.Create(context.Background(), &unstructured.Unstructured{}, metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } + + return got.UnstructuredContent() + }, + }, + { + name: "update", + response: "nonlist", + do: func(t *testing.T, client dynamic.ResourceInterface) interface{} { + got, err := client.Update(context.Background(), &unstructured.Unstructured{Object: map[string]interface{}{"metadata": map[string]interface{}{"name": "name"}}}, metav1.UpdateOptions{}) + if err != nil { + t.Fatal(err) + } + + return got.UnstructuredContent() + }, + }, + { + name: "updatestatus", + response: "nonlist", + do: func(t *testing.T, client dynamic.ResourceInterface) interface{} { + got, err := client.UpdateStatus(context.Background(), &unstructured.Unstructured{Object: map[string]interface{}{"metadata": map[string]interface{}{"name": "name"}}}, metav1.UpdateOptions{}) + + if err != nil { + t.Fatal(err) + } + + return got.UnstructuredContent() + }, + }, + { + name: "get", + response: "nonlist", + do: func(t *testing.T, client dynamic.ResourceInterface) interface{} { + got, err := client.Get(context.Background(), "name", metav1.GetOptions{}) + if err != nil { + t.Fatal(err) + } + + return got.UnstructuredContent() + }, + }, + { + name: "list", + response: "list", + do: func(t *testing.T, client dynamic.ResourceInterface) interface{} { + got, err := client.List(context.Background(), metav1.ListOptions{}) + if err != nil { + t.Fatal(err) + } + + return got.UnstructuredContent() + + }, + }, + { + name: "watch", + response: "events", + do: func(t *testing.T, client dynamic.ResourceInterface) interface{} { + w, err := client.Watch(context.Background(), metav1.ListOptions{}) + if err != nil { + t.Fatal(err) + } + defer w.Stop() + + var got []interface{} + for e := range w.ResultChan() { + u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&e) + if err != nil { + t.Fatalf("failed to convert watch event to unstructured content: %v", err) + } + got = append(got, u) + } + + return got + }, + }, + } { + parentTestName := t.Name() + t.Run(tc.name, func(t *testing.T) { + client, err := dynamic.NewForConfig(&rest.Config{ + + Transport: RoundTripperFunc(func(request *http.Request) (*http.Response, error) { + fd, err := os.Open(filepath.Join("testdata", filepath.FromSlash(parentTestName), "responses", tc.response)) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := fd.Close(); err != nil { + t.Fatal(err) + } + }() + + response, err := http.ReadResponse(bufio.NewReader(fd), request) + if err != nil { + t.Fatal(err) + } + return response, nil + }), + }) + if err != nil { + t.Fatal(err) + } + + got := tc.do(t, client.Resource(schema.GroupVersionResource{})) + + path := filepath.Join("testdata", filepath.FromSlash(t.Name())) + if os.Getenv("UPDATE_DYNAMIC_CLIENT_FIXTURES") == "true" { + fixture, err := json.Marshal(got) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, fixture, os.FileMode(0644)); err != nil { + t.Fatalf("failed to update fixture: %v", err) + } + } + fixture, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + + var want interface{} + if err := json.Unmarshal(fixture, &want); err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("unexpected diff:\n%s", diff) + } + }) + } +} diff --git a/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/.gitattributes b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/.gitattributes new file mode 100644 index 00000000000..464fa4e9dbf --- /dev/null +++ b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/.gitattributes @@ -0,0 +1,2 @@ +# disable end-of-line conversion for fixtures +* -text diff --git a/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/create b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/create new file mode 100644 index 00000000000..f901b605c1a --- /dev/null +++ b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/create @@ -0,0 +1 @@ +{"apiVersion":"v1","kind":"ServiceAccount","metadata":{"creationTimestamp":"2024-09-20T17:16:28Z","name":"foobar","namespace":"default","resourceVersion":"207","uid":"a6bb38b5-67ca-4d0d-bd87-da746a94c8a5"}} \ No newline at end of file diff --git a/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/get b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/get new file mode 100644 index 00000000000..f901b605c1a --- /dev/null +++ b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/get @@ -0,0 +1 @@ +{"apiVersion":"v1","kind":"ServiceAccount","metadata":{"creationTimestamp":"2024-09-20T17:16:28Z","name":"foobar","namespace":"default","resourceVersion":"207","uid":"a6bb38b5-67ca-4d0d-bd87-da746a94c8a5"}} \ No newline at end of file diff --git a/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/list b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/list new file mode 100644 index 00000000000..11e0cb8e8f2 --- /dev/null +++ b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/list @@ -0,0 +1 @@ +{"apiVersion":"v1","items":[{"apiVersion":"v1","kind":"ServiceAccount","metadata":{"creationTimestamp":"2024-09-20T17:16:28Z","name":"foobar","namespace":"default","resourceVersion":"207","uid":"a6bb38b5-67ca-4d0d-bd87-da746a94c8a5"}}],"kind":"ServiceAccountList","metadata":{"resourceVersion":"222"}} \ No newline at end of file diff --git a/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/responses/events b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/responses/events new file mode 100644 index 00000000000..29078229f88 --- /dev/null +++ b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/responses/events @@ -0,0 +1,17 @@ +HTTP/1.1 200 OK +Audit-Id: 1ef157ba-f535-4398-8813-80d86604fa8b +Cache-Control: no-cache, private +Content-Type: application/json +X-Kubernetes-Pf-Flowschema-Uid: 6027abd0-aa72-4d3c-a2cb-42b534068f6d +X-Kubernetes-Pf-Prioritylevel-Uid: f72ba9d7-6df9-4dd9-8720-be5485197492 +Date: Wed, 18 Sep 2024 15:17:06 GMT +Transfer-Encoding: chunked + +e9 +{"type":"ADDED","object":{"kind":"ServiceAccount","apiVersion":"v1","metadata":{"name":"foobar","namespace":"default","uid":"a1453396-7a5c-405a-93f9-ff44af8e7689","resourceVersion":"217","creationTimestamp":"2024-09-18T14:06:40Z"}}} + +ba +{"type":"BOOKMARK","object":{"kind":"ServiceAccount","apiVersion":"v1","metadata":{"resourceVersion":"964","creationTimestamp":null,"annotations":{"k8s.io/initial-events-end":"true"}}}} + +0 + diff --git a/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/responses/list b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/responses/list new file mode 100644 index 00000000000..eda1ff0586e --- /dev/null +++ b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/responses/list @@ -0,0 +1,10 @@ +HTTP/1.1 200 OK +Audit-Id: 683f3c08-8e36-406a-ba2f-e327b7f107f1 +Cache-Control: no-cache, private +Content-Type: application/json +X-Kubernetes-Pf-Flowschema-Uid: f04daf25-4159-46d9-8547-499158737500 +X-Kubernetes-Pf-Prioritylevel-Uid: a8711f26-ec13-47dd-ae6c-27c345737e12 +Date: Fri, 20 Sep 2024 17:17:40 GMT +Content-Length: 260 + +{"kind":"ServiceAccountList","apiVersion":"v1","metadata":{"resourceVersion":"222"},"items":[{"metadata":{"name":"foobar","namespace":"default","uid":"a6bb38b5-67ca-4d0d-bd87-da746a94c8a5","resourceVersion":"207","creationTimestamp":"2024-09-20T17:16:28Z"}}]} diff --git a/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/responses/nonlist b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/responses/nonlist new file mode 100644 index 00000000000..fac73654402 --- /dev/null +++ b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/responses/nonlist @@ -0,0 +1,10 @@ +HTTP/1.1 200 OK +Audit-Id: a050a5c6-5cc9-40b9-bf0e-7d178e72794a +Cache-Control: no-cache, private +Content-Type: application/json +X-Kubernetes-Pf-Flowschema-Uid: f04daf25-4159-46d9-8547-499158737500 +X-Kubernetes-Pf-Prioritylevel-Uid: a8711f26-ec13-47dd-ae6c-27c345737e12 +Date: Fri, 20 Sep 2024 17:17:33 GMT +Content-Length: 207 + +{"kind":"ServiceAccount","apiVersion":"v1","metadata":{"name":"foobar","namespace":"default","uid":"a6bb38b5-67ca-4d0d-bd87-da746a94c8a5","resourceVersion":"207","creationTimestamp":"2024-09-20T17:16:28Z"}} diff --git a/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/update b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/update new file mode 100644 index 00000000000..f901b605c1a --- /dev/null +++ b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/update @@ -0,0 +1 @@ +{"apiVersion":"v1","kind":"ServiceAccount","metadata":{"creationTimestamp":"2024-09-20T17:16:28Z","name":"foobar","namespace":"default","resourceVersion":"207","uid":"a6bb38b5-67ca-4d0d-bd87-da746a94c8a5"}} \ No newline at end of file diff --git a/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/updatestatus b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/updatestatus new file mode 100644 index 00000000000..f901b605c1a --- /dev/null +++ b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/updatestatus @@ -0,0 +1 @@ +{"apiVersion":"v1","kind":"ServiceAccount","metadata":{"creationTimestamp":"2024-09-20T17:16:28Z","name":"foobar","namespace":"default","resourceVersion":"207","uid":"a6bb38b5-67ca-4d0d-bd87-da746a94c8a5"}} \ No newline at end of file diff --git a/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/watch b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/watch new file mode 100644 index 00000000000..66ce48c6379 --- /dev/null +++ b/staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenResponse/watch @@ -0,0 +1 @@ +[{"object":{"apiVersion":"v1","kind":"ServiceAccount","metadata":{"creationTimestamp":"2024-09-18T14:06:40Z","name":"foobar","namespace":"default","resourceVersion":"217","uid":"a1453396-7a5c-405a-93f9-ff44af8e7689"}},"type":"ADDED"},{"object":{"apiVersion":"v1","kind":"ServiceAccount","metadata":{"annotations":{"k8s.io/initial-events-end":"true"},"creationTimestamp":null,"resourceVersion":"964"}},"type":"BOOKMARK"}] \ No newline at end of file