From b17c363481226d5576e8c6cfb9e0c318b9222ec3 Mon Sep 17 00:00:00 2001 From: Ben Luddy Date: Tue, 16 Apr 2024 12:48:12 -0400 Subject: [PATCH 1/2] Add test to detect unintentional changes in dynamic client requests. Kubernetes-commit: a803c8034d60a81b0da71ea8631e27888a607476 --- dynamic/golden_test.go | 232 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 dynamic/golden_test.go diff --git a/dynamic/golden_test.go b/dynamic/golden_test.go new file mode 100644 index 00000000..20bede85 --- /dev/null +++ b/dynamic/golden_test.go @@ -0,0 +1,232 @@ +package dynamic_test + +import ( + "context" + "fmt" + "net" + "net/http" + "net/http/httptest" + "net/http/httputil" + "net/url" + "os" + "path/filepath" + "testing" + "time" + + "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/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" +) + +func TestGoldenRequest(t *testing.T) { + for _, tc := range []struct { + name string + do func(context.Context, dynamic.Interface) error + }{ + { + name: "create", + do: func(ctx context.Context, client dynamic.Interface) error { + _, err := client.Resource(schema.GroupVersionResource{Group: "flops", Version: "v1alpha1", Resource: "flips"}).Namespace("mops").Create( + ctx, + &unstructured.Unstructured{Object: map[string]interface{}{ + "metadata": map[string]interface{}{"name": "mips"}, + }}, + metav1.CreateOptions{FieldValidation: "warn"}, + "fin", + ) + return err + }, + }, + { + name: "update", + do: func(ctx context.Context, client dynamic.Interface) error { + _, err := client.Resource(schema.GroupVersionResource{Group: "flops", Version: "v1alpha1", Resource: "flips"}).Namespace("mops").Update( + ctx, + &unstructured.Unstructured{Object: map[string]interface{}{ + "metadata": map[string]interface{}{"name": "mips"}, + }}, + metav1.UpdateOptions{FieldValidation: "warn"}, + "fin", + ) + return err + }, + }, + { + name: "updatestatus", + do: func(ctx context.Context, client dynamic.Interface) error { + _, err := client.Resource(schema.GroupVersionResource{Group: "flops", Version: "v1alpha1", Resource: "flips"}).Namespace("mops").UpdateStatus( + ctx, + &unstructured.Unstructured{Object: map[string]interface{}{ + "metadata": map[string]interface{}{"name": "mips"}, + }}, + metav1.UpdateOptions{FieldValidation: "warn"}, + ) + return err + }, + }, + { + name: "delete", + do: func(ctx context.Context, client dynamic.Interface) error { + return client.Resource(schema.GroupVersionResource{Group: "flops", Version: "v1alpha1", Resource: "flips"}).Namespace("mops").Delete( + ctx, + "mips", + metav1.DeleteOptions{DryRun: []string{metav1.DryRunAll}}, + "fin", + ) + }, + }, + { + name: "deletecollection", + do: func(ctx context.Context, client dynamic.Interface) error { + return client.Resource(schema.GroupVersionResource{Group: "flops", Version: "v1alpha1", Resource: "flips"}).Namespace("mops").DeleteCollection( + ctx, + metav1.DeleteOptions{DryRun: []string{metav1.DryRunAll}}, + metav1.ListOptions{ResourceVersion: "42"}, + ) + }, + }, + { + name: "get", + do: func(ctx context.Context, client dynamic.Interface) error { + _, err := client.Resource(schema.GroupVersionResource{Group: "flops", Version: "v1alpha1", Resource: "flips"}).Namespace("mops").Get( + ctx, + "mips", + metav1.GetOptions{ResourceVersion: "42"}, + "fin", + ) + return err + }, + }, + { + name: "list", + do: func(ctx context.Context, client dynamic.Interface) error { + _, err := client.Resource(schema.GroupVersionResource{Group: "flops", Version: "v1alpha1", Resource: "flips"}).Namespace("mops").List( + ctx, + metav1.ListOptions{ResourceVersion: "42"}, + ) + return err + }, + }, + { + name: "watch", + do: func(ctx context.Context, client dynamic.Interface) error { + _, err := client.Resource(schema.GroupVersionResource{Group: "flops", Version: "v1alpha1", Resource: "flips"}).Namespace("mops").Watch( + ctx, + metav1.ListOptions{ResourceVersion: "42"}, + ) + return err + }, + }, + { + name: "patch", + do: func(ctx context.Context, client dynamic.Interface) error { + _, err := client.Resource(schema.GroupVersionResource{Group: "flops", Version: "v1alpha1", Resource: "flips"}).Namespace("mops").Patch( + ctx, + "mips", + types.StrategicMergePatchType, + []byte("{\"foo\":\"bar\"}\n"), + metav1.PatchOptions{FieldManager: "baz"}, + "fin", + ) + return err + }, + }, + { + name: "apply", + do: func(ctx context.Context, client dynamic.Interface) error { + _, err := client.Resource(schema.GroupVersionResource{Group: "flops", Version: "v1alpha1", Resource: "flips"}).Namespace("mops").Apply( + ctx, + "mips", + &unstructured.Unstructured{Object: map[string]interface{}{ + "metadata": map[string]interface{}{"name": "mips"}, + }}, + metav1.ApplyOptions{Force: true}, + "fin", + ) + return err + }, + }, + { + name: "applystatus", + do: func(ctx context.Context, client dynamic.Interface) error { + _, err := client.Resource(schema.GroupVersionResource{Group: "flops", Version: "v1alpha1", Resource: "flips"}).Namespace("mops").ApplyStatus( + ctx, + "mips", + &unstructured.Unstructured{Object: map[string]interface{}{ + "metadata": map[string]interface{}{"name": "mips"}, + }}, + metav1.ApplyOptions{Force: true}, + ) + return err + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + handled := make(chan struct{}) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer close(handled) + + got, err := httputil.DumpRequest(r, true) + if err != nil { + t.Fatal(err) + } + + path := filepath.Join("testdata", filepath.FromSlash(t.Name())) + + if os.Getenv("UPDATE_DYNAMIC_CLIENT_FIXTURES") == "true" { + err := os.WriteFile(path, got, os.FileMode(0755)) + if err != nil { + t.Fatalf("failed to update fixture: %v", err) + } + } + + want, err := os.ReadFile(path) + if err != nil { + t.Fatalf("failed to load fixture: %v", err) + } + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("unexpected difference from expected bytes:\n%s", diff) + } + })) + defer srv.Close() + + client, err := dynamic.NewForConfig(&rest.Config{ + Host: "example.com", + UserAgent: "TestGoldenRequest", + Transport: &http.Transport{ + // The client will send a static Host header while always + // connecting to the test server. + DialContext: func(ctx context.Context, network string, addr string) (net.Conn, error) { + u, err := url.Parse(srv.URL) + if err != nil { + return nil, fmt.Errorf("failed to parse test server url: %w", err) + } + return (&net.Dialer{}).DialContext(ctx, "tcp", u.Host) + }, + }, + }) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + if err := tc.do(ctx, client); err != nil { + // This test detects server-perceptible changes to the request. As + // long as the server receives the expected request, a non-nil error + // returned from a client method is not a failure. + t.Logf("client returned non-nil error: %v", err) + } + + select { + case <-handled: + default: + t.Fatal("no request received") + } + }) + } +} From 4cd6b756be04aa05f4f881118a63ac43ad8c63ce Mon Sep 17 00:00:00 2001 From: Ben Luddy Date: Tue, 16 Apr 2024 12:49:15 -0400 Subject: [PATCH 2/2] Generate HTTP request fixtures for dynamic client tests. Kubernetes-commit: e335b8e81b467c70f0f8a485f69e5664adbbc687 --- dynamic/golden_test.go | 16 ++++++++++++++++ dynamic/testdata/TestGoldenRequest/apply | 9 +++++++++ dynamic/testdata/TestGoldenRequest/applystatus | 9 +++++++++ dynamic/testdata/TestGoldenRequest/create | 9 +++++++++ dynamic/testdata/TestGoldenRequest/delete | 9 +++++++++ .../testdata/TestGoldenRequest/deletecollection | 9 +++++++++ dynamic/testdata/TestGoldenRequest/get | 6 ++++++ dynamic/testdata/TestGoldenRequest/list | 6 ++++++ dynamic/testdata/TestGoldenRequest/patch | 9 +++++++++ dynamic/testdata/TestGoldenRequest/update | 9 +++++++++ dynamic/testdata/TestGoldenRequest/updatestatus | 9 +++++++++ dynamic/testdata/TestGoldenRequest/watch | 6 ++++++ 12 files changed, 106 insertions(+) create mode 100755 dynamic/testdata/TestGoldenRequest/apply create mode 100755 dynamic/testdata/TestGoldenRequest/applystatus create mode 100755 dynamic/testdata/TestGoldenRequest/create create mode 100755 dynamic/testdata/TestGoldenRequest/delete create mode 100755 dynamic/testdata/TestGoldenRequest/deletecollection create mode 100755 dynamic/testdata/TestGoldenRequest/get create mode 100755 dynamic/testdata/TestGoldenRequest/list create mode 100755 dynamic/testdata/TestGoldenRequest/patch create mode 100755 dynamic/testdata/TestGoldenRequest/update create mode 100755 dynamic/testdata/TestGoldenRequest/updatestatus create mode 100755 dynamic/testdata/TestGoldenRequest/watch diff --git a/dynamic/golden_test.go b/dynamic/golden_test.go index 20bede85..2ae6dfc6 100644 --- a/dynamic/golden_test.go +++ b/dynamic/golden_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2024 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 dynamic_test import ( diff --git a/dynamic/testdata/TestGoldenRequest/apply b/dynamic/testdata/TestGoldenRequest/apply new file mode 100755 index 00000000..9f6385a7 --- /dev/null +++ b/dynamic/testdata/TestGoldenRequest/apply @@ -0,0 +1,9 @@ +PATCH /apis/flops/v1alpha1/namespaces/mops/flips/mips/fin?force=true HTTP/1.1 +Host: example.com +Accept: application/json +Accept-Encoding: gzip +Content-Length: 29 +Content-Type: application/apply-patch+yaml +User-Agent: TestGoldenRequest + +{"metadata":{"name":"mips"}} diff --git a/dynamic/testdata/TestGoldenRequest/applystatus b/dynamic/testdata/TestGoldenRequest/applystatus new file mode 100755 index 00000000..ce69f166 --- /dev/null +++ b/dynamic/testdata/TestGoldenRequest/applystatus @@ -0,0 +1,9 @@ +PATCH /apis/flops/v1alpha1/namespaces/mops/flips/mips/status?force=true HTTP/1.1 +Host: example.com +Accept: application/json +Accept-Encoding: gzip +Content-Length: 29 +Content-Type: application/apply-patch+yaml +User-Agent: TestGoldenRequest + +{"metadata":{"name":"mips"}} diff --git a/dynamic/testdata/TestGoldenRequest/create b/dynamic/testdata/TestGoldenRequest/create new file mode 100755 index 00000000..5a742e1c --- /dev/null +++ b/dynamic/testdata/TestGoldenRequest/create @@ -0,0 +1,9 @@ +POST /apis/flops/v1alpha1/namespaces/mops/flips/mips/fin?fieldValidation=warn HTTP/1.1 +Host: example.com +Accept: application/json +Accept-Encoding: gzip +Content-Length: 29 +Content-Type: application/json +User-Agent: TestGoldenRequest + +{"metadata":{"name":"mips"}} diff --git a/dynamic/testdata/TestGoldenRequest/delete b/dynamic/testdata/TestGoldenRequest/delete new file mode 100755 index 00000000..b1622968 --- /dev/null +++ b/dynamic/testdata/TestGoldenRequest/delete @@ -0,0 +1,9 @@ +DELETE /apis/flops/v1alpha1/namespaces/mops/flips/mips/fin HTTP/1.1 +Host: example.com +Accept: application/json +Accept-Encoding: gzip +Content-Length: 60 +Content-Type: application/json +User-Agent: TestGoldenRequest + +{"kind":"DeleteOptions","apiVersion":"v1","dryRun":["All"]} diff --git a/dynamic/testdata/TestGoldenRequest/deletecollection b/dynamic/testdata/TestGoldenRequest/deletecollection new file mode 100755 index 00000000..8e9a2437 --- /dev/null +++ b/dynamic/testdata/TestGoldenRequest/deletecollection @@ -0,0 +1,9 @@ +DELETE /apis/flops/v1alpha1/namespaces/mops/flips?resourceVersion=42 HTTP/1.1 +Host: example.com +Accept: application/json +Accept-Encoding: gzip +Content-Length: 60 +Content-Type: application/json +User-Agent: TestGoldenRequest + +{"kind":"DeleteOptions","apiVersion":"v1","dryRun":["All"]} diff --git a/dynamic/testdata/TestGoldenRequest/get b/dynamic/testdata/TestGoldenRequest/get new file mode 100755 index 00000000..d7e78a8a --- /dev/null +++ b/dynamic/testdata/TestGoldenRequest/get @@ -0,0 +1,6 @@ +GET /apis/flops/v1alpha1/namespaces/mops/flips/mips/fin?resourceVersion=42 HTTP/1.1 +Host: example.com +Accept: application/json +Accept-Encoding: gzip +User-Agent: TestGoldenRequest + diff --git a/dynamic/testdata/TestGoldenRequest/list b/dynamic/testdata/TestGoldenRequest/list new file mode 100755 index 00000000..68e673f1 --- /dev/null +++ b/dynamic/testdata/TestGoldenRequest/list @@ -0,0 +1,6 @@ +GET /apis/flops/v1alpha1/namespaces/mops/flips?resourceVersion=42 HTTP/1.1 +Host: example.com +Accept: application/json +Accept-Encoding: gzip +User-Agent: TestGoldenRequest + diff --git a/dynamic/testdata/TestGoldenRequest/patch b/dynamic/testdata/TestGoldenRequest/patch new file mode 100755 index 00000000..4b607065 --- /dev/null +++ b/dynamic/testdata/TestGoldenRequest/patch @@ -0,0 +1,9 @@ +PATCH /apis/flops/v1alpha1/namespaces/mops/flips/mips/fin?fieldManager=baz HTTP/1.1 +Host: example.com +Accept: application/json +Accept-Encoding: gzip +Content-Length: 14 +Content-Type: application/strategic-merge-patch+json +User-Agent: TestGoldenRequest + +{"foo":"bar"} diff --git a/dynamic/testdata/TestGoldenRequest/update b/dynamic/testdata/TestGoldenRequest/update new file mode 100755 index 00000000..5d8e7021 --- /dev/null +++ b/dynamic/testdata/TestGoldenRequest/update @@ -0,0 +1,9 @@ +PUT /apis/flops/v1alpha1/namespaces/mops/flips/mips/fin?fieldValidation=warn HTTP/1.1 +Host: example.com +Accept: application/json +Accept-Encoding: gzip +Content-Length: 29 +Content-Type: application/json +User-Agent: TestGoldenRequest + +{"metadata":{"name":"mips"}} diff --git a/dynamic/testdata/TestGoldenRequest/updatestatus b/dynamic/testdata/TestGoldenRequest/updatestatus new file mode 100755 index 00000000..9c2724e3 --- /dev/null +++ b/dynamic/testdata/TestGoldenRequest/updatestatus @@ -0,0 +1,9 @@ +PUT /apis/flops/v1alpha1/namespaces/mops/flips/mips/status?fieldValidation=warn HTTP/1.1 +Host: example.com +Accept: application/json +Accept-Encoding: gzip +Content-Length: 29 +Content-Type: application/json +User-Agent: TestGoldenRequest + +{"metadata":{"name":"mips"}} diff --git a/dynamic/testdata/TestGoldenRequest/watch b/dynamic/testdata/TestGoldenRequest/watch new file mode 100755 index 00000000..8436fb77 --- /dev/null +++ b/dynamic/testdata/TestGoldenRequest/watch @@ -0,0 +1,6 @@ +GET /apis/flops/v1alpha1/namespaces/mops/flips?resourceVersion=42&watch=true HTTP/1.1 +Host: example.com +Accept: application/json +Accept-Encoding: gzip +User-Agent: TestGoldenRequest +