knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/resourcesemantics/defaulting/defaulting_test.go (about)

     1  /*
     2  Copyright 2017 The Knative 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 defaulting
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"reflect"
    25  	"testing"
    26  
    27  	corev1 "k8s.io/api/core/v1"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  
    31  	// Injection stuff
    32  	_ "knative.dev/pkg/client/injection/kube/client/fake"
    33  	_ "knative.dev/pkg/client/injection/kube/informers/admissionregistration/v1/mutatingwebhookconfiguration/fake"
    34  	_ "knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret/fake"
    35  	"knative.dev/pkg/ptr"
    36  
    37  	"gomodules.xyz/jsonpatch/v2"
    38  	admissionv1 "k8s.io/api/admission/v1"
    39  	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
    40  	authenticationv1 "k8s.io/api/authentication/v1"
    41  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    42  	"k8s.io/apimachinery/pkg/runtime/schema"
    43  	fakekubeclientset "k8s.io/client-go/kubernetes/fake"
    44  
    45  	"knative.dev/pkg/apis"
    46  	"knative.dev/pkg/system"
    47  	"knative.dev/pkg/webhook"
    48  
    49  	_ "knative.dev/pkg/system/testing"
    50  
    51  	. "knative.dev/pkg/logging/testing"
    52  	. "knative.dev/pkg/reconciler/testing"
    53  	. "knative.dev/pkg/testing"
    54  	"knative.dev/pkg/webhook/resourcesemantics"
    55  	. "knative.dev/pkg/webhook/testing"
    56  )
    57  
    58  const (
    59  	testResourceValidationPath = "/foo"
    60  	testResourceValidationName = "webhook.knative.dev"
    61  	user1                      = "brutto@knative.dev"
    62  	user2                      = "arrabbiato@knative.dev"
    63  )
    64  
    65  var (
    66  	handlers = map[schema.GroupVersionKind]resourcesemantics.GenericCRD{
    67  		{
    68  			Group:   "pkg.knative.dev",
    69  			Version: "v1alpha1",
    70  			Kind:    "Resource",
    71  		}: &Resource{},
    72  		{
    73  			Group:   "pkg.knative.dev",
    74  			Version: "v1beta1",
    75  			Kind:    "Resource",
    76  		}: &Resource{},
    77  		{
    78  			Group:   "pkg.knative.dev",
    79  			Version: "v1alpha1",
    80  			Kind:    "InnerDefaultResource",
    81  		}: &InnerDefaultResource{},
    82  		{
    83  			Group:   "pkg.knative.io",
    84  			Version: "v1alpha1",
    85  			Kind:    "InnerDefaultResource",
    86  		}: &InnerDefaultResource{},
    87  	}
    88  
    89  	callbacks = map[schema.GroupVersionKind]Callback{
    90  		{
    91  			Group:   "pkg.knative.dev",
    92  			Version: "v1alpha1",
    93  			Kind:    "Resource",
    94  		}: NewCallback(resourceCallback, webhook.Create, webhook.Update),
    95  		{
    96  			Group:   "pkg.knative.dev",
    97  			Version: "v1beta1",
    98  			Kind:    "Resource",
    99  		}: NewCallback(resourceCallback, webhook.Create, webhook.Update),
   100  		{
   101  			Group:   "pkg.knative.dev",
   102  			Version: "v1beta1",
   103  			Kind:    "ResourceCallbackDefault",
   104  		}: NewCallback(resourceCallback, webhook.Create, webhook.Update),
   105  		{
   106  			Group:   "pkg.knative.dev",
   107  			Version: "v1beta1",
   108  			Kind:    "ResourceCallbackDefaultCreate",
   109  		}: NewCallback(resourceCallback, webhook.Create),
   110  		corev1.SchemeGroupVersion.WithKind("Pod"): NewCallback(podCallback, webhook.Create, webhook.Update),
   111  	}
   112  
   113  	initialResourceWebhook = &admissionregistrationv1.MutatingWebhookConfiguration{
   114  		ObjectMeta: metav1.ObjectMeta{
   115  			Name: "webhook.knative.dev",
   116  			OwnerReferences: []metav1.OwnerReference{{
   117  				Name: "asdf",
   118  			}},
   119  		},
   120  		Webhooks: []admissionregistrationv1.MutatingWebhook{{
   121  			Name: "webhook.knative.dev",
   122  			ClientConfig: admissionregistrationv1.WebhookClientConfig{
   123  				Service: &admissionregistrationv1.ServiceReference{
   124  					Namespace: system.Namespace(),
   125  					Name:      "webhook",
   126  				},
   127  			},
   128  			ReinvocationPolicy: ptrReinvocationPolicyType(admissionregistrationv1.IfNeededReinvocationPolicy),
   129  		}},
   130  	}
   131  )
   132  
   133  func newNonRunningTestResourceAdmissionController(t *testing.T) (
   134  	kubeClient *fakekubeclientset.Clientset,
   135  	ac webhook.AdmissionController,
   136  ) {
   137  	t.Helper()
   138  	// Create fake clients
   139  	kubeClient = fakekubeclientset.NewSimpleClientset(initialResourceWebhook)
   140  
   141  	ac = newTestResourceAdmissionController(t)
   142  	return kubeClient, ac
   143  }
   144  
   145  func TestDeleteAllowed(t *testing.T) {
   146  	_, ac := newNonRunningTestResourceAdmissionController(t)
   147  
   148  	req := &admissionv1.AdmissionRequest{
   149  		Operation: admissionv1.Delete,
   150  	}
   151  
   152  	if resp := ac.Admit(TestContextWithLogger(t), req); !resp.Allowed {
   153  		t.Fatal("Unexpected denial of delete")
   154  	}
   155  }
   156  
   157  func TestConnectAllowed(t *testing.T) {
   158  	_, ac := newNonRunningTestResourceAdmissionController(t)
   159  
   160  	req := &admissionv1.AdmissionRequest{
   161  		Operation: admissionv1.Connect,
   162  	}
   163  
   164  	resp := ac.Admit(TestContextWithLogger(t), req)
   165  	if !resp.Allowed {
   166  		t.Fatalf("Unexpected denial of connect")
   167  	}
   168  }
   169  
   170  func TestUnknownKindFails(t *testing.T) {
   171  	_, ac := newNonRunningTestResourceAdmissionController(t)
   172  
   173  	req := &admissionv1.AdmissionRequest{
   174  		Operation: admissionv1.Create,
   175  		Kind: metav1.GroupVersionKind{
   176  			Group:   "pkg.knative.dev",
   177  			Version: "v1alpha1",
   178  			Kind:    "Garbage",
   179  		},
   180  	}
   181  
   182  	ExpectFailsWith(t, ac.Admit(TestContextWithLogger(t), req), "unhandled kind")
   183  }
   184  
   185  func TestUnknownVersionFails(t *testing.T) {
   186  	_, ac := newNonRunningTestResourceAdmissionController(t)
   187  	req := &admissionv1.AdmissionRequest{
   188  		Operation: admissionv1.Create,
   189  		Kind: metav1.GroupVersionKind{
   190  			Group:   "pkg.knative.dev",
   191  			Version: "v1beta2",
   192  			Kind:    "Resource",
   193  		},
   194  	}
   195  	ExpectFailsWith(t, ac.Admit(TestContextWithLogger(t), req), "unhandled kind")
   196  }
   197  
   198  func TestUnknownFieldFails(t *testing.T) {
   199  	_, ac := newNonRunningTestResourceAdmissionController(t)
   200  	req := &admissionv1.AdmissionRequest{
   201  		Operation: admissionv1.Create,
   202  		Kind: metav1.GroupVersionKind{
   203  			Group:   "pkg.knative.dev",
   204  			Version: "v1alpha1",
   205  			Kind:    "Resource",
   206  		},
   207  	}
   208  
   209  	marshaled, err := json.Marshal(map[string]interface{}{
   210  		"spec": map[string]interface{}{
   211  			"foo": "bar",
   212  		},
   213  	})
   214  	if err != nil {
   215  		t.Fatal("Failed to marshal resource:", err)
   216  	}
   217  	req.Object.Raw = marshaled
   218  
   219  	ExpectFailsWith(t, ac.Admit(TestContextWithLogger(t), req),
   220  		`mutation failed: cannot decode incoming new object: json: unknown field "foo"`)
   221  }
   222  
   223  func TestUnknownMetadataFieldSucceeds(t *testing.T) {
   224  	_, ac := newNonRunningTestResourceAdmissionController(t)
   225  	req := &admissionv1.AdmissionRequest{
   226  		Operation: admissionv1.Create,
   227  		Kind: metav1.GroupVersionKind{
   228  			Group:   "pkg.knative.dev",
   229  			Version: "v1alpha1",
   230  			Kind:    "Resource",
   231  		},
   232  	}
   233  
   234  	marshaled, err := json.Marshal(map[string]interface{}{
   235  		"apiVersion": "pkg.knative.dev/v1alpha1",
   236  		"kind":       "Resource",
   237  		"metadata": map[string]string{
   238  			"unknown": "property",
   239  		},
   240  		"spec": map[string]string{
   241  			"fieldWithValidation": "magic value",
   242  		},
   243  	})
   244  	if err != nil {
   245  		t.Fatal("Failed to marshal resource:", err)
   246  	}
   247  	req.Object.Raw = marshaled
   248  
   249  	ExpectAllowed(t, ac.Admit(TestContextWithLogger(t), req))
   250  }
   251  
   252  func TestAdmitCreates(t *testing.T) {
   253  	tests := []struct {
   254  		name              string
   255  		setup             func(context.Context, *Resource)
   256  		rejection         string
   257  		patches           []jsonpatch.JsonPatchOperation
   258  		createRequestFunc func(ctx context.Context, t *testing.T, r *Resource) *admissionv1.AdmissionRequest
   259  	}{{
   260  		name: "test simple creation (alpha, no diff)",
   261  		setup: func(ctx context.Context, r *Resource) {
   262  			r.TypeMeta.APIVersion = "v1alpha1"
   263  			r.SetDefaults(ctx)
   264  			r.Annotations = map[string]string{
   265  				"pkg.knative.dev/creator":      user1,
   266  				"pkg.knative.dev/lastModifier": user1,
   267  			}
   268  		},
   269  		patches: []jsonpatch.JsonPatchOperation{},
   270  	}, {
   271  		name: "test simple creation (beta, no diff)",
   272  		setup: func(ctx context.Context, r *Resource) {
   273  			r.TypeMeta.APIVersion = "v1beta1"
   274  			r.SetDefaults(ctx)
   275  			r.Annotations = map[string]string{
   276  				"pkg.knative.dev/creator":      user1,
   277  				"pkg.knative.dev/lastModifier": user1,
   278  			}
   279  		},
   280  		patches: []jsonpatch.JsonPatchOperation{},
   281  	}, {
   282  		name: "test simple creation (with defaults)",
   283  		setup: func(ctx context.Context, r *Resource) {
   284  		},
   285  		patches: []jsonpatch.JsonPatchOperation{{
   286  			Operation: "add",
   287  			Path:      "/metadata/annotations",
   288  			Value: map[string]interface{}{
   289  				"pkg.knative.dev/creator":      user1,
   290  				"pkg.knative.dev/lastModifier": user1,
   291  			},
   292  		}, {
   293  			Operation: "add",
   294  			Path:      "/spec/fieldThatsImmutableWithDefault",
   295  			Value:     "this is another default value",
   296  		}, {
   297  			Operation: "add",
   298  			Path:      "/spec/fieldWithDefault",
   299  			Value:     "I'm a default.",
   300  		}},
   301  	}, {
   302  		name: "test simple creation (with defaults around annotations)",
   303  		setup: func(ctx context.Context, r *Resource) {
   304  			r.Annotations = map[string]string{
   305  				"foo": "bar",
   306  			}
   307  		},
   308  		patches: []jsonpatch.JsonPatchOperation{{
   309  			Operation: "add",
   310  			Path:      "/metadata/annotations/pkg.knative.dev~1creator",
   311  			Value:     user1,
   312  		}, {
   313  			Operation: "add",
   314  			Path:      "/metadata/annotations/pkg.knative.dev~1lastModifier",
   315  			Value:     user1,
   316  		}, {
   317  			Operation: "add",
   318  			Path:      "/spec/fieldThatsImmutableWithDefault",
   319  			Value:     "this is another default value",
   320  		}, {
   321  			Operation: "add",
   322  			Path:      "/spec/fieldWithDefault",
   323  			Value:     "I'm a default.",
   324  		}},
   325  	}, {
   326  		name: "test simple creation (with partially overridden defaults)",
   327  		setup: func(ctx context.Context, r *Resource) {
   328  			r.Spec.FieldThatsImmutableWithDefault = "not the default"
   329  		},
   330  		patches: []jsonpatch.JsonPatchOperation{{
   331  			Operation: "add",
   332  			Path:      "/metadata/annotations",
   333  			Value: map[string]interface{}{
   334  				"pkg.knative.dev/creator":      user1,
   335  				"pkg.knative.dev/lastModifier": user1,
   336  			},
   337  		}, {
   338  			Operation: "add",
   339  			Path:      "/spec/fieldWithDefault",
   340  			Value:     "I'm a default.",
   341  		}},
   342  	}, {
   343  		name: "test simple creation (webhook corrects user annotation)",
   344  		setup: func(ctx context.Context, r *Resource) {
   345  			r.SetDefaults(ctx)
   346  			// THIS IS NOT WHO IS CREATING IT, IT IS LIES!
   347  			r.Annotations = map[string]string{
   348  				"pkg.knative.dev/lastModifier": user2,
   349  			}
   350  		},
   351  		patches: []jsonpatch.JsonPatchOperation{{
   352  			Operation: "replace",
   353  			Path:      "/metadata/annotations/pkg.knative.dev~1lastModifier",
   354  			Value:     user1,
   355  		}, {
   356  			Operation: "add",
   357  			Path:      "/metadata/annotations/pkg.knative.dev~1creator",
   358  			Value:     user1,
   359  		}},
   360  	}, {
   361  		name: "test simple creation (callback return error)",
   362  		setup: func(ctx context.Context, resource *Resource) {
   363  			resource.Spec.FieldForCallbackDefaulting = "no magic value"
   364  		},
   365  		rejection: "no magic value",
   366  	}, {
   367  		name: "test simple creation (resource and callback defaults)",
   368  		setup: func(ctx context.Context, r *Resource) {
   369  			r.SetDefaults(ctx)
   370  			r.Spec.FieldForCallbackDefaulting = "magic value"
   371  			// THIS IS NOT WHO IS CREATING IT, IT IS LIES!
   372  			r.Annotations = map[string]string{
   373  				"pkg.knative.dev/lastModifier": user2,
   374  			}
   375  		},
   376  		patches: []jsonpatch.JsonPatchOperation{{
   377  			Operation: "replace",
   378  			Path:      "/spec/fieldForCallbackDefaulting",
   379  			Value:     "I'm a default",
   380  		}, {
   381  			Operation: "replace",
   382  			Path:      "/metadata/annotations/pkg.knative.dev~1lastModifier",
   383  			Value:     user1,
   384  		}, {
   385  			Operation: "add",
   386  			Path:      "/metadata/annotations/pkg.knative.dev~1creator",
   387  			Value:     user1,
   388  		}, {
   389  			Operation: "add",
   390  			Path:      "/spec/fieldForCallbackDefaultingUsername",
   391  			Value:     user1,
   392  		}},
   393  	}, {
   394  		name: "test simple creation (only callback defaults)",
   395  		setup: func(ctx context.Context, r *Resource) {
   396  			r.TypeMeta.APIVersion = "pkg.knative.dev/v1beta1"
   397  			r.TypeMeta.Kind = "ResourceCallbackDefault"
   398  			r.Spec.FieldForCallbackDefaulting = "magic value"
   399  			// THIS IS NOT WHO IS CREATING IT, IT LIES!
   400  			r.Annotations = map[string]string{
   401  				"pkg.knative.dev/lastModifier": user2,
   402  			}
   403  		},
   404  		createRequestFunc: func(ctx context.Context, t *testing.T, r *Resource) *admissionv1.AdmissionRequest {
   405  			req := createCreateResource(ctx, t, r)
   406  			req.Kind = r.GetGroupVersionKindMeta()
   407  			return req
   408  		},
   409  		patches: []jsonpatch.JsonPatchOperation{{
   410  			Operation: "replace",
   411  			Path:      "/spec/fieldForCallbackDefaulting",
   412  			Value:     "I'm a default",
   413  		}, {
   414  			Operation: "add",
   415  			Path:      "/spec/fieldForCallbackDefaultingUsername",
   416  			Value:     user1,
   417  		}, {
   418  			Operation: "replace",
   419  			Path:      "/metadata/annotations/pkg.knative.dev~1lastModifier",
   420  			Value:     user1,
   421  		}, {
   422  			Operation: "add",
   423  			Path:      "/metadata/annotations/pkg.knative.dev~1creator",
   424  			Value:     user1,
   425  		}},
   426  	}}
   427  
   428  	for _, tc := range tests {
   429  		t.Run(tc.name, func(t *testing.T) {
   430  			r := CreateResource("a name")
   431  			ctx := apis.WithinCreate(apis.WithUserInfo(
   432  				TestContextWithLogger(t),
   433  				&authenticationv1.UserInfo{Username: user1}))
   434  
   435  			// Setup the resource.
   436  			tc.setup(ctx, r)
   437  
   438  			_, ac := newNonRunningTestResourceAdmissionController(t)
   439  			var req *admissionv1.AdmissionRequest
   440  			if tc.createRequestFunc == nil {
   441  				req = createCreateResource(ctx, t, r)
   442  			} else {
   443  				req = tc.createRequestFunc(ctx, t, r)
   444  			}
   445  			resp := ac.Admit(ctx, req)
   446  
   447  			if tc.rejection == "" {
   448  				ExpectAllowed(t, resp)
   449  				ExpectPatches(t, resp.Patch, tc.patches)
   450  			} else {
   451  				ExpectFailsWith(t, resp, tc.rejection)
   452  			}
   453  		})
   454  	}
   455  }
   456  
   457  func createCreateResource(ctx context.Context, t *testing.T, r *Resource) *admissionv1.AdmissionRequest {
   458  	t.Helper()
   459  	req := &admissionv1.AdmissionRequest{
   460  		Operation: admissionv1.Create,
   461  		Kind: metav1.GroupVersionKind{
   462  			Group:   "pkg.knative.dev",
   463  			Version: "v1alpha1",
   464  			Kind:    "Resource",
   465  		},
   466  		UserInfo: *apis.GetUserInfo(ctx),
   467  	}
   468  	marshaled, err := json.Marshal(r)
   469  	if err != nil {
   470  		t.Fatal("Failed to marshal resource:", err)
   471  	}
   472  	req.Object.Raw = marshaled
   473  	req.Resource.Group = "pkg.knative.dev"
   474  	return req
   475  }
   476  
   477  func TestAdmitUpdates(t *testing.T) {
   478  	tests := []struct {
   479  		name      string
   480  		setup     func(context.Context, *Resource)
   481  		mutate    func(context.Context, *Resource)
   482  		rejection string
   483  		patches   []jsonpatch.JsonPatchOperation
   484  	}{{
   485  		name: "test simple update (no diff)",
   486  		setup: func(ctx context.Context, r *Resource) {
   487  			r.SetDefaults(ctx)
   488  		},
   489  		mutate: func(ctx context.Context, r *Resource) {
   490  			// If we don't change anything, the updater
   491  			// annotation doesn't change.
   492  		},
   493  		patches: []jsonpatch.JsonPatchOperation{},
   494  	}, {
   495  		name: "test simple update (callback defaults error)",
   496  		setup: func(ctx context.Context, r *Resource) {
   497  			r.SetDefaults(ctx)
   498  		},
   499  		mutate: func(ctx context.Context, r *Resource) {
   500  			r.Spec.FieldForCallbackDefaulting = "no magic value"
   501  		},
   502  		rejection: "no magic value",
   503  	}, {
   504  		name: "test simple update (update updater annotation)",
   505  		setup: func(ctx context.Context, r *Resource) {
   506  			r.SetDefaults(ctx)
   507  		},
   508  		mutate: func(ctx context.Context, r *Resource) {
   509  			// When we change the spec, the updater
   510  			// annotation changes.
   511  			r.Spec.FieldWithDefault = "not the default"
   512  		},
   513  		patches: []jsonpatch.JsonPatchOperation{{
   514  			Operation: "replace",
   515  			Path:      "/metadata/annotations/pkg.knative.dev~1lastModifier",
   516  			Value:     user2,
   517  		}},
   518  	}, {
   519  		name: "test simple update (annotation change doesn't change updater)",
   520  		setup: func(ctx context.Context, r *Resource) {
   521  			r.SetDefaults(ctx)
   522  		},
   523  		mutate: func(ctx context.Context, r *Resource) {
   524  			// When we change an annotation, the updater doesn't change.
   525  			r.Annotations["foo"] = "bar"
   526  		},
   527  		patches: []jsonpatch.JsonPatchOperation{},
   528  	}, {
   529  		name: "test that updates dropping immutable defaults are filled back in",
   530  		setup: func(ctx context.Context, r *Resource) {
   531  			r.SetDefaults(ctx)
   532  			r.Spec.FieldThatsImmutableWithDefault = ""
   533  		},
   534  		mutate: func(ctx context.Context, r *Resource) {
   535  			r.Spec.FieldThatsImmutableWithDefault = ""
   536  		},
   537  		patches: []jsonpatch.JsonPatchOperation{{
   538  			Operation: "add",
   539  			Path:      "/spec/fieldThatsImmutableWithDefault",
   540  			Value:     "this is another default value",
   541  		}},
   542  	}}
   543  
   544  	for _, tc := range tests {
   545  		t.Run(tc.name, func(t *testing.T) {
   546  			name := "a name"
   547  
   548  			old := CreateResource(name)
   549  			ctx := TestContextWithLogger(t)
   550  
   551  			old.Annotations = map[string]string{
   552  				"pkg.knative.dev/creator":      user1,
   553  				"pkg.knative.dev/lastModifier": user1,
   554  			}
   555  
   556  			tc.setup(ctx, old)
   557  
   558  			new := old.DeepCopy()
   559  
   560  			// Mutate the resource using the update context as user2
   561  			ctx = apis.WithUserInfo(apis.WithinUpdate(ctx, old),
   562  				&authenticationv1.UserInfo{Username: user2})
   563  			tc.mutate(ctx, new)
   564  
   565  			_, ac := newNonRunningTestResourceAdmissionController(t)
   566  
   567  			req := createUpdateResource(ctx, t, old, new)
   568  
   569  			resp := ac.Admit(ctx, req)
   570  
   571  			if tc.rejection == "" {
   572  				ExpectAllowed(t, resp)
   573  				ExpectPatches(t, resp.Patch, tc.patches)
   574  			} else {
   575  				ExpectFailsWith(t, resp, tc.rejection)
   576  			}
   577  		})
   578  	}
   579  }
   580  
   581  func TestAdmitUpdatesCallback(t *testing.T) {
   582  	tests := []struct {
   583  		name                     string
   584  		setup                    func(context.Context, *Resource)
   585  		mutate                   func(context.Context, *Resource)
   586  		createResourceFunc       func(name string) *Resource
   587  		createUpdateResourceFunc func(ctx context.Context, t *testing.T, old, new *Resource) *admissionv1.AdmissionRequest
   588  		rejection                string
   589  		patches                  []jsonpatch.JsonPatchOperation
   590  	}{
   591  		{
   592  			name: "test simple update (callback defaults error)",
   593  			setup: func(ctx context.Context, r *Resource) {
   594  				r.SetDefaults(ctx)
   595  			},
   596  			mutate: func(ctx context.Context, r *Resource) {
   597  				r.Spec.FieldForCallbackDefaulting = "no magic value"
   598  			},
   599  			rejection:                "no magic value",
   600  			createUpdateResourceFunc: createUpdateResource,
   601  		}, {
   602  			name: "test simple update (callback defaults)",
   603  			setup: func(ctx context.Context, r *Resource) {
   604  				r.SetDefaults(ctx)
   605  			},
   606  			mutate: func(ctx context.Context, r *Resource) {
   607  				r.Spec.FieldForCallbackDefaulting = "magic value"
   608  			},
   609  			patches: []jsonpatch.JsonPatchOperation{{
   610  				Operation: "replace",
   611  				Path:      "/spec/fieldForCallbackDefaulting",
   612  				Value:     "I'm a default",
   613  			}, {
   614  				Operation: "replace",
   615  				Path:      "/metadata/annotations/pkg.knative.dev~1lastModifier",
   616  				Value:     user2,
   617  			}, {
   618  				Operation: "add",
   619  				Path:      "/spec/fieldForCallbackDefaultingIsWithinUpdate",
   620  				Value:     true,
   621  			}, {
   622  				Operation: "add",
   623  				Path:      "/spec/fieldForCallbackDefaultingUsername",
   624  				Value:     user2,
   625  			}},
   626  			createUpdateResourceFunc: createUpdateResource,
   627  		}, {
   628  			name: "test simple update (callback defaults only)",
   629  			setup: func(ctx context.Context, r *Resource) {
   630  				r.TypeMeta.APIVersion = "pkg.knative.dev/v1beta1"
   631  				r.TypeMeta.Kind = "ResourceCallbackDefault"
   632  				r.Spec.FieldForCallbackDefaulting = "magic value"
   633  				r.SetDefaults(ctx)
   634  			},
   635  			mutate: func(ctx context.Context, r *Resource) {
   636  				r.Spec.FieldForCallbackDefaulting = "magic value"
   637  			},
   638  			createUpdateResourceFunc: func(ctx context.Context, t *testing.T, old, new *Resource) *admissionv1.AdmissionRequest {
   639  				req := createUpdateResource(ctx, t, old, new)
   640  				req.Kind = new.GetGroupVersionKindMeta()
   641  				return req
   642  			},
   643  			patches: []jsonpatch.JsonPatchOperation{{
   644  				Operation: "replace",
   645  				Path:      "/metadata/annotations/pkg.knative.dev~1lastModifier",
   646  				Value:     user2,
   647  			}, {
   648  				Operation: "replace",
   649  				Path:      "/spec/fieldForCallbackDefaulting",
   650  				Value:     "I'm a default",
   651  			}, {
   652  				Operation: "add",
   653  				Path:      "/spec/fieldForCallbackDefaultingIsWithinUpdate",
   654  				Value:     true,
   655  			}, {
   656  				Operation: "add",
   657  				Path:      "/spec/fieldForCallbackDefaultingUsername",
   658  				Value:     user2,
   659  			}},
   660  		}, {
   661  			name: "test simple update (callback defaults only, operation not supported)",
   662  			setup: func(ctx context.Context, r *Resource) {
   663  				r.TypeMeta.APIVersion = "pkg.knative.dev/v1beta1"
   664  				r.TypeMeta.Kind = "ResourceCallbackDefaultCreate"
   665  				r.Spec.FieldForCallbackDefaulting = "magic value"
   666  				r.SetDefaults(ctx)
   667  			},
   668  			mutate: func(ctx context.Context, r *Resource) {
   669  				r.Spec.FieldForCallbackDefaulting = "magic value"
   670  			},
   671  			createUpdateResourceFunc: func(ctx context.Context, t *testing.T, old, new *Resource) *admissionv1.AdmissionRequest {
   672  				req := createUpdateResource(ctx, t, old, new)
   673  				req.Operation = admissionv1.Update
   674  				req.Kind = new.GetGroupVersionKindMeta()
   675  				return req
   676  			},
   677  		}, {
   678  			name: "test simple update (callback defaults)",
   679  			setup: func(ctx context.Context, r *Resource) {
   680  				r.SetDefaults(ctx)
   681  			},
   682  			mutate: func(ctx context.Context, r *Resource) {
   683  				r.Spec.FieldForCallbackDefaulting = "magic value"
   684  			},
   685  			patches: []jsonpatch.JsonPatchOperation{{
   686  				Operation: "replace",
   687  				Path:      "/spec/fieldForCallbackDefaulting",
   688  				Value:     "I'm a default",
   689  			}, {
   690  				Operation: "replace",
   691  				Path:      "/metadata/annotations/pkg.knative.dev~1lastModifier",
   692  				Value:     user2,
   693  			}, {
   694  				Operation: "add",
   695  				Path:      "/spec/fieldForCallbackDefaultingIsWithinUpdate",
   696  				Value:     true,
   697  			}, {
   698  				Operation: "add",
   699  				Path:      "/spec/fieldForCallbackDefaultingUsername",
   700  				Value:     user2,
   701  			}},
   702  			createUpdateResourceFunc: createUpdateResource,
   703  		},
   704  	}
   705  
   706  	for _, tc := range tests {
   707  		t.Run(tc.name, func(t *testing.T) {
   708  			name := "a name"
   709  
   710  			old := CreateResource(name)
   711  			ctx := TestContextWithLogger(t)
   712  
   713  			old.Annotations = map[string]string{
   714  				"pkg.knative.dev/creator":      user1,
   715  				"pkg.knative.dev/lastModifier": user1,
   716  			}
   717  
   718  			tc.setup(ctx, old)
   719  
   720  			new := old.DeepCopy()
   721  
   722  			// Mutate the resource using the update context as user2
   723  			ctx = apis.WithUserInfo(apis.WithinUpdate(ctx, old),
   724  				&authenticationv1.UserInfo{Username: user2})
   725  			tc.mutate(ctx, new)
   726  
   727  			_, ac := newNonRunningTestResourceAdmissionController(t)
   728  
   729  			req := tc.createUpdateResourceFunc(ctx, t, old, new)
   730  
   731  			resp := ac.Admit(ctx, req)
   732  
   733  			if tc.rejection == "" {
   734  				ExpectAllowed(t, resp)
   735  				ExpectPatches(t, resp.Patch, tc.patches)
   736  			} else {
   737  				ExpectFailsWith(t, resp, tc.rejection)
   738  			}
   739  		})
   740  	}
   741  }
   742  
   743  func TestAdmitCoreUserInfo(t *testing.T) {
   744  	gvk := corev1.SchemeGroupVersion.WithKind("Pod")
   745  
   746  	ctx, _ := SetupFakeContext(t)
   747  	ctx = webhook.WithOptions(ctx, webhook.Options{SecretName: "webhook-secret"})
   748  	r := newTestResourceAdmissionController(t)
   749  
   750  	mGvk := metav1.GroupVersionKind{
   751  		Group:   gvk.Group,
   752  		Version: gvk.Version,
   753  		Kind:    gvk.Kind,
   754  	}
   755  	mGvr := metav1.GroupVersionResource{
   756  		Group:    gvk.Group,
   757  		Version:  gvk.Version,
   758  		Resource: "pods",
   759  	}
   760  
   761  	req := &admissionv1.AdmissionRequest{
   762  		UID:             "58e22c80-9675-4fa4-801c-cb6bf348c799",
   763  		Kind:            mGvk,
   764  		Resource:        metav1.GroupVersionResource{},
   765  		RequestKind:     &mGvk,
   766  		RequestResource: &mGvr,
   767  		Name:            "p-1",
   768  		Namespace:       "ns",
   769  		Operation:       webhook.Create,
   770  		UserInfo: authenticationv1.UserInfo{
   771  			Username: "username",
   772  			UID:      "e4a45e22-c352-4353-a7f1-bcbdcbf7af21",
   773  		},
   774  	}
   775  
   776  	tt := []struct {
   777  		name      string
   778  		allowed   bool
   779  		object    *corev1.Pod
   780  		oldObject *corev1.Pod
   781  		expected  *corev1.Pod
   782  		patches   []jsonpatch.JsonPatchOperation
   783  	}{{
   784  		name:    "create",
   785  		allowed: true,
   786  		object: &corev1.Pod{
   787  			TypeMeta: metav1.TypeMeta{
   788  				Kind:       gvk.Kind,
   789  				APIVersion: gvk.GroupVersion().String(),
   790  			},
   791  			ObjectMeta: metav1.ObjectMeta{
   792  				Namespace: req.Namespace,
   793  				Name:      req.Name,
   794  			},
   795  			Spec: corev1.PodSpec{},
   796  		},
   797  		patches: []jsonpatch.JsonPatchOperation{{
   798  			Operation: "add",
   799  			Path:      "/metadata/annotations",
   800  			Value: map[string]interface{}{
   801  				"creator":      req.UserInfo.Username,
   802  				"lastModifier": req.UserInfo.Username,
   803  			},
   804  		}, {
   805  			Operation: "add",
   806  			Path:      "/spec/automountServiceAccountToken",
   807  			Value:     true,
   808  		}},
   809  	}, {
   810  		name:    "update",
   811  		allowed: true,
   812  		object: &corev1.Pod{
   813  			TypeMeta: metav1.TypeMeta{
   814  				Kind:       gvk.Kind,
   815  				APIVersion: gvk.GroupVersion().String(),
   816  			},
   817  			ObjectMeta: metav1.ObjectMeta{
   818  				Namespace: req.Namespace,
   819  				Name:      req.Name,
   820  			},
   821  			Spec: corev1.PodSpec{},
   822  		},
   823  		oldObject: &corev1.Pod{
   824  			TypeMeta: metav1.TypeMeta{
   825  				Kind:       gvk.Kind,
   826  				APIVersion: gvk.GroupVersion().String(),
   827  			},
   828  			ObjectMeta: metav1.ObjectMeta{
   829  				Namespace: req.Namespace,
   830  				Name:      req.Name,
   831  			},
   832  			Spec: corev1.PodSpec{},
   833  		},
   834  		patches: []jsonpatch.JsonPatchOperation{{
   835  			Operation: "add",
   836  			Path:      "/metadata/annotations",
   837  			Value: map[string]interface{}{
   838  				"lastModifier": req.UserInfo.Username,
   839  			},
   840  		}, {
   841  			Operation: "add",
   842  			Path:      "/spec/automountServiceAccountToken",
   843  			Value:     true,
   844  		}},
   845  	}}
   846  
   847  	for _, tc := range tt {
   848  		t.Run(tc.name, func(t *testing.T) {
   849  			req := req.DeepCopy()
   850  
   851  			if tc.object != nil {
   852  				objRaw, _ := json.Marshal(tc.object)
   853  				req.Object = runtime.RawExtension{Raw: objRaw, Object: tc.object}
   854  			}
   855  
   856  			if tc.oldObject != nil {
   857  				oldObjRaw, _ := json.Marshal(tc.oldObject)
   858  				req.OldObject = runtime.RawExtension{Raw: oldObjRaw, Object: tc.oldObject}
   859  			}
   860  
   861  			res := r.Admit(ctx, req)
   862  
   863  			if tc.allowed != res.Allowed {
   864  				t.Fatal("Request must be allowed", res.Result)
   865  			}
   866  
   867  			if tc.allowed {
   868  				ExpectPatches(t, res.Patch, tc.patches)
   869  			}
   870  		})
   871  	}
   872  }
   873  
   874  func createUpdateResource(ctx context.Context, t *testing.T, old, new *Resource) *admissionv1.AdmissionRequest {
   875  	t.Helper()
   876  	req := &admissionv1.AdmissionRequest{
   877  		Operation: admissionv1.Update,
   878  		Kind: metav1.GroupVersionKind{
   879  			Group:   "pkg.knative.dev",
   880  			Version: "v1alpha1",
   881  			Kind:    "Resource",
   882  		},
   883  		UserInfo: *apis.GetUserInfo(ctx),
   884  	}
   885  	marshaled, err := json.Marshal(new)
   886  	if err != nil {
   887  		t.Error("Failed to marshal resource:", err)
   888  	}
   889  	req.Object.Raw = marshaled
   890  	marshaledOld, err := json.Marshal(old)
   891  	if err != nil {
   892  		t.Error("Failed to marshal resource:", err)
   893  	}
   894  	req.OldObject.Raw = marshaledOld
   895  	req.Resource.Group = "pkg.knative.dev"
   896  	return req
   897  }
   898  
   899  func TestValidCreateResourceSucceedsWithRoundTripAndDefaultPatch(t *testing.T) {
   900  	req := &admissionv1.AdmissionRequest{
   901  		Operation: admissionv1.Create,
   902  		Kind: metav1.GroupVersionKind{
   903  			Group:   "pkg.knative.dev",
   904  			Version: "v1alpha1",
   905  			Kind:    "InnerDefaultResource",
   906  		},
   907  	}
   908  	req.Object.Raw = createInnerDefaultResourceWithoutSpec(t)
   909  
   910  	_, ac := newNonRunningTestResourceAdmissionController(t)
   911  	resp := ac.Admit(TestContextWithLogger(t), req)
   912  	ExpectAllowed(t, resp)
   913  	ExpectPatches(t, resp.Patch, []jsonpatch.JsonPatchOperation{{
   914  		Operation: "add",
   915  		Path:      "/spec",
   916  		Value:     map[string]interface{}{},
   917  	}, {
   918  		Operation: "add",
   919  		Path:      "/spec/fieldWithDefault",
   920  		Value:     "I'm a default.",
   921  	}})
   922  }
   923  
   924  func createInnerDefaultResourceWithoutSpec(t *testing.T) []byte {
   925  	t.Helper()
   926  	r := InnerDefaultResource{
   927  		TypeMeta: metav1.TypeMeta{
   928  			Kind:       "InnerDefaultResource",
   929  			APIVersion: fmt.Sprintf("%s/%s", SchemeGroupVersion.Group, SchemeGroupVersion.Version),
   930  		},
   931  		ObjectMeta: metav1.ObjectMeta{
   932  			Namespace: system.Namespace(),
   933  			Name:      "a name",
   934  		},
   935  	}
   936  	// Remove the 'spec' field of the generated JSON by marshaling it to JSON, parsing that as a
   937  	// generic map[string]interface{}, removing 'spec', and marshaling it again.
   938  	origBytes, err := json.Marshal(r)
   939  	if err != nil {
   940  		t.Fatal("Error marshaling origBytes:", err)
   941  	}
   942  	var q map[string]interface{}
   943  	if err := json.Unmarshal(origBytes, &q); err != nil {
   944  		t.Fatal("Error unmarshaling origBytes:", err)
   945  	}
   946  	delete(q, "spec")
   947  	b, err := json.Marshal(q)
   948  	if err != nil {
   949  		t.Fatal("Error marshaling q:", err)
   950  	}
   951  	return b
   952  }
   953  
   954  func newTestResourceAdmissionController(t *testing.T) webhook.AdmissionController {
   955  	ctx, _ := SetupFakeContext(t)
   956  	ctx = webhook.WithOptions(ctx, webhook.Options{
   957  		SecretName: "webhook-secret",
   958  	})
   959  	return NewAdmissionController(
   960  		ctx, testResourceValidationName, testResourceValidationPath,
   961  		handlers, func(ctx context.Context) context.Context {
   962  			return ctx
   963  		}, true, callbacks).Reconciler.(*reconciler)
   964  }
   965  
   966  func resourceCallback(ctx context.Context, uns *unstructured.Unstructured) error {
   967  	var resource Resource
   968  	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(uns.UnstructuredContent(), &resource); err != nil {
   969  		return err
   970  	}
   971  
   972  	if resource.Spec.FieldForCallbackDefaulting != "" {
   973  		if resource.Spec.FieldForCallbackDefaulting != "magic value" {
   974  			return errors.New(resource.Spec.FieldForCallbackDefaulting)
   975  		}
   976  		resource.Spec.FieldForCallbackDefaultingIsWithinUpdate = apis.IsInUpdate(ctx)
   977  		resource.Spec.FieldForCallbackDefaultingUsername = apis.GetUserInfo(ctx).Username
   978  		resource.Spec.FieldForCallbackDefaulting = "I'm a default"
   979  		if apis.IsInUpdate(ctx) {
   980  			if apis.GetBaseline(ctx) == nil {
   981  				return errors.New("expected baseline object")
   982  			}
   983  			if v, ok := apis.GetBaseline(ctx).(*unstructured.Unstructured); !ok {
   984  				return fmt.Errorf("expected *unstructured.Unstructured, got %v", reflect.TypeOf(v))
   985  			}
   986  		} else if !apis.IsInCreate(ctx) {
   987  			return errors.New("expected to have context within update or create")
   988  		}
   989  	}
   990  
   991  	u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&resource)
   992  	if err != nil {
   993  		return err
   994  	}
   995  	uns.Object = u
   996  
   997  	return nil
   998  }
   999  
  1000  func podCallback(ctx context.Context, u *unstructured.Unstructured) error {
  1001  	pod := &corev1.Pod{}
  1002  	err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), pod)
  1003  	if err != nil {
  1004  		return err
  1005  	}
  1006  
  1007  	pod.Spec.AutomountServiceAccountToken = ptr.Bool(true)
  1008  
  1009  	r, err := runtime.DefaultUnstructuredConverter.ToUnstructured(pod)
  1010  	if err != nil {
  1011  		return err
  1012  	}
  1013  	u.Object = r
  1014  
  1015  	return nil
  1016  }