k8s.io/kubernetes@v1.29.3/test/integration/apiserver/admissionwebhook/admission_test.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package admissionwebhook
    18  
    19  import (
    20  	"context"
    21  	"crypto/tls"
    22  	"crypto/x509"
    23  	"encoding/json"
    24  	"fmt"
    25  	"io"
    26  	"net/http"
    27  	"net/http/httptest"
    28  	"path"
    29  	"sort"
    30  	"strings"
    31  	"sync"
    32  	"testing"
    33  	"time"
    34  
    35  	clientv3 "go.etcd.io/etcd/client/v3"
    36  	admissionreviewv1 "k8s.io/api/admission/v1"
    37  	"k8s.io/api/admission/v1beta1"
    38  	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
    39  	admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
    40  	appsv1beta1 "k8s.io/api/apps/v1beta1"
    41  	authenticationv1 "k8s.io/api/authentication/v1"
    42  	corev1 "k8s.io/api/core/v1"
    43  	extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
    44  	policyv1 "k8s.io/api/policy/v1"
    45  	apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    46  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    47  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    48  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    49  	"k8s.io/apimachinery/pkg/runtime"
    50  	"k8s.io/apimachinery/pkg/runtime/schema"
    51  	"k8s.io/apimachinery/pkg/types"
    52  	"k8s.io/apimachinery/pkg/util/sets"
    53  	"k8s.io/apimachinery/pkg/util/wait"
    54  	genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
    55  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    56  	"k8s.io/client-go/dynamic"
    57  	clientset "k8s.io/client-go/kubernetes"
    58  	"k8s.io/client-go/rest"
    59  	"k8s.io/client-go/util/retry"
    60  	featuregatetesting "k8s.io/component-base/featuregate/testing"
    61  	kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
    62  	apisv1beta1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1beta1"
    63  	"k8s.io/kubernetes/pkg/features"
    64  	"k8s.io/kubernetes/test/integration/etcd"
    65  	"k8s.io/kubernetes/test/integration/framework"
    66  )
    67  
    68  const (
    69  	testNamespace      = "webhook-integration"
    70  	testClientUsername = "webhook-integration-client"
    71  
    72  	mutation   = "mutation"
    73  	validation = "validation"
    74  )
    75  
    76  var (
    77  	noSideEffects = admissionregistrationv1.SideEffectClassNone
    78  )
    79  
    80  type testContext struct {
    81  	t *testing.T
    82  
    83  	admissionHolder *holder
    84  
    85  	client    dynamic.Interface
    86  	clientset clientset.Interface
    87  	verb      string
    88  	gvr       schema.GroupVersionResource
    89  	resource  metav1.APIResource
    90  	resources map[schema.GroupVersionResource]metav1.APIResource
    91  }
    92  
    93  type testFunc func(*testContext)
    94  
    95  var (
    96  	// defaultResourceFuncs holds the default test functions.
    97  	// may be overridden for specific resources by customTestFuncs.
    98  	defaultResourceFuncs = map[string]testFunc{
    99  		"create":           testResourceCreate,
   100  		"update":           testResourceUpdate,
   101  		"patch":            testResourcePatch,
   102  		"delete":           testResourceDelete,
   103  		"deletecollection": testResourceDeletecollection,
   104  	}
   105  
   106  	// defaultSubresourceFuncs holds default subresource test functions.
   107  	// may be overridden for specific resources by customTestFuncs.
   108  	defaultSubresourceFuncs = map[string]testFunc{
   109  		"update": testSubresourceUpdate,
   110  		"patch":  testSubresourcePatch,
   111  	}
   112  
   113  	// customTestFuncs holds custom test functions by resource and verb.
   114  	customTestFuncs = map[schema.GroupVersionResource]map[string]testFunc{
   115  		gvr("", "v1", "namespaces"): {"delete": testNamespaceDelete},
   116  
   117  		gvr("apps", "v1beta1", "deployments/rollback"):       {"create": testDeploymentRollback},
   118  		gvr("extensions", "v1beta1", "deployments/rollback"): {"create": testDeploymentRollback},
   119  
   120  		gvr("", "v1", "pods/attach"):      {"create": testPodConnectSubresource},
   121  		gvr("", "v1", "pods/exec"):        {"create": testPodConnectSubresource},
   122  		gvr("", "v1", "pods/portforward"): {"create": testPodConnectSubresource},
   123  
   124  		gvr("", "v1", "bindings"):      {"create": testPodBindingEviction},
   125  		gvr("", "v1", "pods/binding"):  {"create": testPodBindingEviction},
   126  		gvr("", "v1", "pods/eviction"): {"create": testPodBindingEviction},
   127  
   128  		gvr("", "v1", "nodes/proxy"):    {"*": testSubresourceProxy},
   129  		gvr("", "v1", "pods/proxy"):     {"*": testSubresourceProxy},
   130  		gvr("", "v1", "services/proxy"): {"*": testSubresourceProxy},
   131  
   132  		gvr("", "v1", "serviceaccounts/token"): {"create": testTokenCreate},
   133  
   134  		gvr("random.numbers.com", "v1", "integers"): {"create": testPruningRandomNumbers},
   135  		gvr("custom.fancy.com", "v2", "pants"):      {"create": testNoPruningCustomFancy},
   136  	}
   137  
   138  	// admissionExemptResources lists objects which are exempt from admission validation/mutation,
   139  	// only resources exempted from admission processing by API server should be listed here.
   140  	admissionExemptResources = map[schema.GroupVersionResource]bool{
   141  		gvr("admissionregistration.k8s.io", "v1beta1", "mutatingwebhookconfigurations"):       true,
   142  		gvr("admissionregistration.k8s.io", "v1beta1", "validatingwebhookconfigurations"):     true,
   143  		gvr("admissionregistration.k8s.io", "v1", "mutatingwebhookconfigurations"):            true,
   144  		gvr("admissionregistration.k8s.io", "v1", "validatingwebhookconfigurations"):          true,
   145  		gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"):        true,
   146  		gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies/status"): true,
   147  		gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicybindings"):  true,
   148  		gvr("admissionregistration.k8s.io", "v1beta1", "validatingadmissionpolicies"):         true,
   149  		gvr("admissionregistration.k8s.io", "v1beta1", "validatingadmissionpolicies/status"):  true,
   150  		gvr("admissionregistration.k8s.io", "v1beta1", "validatingadmissionpolicybindings"):   true,
   151  	}
   152  
   153  	parentResources = map[schema.GroupVersionResource]schema.GroupVersionResource{
   154  		gvr("extensions", "v1beta1", "replicationcontrollers/scale"): gvr("", "v1", "replicationcontrollers"),
   155  	}
   156  
   157  	// stubDataOverrides holds either non persistent resources' definitions or resources where default stub needs to be overridden.
   158  	stubDataOverrides = map[schema.GroupVersionResource]string{
   159  		// Non persistent Reviews resource
   160  		gvr("authentication.k8s.io", "v1", "tokenreviews"):                  `{"metadata": {"name": "tokenreview"}, "spec": {"token": "token", "audience": ["audience1","audience2"]}}`,
   161  		gvr("authentication.k8s.io", "v1beta1", "tokenreviews"):             `{"metadata": {"name": "tokenreview"}, "spec": {"token": "token", "audience": ["audience1","audience2"]}}`,
   162  		gvr("authentication.k8s.io", "v1alpha1", "selfsubjectreviews"):      `{"metadata": {"name": "SelfSubjectReview"},"status":{"userInfo":{}}}`,
   163  		gvr("authentication.k8s.io", "v1beta1", "selfsubjectreviews"):       `{"metadata": {"name": "SelfSubjectReview"},"status":{"userInfo":{}}}`,
   164  		gvr("authentication.k8s.io", "v1", "selfsubjectreviews"):            `{"metadata": {"name": "SelfSubjectReview"},"status":{"userInfo":{}}}`,
   165  		gvr("authorization.k8s.io", "v1", "localsubjectaccessreviews"):      `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"uid": "token", "user": "user1","groups": ["group1","group2"],"resourceAttributes": {"name":"name1","namespace":"` + testNamespace + `"}}}`,
   166  		gvr("authorization.k8s.io", "v1", "subjectaccessreviews"):           `{"metadata": {"name": "", "namespace":""}, "spec": {"user":"user1","resourceAttributes": {"name":"name1", "namespace":"` + testNamespace + `"}}}`,
   167  		gvr("authorization.k8s.io", "v1", "selfsubjectaccessreviews"):       `{"metadata": {"name": "", "namespace":""}, "spec": {"resourceAttributes": {"name":"name1", "namespace":""}}}`,
   168  		gvr("authorization.k8s.io", "v1", "selfsubjectrulesreviews"):        `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"namespace":"` + testNamespace + `"}}`,
   169  		gvr("authorization.k8s.io", "v1beta1", "localsubjectaccessreviews"): `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"uid": "token", "user": "user1","groups": ["group1","group2"],"resourceAttributes": {"name":"name1","namespace":"` + testNamespace + `"}}}`,
   170  		gvr("authorization.k8s.io", "v1beta1", "subjectaccessreviews"):      `{"metadata": {"name": "", "namespace":""}, "spec": {"user":"user1","resourceAttributes": {"name":"name1", "namespace":"` + testNamespace + `"}}}`,
   171  		gvr("authorization.k8s.io", "v1beta1", "selfsubjectaccessreviews"):  `{"metadata": {"name": "", "namespace":""}, "spec": {"resourceAttributes": {"name":"name1", "namespace":""}}}`,
   172  		gvr("authorization.k8s.io", "v1beta1", "selfsubjectrulesreviews"):   `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"namespace":"` + testNamespace + `"}}`,
   173  
   174  		// Other Non persistent resources
   175  	}
   176  )
   177  
   178  type webhookOptions struct {
   179  	version string
   180  
   181  	// phase indicates whether this is a mutating or validating webhook
   182  	phase string
   183  	// converted indicates if this webhook makes use of matchPolicy:equivalent and expects conversion.
   184  	// if true, recordGVR and expectGVK are mapped through gvrToConvertedGVR/gvrToConvertedGVK.
   185  	// if false, recordGVR and expectGVK are compared directly to the admission review.
   186  	converted bool
   187  }
   188  
   189  type holder struct {
   190  	lock sync.RWMutex
   191  
   192  	t *testing.T
   193  
   194  	warningHandler *warningHandler
   195  
   196  	recordGVR       metav1.GroupVersionResource
   197  	recordOperation string
   198  	recordNamespace string
   199  	recordName      string
   200  
   201  	expectGVK        schema.GroupVersionKind
   202  	expectObject     bool
   203  	expectOldObject  bool
   204  	expectOptionsGVK schema.GroupVersionKind
   205  	expectOptions    bool
   206  
   207  	// gvrToConvertedGVR maps the GVR submitted to the API server to the expected GVR when converted to the webhook-recognized resource.
   208  	// When a converted request is recorded, gvrToConvertedGVR[recordGVR] is compared to the GVR seen by the webhook.
   209  	gvrToConvertedGVR map[metav1.GroupVersionResource]metav1.GroupVersionResource
   210  	// gvrToConvertedGVR maps the GVR submitted to the API server to the expected GVK when converted to the webhook-recognized resource.
   211  	// When a converted request is recorded, gvrToConvertedGVR[expectGVK] is compared to the GVK seen by the webhook.
   212  	gvrToConvertedGVK map[metav1.GroupVersionResource]schema.GroupVersionKind
   213  
   214  	recorded map[webhookOptions]*admissionRequest
   215  }
   216  
   217  func (h *holder) reset(t *testing.T) {
   218  	h.lock.Lock()
   219  	defer h.lock.Unlock()
   220  	h.t = t
   221  	h.recordGVR = metav1.GroupVersionResource{}
   222  	h.expectGVK = schema.GroupVersionKind{}
   223  	h.recordOperation = ""
   224  	h.recordName = ""
   225  	h.recordNamespace = ""
   226  	h.expectObject = false
   227  	h.expectOldObject = false
   228  	h.expectOptionsGVK = schema.GroupVersionKind{}
   229  	h.expectOptions = false
   230  	h.warningHandler.reset()
   231  
   232  	// Set up the recorded map with nil records for all combinations
   233  	h.recorded = map[webhookOptions]*admissionRequest{}
   234  	for _, phase := range []string{mutation, validation} {
   235  		for _, converted := range []bool{true, false} {
   236  			for _, version := range []string{"v1", "v1beta1"} {
   237  				h.recorded[webhookOptions{version: version, phase: phase, converted: converted}] = nil
   238  			}
   239  		}
   240  	}
   241  }
   242  func (h *holder) expect(gvr schema.GroupVersionResource, gvk, optionsGVK schema.GroupVersionKind, operation v1beta1.Operation, name, namespace string, object, oldObject, options bool) {
   243  	// Special-case namespaces, since the object name shows up in request attributes
   244  	if len(namespace) == 0 && gvk.Group == "" && gvk.Version == "v1" && gvk.Kind == "Namespace" {
   245  		namespace = name
   246  	}
   247  
   248  	h.lock.Lock()
   249  	defer h.lock.Unlock()
   250  	h.recordGVR = metav1.GroupVersionResource{Group: gvr.Group, Version: gvr.Version, Resource: gvr.Resource}
   251  	h.expectGVK = gvk
   252  	h.recordOperation = string(operation)
   253  	h.recordName = name
   254  	h.recordNamespace = namespace
   255  	h.expectObject = object
   256  	h.expectOldObject = oldObject
   257  	h.expectOptionsGVK = optionsGVK
   258  	h.expectOptions = options
   259  	h.warningHandler.reset()
   260  
   261  	// Set up the recorded map with nil records for all combinations
   262  	h.recorded = map[webhookOptions]*admissionRequest{}
   263  	for _, phase := range []string{mutation, validation} {
   264  		for _, converted := range []bool{true, false} {
   265  			for _, version := range []string{"v1", "v1beta1"} {
   266  				h.recorded[webhookOptions{version: version, phase: phase, converted: converted}] = nil
   267  			}
   268  		}
   269  	}
   270  }
   271  
   272  type admissionRequest struct {
   273  	Operation   string
   274  	Resource    metav1.GroupVersionResource
   275  	SubResource string
   276  	Namespace   string
   277  	Name        string
   278  	Object      runtime.RawExtension
   279  	OldObject   runtime.RawExtension
   280  	Options     runtime.RawExtension
   281  }
   282  
   283  func (h *holder) record(version string, phase string, converted bool, request *admissionRequest) {
   284  	h.lock.Lock()
   285  	defer h.lock.Unlock()
   286  
   287  	// this is useful to turn on if items aren't getting recorded and you need to figure out why
   288  	debug := false
   289  	if debug {
   290  		h.t.Logf("%s %#v %v", request.Operation, request.Resource, request.SubResource)
   291  	}
   292  
   293  	resource := request.Resource
   294  	if len(request.SubResource) > 0 {
   295  		resource.Resource += "/" + request.SubResource
   296  	}
   297  
   298  	// See if we should record this
   299  	gvrToRecord := h.recordGVR
   300  	if converted {
   301  		// If this is a converted webhook, map to the GVR we expect the webhook to see
   302  		gvrToRecord = h.gvrToConvertedGVR[h.recordGVR]
   303  	}
   304  	if resource != gvrToRecord {
   305  		if debug {
   306  			h.t.Log(resource, "!=", gvrToRecord)
   307  		}
   308  		return
   309  	}
   310  
   311  	if request.Operation != h.recordOperation {
   312  		if debug {
   313  			h.t.Log(request.Operation, "!=", h.recordOperation)
   314  		}
   315  		return
   316  	}
   317  	if request.Namespace != h.recordNamespace {
   318  		if debug {
   319  			h.t.Log(request.Namespace, "!=", h.recordNamespace)
   320  		}
   321  		return
   322  	}
   323  
   324  	name := request.Name
   325  	if name != h.recordName {
   326  		if debug {
   327  			h.t.Log(name, "!=", h.recordName)
   328  		}
   329  		return
   330  	}
   331  
   332  	if debug {
   333  		h.t.Logf("recording: %#v = %s %#v %v", webhookOptions{version: version, phase: phase, converted: converted}, request.Operation, request.Resource, request.SubResource)
   334  	}
   335  	h.recorded[webhookOptions{version: version, phase: phase, converted: converted}] = request
   336  }
   337  
   338  func (h *holder) verify(t *testing.T) {
   339  	h.lock.Lock()
   340  	defer h.lock.Unlock()
   341  
   342  	for options, value := range h.recorded {
   343  		if err := h.verifyRequest(options, value); err != nil {
   344  			t.Errorf("version: %v, phase:%v, converted:%v error: %v", options.version, options.phase, options.converted, err)
   345  		}
   346  	}
   347  }
   348  
   349  func (h *holder) verifyRequest(webhookOptions webhookOptions, request *admissionRequest) error {
   350  	converted := webhookOptions.converted
   351  
   352  	// Check if current resource should be exempted from Admission processing
   353  	if admissionExemptResources[gvr(h.recordGVR.Group, h.recordGVR.Version, h.recordGVR.Resource)] {
   354  		if request == nil {
   355  			return nil
   356  		}
   357  		return fmt.Errorf("admission webhook was called, but not supposed to")
   358  	}
   359  
   360  	if request == nil {
   361  		return fmt.Errorf("no request received")
   362  	}
   363  
   364  	if h.expectObject {
   365  		if err := h.verifyObject(converted, request.Object.Object); err != nil {
   366  			return fmt.Errorf("object error: %v", err)
   367  		}
   368  	} else if request.Object.Object != nil {
   369  		return fmt.Errorf("unexpected object: %#v", request.Object.Object)
   370  	}
   371  
   372  	if h.expectOldObject {
   373  		if err := h.verifyObject(converted, request.OldObject.Object); err != nil {
   374  			return fmt.Errorf("old object error: %v", err)
   375  		}
   376  	} else if request.OldObject.Object != nil {
   377  		return fmt.Errorf("unexpected old object: %#v", request.OldObject.Object)
   378  	}
   379  
   380  	if h.expectOptions {
   381  		if err := h.verifyOptions(request.Options.Object); err != nil {
   382  			return fmt.Errorf("options error: %v", err)
   383  		}
   384  	} else if request.Options.Object != nil {
   385  		return fmt.Errorf("unexpected options: %#v", request.Options.Object)
   386  	}
   387  
   388  	if !h.warningHandler.hasWarning(makeWarning(webhookOptions.version, webhookOptions.phase, webhookOptions.converted)) {
   389  		return fmt.Errorf("no warning received from webhook")
   390  	}
   391  
   392  	return nil
   393  }
   394  
   395  func (h *holder) verifyObject(converted bool, obj runtime.Object) error {
   396  	if obj == nil {
   397  		return fmt.Errorf("no object sent")
   398  	}
   399  	expectGVK := h.expectGVK
   400  	if converted {
   401  		expectGVK = h.gvrToConvertedGVK[h.recordGVR]
   402  	}
   403  	if obj.GetObjectKind().GroupVersionKind() != expectGVK {
   404  		return fmt.Errorf("expected %#v, got %#v", expectGVK, obj.GetObjectKind().GroupVersionKind())
   405  	}
   406  	return nil
   407  }
   408  
   409  func (h *holder) verifyOptions(options runtime.Object) error {
   410  	if options == nil {
   411  		return fmt.Errorf("no options sent")
   412  	}
   413  	if options.GetObjectKind().GroupVersionKind() != h.expectOptionsGVK {
   414  		return fmt.Errorf("expected %#v, got %#v", h.expectOptionsGVK, options.GetObjectKind().GroupVersionKind())
   415  	}
   416  	return nil
   417  }
   418  
   419  type warningHandler struct {
   420  	lock     sync.Mutex
   421  	warnings map[string]bool
   422  }
   423  
   424  func (w *warningHandler) reset() {
   425  	w.lock.Lock()
   426  	defer w.lock.Unlock()
   427  	w.warnings = map[string]bool{}
   428  }
   429  func (w *warningHandler) hasWarning(warning string) bool {
   430  	w.lock.Lock()
   431  	defer w.lock.Unlock()
   432  	return w.warnings[warning]
   433  }
   434  func makeWarning(version string, phase string, converted bool) string {
   435  	return fmt.Sprintf("%v/%v/%v", version, phase, converted)
   436  }
   437  
   438  func (w *warningHandler) HandleWarningHeader(code int, agent string, message string) {
   439  	if code != 299 || len(message) == 0 {
   440  		return
   441  	}
   442  	w.lock.Lock()
   443  	defer w.lock.Unlock()
   444  	w.warnings[message] = true
   445  }
   446  
   447  // TestWebhookAdmissionWithWatchCache tests communication between API server and webhook process.
   448  func TestWebhookAdmissionWithWatchCache(t *testing.T) {
   449  	testWebhookAdmission(t, true)
   450  }
   451  
   452  // TestWebhookAdmissionWithoutWatchCache tests communication between API server and webhook process.
   453  func TestWebhookAdmissionWithoutWatchCache(t *testing.T) {
   454  	testWebhookAdmission(t, false)
   455  }
   456  
   457  // testWebhookAdmission tests communication between API server and webhook process.
   458  func testWebhookAdmission(t *testing.T, watchCache bool) {
   459  	defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.APISelfSubjectReview, true)()
   460  
   461  	// holder communicates expectations to webhooks, and results from webhooks
   462  	holder := &holder{
   463  		t:                 t,
   464  		warningHandler:    &warningHandler{warnings: map[string]bool{}},
   465  		gvrToConvertedGVR: map[metav1.GroupVersionResource]metav1.GroupVersionResource{},
   466  		gvrToConvertedGVK: map[metav1.GroupVersionResource]schema.GroupVersionKind{},
   467  	}
   468  
   469  	// set up webhook server
   470  	roots := x509.NewCertPool()
   471  	if !roots.AppendCertsFromPEM(localhostCert) {
   472  		t.Fatal("Failed to append Cert from PEM")
   473  	}
   474  	cert, err := tls.X509KeyPair(localhostCert, localhostKey)
   475  	if err != nil {
   476  		t.Fatalf("Failed to build cert with error: %+v", err)
   477  	}
   478  
   479  	webhookMux := http.NewServeMux()
   480  	webhookMux.Handle("/v1beta1/"+mutation, newV1beta1WebhookHandler(t, holder, mutation, false))
   481  	webhookMux.Handle("/v1beta1/convert/"+mutation, newV1beta1WebhookHandler(t, holder, mutation, true))
   482  	webhookMux.Handle("/v1beta1/"+validation, newV1beta1WebhookHandler(t, holder, validation, false))
   483  	webhookMux.Handle("/v1beta1/convert/"+validation, newV1beta1WebhookHandler(t, holder, validation, true))
   484  	webhookMux.Handle("/v1/"+mutation, newV1WebhookHandler(t, holder, mutation, false))
   485  	webhookMux.Handle("/v1/convert/"+mutation, newV1WebhookHandler(t, holder, mutation, true))
   486  	webhookMux.Handle("/v1/"+validation, newV1WebhookHandler(t, holder, validation, false))
   487  	webhookMux.Handle("/v1/convert/"+validation, newV1WebhookHandler(t, holder, validation, true))
   488  	webhookMux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
   489  		holder.t.Errorf("unexpected request to %v", req.URL.Path)
   490  	}))
   491  	webhookServer := httptest.NewUnstartedServer(webhookMux)
   492  	webhookServer.TLS = &tls.Config{
   493  		RootCAs:      roots,
   494  		Certificates: []tls.Certificate{cert},
   495  	}
   496  	webhookServer.StartTLS()
   497  	defer webhookServer.Close()
   498  
   499  	// start API server
   500  	etcdConfig := framework.SharedEtcd()
   501  	server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{
   502  		fmt.Sprintf("--watch-cache=%v", watchCache),
   503  		// turn off admission plugins that add finalizers
   504  		"--disable-admission-plugins=ServiceAccount,StorageObjectInUseProtection",
   505  		// force enable all resources so we can check storage.
   506  		"--runtime-config=api/all=true",
   507  		// enable feature-gates that protect resources to check their storage, too.
   508  		// e.g. "--feature-gates=EphemeralContainers=true",
   509  	}, etcdConfig)
   510  	defer server.TearDownFn()
   511  
   512  	// Configure a client with a distinct user name so that it is easy to distinguish requests
   513  	// made by the client from requests made by controllers. We use this to filter out requests
   514  	// before recording them to ensure we don't accidentally mistake requests from controllers
   515  	// as requests made by the client.
   516  	clientConfig := rest.CopyConfig(server.ClientConfig)
   517  	clientConfig.Impersonate.UserName = testClientUsername
   518  	clientConfig.Impersonate.Groups = []string{"system:masters", "system:authenticated"}
   519  	clientConfig.WarningHandler = holder.warningHandler
   520  	client, err := clientset.NewForConfig(clientConfig)
   521  	if err != nil {
   522  		t.Fatalf("unexpected error: %v", err)
   523  	}
   524  
   525  	// create CRDs
   526  	etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...)
   527  
   528  	if _, err := client.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}, metav1.CreateOptions{}); err != nil {
   529  		t.Fatal(err)
   530  	}
   531  
   532  	// gather resources to test
   533  	dynamicClient, err := dynamic.NewForConfig(clientConfig)
   534  	if err != nil {
   535  		t.Fatal(err)
   536  	}
   537  	_, resources, err := client.Discovery().ServerGroupsAndResources()
   538  	if err != nil {
   539  		t.Fatalf("Failed to get ServerGroupsAndResources with error: %+v", err)
   540  	}
   541  
   542  	gvrsToTest := []schema.GroupVersionResource{}
   543  	resourcesByGVR := map[schema.GroupVersionResource]metav1.APIResource{}
   544  
   545  	for _, list := range resources {
   546  		defaultGroupVersion, err := schema.ParseGroupVersion(list.GroupVersion)
   547  		if err != nil {
   548  			t.Errorf("Failed to get GroupVersion for: %+v", list)
   549  			continue
   550  		}
   551  		for _, resource := range list.APIResources {
   552  			if resource.Group == "" {
   553  				resource.Group = defaultGroupVersion.Group
   554  			}
   555  			if resource.Version == "" {
   556  				resource.Version = defaultGroupVersion.Version
   557  			}
   558  			gvr := defaultGroupVersion.WithResource(resource.Name)
   559  			resourcesByGVR[gvr] = resource
   560  			if shouldTestResource(gvr, resource) {
   561  				gvrsToTest = append(gvrsToTest, gvr)
   562  			}
   563  		}
   564  	}
   565  
   566  	sort.SliceStable(gvrsToTest, func(i, j int) bool {
   567  		if gvrsToTest[i].Group < gvrsToTest[j].Group {
   568  			return true
   569  		}
   570  		if gvrsToTest[i].Group > gvrsToTest[j].Group {
   571  			return false
   572  		}
   573  		if gvrsToTest[i].Version < gvrsToTest[j].Version {
   574  			return true
   575  		}
   576  		if gvrsToTest[i].Version > gvrsToTest[j].Version {
   577  			return false
   578  		}
   579  		if gvrsToTest[i].Resource < gvrsToTest[j].Resource {
   580  			return true
   581  		}
   582  		if gvrsToTest[i].Resource > gvrsToTest[j].Resource {
   583  			return false
   584  		}
   585  		return true
   586  	})
   587  
   588  	// map unqualified resource names to the fully qualified resource we will expect to be converted to
   589  	// Note: this only works because there are no overlapping resource names in-process that are not co-located
   590  	convertedResources := map[string]schema.GroupVersionResource{}
   591  	// build the webhook rules enumerating the specific group/version/resources we want
   592  	convertedV1beta1Rules := []admissionregistrationv1beta1.RuleWithOperations{}
   593  	convertedV1Rules := []admissionregistrationv1.RuleWithOperations{}
   594  	for _, gvr := range gvrsToTest {
   595  		metaGVR := metav1.GroupVersionResource{Group: gvr.Group, Version: gvr.Version, Resource: gvr.Resource}
   596  
   597  		convertedGVR, ok := convertedResources[gvr.Resource]
   598  		if !ok {
   599  			// this is the first time we've seen this resource
   600  			// record the fully qualified resource we expect
   601  			convertedGVR = gvr
   602  			convertedResources[gvr.Resource] = gvr
   603  			// add an admission rule indicating we can receive this version
   604  			convertedV1beta1Rules = append(convertedV1beta1Rules, admissionregistrationv1beta1.RuleWithOperations{
   605  				Operations: []admissionregistrationv1beta1.OperationType{admissionregistrationv1beta1.OperationAll},
   606  				Rule:       admissionregistrationv1beta1.Rule{APIGroups: []string{gvr.Group}, APIVersions: []string{gvr.Version}, Resources: []string{gvr.Resource}},
   607  			})
   608  			convertedV1Rules = append(convertedV1Rules, admissionregistrationv1.RuleWithOperations{
   609  				Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
   610  				Rule:       admissionregistrationv1.Rule{APIGroups: []string{gvr.Group}, APIVersions: []string{gvr.Version}, Resources: []string{gvr.Resource}},
   611  			})
   612  		}
   613  
   614  		// record the expected resource and kind
   615  		holder.gvrToConvertedGVR[metaGVR] = metav1.GroupVersionResource{Group: convertedGVR.Group, Version: convertedGVR.Version, Resource: convertedGVR.Resource}
   616  		holder.gvrToConvertedGVK[metaGVR] = schema.GroupVersionKind{Group: resourcesByGVR[convertedGVR].Group, Version: resourcesByGVR[convertedGVR].Version, Kind: resourcesByGVR[convertedGVR].Kind}
   617  	}
   618  
   619  	if err := createV1beta1MutationWebhook(server.EtcdClient, server.EtcdStoragePrefix, client, webhookServer.URL+"/v1beta1/"+mutation, webhookServer.URL+"/v1beta1/convert/"+mutation, convertedV1beta1Rules); err != nil {
   620  		t.Fatal(err)
   621  	}
   622  	if err := createV1beta1ValidationWebhook(server.EtcdClient, server.EtcdStoragePrefix, client, webhookServer.URL+"/v1beta1/"+validation, webhookServer.URL+"/v1beta1/convert/"+validation, convertedV1beta1Rules); err != nil {
   623  		t.Fatal(err)
   624  	}
   625  	if err := createV1MutationWebhook(client, webhookServer.URL+"/v1/"+mutation, webhookServer.URL+"/v1/convert/"+mutation, convertedV1Rules); err != nil {
   626  		t.Fatal(err)
   627  	}
   628  	if err := createV1ValidationWebhook(client, webhookServer.URL+"/v1/"+validation, webhookServer.URL+"/v1/convert/"+validation, convertedV1Rules); err != nil {
   629  		t.Fatal(err)
   630  	}
   631  
   632  	// Allow the webhook to establish
   633  	time.Sleep(time.Second)
   634  
   635  	start := time.Now()
   636  	count := 0
   637  
   638  	// Test admission on all resources, subresources, and verbs
   639  	for _, gvr := range gvrsToTest {
   640  		resource := resourcesByGVR[gvr]
   641  		t.Run(gvr.Group+"."+gvr.Version+"."+strings.ReplaceAll(resource.Name, "/", "."), func(t *testing.T) {
   642  			for _, verb := range []string{"create", "update", "patch", "connect", "delete", "deletecollection"} {
   643  				if shouldTestResourceVerb(gvr, resource, verb) {
   644  					t.Run(verb, func(t *testing.T) {
   645  						count++
   646  						holder.reset(t)
   647  						testFunc := getTestFunc(gvr, verb)
   648  						testFunc(&testContext{
   649  							t:               t,
   650  							admissionHolder: holder,
   651  							client:          dynamicClient,
   652  							clientset:       client,
   653  							verb:            verb,
   654  							gvr:             gvr,
   655  							resource:        resource,
   656  							resources:       resourcesByGVR,
   657  						})
   658  						holder.verify(t)
   659  					})
   660  				}
   661  			}
   662  		})
   663  	}
   664  
   665  	duration := time.Since(start)
   666  	perResourceDuration := time.Duration(int(duration) / count)
   667  	if perResourceDuration >= 150*time.Millisecond {
   668  		t.Errorf("expected resources to process in < 150ms, average was %v", perResourceDuration)
   669  	}
   670  }
   671  
   672  //
   673  // generic resource testing
   674  //
   675  
   676  func testResourceCreate(c *testContext) {
   677  	stubObj, err := getStubObj(c.gvr, c.resource)
   678  	if err != nil {
   679  		c.t.Error(err)
   680  		return
   681  	}
   682  	ns := ""
   683  	if c.resource.Namespaced {
   684  		ns = testNamespace
   685  	}
   686  	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkCreateOptions, v1beta1.Create, stubObj.GetName(), ns, true, false, true)
   687  	_, err = c.client.Resource(c.gvr).Namespace(ns).Create(context.TODO(), stubObj, metav1.CreateOptions{})
   688  	if err != nil {
   689  		c.t.Error(err)
   690  		return
   691  	}
   692  }
   693  
   694  func testResourceUpdate(c *testContext) {
   695  	if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
   696  		obj, err := createOrGetResource(c.client, c.gvr, c.resource)
   697  		if err != nil {
   698  			return err
   699  		}
   700  		obj.SetAnnotations(map[string]string{"update": "true"})
   701  		c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkUpdateOptions, v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true, true)
   702  		_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Update(context.TODO(), obj, metav1.UpdateOptions{})
   703  		return err
   704  	}); err != nil {
   705  		c.t.Error(err)
   706  		return
   707  	}
   708  }
   709  
   710  func testResourcePatch(c *testContext) {
   711  	obj, err := createOrGetResource(c.client, c.gvr, c.resource)
   712  	if err != nil {
   713  		c.t.Error(err)
   714  		return
   715  	}
   716  	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkUpdateOptions, v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true, true)
   717  	_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch(
   718  		context.TODO(),
   719  		obj.GetName(),
   720  		types.MergePatchType,
   721  		[]byte(`{"metadata":{"annotations":{"patch":"true"}}}`),
   722  		metav1.PatchOptions{})
   723  	if err != nil {
   724  		c.t.Error(err)
   725  		return
   726  	}
   727  }
   728  
   729  func testResourceDelete(c *testContext) {
   730  	// Verify that an immediate delete triggers the webhook and populates the admisssionRequest.oldObject.
   731  	obj, err := createOrGetResource(c.client, c.gvr, c.resource)
   732  	if err != nil {
   733  		c.t.Error(err)
   734  		return
   735  	}
   736  	background := metav1.DeletePropagationBackground
   737  	zero := int64(0)
   738  	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkDeleteOptions, v1beta1.Delete, obj.GetName(), obj.GetNamespace(), false, true, true)
   739  	err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(context.TODO(), obj.GetName(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background})
   740  	if err != nil {
   741  		c.t.Error(err)
   742  		return
   743  	}
   744  	c.admissionHolder.verify(c.t)
   745  
   746  	// wait for the item to be gone
   747  	err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
   748  		obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
   749  		if apierrors.IsNotFound(err) {
   750  			return true, nil
   751  		}
   752  		if err == nil {
   753  			c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers())
   754  			return false, nil
   755  		}
   756  		return false, err
   757  	})
   758  	if err != nil {
   759  		c.t.Error(err)
   760  		return
   761  	}
   762  
   763  	// Verify that an update-on-delete triggers the webhook and populates the admisssionRequest.oldObject.
   764  	obj, err = createOrGetResource(c.client, c.gvr, c.resource)
   765  	if err != nil {
   766  		c.t.Error(err)
   767  		return
   768  	}
   769  	// Adding finalizer to the object, then deleting it.
   770  	// We don't add finalizers by setting DeleteOptions.PropagationPolicy
   771  	// because some resource (e.g., events) do not support garbage
   772  	// collector finalizers.
   773  	_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch(
   774  		context.TODO(),
   775  		obj.GetName(),
   776  		types.MergePatchType,
   777  		[]byte(`{"metadata":{"finalizers":["test/k8s.io"]}}`),
   778  		metav1.PatchOptions{})
   779  	if err != nil {
   780  		c.t.Error(err)
   781  		return
   782  	}
   783  	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkDeleteOptions, v1beta1.Delete, obj.GetName(), obj.GetNamespace(), false, true, true)
   784  	err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(context.TODO(), obj.GetName(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background})
   785  	if err != nil {
   786  		c.t.Error(err)
   787  		return
   788  	}
   789  	c.admissionHolder.verify(c.t)
   790  
   791  	// wait other finalizers (e.g., crd's customresourcecleanup finalizer) to be removed.
   792  	err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
   793  		obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
   794  		if err != nil {
   795  			return false, err
   796  		}
   797  		finalizers := obj.GetFinalizers()
   798  		if len(finalizers) != 1 {
   799  			c.t.Logf("waiting for other finalizers on %#v %s to be removed, existing finalizers are %v", c.gvr, obj.GetName(), obj.GetFinalizers())
   800  			return false, nil
   801  		}
   802  		if finalizers[0] != "test/k8s.io" {
   803  			return false, fmt.Errorf("expected the single finalizer on %#v %s to be test/k8s.io, got %v", c.gvr, obj.GetName(), obj.GetFinalizers())
   804  		}
   805  		return true, nil
   806  	})
   807  	if err != nil {
   808  		c.t.Error(err)
   809  		return
   810  	}
   811  
   812  	// remove the finalizer
   813  	_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch(
   814  		context.TODO(),
   815  		obj.GetName(),
   816  		types.MergePatchType,
   817  		[]byte(`{"metadata":{"finalizers":[]}}`),
   818  		metav1.PatchOptions{})
   819  	if err != nil {
   820  		c.t.Error(err)
   821  		return
   822  	}
   823  	// wait for the item to be gone
   824  	err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
   825  		obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
   826  		if apierrors.IsNotFound(err) {
   827  			return true, nil
   828  		}
   829  		if err == nil {
   830  			c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers())
   831  			return false, nil
   832  		}
   833  		return false, err
   834  	})
   835  	if err != nil {
   836  		c.t.Error(err)
   837  		return
   838  	}
   839  }
   840  
   841  func testResourceDeletecollection(c *testContext) {
   842  	obj, err := createOrGetResource(c.client, c.gvr, c.resource)
   843  	if err != nil {
   844  		c.t.Error(err)
   845  		return
   846  	}
   847  	background := metav1.DeletePropagationBackground
   848  	zero := int64(0)
   849  
   850  	// update the object with a label that matches our selector
   851  	_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch(
   852  		context.TODO(),
   853  		obj.GetName(),
   854  		types.MergePatchType,
   855  		[]byte(`{"metadata":{"labels":{"webhooktest":"true"}}}`),
   856  		metav1.PatchOptions{})
   857  	if err != nil {
   858  		c.t.Error(err)
   859  		return
   860  	}
   861  
   862  	// set expectations
   863  	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkDeleteOptions, v1beta1.Delete, "", obj.GetNamespace(), false, true, true)
   864  
   865  	// delete
   866  	err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).DeleteCollection(context.TODO(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background}, metav1.ListOptions{LabelSelector: "webhooktest=true"})
   867  	if err != nil {
   868  		c.t.Error(err)
   869  		return
   870  	}
   871  
   872  	// wait for the item to be gone
   873  	err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
   874  		obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
   875  		if apierrors.IsNotFound(err) {
   876  			return true, nil
   877  		}
   878  		if err == nil {
   879  			c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers())
   880  			return false, nil
   881  		}
   882  		return false, err
   883  	})
   884  	if err != nil {
   885  		c.t.Error(err)
   886  		return
   887  	}
   888  }
   889  
   890  func getParentGVR(gvr schema.GroupVersionResource) schema.GroupVersionResource {
   891  	parentGVR, found := parentResources[gvr]
   892  	// if no special override is found, just drop the subresource
   893  	if !found {
   894  		parentGVR = gvr
   895  		parentGVR.Resource = strings.Split(parentGVR.Resource, "/")[0]
   896  	}
   897  	return parentGVR
   898  }
   899  
   900  func testTokenCreate(c *testContext) {
   901  	saGVR := gvr("", "v1", "serviceaccounts")
   902  	sa, err := createOrGetResource(c.client, saGVR, c.resources[saGVR])
   903  	if err != nil {
   904  		c.t.Error(err)
   905  		return
   906  	}
   907  
   908  	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkCreateOptions, v1beta1.Create, sa.GetName(), sa.GetNamespace(), true, false, true)
   909  	if err = c.clientset.CoreV1().RESTClient().Post().Namespace(sa.GetNamespace()).Resource("serviceaccounts").Name(sa.GetName()).SubResource("token").Body(&authenticationv1.TokenRequest{
   910  		ObjectMeta: metav1.ObjectMeta{Name: sa.GetName()},
   911  		Spec: authenticationv1.TokenRequestSpec{
   912  			Audiences: []string{"api"},
   913  		},
   914  	}).Do(context.TODO()).Error(); err != nil {
   915  		c.t.Error(err)
   916  		return
   917  	}
   918  	c.admissionHolder.verify(c.t)
   919  }
   920  
   921  func testSubresourceUpdate(c *testContext) {
   922  	if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
   923  		parentGVR := getParentGVR(c.gvr)
   924  		parentResource := c.resources[parentGVR]
   925  		obj, err := createOrGetResource(c.client, parentGVR, parentResource)
   926  		if err != nil {
   927  			return err
   928  		}
   929  
   930  		// Save the parent object as what we submit
   931  		submitObj := obj
   932  
   933  		gvrWithoutSubresources := c.gvr
   934  		gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
   935  		subresources := strings.Split(c.gvr.Resource, "/")[1:]
   936  
   937  		// If the subresource supports get, fetch that as the object to submit (namespaces/finalize, */scale, etc)
   938  		if sets.NewString(c.resource.Verbs...).Has("get") {
   939  			submitObj, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{}, subresources...)
   940  			if err != nil {
   941  				return err
   942  			}
   943  		}
   944  
   945  		// Modify the object
   946  		submitObj.SetAnnotations(map[string]string{"subresourceupdate": "true"})
   947  
   948  		// set expectations
   949  		c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkUpdateOptions, v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true, true)
   950  
   951  		_, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Update(
   952  			context.TODO(),
   953  			submitObj,
   954  			metav1.UpdateOptions{},
   955  			subresources...,
   956  		)
   957  		return err
   958  	}); err != nil {
   959  		c.t.Error(err)
   960  	}
   961  }
   962  
   963  func testSubresourcePatch(c *testContext) {
   964  	parentGVR := getParentGVR(c.gvr)
   965  	parentResource := c.resources[parentGVR]
   966  	obj, err := createOrGetResource(c.client, parentGVR, parentResource)
   967  	if err != nil {
   968  		c.t.Error(err)
   969  		return
   970  	}
   971  
   972  	gvrWithoutSubresources := c.gvr
   973  	gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
   974  	subresources := strings.Split(c.gvr.Resource, "/")[1:]
   975  
   976  	// set expectations
   977  	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkUpdateOptions, v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true, true)
   978  
   979  	_, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Patch(
   980  		context.TODO(),
   981  		obj.GetName(),
   982  		types.MergePatchType,
   983  		[]byte(`{"metadata":{"annotations":{"subresourcepatch":"true"}}}`),
   984  		metav1.PatchOptions{},
   985  		subresources...,
   986  	)
   987  	if err != nil {
   988  		c.t.Error(err)
   989  		return
   990  	}
   991  }
   992  
   993  func unimplemented(c *testContext) {
   994  	c.t.Errorf("Test function for %+v has not been implemented...", c.gvr)
   995  }
   996  
   997  //
   998  // custom methods
   999  //
  1000  
  1001  // testNamespaceDelete verifies namespace-specific delete behavior:
  1002  // - ensures admission is called on first delete (which only sets deletionTimestamp and terminating state)
  1003  // - removes finalizer from namespace
  1004  // - ensures admission is called on final delete once finalizers are removed
  1005  func testNamespaceDelete(c *testContext) {
  1006  	obj, err := createOrGetResource(c.client, c.gvr, c.resource)
  1007  	if err != nil {
  1008  		c.t.Error(err)
  1009  		return
  1010  	}
  1011  	background := metav1.DeletePropagationBackground
  1012  	zero := int64(0)
  1013  
  1014  	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkDeleteOptions, v1beta1.Delete, obj.GetName(), obj.GetNamespace(), false, true, true)
  1015  	err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(context.TODO(), obj.GetName(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background})
  1016  	if err != nil {
  1017  		c.t.Error(err)
  1018  		return
  1019  	}
  1020  	c.admissionHolder.verify(c.t)
  1021  
  1022  	// do the finalization so the namespace can be deleted
  1023  	obj, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
  1024  	if err != nil {
  1025  		c.t.Error(err)
  1026  		return
  1027  	}
  1028  	err = unstructured.SetNestedField(obj.Object, nil, "spec", "finalizers")
  1029  	if err != nil {
  1030  		c.t.Error(err)
  1031  		return
  1032  	}
  1033  	_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Update(context.TODO(), obj, metav1.UpdateOptions{}, "finalize")
  1034  	if err != nil {
  1035  		c.t.Error(err)
  1036  		return
  1037  	}
  1038  	// verify namespace is gone
  1039  	obj, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
  1040  	if err == nil || !apierrors.IsNotFound(err) {
  1041  		c.t.Errorf("expected namespace to be gone, got %#v, %v", obj, err)
  1042  	}
  1043  }
  1044  
  1045  // testDeploymentRollback verifies rollback-specific behavior:
  1046  // - creates a parent deployment
  1047  // - creates a rollback object and posts it
  1048  func testDeploymentRollback(c *testContext) {
  1049  	deploymentGVR := gvr("apps", "v1", "deployments")
  1050  	obj, err := createOrGetResource(c.client, deploymentGVR, c.resources[deploymentGVR])
  1051  	if err != nil {
  1052  		c.t.Error(err)
  1053  		return
  1054  	}
  1055  
  1056  	gvrWithoutSubresources := c.gvr
  1057  	gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
  1058  	subresources := strings.Split(c.gvr.Resource, "/")[1:]
  1059  
  1060  	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkCreateOptions, v1beta1.Create, obj.GetName(), obj.GetNamespace(), true, false, true)
  1061  
  1062  	var rollbackObj runtime.Object
  1063  	switch c.gvr {
  1064  	case gvr("apps", "v1beta1", "deployments/rollback"):
  1065  		rollbackObj = &appsv1beta1.DeploymentRollback{
  1066  			TypeMeta:   metav1.TypeMeta{APIVersion: "apps/v1beta1", Kind: "DeploymentRollback"},
  1067  			Name:       obj.GetName(),
  1068  			RollbackTo: appsv1beta1.RollbackConfig{Revision: 0},
  1069  		}
  1070  	case gvr("extensions", "v1beta1", "deployments/rollback"):
  1071  		rollbackObj = &extensionsv1beta1.DeploymentRollback{
  1072  			TypeMeta:   metav1.TypeMeta{APIVersion: "extensions/v1beta1", Kind: "DeploymentRollback"},
  1073  			Name:       obj.GetName(),
  1074  			RollbackTo: extensionsv1beta1.RollbackConfig{Revision: 0},
  1075  		}
  1076  	default:
  1077  		c.t.Errorf("unknown rollback resource %#v", c.gvr)
  1078  		return
  1079  	}
  1080  
  1081  	rollbackUnstructuredBody, err := runtime.DefaultUnstructuredConverter.ToUnstructured(rollbackObj)
  1082  	if err != nil {
  1083  		c.t.Errorf("ToUnstructured failed: %v", err)
  1084  		return
  1085  	}
  1086  	rollbackUnstructuredObj := &unstructured.Unstructured{Object: rollbackUnstructuredBody}
  1087  	rollbackUnstructuredObj.SetName(obj.GetName())
  1088  
  1089  	_, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Create(context.TODO(), rollbackUnstructuredObj, metav1.CreateOptions{}, subresources...)
  1090  	if err != nil {
  1091  		c.t.Error(err)
  1092  		return
  1093  	}
  1094  }
  1095  
  1096  // testPodConnectSubresource verifies connect subresources
  1097  func testPodConnectSubresource(c *testContext) {
  1098  	podGVR := gvr("", "v1", "pods")
  1099  	pod, err := createOrGetResource(c.client, podGVR, c.resources[podGVR])
  1100  	if err != nil {
  1101  		c.t.Error(err)
  1102  		return
  1103  	}
  1104  
  1105  	// check all upgradeable verbs
  1106  	for _, httpMethod := range []string{"GET", "POST"} {
  1107  		c.t.Logf("verifying %v", httpMethod)
  1108  
  1109  		c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), schema.GroupVersionKind{}, v1beta1.Connect, pod.GetName(), pod.GetNamespace(), true, false, false)
  1110  		var err error
  1111  		switch c.gvr {
  1112  		case gvr("", "v1", "pods/exec"):
  1113  			err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("exec").Do(context.TODO()).Error()
  1114  		case gvr("", "v1", "pods/attach"):
  1115  			err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("attach").Do(context.TODO()).Error()
  1116  		case gvr("", "v1", "pods/portforward"):
  1117  			err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("portforward").Do(context.TODO()).Error()
  1118  		default:
  1119  			c.t.Errorf("unknown subresource %#v", c.gvr)
  1120  			return
  1121  		}
  1122  
  1123  		if err != nil {
  1124  			c.t.Logf("debug: result of subresource connect: %v", err)
  1125  		}
  1126  		c.admissionHolder.verify(c.t)
  1127  
  1128  	}
  1129  }
  1130  
  1131  // testPodBindingEviction verifies pod binding and eviction admission
  1132  func testPodBindingEviction(c *testContext) {
  1133  	podGVR := gvr("", "v1", "pods")
  1134  	pod, err := createOrGetResource(c.client, podGVR, c.resources[podGVR])
  1135  	if err != nil {
  1136  		c.t.Error(err)
  1137  		return
  1138  	}
  1139  
  1140  	background := metav1.DeletePropagationBackground
  1141  	zero := int64(0)
  1142  	forceDelete := metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background}
  1143  	defer func() {
  1144  		err := c.clientset.CoreV1().Pods(pod.GetNamespace()).Delete(context.TODO(), pod.GetName(), forceDelete)
  1145  		if err != nil && !apierrors.IsNotFound(err) {
  1146  			c.t.Error(err)
  1147  			return
  1148  		}
  1149  	}()
  1150  
  1151  	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkCreateOptions, v1beta1.Create, pod.GetName(), pod.GetNamespace(), true, false, true)
  1152  
  1153  	switch c.gvr {
  1154  	case gvr("", "v1", "bindings"):
  1155  		err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("bindings").Body(&corev1.Binding{
  1156  			ObjectMeta: metav1.ObjectMeta{Name: pod.GetName()},
  1157  			Target:     corev1.ObjectReference{Name: "foo", Kind: "Node", APIVersion: "v1"},
  1158  		}).Do(context.TODO()).Error()
  1159  
  1160  	case gvr("", "v1", "pods/binding"):
  1161  		err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("binding").Body(&corev1.Binding{
  1162  			ObjectMeta: metav1.ObjectMeta{Name: pod.GetName()},
  1163  			Target:     corev1.ObjectReference{Name: "foo", Kind: "Node", APIVersion: "v1"},
  1164  		}).Do(context.TODO()).Error()
  1165  
  1166  	case gvr("", "v1", "pods/eviction"):
  1167  		err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("eviction").Body(&policyv1.Eviction{
  1168  			ObjectMeta:    metav1.ObjectMeta{Name: pod.GetName()},
  1169  			DeleteOptions: &forceDelete,
  1170  		}).Do(context.TODO()).Error()
  1171  
  1172  	default:
  1173  		c.t.Errorf("unhandled resource %#v", c.gvr)
  1174  		return
  1175  	}
  1176  
  1177  	if err != nil {
  1178  		c.t.Error(err)
  1179  		return
  1180  	}
  1181  }
  1182  
  1183  // testSubresourceProxy verifies proxy subresources
  1184  func testSubresourceProxy(c *testContext) {
  1185  	parentGVR := getParentGVR(c.gvr)
  1186  	parentResource := c.resources[parentGVR]
  1187  	obj, err := createOrGetResource(c.client, parentGVR, parentResource)
  1188  	if err != nil {
  1189  		c.t.Error(err)
  1190  		return
  1191  	}
  1192  
  1193  	gvrWithoutSubresources := c.gvr
  1194  	gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
  1195  	subresources := strings.Split(c.gvr.Resource, "/")[1:]
  1196  
  1197  	verbToHTTPMethods := map[string][]string{
  1198  		"create": {"POST", "GET", "HEAD", "OPTIONS"}, // also test read-only verbs map to Connect admission
  1199  		"update": {"PUT"},
  1200  		"patch":  {"PATCH"},
  1201  		"delete": {"DELETE"},
  1202  	}
  1203  	httpMethodsToTest, ok := verbToHTTPMethods[c.verb]
  1204  	if !ok {
  1205  		c.t.Errorf("unknown verb %v", c.verb)
  1206  		return
  1207  	}
  1208  
  1209  	for _, httpMethod := range httpMethodsToTest {
  1210  		c.t.Logf("testing %v", httpMethod)
  1211  		request := c.clientset.CoreV1().RESTClient().Verb(httpMethod)
  1212  
  1213  		// add the namespace if required
  1214  		if len(obj.GetNamespace()) > 0 {
  1215  			request = request.Namespace(obj.GetNamespace())
  1216  		}
  1217  
  1218  		// set expectations
  1219  		c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), schema.GroupVersionKind{}, v1beta1.Connect, obj.GetName(), obj.GetNamespace(), true, false, false)
  1220  		// run the request. we don't actually care if the request is successful, just that admission gets called as expected
  1221  		err = request.Resource(gvrWithoutSubresources.Resource).Name(obj.GetName()).SubResource(subresources...).Do(context.TODO()).Error()
  1222  		if err != nil {
  1223  			c.t.Logf("debug: result of subresource proxy (error expected): %v", err)
  1224  		}
  1225  		// verify the result
  1226  		c.admissionHolder.verify(c.t)
  1227  	}
  1228  }
  1229  
  1230  func testPruningRandomNumbers(c *testContext) {
  1231  	testResourceCreate(c)
  1232  
  1233  	cr2pant, err := c.client.Resource(c.gvr).Get(context.TODO(), "fortytwo", metav1.GetOptions{})
  1234  	if err != nil {
  1235  		c.t.Error(err)
  1236  		return
  1237  	}
  1238  
  1239  	foo, found, err := unstructured.NestedString(cr2pant.Object, "foo")
  1240  	if err != nil {
  1241  		c.t.Error(err)
  1242  		return
  1243  	}
  1244  	if found {
  1245  		c.t.Errorf("expected .foo to be pruned, but got: %s", foo)
  1246  	}
  1247  }
  1248  
  1249  func testNoPruningCustomFancy(c *testContext) {
  1250  	testResourceCreate(c)
  1251  
  1252  	cr2pant, err := c.client.Resource(c.gvr).Get(context.TODO(), "cr2pant", metav1.GetOptions{})
  1253  	if err != nil {
  1254  		c.t.Error(err)
  1255  		return
  1256  	}
  1257  
  1258  	foo, _, err := unstructured.NestedString(cr2pant.Object, "foo")
  1259  	if err != nil {
  1260  		c.t.Error(err)
  1261  		return
  1262  	}
  1263  
  1264  	// check that no pruning took place
  1265  	if expected, got := "test", foo; expected != got {
  1266  		c.t.Errorf("expected /foo to be %q, got: %q", expected, got)
  1267  	}
  1268  }
  1269  
  1270  //
  1271  // utility methods
  1272  //
  1273  
  1274  func newV1beta1WebhookHandler(t *testing.T, holder *holder, phase string, converted bool) http.Handler {
  1275  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1276  		defer r.Body.Close()
  1277  		data, err := io.ReadAll(r.Body)
  1278  		if err != nil {
  1279  			t.Error(err)
  1280  			return
  1281  		}
  1282  
  1283  		if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
  1284  			t.Errorf("contentType=%s, expect application/json", contentType)
  1285  			return
  1286  		}
  1287  
  1288  		review := v1beta1.AdmissionReview{}
  1289  		if err := json.Unmarshal(data, &review); err != nil {
  1290  			t.Errorf("Fail to deserialize object: %s with error: %v", string(data), err)
  1291  			http.Error(w, err.Error(), 400)
  1292  			return
  1293  		}
  1294  
  1295  		if review.GetObjectKind().GroupVersionKind() != gvk("admission.k8s.io", "v1beta1", "AdmissionReview") {
  1296  			t.Errorf("Invalid admission review kind: %#v", review.GetObjectKind().GroupVersionKind())
  1297  			http.Error(w, err.Error(), 400)
  1298  			return
  1299  		}
  1300  
  1301  		if len(review.Request.Object.Raw) > 0 {
  1302  			u := &unstructured.Unstructured{Object: map[string]interface{}{}}
  1303  			if err := json.Unmarshal(review.Request.Object.Raw, u); err != nil {
  1304  				t.Errorf("Fail to deserialize object: %s with error: %v", string(review.Request.Object.Raw), err)
  1305  				http.Error(w, err.Error(), 400)
  1306  				return
  1307  			}
  1308  			review.Request.Object.Object = u
  1309  		}
  1310  		if len(review.Request.OldObject.Raw) > 0 {
  1311  			u := &unstructured.Unstructured{Object: map[string]interface{}{}}
  1312  			if err := json.Unmarshal(review.Request.OldObject.Raw, u); err != nil {
  1313  				t.Errorf("Fail to deserialize object: %s with error: %v", string(review.Request.OldObject.Raw), err)
  1314  				http.Error(w, err.Error(), 400)
  1315  				return
  1316  			}
  1317  			review.Request.OldObject.Object = u
  1318  		}
  1319  
  1320  		if len(review.Request.Options.Raw) > 0 {
  1321  			u := &unstructured.Unstructured{Object: map[string]interface{}{}}
  1322  			if err := json.Unmarshal(review.Request.Options.Raw, u); err != nil {
  1323  				t.Errorf("Fail to deserialize options object: %s for admission request %#+v with error: %v", string(review.Request.Options.Raw), review.Request, err)
  1324  				http.Error(w, err.Error(), 400)
  1325  				return
  1326  			}
  1327  			review.Request.Options.Object = u
  1328  		}
  1329  
  1330  		if review.Request.UserInfo.Username == testClientUsername {
  1331  			// only record requests originating from this integration test's client
  1332  			reviewRequest := &admissionRequest{
  1333  				Operation:   string(review.Request.Operation),
  1334  				Resource:    review.Request.Resource,
  1335  				SubResource: review.Request.SubResource,
  1336  				Namespace:   review.Request.Namespace,
  1337  				Name:        review.Request.Name,
  1338  				Object:      review.Request.Object,
  1339  				OldObject:   review.Request.OldObject,
  1340  				Options:     review.Request.Options,
  1341  			}
  1342  			holder.record("v1beta1", phase, converted, reviewRequest)
  1343  		}
  1344  
  1345  		review.Response = &v1beta1.AdmissionResponse{
  1346  			Allowed: true,
  1347  			Result:  &metav1.Status{Message: "admitted"},
  1348  		}
  1349  
  1350  		// v1beta1 webhook handler tolerated these not being set. verify the server continues to accept these as unset.
  1351  		review.APIVersion = ""
  1352  		review.Kind = ""
  1353  		review.Response.UID = ""
  1354  
  1355  		// test plumbing warnings back to the client
  1356  		review.Response.Warnings = []string{makeWarning("v1beta1", phase, converted)}
  1357  
  1358  		// If we're mutating, and have an object, return a patch to exercise conversion
  1359  		if phase == mutation && len(review.Request.Object.Raw) > 0 {
  1360  			review.Response.Patch = []byte(`[{"op":"add","path":"/foo","value":"test"}]`)
  1361  			jsonPatch := v1beta1.PatchTypeJSONPatch
  1362  			review.Response.PatchType = &jsonPatch
  1363  		}
  1364  
  1365  		w.Header().Set("Content-Type", "application/json")
  1366  		if err := json.NewEncoder(w).Encode(review); err != nil {
  1367  			t.Errorf("Marshal of response failed with error: %v", err)
  1368  		}
  1369  	})
  1370  }
  1371  
  1372  func newV1WebhookHandler(t *testing.T, holder *holder, phase string, converted bool) http.Handler {
  1373  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1374  		defer r.Body.Close()
  1375  		data, err := io.ReadAll(r.Body)
  1376  		if err != nil {
  1377  			t.Error(err)
  1378  			return
  1379  		}
  1380  
  1381  		if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
  1382  			t.Errorf("contentType=%s, expect application/json", contentType)
  1383  			return
  1384  		}
  1385  
  1386  		review := admissionreviewv1.AdmissionReview{}
  1387  		if err := json.Unmarshal(data, &review); err != nil {
  1388  			t.Errorf("Fail to deserialize object: %s with error: %v", string(data), err)
  1389  			http.Error(w, err.Error(), 400)
  1390  			return
  1391  		}
  1392  
  1393  		if review.GetObjectKind().GroupVersionKind() != gvk("admission.k8s.io", "v1", "AdmissionReview") {
  1394  			err := fmt.Errorf("Invalid admission review kind: %#v", review.GetObjectKind().GroupVersionKind())
  1395  			t.Error(err)
  1396  			http.Error(w, err.Error(), 400)
  1397  			return
  1398  		}
  1399  
  1400  		if len(review.Request.Object.Raw) > 0 {
  1401  			u := &unstructured.Unstructured{Object: map[string]interface{}{}}
  1402  			if err := json.Unmarshal(review.Request.Object.Raw, u); err != nil {
  1403  				t.Errorf("Fail to deserialize object: %s with error: %v", string(review.Request.Object.Raw), err)
  1404  				http.Error(w, err.Error(), 400)
  1405  				return
  1406  			}
  1407  			review.Request.Object.Object = u
  1408  		}
  1409  		if len(review.Request.OldObject.Raw) > 0 {
  1410  			u := &unstructured.Unstructured{Object: map[string]interface{}{}}
  1411  			if err := json.Unmarshal(review.Request.OldObject.Raw, u); err != nil {
  1412  				t.Errorf("Fail to deserialize object: %s with error: %v", string(review.Request.OldObject.Raw), err)
  1413  				http.Error(w, err.Error(), 400)
  1414  				return
  1415  			}
  1416  			review.Request.OldObject.Object = u
  1417  		}
  1418  
  1419  		if len(review.Request.Options.Raw) > 0 {
  1420  			u := &unstructured.Unstructured{Object: map[string]interface{}{}}
  1421  			if err := json.Unmarshal(review.Request.Options.Raw, u); err != nil {
  1422  				t.Errorf("Fail to deserialize options object: %s for admission request %#+v with error: %v", string(review.Request.Options.Raw), review.Request, err)
  1423  				http.Error(w, err.Error(), 400)
  1424  				return
  1425  			}
  1426  			review.Request.Options.Object = u
  1427  		}
  1428  
  1429  		if review.Request.UserInfo.Username == testClientUsername {
  1430  			// only record requests originating from this integration test's client
  1431  			reviewRequest := &admissionRequest{
  1432  				Operation:   string(review.Request.Operation),
  1433  				Resource:    review.Request.Resource,
  1434  				SubResource: review.Request.SubResource,
  1435  				Namespace:   review.Request.Namespace,
  1436  				Name:        review.Request.Name,
  1437  				Object:      review.Request.Object,
  1438  				OldObject:   review.Request.OldObject,
  1439  				Options:     review.Request.Options,
  1440  			}
  1441  			holder.record("v1", phase, converted, reviewRequest)
  1442  		}
  1443  
  1444  		review.Response = &admissionreviewv1.AdmissionResponse{
  1445  			Allowed: true,
  1446  			UID:     review.Request.UID,
  1447  			Result:  &metav1.Status{Message: "admitted"},
  1448  
  1449  			// test plumbing warnings back
  1450  			Warnings: []string{makeWarning("v1", phase, converted)},
  1451  		}
  1452  		// If we're mutating, and have an object, return a patch to exercise conversion
  1453  		if phase == mutation && len(review.Request.Object.Raw) > 0 {
  1454  			review.Response.Patch = []byte(`[{"op":"add","path":"/bar","value":"test"}]`)
  1455  			jsonPatch := admissionreviewv1.PatchTypeJSONPatch
  1456  			review.Response.PatchType = &jsonPatch
  1457  		}
  1458  
  1459  		w.Header().Set("Content-Type", "application/json")
  1460  		if err := json.NewEncoder(w).Encode(review); err != nil {
  1461  			t.Errorf("Marshal of response failed with error: %v", err)
  1462  		}
  1463  	})
  1464  }
  1465  
  1466  func getTestFunc(gvr schema.GroupVersionResource, verb string) testFunc {
  1467  	if f, found := customTestFuncs[gvr][verb]; found {
  1468  		return f
  1469  	}
  1470  	if f, found := customTestFuncs[gvr]["*"]; found {
  1471  		return f
  1472  	}
  1473  	if strings.Contains(gvr.Resource, "/") {
  1474  		if f, found := defaultSubresourceFuncs[verb]; found {
  1475  			return f
  1476  		}
  1477  		return unimplemented
  1478  	}
  1479  	if f, found := defaultResourceFuncs[verb]; found {
  1480  		return f
  1481  	}
  1482  	return unimplemented
  1483  }
  1484  
  1485  func getStubObj(gvr schema.GroupVersionResource, resource metav1.APIResource) (*unstructured.Unstructured, error) {
  1486  	stub := ""
  1487  	if data, ok := etcd.GetEtcdStorageDataForNamespace(testNamespace)[gvr]; ok {
  1488  		stub = data.Stub
  1489  	}
  1490  	if data, ok := stubDataOverrides[gvr]; ok {
  1491  		stub = data
  1492  	}
  1493  	if len(stub) == 0 {
  1494  		return nil, fmt.Errorf("no stub data for %#v", gvr)
  1495  	}
  1496  
  1497  	stubObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
  1498  	if err := json.Unmarshal([]byte(stub), &stubObj.Object); err != nil {
  1499  		return nil, fmt.Errorf("error unmarshaling stub for %#v: %v", gvr, err)
  1500  	}
  1501  	return stubObj, nil
  1502  }
  1503  
  1504  func createOrGetResource(client dynamic.Interface, gvr schema.GroupVersionResource, resource metav1.APIResource) (*unstructured.Unstructured, error) {
  1505  	stubObj, err := getStubObj(gvr, resource)
  1506  	if err != nil {
  1507  		return nil, err
  1508  	}
  1509  	ns := ""
  1510  	if resource.Namespaced {
  1511  		ns = testNamespace
  1512  	}
  1513  	obj, err := client.Resource(gvr).Namespace(ns).Get(context.TODO(), stubObj.GetName(), metav1.GetOptions{})
  1514  	if err == nil {
  1515  		return obj, nil
  1516  	}
  1517  	if !apierrors.IsNotFound(err) {
  1518  		return nil, err
  1519  	}
  1520  	return client.Resource(gvr).Namespace(ns).Create(context.TODO(), stubObj, metav1.CreateOptions{})
  1521  }
  1522  
  1523  func gvr(group, version, resource string) schema.GroupVersionResource {
  1524  	return schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
  1525  }
  1526  func gvk(group, version, kind string) schema.GroupVersionKind {
  1527  	return schema.GroupVersionKind{Group: group, Version: version, Kind: kind}
  1528  }
  1529  
  1530  var (
  1531  	gvkCreateOptions = metav1.SchemeGroupVersion.WithKind("CreateOptions")
  1532  	gvkUpdateOptions = metav1.SchemeGroupVersion.WithKind("UpdateOptions")
  1533  	gvkDeleteOptions = metav1.SchemeGroupVersion.WithKind("DeleteOptions")
  1534  )
  1535  
  1536  func shouldTestResource(gvr schema.GroupVersionResource, resource metav1.APIResource) bool {
  1537  	return sets.NewString(resource.Verbs...).HasAny("create", "update", "patch", "connect", "delete", "deletecollection")
  1538  }
  1539  
  1540  func shouldTestResourceVerb(gvr schema.GroupVersionResource, resource metav1.APIResource, verb string) bool {
  1541  	return sets.NewString(resource.Verbs...).Has(verb)
  1542  }
  1543  
  1544  //
  1545  // webhook registration helpers
  1546  //
  1547  
  1548  func createV1beta1ValidationWebhook(etcdClient *clientv3.Client, etcdStoragePrefix string, client clientset.Interface, endpoint, convertedEndpoint string, convertedRules []admissionregistrationv1beta1.RuleWithOperations) error {
  1549  	fail := admissionregistrationv1beta1.Fail
  1550  	equivalent := admissionregistrationv1beta1.Equivalent
  1551  	webhookConfig := &admissionregistrationv1beta1.ValidatingWebhookConfiguration{
  1552  		ObjectMeta: metav1.ObjectMeta{Name: "admission.integration.test"},
  1553  		Webhooks: []admissionregistrationv1beta1.ValidatingWebhook{
  1554  			{
  1555  				Name: "admission.integration.test",
  1556  				ClientConfig: admissionregistrationv1beta1.WebhookClientConfig{
  1557  					URL:      &endpoint,
  1558  					CABundle: localhostCert,
  1559  				},
  1560  				Rules: []admissionregistrationv1beta1.RuleWithOperations{{
  1561  					Operations: []admissionregistrationv1beta1.OperationType{admissionregistrationv1beta1.OperationAll},
  1562  					Rule:       admissionregistrationv1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
  1563  				}},
  1564  				FailurePolicy:           &fail,
  1565  				AdmissionReviewVersions: []string{"v1beta1"},
  1566  			},
  1567  			{
  1568  				Name: "admission.integration.testconversion",
  1569  				ClientConfig: admissionregistrationv1beta1.WebhookClientConfig{
  1570  					URL:      &convertedEndpoint,
  1571  					CABundle: localhostCert,
  1572  				},
  1573  				Rules:                   convertedRules,
  1574  				FailurePolicy:           &fail,
  1575  				MatchPolicy:             &equivalent,
  1576  				AdmissionReviewVersions: []string{"v1beta1"},
  1577  			},
  1578  		},
  1579  	}
  1580  	// run through to get defaulting
  1581  	apisv1beta1.SetObjectDefaults_ValidatingWebhookConfiguration(webhookConfig)
  1582  	webhookConfig.TypeMeta.Kind = "ValidatingWebhookConfiguration"
  1583  	webhookConfig.TypeMeta.APIVersion = "admissionregistration.k8s.io/v1beta1"
  1584  
  1585  	// Attaching Mutation webhook to API server
  1586  	ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceNone)
  1587  	key := path.Join("/", etcdStoragePrefix, "validatingwebhookconfigurations", webhookConfig.Name)
  1588  	val, _ := json.Marshal(webhookConfig)
  1589  	if _, err := etcdClient.Put(ctx, key, string(val)); err != nil {
  1590  		return err
  1591  	}
  1592  
  1593  	// make sure we can get the webhook
  1594  	if _, err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Get(context.TODO(), webhookConfig.Name, metav1.GetOptions{}); err != nil {
  1595  		return err
  1596  	}
  1597  
  1598  	return nil
  1599  }
  1600  
  1601  func createV1beta1MutationWebhook(etcdClient *clientv3.Client, etcdStoragePrefix string, client clientset.Interface, endpoint, convertedEndpoint string, convertedRules []admissionregistrationv1beta1.RuleWithOperations) error {
  1602  	fail := admissionregistrationv1beta1.Fail
  1603  	equivalent := admissionregistrationv1beta1.Equivalent
  1604  	webhookConfig := &admissionregistrationv1beta1.MutatingWebhookConfiguration{
  1605  		ObjectMeta: metav1.ObjectMeta{Name: "mutation.integration.test"},
  1606  		Webhooks: []admissionregistrationv1beta1.MutatingWebhook{
  1607  			{
  1608  				Name: "mutation.integration.test",
  1609  				ClientConfig: admissionregistrationv1beta1.WebhookClientConfig{
  1610  					URL:      &endpoint,
  1611  					CABundle: localhostCert,
  1612  				},
  1613  				Rules: []admissionregistrationv1beta1.RuleWithOperations{{
  1614  					Operations: []admissionregistrationv1beta1.OperationType{admissionregistrationv1beta1.OperationAll},
  1615  					Rule:       admissionregistrationv1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
  1616  				}},
  1617  				FailurePolicy:           &fail,
  1618  				AdmissionReviewVersions: []string{"v1beta1"},
  1619  			},
  1620  			{
  1621  				Name: "mutation.integration.testconversion",
  1622  				ClientConfig: admissionregistrationv1beta1.WebhookClientConfig{
  1623  					URL:      &convertedEndpoint,
  1624  					CABundle: localhostCert,
  1625  				},
  1626  				Rules:                   convertedRules,
  1627  				FailurePolicy:           &fail,
  1628  				MatchPolicy:             &equivalent,
  1629  				AdmissionReviewVersions: []string{"v1beta1"},
  1630  			},
  1631  		},
  1632  	}
  1633  	// run through to get defaulting
  1634  	apisv1beta1.SetObjectDefaults_MutatingWebhookConfiguration(webhookConfig)
  1635  	webhookConfig.TypeMeta.Kind = "MutatingWebhookConfiguration"
  1636  	webhookConfig.TypeMeta.APIVersion = "admissionregistration.k8s.io/v1beta1"
  1637  
  1638  	// Attaching Mutation webhook to API server
  1639  	ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceNone)
  1640  	key := path.Join("/", etcdStoragePrefix, "mutatingwebhookconfigurations", webhookConfig.Name)
  1641  	val, _ := json.Marshal(webhookConfig)
  1642  	if _, err := etcdClient.Put(ctx, key, string(val)); err != nil {
  1643  		return err
  1644  	}
  1645  
  1646  	// make sure we can get the webhook
  1647  	if _, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Get(context.TODO(), webhookConfig.Name, metav1.GetOptions{}); err != nil {
  1648  		return err
  1649  	}
  1650  
  1651  	return nil
  1652  }
  1653  
  1654  func createV1ValidationWebhook(client clientset.Interface, endpoint, convertedEndpoint string, convertedRules []admissionregistrationv1.RuleWithOperations) error {
  1655  	fail := admissionregistrationv1.Fail
  1656  	equivalent := admissionregistrationv1.Equivalent
  1657  	none := admissionregistrationv1.SideEffectClassNone
  1658  	// Attaching Admission webhook to API server
  1659  	_, err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(context.TODO(), &admissionregistrationv1.ValidatingWebhookConfiguration{
  1660  		ObjectMeta: metav1.ObjectMeta{Name: "admissionregistrationv1.integration.test"},
  1661  		Webhooks: []admissionregistrationv1.ValidatingWebhook{
  1662  			{
  1663  				Name: "admissionregistrationv1.integration.test",
  1664  				ClientConfig: admissionregistrationv1.WebhookClientConfig{
  1665  					URL:      &endpoint,
  1666  					CABundle: localhostCert,
  1667  				},
  1668  				Rules: []admissionregistrationv1.RuleWithOperations{{
  1669  					Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
  1670  					Rule:       admissionregistrationv1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
  1671  				}},
  1672  				FailurePolicy:           &fail,
  1673  				AdmissionReviewVersions: []string{"v1", "v1beta1"},
  1674  				SideEffects:             &none,
  1675  			},
  1676  			{
  1677  				Name: "admissionregistrationv1.integration.testconversion",
  1678  				ClientConfig: admissionregistrationv1.WebhookClientConfig{
  1679  					URL:      &convertedEndpoint,
  1680  					CABundle: localhostCert,
  1681  				},
  1682  				Rules:                   convertedRules,
  1683  				FailurePolicy:           &fail,
  1684  				MatchPolicy:             &equivalent,
  1685  				AdmissionReviewVersions: []string{"v1", "v1beta1"},
  1686  				SideEffects:             &none,
  1687  			},
  1688  		},
  1689  	}, metav1.CreateOptions{})
  1690  	return err
  1691  }
  1692  
  1693  func createV1MutationWebhook(client clientset.Interface, endpoint, convertedEndpoint string, convertedRules []admissionregistrationv1.RuleWithOperations) error {
  1694  	fail := admissionregistrationv1.Fail
  1695  	equivalent := admissionregistrationv1.Equivalent
  1696  	none := admissionregistrationv1.SideEffectClassNone
  1697  	// Attaching Mutation webhook to API server
  1698  	_, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), &admissionregistrationv1.MutatingWebhookConfiguration{
  1699  		ObjectMeta: metav1.ObjectMeta{Name: "mutationv1.integration.test"},
  1700  		Webhooks: []admissionregistrationv1.MutatingWebhook{
  1701  			{
  1702  				Name: "mutationv1.integration.test",
  1703  				ClientConfig: admissionregistrationv1.WebhookClientConfig{
  1704  					URL:      &endpoint,
  1705  					CABundle: localhostCert,
  1706  				},
  1707  				Rules: []admissionregistrationv1.RuleWithOperations{{
  1708  					Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
  1709  					Rule:       admissionregistrationv1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
  1710  				}},
  1711  				FailurePolicy:           &fail,
  1712  				AdmissionReviewVersions: []string{"v1", "v1beta1"},
  1713  				SideEffects:             &none,
  1714  			},
  1715  			{
  1716  				Name: "mutationv1.integration.testconversion",
  1717  				ClientConfig: admissionregistrationv1.WebhookClientConfig{
  1718  					URL:      &convertedEndpoint,
  1719  					CABundle: localhostCert,
  1720  				},
  1721  				Rules:                   convertedRules,
  1722  				FailurePolicy:           &fail,
  1723  				MatchPolicy:             &equivalent,
  1724  				AdmissionReviewVersions: []string{"v1", "v1beta1"},
  1725  				SideEffects:             &none,
  1726  			},
  1727  		},
  1728  	}, metav1.CreateOptions{})
  1729  	return err
  1730  }
  1731  
  1732  // localhostCert was generated from crypto/tls/generate_cert.go with the following command:
  1733  //
  1734  //	go run generate_cert.go  --rsa-bits 2048 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
  1735  var localhostCert = []byte(`-----BEGIN CERTIFICATE-----
  1736  MIIDGDCCAgCgAwIBAgIQTKCKn99d5HhQVCLln2Q+eTANBgkqhkiG9w0BAQsFADAS
  1737  MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
  1738  MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
  1739  MIIBCgKCAQEA1Z5/aTwqY706M34tn60l8ZHkanWDl8mM1pYf4Q7qg3zA9XqWLX6S
  1740  4rTYDYCb4stEasC72lQnbEWHbthiQE76zubP8WOFHdvGR3mjAvHWz4FxvLOTheZ+
  1741  3iDUrl6Aj9UIsYqzmpBJAoY4+vGGf+xHvuukHrVcFqR9ZuBdZuJ/HbbjUyuNr3X9
  1742  erNIr5Ha17gVzf17SNbYgNrX9gbCeEB8Z9Ox7dVuJhLDkpF0T/B5Zld3BjyUVY/T
  1743  cukU4dTVp6isbWPvCMRCZCCOpb+qIhxEjJ0n6tnPt8nf9lvDl4SWMl6X1bH+2EFa
  1744  a8R06G0QI+XhwPyjXUyCR8QEOZPCR5wyqQIDAQABo2gwZjAOBgNVHQ8BAf8EBAMC
  1745  AqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAuBgNVHREE
  1746  JzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG
  1747  9w0BAQsFAAOCAQEAThqgJ/AFqaANsOp48lojDZfZBFxJQ3A4zfR/MgggUoQ9cP3V
  1748  rxuKAFWQjze1EZc7J9iO1WvH98lOGVNRY/t2VIrVoSsBiALP86Eew9WucP60tbv2
  1749  8/zsBDSfEo9Wl+Q/gwdEh8dgciUKROvCm76EgAwPGicMAgRsxXgwXHhS5e8nnbIE
  1750  Ewaqvb5dY++6kh0Oz+adtNT5OqOwXTIRI67WuEe6/B3Z4LNVPQDIj7ZUJGNw8e6L
  1751  F4nkUthwlKx4yEJHZBRuFPnO7Z81jNKuwL276+mczRH7piI6z9uyMV/JbEsOIxyL
  1752  W6CzB7pZ9Nj1YLpgzc1r6oONHLokMJJIz/IvkQ==
  1753  -----END CERTIFICATE-----`)
  1754  
  1755  // localhostKey is the private key for localhostCert.
  1756  var localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
  1757  MIIEowIBAAKCAQEA1Z5/aTwqY706M34tn60l8ZHkanWDl8mM1pYf4Q7qg3zA9XqW
  1758  LX6S4rTYDYCb4stEasC72lQnbEWHbthiQE76zubP8WOFHdvGR3mjAvHWz4FxvLOT
  1759  heZ+3iDUrl6Aj9UIsYqzmpBJAoY4+vGGf+xHvuukHrVcFqR9ZuBdZuJ/HbbjUyuN
  1760  r3X9erNIr5Ha17gVzf17SNbYgNrX9gbCeEB8Z9Ox7dVuJhLDkpF0T/B5Zld3BjyU
  1761  VY/TcukU4dTVp6isbWPvCMRCZCCOpb+qIhxEjJ0n6tnPt8nf9lvDl4SWMl6X1bH+
  1762  2EFaa8R06G0QI+XhwPyjXUyCR8QEOZPCR5wyqQIDAQABAoIBAFAJmb1pMIy8OpFO
  1763  hnOcYWoYepe0vgBiIOXJy9n8R7vKQ1X2f0w+b3SHw6eTd1TLSjAhVIEiJL85cdwD
  1764  MRTdQrXA30qXOioMzUa8eWpCCHUpD99e/TgfO4uoi2dluw+pBx/WUyLnSqOqfLDx
  1765  S66kbeFH0u86jm1hZibki7pfxLbxvu7KQgPe0meO5/13Retztz7/xa/pWIY71Zqd
  1766  YC8UckuQdWUTxfuQf0470lAK34GZlDy9tvdVOG/PmNkG4j6OQjy0Kmz4Uk7rewKo
  1767  ZbdphaLPJ2A4Rdqfn4WCoyDnxlfV861T922/dEDZEbNWiQpB81G8OfLL+FLHxyIT
  1768  LKEu4R0CgYEA4RDj9jatJ/wGkMZBt+UF05mcJlRVMEijqdKgFwR2PP8b924Ka1mj
  1769  9zqWsfbxQbdPdwsCeVBZrSlTEmuFSQLeWtqBxBKBTps/tUP0qZf7HjfSmcVI89WE
  1770  3ab8LFjfh4PtK/LOq2D1GRZZkFliqi0gKwYdDoK6gxXWwrumXq4c2l8CgYEA8vrX
  1771  dMuGCNDjNQkGXx3sr8pyHCDrSNR4Z4FrSlVUkgAW1L7FrCM911BuGh86FcOu9O/1
  1772  Ggo0E8ge7qhQiXhB5vOo7hiVzSp0FxxCtGSlpdp4W6wx6ZWK8+Pc+6Moos03XdG7
  1773  MKsdPGDciUn9VMOP3r8huX/btFTh90C/L50sH/cCgYAd02wyW8qUqux/0RYydZJR
  1774  GWE9Hx3u+SFfRv9aLYgxyyj8oEOXOFjnUYdY7D3KlK1ePEJGq2RG81wD6+XM6Clp
  1775  Zt2di0pBjYdi0S+iLfbkaUdqg1+ImLoz2YY/pkNxJQWQNmw2//FbMsAJxh6yKKrD
  1776  qNq+6oonBwTf55hDodVHBwKBgEHgEBnyM9ygBXmTgM645jqiwF0v75pHQH2PcO8u
  1777  Q0dyDr6PGjiZNWLyw2cBoFXWP9DYXbM5oPTcBMbfizY6DGP5G4uxzqtZHzBE0TDn
  1778  OKHGoWr5PG7/xDRrSrZOfe3lhWVCP2XqfnqoKCJwlOYuPws89n+8UmyJttm6DBt0
  1779  mUnxAoGBAIvbR87ZFXkvqstLs4KrdqTz4TQIcpzB3wENukHODPA6C1gzWTqp+OEe
  1780  GMNltPfGCLO+YmoMQuTpb0kECYV3k4jR3gXO6YvlL9KbY+UOA6P0dDX4ROi2Rklj
  1781  yh+lxFLYa1vlzzi9r8B7nkR9hrOGMvkfXF42X89g7lx4uMtu2I4q
  1782  -----END RSA PRIVATE KEY-----`)