k8s.io/apiserver@v0.31.1/pkg/admission/plugin/webhook/generic/webhook_test.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package generic
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"strings"
    24  	"testing"
    25  
    26  	v1 "k8s.io/api/admissionregistration/v1"
    27  	corev1 "k8s.io/api/core/v1"
    28  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/labels"
    31  	"k8s.io/apimachinery/pkg/runtime"
    32  	"k8s.io/apimachinery/pkg/runtime/schema"
    33  	"k8s.io/apiserver/pkg/admission"
    34  	"k8s.io/apiserver/pkg/admission/plugin/cel"
    35  	"k8s.io/apiserver/pkg/admission/plugin/webhook"
    36  	"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
    37  	"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/namespace"
    38  	"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
    39  	"k8s.io/apiserver/pkg/authorization/authorizer"
    40  )
    41  
    42  func gvr(group, version, resource string) schema.GroupVersionResource {
    43  	return schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
    44  }
    45  
    46  func gvk(group, version, kind string) schema.GroupVersionKind {
    47  	return schema.GroupVersionKind{Group: group, Version: version, Kind: kind}
    48  }
    49  
    50  var _ matchconditions.Matcher = &fakeMatcher{}
    51  
    52  type fakeMatcher struct {
    53  	throwError  error
    54  	matchResult bool
    55  }
    56  
    57  func (f *fakeMatcher) Match(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, authz authorizer.Authorizer) matchconditions.MatchResult {
    58  	if f.throwError != nil {
    59  		return matchconditions.MatchResult{
    60  			Matches:             true,
    61  			FailedConditionName: "",
    62  			Error:               f.throwError,
    63  		}
    64  	}
    65  	return matchconditions.MatchResult{
    66  		Matches:             f.matchResult,
    67  		FailedConditionName: "",
    68  	}
    69  }
    70  
    71  var _ webhook.WebhookAccessor = &fakeWebhookAccessor{}
    72  
    73  type fakeWebhookAccessor struct {
    74  	webhook.WebhookAccessor
    75  	throwError  error
    76  	matchResult bool
    77  }
    78  
    79  func (f *fakeWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher {
    80  	return &fakeMatcher{
    81  		throwError:  f.throwError,
    82  		matchResult: f.matchResult,
    83  	}
    84  }
    85  
    86  var _ VersionedAttributeAccessor = &fakeVersionedAttributeAccessor{}
    87  
    88  type fakeVersionedAttributeAccessor struct{}
    89  
    90  func (v *fakeVersionedAttributeAccessor) VersionedAttribute(gvk schema.GroupVersionKind) (*admission.VersionedAttributes, error) {
    91  	return nil, nil
    92  }
    93  
    94  func TestShouldCallHook(t *testing.T) {
    95  	a := &Webhook{
    96  		namespaceMatcher: &namespace.Matcher{},
    97  		objectMatcher:    &object.Matcher{},
    98  	}
    99  
   100  	allScopes := v1.AllScopes
   101  	exactMatch := v1.Exact
   102  	equivalentMatch := v1.Equivalent
   103  
   104  	mapper := runtime.NewEquivalentResourceRegistryWithIdentity(func(resource schema.GroupResource) string {
   105  		if resource.Resource == "deployments" {
   106  			// co-locate deployments in all API groups
   107  			return "/deployments"
   108  		}
   109  		return ""
   110  	})
   111  	mapper.RegisterKindFor(gvr("extensions", "v1beta1", "deployments"), "", gvk("extensions", "v1beta1", "Deployment"))
   112  	mapper.RegisterKindFor(gvr("apps", "v1", "deployments"), "", gvk("apps", "v1", "Deployment"))
   113  	mapper.RegisterKindFor(gvr("apps", "v1beta1", "deployments"), "", gvk("apps", "v1beta1", "Deployment"))
   114  	mapper.RegisterKindFor(gvr("apps", "v1alpha1", "deployments"), "", gvk("apps", "v1alpha1", "Deployment"))
   115  
   116  	mapper.RegisterKindFor(gvr("extensions", "v1beta1", "deployments"), "scale", gvk("extensions", "v1beta1", "Scale"))
   117  	mapper.RegisterKindFor(gvr("apps", "v1", "deployments"), "scale", gvk("autoscaling", "v1", "Scale"))
   118  	mapper.RegisterKindFor(gvr("apps", "v1beta1", "deployments"), "scale", gvk("apps", "v1beta1", "Scale"))
   119  	mapper.RegisterKindFor(gvr("apps", "v1alpha1", "deployments"), "scale", gvk("apps", "v1alpha1", "Scale"))
   120  
   121  	// register invalid kinds to trigger an error
   122  	mapper.RegisterKindFor(gvr("example.com", "v1", "widgets"), "", gvk("", "", ""))
   123  	mapper.RegisterKindFor(gvr("example.com", "v2", "widgets"), "", gvk("", "", ""))
   124  
   125  	interfaces := &admission.RuntimeObjectInterfaces{EquivalentResourceMapper: mapper}
   126  
   127  	testcases := []struct {
   128  		name string
   129  
   130  		webhook *v1.ValidatingWebhook
   131  		attrs   admission.Attributes
   132  
   133  		expectCall            bool
   134  		expectErr             string
   135  		expectCallResource    schema.GroupVersionResource
   136  		expectCallSubresource string
   137  		expectCallKind        schema.GroupVersionKind
   138  		matchError            error
   139  		matchResult           bool
   140  	}{
   141  		{
   142  			name:        "no rules (just write)",
   143  			webhook:     &v1.ValidatingWebhook{NamespaceSelector: &metav1.LabelSelector{}, Rules: []v1.RuleWithOperations{}},
   144  			attrs:       admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
   145  			expectCall:  false,
   146  			matchResult: true,
   147  		},
   148  		{
   149  			name: "invalid kind lookup",
   150  			webhook: &v1.ValidatingWebhook{
   151  				NamespaceSelector: &metav1.LabelSelector{},
   152  				ObjectSelector:    &metav1.LabelSelector{},
   153  				MatchPolicy:       &equivalentMatch,
   154  				Rules: []v1.RuleWithOperations{{
   155  					Operations: []v1.OperationType{"*"},
   156  					Rule:       v1.Rule{APIGroups: []string{"example.com"}, APIVersions: []string{"v1"}, Resources: []string{"widgets"}, Scope: &allScopes},
   157  				}}},
   158  			attrs:       admission.NewAttributesRecord(nil, nil, gvk("example.com", "v2", "Widget"), "ns", "name", gvr("example.com", "v2", "widgets"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
   159  			expectCall:  false,
   160  			expectErr:   "unknown kind",
   161  			matchResult: true,
   162  		},
   163  		{
   164  			name: "wildcard rule, match as requested",
   165  			webhook: &v1.ValidatingWebhook{
   166  				NamespaceSelector: &metav1.LabelSelector{},
   167  				ObjectSelector:    &metav1.LabelSelector{},
   168  				Rules: []v1.RuleWithOperations{{
   169  					Operations: []v1.OperationType{"*"},
   170  					Rule:       v1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}, Scope: &allScopes},
   171  				}}},
   172  			attrs:                 admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
   173  			expectCall:            true,
   174  			expectCallKind:        gvk("apps", "v1", "Deployment"),
   175  			expectCallResource:    gvr("apps", "v1", "deployments"),
   176  			expectCallSubresource: "",
   177  			matchResult:           true,
   178  		},
   179  		{
   180  			name: "specific rules, prefer exact match",
   181  			webhook: &v1.ValidatingWebhook{
   182  				NamespaceSelector: &metav1.LabelSelector{},
   183  				ObjectSelector:    &metav1.LabelSelector{},
   184  				Rules: []v1.RuleWithOperations{{
   185  					Operations: []v1.OperationType{"*"},
   186  					Rule:       v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
   187  				}, {
   188  					Operations: []v1.OperationType{"*"},
   189  					Rule:       v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
   190  				}, {
   191  					Operations: []v1.OperationType{"*"},
   192  					Rule:       v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Resources: []string{"deployments"}, Scope: &allScopes},
   193  				}}},
   194  			attrs:                 admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
   195  			expectCall:            true,
   196  			expectCallKind:        gvk("apps", "v1", "Deployment"),
   197  			expectCallResource:    gvr("apps", "v1", "deployments"),
   198  			expectCallSubresource: "",
   199  			matchResult:           true,
   200  		},
   201  		{
   202  			name: "specific rules, match miss",
   203  			webhook: &v1.ValidatingWebhook{
   204  				NamespaceSelector: &metav1.LabelSelector{},
   205  				ObjectSelector:    &metav1.LabelSelector{},
   206  				Rules: []v1.RuleWithOperations{{
   207  					Operations: []v1.OperationType{"*"},
   208  					Rule:       v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
   209  				}, {
   210  					Operations: []v1.OperationType{"*"},
   211  					Rule:       v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
   212  				}}},
   213  			attrs:       admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
   214  			expectCall:  false,
   215  			matchResult: true,
   216  		},
   217  		{
   218  			name: "specific rules, exact match miss",
   219  			webhook: &v1.ValidatingWebhook{
   220  				MatchPolicy:       &exactMatch,
   221  				NamespaceSelector: &metav1.LabelSelector{},
   222  				ObjectSelector:    &metav1.LabelSelector{},
   223  				Rules: []v1.RuleWithOperations{{
   224  					Operations: []v1.OperationType{"*"},
   225  					Rule:       v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
   226  				}, {
   227  					Operations: []v1.OperationType{"*"},
   228  					Rule:       v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
   229  				}}},
   230  			attrs:       admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
   231  			expectCall:  false,
   232  			matchResult: true,
   233  		},
   234  		{
   235  			name: "specific rules, equivalent match, prefer extensions",
   236  			webhook: &v1.ValidatingWebhook{
   237  				MatchPolicy:       &equivalentMatch,
   238  				NamespaceSelector: &metav1.LabelSelector{},
   239  				ObjectSelector:    &metav1.LabelSelector{},
   240  				Rules: []v1.RuleWithOperations{{
   241  					Operations: []v1.OperationType{"*"},
   242  					Rule:       v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
   243  				}, {
   244  					Operations: []v1.OperationType{"*"},
   245  					Rule:       v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
   246  				}}},
   247  			attrs:                 admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
   248  			expectCall:            true,
   249  			expectCallKind:        gvk("extensions", "v1beta1", "Deployment"),
   250  			expectCallResource:    gvr("extensions", "v1beta1", "deployments"),
   251  			expectCallSubresource: "",
   252  			matchResult:           true,
   253  		},
   254  		{
   255  			name: "specific rules, equivalent match, prefer apps",
   256  			webhook: &v1.ValidatingWebhook{
   257  				MatchPolicy:       &equivalentMatch,
   258  				NamespaceSelector: &metav1.LabelSelector{},
   259  				ObjectSelector:    &metav1.LabelSelector{},
   260  				Rules: []v1.RuleWithOperations{{
   261  					Operations: []v1.OperationType{"*"},
   262  					Rule:       v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
   263  				}, {
   264  					Operations: []v1.OperationType{"*"},
   265  					Rule:       v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
   266  				}}},
   267  			attrs:                 admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
   268  			expectCall:            true,
   269  			expectCallKind:        gvk("apps", "v1beta1", "Deployment"),
   270  			expectCallResource:    gvr("apps", "v1beta1", "deployments"),
   271  			expectCallSubresource: "",
   272  			matchResult:           true,
   273  		},
   274  
   275  		{
   276  			name: "specific rules, subresource prefer exact match",
   277  			webhook: &v1.ValidatingWebhook{
   278  				NamespaceSelector: &metav1.LabelSelector{},
   279  				ObjectSelector:    &metav1.LabelSelector{},
   280  				Rules: []v1.RuleWithOperations{{
   281  					Operations: []v1.OperationType{"*"},
   282  					Rule:       v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
   283  				}, {
   284  					Operations: []v1.OperationType{"*"},
   285  					Rule:       v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
   286  				}, {
   287  					Operations: []v1.OperationType{"*"},
   288  					Rule:       v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
   289  				}}},
   290  			attrs:                 admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
   291  			expectCall:            true,
   292  			expectCallKind:        gvk("autoscaling", "v1", "Scale"),
   293  			expectCallResource:    gvr("apps", "v1", "deployments"),
   294  			expectCallSubresource: "scale",
   295  			matchResult:           true,
   296  		},
   297  		{
   298  			name: "specific rules, subresource match miss",
   299  			webhook: &v1.ValidatingWebhook{
   300  				NamespaceSelector: &metav1.LabelSelector{},
   301  				ObjectSelector:    &metav1.LabelSelector{},
   302  				Rules: []v1.RuleWithOperations{{
   303  					Operations: []v1.OperationType{"*"},
   304  					Rule:       v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
   305  				}, {
   306  					Operations: []v1.OperationType{"*"},
   307  					Rule:       v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
   308  				}}},
   309  			attrs:       admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
   310  			expectCall:  false,
   311  			matchResult: true,
   312  		},
   313  		{
   314  			name: "specific rules, subresource exact match miss",
   315  			webhook: &v1.ValidatingWebhook{
   316  				MatchPolicy:       &exactMatch,
   317  				NamespaceSelector: &metav1.LabelSelector{},
   318  				ObjectSelector:    &metav1.LabelSelector{},
   319  				Rules: []v1.RuleWithOperations{{
   320  					Operations: []v1.OperationType{"*"},
   321  					Rule:       v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
   322  				}, {
   323  					Operations: []v1.OperationType{"*"},
   324  					Rule:       v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
   325  				}}},
   326  			attrs:       admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
   327  			expectCall:  false,
   328  			matchResult: true,
   329  		},
   330  		{
   331  			name: "specific rules, subresource equivalent match, prefer extensions",
   332  			webhook: &v1.ValidatingWebhook{
   333  				MatchPolicy:       &equivalentMatch,
   334  				NamespaceSelector: &metav1.LabelSelector{},
   335  				ObjectSelector:    &metav1.LabelSelector{},
   336  				Rules: []v1.RuleWithOperations{{
   337  					Operations: []v1.OperationType{"*"},
   338  					Rule:       v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
   339  				}, {
   340  					Operations: []v1.OperationType{"*"},
   341  					Rule:       v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
   342  				}}},
   343  			attrs:                 admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
   344  			expectCall:            true,
   345  			expectCallKind:        gvk("extensions", "v1beta1", "Scale"),
   346  			expectCallResource:    gvr("extensions", "v1beta1", "deployments"),
   347  			expectCallSubresource: "scale",
   348  			matchResult:           true,
   349  		},
   350  		{
   351  			name: "specific rules, subresource equivalent match, prefer apps",
   352  			webhook: &v1.ValidatingWebhook{
   353  				MatchPolicy:       &equivalentMatch,
   354  				NamespaceSelector: &metav1.LabelSelector{},
   355  				ObjectSelector:    &metav1.LabelSelector{},
   356  				Rules: []v1.RuleWithOperations{{
   357  					Operations: []v1.OperationType{"*"},
   358  					Rule:       v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
   359  				}, {
   360  					Operations: []v1.OperationType{"*"},
   361  					Rule:       v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
   362  				}}},
   363  			attrs:                 admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
   364  			expectCall:            true,
   365  			expectCallKind:        gvk("apps", "v1beta1", "Scale"),
   366  			expectCallResource:    gvr("apps", "v1beta1", "deployments"),
   367  			expectCallSubresource: "scale",
   368  			matchResult:           true,
   369  		},
   370  		{
   371  			name: "wildcard rule, match conditions also match",
   372  			webhook: &v1.ValidatingWebhook{
   373  				NamespaceSelector: &metav1.LabelSelector{},
   374  				ObjectSelector:    &metav1.LabelSelector{},
   375  				Rules: []v1.RuleWithOperations{{
   376  					Operations: []v1.OperationType{"*"},
   377  					Rule:       v1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}, Scope: &allScopes},
   378  				}},
   379  				MatchConditions: []v1.MatchCondition{
   380  					{
   381  						Name:       "test1",
   382  						Expression: "test expression",
   383  					},
   384  				},
   385  			},
   386  			attrs:                 admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
   387  			expectCall:            true,
   388  			expectCallKind:        gvk("apps", "v1", "Deployment"),
   389  			expectCallResource:    gvr("apps", "v1", "deployments"),
   390  			expectCallSubresource: "",
   391  			matchResult:           true,
   392  		},
   393  		{
   394  			name: "wildcard rule, match conditions do not match",
   395  			webhook: &v1.ValidatingWebhook{
   396  				NamespaceSelector: &metav1.LabelSelector{},
   397  				ObjectSelector:    &metav1.LabelSelector{},
   398  				Rules: []v1.RuleWithOperations{{
   399  					Operations: []v1.OperationType{"*"},
   400  					Rule:       v1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}, Scope: &allScopes},
   401  				}},
   402  				MatchConditions: []v1.MatchCondition{
   403  					{
   404  						Name:       "test1",
   405  						Expression: "test expression",
   406  					},
   407  				},
   408  			},
   409  			attrs:       admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
   410  			expectCall:  false,
   411  			matchResult: false,
   412  		},
   413  		{
   414  			name: "wildcard rule, match conditions error",
   415  			webhook: &v1.ValidatingWebhook{
   416  				NamespaceSelector: &metav1.LabelSelector{},
   417  				ObjectSelector:    &metav1.LabelSelector{},
   418  				Rules: []v1.RuleWithOperations{{
   419  					Operations: []v1.OperationType{"*"},
   420  					Rule:       v1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}, Scope: &allScopes},
   421  				}},
   422  				MatchConditions: []v1.MatchCondition{
   423  					{
   424  						Name:       "test1",
   425  						Expression: "test expression",
   426  					},
   427  				},
   428  			},
   429  			attrs:                 admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil),
   430  			expectCall:            false,
   431  			expectErr:             "deployments.apps \"name\" is forbidden: test error",
   432  			expectCallKind:        gvk("apps", "v1", "Deployment"),
   433  			expectCallResource:    gvr("apps", "v1", "deployments"),
   434  			expectCallSubresource: "",
   435  			matchError:            errors.New("test error"),
   436  		},
   437  	}
   438  
   439  	for i, testcase := range testcases {
   440  		t.Run(testcase.name, func(t *testing.T) {
   441  			fakeWebhook := &fakeWebhookAccessor{
   442  				WebhookAccessor: webhook.NewValidatingWebhookAccessor(fmt.Sprintf("webhook-%d", i), fmt.Sprintf("webhook-cfg-%d", i), testcase.webhook),
   443  				matchResult:     testcase.matchResult,
   444  				throwError:      testcase.matchError,
   445  			}
   446  
   447  			invocation, err := a.ShouldCallHook(context.TODO(), fakeWebhook, testcase.attrs, interfaces, &fakeVersionedAttributeAccessor{})
   448  			if err != nil {
   449  				if len(testcase.expectErr) == 0 {
   450  					t.Fatal(err)
   451  				}
   452  				if !strings.Contains(err.Error(), testcase.expectErr) {
   453  					t.Fatalf("expected error containing %q, got %s", testcase.expectErr, err.Error())
   454  				}
   455  				return
   456  			} else if len(testcase.expectErr) > 0 {
   457  				t.Fatalf("expected error %q, got no error and %#v", testcase.expectErr, invocation)
   458  			}
   459  
   460  			if invocation == nil {
   461  				if testcase.expectCall {
   462  					t.Fatal("expected invocation, got nil")
   463  				}
   464  				return
   465  			}
   466  
   467  			if !testcase.expectCall {
   468  				t.Fatal("unexpected invocation")
   469  			}
   470  
   471  			if invocation.Kind != testcase.expectCallKind {
   472  				t.Fatalf("expected %#v, got %#v", testcase.expectCallKind, invocation.Kind)
   473  			}
   474  			if invocation.Resource != testcase.expectCallResource {
   475  				t.Fatalf("expected %#v, got %#v", testcase.expectCallResource, invocation.Resource)
   476  			}
   477  			if invocation.Subresource != testcase.expectCallSubresource {
   478  				t.Fatalf("expected %#v, got %#v", testcase.expectCallSubresource, invocation.Subresource)
   479  			}
   480  		})
   481  	}
   482  }
   483  
   484  type fakeNamespaceLister struct {
   485  	namespaces map[string]*corev1.Namespace
   486  }
   487  
   488  func (f fakeNamespaceLister) List(selector labels.Selector) (ret []*corev1.Namespace, err error) {
   489  	return nil, nil
   490  }
   491  func (f fakeNamespaceLister) Get(name string) (*corev1.Namespace, error) {
   492  	ns, ok := f.namespaces[name]
   493  	if ok {
   494  		return ns, nil
   495  	}
   496  	return nil, k8serrors.NewNotFound(corev1.Resource("namespaces"), name)
   497  }
   498  
   499  func BenchmarkShouldCallHookWithComplexSelector(b *testing.B) {
   500  	allScopes := v1.AllScopes
   501  	equivalentMatch := v1.Equivalent
   502  
   503  	namespace1Labels := map[string]string{"ns": "ns1"}
   504  	namespace1 := corev1.Namespace{
   505  		ObjectMeta: metav1.ObjectMeta{
   506  			Name:   "ns1",
   507  			Labels: namespace1Labels,
   508  		},
   509  	}
   510  	namespaceLister := fakeNamespaceLister{map[string]*corev1.Namespace{"ns": &namespace1}}
   511  
   512  	mapper := runtime.NewEquivalentResourceRegistryWithIdentity(func(resource schema.GroupResource) string {
   513  		if resource.Resource == "deployments" {
   514  			// co-locate deployments in all API groups
   515  			return "/deployments"
   516  		}
   517  		return ""
   518  	})
   519  	mapper.RegisterKindFor(gvr("extensions", "v1beta1", "deployments"), "", gvk("extensions", "v1beta1", "Deployment"))
   520  	mapper.RegisterKindFor(gvr("apps", "v1", "deployments"), "", gvk("apps", "v1", "Deployment"))
   521  	mapper.RegisterKindFor(gvr("apps", "v1beta1", "deployments"), "", gvk("apps", "v1beta1", "Deployment"))
   522  	mapper.RegisterKindFor(gvr("apps", "v1alpha1", "deployments"), "", gvk("apps", "v1alpha1", "Deployment"))
   523  
   524  	mapper.RegisterKindFor(gvr("extensions", "v1beta1", "deployments"), "scale", gvk("extensions", "v1beta1", "Scale"))
   525  	mapper.RegisterKindFor(gvr("apps", "v1", "deployments"), "scale", gvk("autoscaling", "v1", "Scale"))
   526  	mapper.RegisterKindFor(gvr("apps", "v1beta1", "deployments"), "scale", gvk("apps", "v1beta1", "Scale"))
   527  	mapper.RegisterKindFor(gvr("apps", "v1alpha1", "deployments"), "scale", gvk("apps", "v1alpha1", "Scale"))
   528  
   529  	mapper.RegisterKindFor(gvr("apps", "v1", "statefulset"), "", gvk("apps", "v1", "StatefulSet"))
   530  	mapper.RegisterKindFor(gvr("apps", "v1beta1", "statefulset"), "", gvk("apps", "v1beta1", "StatefulSet"))
   531  	mapper.RegisterKindFor(gvr("apps", "v1beta2", "statefulset"), "", gvk("apps", "v1beta2", "StatefulSet"))
   532  
   533  	mapper.RegisterKindFor(gvr("apps", "v1", "statefulset"), "scale", gvk("apps", "v1", "Scale"))
   534  	mapper.RegisterKindFor(gvr("apps", "v1beta1", "statefulset"), "scale", gvk("apps", "v1beta1", "Scale"))
   535  	mapper.RegisterKindFor(gvr("apps", "v1alpha2", "statefulset"), "scale", gvk("apps", "v1beta2", "Scale"))
   536  
   537  	nsSelector := make(map[string]string)
   538  	for i := 0; i < 100; i++ {
   539  		nsSelector[fmt.Sprintf("key-%d", i)] = fmt.Sprintf("val-%d", i)
   540  	}
   541  
   542  	wb := &v1.ValidatingWebhook{
   543  		MatchPolicy:       &equivalentMatch,
   544  		NamespaceSelector: &metav1.LabelSelector{MatchLabels: nsSelector},
   545  		ObjectSelector:    &metav1.LabelSelector{},
   546  		Rules: []v1.RuleWithOperations{
   547  			{
   548  				Operations: []v1.OperationType{"*"},
   549  				Rule:       v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
   550  			},
   551  			{
   552  				Operations: []v1.OperationType{"*"},
   553  				Rule:       v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
   554  			},
   555  		},
   556  	}
   557  
   558  	wbAccessor := &fakeWebhookAccessor{
   559  		WebhookAccessor: webhook.NewValidatingWebhookAccessor("webhook", "webhook-cfg", wb),
   560  		matchResult:     true,
   561  	}
   562  	attrs := admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil)
   563  	interfaces := &admission.RuntimeObjectInterfaces{EquivalentResourceMapper: mapper}
   564  	a := &Webhook{namespaceMatcher: &namespace.Matcher{NamespaceLister: namespaceLister}, objectMatcher: &object.Matcher{}}
   565  
   566  	for i := 0; i < b.N; i++ {
   567  		a.ShouldCallHook(context.TODO(), wbAccessor, attrs, interfaces, nil)
   568  	}
   569  }
   570  
   571  func BenchmarkShouldCallHookWithComplexRule(b *testing.B) {
   572  	allScopes := v1.AllScopes
   573  	equivalentMatch := v1.Equivalent
   574  
   575  	namespace1Labels := map[string]string{"ns": "ns1"}
   576  	namespace1 := corev1.Namespace{
   577  		ObjectMeta: metav1.ObjectMeta{
   578  			Name:   "ns1",
   579  			Labels: namespace1Labels,
   580  		},
   581  	}
   582  	namespaceLister := fakeNamespaceLister{map[string]*corev1.Namespace{"ns": &namespace1}}
   583  
   584  	mapper := runtime.NewEquivalentResourceRegistryWithIdentity(func(resource schema.GroupResource) string {
   585  		if resource.Resource == "deployments" {
   586  			// co-locate deployments in all API groups
   587  			return "/deployments"
   588  		}
   589  		return ""
   590  	})
   591  	mapper.RegisterKindFor(gvr("extensions", "v1beta1", "deployments"), "", gvk("extensions", "v1beta1", "Deployment"))
   592  	mapper.RegisterKindFor(gvr("apps", "v1", "deployments"), "", gvk("apps", "v1", "Deployment"))
   593  	mapper.RegisterKindFor(gvr("apps", "v1beta1", "deployments"), "", gvk("apps", "v1beta1", "Deployment"))
   594  	mapper.RegisterKindFor(gvr("apps", "v1alpha1", "deployments"), "", gvk("apps", "v1alpha1", "Deployment"))
   595  
   596  	mapper.RegisterKindFor(gvr("extensions", "v1beta1", "deployments"), "scale", gvk("extensions", "v1beta1", "Scale"))
   597  	mapper.RegisterKindFor(gvr("apps", "v1", "deployments"), "scale", gvk("autoscaling", "v1", "Scale"))
   598  	mapper.RegisterKindFor(gvr("apps", "v1beta1", "deployments"), "scale", gvk("apps", "v1beta1", "Scale"))
   599  	mapper.RegisterKindFor(gvr("apps", "v1alpha1", "deployments"), "scale", gvk("apps", "v1alpha1", "Scale"))
   600  
   601  	mapper.RegisterKindFor(gvr("apps", "v1", "statefulset"), "", gvk("apps", "v1", "StatefulSet"))
   602  	mapper.RegisterKindFor(gvr("apps", "v1beta1", "statefulset"), "", gvk("apps", "v1beta1", "StatefulSet"))
   603  	mapper.RegisterKindFor(gvr("apps", "v1beta2", "statefulset"), "", gvk("apps", "v1beta2", "StatefulSet"))
   604  
   605  	mapper.RegisterKindFor(gvr("apps", "v1", "statefulset"), "scale", gvk("apps", "v1", "Scale"))
   606  	mapper.RegisterKindFor(gvr("apps", "v1beta1", "statefulset"), "scale", gvk("apps", "v1beta1", "Scale"))
   607  	mapper.RegisterKindFor(gvr("apps", "v1alpha2", "statefulset"), "scale", gvk("apps", "v1beta2", "Scale"))
   608  
   609  	wb := &v1.ValidatingWebhook{
   610  		MatchPolicy:       &equivalentMatch,
   611  		NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"a": "b"}},
   612  		ObjectSelector:    &metav1.LabelSelector{},
   613  		Rules:             []v1.RuleWithOperations{},
   614  	}
   615  
   616  	for i := 0; i < 100; i++ {
   617  		rule := v1.RuleWithOperations{
   618  			Operations: []v1.OperationType{"*"},
   619  			Rule: v1.Rule{
   620  				APIGroups:   []string{fmt.Sprintf("app-%d", i)},
   621  				APIVersions: []string{fmt.Sprintf("v%d", i)},
   622  				Resources:   []string{fmt.Sprintf("resource%d", i), fmt.Sprintf("resource%d/scale", i)},
   623  				Scope:       &allScopes,
   624  			},
   625  		}
   626  		wb.Rules = append(wb.Rules, rule)
   627  	}
   628  
   629  	wbAccessor := &fakeWebhookAccessor{
   630  		WebhookAccessor: webhook.NewValidatingWebhookAccessor("webhook", "webhook-cfg", wb),
   631  		matchResult:     true,
   632  	}
   633  	attrs := admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil)
   634  	interfaces := &admission.RuntimeObjectInterfaces{EquivalentResourceMapper: mapper}
   635  	a := &Webhook{namespaceMatcher: &namespace.Matcher{NamespaceLister: namespaceLister}, objectMatcher: &object.Matcher{}}
   636  
   637  	for i := 0; i < b.N; i++ {
   638  		a.ShouldCallHook(context.TODO(), wbAccessor, attrs, interfaces, &fakeVersionedAttributeAccessor{})
   639  	}
   640  }
   641  
   642  func BenchmarkShouldCallHookWithComplexSelectorAndRule(b *testing.B) {
   643  	allScopes := v1.AllScopes
   644  	equivalentMatch := v1.Equivalent
   645  
   646  	namespace1Labels := map[string]string{"ns": "ns1"}
   647  	namespace1 := corev1.Namespace{
   648  		ObjectMeta: metav1.ObjectMeta{
   649  			Name:   "ns1",
   650  			Labels: namespace1Labels,
   651  		},
   652  	}
   653  	namespaceLister := fakeNamespaceLister{map[string]*corev1.Namespace{"ns": &namespace1}}
   654  
   655  	mapper := runtime.NewEquivalentResourceRegistryWithIdentity(func(resource schema.GroupResource) string {
   656  		if resource.Resource == "deployments" {
   657  			// co-locate deployments in all API groups
   658  			return "/deployments"
   659  		}
   660  		return ""
   661  	})
   662  	mapper.RegisterKindFor(gvr("extensions", "v1beta1", "deployments"), "", gvk("extensions", "v1beta1", "Deployment"))
   663  	mapper.RegisterKindFor(gvr("apps", "v1", "deployments"), "", gvk("apps", "v1", "Deployment"))
   664  	mapper.RegisterKindFor(gvr("apps", "v1beta1", "deployments"), "", gvk("apps", "v1beta1", "Deployment"))
   665  	mapper.RegisterKindFor(gvr("apps", "v1alpha1", "deployments"), "", gvk("apps", "v1alpha1", "Deployment"))
   666  
   667  	mapper.RegisterKindFor(gvr("extensions", "v1beta1", "deployments"), "scale", gvk("extensions", "v1beta1", "Scale"))
   668  	mapper.RegisterKindFor(gvr("apps", "v1", "deployments"), "scale", gvk("autoscaling", "v1", "Scale"))
   669  	mapper.RegisterKindFor(gvr("apps", "v1beta1", "deployments"), "scale", gvk("apps", "v1beta1", "Scale"))
   670  	mapper.RegisterKindFor(gvr("apps", "v1alpha1", "deployments"), "scale", gvk("apps", "v1alpha1", "Scale"))
   671  
   672  	mapper.RegisterKindFor(gvr("apps", "v1", "statefulset"), "", gvk("apps", "v1", "StatefulSet"))
   673  	mapper.RegisterKindFor(gvr("apps", "v1beta1", "statefulset"), "", gvk("apps", "v1beta1", "StatefulSet"))
   674  	mapper.RegisterKindFor(gvr("apps", "v1beta2", "statefulset"), "", gvk("apps", "v1beta2", "StatefulSet"))
   675  
   676  	mapper.RegisterKindFor(gvr("apps", "v1", "statefulset"), "scale", gvk("apps", "v1", "Scale"))
   677  	mapper.RegisterKindFor(gvr("apps", "v1beta1", "statefulset"), "scale", gvk("apps", "v1beta1", "Scale"))
   678  	mapper.RegisterKindFor(gvr("apps", "v1alpha2", "statefulset"), "scale", gvk("apps", "v1beta2", "Scale"))
   679  
   680  	nsSelector := make(map[string]string)
   681  	for i := 0; i < 100; i++ {
   682  		nsSelector[fmt.Sprintf("key-%d", i)] = fmt.Sprintf("val-%d", i)
   683  	}
   684  
   685  	wb := &v1.ValidatingWebhook{
   686  		MatchPolicy:       &equivalentMatch,
   687  		NamespaceSelector: &metav1.LabelSelector{MatchLabels: nsSelector},
   688  		ObjectSelector:    &metav1.LabelSelector{},
   689  		Rules:             []v1.RuleWithOperations{},
   690  	}
   691  
   692  	for i := 0; i < 100; i++ {
   693  		rule := v1.RuleWithOperations{
   694  			Operations: []v1.OperationType{"*"},
   695  			Rule: v1.Rule{
   696  				APIGroups:   []string{fmt.Sprintf("app-%d", i)},
   697  				APIVersions: []string{fmt.Sprintf("v%d", i)},
   698  				Resources:   []string{fmt.Sprintf("resource%d", i), fmt.Sprintf("resource%d/scale", i)},
   699  				Scope:       &allScopes,
   700  			},
   701  		}
   702  		wb.Rules = append(wb.Rules, rule)
   703  	}
   704  
   705  	wbAccessor := &fakeWebhookAccessor{
   706  		WebhookAccessor: webhook.NewValidatingWebhookAccessor("webhook", "webhook-cfg", wb),
   707  		matchResult:     true,
   708  	}
   709  	attrs := admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil)
   710  	interfaces := &admission.RuntimeObjectInterfaces{EquivalentResourceMapper: mapper}
   711  	a := &Webhook{namespaceMatcher: &namespace.Matcher{NamespaceLister: namespaceLister}, objectMatcher: &object.Matcher{}}
   712  
   713  	for i := 0; i < b.N; i++ {
   714  		a.ShouldCallHook(context.TODO(), wbAccessor, attrs, interfaces, nil)
   715  	}
   716  }