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