k8s.io/kubernetes@v1.29.3/test/integration/apiserver/cel/admission_test_util.go (about)

     1  /*
     2  Copyright 2023 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 cel
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"strings"
    24  	"sync"
    25  	"testing"
    26  	"time"
    27  
    28  	"k8s.io/api/admission/v1beta1"
    29  	appsv1beta1 "k8s.io/api/apps/v1beta1"
    30  	authenticationv1 "k8s.io/api/authentication/v1"
    31  	corev1 "k8s.io/api/core/v1"
    32  	extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
    33  	policyv1 "k8s.io/api/policy/v1"
    34  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    35  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    36  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    37  	"k8s.io/apimachinery/pkg/runtime"
    38  	"k8s.io/apimachinery/pkg/runtime/schema"
    39  	"k8s.io/apimachinery/pkg/types"
    40  	"k8s.io/apimachinery/pkg/util/sets"
    41  	"k8s.io/apimachinery/pkg/util/wait"
    42  	"k8s.io/client-go/dynamic"
    43  	clientset "k8s.io/client-go/kubernetes"
    44  	"k8s.io/client-go/util/retry"
    45  	"k8s.io/kubernetes/test/integration/etcd"
    46  )
    47  
    48  // Admission test framework copied from test/integration/apiserver/admissionwebhook/admission_test.go
    49  //
    50  // All differences between two are minor and called out in comments prefixed with
    51  // "DIFF"
    52  
    53  const (
    54  	testNamespace      = "webhook-integration"
    55  	testClientUsername = "webhook-integration-client"
    56  
    57  	mutation   = "mutation"
    58  	validation = "validation"
    59  )
    60  
    61  // DIFF: Added interface to replace direct *holder usage in testContext to be
    62  // able to inject a policy-specific holder
    63  type admissionTestExpectationHolder interface {
    64  	reset(t *testing.T)
    65  	expect(gvr schema.GroupVersionResource, gvk, optionsGVK schema.GroupVersionKind, operation v1beta1.Operation, name, namespace string, object, oldObject, options bool)
    66  	verify(t *testing.T)
    67  }
    68  
    69  type testContext struct {
    70  	t *testing.T
    71  
    72  	// DIFF: Changed from *holder to interface
    73  	admissionHolder admissionTestExpectationHolder
    74  
    75  	client    dynamic.Interface
    76  	clientset clientset.Interface
    77  	verb      string
    78  	gvr       schema.GroupVersionResource
    79  	resource  metav1.APIResource
    80  	resources map[schema.GroupVersionResource]metav1.APIResource
    81  }
    82  
    83  type testFunc func(*testContext)
    84  
    85  var (
    86  	// defaultResourceFuncs holds the default test functions.
    87  	// may be overridden for specific resources by customTestFuncs.
    88  	defaultResourceFuncs = map[string]testFunc{
    89  		"create":           testResourceCreate,
    90  		"update":           testResourceUpdate,
    91  		"patch":            testResourcePatch,
    92  		"delete":           testResourceDelete,
    93  		"deletecollection": testResourceDeletecollection,
    94  	}
    95  
    96  	// defaultSubresourceFuncs holds default subresource test functions.
    97  	// may be overridden for specific resources by customTestFuncs.
    98  	defaultSubresourceFuncs = map[string]testFunc{
    99  		"update": testSubresourceUpdate,
   100  		"patch":  testSubresourcePatch,
   101  	}
   102  
   103  	// customTestFuncs holds custom test functions by resource and verb.
   104  	customTestFuncs = map[schema.GroupVersionResource]map[string]testFunc{
   105  		gvr("", "v1", "namespaces"): {"delete": testNamespaceDelete},
   106  
   107  		gvr("apps", "v1beta1", "deployments/rollback"):       {"create": testDeploymentRollback},
   108  		gvr("extensions", "v1beta1", "deployments/rollback"): {"create": testDeploymentRollback},
   109  
   110  		gvr("", "v1", "pods/attach"):      {"create": testPodConnectSubresource},
   111  		gvr("", "v1", "pods/exec"):        {"create": testPodConnectSubresource},
   112  		gvr("", "v1", "pods/portforward"): {"create": testPodConnectSubresource},
   113  
   114  		gvr("", "v1", "bindings"):      {"create": testPodBindingEviction},
   115  		gvr("", "v1", "pods/binding"):  {"create": testPodBindingEviction},
   116  		gvr("", "v1", "pods/eviction"): {"create": testPodBindingEviction},
   117  
   118  		gvr("", "v1", "nodes/proxy"):    {"*": testSubresourceProxy},
   119  		gvr("", "v1", "pods/proxy"):     {"*": testSubresourceProxy},
   120  		gvr("", "v1", "services/proxy"): {"*": testSubresourceProxy},
   121  
   122  		gvr("", "v1", "serviceaccounts/token"): {"create": testTokenCreate},
   123  
   124  		gvr("random.numbers.com", "v1", "integers"): {"create": testPruningRandomNumbers},
   125  
   126  		// DIFF: This test is used for webhook test but disabled here until we have mutating
   127  		// admission policy to write to "foo" field
   128  		// gvr("custom.fancy.com", "v2", "pants"):      {"create": testNoPruningCustomFancy},
   129  	}
   130  
   131  	// admissionExemptResources lists objects which are exempt from admission validation/mutation,
   132  	// only resources exempted from admission processing by API server should be listed here.
   133  	admissionExemptResources = map[schema.GroupVersionResource]bool{
   134  		// DIFF: WebhookConfigurations are exempt for webhook admission but not
   135  		// for policy admission.
   136  		// gvr("admissionregistration.k8s.io", "v1beta1", "mutatingwebhookconfigurations"):       true,
   137  		// gvr("admissionregistration.k8s.io", "v1beta1", "validatingwebhookconfigurations"):     true,
   138  		// gvr("admissionregistration.k8s.io", "v1", "mutatingwebhookconfigurations"):            true,
   139  		// gvr("admissionregistration.k8s.io", "v1", "validatingwebhookconfigurations"):          true,
   140  		gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"):        true,
   141  		gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies/status"): true,
   142  		gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicybindings"):  true,
   143  		gvr("admissionregistration.k8s.io", "v1beta1", "validatingadmissionpolicies"):         true,
   144  		gvr("admissionregistration.k8s.io", "v1beta1", "validatingadmissionpolicies/status"):  true,
   145  		gvr("admissionregistration.k8s.io", "v1beta1", "validatingadmissionpolicybindings"):   true,
   146  	}
   147  
   148  	parentResources = map[schema.GroupVersionResource]schema.GroupVersionResource{
   149  		gvr("extensions", "v1beta1", "replicationcontrollers/scale"): gvr("", "v1", "replicationcontrollers"),
   150  	}
   151  
   152  	// stubDataOverrides holds either non persistent resources' definitions or resources where default stub needs to be overridden.
   153  	stubDataOverrides = map[schema.GroupVersionResource]string{
   154  		// Non persistent Reviews resource
   155  		gvr("authentication.k8s.io", "v1", "tokenreviews"):                  `{"metadata": {"name": "tokenreview"}, "spec": {"token": "token", "audience": ["audience1","audience2"]}}`,
   156  		gvr("authentication.k8s.io", "v1beta1", "tokenreviews"):             `{"metadata": {"name": "tokenreview"}, "spec": {"token": "token", "audience": ["audience1","audience2"]}}`,
   157  		gvr("authentication.k8s.io", "v1alpha1", "selfsubjectreviews"):      `{"metadata": {"name": "SelfSubjectReview"},"status":{"userInfo":{}}}`,
   158  		gvr("authentication.k8s.io", "v1beta1", "selfsubjectreviews"):       `{"metadata": {"name": "SelfSubjectReview"},"status":{"userInfo":{}}}`,
   159  		gvr("authentication.k8s.io", "v1", "selfsubjectreviews"):            `{"metadata": {"name": "SelfSubjectReview"},"status":{"userInfo":{}}}`,
   160  		gvr("authorization.k8s.io", "v1", "localsubjectaccessreviews"):      `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"uid": "token", "user": "user1","groups": ["group1","group2"],"resourceAttributes": {"name":"name1","namespace":"` + testNamespace + `"}}}`,
   161  		gvr("authorization.k8s.io", "v1", "subjectaccessreviews"):           `{"metadata": {"name": "", "namespace":""}, "spec": {"user":"user1","resourceAttributes": {"name":"name1", "namespace":"` + testNamespace + `"}}}`,
   162  		gvr("authorization.k8s.io", "v1", "selfsubjectaccessreviews"):       `{"metadata": {"name": "", "namespace":""}, "spec": {"resourceAttributes": {"name":"name1", "namespace":""}}}`,
   163  		gvr("authorization.k8s.io", "v1", "selfsubjectrulesreviews"):        `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"namespace":"` + testNamespace + `"}}`,
   164  		gvr("authorization.k8s.io", "v1beta1", "localsubjectaccessreviews"): `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"uid": "token", "user": "user1","groups": ["group1","group2"],"resourceAttributes": {"name":"name1","namespace":"` + testNamespace + `"}}}`,
   165  		gvr("authorization.k8s.io", "v1beta1", "subjectaccessreviews"):      `{"metadata": {"name": "", "namespace":""}, "spec": {"user":"user1","resourceAttributes": {"name":"name1", "namespace":"` + testNamespace + `"}}}`,
   166  		gvr("authorization.k8s.io", "v1beta1", "selfsubjectaccessreviews"):  `{"metadata": {"name": "", "namespace":""}, "spec": {"resourceAttributes": {"name":"name1", "namespace":""}}}`,
   167  		gvr("authorization.k8s.io", "v1beta1", "selfsubjectrulesreviews"):   `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"namespace":"` + testNamespace + `"}}`,
   168  
   169  		// Other Non persistent resources
   170  	}
   171  )
   172  
   173  type webhookOptions struct {
   174  	version string
   175  
   176  	// phase indicates whether this is a mutating or validating webhook
   177  	phase string
   178  	// converted indicates if this webhook makes use of matchPolicy:equivalent and expects conversion.
   179  	// if true, recordGVR and expectGVK are mapped through gvrToConvertedGVR/gvrToConvertedGVK.
   180  	// if false, recordGVR and expectGVK are compared directly to the admission review.
   181  	converted bool
   182  }
   183  
   184  type holder struct {
   185  	lock sync.RWMutex
   186  
   187  	t *testing.T
   188  
   189  	// DIFF: Warning handler removed in policy test.
   190  	// warningHandler *warningHandler
   191  
   192  	recordGVR       metav1.GroupVersionResource
   193  	recordOperation string
   194  	recordNamespace string
   195  	recordName      string
   196  
   197  	expectGVK        schema.GroupVersionKind
   198  	expectObject     bool
   199  	expectOldObject  bool
   200  	expectOptionsGVK schema.GroupVersionKind
   201  	expectOptions    bool
   202  
   203  	// gvrToConvertedGVR maps the GVR submitted to the API server to the expected GVR when converted to the webhook-recognized resource.
   204  	// When a converted request is recorded, gvrToConvertedGVR[recordGVR] is compared to the GVR seen by the webhook.
   205  	gvrToConvertedGVR map[metav1.GroupVersionResource]metav1.GroupVersionResource
   206  	// gvrToConvertedGVR maps the GVR submitted to the API server to the expected GVK when converted to the webhook-recognized resource.
   207  	// When a converted request is recorded, gvrToConvertedGVR[expectGVK] is compared to the GVK seen by the webhook.
   208  	gvrToConvertedGVK map[metav1.GroupVersionResource]schema.GroupVersionKind
   209  
   210  	recorded map[webhookOptions]*admissionRequest
   211  }
   212  
   213  func (h *holder) reset(t *testing.T) {
   214  	h.lock.Lock()
   215  	defer h.lock.Unlock()
   216  	h.t = t
   217  	h.recordGVR = metav1.GroupVersionResource{}
   218  	h.expectGVK = schema.GroupVersionKind{}
   219  	h.recordOperation = ""
   220  	h.recordName = ""
   221  	h.recordNamespace = ""
   222  	h.expectObject = false
   223  	h.expectOldObject = false
   224  	h.expectOptionsGVK = schema.GroupVersionKind{}
   225  	h.expectOptions = false
   226  	// DIFF: Warning handler removed
   227  	// h.warningHandler.reset()
   228  
   229  	// Set up the recorded map with nil records for all combinations
   230  	h.recorded = map[webhookOptions]*admissionRequest{}
   231  	for _, phase := range []string{mutation, validation} {
   232  		for _, converted := range []bool{true, false} {
   233  			for _, version := range []string{"v1", "v1beta1"} {
   234  				h.recorded[webhookOptions{version: version, phase: phase, converted: converted}] = nil
   235  			}
   236  		}
   237  	}
   238  }
   239  func (h *holder) expect(gvr schema.GroupVersionResource, gvk, optionsGVK schema.GroupVersionKind, operation v1beta1.Operation, name, namespace string, object, oldObject, options bool) {
   240  	// Special-case namespaces, since the object name shows up in request attributes
   241  	if len(namespace) == 0 && gvk.Group == "" && gvk.Version == "v1" && gvk.Kind == "Namespace" {
   242  		namespace = name
   243  	}
   244  
   245  	h.lock.Lock()
   246  	defer h.lock.Unlock()
   247  	h.recordGVR = metav1.GroupVersionResource{Group: gvr.Group, Version: gvr.Version, Resource: gvr.Resource}
   248  	h.expectGVK = gvk
   249  	h.recordOperation = string(operation)
   250  	h.recordName = name
   251  	h.recordNamespace = namespace
   252  	h.expectObject = object
   253  	h.expectOldObject = oldObject
   254  	h.expectOptionsGVK = optionsGVK
   255  	h.expectOptions = options
   256  	// DIFF: Warning handler removed
   257  	// h.warningHandler.reset()
   258  
   259  	// Set up the recorded map with nil records for all combinations
   260  	h.recorded = map[webhookOptions]*admissionRequest{}
   261  	for _, phase := range []string{mutation, validation} {
   262  		for _, converted := range []bool{true, false} {
   263  			for _, version := range []string{"v1", "v1beta1"} {
   264  				h.recorded[webhookOptions{version: version, phase: phase, converted: converted}] = nil
   265  			}
   266  		}
   267  	}
   268  }
   269  
   270  type admissionRequest struct {
   271  	Operation   string
   272  	Resource    metav1.GroupVersionResource
   273  	SubResource string
   274  	Namespace   string
   275  	Name        string
   276  	Object      runtime.RawExtension
   277  	OldObject   runtime.RawExtension
   278  	Options     runtime.RawExtension
   279  }
   280  
   281  func (h *holder) record(version string, phase string, converted bool, request *admissionRequest) {
   282  	h.lock.Lock()
   283  	defer h.lock.Unlock()
   284  
   285  	// this is useful to turn on if items aren't getting recorded and you need to figure out why
   286  	debug := false
   287  	if debug {
   288  		h.t.Logf("%s %#v %v", request.Operation, request.Resource, request.SubResource)
   289  	}
   290  
   291  	resource := request.Resource
   292  	if len(request.SubResource) > 0 {
   293  		resource.Resource += "/" + request.SubResource
   294  	}
   295  
   296  	// See if we should record this
   297  	gvrToRecord := h.recordGVR
   298  	if converted {
   299  		// If this is a converted webhook, map to the GVR we expect the webhook to see
   300  		gvrToRecord = h.gvrToConvertedGVR[h.recordGVR]
   301  	}
   302  	if resource != gvrToRecord {
   303  		if debug {
   304  			h.t.Log(resource, "!=", gvrToRecord)
   305  		}
   306  		return
   307  	}
   308  
   309  	if request.Operation != h.recordOperation {
   310  		if debug {
   311  			h.t.Log(request.Operation, "!=", h.recordOperation)
   312  		}
   313  		return
   314  	}
   315  	if request.Namespace != h.recordNamespace {
   316  		if debug {
   317  			h.t.Log(request.Namespace, "!=", h.recordNamespace)
   318  		}
   319  		return
   320  	}
   321  
   322  	name := request.Name
   323  	if name != h.recordName {
   324  		if debug {
   325  			h.t.Log(name, "!=", h.recordName)
   326  		}
   327  		return
   328  	}
   329  
   330  	if debug {
   331  		h.t.Logf("recording: %#v = %s %#v %v", webhookOptions{version: version, phase: phase, converted: converted}, request.Operation, request.Resource, request.SubResource)
   332  	}
   333  	h.recorded[webhookOptions{version: version, phase: phase, converted: converted}] = request
   334  }
   335  
   336  func (h *holder) verify(t *testing.T) {
   337  	h.lock.Lock()
   338  	defer h.lock.Unlock()
   339  
   340  	for options, value := range h.recorded {
   341  		if err := h.verifyRequest(options, value); err != nil {
   342  			t.Errorf("version: %v, phase:%v, converted:%v error: %v", options.version, options.phase, options.converted, err)
   343  		}
   344  	}
   345  }
   346  
   347  func (h *holder) verifyRequest(webhookOptions webhookOptions, request *admissionRequest) error {
   348  	converted := webhookOptions.converted
   349  
   350  	// Check if current resource should be exempted from Admission processing
   351  	if admissionExemptResources[gvr(h.recordGVR.Group, h.recordGVR.Version, h.recordGVR.Resource)] {
   352  		if request == nil {
   353  			return nil
   354  		}
   355  		return fmt.Errorf("admission webhook was called, but not supposed to")
   356  	}
   357  
   358  	if request == nil {
   359  		return fmt.Errorf("no request received")
   360  	}
   361  
   362  	if h.expectObject {
   363  		if err := h.verifyObject(converted, request.Object.Object); err != nil {
   364  			return fmt.Errorf("object error: %v", err)
   365  		}
   366  	} else if request.Object.Object != nil {
   367  		return fmt.Errorf("unexpected object: %#v", request.Object.Object)
   368  	}
   369  
   370  	if h.expectOldObject {
   371  		if err := h.verifyObject(converted, request.OldObject.Object); err != nil {
   372  			return fmt.Errorf("old object error: %v", err)
   373  		}
   374  	} else if request.OldObject.Object != nil {
   375  		return fmt.Errorf("unexpected old object: %#v", request.OldObject.Object)
   376  	}
   377  
   378  	if h.expectOptions {
   379  		if err := h.verifyOptions(request.Options.Object); err != nil {
   380  			return fmt.Errorf("options error: %v", err)
   381  		}
   382  	} else if request.Options.Object != nil {
   383  		return fmt.Errorf("unexpected options: %#v", request.Options.Object)
   384  	}
   385  
   386  	// DIFF: This check was removed for policy tests since it only applies
   387  	// to webhook
   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  func getTestFunc(gvr schema.GroupVersionResource, verb string) testFunc {
   420  	if f, found := customTestFuncs[gvr][verb]; found {
   421  		return f
   422  	}
   423  	if f, found := customTestFuncs[gvr]["*"]; found {
   424  		return f
   425  	}
   426  	if strings.Contains(gvr.Resource, "/") {
   427  		if f, found := defaultSubresourceFuncs[verb]; found {
   428  			return f
   429  		}
   430  		return unimplemented
   431  	}
   432  	if f, found := defaultResourceFuncs[verb]; found {
   433  		return f
   434  	}
   435  	return unimplemented
   436  }
   437  
   438  func getStubObj(gvr schema.GroupVersionResource, resource metav1.APIResource) (*unstructured.Unstructured, error) {
   439  	stub := ""
   440  	if data, ok := etcd.GetEtcdStorageDataForNamespace(testNamespace)[gvr]; ok {
   441  		stub = data.Stub
   442  	}
   443  	if data, ok := stubDataOverrides[gvr]; ok {
   444  		stub = data
   445  	}
   446  	if len(stub) == 0 {
   447  		return nil, fmt.Errorf("no stub data for %#v", gvr)
   448  	}
   449  
   450  	stubObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
   451  	if err := json.Unmarshal([]byte(stub), &stubObj.Object); err != nil {
   452  		return nil, fmt.Errorf("error unmarshaling stub for %#v: %v", gvr, err)
   453  	}
   454  	return stubObj, nil
   455  }
   456  
   457  func createOrGetResource(client dynamic.Interface, gvr schema.GroupVersionResource, resource metav1.APIResource) (*unstructured.Unstructured, error) {
   458  	stubObj, err := getStubObj(gvr, resource)
   459  	if err != nil {
   460  		return nil, err
   461  	}
   462  	ns := ""
   463  	if resource.Namespaced {
   464  		ns = testNamespace
   465  	}
   466  	obj, err := client.Resource(gvr).Namespace(ns).Get(context.TODO(), stubObj.GetName(), metav1.GetOptions{})
   467  	if err == nil {
   468  		return obj, nil
   469  	}
   470  	if !apierrors.IsNotFound(err) {
   471  		return nil, err
   472  	}
   473  	return client.Resource(gvr).Namespace(ns).Create(context.TODO(), stubObj, metav1.CreateOptions{})
   474  }
   475  
   476  func gvr(group, version, resource string) schema.GroupVersionResource {
   477  	return schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
   478  }
   479  func gvk(group, version, kind string) schema.GroupVersionKind {
   480  	return schema.GroupVersionKind{Group: group, Version: version, Kind: kind}
   481  }
   482  
   483  var (
   484  	gvkCreateOptions = metav1.SchemeGroupVersion.WithKind("CreateOptions")
   485  	gvkUpdateOptions = metav1.SchemeGroupVersion.WithKind("UpdateOptions")
   486  	gvkDeleteOptions = metav1.SchemeGroupVersion.WithKind("DeleteOptions")
   487  )
   488  
   489  func shouldTestResource(gvr schema.GroupVersionResource, resource metav1.APIResource) bool {
   490  	return sets.NewString(resource.Verbs...).HasAny("create", "update", "patch", "connect", "delete", "deletecollection")
   491  }
   492  
   493  func shouldTestResourceVerb(gvr schema.GroupVersionResource, resource metav1.APIResource, verb string) bool {
   494  	return sets.NewString(resource.Verbs...).Has(verb)
   495  }
   496  
   497  //
   498  // generic resource testing
   499  //
   500  
   501  func testResourceCreate(c *testContext) {
   502  	stubObj, err := getStubObj(c.gvr, c.resource)
   503  	if err != nil {
   504  		c.t.Error(err)
   505  		return
   506  	}
   507  	ns := ""
   508  	if c.resource.Namespaced {
   509  		ns = testNamespace
   510  	}
   511  	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkCreateOptions, v1beta1.Create, stubObj.GetName(), ns, true, false, true)
   512  	_, err = c.client.Resource(c.gvr).Namespace(ns).Create(context.TODO(), stubObj, metav1.CreateOptions{})
   513  	if err != nil {
   514  		c.t.Error(err)
   515  		return
   516  	}
   517  }
   518  
   519  func testResourceUpdate(c *testContext) {
   520  	if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
   521  		obj, err := createOrGetResource(c.client, c.gvr, c.resource)
   522  		if err != nil {
   523  			return err
   524  		}
   525  		obj.SetAnnotations(map[string]string{"update": "true"})
   526  		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)
   527  		_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Update(context.TODO(), obj, metav1.UpdateOptions{})
   528  		return err
   529  	}); err != nil {
   530  		c.t.Error(err)
   531  		return
   532  	}
   533  }
   534  
   535  func testResourcePatch(c *testContext) {
   536  	obj, err := createOrGetResource(c.client, c.gvr, c.resource)
   537  	if err != nil {
   538  		c.t.Error(err)
   539  		return
   540  	}
   541  	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)
   542  	_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch(
   543  		context.TODO(),
   544  		obj.GetName(),
   545  		types.MergePatchType,
   546  		[]byte(`{"metadata":{"annotations":{"patch":"true"}}}`),
   547  		metav1.PatchOptions{})
   548  	if err != nil {
   549  		c.t.Error(err)
   550  		return
   551  	}
   552  }
   553  
   554  func testResourceDelete(c *testContext) {
   555  	// Verify that an immediate delete triggers the webhook and populates the admisssionRequest.oldObject.
   556  	obj, err := createOrGetResource(c.client, c.gvr, c.resource)
   557  	if err != nil {
   558  		c.t.Error(err)
   559  		return
   560  	}
   561  	background := metav1.DeletePropagationBackground
   562  	zero := int64(0)
   563  	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)
   564  	err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(context.TODO(), obj.GetName(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background})
   565  	if err != nil {
   566  		c.t.Error(err)
   567  		return
   568  	}
   569  	c.admissionHolder.verify(c.t)
   570  
   571  	// wait for the item to be gone
   572  	err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
   573  		obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
   574  		if apierrors.IsNotFound(err) {
   575  			return true, nil
   576  		}
   577  		if err == nil {
   578  			c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers())
   579  			return false, nil
   580  		}
   581  		return false, err
   582  	})
   583  	if err != nil {
   584  		c.t.Error(err)
   585  		return
   586  	}
   587  
   588  	// Verify that an update-on-delete triggers the webhook and populates the admisssionRequest.oldObject.
   589  	obj, err = createOrGetResource(c.client, c.gvr, c.resource)
   590  	if err != nil {
   591  		c.t.Error(err)
   592  		return
   593  	}
   594  	// Adding finalizer to the object, then deleting it.
   595  	// We don't add finalizers by setting DeleteOptions.PropagationPolicy
   596  	// because some resource (e.g., events) do not support garbage
   597  	// collector finalizers.
   598  	_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch(
   599  		context.TODO(),
   600  		obj.GetName(),
   601  		types.MergePatchType,
   602  		[]byte(`{"metadata":{"finalizers":["test/k8s.io"]}}`),
   603  		metav1.PatchOptions{})
   604  	if err != nil {
   605  		c.t.Error(err)
   606  		return
   607  	}
   608  	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)
   609  	err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(context.TODO(), obj.GetName(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background})
   610  	if err != nil {
   611  		c.t.Error(err)
   612  		return
   613  	}
   614  	c.admissionHolder.verify(c.t)
   615  
   616  	// wait other finalizers (e.g., crd's customresourcecleanup finalizer) to be removed.
   617  	err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
   618  		obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
   619  		if err != nil {
   620  			return false, err
   621  		}
   622  		finalizers := obj.GetFinalizers()
   623  		if len(finalizers) != 1 {
   624  			c.t.Logf("waiting for other finalizers on %#v %s to be removed, existing finalizers are %v", c.gvr, obj.GetName(), obj.GetFinalizers())
   625  			return false, nil
   626  		}
   627  		if finalizers[0] != "test/k8s.io" {
   628  			return false, fmt.Errorf("expected the single finalizer on %#v %s to be test/k8s.io, got %v", c.gvr, obj.GetName(), obj.GetFinalizers())
   629  		}
   630  		return true, nil
   631  	})
   632  	if err != nil {
   633  		c.t.Error(err)
   634  		return
   635  	}
   636  
   637  	// remove the finalizer
   638  	_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch(
   639  		context.TODO(),
   640  		obj.GetName(),
   641  		types.MergePatchType,
   642  		[]byte(`{"metadata":{"finalizers":[]}}`),
   643  		metav1.PatchOptions{})
   644  	if err != nil {
   645  		c.t.Error(err)
   646  		return
   647  	}
   648  	// wait for the item to be gone
   649  	err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
   650  		obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
   651  		if apierrors.IsNotFound(err) {
   652  			return true, nil
   653  		}
   654  		if err == nil {
   655  			c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers())
   656  			return false, nil
   657  		}
   658  		return false, err
   659  	})
   660  	if err != nil {
   661  		c.t.Error(err)
   662  		return
   663  	}
   664  }
   665  
   666  func testResourceDeletecollection(c *testContext) {
   667  	obj, err := createOrGetResource(c.client, c.gvr, c.resource)
   668  	if err != nil {
   669  		c.t.Error(err)
   670  		return
   671  	}
   672  	background := metav1.DeletePropagationBackground
   673  	zero := int64(0)
   674  
   675  	// update the object with a label that matches our selector
   676  	_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch(
   677  		context.TODO(),
   678  		obj.GetName(),
   679  		types.MergePatchType,
   680  		[]byte(`{"metadata":{"labels":{"webhooktest":"true"}}}`),
   681  		metav1.PatchOptions{})
   682  	if err != nil {
   683  		c.t.Error(err)
   684  		return
   685  	}
   686  
   687  	// set expectations
   688  	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkDeleteOptions, v1beta1.Delete, "", obj.GetNamespace(), false, true, true)
   689  
   690  	// delete
   691  	err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).DeleteCollection(context.TODO(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background}, metav1.ListOptions{LabelSelector: "webhooktest=true"})
   692  	if err != nil {
   693  		c.t.Error(err)
   694  		return
   695  	}
   696  
   697  	// wait for the item to be gone
   698  	err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
   699  		obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
   700  		if apierrors.IsNotFound(err) {
   701  			return true, nil
   702  		}
   703  		if err == nil {
   704  			c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers())
   705  			return false, nil
   706  		}
   707  		return false, err
   708  	})
   709  	if err != nil {
   710  		c.t.Error(err)
   711  		return
   712  	}
   713  }
   714  
   715  func getParentGVR(gvr schema.GroupVersionResource) schema.GroupVersionResource {
   716  	parentGVR, found := parentResources[gvr]
   717  	// if no special override is found, just drop the subresource
   718  	if !found {
   719  		parentGVR = gvr
   720  		parentGVR.Resource = strings.Split(parentGVR.Resource, "/")[0]
   721  	}
   722  	return parentGVR
   723  }
   724  
   725  func testTokenCreate(c *testContext) {
   726  	saGVR := gvr("", "v1", "serviceaccounts")
   727  	sa, err := createOrGetResource(c.client, saGVR, c.resources[saGVR])
   728  	if err != nil {
   729  		c.t.Error(err)
   730  		return
   731  	}
   732  
   733  	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)
   734  	if err = c.clientset.CoreV1().RESTClient().Post().Namespace(sa.GetNamespace()).Resource("serviceaccounts").Name(sa.GetName()).SubResource("token").Body(&authenticationv1.TokenRequest{
   735  		ObjectMeta: metav1.ObjectMeta{Name: sa.GetName()},
   736  		Spec: authenticationv1.TokenRequestSpec{
   737  			Audiences: []string{"api"},
   738  		},
   739  	}).Do(context.TODO()).Error(); err != nil {
   740  		c.t.Error(err)
   741  		return
   742  	}
   743  	c.admissionHolder.verify(c.t)
   744  }
   745  
   746  func testSubresourceUpdate(c *testContext) {
   747  	if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
   748  		parentGVR := getParentGVR(c.gvr)
   749  		parentResource := c.resources[parentGVR]
   750  		obj, err := createOrGetResource(c.client, parentGVR, parentResource)
   751  		if err != nil {
   752  			return err
   753  		}
   754  
   755  		// Save the parent object as what we submit
   756  		submitObj := obj
   757  
   758  		gvrWithoutSubresources := c.gvr
   759  		gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
   760  		subresources := strings.Split(c.gvr.Resource, "/")[1:]
   761  
   762  		// If the subresource supports get, fetch that as the object to submit (namespaces/finalize, */scale, etc)
   763  		if sets.NewString(c.resource.Verbs...).Has("get") {
   764  			submitObj, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{}, subresources...)
   765  			if err != nil {
   766  				return err
   767  			}
   768  		}
   769  
   770  		// Modify the object
   771  		submitObj.SetAnnotations(map[string]string{"subresourceupdate": "true"})
   772  
   773  		// set expectations
   774  		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)
   775  
   776  		_, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Update(
   777  			context.TODO(),
   778  			submitObj,
   779  			metav1.UpdateOptions{},
   780  			subresources...,
   781  		)
   782  		return err
   783  	}); err != nil {
   784  		c.t.Error(err)
   785  	}
   786  }
   787  
   788  func testSubresourcePatch(c *testContext) {
   789  	parentGVR := getParentGVR(c.gvr)
   790  	parentResource := c.resources[parentGVR]
   791  	obj, err := createOrGetResource(c.client, parentGVR, parentResource)
   792  	if err != nil {
   793  		c.t.Error(err)
   794  		return
   795  	}
   796  
   797  	gvrWithoutSubresources := c.gvr
   798  	gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
   799  	subresources := strings.Split(c.gvr.Resource, "/")[1:]
   800  
   801  	// set expectations
   802  	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)
   803  
   804  	_, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Patch(
   805  		context.TODO(),
   806  		obj.GetName(),
   807  		types.MergePatchType,
   808  		[]byte(`{"metadata":{"annotations":{"subresourcepatch":"true"}}}`),
   809  		metav1.PatchOptions{},
   810  		subresources...,
   811  	)
   812  	if err != nil {
   813  		c.t.Error(err)
   814  		return
   815  	}
   816  }
   817  
   818  func unimplemented(c *testContext) {
   819  	c.t.Errorf("Test function for %+v has not been implemented...", c.gvr)
   820  }
   821  
   822  //
   823  // custom methods
   824  //
   825  
   826  // testNamespaceDelete verifies namespace-specific delete behavior:
   827  // - ensures admission is called on first delete (which only sets deletionTimestamp and terminating state)
   828  // - removes finalizer from namespace
   829  // - ensures admission is called on final delete once finalizers are removed
   830  func testNamespaceDelete(c *testContext) {
   831  	obj, err := createOrGetResource(c.client, c.gvr, c.resource)
   832  	if err != nil {
   833  		c.t.Error(err)
   834  		return
   835  	}
   836  	background := metav1.DeletePropagationBackground
   837  	zero := int64(0)
   838  
   839  	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)
   840  	err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(context.TODO(), obj.GetName(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background})
   841  	if err != nil {
   842  		c.t.Error(err)
   843  		return
   844  	}
   845  	c.admissionHolder.verify(c.t)
   846  
   847  	// do the finalization so the namespace can be deleted
   848  	obj, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
   849  	if err != nil {
   850  		c.t.Error(err)
   851  		return
   852  	}
   853  	err = unstructured.SetNestedField(obj.Object, nil, "spec", "finalizers")
   854  	if err != nil {
   855  		c.t.Error(err)
   856  		return
   857  	}
   858  	_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Update(context.TODO(), obj, metav1.UpdateOptions{}, "finalize")
   859  	if err != nil {
   860  		c.t.Error(err)
   861  		return
   862  	}
   863  	// verify namespace is gone
   864  	obj, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
   865  	if err == nil || !apierrors.IsNotFound(err) {
   866  		c.t.Errorf("expected namespace to be gone, got %#v, %v", obj, err)
   867  	}
   868  }
   869  
   870  // testDeploymentRollback verifies rollback-specific behavior:
   871  // - creates a parent deployment
   872  // - creates a rollback object and posts it
   873  func testDeploymentRollback(c *testContext) {
   874  	deploymentGVR := gvr("apps", "v1", "deployments")
   875  	obj, err := createOrGetResource(c.client, deploymentGVR, c.resources[deploymentGVR])
   876  	if err != nil {
   877  		c.t.Error(err)
   878  		return
   879  	}
   880  
   881  	gvrWithoutSubresources := c.gvr
   882  	gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
   883  	subresources := strings.Split(c.gvr.Resource, "/")[1:]
   884  
   885  	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)
   886  
   887  	var rollbackObj runtime.Object
   888  	switch c.gvr {
   889  	case gvr("apps", "v1beta1", "deployments/rollback"):
   890  		rollbackObj = &appsv1beta1.DeploymentRollback{
   891  			TypeMeta:   metav1.TypeMeta{APIVersion: "apps/v1beta1", Kind: "DeploymentRollback"},
   892  			Name:       obj.GetName(),
   893  			RollbackTo: appsv1beta1.RollbackConfig{Revision: 0},
   894  		}
   895  	case gvr("extensions", "v1beta1", "deployments/rollback"):
   896  		rollbackObj = &extensionsv1beta1.DeploymentRollback{
   897  			TypeMeta:   metav1.TypeMeta{APIVersion: "extensions/v1beta1", Kind: "DeploymentRollback"},
   898  			Name:       obj.GetName(),
   899  			RollbackTo: extensionsv1beta1.RollbackConfig{Revision: 0},
   900  		}
   901  	default:
   902  		c.t.Errorf("unknown rollback resource %#v", c.gvr)
   903  		return
   904  	}
   905  
   906  	rollbackUnstructuredBody, err := runtime.DefaultUnstructuredConverter.ToUnstructured(rollbackObj)
   907  	if err != nil {
   908  		c.t.Errorf("ToUnstructured failed: %v", err)
   909  		return
   910  	}
   911  	rollbackUnstructuredObj := &unstructured.Unstructured{Object: rollbackUnstructuredBody}
   912  	rollbackUnstructuredObj.SetName(obj.GetName())
   913  
   914  	_, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Create(context.TODO(), rollbackUnstructuredObj, metav1.CreateOptions{}, subresources...)
   915  	if err != nil {
   916  		c.t.Error(err)
   917  		return
   918  	}
   919  }
   920  
   921  // testPodConnectSubresource verifies connect subresources
   922  func testPodConnectSubresource(c *testContext) {
   923  	podGVR := gvr("", "v1", "pods")
   924  	pod, err := createOrGetResource(c.client, podGVR, c.resources[podGVR])
   925  	if err != nil {
   926  		c.t.Error(err)
   927  		return
   928  	}
   929  
   930  	// check all upgradeable verbs
   931  	for _, httpMethod := range []string{"GET", "POST"} {
   932  		c.t.Logf("verifying %v", httpMethod)
   933  
   934  		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)
   935  		var err error
   936  		switch c.gvr {
   937  		case gvr("", "v1", "pods/exec"):
   938  			err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("exec").Do(context.TODO()).Error()
   939  		case gvr("", "v1", "pods/attach"):
   940  			err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("attach").Do(context.TODO()).Error()
   941  		case gvr("", "v1", "pods/portforward"):
   942  			err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("portforward").Do(context.TODO()).Error()
   943  		default:
   944  			c.t.Errorf("unknown subresource %#v", c.gvr)
   945  			return
   946  		}
   947  
   948  		if err != nil {
   949  			c.t.Logf("debug: result of subresource connect: %v", err)
   950  		}
   951  		c.admissionHolder.verify(c.t)
   952  
   953  	}
   954  }
   955  
   956  // testPodBindingEviction verifies pod binding and eviction admission
   957  func testPodBindingEviction(c *testContext) {
   958  	podGVR := gvr("", "v1", "pods")
   959  	pod, err := createOrGetResource(c.client, podGVR, c.resources[podGVR])
   960  	if err != nil {
   961  		c.t.Error(err)
   962  		return
   963  	}
   964  
   965  	background := metav1.DeletePropagationBackground
   966  	zero := int64(0)
   967  	forceDelete := metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background}
   968  	defer func() {
   969  		err := c.clientset.CoreV1().Pods(pod.GetNamespace()).Delete(context.TODO(), pod.GetName(), forceDelete)
   970  		if err != nil && !apierrors.IsNotFound(err) {
   971  			c.t.Error(err)
   972  			return
   973  		}
   974  	}()
   975  
   976  	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)
   977  
   978  	switch c.gvr {
   979  	case gvr("", "v1", "bindings"):
   980  		err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("bindings").Body(&corev1.Binding{
   981  			ObjectMeta: metav1.ObjectMeta{Name: pod.GetName()},
   982  			Target:     corev1.ObjectReference{Name: "foo", Kind: "Node", APIVersion: "v1"},
   983  		}).Do(context.TODO()).Error()
   984  
   985  	case gvr("", "v1", "pods/binding"):
   986  		err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("binding").Body(&corev1.Binding{
   987  			ObjectMeta: metav1.ObjectMeta{Name: pod.GetName()},
   988  			Target:     corev1.ObjectReference{Name: "foo", Kind: "Node", APIVersion: "v1"},
   989  		}).Do(context.TODO()).Error()
   990  
   991  	case gvr("", "v1", "pods/eviction"):
   992  		err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("eviction").Body(&policyv1.Eviction{
   993  			ObjectMeta:    metav1.ObjectMeta{Name: pod.GetName()},
   994  			DeleteOptions: &forceDelete,
   995  		}).Do(context.TODO()).Error()
   996  
   997  	default:
   998  		c.t.Errorf("unhandled resource %#v", c.gvr)
   999  		return
  1000  	}
  1001  
  1002  	if err != nil {
  1003  		c.t.Error(err)
  1004  		return
  1005  	}
  1006  }
  1007  
  1008  // testSubresourceProxy verifies proxy subresources
  1009  func testSubresourceProxy(c *testContext) {
  1010  	parentGVR := getParentGVR(c.gvr)
  1011  	parentResource := c.resources[parentGVR]
  1012  	obj, err := createOrGetResource(c.client, parentGVR, parentResource)
  1013  	if err != nil {
  1014  		c.t.Error(err)
  1015  		return
  1016  	}
  1017  
  1018  	gvrWithoutSubresources := c.gvr
  1019  	gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
  1020  	subresources := strings.Split(c.gvr.Resource, "/")[1:]
  1021  
  1022  	verbToHTTPMethods := map[string][]string{
  1023  		"create": {"POST", "GET", "HEAD", "OPTIONS"}, // also test read-only verbs map to Connect admission
  1024  		"update": {"PUT"},
  1025  		"patch":  {"PATCH"},
  1026  		"delete": {"DELETE"},
  1027  	}
  1028  	httpMethodsToTest, ok := verbToHTTPMethods[c.verb]
  1029  	if !ok {
  1030  		c.t.Errorf("unknown verb %v", c.verb)
  1031  		return
  1032  	}
  1033  
  1034  	for _, httpMethod := range httpMethodsToTest {
  1035  		c.t.Logf("testing %v", httpMethod)
  1036  		request := c.clientset.CoreV1().RESTClient().Verb(httpMethod)
  1037  
  1038  		// add the namespace if required
  1039  		if len(obj.GetNamespace()) > 0 {
  1040  			request = request.Namespace(obj.GetNamespace())
  1041  		}
  1042  
  1043  		// set expectations
  1044  		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)
  1045  		// run the request. we don't actually care if the request is successful, just that admission gets called as expected
  1046  		err = request.Resource(gvrWithoutSubresources.Resource).Name(obj.GetName()).SubResource(subresources...).Do(context.TODO()).Error()
  1047  		if err != nil {
  1048  			c.t.Logf("debug: result of subresource proxy (error expected): %v", err)
  1049  		}
  1050  		// verify the result
  1051  		c.admissionHolder.verify(c.t)
  1052  	}
  1053  }
  1054  
  1055  func testPruningRandomNumbers(c *testContext) {
  1056  	testResourceCreate(c)
  1057  
  1058  	cr2pant, err := c.client.Resource(c.gvr).Get(context.TODO(), "fortytwo", metav1.GetOptions{})
  1059  	if err != nil {
  1060  		c.t.Error(err)
  1061  		return
  1062  	}
  1063  
  1064  	foo, found, err := unstructured.NestedString(cr2pant.Object, "foo")
  1065  	if err != nil {
  1066  		c.t.Error(err)
  1067  		return
  1068  	}
  1069  	if found {
  1070  		c.t.Errorf("expected .foo to be pruned, but got: %s", foo)
  1071  	}
  1072  }
  1073  
  1074  // DIFF: Commented out for policy test. To be added back for mutating policy tests.
  1075  // This test deoends on "foo" being set to test by admission webhook/policy.
  1076  // func testNoPruningCustomFancy(c *testContext) {
  1077  // 	testResourceCreate(c)
  1078  
  1079  // 	cr2pant, err := c.client.Resource(c.gvr).Get(context.TODO(), "cr2pant", metav1.GetOptions{})
  1080  // 	if err != nil {
  1081  // 		c.t.Error(err)
  1082  // 		return
  1083  // 	}
  1084  
  1085  // 	foo, _, err := unstructured.NestedString(cr2pant.Object, "foo")
  1086  // 	if err != nil {
  1087  // 		c.t.Error(err)
  1088  // 		return
  1089  // 	}
  1090  
  1091  // 	// check that no pruning took place
  1092  // 	if expected, got := "test", foo; expected != got {
  1093  // 		c.t.Errorf("expected /foo to be %q, got: %q", expected, got)
  1094  // 	}
  1095  // }