k8s.io/apiserver@v0.31.1/pkg/admission/plugin/webhook/request/admissionreview_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 request
    18  
    19  import (
    20  	"reflect"
    21  	"strings"
    22  	"testing"
    23  
    24  	"github.com/google/go-cmp/cmp"
    25  
    26  	admissionv1 "k8s.io/api/admission/v1"
    27  	admissionv1beta1 "k8s.io/api/admission/v1beta1"
    28  	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
    29  	appsv1 "k8s.io/api/apps/v1"
    30  	authenticationv1 "k8s.io/api/authentication/v1"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/runtime"
    33  	"k8s.io/apimachinery/pkg/runtime/schema"
    34  	"k8s.io/apimachinery/pkg/types"
    35  	"k8s.io/apiserver/pkg/admission"
    36  	"k8s.io/apiserver/pkg/admission/plugin/webhook"
    37  	"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
    38  	"k8s.io/apiserver/pkg/authentication/user"
    39  	utilpointer "k8s.io/utils/pointer"
    40  )
    41  
    42  func TestVerifyAdmissionResponse(t *testing.T) {
    43  	v1beta1JSONPatch := admissionv1beta1.PatchTypeJSONPatch
    44  	v1JSONPatch := admissionv1.PatchTypeJSONPatch
    45  
    46  	emptyv1beta1Patch := admissionv1beta1.PatchType("")
    47  	emptyv1Patch := admissionv1.PatchType("")
    48  
    49  	invalidv1beta1Patch := admissionv1beta1.PatchType("Foo")
    50  	invalidv1Patch := admissionv1.PatchType("Foo")
    51  
    52  	testcases := []struct {
    53  		name     string
    54  		uid      types.UID
    55  		mutating bool
    56  		review   runtime.Object
    57  
    58  		expectAuditAnnotations map[string]string
    59  		expectAllowed          bool
    60  		expectPatch            []byte
    61  		expectPatchType        admissionv1.PatchType
    62  		expectResult           *metav1.Status
    63  		expectErr              string
    64  	}{
    65  		// Allowed validating
    66  		{
    67  			name: "v1beta1 allowed validating",
    68  			uid:  "123",
    69  			review: &admissionv1beta1.AdmissionReview{
    70  				Response: &admissionv1beta1.AdmissionResponse{Allowed: true},
    71  			},
    72  			expectAllowed: true,
    73  		},
    74  		{
    75  			name: "v1 allowed validating",
    76  			uid:  "123",
    77  			review: &admissionv1.AdmissionReview{
    78  				TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"},
    79  				Response: &admissionv1.AdmissionResponse{UID: "123", Allowed: true},
    80  			},
    81  			expectAllowed: true,
    82  		},
    83  		// Allowed mutating
    84  		{
    85  			name:     "v1beta1 allowed mutating",
    86  			uid:      "123",
    87  			mutating: true,
    88  			review: &admissionv1beta1.AdmissionReview{
    89  				Response: &admissionv1beta1.AdmissionResponse{Allowed: true},
    90  			},
    91  			expectAllowed: true,
    92  		},
    93  		{
    94  			name:     "v1 allowed mutating",
    95  			uid:      "123",
    96  			mutating: true,
    97  			review: &admissionv1.AdmissionReview{
    98  				TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"},
    99  				Response: &admissionv1.AdmissionResponse{UID: "123", Allowed: true},
   100  			},
   101  			expectAllowed: true,
   102  		},
   103  
   104  		// Audit annotations
   105  		{
   106  			name: "v1beta1 auditAnnotations",
   107  			uid:  "123",
   108  			review: &admissionv1beta1.AdmissionReview{
   109  				Response: &admissionv1beta1.AdmissionResponse{
   110  					Allowed:          true,
   111  					AuditAnnotations: map[string]string{"foo": "bar"},
   112  				},
   113  			},
   114  			expectAllowed:          true,
   115  			expectAuditAnnotations: map[string]string{"foo": "bar"},
   116  		},
   117  		{
   118  			name: "v1 auditAnnotations",
   119  			uid:  "123",
   120  			review: &admissionv1.AdmissionReview{
   121  				TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"},
   122  				Response: &admissionv1.AdmissionResponse{
   123  					UID:              "123",
   124  					Allowed:          true,
   125  					AuditAnnotations: map[string]string{"foo": "bar"},
   126  				},
   127  			},
   128  			expectAllowed:          true,
   129  			expectAuditAnnotations: map[string]string{"foo": "bar"},
   130  		},
   131  
   132  		// Patch
   133  		{
   134  			name:     "v1beta1 patch",
   135  			uid:      "123",
   136  			mutating: true,
   137  			review: &admissionv1beta1.AdmissionReview{
   138  				Response: &admissionv1beta1.AdmissionResponse{
   139  					Allowed: true,
   140  					Patch:   []byte(`[{"op":"add","path":"/foo","value":"bar"}]`),
   141  				},
   142  			},
   143  			expectAllowed:   true,
   144  			expectPatch:     []byte(`[{"op":"add","path":"/foo","value":"bar"}]`),
   145  			expectPatchType: "JSONPatch",
   146  		},
   147  		{
   148  			name:     "v1 patch",
   149  			uid:      "123",
   150  			mutating: true,
   151  			review: &admissionv1.AdmissionReview{
   152  				TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"},
   153  				Response: &admissionv1.AdmissionResponse{
   154  					UID:       "123",
   155  					Allowed:   true,
   156  					Patch:     []byte(`[{"op":"add","path":"/foo","value":"bar"}]`),
   157  					PatchType: &v1JSONPatch,
   158  				},
   159  			},
   160  			expectAllowed:   true,
   161  			expectPatch:     []byte(`[{"op":"add","path":"/foo","value":"bar"}]`),
   162  			expectPatchType: "JSONPatch",
   163  		},
   164  
   165  		// Result
   166  		{
   167  			name: "v1beta1 result",
   168  			uid:  "123",
   169  			review: &admissionv1beta1.AdmissionReview{
   170  				Response: &admissionv1beta1.AdmissionResponse{
   171  					Allowed: false,
   172  					Result:  &metav1.Status{Status: "Failure", Message: "Foo", Code: 401},
   173  				},
   174  			},
   175  			expectAllowed: false,
   176  			expectResult:  &metav1.Status{Status: "Failure", Message: "Foo", Code: 401},
   177  		},
   178  		{
   179  			name: "v1 result",
   180  			uid:  "123",
   181  			review: &admissionv1.AdmissionReview{
   182  				TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"},
   183  				Response: &admissionv1.AdmissionResponse{
   184  					UID:     "123",
   185  					Allowed: false,
   186  					Result:  &metav1.Status{Status: "Failure", Message: "Foo", Code: 401},
   187  				},
   188  			},
   189  			expectAllowed: false,
   190  			expectResult:  &metav1.Status{Status: "Failure", Message: "Foo", Code: 401},
   191  		},
   192  
   193  		// Missing response
   194  		{
   195  			name:      "v1beta1 no response",
   196  			uid:       "123",
   197  			review:    &admissionv1beta1.AdmissionReview{},
   198  			expectErr: "response was absent",
   199  		},
   200  		{
   201  			name: "v1 no response",
   202  			uid:  "123",
   203  			review: &admissionv1.AdmissionReview{
   204  				TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"},
   205  			},
   206  			expectErr: "response was absent",
   207  		},
   208  
   209  		// v1 invalid responses
   210  		{
   211  			name: "v1 wrong group",
   212  			uid:  "123",
   213  			review: &admissionv1.AdmissionReview{
   214  				TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io2/v1", Kind: "AdmissionReview"},
   215  				Response: &admissionv1.AdmissionResponse{
   216  					UID:     "123",
   217  					Allowed: true,
   218  				},
   219  			},
   220  			expectErr: "expected webhook response of admission.k8s.io/v1, Kind=AdmissionReview",
   221  		},
   222  		{
   223  			name: "v1 wrong version",
   224  			uid:  "123",
   225  			review: &admissionv1.AdmissionReview{
   226  				TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v2", Kind: "AdmissionReview"},
   227  				Response: &admissionv1.AdmissionResponse{
   228  					UID:     "123",
   229  					Allowed: true,
   230  				},
   231  			},
   232  			expectErr: "expected webhook response of admission.k8s.io/v1, Kind=AdmissionReview",
   233  		},
   234  		{
   235  			name: "v1 wrong kind",
   236  			uid:  "123",
   237  			review: &admissionv1.AdmissionReview{
   238  				TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview2"},
   239  				Response: &admissionv1.AdmissionResponse{
   240  					UID:     "123",
   241  					Allowed: true,
   242  				},
   243  			},
   244  			expectErr: "expected webhook response of admission.k8s.io/v1, Kind=AdmissionReview",
   245  		},
   246  		{
   247  			name: "v1 wrong uid",
   248  			uid:  "123",
   249  			review: &admissionv1.AdmissionReview{
   250  				TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"},
   251  				Response: &admissionv1.AdmissionResponse{
   252  					UID:     "1234",
   253  					Allowed: true,
   254  				},
   255  			},
   256  			expectErr: `expected response.uid="123"`,
   257  		},
   258  		{
   259  			name:     "v1 patch without patch type",
   260  			uid:      "123",
   261  			mutating: true,
   262  			review: &admissionv1.AdmissionReview{
   263  				TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"},
   264  				Response: &admissionv1.AdmissionResponse{
   265  					UID:     "123",
   266  					Allowed: true,
   267  					Patch:   []byte(`[{"op":"add","path":"/foo","value":"bar"}]`),
   268  				},
   269  			},
   270  			expectErr: `webhook returned response.patch but not response.patchType`,
   271  		},
   272  		{
   273  			name:     "v1 patch type without patch",
   274  			uid:      "123",
   275  			mutating: true,
   276  			review: &admissionv1.AdmissionReview{
   277  				TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"},
   278  				Response: &admissionv1.AdmissionResponse{
   279  					UID:       "123",
   280  					Allowed:   true,
   281  					PatchType: &v1JSONPatch,
   282  				},
   283  			},
   284  			expectErr: `webhook returned response.patchType but not response.patch`,
   285  		},
   286  		{
   287  			name:     "v1 empty patch type",
   288  			uid:      "123",
   289  			mutating: true,
   290  			review: &admissionv1.AdmissionReview{
   291  				TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"},
   292  				Response: &admissionv1.AdmissionResponse{
   293  					UID:       "123",
   294  					Allowed:   true,
   295  					Patch:     []byte(`[{"op":"add","path":"/foo","value":"bar"}]`),
   296  					PatchType: &emptyv1Patch,
   297  				},
   298  			},
   299  			expectErr: `webhook returned invalid response.patchType of ""`,
   300  		},
   301  		{
   302  			name:     "v1 invalid patch type",
   303  			uid:      "123",
   304  			mutating: true,
   305  			review: &admissionv1.AdmissionReview{
   306  				TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"},
   307  				Response: &admissionv1.AdmissionResponse{
   308  					UID:       "123",
   309  					Allowed:   true,
   310  					Patch:     []byte(`[{"op":"add","path":"/foo","value":"bar"}]`),
   311  					PatchType: &invalidv1Patch,
   312  				},
   313  			},
   314  			expectAllowed:   true,
   315  			expectPatch:     []byte(`[{"op":"add","path":"/foo","value":"bar"}]`),
   316  			expectPatchType: invalidv1Patch, // invalid patch types are caught when the mutating dispatcher evaluates the patch
   317  		},
   318  		{
   319  			name:     "v1 patch for validating webhook",
   320  			uid:      "123",
   321  			mutating: false,
   322  			review: &admissionv1.AdmissionReview{
   323  				TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"},
   324  				Response: &admissionv1.AdmissionResponse{
   325  					UID:     "123",
   326  					Allowed: true,
   327  					Patch:   []byte(`[{"op":"add","path":"/foo","value":"bar"}]`),
   328  				},
   329  			},
   330  			expectErr: `validating webhook may not return response.patch`,
   331  		},
   332  		{
   333  			name:     "v1 patch type for validating webhook",
   334  			uid:      "123",
   335  			mutating: false,
   336  			review: &admissionv1.AdmissionReview{
   337  				TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"},
   338  				Response: &admissionv1.AdmissionResponse{
   339  					UID:       "123",
   340  					Allowed:   true,
   341  					PatchType: &invalidv1Patch,
   342  				},
   343  			},
   344  			expectErr: `validating webhook may not return response.patchType`,
   345  		},
   346  
   347  		// v1beta1 invalid responses that we have to allow/fixup for compatibility
   348  		{
   349  			name: "v1beta1 wrong group/version/kind",
   350  			uid:  "123",
   351  			review: &admissionv1beta1.AdmissionReview{
   352  				TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io2/v2", Kind: "AdmissionReview2"},
   353  				Response: &admissionv1beta1.AdmissionResponse{
   354  					Allowed: true,
   355  				},
   356  			},
   357  			expectAllowed: true,
   358  		},
   359  		{
   360  			name: "v1beta1 wrong uid",
   361  			uid:  "123",
   362  			review: &admissionv1beta1.AdmissionReview{
   363  				Response: &admissionv1beta1.AdmissionResponse{
   364  					UID:     "1234",
   365  					Allowed: true,
   366  				},
   367  			},
   368  			expectAllowed: true,
   369  		},
   370  		{
   371  			name:     "v1beta1 validating returns patch/patchType",
   372  			uid:      "123",
   373  			mutating: false,
   374  			review: &admissionv1beta1.AdmissionReview{
   375  				Response: &admissionv1beta1.AdmissionResponse{
   376  					UID:       "1234",
   377  					Allowed:   true,
   378  					Patch:     []byte(`[{"op":"add","path":"/foo","value":"bar"}]`),
   379  					PatchType: &v1beta1JSONPatch,
   380  				},
   381  			},
   382  			expectAllowed: true,
   383  		},
   384  		{
   385  			name:     "v1beta1 empty patch type",
   386  			uid:      "123",
   387  			mutating: true,
   388  			review: &admissionv1beta1.AdmissionReview{
   389  				Response: &admissionv1beta1.AdmissionResponse{
   390  					Allowed:   true,
   391  					Patch:     []byte(`[{"op":"add","path":"/foo","value":"bar"}]`),
   392  					PatchType: &emptyv1beta1Patch,
   393  				},
   394  			},
   395  			expectAllowed:   true,
   396  			expectPatch:     []byte(`[{"op":"add","path":"/foo","value":"bar"}]`),
   397  			expectPatchType: admissionv1.PatchTypeJSONPatch,
   398  		},
   399  		{
   400  			name:     "v1beta1 invalid patchType",
   401  			uid:      "123",
   402  			mutating: true,
   403  			review: &admissionv1beta1.AdmissionReview{
   404  				Response: &admissionv1beta1.AdmissionResponse{
   405  					Allowed:   true,
   406  					Patch:     []byte(`[{"op":"add","path":"/foo","value":"bar"}]`),
   407  					PatchType: &invalidv1beta1Patch,
   408  				},
   409  			},
   410  			expectAllowed:   true,
   411  			expectPatch:     []byte(`[{"op":"add","path":"/foo","value":"bar"}]`),
   412  			expectPatchType: admissionv1.PatchTypeJSONPatch,
   413  		},
   414  	}
   415  
   416  	for _, tc := range testcases {
   417  		t.Run(tc.name, func(t *testing.T) {
   418  			result, err := VerifyAdmissionResponse(tc.uid, tc.mutating, tc.review)
   419  			if err != nil {
   420  				if len(tc.expectErr) > 0 {
   421  					if !strings.Contains(err.Error(), tc.expectErr) {
   422  						t.Errorf("expected error '%s', got %v", tc.expectErr, err)
   423  					}
   424  				} else {
   425  					t.Errorf("unexpected error %v", err)
   426  				}
   427  				return
   428  			} else if len(tc.expectErr) > 0 {
   429  				t.Errorf("expected error '%s', got none", tc.expectErr)
   430  				return
   431  			}
   432  
   433  			if e, a := tc.expectAuditAnnotations, result.AuditAnnotations; !reflect.DeepEqual(e, a) {
   434  				t.Errorf("unexpected: %v", cmp.Diff(e, a))
   435  			}
   436  			if e, a := tc.expectAllowed, result.Allowed; !reflect.DeepEqual(e, a) {
   437  				t.Errorf("unexpected: %v", cmp.Diff(e, a))
   438  			}
   439  			if e, a := tc.expectPatch, result.Patch; !reflect.DeepEqual(e, a) {
   440  				t.Errorf("unexpected: %v", cmp.Diff(e, a))
   441  			}
   442  			if e, a := tc.expectPatchType, result.PatchType; !reflect.DeepEqual(e, a) {
   443  				t.Errorf("unexpected: %v", cmp.Diff(e, a))
   444  			}
   445  			if e, a := tc.expectResult, result.Result; !reflect.DeepEqual(e, a) {
   446  				t.Errorf("unexpected: %v", cmp.Diff(e, a))
   447  			}
   448  		})
   449  	}
   450  }
   451  
   452  func TestCreateAdmissionObjects(t *testing.T) {
   453  	internalObj := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{ResourceVersion: "2", Name: "myname", Namespace: "myns"}}
   454  	internalObjOld := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{ResourceVersion: "1", Name: "myname", Namespace: "myns"}}
   455  	versionedObj := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{ResourceVersion: "2", Name: "myname", Namespace: "myns"}}
   456  	versionedObjOld := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{ResourceVersion: "1", Name: "myname", Namespace: "myns"}}
   457  	userInfo := &user.DefaultInfo{
   458  		Name:   "myuser",
   459  		Groups: []string{"mygroup"},
   460  		UID:    "myuid",
   461  		Extra:  map[string][]string{"extrakey": {"value1", "value2"}},
   462  	}
   463  	attrs := admission.NewAttributesRecord(
   464  		internalObj.DeepCopyObject(),
   465  		internalObjOld.DeepCopyObject(),
   466  		schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
   467  		"myns",
   468  		"myname",
   469  		schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"},
   470  		"",
   471  		admission.Update,
   472  		&metav1.UpdateOptions{FieldManager: "foo"},
   473  		false,
   474  		userInfo,
   475  	)
   476  
   477  	testcases := []struct {
   478  		name       string
   479  		attrs      *admission.VersionedAttributes
   480  		invocation *generic.WebhookInvocation
   481  
   482  		expectRequest  func(uid types.UID) runtime.Object
   483  		expectResponse runtime.Object
   484  		expectErr      string
   485  	}{
   486  		{
   487  			name: "no supported versions",
   488  			invocation: &generic.WebhookInvocation{
   489  				Webhook: webhook.NewMutatingWebhookAccessor("mywebhook", "mycfg", &admissionregistrationv1.MutatingWebhook{}),
   490  			},
   491  			expectErr: "webhook does not accept known AdmissionReview versions",
   492  		},
   493  		{
   494  			name: "no known supported versions",
   495  			invocation: &generic.WebhookInvocation{
   496  				Webhook: webhook.NewMutatingWebhookAccessor("mywebhook", "mycfg", &admissionregistrationv1.MutatingWebhook{
   497  					AdmissionReviewVersions: []string{"vX"},
   498  				}),
   499  			},
   500  			expectErr: "webhook does not accept known AdmissionReview versions",
   501  		},
   502  		{
   503  			name: "v1",
   504  			attrs: &admission.VersionedAttributes{
   505  				VersionedObject:    versionedObj.DeepCopyObject(),
   506  				VersionedOldObject: versionedObjOld.DeepCopyObject(),
   507  				Attributes:         attrs,
   508  			},
   509  			invocation: &generic.WebhookInvocation{
   510  				Resource:    schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"},
   511  				Subresource: "",
   512  				Kind:        schema.GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "Deployment"},
   513  				Webhook: webhook.NewMutatingWebhookAccessor("mywebhook", "mycfg", &admissionregistrationv1.MutatingWebhook{
   514  					AdmissionReviewVersions: []string{"v1", "v1beta1"},
   515  				}),
   516  			},
   517  			expectRequest: func(uid types.UID) runtime.Object {
   518  				return &admissionv1.AdmissionReview{
   519  					Request: &admissionv1.AdmissionRequest{
   520  						UID:                uid,
   521  						Kind:               metav1.GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "Deployment"},
   522  						Resource:           metav1.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"},
   523  						SubResource:        "",
   524  						RequestKind:        &metav1.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
   525  						RequestResource:    &metav1.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"},
   526  						RequestSubResource: "",
   527  						Name:               "myname",
   528  						Namespace:          "myns",
   529  						Operation:          "UPDATE",
   530  						UserInfo: authenticationv1.UserInfo{
   531  							Username: "myuser",
   532  							UID:      "myuid",
   533  							Groups:   []string{"mygroup"},
   534  							Extra:    map[string]authenticationv1.ExtraValue{"extrakey": {"value1", "value2"}},
   535  						},
   536  						Object:    runtime.RawExtension{Object: versionedObj},
   537  						OldObject: runtime.RawExtension{Object: versionedObjOld},
   538  						DryRun:    utilpointer.BoolPtr(false),
   539  						Options:   runtime.RawExtension{Object: &metav1.UpdateOptions{FieldManager: "foo"}},
   540  					},
   541  				}
   542  			},
   543  			expectResponse: &admissionv1.AdmissionReview{},
   544  		},
   545  		{
   546  			name: "v1beta1",
   547  			attrs: &admission.VersionedAttributes{
   548  				VersionedObject:    versionedObj.DeepCopyObject(),
   549  				VersionedOldObject: versionedObjOld.DeepCopyObject(),
   550  				Attributes:         attrs,
   551  			},
   552  			invocation: &generic.WebhookInvocation{
   553  				Resource:    schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"},
   554  				Subresource: "",
   555  				Kind:        schema.GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "Deployment"},
   556  				Webhook: webhook.NewMutatingWebhookAccessor("mywebhook", "mycfg", &admissionregistrationv1.MutatingWebhook{
   557  					AdmissionReviewVersions: []string{"v1beta1", "v1"},
   558  				}),
   559  			},
   560  			expectRequest: func(uid types.UID) runtime.Object {
   561  				return &admissionv1beta1.AdmissionReview{
   562  					Request: &admissionv1beta1.AdmissionRequest{
   563  						UID:                uid,
   564  						Kind:               metav1.GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "Deployment"},
   565  						Resource:           metav1.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"},
   566  						SubResource:        "",
   567  						RequestKind:        &metav1.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
   568  						RequestResource:    &metav1.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"},
   569  						RequestSubResource: "",
   570  						Name:               "myname",
   571  						Namespace:          "myns",
   572  						Operation:          "UPDATE",
   573  						UserInfo: authenticationv1.UserInfo{
   574  							Username: "myuser",
   575  							UID:      "myuid",
   576  							Groups:   []string{"mygroup"},
   577  							Extra:    map[string]authenticationv1.ExtraValue{"extrakey": {"value1", "value2"}},
   578  						},
   579  						Object:    runtime.RawExtension{Object: versionedObj},
   580  						OldObject: runtime.RawExtension{Object: versionedObjOld},
   581  						DryRun:    utilpointer.BoolPtr(false),
   582  						Options:   runtime.RawExtension{Object: &metav1.UpdateOptions{FieldManager: "foo"}},
   583  					},
   584  				}
   585  			},
   586  			expectResponse: &admissionv1beta1.AdmissionReview{},
   587  		},
   588  	}
   589  
   590  	for _, tc := range testcases {
   591  		t.Run(tc.name, func(t *testing.T) {
   592  			uid, request, response, err := CreateAdmissionObjects(tc.attrs, tc.invocation)
   593  			if err != nil {
   594  				if len(tc.expectErr) > 0 {
   595  					if !strings.Contains(err.Error(), tc.expectErr) {
   596  						t.Errorf("expected error '%s', got %v", tc.expectErr, err)
   597  					}
   598  				} else {
   599  					t.Errorf("unexpected error %v", err)
   600  				}
   601  				return
   602  			} else if len(tc.expectErr) > 0 {
   603  				t.Errorf("expected error '%s', got none", tc.expectErr)
   604  				return
   605  			}
   606  
   607  			if len(uid) == 0 {
   608  				t.Errorf("expected uid, got none")
   609  			}
   610  			if e, a := tc.expectRequest(uid), request; !reflect.DeepEqual(e, a) {
   611  				t.Errorf("unexpected: %v", cmp.Diff(e, a))
   612  			}
   613  			if e, a := tc.expectResponse, response; !reflect.DeepEqual(e, a) {
   614  				t.Errorf("unexpected: %v", cmp.Diff(e, a))
   615  			}
   616  		})
   617  	}
   618  }