Merge pull request #124340 from benluddy/dynamic-client-golden-request-test
Add test to detect unintentional changes in dynamic client requests.
This commit is contained in:
		
							
								
								
									
										248
									
								
								staging/src/k8s.io/client-go/dynamic/golden_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										248
									
								
								staging/src/k8s.io/client-go/dynamic/golden_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,248 @@ | ||||
| /* | ||||
| 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 ( | ||||
| 	"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") | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										2
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| # disable end-of-line conversion for fixtures | ||||
| * -text | ||||
							
								
								
									
										9
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/apply
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										9
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/apply
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @@ -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"}} | ||||
							
								
								
									
										9
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/applystatus
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										9
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/applystatus
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @@ -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"}} | ||||
							
								
								
									
										9
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/create
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										9
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/create
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @@ -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"}} | ||||
							
								
								
									
										9
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/delete
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										9
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/delete
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @@ -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"]} | ||||
							
								
								
									
										9
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/deletecollection
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										9
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/deletecollection
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @@ -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"]} | ||||
							
								
								
									
										6
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/get
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										6
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/get
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @@ -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 | ||||
|  | ||||
							
								
								
									
										6
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/list
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										6
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/list
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @@ -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 | ||||
|  | ||||
							
								
								
									
										9
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/patch
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										9
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/patch
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @@ -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"} | ||||
							
								
								
									
										9
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/update
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										9
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/update
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @@ -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"}} | ||||
							
								
								
									
										9
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/updatestatus
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										9
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/updatestatus
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @@ -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"}} | ||||
							
								
								
									
										6
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/watch
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										6
									
								
								staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/watch
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @@ -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 | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Kubernetes Prow Robot
					Kubernetes Prow Robot