k8s.io/kubernetes@v1.29.3/test/integration/apiserver/cel/admission_test_util.go (about) 1 /* 2 Copyright 2023 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 cel 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "strings" 24 "sync" 25 "testing" 26 "time" 27 28 "k8s.io/api/admission/v1beta1" 29 appsv1beta1 "k8s.io/api/apps/v1beta1" 30 authenticationv1 "k8s.io/api/authentication/v1" 31 corev1 "k8s.io/api/core/v1" 32 extensionsv1beta1 "k8s.io/api/extensions/v1beta1" 33 policyv1 "k8s.io/api/policy/v1" 34 apierrors "k8s.io/apimachinery/pkg/api/errors" 35 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 36 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 37 "k8s.io/apimachinery/pkg/runtime" 38 "k8s.io/apimachinery/pkg/runtime/schema" 39 "k8s.io/apimachinery/pkg/types" 40 "k8s.io/apimachinery/pkg/util/sets" 41 "k8s.io/apimachinery/pkg/util/wait" 42 "k8s.io/client-go/dynamic" 43 clientset "k8s.io/client-go/kubernetes" 44 "k8s.io/client-go/util/retry" 45 "k8s.io/kubernetes/test/integration/etcd" 46 ) 47 48 // Admission test framework copied from test/integration/apiserver/admissionwebhook/admission_test.go 49 // 50 // All differences between two are minor and called out in comments prefixed with 51 // "DIFF" 52 53 const ( 54 testNamespace = "webhook-integration" 55 testClientUsername = "webhook-integration-client" 56 57 mutation = "mutation" 58 validation = "validation" 59 ) 60 61 // DIFF: Added interface to replace direct *holder usage in testContext to be 62 // able to inject a policy-specific holder 63 type admissionTestExpectationHolder interface { 64 reset(t *testing.T) 65 expect(gvr schema.GroupVersionResource, gvk, optionsGVK schema.GroupVersionKind, operation v1beta1.Operation, name, namespace string, object, oldObject, options bool) 66 verify(t *testing.T) 67 } 68 69 type testContext struct { 70 t *testing.T 71 72 // DIFF: Changed from *holder to interface 73 admissionHolder admissionTestExpectationHolder 74 75 client dynamic.Interface 76 clientset clientset.Interface 77 verb string 78 gvr schema.GroupVersionResource 79 resource metav1.APIResource 80 resources map[schema.GroupVersionResource]metav1.APIResource 81 } 82 83 type testFunc func(*testContext) 84 85 var ( 86 // defaultResourceFuncs holds the default test functions. 87 // may be overridden for specific resources by customTestFuncs. 88 defaultResourceFuncs = map[string]testFunc{ 89 "create": testResourceCreate, 90 "update": testResourceUpdate, 91 "patch": testResourcePatch, 92 "delete": testResourceDelete, 93 "deletecollection": testResourceDeletecollection, 94 } 95 96 // defaultSubresourceFuncs holds default subresource test functions. 97 // may be overridden for specific resources by customTestFuncs. 98 defaultSubresourceFuncs = map[string]testFunc{ 99 "update": testSubresourceUpdate, 100 "patch": testSubresourcePatch, 101 } 102 103 // customTestFuncs holds custom test functions by resource and verb. 104 customTestFuncs = map[schema.GroupVersionResource]map[string]testFunc{ 105 gvr("", "v1", "namespaces"): {"delete": testNamespaceDelete}, 106 107 gvr("apps", "v1beta1", "deployments/rollback"): {"create": testDeploymentRollback}, 108 gvr("extensions", "v1beta1", "deployments/rollback"): {"create": testDeploymentRollback}, 109 110 gvr("", "v1", "pods/attach"): {"create": testPodConnectSubresource}, 111 gvr("", "v1", "pods/exec"): {"create": testPodConnectSubresource}, 112 gvr("", "v1", "pods/portforward"): {"create": testPodConnectSubresource}, 113 114 gvr("", "v1", "bindings"): {"create": testPodBindingEviction}, 115 gvr("", "v1", "pods/binding"): {"create": testPodBindingEviction}, 116 gvr("", "v1", "pods/eviction"): {"create": testPodBindingEviction}, 117 118 gvr("", "v1", "nodes/proxy"): {"*": testSubresourceProxy}, 119 gvr("", "v1", "pods/proxy"): {"*": testSubresourceProxy}, 120 gvr("", "v1", "services/proxy"): {"*": testSubresourceProxy}, 121 122 gvr("", "v1", "serviceaccounts/token"): {"create": testTokenCreate}, 123 124 gvr("random.numbers.com", "v1", "integers"): {"create": testPruningRandomNumbers}, 125 126 // DIFF: This test is used for webhook test but disabled here until we have mutating 127 // admission policy to write to "foo" field 128 // gvr("custom.fancy.com", "v2", "pants"): {"create": testNoPruningCustomFancy}, 129 } 130 131 // admissionExemptResources lists objects which are exempt from admission validation/mutation, 132 // only resources exempted from admission processing by API server should be listed here. 133 admissionExemptResources = map[schema.GroupVersionResource]bool{ 134 // DIFF: WebhookConfigurations are exempt for webhook admission but not 135 // for policy admission. 136 // gvr("admissionregistration.k8s.io", "v1beta1", "mutatingwebhookconfigurations"): true, 137 // gvr("admissionregistration.k8s.io", "v1beta1", "validatingwebhookconfigurations"): true, 138 // gvr("admissionregistration.k8s.io", "v1", "mutatingwebhookconfigurations"): true, 139 // gvr("admissionregistration.k8s.io", "v1", "validatingwebhookconfigurations"): true, 140 gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"): true, 141 gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies/status"): true, 142 gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicybindings"): true, 143 gvr("admissionregistration.k8s.io", "v1beta1", "validatingadmissionpolicies"): true, 144 gvr("admissionregistration.k8s.io", "v1beta1", "validatingadmissionpolicies/status"): true, 145 gvr("admissionregistration.k8s.io", "v1beta1", "validatingadmissionpolicybindings"): true, 146 } 147 148 parentResources = map[schema.GroupVersionResource]schema.GroupVersionResource{ 149 gvr("extensions", "v1beta1", "replicationcontrollers/scale"): gvr("", "v1", "replicationcontrollers"), 150 } 151 152 // stubDataOverrides holds either non persistent resources' definitions or resources where default stub needs to be overridden. 153 stubDataOverrides = map[schema.GroupVersionResource]string{ 154 // Non persistent Reviews resource 155 gvr("authentication.k8s.io", "v1", "tokenreviews"): `{"metadata": {"name": "tokenreview"}, "spec": {"token": "token", "audience": ["audience1","audience2"]}}`, 156 gvr("authentication.k8s.io", "v1beta1", "tokenreviews"): `{"metadata": {"name": "tokenreview"}, "spec": {"token": "token", "audience": ["audience1","audience2"]}}`, 157 gvr("authentication.k8s.io", "v1alpha1", "selfsubjectreviews"): `{"metadata": {"name": "SelfSubjectReview"},"status":{"userInfo":{}}}`, 158 gvr("authentication.k8s.io", "v1beta1", "selfsubjectreviews"): `{"metadata": {"name": "SelfSubjectReview"},"status":{"userInfo":{}}}`, 159 gvr("authentication.k8s.io", "v1", "selfsubjectreviews"): `{"metadata": {"name": "SelfSubjectReview"},"status":{"userInfo":{}}}`, 160 gvr("authorization.k8s.io", "v1", "localsubjectaccessreviews"): `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"uid": "token", "user": "user1","groups": ["group1","group2"],"resourceAttributes": {"name":"name1","namespace":"` + testNamespace + `"}}}`, 161 gvr("authorization.k8s.io", "v1", "subjectaccessreviews"): `{"metadata": {"name": "", "namespace":""}, "spec": {"user":"user1","resourceAttributes": {"name":"name1", "namespace":"` + testNamespace + `"}}}`, 162 gvr("authorization.k8s.io", "v1", "selfsubjectaccessreviews"): `{"metadata": {"name": "", "namespace":""}, "spec": {"resourceAttributes": {"name":"name1", "namespace":""}}}`, 163 gvr("authorization.k8s.io", "v1", "selfsubjectrulesreviews"): `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"namespace":"` + testNamespace + `"}}`, 164 gvr("authorization.k8s.io", "v1beta1", "localsubjectaccessreviews"): `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"uid": "token", "user": "user1","groups": ["group1","group2"],"resourceAttributes": {"name":"name1","namespace":"` + testNamespace + `"}}}`, 165 gvr("authorization.k8s.io", "v1beta1", "subjectaccessreviews"): `{"metadata": {"name": "", "namespace":""}, "spec": {"user":"user1","resourceAttributes": {"name":"name1", "namespace":"` + testNamespace + `"}}}`, 166 gvr("authorization.k8s.io", "v1beta1", "selfsubjectaccessreviews"): `{"metadata": {"name": "", "namespace":""}, "spec": {"resourceAttributes": {"name":"name1", "namespace":""}}}`, 167 gvr("authorization.k8s.io", "v1beta1", "selfsubjectrulesreviews"): `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"namespace":"` + testNamespace + `"}}`, 168 169 // Other Non persistent resources 170 } 171 ) 172 173 type webhookOptions struct { 174 version string 175 176 // phase indicates whether this is a mutating or validating webhook 177 phase string 178 // converted indicates if this webhook makes use of matchPolicy:equivalent and expects conversion. 179 // if true, recordGVR and expectGVK are mapped through gvrToConvertedGVR/gvrToConvertedGVK. 180 // if false, recordGVR and expectGVK are compared directly to the admission review. 181 converted bool 182 } 183 184 type holder struct { 185 lock sync.RWMutex 186 187 t *testing.T 188 189 // DIFF: Warning handler removed in policy test. 190 // warningHandler *warningHandler 191 192 recordGVR metav1.GroupVersionResource 193 recordOperation string 194 recordNamespace string 195 recordName string 196 197 expectGVK schema.GroupVersionKind 198 expectObject bool 199 expectOldObject bool 200 expectOptionsGVK schema.GroupVersionKind 201 expectOptions bool 202 203 // gvrToConvertedGVR maps the GVR submitted to the API server to the expected GVR when converted to the webhook-recognized resource. 204 // When a converted request is recorded, gvrToConvertedGVR[recordGVR] is compared to the GVR seen by the webhook. 205 gvrToConvertedGVR map[metav1.GroupVersionResource]metav1.GroupVersionResource 206 // gvrToConvertedGVR maps the GVR submitted to the API server to the expected GVK when converted to the webhook-recognized resource. 207 // When a converted request is recorded, gvrToConvertedGVR[expectGVK] is compared to the GVK seen by the webhook. 208 gvrToConvertedGVK map[metav1.GroupVersionResource]schema.GroupVersionKind 209 210 recorded map[webhookOptions]*admissionRequest 211 } 212 213 func (h *holder) reset(t *testing.T) { 214 h.lock.Lock() 215 defer h.lock.Unlock() 216 h.t = t 217 h.recordGVR = metav1.GroupVersionResource{} 218 h.expectGVK = schema.GroupVersionKind{} 219 h.recordOperation = "" 220 h.recordName = "" 221 h.recordNamespace = "" 222 h.expectObject = false 223 h.expectOldObject = false 224 h.expectOptionsGVK = schema.GroupVersionKind{} 225 h.expectOptions = false 226 // DIFF: Warning handler removed 227 // h.warningHandler.reset() 228 229 // Set up the recorded map with nil records for all combinations 230 h.recorded = map[webhookOptions]*admissionRequest{} 231 for _, phase := range []string{mutation, validation} { 232 for _, converted := range []bool{true, false} { 233 for _, version := range []string{"v1", "v1beta1"} { 234 h.recorded[webhookOptions{version: version, phase: phase, converted: converted}] = nil 235 } 236 } 237 } 238 } 239 func (h *holder) expect(gvr schema.GroupVersionResource, gvk, optionsGVK schema.GroupVersionKind, operation v1beta1.Operation, name, namespace string, object, oldObject, options bool) { 240 // Special-case namespaces, since the object name shows up in request attributes 241 if len(namespace) == 0 && gvk.Group == "" && gvk.Version == "v1" && gvk.Kind == "Namespace" { 242 namespace = name 243 } 244 245 h.lock.Lock() 246 defer h.lock.Unlock() 247 h.recordGVR = metav1.GroupVersionResource{Group: gvr.Group, Version: gvr.Version, Resource: gvr.Resource} 248 h.expectGVK = gvk 249 h.recordOperation = string(operation) 250 h.recordName = name 251 h.recordNamespace = namespace 252 h.expectObject = object 253 h.expectOldObject = oldObject 254 h.expectOptionsGVK = optionsGVK 255 h.expectOptions = options 256 // DIFF: Warning handler removed 257 // h.warningHandler.reset() 258 259 // Set up the recorded map with nil records for all combinations 260 h.recorded = map[webhookOptions]*admissionRequest{} 261 for _, phase := range []string{mutation, validation} { 262 for _, converted := range []bool{true, false} { 263 for _, version := range []string{"v1", "v1beta1"} { 264 h.recorded[webhookOptions{version: version, phase: phase, converted: converted}] = nil 265 } 266 } 267 } 268 } 269 270 type admissionRequest struct { 271 Operation string 272 Resource metav1.GroupVersionResource 273 SubResource string 274 Namespace string 275 Name string 276 Object runtime.RawExtension 277 OldObject runtime.RawExtension 278 Options runtime.RawExtension 279 } 280 281 func (h *holder) record(version string, phase string, converted bool, request *admissionRequest) { 282 h.lock.Lock() 283 defer h.lock.Unlock() 284 285 // this is useful to turn on if items aren't getting recorded and you need to figure out why 286 debug := false 287 if debug { 288 h.t.Logf("%s %#v %v", request.Operation, request.Resource, request.SubResource) 289 } 290 291 resource := request.Resource 292 if len(request.SubResource) > 0 { 293 resource.Resource += "/" + request.SubResource 294 } 295 296 // See if we should record this 297 gvrToRecord := h.recordGVR 298 if converted { 299 // If this is a converted webhook, map to the GVR we expect the webhook to see 300 gvrToRecord = h.gvrToConvertedGVR[h.recordGVR] 301 } 302 if resource != gvrToRecord { 303 if debug { 304 h.t.Log(resource, "!=", gvrToRecord) 305 } 306 return 307 } 308 309 if request.Operation != h.recordOperation { 310 if debug { 311 h.t.Log(request.Operation, "!=", h.recordOperation) 312 } 313 return 314 } 315 if request.Namespace != h.recordNamespace { 316 if debug { 317 h.t.Log(request.Namespace, "!=", h.recordNamespace) 318 } 319 return 320 } 321 322 name := request.Name 323 if name != h.recordName { 324 if debug { 325 h.t.Log(name, "!=", h.recordName) 326 } 327 return 328 } 329 330 if debug { 331 h.t.Logf("recording: %#v = %s %#v %v", webhookOptions{version: version, phase: phase, converted: converted}, request.Operation, request.Resource, request.SubResource) 332 } 333 h.recorded[webhookOptions{version: version, phase: phase, converted: converted}] = request 334 } 335 336 func (h *holder) verify(t *testing.T) { 337 h.lock.Lock() 338 defer h.lock.Unlock() 339 340 for options, value := range h.recorded { 341 if err := h.verifyRequest(options, value); err != nil { 342 t.Errorf("version: %v, phase:%v, converted:%v error: %v", options.version, options.phase, options.converted, err) 343 } 344 } 345 } 346 347 func (h *holder) verifyRequest(webhookOptions webhookOptions, request *admissionRequest) error { 348 converted := webhookOptions.converted 349 350 // Check if current resource should be exempted from Admission processing 351 if admissionExemptResources[gvr(h.recordGVR.Group, h.recordGVR.Version, h.recordGVR.Resource)] { 352 if request == nil { 353 return nil 354 } 355 return fmt.Errorf("admission webhook was called, but not supposed to") 356 } 357 358 if request == nil { 359 return fmt.Errorf("no request received") 360 } 361 362 if h.expectObject { 363 if err := h.verifyObject(converted, request.Object.Object); err != nil { 364 return fmt.Errorf("object error: %v", err) 365 } 366 } else if request.Object.Object != nil { 367 return fmt.Errorf("unexpected object: %#v", request.Object.Object) 368 } 369 370 if h.expectOldObject { 371 if err := h.verifyObject(converted, request.OldObject.Object); err != nil { 372 return fmt.Errorf("old object error: %v", err) 373 } 374 } else if request.OldObject.Object != nil { 375 return fmt.Errorf("unexpected old object: %#v", request.OldObject.Object) 376 } 377 378 if h.expectOptions { 379 if err := h.verifyOptions(request.Options.Object); err != nil { 380 return fmt.Errorf("options error: %v", err) 381 } 382 } else if request.Options.Object != nil { 383 return fmt.Errorf("unexpected options: %#v", request.Options.Object) 384 } 385 386 // DIFF: This check was removed for policy tests since it only applies 387 // to webhook 388 // if !h.warningHandler.hasWarning(makeWarning(webhookOptions.version, webhookOptions.phase, webhookOptions.converted)) { 389 // return fmt.Errorf("no warning received from webhook") 390 // } 391 392 return nil 393 } 394 395 func (h *holder) verifyObject(converted bool, obj runtime.Object) error { 396 if obj == nil { 397 return fmt.Errorf("no object sent") 398 } 399 expectGVK := h.expectGVK 400 if converted { 401 expectGVK = h.gvrToConvertedGVK[h.recordGVR] 402 } 403 if obj.GetObjectKind().GroupVersionKind() != expectGVK { 404 return fmt.Errorf("expected %#v, got %#v", expectGVK, obj.GetObjectKind().GroupVersionKind()) 405 } 406 return nil 407 } 408 409 func (h *holder) verifyOptions(options runtime.Object) error { 410 if options == nil { 411 return fmt.Errorf("no options sent") 412 } 413 if options.GetObjectKind().GroupVersionKind() != h.expectOptionsGVK { 414 return fmt.Errorf("expected %#v, got %#v", h.expectOptionsGVK, options.GetObjectKind().GroupVersionKind()) 415 } 416 return nil 417 } 418 419 func getTestFunc(gvr schema.GroupVersionResource, verb string) testFunc { 420 if f, found := customTestFuncs[gvr][verb]; found { 421 return f 422 } 423 if f, found := customTestFuncs[gvr]["*"]; found { 424 return f 425 } 426 if strings.Contains(gvr.Resource, "/") { 427 if f, found := defaultSubresourceFuncs[verb]; found { 428 return f 429 } 430 return unimplemented 431 } 432 if f, found := defaultResourceFuncs[verb]; found { 433 return f 434 } 435 return unimplemented 436 } 437 438 func getStubObj(gvr schema.GroupVersionResource, resource metav1.APIResource) (*unstructured.Unstructured, error) { 439 stub := "" 440 if data, ok := etcd.GetEtcdStorageDataForNamespace(testNamespace)[gvr]; ok { 441 stub = data.Stub 442 } 443 if data, ok := stubDataOverrides[gvr]; ok { 444 stub = data 445 } 446 if len(stub) == 0 { 447 return nil, fmt.Errorf("no stub data for %#v", gvr) 448 } 449 450 stubObj := &unstructured.Unstructured{Object: map[string]interface{}{}} 451 if err := json.Unmarshal([]byte(stub), &stubObj.Object); err != nil { 452 return nil, fmt.Errorf("error unmarshaling stub for %#v: %v", gvr, err) 453 } 454 return stubObj, nil 455 } 456 457 func createOrGetResource(client dynamic.Interface, gvr schema.GroupVersionResource, resource metav1.APIResource) (*unstructured.Unstructured, error) { 458 stubObj, err := getStubObj(gvr, resource) 459 if err != nil { 460 return nil, err 461 } 462 ns := "" 463 if resource.Namespaced { 464 ns = testNamespace 465 } 466 obj, err := client.Resource(gvr).Namespace(ns).Get(context.TODO(), stubObj.GetName(), metav1.GetOptions{}) 467 if err == nil { 468 return obj, nil 469 } 470 if !apierrors.IsNotFound(err) { 471 return nil, err 472 } 473 return client.Resource(gvr).Namespace(ns).Create(context.TODO(), stubObj, metav1.CreateOptions{}) 474 } 475 476 func gvr(group, version, resource string) schema.GroupVersionResource { 477 return schema.GroupVersionResource{Group: group, Version: version, Resource: resource} 478 } 479 func gvk(group, version, kind string) schema.GroupVersionKind { 480 return schema.GroupVersionKind{Group: group, Version: version, Kind: kind} 481 } 482 483 var ( 484 gvkCreateOptions = metav1.SchemeGroupVersion.WithKind("CreateOptions") 485 gvkUpdateOptions = metav1.SchemeGroupVersion.WithKind("UpdateOptions") 486 gvkDeleteOptions = metav1.SchemeGroupVersion.WithKind("DeleteOptions") 487 ) 488 489 func shouldTestResource(gvr schema.GroupVersionResource, resource metav1.APIResource) bool { 490 return sets.NewString(resource.Verbs...).HasAny("create", "update", "patch", "connect", "delete", "deletecollection") 491 } 492 493 func shouldTestResourceVerb(gvr schema.GroupVersionResource, resource metav1.APIResource, verb string) bool { 494 return sets.NewString(resource.Verbs...).Has(verb) 495 } 496 497 // 498 // generic resource testing 499 // 500 501 func testResourceCreate(c *testContext) { 502 stubObj, err := getStubObj(c.gvr, c.resource) 503 if err != nil { 504 c.t.Error(err) 505 return 506 } 507 ns := "" 508 if c.resource.Namespaced { 509 ns = testNamespace 510 } 511 c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkCreateOptions, v1beta1.Create, stubObj.GetName(), ns, true, false, true) 512 _, err = c.client.Resource(c.gvr).Namespace(ns).Create(context.TODO(), stubObj, metav1.CreateOptions{}) 513 if err != nil { 514 c.t.Error(err) 515 return 516 } 517 } 518 519 func testResourceUpdate(c *testContext) { 520 if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { 521 obj, err := createOrGetResource(c.client, c.gvr, c.resource) 522 if err != nil { 523 return err 524 } 525 obj.SetAnnotations(map[string]string{"update": "true"}) 526 c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkUpdateOptions, v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true, true) 527 _, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Update(context.TODO(), obj, metav1.UpdateOptions{}) 528 return err 529 }); err != nil { 530 c.t.Error(err) 531 return 532 } 533 } 534 535 func testResourcePatch(c *testContext) { 536 obj, err := createOrGetResource(c.client, c.gvr, c.resource) 537 if err != nil { 538 c.t.Error(err) 539 return 540 } 541 c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkUpdateOptions, v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true, true) 542 _, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch( 543 context.TODO(), 544 obj.GetName(), 545 types.MergePatchType, 546 []byte(`{"metadata":{"annotations":{"patch":"true"}}}`), 547 metav1.PatchOptions{}) 548 if err != nil { 549 c.t.Error(err) 550 return 551 } 552 } 553 554 func testResourceDelete(c *testContext) { 555 // Verify that an immediate delete triggers the webhook and populates the admisssionRequest.oldObject. 556 obj, err := createOrGetResource(c.client, c.gvr, c.resource) 557 if err != nil { 558 c.t.Error(err) 559 return 560 } 561 background := metav1.DeletePropagationBackground 562 zero := int64(0) 563 c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkDeleteOptions, v1beta1.Delete, obj.GetName(), obj.GetNamespace(), false, true, true) 564 err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(context.TODO(), obj.GetName(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background}) 565 if err != nil { 566 c.t.Error(err) 567 return 568 } 569 c.admissionHolder.verify(c.t) 570 571 // wait for the item to be gone 572 err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) { 573 obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{}) 574 if apierrors.IsNotFound(err) { 575 return true, nil 576 } 577 if err == nil { 578 c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers()) 579 return false, nil 580 } 581 return false, err 582 }) 583 if err != nil { 584 c.t.Error(err) 585 return 586 } 587 588 // Verify that an update-on-delete triggers the webhook and populates the admisssionRequest.oldObject. 589 obj, err = createOrGetResource(c.client, c.gvr, c.resource) 590 if err != nil { 591 c.t.Error(err) 592 return 593 } 594 // Adding finalizer to the object, then deleting it. 595 // We don't add finalizers by setting DeleteOptions.PropagationPolicy 596 // because some resource (e.g., events) do not support garbage 597 // collector finalizers. 598 _, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch( 599 context.TODO(), 600 obj.GetName(), 601 types.MergePatchType, 602 []byte(`{"metadata":{"finalizers":["test/k8s.io"]}}`), 603 metav1.PatchOptions{}) 604 if err != nil { 605 c.t.Error(err) 606 return 607 } 608 c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkDeleteOptions, v1beta1.Delete, obj.GetName(), obj.GetNamespace(), false, true, true) 609 err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(context.TODO(), obj.GetName(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background}) 610 if err != nil { 611 c.t.Error(err) 612 return 613 } 614 c.admissionHolder.verify(c.t) 615 616 // wait other finalizers (e.g., crd's customresourcecleanup finalizer) to be removed. 617 err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) { 618 obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{}) 619 if err != nil { 620 return false, err 621 } 622 finalizers := obj.GetFinalizers() 623 if len(finalizers) != 1 { 624 c.t.Logf("waiting for other finalizers on %#v %s to be removed, existing finalizers are %v", c.gvr, obj.GetName(), obj.GetFinalizers()) 625 return false, nil 626 } 627 if finalizers[0] != "test/k8s.io" { 628 return false, fmt.Errorf("expected the single finalizer on %#v %s to be test/k8s.io, got %v", c.gvr, obj.GetName(), obj.GetFinalizers()) 629 } 630 return true, nil 631 }) 632 if err != nil { 633 c.t.Error(err) 634 return 635 } 636 637 // remove the finalizer 638 _, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch( 639 context.TODO(), 640 obj.GetName(), 641 types.MergePatchType, 642 []byte(`{"metadata":{"finalizers":[]}}`), 643 metav1.PatchOptions{}) 644 if err != nil { 645 c.t.Error(err) 646 return 647 } 648 // wait for the item to be gone 649 err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) { 650 obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{}) 651 if apierrors.IsNotFound(err) { 652 return true, nil 653 } 654 if err == nil { 655 c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers()) 656 return false, nil 657 } 658 return false, err 659 }) 660 if err != nil { 661 c.t.Error(err) 662 return 663 } 664 } 665 666 func testResourceDeletecollection(c *testContext) { 667 obj, err := createOrGetResource(c.client, c.gvr, c.resource) 668 if err != nil { 669 c.t.Error(err) 670 return 671 } 672 background := metav1.DeletePropagationBackground 673 zero := int64(0) 674 675 // update the object with a label that matches our selector 676 _, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch( 677 context.TODO(), 678 obj.GetName(), 679 types.MergePatchType, 680 []byte(`{"metadata":{"labels":{"webhooktest":"true"}}}`), 681 metav1.PatchOptions{}) 682 if err != nil { 683 c.t.Error(err) 684 return 685 } 686 687 // set expectations 688 c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkDeleteOptions, v1beta1.Delete, "", obj.GetNamespace(), false, true, true) 689 690 // delete 691 err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).DeleteCollection(context.TODO(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background}, metav1.ListOptions{LabelSelector: "webhooktest=true"}) 692 if err != nil { 693 c.t.Error(err) 694 return 695 } 696 697 // wait for the item to be gone 698 err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) { 699 obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{}) 700 if apierrors.IsNotFound(err) { 701 return true, nil 702 } 703 if err == nil { 704 c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers()) 705 return false, nil 706 } 707 return false, err 708 }) 709 if err != nil { 710 c.t.Error(err) 711 return 712 } 713 } 714 715 func getParentGVR(gvr schema.GroupVersionResource) schema.GroupVersionResource { 716 parentGVR, found := parentResources[gvr] 717 // if no special override is found, just drop the subresource 718 if !found { 719 parentGVR = gvr 720 parentGVR.Resource = strings.Split(parentGVR.Resource, "/")[0] 721 } 722 return parentGVR 723 } 724 725 func testTokenCreate(c *testContext) { 726 saGVR := gvr("", "v1", "serviceaccounts") 727 sa, err := createOrGetResource(c.client, saGVR, c.resources[saGVR]) 728 if err != nil { 729 c.t.Error(err) 730 return 731 } 732 733 c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkCreateOptions, v1beta1.Create, sa.GetName(), sa.GetNamespace(), true, false, true) 734 if err = c.clientset.CoreV1().RESTClient().Post().Namespace(sa.GetNamespace()).Resource("serviceaccounts").Name(sa.GetName()).SubResource("token").Body(&authenticationv1.TokenRequest{ 735 ObjectMeta: metav1.ObjectMeta{Name: sa.GetName()}, 736 Spec: authenticationv1.TokenRequestSpec{ 737 Audiences: []string{"api"}, 738 }, 739 }).Do(context.TODO()).Error(); err != nil { 740 c.t.Error(err) 741 return 742 } 743 c.admissionHolder.verify(c.t) 744 } 745 746 func testSubresourceUpdate(c *testContext) { 747 if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { 748 parentGVR := getParentGVR(c.gvr) 749 parentResource := c.resources[parentGVR] 750 obj, err := createOrGetResource(c.client, parentGVR, parentResource) 751 if err != nil { 752 return err 753 } 754 755 // Save the parent object as what we submit 756 submitObj := obj 757 758 gvrWithoutSubresources := c.gvr 759 gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0] 760 subresources := strings.Split(c.gvr.Resource, "/")[1:] 761 762 // If the subresource supports get, fetch that as the object to submit (namespaces/finalize, */scale, etc) 763 if sets.NewString(c.resource.Verbs...).Has("get") { 764 submitObj, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{}, subresources...) 765 if err != nil { 766 return err 767 } 768 } 769 770 // Modify the object 771 submitObj.SetAnnotations(map[string]string{"subresourceupdate": "true"}) 772 773 // set expectations 774 c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkUpdateOptions, v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true, true) 775 776 _, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Update( 777 context.TODO(), 778 submitObj, 779 metav1.UpdateOptions{}, 780 subresources..., 781 ) 782 return err 783 }); err != nil { 784 c.t.Error(err) 785 } 786 } 787 788 func testSubresourcePatch(c *testContext) { 789 parentGVR := getParentGVR(c.gvr) 790 parentResource := c.resources[parentGVR] 791 obj, err := createOrGetResource(c.client, parentGVR, parentResource) 792 if err != nil { 793 c.t.Error(err) 794 return 795 } 796 797 gvrWithoutSubresources := c.gvr 798 gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0] 799 subresources := strings.Split(c.gvr.Resource, "/")[1:] 800 801 // set expectations 802 c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkUpdateOptions, v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true, true) 803 804 _, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Patch( 805 context.TODO(), 806 obj.GetName(), 807 types.MergePatchType, 808 []byte(`{"metadata":{"annotations":{"subresourcepatch":"true"}}}`), 809 metav1.PatchOptions{}, 810 subresources..., 811 ) 812 if err != nil { 813 c.t.Error(err) 814 return 815 } 816 } 817 818 func unimplemented(c *testContext) { 819 c.t.Errorf("Test function for %+v has not been implemented...", c.gvr) 820 } 821 822 // 823 // custom methods 824 // 825 826 // testNamespaceDelete verifies namespace-specific delete behavior: 827 // - ensures admission is called on first delete (which only sets deletionTimestamp and terminating state) 828 // - removes finalizer from namespace 829 // - ensures admission is called on final delete once finalizers are removed 830 func testNamespaceDelete(c *testContext) { 831 obj, err := createOrGetResource(c.client, c.gvr, c.resource) 832 if err != nil { 833 c.t.Error(err) 834 return 835 } 836 background := metav1.DeletePropagationBackground 837 zero := int64(0) 838 839 c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkDeleteOptions, v1beta1.Delete, obj.GetName(), obj.GetNamespace(), false, true, true) 840 err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(context.TODO(), obj.GetName(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background}) 841 if err != nil { 842 c.t.Error(err) 843 return 844 } 845 c.admissionHolder.verify(c.t) 846 847 // do the finalization so the namespace can be deleted 848 obj, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{}) 849 if err != nil { 850 c.t.Error(err) 851 return 852 } 853 err = unstructured.SetNestedField(obj.Object, nil, "spec", "finalizers") 854 if err != nil { 855 c.t.Error(err) 856 return 857 } 858 _, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Update(context.TODO(), obj, metav1.UpdateOptions{}, "finalize") 859 if err != nil { 860 c.t.Error(err) 861 return 862 } 863 // verify namespace is gone 864 obj, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{}) 865 if err == nil || !apierrors.IsNotFound(err) { 866 c.t.Errorf("expected namespace to be gone, got %#v, %v", obj, err) 867 } 868 } 869 870 // testDeploymentRollback verifies rollback-specific behavior: 871 // - creates a parent deployment 872 // - creates a rollback object and posts it 873 func testDeploymentRollback(c *testContext) { 874 deploymentGVR := gvr("apps", "v1", "deployments") 875 obj, err := createOrGetResource(c.client, deploymentGVR, c.resources[deploymentGVR]) 876 if err != nil { 877 c.t.Error(err) 878 return 879 } 880 881 gvrWithoutSubresources := c.gvr 882 gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0] 883 subresources := strings.Split(c.gvr.Resource, "/")[1:] 884 885 c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkCreateOptions, v1beta1.Create, obj.GetName(), obj.GetNamespace(), true, false, true) 886 887 var rollbackObj runtime.Object 888 switch c.gvr { 889 case gvr("apps", "v1beta1", "deployments/rollback"): 890 rollbackObj = &appsv1beta1.DeploymentRollback{ 891 TypeMeta: metav1.TypeMeta{APIVersion: "apps/v1beta1", Kind: "DeploymentRollback"}, 892 Name: obj.GetName(), 893 RollbackTo: appsv1beta1.RollbackConfig{Revision: 0}, 894 } 895 case gvr("extensions", "v1beta1", "deployments/rollback"): 896 rollbackObj = &extensionsv1beta1.DeploymentRollback{ 897 TypeMeta: metav1.TypeMeta{APIVersion: "extensions/v1beta1", Kind: "DeploymentRollback"}, 898 Name: obj.GetName(), 899 RollbackTo: extensionsv1beta1.RollbackConfig{Revision: 0}, 900 } 901 default: 902 c.t.Errorf("unknown rollback resource %#v", c.gvr) 903 return 904 } 905 906 rollbackUnstructuredBody, err := runtime.DefaultUnstructuredConverter.ToUnstructured(rollbackObj) 907 if err != nil { 908 c.t.Errorf("ToUnstructured failed: %v", err) 909 return 910 } 911 rollbackUnstructuredObj := &unstructured.Unstructured{Object: rollbackUnstructuredBody} 912 rollbackUnstructuredObj.SetName(obj.GetName()) 913 914 _, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Create(context.TODO(), rollbackUnstructuredObj, metav1.CreateOptions{}, subresources...) 915 if err != nil { 916 c.t.Error(err) 917 return 918 } 919 } 920 921 // testPodConnectSubresource verifies connect subresources 922 func testPodConnectSubresource(c *testContext) { 923 podGVR := gvr("", "v1", "pods") 924 pod, err := createOrGetResource(c.client, podGVR, c.resources[podGVR]) 925 if err != nil { 926 c.t.Error(err) 927 return 928 } 929 930 // check all upgradeable verbs 931 for _, httpMethod := range []string{"GET", "POST"} { 932 c.t.Logf("verifying %v", httpMethod) 933 934 c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), schema.GroupVersionKind{}, v1beta1.Connect, pod.GetName(), pod.GetNamespace(), true, false, false) 935 var err error 936 switch c.gvr { 937 case gvr("", "v1", "pods/exec"): 938 err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("exec").Do(context.TODO()).Error() 939 case gvr("", "v1", "pods/attach"): 940 err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("attach").Do(context.TODO()).Error() 941 case gvr("", "v1", "pods/portforward"): 942 err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("portforward").Do(context.TODO()).Error() 943 default: 944 c.t.Errorf("unknown subresource %#v", c.gvr) 945 return 946 } 947 948 if err != nil { 949 c.t.Logf("debug: result of subresource connect: %v", err) 950 } 951 c.admissionHolder.verify(c.t) 952 953 } 954 } 955 956 // testPodBindingEviction verifies pod binding and eviction admission 957 func testPodBindingEviction(c *testContext) { 958 podGVR := gvr("", "v1", "pods") 959 pod, err := createOrGetResource(c.client, podGVR, c.resources[podGVR]) 960 if err != nil { 961 c.t.Error(err) 962 return 963 } 964 965 background := metav1.DeletePropagationBackground 966 zero := int64(0) 967 forceDelete := metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background} 968 defer func() { 969 err := c.clientset.CoreV1().Pods(pod.GetNamespace()).Delete(context.TODO(), pod.GetName(), forceDelete) 970 if err != nil && !apierrors.IsNotFound(err) { 971 c.t.Error(err) 972 return 973 } 974 }() 975 976 c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkCreateOptions, v1beta1.Create, pod.GetName(), pod.GetNamespace(), true, false, true) 977 978 switch c.gvr { 979 case gvr("", "v1", "bindings"): 980 err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("bindings").Body(&corev1.Binding{ 981 ObjectMeta: metav1.ObjectMeta{Name: pod.GetName()}, 982 Target: corev1.ObjectReference{Name: "foo", Kind: "Node", APIVersion: "v1"}, 983 }).Do(context.TODO()).Error() 984 985 case gvr("", "v1", "pods/binding"): 986 err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("binding").Body(&corev1.Binding{ 987 ObjectMeta: metav1.ObjectMeta{Name: pod.GetName()}, 988 Target: corev1.ObjectReference{Name: "foo", Kind: "Node", APIVersion: "v1"}, 989 }).Do(context.TODO()).Error() 990 991 case gvr("", "v1", "pods/eviction"): 992 err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("eviction").Body(&policyv1.Eviction{ 993 ObjectMeta: metav1.ObjectMeta{Name: pod.GetName()}, 994 DeleteOptions: &forceDelete, 995 }).Do(context.TODO()).Error() 996 997 default: 998 c.t.Errorf("unhandled resource %#v", c.gvr) 999 return 1000 } 1001 1002 if err != nil { 1003 c.t.Error(err) 1004 return 1005 } 1006 } 1007 1008 // testSubresourceProxy verifies proxy subresources 1009 func testSubresourceProxy(c *testContext) { 1010 parentGVR := getParentGVR(c.gvr) 1011 parentResource := c.resources[parentGVR] 1012 obj, err := createOrGetResource(c.client, parentGVR, parentResource) 1013 if err != nil { 1014 c.t.Error(err) 1015 return 1016 } 1017 1018 gvrWithoutSubresources := c.gvr 1019 gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0] 1020 subresources := strings.Split(c.gvr.Resource, "/")[1:] 1021 1022 verbToHTTPMethods := map[string][]string{ 1023 "create": {"POST", "GET", "HEAD", "OPTIONS"}, // also test read-only verbs map to Connect admission 1024 "update": {"PUT"}, 1025 "patch": {"PATCH"}, 1026 "delete": {"DELETE"}, 1027 } 1028 httpMethodsToTest, ok := verbToHTTPMethods[c.verb] 1029 if !ok { 1030 c.t.Errorf("unknown verb %v", c.verb) 1031 return 1032 } 1033 1034 for _, httpMethod := range httpMethodsToTest { 1035 c.t.Logf("testing %v", httpMethod) 1036 request := c.clientset.CoreV1().RESTClient().Verb(httpMethod) 1037 1038 // add the namespace if required 1039 if len(obj.GetNamespace()) > 0 { 1040 request = request.Namespace(obj.GetNamespace()) 1041 } 1042 1043 // set expectations 1044 c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), schema.GroupVersionKind{}, v1beta1.Connect, obj.GetName(), obj.GetNamespace(), true, false, false) 1045 // run the request. we don't actually care if the request is successful, just that admission gets called as expected 1046 err = request.Resource(gvrWithoutSubresources.Resource).Name(obj.GetName()).SubResource(subresources...).Do(context.TODO()).Error() 1047 if err != nil { 1048 c.t.Logf("debug: result of subresource proxy (error expected): %v", err) 1049 } 1050 // verify the result 1051 c.admissionHolder.verify(c.t) 1052 } 1053 } 1054 1055 func testPruningRandomNumbers(c *testContext) { 1056 testResourceCreate(c) 1057 1058 cr2pant, err := c.client.Resource(c.gvr).Get(context.TODO(), "fortytwo", metav1.GetOptions{}) 1059 if err != nil { 1060 c.t.Error(err) 1061 return 1062 } 1063 1064 foo, found, err := unstructured.NestedString(cr2pant.Object, "foo") 1065 if err != nil { 1066 c.t.Error(err) 1067 return 1068 } 1069 if found { 1070 c.t.Errorf("expected .foo to be pruned, but got: %s", foo) 1071 } 1072 } 1073 1074 // DIFF: Commented out for policy test. To be added back for mutating policy tests. 1075 // This test deoends on "foo" being set to test by admission webhook/policy. 1076 // func testNoPruningCustomFancy(c *testContext) { 1077 // testResourceCreate(c) 1078 1079 // cr2pant, err := c.client.Resource(c.gvr).Get(context.TODO(), "cr2pant", metav1.GetOptions{}) 1080 // if err != nil { 1081 // c.t.Error(err) 1082 // return 1083 // } 1084 1085 // foo, _, err := unstructured.NestedString(cr2pant.Object, "foo") 1086 // if err != nil { 1087 // c.t.Error(err) 1088 // return 1089 // } 1090 1091 // // check that no pruning took place 1092 // if expected, got := "test", foo; expected != got { 1093 // c.t.Errorf("expected /foo to be %q, got: %q", expected, got) 1094 // } 1095 // }