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 }