...

Source file src/k8s.io/kubernetes/test/integration/apiserver/cel/admission_test_util.go

Documentation: k8s.io/kubernetes/test/integration/apiserver/cel

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

View as plain text