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

     1  /*
     2  Copyright 2018 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 testing
    18  
    19  import (
    20  	"fmt"
    21  	"net/http"
    22  	"net/url"
    23  	"reflect"
    24  	"strings"
    25  	"sync"
    26  	"time"
    27  
    28  	registrationv1 "k8s.io/api/admissionregistration/v1"
    29  	corev1 "k8s.io/api/core/v1"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    32  	"k8s.io/apimachinery/pkg/runtime"
    33  	"k8s.io/apimachinery/pkg/runtime/schema"
    34  	"k8s.io/apiserver/pkg/admission"
    35  	"k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts"
    36  	auditinternal "k8s.io/apiserver/pkg/apis/audit"
    37  	"k8s.io/apiserver/pkg/authentication/user"
    38  	"k8s.io/client-go/informers"
    39  	"k8s.io/client-go/kubernetes"
    40  	fakeclientset "k8s.io/client-go/kubernetes/fake"
    41  )
    42  
    43  var matchEverythingRules = []registrationv1.RuleWithOperations{{
    44  	Operations: []registrationv1.OperationType{registrationv1.OperationAll},
    45  	Rule: registrationv1.Rule{
    46  		APIGroups:   []string{"*"},
    47  		APIVersions: []string{"*"},
    48  		Resources:   []string{"*/*"},
    49  	},
    50  }}
    51  
    52  var sideEffectsUnknown = registrationv1.SideEffectClassUnknown
    53  var sideEffectsNone = registrationv1.SideEffectClassNone
    54  var sideEffectsSome = registrationv1.SideEffectClassSome
    55  var sideEffectsNoneOnDryRun = registrationv1.SideEffectClassNoneOnDryRun
    56  
    57  var reinvokeNever = registrationv1.NeverReinvocationPolicy
    58  var reinvokeIfNeeded = registrationv1.IfNeededReinvocationPolicy
    59  
    60  // NewFakeValidatingDataSource returns a mock client and informer returning the given webhooks.
    61  func NewFakeValidatingDataSource(name string, webhooks []registrationv1.ValidatingWebhook, stopCh <-chan struct{}) (clientset kubernetes.Interface, factory informers.SharedInformerFactory) {
    62  	var objs = []runtime.Object{
    63  		&corev1.Namespace{
    64  			ObjectMeta: metav1.ObjectMeta{
    65  				Name: name,
    66  				Labels: map[string]string{
    67  					"runlevel": "0",
    68  				},
    69  			},
    70  		},
    71  	}
    72  	objs = append(objs, &registrationv1.ValidatingWebhookConfiguration{
    73  		ObjectMeta: metav1.ObjectMeta{
    74  			Name: "test-webhooks",
    75  		},
    76  		Webhooks: webhooks,
    77  	})
    78  
    79  	client := fakeclientset.NewSimpleClientset(objs...)
    80  	informerFactory := informers.NewSharedInformerFactory(client, 0)
    81  
    82  	return client, informerFactory
    83  }
    84  
    85  // NewFakeMutatingDataSource returns a mock client and informer returning the given webhooks.
    86  func NewFakeMutatingDataSource(name string, webhooks []registrationv1.MutatingWebhook, stopCh <-chan struct{}) (clientset kubernetes.Interface, factory informers.SharedInformerFactory) {
    87  	var objs = []runtime.Object{
    88  		&corev1.Namespace{
    89  			ObjectMeta: metav1.ObjectMeta{
    90  				Name: name,
    91  				Labels: map[string]string{
    92  					"runlevel": "0",
    93  				},
    94  			},
    95  		},
    96  	}
    97  	objs = append(objs, &registrationv1.MutatingWebhookConfiguration{
    98  		ObjectMeta: metav1.ObjectMeta{
    99  			Name: "test-webhooks",
   100  		},
   101  		Webhooks: webhooks,
   102  	})
   103  
   104  	client := fakeclientset.NewSimpleClientset(objs...)
   105  	informerFactory := informers.NewSharedInformerFactory(client, 0)
   106  
   107  	return client, informerFactory
   108  }
   109  
   110  func newAttributesRecord(object metav1.Object, oldObject metav1.Object, kind schema.GroupVersionKind, namespace string, name string, resource string, labels map[string]string, dryRun bool) admission.Attributes {
   111  	object.SetName(name)
   112  	object.SetNamespace(namespace)
   113  	objectLabels := map[string]string{resource + ".name": name}
   114  	for k, v := range labels {
   115  		objectLabels[k] = v
   116  	}
   117  	object.SetLabels(objectLabels)
   118  
   119  	oldObject.SetName(name)
   120  	oldObject.SetNamespace(namespace)
   121  
   122  	gvr := kind.GroupVersion().WithResource(resource)
   123  	subResource := ""
   124  	userInfo := user.DefaultInfo{
   125  		Name: "webhook-test",
   126  		UID:  "webhook-test",
   127  	}
   128  	options := &metav1.UpdateOptions{}
   129  
   130  	return &FakeAttributes{
   131  		Attributes: admission.NewAttributesRecord(object.(runtime.Object), oldObject.(runtime.Object), kind, namespace, name, gvr, subResource, admission.Update, options, dryRun, &userInfo),
   132  	}
   133  }
   134  
   135  // FakeAttributes decorate admission.Attributes. It's used to trace the added annotations.
   136  type FakeAttributes struct {
   137  	admission.Attributes
   138  	annotations map[string]string
   139  	mutex       sync.Mutex
   140  }
   141  
   142  // AddAnnotation adds an annotation key value pair to FakeAttributes
   143  func (f *FakeAttributes) AddAnnotation(k, v string) error {
   144  	return f.AddAnnotationWithLevel(k, v, auditinternal.LevelMetadata)
   145  }
   146  
   147  // AddAnnotationWithLevel adds an annotation key value pair to FakeAttributes
   148  func (f *FakeAttributes) AddAnnotationWithLevel(k, v string, _ auditinternal.Level) error {
   149  	f.mutex.Lock()
   150  	defer f.mutex.Unlock()
   151  	if err := f.Attributes.AddAnnotation(k, v); err != nil {
   152  		return err
   153  	}
   154  	if f.annotations == nil {
   155  		f.annotations = make(map[string]string)
   156  	}
   157  	f.annotations[k] = v
   158  	return nil
   159  }
   160  
   161  // GetAnnotations reads annotations from FakeAttributes
   162  func (f *FakeAttributes) GetAnnotations(level auditinternal.Level) map[string]string {
   163  	f.mutex.Lock()
   164  	defer f.mutex.Unlock()
   165  	return f.annotations
   166  }
   167  
   168  // NewAttribute returns static admission Attributes for testing.
   169  func NewAttribute(namespace string, labels map[string]string, dryRun bool) admission.Attributes {
   170  	// Set up a test object for the call
   171  	object := corev1.Pod{
   172  		TypeMeta: metav1.TypeMeta{
   173  			APIVersion: "v1",
   174  			Kind:       "Pod",
   175  		},
   176  	}
   177  	oldObject := corev1.Pod{}
   178  	kind := corev1.SchemeGroupVersion.WithKind("Pod")
   179  	name := "my-pod"
   180  
   181  	return newAttributesRecord(&object, &oldObject, kind, namespace, name, "pod", labels, dryRun)
   182  }
   183  
   184  // NewAttributeUnstructured returns static admission Attributes for testing with custom resources.
   185  func NewAttributeUnstructured(namespace string, labels map[string]string, dryRun bool) admission.Attributes {
   186  	// Set up a test object for the call
   187  	object := unstructured.Unstructured{}
   188  	object.SetKind("TestCRD")
   189  	object.SetAPIVersion("custom.resource/v1")
   190  	oldObject := unstructured.Unstructured{}
   191  	oldObject.SetKind("TestCRD")
   192  	oldObject.SetAPIVersion("custom.resource/v1")
   193  	kind := object.GroupVersionKind()
   194  	name := "my-test-crd"
   195  
   196  	return newAttributesRecord(&object, &oldObject, kind, namespace, name, "crd", labels, dryRun)
   197  }
   198  
   199  type urlConfigGenerator struct {
   200  	baseURL *url.URL
   201  }
   202  
   203  func (c urlConfigGenerator) ccfgURL(urlPath string) registrationv1.WebhookClientConfig {
   204  	u2 := *c.baseURL
   205  	u2.Path = urlPath
   206  	urlString := u2.String()
   207  	return registrationv1.WebhookClientConfig{
   208  		URL:      &urlString,
   209  		CABundle: testcerts.CACert,
   210  	}
   211  }
   212  
   213  // ValidatingTest is a validating webhook test case.
   214  type ValidatingTest struct {
   215  	Name                   string
   216  	Webhooks               []registrationv1.ValidatingWebhook
   217  	Path                   string
   218  	IsCRD                  bool
   219  	IsDryRun               bool
   220  	AdditionalLabels       map[string]string
   221  	SkipBenchmark          bool
   222  	ExpectLabels           map[string]string
   223  	ExpectAllow            bool
   224  	ErrorContains          string
   225  	ExpectAnnotations      map[string]string
   226  	ExpectStatusCode       int32
   227  	ExpectReinvokeWebhooks map[string]bool
   228  }
   229  
   230  // MutatingTest is a mutating webhook test case.
   231  type MutatingTest struct {
   232  	Name                   string
   233  	Webhooks               []registrationv1.MutatingWebhook
   234  	Path                   string
   235  	IsCRD                  bool
   236  	IsDryRun               bool
   237  	AdditionalLabels       map[string]string
   238  	SkipBenchmark          bool
   239  	ExpectLabels           map[string]string
   240  	ExpectAllow            bool
   241  	ErrorContains          string
   242  	ExpectAnnotations      map[string]string
   243  	ExpectStatusCode       int32
   244  	ExpectReinvokeWebhooks map[string]bool
   245  }
   246  
   247  // DurationTest is webhook duration test case, used both in mutating and
   248  // validating plugin test cases.
   249  type DurationTest struct {
   250  	Name                string
   251  	Webhooks            []registrationv1.ValidatingWebhook
   252  	InitContext         bool
   253  	IsDryRun            bool
   254  	ExpectedDurationSum time.Duration
   255  	ExpectedDurationMax time.Duration
   256  }
   257  
   258  // ConvertToMutatingTestCases converts a validating test case to a mutating one for test purposes.
   259  func ConvertToMutatingTestCases(tests []ValidatingTest, configurationName string) []MutatingTest {
   260  	r := make([]MutatingTest, len(tests))
   261  	for i, t := range tests {
   262  		for idx, hook := range t.Webhooks {
   263  			if t.ExpectAnnotations == nil {
   264  				t.ExpectAnnotations = map[string]string{}
   265  			}
   266  			// Add expected annotation if the converted webhook is intended to match
   267  			if reflect.DeepEqual(hook.NamespaceSelector, &metav1.LabelSelector{}) &&
   268  				reflect.DeepEqual(hook.ObjectSelector, &metav1.LabelSelector{}) &&
   269  				reflect.DeepEqual(hook.Rules, matchEverythingRules) {
   270  				key := fmt.Sprintf("mutation.webhook.admission.k8s.io/round_0_index_%d", idx)
   271  				value := mutationAnnotationValue(configurationName, hook.Name, false)
   272  				t.ExpectAnnotations[key] = value
   273  			}
   274  			// Break if the converted webhook is intended to fail close
   275  			if strings.Contains(hook.Name, "internalErr") && (hook.FailurePolicy == nil || *hook.FailurePolicy == registrationv1.Fail) {
   276  				break
   277  			}
   278  		}
   279  		// Change annotation keys for Validating's fail open to Mutating's fail open.
   280  		failOpenAnnotations := map[string]string{}
   281  		for key, value := range t.ExpectAnnotations {
   282  			if strings.HasPrefix(key, "failed-open.validating.webhook.admission.k8s.io/") {
   283  				failOpenAnnotations[key] = value
   284  			}
   285  		}
   286  		for key, value := range failOpenAnnotations {
   287  			newKey := strings.Replace(key, "failed-open.validating.webhook.admission.k8s.io/", "failed-open.mutation.webhook.admission.k8s.io/", 1)
   288  			t.ExpectAnnotations[newKey] = value
   289  			delete(t.ExpectAnnotations, key)
   290  		}
   291  		r[i] = MutatingTest{t.Name, ConvertToMutatingWebhooks(t.Webhooks), t.Path, t.IsCRD, t.IsDryRun, t.AdditionalLabels, t.SkipBenchmark, t.ExpectLabels, t.ExpectAllow, t.ErrorContains, t.ExpectAnnotations, t.ExpectStatusCode, t.ExpectReinvokeWebhooks}
   292  	}
   293  	return r
   294  }
   295  
   296  // ConvertToMutatingWebhooks converts a validating webhook to a mutating one for test purposes.
   297  func ConvertToMutatingWebhooks(webhooks []registrationv1.ValidatingWebhook) []registrationv1.MutatingWebhook {
   298  	mutating := make([]registrationv1.MutatingWebhook, len(webhooks))
   299  	for i, h := range webhooks {
   300  		mutating[i] = registrationv1.MutatingWebhook{
   301  			Name:                    h.Name,
   302  			ClientConfig:            h.ClientConfig,
   303  			Rules:                   h.Rules,
   304  			FailurePolicy:           h.FailurePolicy,
   305  			MatchPolicy:             h.MatchPolicy,
   306  			NamespaceSelector:       h.NamespaceSelector,
   307  			ObjectSelector:          h.ObjectSelector,
   308  			SideEffects:             h.SideEffects,
   309  			TimeoutSeconds:          h.TimeoutSeconds,
   310  			AdmissionReviewVersions: h.AdmissionReviewVersions,
   311  		}
   312  	}
   313  	return mutating
   314  }
   315  
   316  // NewNonMutatingTestCases returns test cases with a given base url.
   317  // All test cases in NewNonMutatingTestCases have no Patch set in
   318  // AdmissionResponse. The test cases are used by both MutatingAdmissionWebhook
   319  // and ValidatingAdmissionWebhook.
   320  func NewNonMutatingTestCases(url *url.URL) []ValidatingTest {
   321  	policyFail := registrationv1.Fail
   322  	policyIgnore := registrationv1.Ignore
   323  	ccfgURL := urlConfigGenerator{url}.ccfgURL
   324  
   325  	return []ValidatingTest{
   326  		{
   327  			Name: "no match",
   328  			Webhooks: []registrationv1.ValidatingWebhook{{
   329  				Name:         "nomatch",
   330  				ClientConfig: ccfgSVC("disallow"),
   331  				Rules: []registrationv1.RuleWithOperations{{
   332  					Operations: []registrationv1.OperationType{registrationv1.Create},
   333  				}},
   334  				NamespaceSelector:       &metav1.LabelSelector{},
   335  				ObjectSelector:          &metav1.LabelSelector{},
   336  				AdmissionReviewVersions: []string{"v1beta1"},
   337  			}},
   338  			ExpectAllow: true,
   339  		},
   340  		{
   341  			Name: "match & allow",
   342  			Webhooks: []registrationv1.ValidatingWebhook{{
   343  				Name:                    "allow.example.com",
   344  				ClientConfig:            ccfgSVC("allow"),
   345  				Rules:                   matchEverythingRules,
   346  				NamespaceSelector:       &metav1.LabelSelector{},
   347  				ObjectSelector:          &metav1.LabelSelector{},
   348  				AdmissionReviewVersions: []string{"v1beta1"},
   349  			}},
   350  			ExpectAllow:       true,
   351  			ExpectAnnotations: map[string]string{"allow.example.com/key1": "value1"},
   352  		},
   353  		{
   354  			Name: "match & disallow",
   355  			Webhooks: []registrationv1.ValidatingWebhook{{
   356  				Name:                    "disallow",
   357  				ClientConfig:            ccfgSVC("disallow"),
   358  				Rules:                   matchEverythingRules,
   359  				NamespaceSelector:       &metav1.LabelSelector{},
   360  				ObjectSelector:          &metav1.LabelSelector{},
   361  				AdmissionReviewVersions: []string{"v1beta1"},
   362  			}},
   363  			ExpectStatusCode: http.StatusForbidden,
   364  			ErrorContains:    "without explanation",
   365  		},
   366  		{
   367  			Name: "match & disallow ii",
   368  			Webhooks: []registrationv1.ValidatingWebhook{{
   369  				Name:                    "disallowReason",
   370  				ClientConfig:            ccfgSVC("disallowReason"),
   371  				Rules:                   matchEverythingRules,
   372  				NamespaceSelector:       &metav1.LabelSelector{},
   373  				ObjectSelector:          &metav1.LabelSelector{},
   374  				AdmissionReviewVersions: []string{"v1beta1"},
   375  			}},
   376  			ExpectStatusCode: http.StatusForbidden,
   377  			ErrorContains:    "you shall not pass",
   378  		},
   379  		{
   380  			Name: "match & disallow & but allowed because namespaceSelector exempt the ns",
   381  			Webhooks: []registrationv1.ValidatingWebhook{{
   382  				Name:         "disallow",
   383  				ClientConfig: ccfgSVC("disallow"),
   384  				Rules:        newMatchEverythingRules(),
   385  				NamespaceSelector: &metav1.LabelSelector{
   386  					MatchExpressions: []metav1.LabelSelectorRequirement{{
   387  						Key:      "runlevel",
   388  						Values:   []string{"1"},
   389  						Operator: metav1.LabelSelectorOpIn,
   390  					}},
   391  				},
   392  				ObjectSelector:          &metav1.LabelSelector{},
   393  				AdmissionReviewVersions: []string{"v1beta1"},
   394  			}},
   395  
   396  			ExpectAllow: true,
   397  		},
   398  		{
   399  			Name: "match & disallow & but allowed because namespaceSelector exempt the ns ii",
   400  			Webhooks: []registrationv1.ValidatingWebhook{{
   401  				Name:         "disallow",
   402  				ClientConfig: ccfgSVC("disallow"),
   403  				Rules:        newMatchEverythingRules(),
   404  				NamespaceSelector: &metav1.LabelSelector{
   405  					MatchExpressions: []metav1.LabelSelectorRequirement{{
   406  						Key:      "runlevel",
   407  						Values:   []string{"0"},
   408  						Operator: metav1.LabelSelectorOpNotIn,
   409  					}},
   410  				},
   411  				ObjectSelector:          &metav1.LabelSelector{},
   412  				AdmissionReviewVersions: []string{"v1beta1"},
   413  			}},
   414  			ExpectAllow: true,
   415  		},
   416  		{
   417  			Name: "match & fail (but allow because fail open)",
   418  			Webhooks: []registrationv1.ValidatingWebhook{{
   419  				Name:                    "internalErr A",
   420  				ClientConfig:            ccfgSVC("internalErr"),
   421  				Rules:                   matchEverythingRules,
   422  				NamespaceSelector:       &metav1.LabelSelector{},
   423  				ObjectSelector:          &metav1.LabelSelector{},
   424  				FailurePolicy:           &policyIgnore,
   425  				AdmissionReviewVersions: []string{"v1beta1"},
   426  			}, {
   427  				Name:                    "internalErr B",
   428  				ClientConfig:            ccfgSVC("internalErr"),
   429  				Rules:                   matchEverythingRules,
   430  				NamespaceSelector:       &metav1.LabelSelector{},
   431  				ObjectSelector:          &metav1.LabelSelector{},
   432  				FailurePolicy:           &policyIgnore,
   433  				AdmissionReviewVersions: []string{"v1beta1"},
   434  			}, {
   435  				Name:                    "internalErr C",
   436  				ClientConfig:            ccfgSVC("internalErr"),
   437  				Rules:                   matchEverythingRules,
   438  				NamespaceSelector:       &metav1.LabelSelector{},
   439  				ObjectSelector:          &metav1.LabelSelector{},
   440  				FailurePolicy:           &policyIgnore,
   441  				AdmissionReviewVersions: []string{"v1beta1"},
   442  			}},
   443  
   444  			SkipBenchmark: true,
   445  			ExpectAllow:   true,
   446  			ExpectAnnotations: map[string]string{
   447  				"failed-open.validating.webhook.admission.k8s.io/round_0_index_0": "internalErr A",
   448  				"failed-open.validating.webhook.admission.k8s.io/round_0_index_1": "internalErr B",
   449  				"failed-open.validating.webhook.admission.k8s.io/round_0_index_2": "internalErr C",
   450  			},
   451  		},
   452  		{
   453  			Name: "match & fail (but disallow because fail close on nil FailurePolicy)",
   454  			Webhooks: []registrationv1.ValidatingWebhook{{
   455  				Name:                    "internalErr A",
   456  				ClientConfig:            ccfgSVC("internalErr"),
   457  				NamespaceSelector:       &metav1.LabelSelector{},
   458  				ObjectSelector:          &metav1.LabelSelector{},
   459  				Rules:                   matchEverythingRules,
   460  				AdmissionReviewVersions: []string{"v1beta1"},
   461  			}, {
   462  				Name:                    "internalErr B",
   463  				ClientConfig:            ccfgSVC("internalErr"),
   464  				NamespaceSelector:       &metav1.LabelSelector{},
   465  				ObjectSelector:          &metav1.LabelSelector{},
   466  				Rules:                   matchEverythingRules,
   467  				AdmissionReviewVersions: []string{"v1beta1"},
   468  			}, {
   469  				Name:                    "internalErr C",
   470  				ClientConfig:            ccfgSVC("internalErr"),
   471  				NamespaceSelector:       &metav1.LabelSelector{},
   472  				ObjectSelector:          &metav1.LabelSelector{},
   473  				Rules:                   matchEverythingRules,
   474  				AdmissionReviewVersions: []string{"v1beta1"},
   475  			}},
   476  			ExpectStatusCode: http.StatusInternalServerError,
   477  			ExpectAllow:      false,
   478  		},
   479  		{
   480  			Name: "match & fail (but fail because fail closed)",
   481  			Webhooks: []registrationv1.ValidatingWebhook{{
   482  				Name:                    "internalErr A",
   483  				ClientConfig:            ccfgSVC("internalErr"),
   484  				Rules:                   matchEverythingRules,
   485  				NamespaceSelector:       &metav1.LabelSelector{},
   486  				ObjectSelector:          &metav1.LabelSelector{},
   487  				FailurePolicy:           &policyFail,
   488  				AdmissionReviewVersions: []string{"v1beta1"},
   489  			}, {
   490  				Name:                    "internalErr B",
   491  				ClientConfig:            ccfgSVC("internalErr"),
   492  				Rules:                   matchEverythingRules,
   493  				NamespaceSelector:       &metav1.LabelSelector{},
   494  				ObjectSelector:          &metav1.LabelSelector{},
   495  				FailurePolicy:           &policyFail,
   496  				AdmissionReviewVersions: []string{"v1beta1"},
   497  			}, {
   498  				Name:                    "internalErr C",
   499  				ClientConfig:            ccfgSVC("internalErr"),
   500  				Rules:                   matchEverythingRules,
   501  				NamespaceSelector:       &metav1.LabelSelector{},
   502  				ObjectSelector:          &metav1.LabelSelector{},
   503  				FailurePolicy:           &policyFail,
   504  				AdmissionReviewVersions: []string{"v1beta1"},
   505  			}},
   506  			ExpectStatusCode: http.StatusInternalServerError,
   507  			ExpectAllow:      false,
   508  		},
   509  		{
   510  			Name: "match & allow (url)",
   511  			Webhooks: []registrationv1.ValidatingWebhook{{
   512  				Name:                    "allow.example.com",
   513  				ClientConfig:            ccfgURL("allow"),
   514  				Rules:                   matchEverythingRules,
   515  				NamespaceSelector:       &metav1.LabelSelector{},
   516  				ObjectSelector:          &metav1.LabelSelector{},
   517  				AdmissionReviewVersions: []string{"v1beta1"},
   518  			}},
   519  			ExpectAllow:       true,
   520  			ExpectAnnotations: map[string]string{"allow.example.com/key1": "value1"},
   521  		},
   522  		{
   523  			Name: "match & disallow (url)",
   524  			Webhooks: []registrationv1.ValidatingWebhook{{
   525  				Name:                    "disallow",
   526  				ClientConfig:            ccfgURL("disallow"),
   527  				Rules:                   matchEverythingRules,
   528  				NamespaceSelector:       &metav1.LabelSelector{},
   529  				ObjectSelector:          &metav1.LabelSelector{},
   530  				AdmissionReviewVersions: []string{"v1beta1"},
   531  			}},
   532  			ExpectStatusCode: http.StatusForbidden,
   533  			ErrorContains:    "without explanation",
   534  		}, {
   535  			Name: "absent response and fail open",
   536  			Webhooks: []registrationv1.ValidatingWebhook{{
   537  				Name:                    "nilResponse",
   538  				ClientConfig:            ccfgURL("nilResponse"),
   539  				FailurePolicy:           &policyIgnore,
   540  				Rules:                   matchEverythingRules,
   541  				NamespaceSelector:       &metav1.LabelSelector{},
   542  				ObjectSelector:          &metav1.LabelSelector{},
   543  				AdmissionReviewVersions: []string{"v1beta1"},
   544  			}},
   545  			SkipBenchmark:     true,
   546  			ExpectAllow:       true,
   547  			ExpectAnnotations: map[string]string{"failed-open.validating.webhook.admission.k8s.io/round_0_index_0": "nilResponse"},
   548  		},
   549  		{
   550  			Name: "absent response and fail closed",
   551  			Webhooks: []registrationv1.ValidatingWebhook{{
   552  				Name:                    "nilResponse",
   553  				ClientConfig:            ccfgURL("nilResponse"),
   554  				FailurePolicy:           &policyFail,
   555  				Rules:                   matchEverythingRules,
   556  				NamespaceSelector:       &metav1.LabelSelector{},
   557  				ObjectSelector:          &metav1.LabelSelector{},
   558  				AdmissionReviewVersions: []string{"v1beta1"},
   559  			}},
   560  			ExpectStatusCode: http.StatusInternalServerError,
   561  			ErrorContains:    "webhook response was absent",
   562  		},
   563  		{
   564  			Name: "no match dry run",
   565  			Webhooks: []registrationv1.ValidatingWebhook{{
   566  				Name:         "nomatch",
   567  				ClientConfig: ccfgSVC("allow"),
   568  				Rules: []registrationv1.RuleWithOperations{{
   569  					Operations: []registrationv1.OperationType{registrationv1.Create},
   570  				}},
   571  				NamespaceSelector:       &metav1.LabelSelector{},
   572  				ObjectSelector:          &metav1.LabelSelector{},
   573  				SideEffects:             &sideEffectsSome,
   574  				AdmissionReviewVersions: []string{"v1beta1"},
   575  			}},
   576  			IsDryRun:    true,
   577  			ExpectAllow: true,
   578  		},
   579  		{
   580  			Name: "match dry run side effects Unknown",
   581  			Webhooks: []registrationv1.ValidatingWebhook{{
   582  				Name:                    "allow",
   583  				ClientConfig:            ccfgSVC("allow"),
   584  				Rules:                   matchEverythingRules,
   585  				NamespaceSelector:       &metav1.LabelSelector{},
   586  				ObjectSelector:          &metav1.LabelSelector{},
   587  				SideEffects:             &sideEffectsUnknown,
   588  				AdmissionReviewVersions: []string{"v1beta1"},
   589  			}},
   590  			IsDryRun:         true,
   591  			ExpectStatusCode: http.StatusBadRequest,
   592  			ErrorContains:    "does not support dry run",
   593  		},
   594  		{
   595  			Name: "match dry run side effects None",
   596  			Webhooks: []registrationv1.ValidatingWebhook{{
   597  				Name:                    "allow",
   598  				ClientConfig:            ccfgSVC("allow"),
   599  				Rules:                   matchEverythingRules,
   600  				NamespaceSelector:       &metav1.LabelSelector{},
   601  				ObjectSelector:          &metav1.LabelSelector{},
   602  				SideEffects:             &sideEffectsNone,
   603  				AdmissionReviewVersions: []string{"v1beta1"},
   604  			}},
   605  			IsDryRun:          true,
   606  			ExpectAllow:       true,
   607  			ExpectAnnotations: map[string]string{"allow/key1": "value1"},
   608  		},
   609  		{
   610  			Name: "match dry run side effects Some",
   611  			Webhooks: []registrationv1.ValidatingWebhook{{
   612  				Name:                    "allow",
   613  				ClientConfig:            ccfgSVC("allow"),
   614  				Rules:                   matchEverythingRules,
   615  				NamespaceSelector:       &metav1.LabelSelector{},
   616  				ObjectSelector:          &metav1.LabelSelector{},
   617  				SideEffects:             &sideEffectsSome,
   618  				AdmissionReviewVersions: []string{"v1beta1"},
   619  			}},
   620  			IsDryRun:         true,
   621  			ExpectStatusCode: http.StatusBadRequest,
   622  			ErrorContains:    "does not support dry run",
   623  		},
   624  		{
   625  			Name: "match dry run side effects NoneOnDryRun",
   626  			Webhooks: []registrationv1.ValidatingWebhook{{
   627  				Name:                    "allow",
   628  				ClientConfig:            ccfgSVC("allow"),
   629  				Rules:                   matchEverythingRules,
   630  				NamespaceSelector:       &metav1.LabelSelector{},
   631  				ObjectSelector:          &metav1.LabelSelector{},
   632  				SideEffects:             &sideEffectsNoneOnDryRun,
   633  				AdmissionReviewVersions: []string{"v1beta1"},
   634  			}},
   635  			IsDryRun:          true,
   636  			ExpectAllow:       true,
   637  			ExpectAnnotations: map[string]string{"allow/key1": "value1"},
   638  		},
   639  		{
   640  			Name: "illegal annotation format",
   641  			Webhooks: []registrationv1.ValidatingWebhook{{
   642  				Name:                    "invalidAnnotation",
   643  				ClientConfig:            ccfgURL("invalidAnnotation"),
   644  				Rules:                   matchEverythingRules,
   645  				NamespaceSelector:       &metav1.LabelSelector{},
   646  				ObjectSelector:          &metav1.LabelSelector{},
   647  				AdmissionReviewVersions: []string{"v1beta1"},
   648  			}},
   649  			ExpectAllow: true,
   650  		},
   651  		{
   652  			Name: "skip webhook whose objectSelector does not match",
   653  			Webhooks: []registrationv1.ValidatingWebhook{{
   654  				Name:                    "allow.example.com",
   655  				ClientConfig:            ccfgSVC("allow"),
   656  				Rules:                   matchEverythingRules,
   657  				NamespaceSelector:       &metav1.LabelSelector{},
   658  				ObjectSelector:          &metav1.LabelSelector{},
   659  				AdmissionReviewVersions: []string{"v1beta1"},
   660  			}, {
   661  				Name:              "shouldNotBeCalled",
   662  				ClientConfig:      ccfgSVC("shouldNotBeCalled"),
   663  				NamespaceSelector: &metav1.LabelSelector{},
   664  				ObjectSelector: &metav1.LabelSelector{
   665  					MatchLabels: map[string]string{
   666  						"label": "nonexistent",
   667  					},
   668  				},
   669  				Rules:                   matchEverythingRules,
   670  				AdmissionReviewVersions: []string{"v1beta1"},
   671  			}},
   672  			ExpectAllow:       true,
   673  			ExpectAnnotations: map[string]string{"allow.example.com/key1": "value1"},
   674  		},
   675  		{
   676  			Name: "skip webhook whose objectSelector does not match CRD's labels",
   677  			Webhooks: []registrationv1.ValidatingWebhook{{
   678  				Name:                    "allow.example.com",
   679  				ClientConfig:            ccfgSVC("allow"),
   680  				Rules:                   matchEverythingRules,
   681  				NamespaceSelector:       &metav1.LabelSelector{},
   682  				ObjectSelector:          &metav1.LabelSelector{},
   683  				AdmissionReviewVersions: []string{"v1beta1"},
   684  			}, {
   685  				Name:              "shouldNotBeCalled",
   686  				ClientConfig:      ccfgSVC("shouldNotBeCalled"),
   687  				NamespaceSelector: &metav1.LabelSelector{},
   688  				ObjectSelector: &metav1.LabelSelector{
   689  					MatchLabels: map[string]string{
   690  						"label": "nonexistent",
   691  					},
   692  				},
   693  				Rules:                   matchEverythingRules,
   694  				AdmissionReviewVersions: []string{"v1beta1"},
   695  			}},
   696  			IsCRD:             true,
   697  			ExpectAllow:       true,
   698  			ExpectAnnotations: map[string]string{"allow.example.com/key1": "value1"},
   699  		},
   700  		// No need to test everything with the url case, since only the
   701  		// connection is different.
   702  	}
   703  }
   704  
   705  // NewNonMutatingPanicTestCases returns test cases with a given base url.
   706  // All test cases in NewNonMutatingTestCases have no Patch set in
   707  // AdmissionResponse. The expected responses are set for panic handling.
   708  func NewNonMutatingPanicTestCases(url *url.URL) []ValidatingTest {
   709  	policyIgnore := registrationv1.Ignore
   710  	policyFail := registrationv1.Fail
   711  
   712  	return []ValidatingTest{
   713  		{
   714  			Name: "match & allow, but panic",
   715  			Webhooks: []registrationv1.ValidatingWebhook{{
   716  				Name:                    "allow.example.com",
   717  				ClientConfig:            ccfgSVC("allow"),
   718  				Rules:                   matchEverythingRules,
   719  				NamespaceSelector:       &metav1.LabelSelector{},
   720  				ObjectSelector:          &metav1.LabelSelector{},
   721  				AdmissionReviewVersions: []string{"v1beta1"},
   722  			}},
   723  			ExpectStatusCode:  http.StatusForbidden,
   724  			ErrorContains:     "ValidatingAdmissionWebhook/allow.example.com has panicked: Start panicking!",
   725  			ExpectAnnotations: map[string]string{},
   726  		},
   727  		{
   728  			Name: "match & fail (but allow because fail open)",
   729  			Webhooks: []registrationv1.ValidatingWebhook{{
   730  				Name:                    "internalErr A",
   731  				ClientConfig:            ccfgSVC("internalErr"),
   732  				Rules:                   matchEverythingRules,
   733  				NamespaceSelector:       &metav1.LabelSelector{},
   734  				ObjectSelector:          &metav1.LabelSelector{},
   735  				FailurePolicy:           &policyIgnore,
   736  				AdmissionReviewVersions: []string{"v1beta1"},
   737  			}, {
   738  				Name:                    "internalErr B",
   739  				ClientConfig:            ccfgSVC("internalErr"),
   740  				Rules:                   matchEverythingRules,
   741  				NamespaceSelector:       &metav1.LabelSelector{},
   742  				ObjectSelector:          &metav1.LabelSelector{},
   743  				FailurePolicy:           &policyIgnore,
   744  				AdmissionReviewVersions: []string{"v1beta1"},
   745  			}, {
   746  				Name:                    "internalErr C",
   747  				ClientConfig:            ccfgSVC("internalErr"),
   748  				Rules:                   matchEverythingRules,
   749  				NamespaceSelector:       &metav1.LabelSelector{},
   750  				ObjectSelector:          &metav1.LabelSelector{},
   751  				FailurePolicy:           &policyIgnore,
   752  				AdmissionReviewVersions: []string{"v1beta1"},
   753  			}},
   754  
   755  			SkipBenchmark: true,
   756  			ExpectAllow:   true,
   757  			ExpectAnnotations: map[string]string{
   758  				"failed-open.validating.webhook.admission.k8s.io/round_0_index_0": "internalErr A",
   759  				"failed-open.validating.webhook.admission.k8s.io/round_0_index_1": "internalErr B",
   760  				"failed-open.validating.webhook.admission.k8s.io/round_0_index_2": "internalErr C",
   761  			},
   762  		},
   763  		{
   764  			Name: "match & fail (but fail because fail closed)",
   765  			Webhooks: []registrationv1.ValidatingWebhook{{
   766  				Name:                    "internalErr A",
   767  				ClientConfig:            ccfgSVC("internalErr"),
   768  				Rules:                   matchEverythingRules,
   769  				NamespaceSelector:       &metav1.LabelSelector{},
   770  				ObjectSelector:          &metav1.LabelSelector{},
   771  				FailurePolicy:           &policyFail,
   772  				AdmissionReviewVersions: []string{"v1beta1"},
   773  			}, {
   774  				Name:                    "internalErr B",
   775  				ClientConfig:            ccfgSVC("internalErr"),
   776  				Rules:                   matchEverythingRules,
   777  				NamespaceSelector:       &metav1.LabelSelector{},
   778  				ObjectSelector:          &metav1.LabelSelector{},
   779  				FailurePolicy:           &policyFail,
   780  				AdmissionReviewVersions: []string{"v1beta1"},
   781  			}, {
   782  				Name:                    "internalErr C",
   783  				ClientConfig:            ccfgSVC("internalErr"),
   784  				Rules:                   matchEverythingRules,
   785  				NamespaceSelector:       &metav1.LabelSelector{},
   786  				ObjectSelector:          &metav1.LabelSelector{},
   787  				FailurePolicy:           &policyFail,
   788  				AdmissionReviewVersions: []string{"v1beta1"},
   789  			}},
   790  			ExpectStatusCode: http.StatusInternalServerError,
   791  			ExpectAllow:      false,
   792  			ErrorContains:    " has panicked: Start panicking!",
   793  		},
   794  	}
   795  }
   796  
   797  func mutationAnnotationValue(configuration, webhook string, mutated bool) string {
   798  	return fmt.Sprintf(`{"configuration":"%s","webhook":"%s","mutated":%t}`, configuration, webhook, mutated)
   799  }
   800  
   801  func patchAnnotationValue(configuration, webhook string, patch string) string {
   802  	return strings.Replace(fmt.Sprintf(`{"configuration": "%s", "webhook": "%s", "patch": %s, "patchType": "JSONPatch"}`, configuration, webhook, patch), " ", "", -1)
   803  }
   804  
   805  // NewMutatingTestCases returns test cases with a given base url.
   806  // All test cases in NewMutatingTestCases have Patch set in
   807  // AdmissionResponse. The test cases are only used by both MutatingAdmissionWebhook.
   808  func NewMutatingTestCases(url *url.URL, configurationName string) []MutatingTest {
   809  	return []MutatingTest{
   810  		{
   811  			Name: "match & remove label",
   812  			Webhooks: []registrationv1.MutatingWebhook{{
   813  				Name:                    "removelabel.example.com",
   814  				ClientConfig:            ccfgSVC("removeLabel"),
   815  				Rules:                   matchEverythingRules,
   816  				NamespaceSelector:       &metav1.LabelSelector{},
   817  				ObjectSelector:          &metav1.LabelSelector{},
   818  				AdmissionReviewVersions: []string{"v1beta1"},
   819  			}},
   820  			ExpectAllow:      true,
   821  			AdditionalLabels: map[string]string{"remove": "me"},
   822  			ExpectLabels:     map[string]string{"pod.name": "my-pod"},
   823  			ExpectAnnotations: map[string]string{
   824  				"removelabel.example.com/key1":                      "value1",
   825  				"mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "removelabel.example.com", true),
   826  				"patch.webhook.admission.k8s.io/round_0_index_0":    patchAnnotationValue(configurationName, "removelabel.example.com", `[{"op": "remove", "path": "/metadata/labels/remove"}]`),
   827  			},
   828  		},
   829  		{
   830  			Name: "match & add label",
   831  			Webhooks: []registrationv1.MutatingWebhook{{
   832  				Name:                    "addLabel",
   833  				ClientConfig:            ccfgSVC("addLabel"),
   834  				Rules:                   matchEverythingRules,
   835  				NamespaceSelector:       &metav1.LabelSelector{},
   836  				ObjectSelector:          &metav1.LabelSelector{},
   837  				AdmissionReviewVersions: []string{"v1beta1"},
   838  			}},
   839  			ExpectAllow:  true,
   840  			ExpectLabels: map[string]string{"pod.name": "my-pod", "added": "test"},
   841  			ExpectAnnotations: map[string]string{
   842  				"mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "addLabel", true),
   843  				"patch.webhook.admission.k8s.io/round_0_index_0":    patchAnnotationValue(configurationName, "addLabel", `[{"op": "add", "path": "/metadata/labels/added", "value": "test"}]`),
   844  			},
   845  		},
   846  		{
   847  			Name: "match CRD & add label",
   848  			Webhooks: []registrationv1.MutatingWebhook{{
   849  				Name:                    "addLabel",
   850  				ClientConfig:            ccfgSVC("addLabel"),
   851  				Rules:                   matchEverythingRules,
   852  				NamespaceSelector:       &metav1.LabelSelector{},
   853  				ObjectSelector:          &metav1.LabelSelector{},
   854  				AdmissionReviewVersions: []string{"v1beta1"},
   855  			}},
   856  			IsCRD:        true,
   857  			ExpectAllow:  true,
   858  			ExpectLabels: map[string]string{"crd.name": "my-test-crd", "added": "test"},
   859  			ExpectAnnotations: map[string]string{
   860  				"mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "addLabel", true),
   861  				"patch.webhook.admission.k8s.io/round_0_index_0":    patchAnnotationValue(configurationName, "addLabel", `[{"op": "add", "path": "/metadata/labels/added", "value": "test"}]`),
   862  			},
   863  		},
   864  		{
   865  			Name: "match CRD & remove label",
   866  			Webhooks: []registrationv1.MutatingWebhook{{
   867  				Name:                    "removelabel.example.com",
   868  				ClientConfig:            ccfgSVC("removeLabel"),
   869  				Rules:                   matchEverythingRules,
   870  				NamespaceSelector:       &metav1.LabelSelector{},
   871  				ObjectSelector:          &metav1.LabelSelector{},
   872  				AdmissionReviewVersions: []string{"v1beta1"},
   873  			}},
   874  			IsCRD:            true,
   875  			ExpectAllow:      true,
   876  			AdditionalLabels: map[string]string{"remove": "me"},
   877  			ExpectLabels:     map[string]string{"crd.name": "my-test-crd"},
   878  			ExpectAnnotations: map[string]string{
   879  				"removelabel.example.com/key1":                      "value1",
   880  				"mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "removelabel.example.com", true),
   881  				"patch.webhook.admission.k8s.io/round_0_index_0":    patchAnnotationValue(configurationName, "removelabel.example.com", `[{"op": "remove", "path": "/metadata/labels/remove"}]`),
   882  			},
   883  		},
   884  		{
   885  			Name: "match & invalid mutation",
   886  			Webhooks: []registrationv1.MutatingWebhook{{
   887  				Name:                    "invalidMutation",
   888  				ClientConfig:            ccfgSVC("invalidMutation"),
   889  				Rules:                   matchEverythingRules,
   890  				NamespaceSelector:       &metav1.LabelSelector{},
   891  				ObjectSelector:          &metav1.LabelSelector{},
   892  				AdmissionReviewVersions: []string{"v1beta1"},
   893  			}},
   894  			ExpectStatusCode: http.StatusInternalServerError,
   895  			ErrorContains:    "invalid character",
   896  			ExpectAnnotations: map[string]string{
   897  				"mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "invalidMutation", false),
   898  			},
   899  		},
   900  		{
   901  			Name: "match & remove label dry run unsupported",
   902  			Webhooks: []registrationv1.MutatingWebhook{{
   903  				Name:                    "removeLabel",
   904  				ClientConfig:            ccfgSVC("removeLabel"),
   905  				Rules:                   matchEverythingRules,
   906  				NamespaceSelector:       &metav1.LabelSelector{},
   907  				ObjectSelector:          &metav1.LabelSelector{},
   908  				SideEffects:             &sideEffectsUnknown,
   909  				AdmissionReviewVersions: []string{"v1beta1"},
   910  			}},
   911  			IsDryRun:         true,
   912  			ExpectStatusCode: http.StatusBadRequest,
   913  			ErrorContains:    "does not support dry run",
   914  			ExpectAnnotations: map[string]string{
   915  				"mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "removeLabel", false),
   916  			},
   917  		},
   918  		{
   919  			Name: "first webhook remove labels, second webhook shouldn't be called",
   920  			Webhooks: []registrationv1.MutatingWebhook{{
   921  				Name:              "removelabel.example.com",
   922  				ClientConfig:      ccfgSVC("removeLabel"),
   923  				Rules:             matchEverythingRules,
   924  				NamespaceSelector: &metav1.LabelSelector{},
   925  				ObjectSelector: &metav1.LabelSelector{
   926  					MatchLabels: map[string]string{
   927  						"remove": "me",
   928  					},
   929  				},
   930  				AdmissionReviewVersions: []string{"v1beta1"},
   931  			}, {
   932  				Name:              "shouldNotBeCalled",
   933  				ClientConfig:      ccfgSVC("shouldNotBeCalled"),
   934  				NamespaceSelector: &metav1.LabelSelector{},
   935  				ObjectSelector: &metav1.LabelSelector{
   936  					MatchLabels: map[string]string{
   937  						"remove": "me",
   938  					},
   939  				},
   940  				Rules:                   matchEverythingRules,
   941  				AdmissionReviewVersions: []string{"v1beta1"},
   942  			}},
   943  			ExpectAllow:      true,
   944  			AdditionalLabels: map[string]string{"remove": "me"},
   945  			ExpectLabels:     map[string]string{"pod.name": "my-pod"},
   946  			ExpectAnnotations: map[string]string{
   947  				"removelabel.example.com/key1":                      "value1",
   948  				"mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "removelabel.example.com", true),
   949  				"patch.webhook.admission.k8s.io/round_0_index_0":    patchAnnotationValue(configurationName, "removelabel.example.com", `[{"op": "remove", "path": "/metadata/labels/remove"}]`),
   950  			},
   951  		},
   952  		{
   953  			Name: "first webhook remove labels from CRD, second webhook shouldn't be called",
   954  			Webhooks: []registrationv1.MutatingWebhook{{
   955  				Name:              "removelabel.example.com",
   956  				ClientConfig:      ccfgSVC("removeLabel"),
   957  				Rules:             matchEverythingRules,
   958  				NamespaceSelector: &metav1.LabelSelector{},
   959  				ObjectSelector: &metav1.LabelSelector{
   960  					MatchLabels: map[string]string{
   961  						"remove": "me",
   962  					},
   963  				},
   964  				AdmissionReviewVersions: []string{"v1beta1"},
   965  			}, {
   966  				Name:              "shouldNotBeCalled",
   967  				ClientConfig:      ccfgSVC("shouldNotBeCalled"),
   968  				NamespaceSelector: &metav1.LabelSelector{},
   969  				ObjectSelector: &metav1.LabelSelector{
   970  					MatchLabels: map[string]string{
   971  						"remove": "me",
   972  					},
   973  				},
   974  				Rules:                   matchEverythingRules,
   975  				AdmissionReviewVersions: []string{"v1beta1"},
   976  			}},
   977  			IsCRD:            true,
   978  			ExpectAllow:      true,
   979  			AdditionalLabels: map[string]string{"remove": "me"},
   980  			ExpectLabels:     map[string]string{"crd.name": "my-test-crd"},
   981  			ExpectAnnotations: map[string]string{
   982  				"removelabel.example.com/key1":                      "value1",
   983  				"mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "removelabel.example.com", true),
   984  				"patch.webhook.admission.k8s.io/round_0_index_0":    patchAnnotationValue(configurationName, "removelabel.example.com", `[{"op": "remove", "path": "/metadata/labels/remove"}]`),
   985  			},
   986  		},
   987  		// No need to test everything with the url case, since only the
   988  		// connection is different.
   989  		{
   990  			Name: "match & reinvoke if needed policy",
   991  			Webhooks: []registrationv1.MutatingWebhook{{
   992  				Name:                    "addLabel",
   993  				ClientConfig:            ccfgSVC("addLabel"),
   994  				Rules:                   matchEverythingRules,
   995  				NamespaceSelector:       &metav1.LabelSelector{},
   996  				ObjectSelector:          &metav1.LabelSelector{},
   997  				AdmissionReviewVersions: []string{"v1beta1"},
   998  				ReinvocationPolicy:      &reinvokeIfNeeded,
   999  			}, {
  1000  				Name:                    "removeLabel",
  1001  				ClientConfig:            ccfgSVC("removeLabel"),
  1002  				Rules:                   matchEverythingRules,
  1003  				NamespaceSelector:       &metav1.LabelSelector{},
  1004  				ObjectSelector:          &metav1.LabelSelector{},
  1005  				AdmissionReviewVersions: []string{"v1beta1"},
  1006  				ReinvocationPolicy:      &reinvokeIfNeeded,
  1007  			}},
  1008  			AdditionalLabels:       map[string]string{"remove": "me"},
  1009  			ExpectAllow:            true,
  1010  			ExpectReinvokeWebhooks: map[string]bool{"addLabel": true},
  1011  			ExpectAnnotations: map[string]string{
  1012  				"mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "addLabel", true),
  1013  				"mutation.webhook.admission.k8s.io/round_0_index_1": mutationAnnotationValue(configurationName, "removeLabel", true),
  1014  				"patch.webhook.admission.k8s.io/round_0_index_0":    patchAnnotationValue(configurationName, "addLabel", `[{"op": "add", "path": "/metadata/labels/added", "value": "test"}]`),
  1015  				"patch.webhook.admission.k8s.io/round_0_index_1":    patchAnnotationValue(configurationName, "removeLabel", `[{"op": "remove", "path": "/metadata/labels/remove"}]`),
  1016  			},
  1017  		},
  1018  		{
  1019  			Name: "match & never reinvoke policy",
  1020  			Webhooks: []registrationv1.MutatingWebhook{{
  1021  				Name:                    "addLabel",
  1022  				ClientConfig:            ccfgSVC("addLabel"),
  1023  				Rules:                   matchEverythingRules,
  1024  				NamespaceSelector:       &metav1.LabelSelector{},
  1025  				ObjectSelector:          &metav1.LabelSelector{},
  1026  				AdmissionReviewVersions: []string{"v1beta1"},
  1027  				ReinvocationPolicy:      &reinvokeNever,
  1028  			}},
  1029  			ExpectAllow:            true,
  1030  			ExpectReinvokeWebhooks: map[string]bool{"addLabel": false},
  1031  			ExpectAnnotations: map[string]string{
  1032  				"mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "addLabel", true),
  1033  				"patch.webhook.admission.k8s.io/round_0_index_0":    patchAnnotationValue(configurationName, "addLabel", `[{"op": "add", "path": "/metadata/labels/added", "value": "test"}]`),
  1034  			},
  1035  		},
  1036  		{
  1037  			Name: "match & never reinvoke policy (by default)",
  1038  			Webhooks: []registrationv1.MutatingWebhook{{
  1039  				Name:                    "addLabel",
  1040  				ClientConfig:            ccfgSVC("addLabel"),
  1041  				Rules:                   matchEverythingRules,
  1042  				NamespaceSelector:       &metav1.LabelSelector{},
  1043  				ObjectSelector:          &metav1.LabelSelector{},
  1044  				AdmissionReviewVersions: []string{"v1beta1"},
  1045  			}},
  1046  			ExpectAllow:            true,
  1047  			ExpectReinvokeWebhooks: map[string]bool{"addLabel": false},
  1048  			ExpectAnnotations: map[string]string{
  1049  				"mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "addLabel", true),
  1050  				"patch.webhook.admission.k8s.io/round_0_index_0":    patchAnnotationValue(configurationName, "addLabel", `[{"op": "add", "path": "/metadata/labels/added", "value": "test"}]`),
  1051  			},
  1052  		},
  1053  		{
  1054  			Name: "match & no reinvoke",
  1055  			Webhooks: []registrationv1.MutatingWebhook{{
  1056  				Name:                    "noop",
  1057  				ClientConfig:            ccfgSVC("noop"),
  1058  				Rules:                   matchEverythingRules,
  1059  				NamespaceSelector:       &metav1.LabelSelector{},
  1060  				ObjectSelector:          &metav1.LabelSelector{},
  1061  				AdmissionReviewVersions: []string{"v1beta1"},
  1062  			}},
  1063  			ExpectAllow: true,
  1064  			ExpectAnnotations: map[string]string{
  1065  				"mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "noop", false),
  1066  			},
  1067  		},
  1068  	}
  1069  }
  1070  
  1071  // CachedTest is a test case for the client manager.
  1072  type CachedTest struct {
  1073  	Name            string
  1074  	Webhooks        []registrationv1.ValidatingWebhook
  1075  	ExpectAllow     bool
  1076  	ExpectCacheMiss bool
  1077  }
  1078  
  1079  // NewCachedClientTestcases returns a set of client manager test cases.
  1080  func NewCachedClientTestcases(url *url.URL) []CachedTest {
  1081  	policyIgnore := registrationv1.Ignore
  1082  	ccfgURL := urlConfigGenerator{url}.ccfgURL
  1083  
  1084  	return []CachedTest{
  1085  		{
  1086  			Name: "uncached: service webhook, path 'allow'",
  1087  			Webhooks: []registrationv1.ValidatingWebhook{{
  1088  				Name:                    "cache1",
  1089  				ClientConfig:            ccfgSVC("allow"),
  1090  				Rules:                   newMatchEverythingRules(),
  1091  				NamespaceSelector:       &metav1.LabelSelector{},
  1092  				ObjectSelector:          &metav1.LabelSelector{},
  1093  				FailurePolicy:           &policyIgnore,
  1094  				AdmissionReviewVersions: []string{"v1beta1"},
  1095  			}},
  1096  			ExpectAllow:     true,
  1097  			ExpectCacheMiss: true,
  1098  		},
  1099  		{
  1100  			Name: "uncached: service webhook, path 'internalErr'",
  1101  			Webhooks: []registrationv1.ValidatingWebhook{{
  1102  				Name:                    "cache2",
  1103  				ClientConfig:            ccfgSVC("internalErr"),
  1104  				Rules:                   newMatchEverythingRules(),
  1105  				NamespaceSelector:       &metav1.LabelSelector{},
  1106  				ObjectSelector:          &metav1.LabelSelector{},
  1107  				FailurePolicy:           &policyIgnore,
  1108  				AdmissionReviewVersions: []string{"v1beta1"},
  1109  			}},
  1110  			ExpectAllow:     true,
  1111  			ExpectCacheMiss: true,
  1112  		},
  1113  		{
  1114  			Name: "cached: service webhook, path 'allow'",
  1115  			Webhooks: []registrationv1.ValidatingWebhook{{
  1116  				Name:                    "cache3",
  1117  				ClientConfig:            ccfgSVC("allow"),
  1118  				Rules:                   newMatchEverythingRules(),
  1119  				NamespaceSelector:       &metav1.LabelSelector{},
  1120  				ObjectSelector:          &metav1.LabelSelector{},
  1121  				FailurePolicy:           &policyIgnore,
  1122  				AdmissionReviewVersions: []string{"v1beta1"},
  1123  			}},
  1124  			ExpectAllow:     true,
  1125  			ExpectCacheMiss: false,
  1126  		},
  1127  		{
  1128  			Name: "uncached: url webhook, path 'allow'",
  1129  			Webhooks: []registrationv1.ValidatingWebhook{{
  1130  				Name:                    "cache4",
  1131  				ClientConfig:            ccfgURL("allow"),
  1132  				Rules:                   newMatchEverythingRules(),
  1133  				NamespaceSelector:       &metav1.LabelSelector{},
  1134  				ObjectSelector:          &metav1.LabelSelector{},
  1135  				FailurePolicy:           &policyIgnore,
  1136  				AdmissionReviewVersions: []string{"v1beta1"},
  1137  			}},
  1138  			ExpectAllow:     true,
  1139  			ExpectCacheMiss: true,
  1140  		},
  1141  		{
  1142  			Name: "cached: url webhook, path 'allow'",
  1143  			Webhooks: []registrationv1.ValidatingWebhook{{
  1144  				Name:                    "cache5",
  1145  				ClientConfig:            ccfgURL("allow"),
  1146  				Rules:                   newMatchEverythingRules(),
  1147  				NamespaceSelector:       &metav1.LabelSelector{},
  1148  				ObjectSelector:          &metav1.LabelSelector{},
  1149  				FailurePolicy:           &policyIgnore,
  1150  				AdmissionReviewVersions: []string{"v1beta1"},
  1151  			}},
  1152  			ExpectAllow:     true,
  1153  			ExpectCacheMiss: false,
  1154  		},
  1155  	}
  1156  }
  1157  
  1158  // ccfgSVC returns a client config using the service reference mechanism.
  1159  func ccfgSVC(urlPath string) registrationv1.WebhookClientConfig {
  1160  	return registrationv1.WebhookClientConfig{
  1161  		Service: &registrationv1.ServiceReference{
  1162  			Name:      "webhook-test",
  1163  			Namespace: "default",
  1164  			Path:      &urlPath,
  1165  		},
  1166  		CABundle: testcerts.CACert,
  1167  	}
  1168  }
  1169  
  1170  func newMatchEverythingRules() []registrationv1.RuleWithOperations {
  1171  	return []registrationv1.RuleWithOperations{{
  1172  		Operations: []registrationv1.OperationType{registrationv1.OperationAll},
  1173  		Rule: registrationv1.Rule{
  1174  			APIGroups:   []string{"*"},
  1175  			APIVersions: []string{"*"},
  1176  			Resources:   []string{"*/*"},
  1177  		},
  1178  	}}
  1179  }
  1180  
  1181  // NewObjectInterfacesForTest returns an ObjectInterfaces appropriate for test cases in this file.
  1182  func NewObjectInterfacesForTest() admission.ObjectInterfaces {
  1183  	scheme := runtime.NewScheme()
  1184  	corev1.AddToScheme(scheme)
  1185  	return admission.NewObjectInterfacesFromScheme(scheme)
  1186  }
  1187  
  1188  // NewValidationDurationTestCases returns test cases for webhook duration test
  1189  func NewValidationDurationTestCases(url *url.URL) []DurationTest {
  1190  	ccfgURL := urlConfigGenerator{url}.ccfgURL
  1191  	webhooks := []registrationv1.ValidatingWebhook{
  1192  		{
  1193  			Name:                    "allow match",
  1194  			ClientConfig:            ccfgURL("allow/100"),
  1195  			Rules:                   matchEverythingRules,
  1196  			NamespaceSelector:       &metav1.LabelSelector{},
  1197  			ObjectSelector:          &metav1.LabelSelector{},
  1198  			AdmissionReviewVersions: []string{"v1beta1"},
  1199  		},
  1200  		{
  1201  			Name:                    "allow no match",
  1202  			ClientConfig:            ccfgURL("allow/200"),
  1203  			NamespaceSelector:       &metav1.LabelSelector{},
  1204  			ObjectSelector:          &metav1.LabelSelector{},
  1205  			AdmissionReviewVersions: []string{"v1beta1"},
  1206  		},
  1207  		{
  1208  			Name:                    "disallow match",
  1209  			ClientConfig:            ccfgURL("disallow/400"),
  1210  			Rules:                   matchEverythingRules,
  1211  			NamespaceSelector:       &metav1.LabelSelector{},
  1212  			ObjectSelector:          &metav1.LabelSelector{},
  1213  			AdmissionReviewVersions: []string{"v1beta1"},
  1214  		},
  1215  		{
  1216  			Name:                    "disallow no match",
  1217  			ClientConfig:            ccfgURL("disallow/800"),
  1218  			NamespaceSelector:       &metav1.LabelSelector{},
  1219  			ObjectSelector:          &metav1.LabelSelector{},
  1220  			AdmissionReviewVersions: []string{"v1beta1"},
  1221  		},
  1222  	}
  1223  
  1224  	return []DurationTest{
  1225  		{
  1226  			Name:                "duration test",
  1227  			IsDryRun:            false,
  1228  			InitContext:         true,
  1229  			Webhooks:            webhooks,
  1230  			ExpectedDurationSum: 500,
  1231  			ExpectedDurationMax: 400,
  1232  		},
  1233  		{
  1234  			Name:                "duration dry run",
  1235  			IsDryRun:            true,
  1236  			InitContext:         true,
  1237  			Webhooks:            webhooks,
  1238  			ExpectedDurationSum: 0,
  1239  			ExpectedDurationMax: 0,
  1240  		},
  1241  		{
  1242  			Name:                "duration no init",
  1243  			IsDryRun:            false,
  1244  			InitContext:         false,
  1245  			Webhooks:            webhooks,
  1246  			ExpectedDurationSum: 0,
  1247  			ExpectedDurationMax: 0,
  1248  		},
  1249  	}
  1250  }