k8s.io/kubernetes@v1.29.3/test/integration/apiserver/admissionwebhook/admission_test.go (about) 1 /* 2 Copyright 2019 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package admissionwebhook 18 19 import ( 20 "context" 21 "crypto/tls" 22 "crypto/x509" 23 "encoding/json" 24 "fmt" 25 "io" 26 "net/http" 27 "net/http/httptest" 28 "path" 29 "sort" 30 "strings" 31 "sync" 32 "testing" 33 "time" 34 35 clientv3 "go.etcd.io/etcd/client/v3" 36 admissionreviewv1 "k8s.io/api/admission/v1" 37 "k8s.io/api/admission/v1beta1" 38 admissionregistrationv1 "k8s.io/api/admissionregistration/v1" 39 admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" 40 appsv1beta1 "k8s.io/api/apps/v1beta1" 41 authenticationv1 "k8s.io/api/authentication/v1" 42 corev1 "k8s.io/api/core/v1" 43 extensionsv1beta1 "k8s.io/api/extensions/v1beta1" 44 policyv1 "k8s.io/api/policy/v1" 45 apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 46 apierrors "k8s.io/apimachinery/pkg/api/errors" 47 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 48 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 49 "k8s.io/apimachinery/pkg/runtime" 50 "k8s.io/apimachinery/pkg/runtime/schema" 51 "k8s.io/apimachinery/pkg/types" 52 "k8s.io/apimachinery/pkg/util/sets" 53 "k8s.io/apimachinery/pkg/util/wait" 54 genericapirequest "k8s.io/apiserver/pkg/endpoints/request" 55 utilfeature "k8s.io/apiserver/pkg/util/feature" 56 "k8s.io/client-go/dynamic" 57 clientset "k8s.io/client-go/kubernetes" 58 "k8s.io/client-go/rest" 59 "k8s.io/client-go/util/retry" 60 featuregatetesting "k8s.io/component-base/featuregate/testing" 61 kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" 62 apisv1beta1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1beta1" 63 "k8s.io/kubernetes/pkg/features" 64 "k8s.io/kubernetes/test/integration/etcd" 65 "k8s.io/kubernetes/test/integration/framework" 66 ) 67 68 const ( 69 testNamespace = "webhook-integration" 70 testClientUsername = "webhook-integration-client" 71 72 mutation = "mutation" 73 validation = "validation" 74 ) 75 76 var ( 77 noSideEffects = admissionregistrationv1.SideEffectClassNone 78 ) 79 80 type testContext struct { 81 t *testing.T 82 83 admissionHolder *holder 84 85 client dynamic.Interface 86 clientset clientset.Interface 87 verb string 88 gvr schema.GroupVersionResource 89 resource metav1.APIResource 90 resources map[schema.GroupVersionResource]metav1.APIResource 91 } 92 93 type testFunc func(*testContext) 94 95 var ( 96 // defaultResourceFuncs holds the default test functions. 97 // may be overridden for specific resources by customTestFuncs. 98 defaultResourceFuncs = map[string]testFunc{ 99 "create": testResourceCreate, 100 "update": testResourceUpdate, 101 "patch": testResourcePatch, 102 "delete": testResourceDelete, 103 "deletecollection": testResourceDeletecollection, 104 } 105 106 // defaultSubresourceFuncs holds default subresource test functions. 107 // may be overridden for specific resources by customTestFuncs. 108 defaultSubresourceFuncs = map[string]testFunc{ 109 "update": testSubresourceUpdate, 110 "patch": testSubresourcePatch, 111 } 112 113 // customTestFuncs holds custom test functions by resource and verb. 114 customTestFuncs = map[schema.GroupVersionResource]map[string]testFunc{ 115 gvr("", "v1", "namespaces"): {"delete": testNamespaceDelete}, 116 117 gvr("apps", "v1beta1", "deployments/rollback"): {"create": testDeploymentRollback}, 118 gvr("extensions", "v1beta1", "deployments/rollback"): {"create": testDeploymentRollback}, 119 120 gvr("", "v1", "pods/attach"): {"create": testPodConnectSubresource}, 121 gvr("", "v1", "pods/exec"): {"create": testPodConnectSubresource}, 122 gvr("", "v1", "pods/portforward"): {"create": testPodConnectSubresource}, 123 124 gvr("", "v1", "bindings"): {"create": testPodBindingEviction}, 125 gvr("", "v1", "pods/binding"): {"create": testPodBindingEviction}, 126 gvr("", "v1", "pods/eviction"): {"create": testPodBindingEviction}, 127 128 gvr("", "v1", "nodes/proxy"): {"*": testSubresourceProxy}, 129 gvr("", "v1", "pods/proxy"): {"*": testSubresourceProxy}, 130 gvr("", "v1", "services/proxy"): {"*": testSubresourceProxy}, 131 132 gvr("", "v1", "serviceaccounts/token"): {"create": testTokenCreate}, 133 134 gvr("random.numbers.com", "v1", "integers"): {"create": testPruningRandomNumbers}, 135 gvr("custom.fancy.com", "v2", "pants"): {"create": testNoPruningCustomFancy}, 136 } 137 138 // admissionExemptResources lists objects which are exempt from admission validation/mutation, 139 // only resources exempted from admission processing by API server should be listed here. 140 admissionExemptResources = map[schema.GroupVersionResource]bool{ 141 gvr("admissionregistration.k8s.io", "v1beta1", "mutatingwebhookconfigurations"): true, 142 gvr("admissionregistration.k8s.io", "v1beta1", "validatingwebhookconfigurations"): true, 143 gvr("admissionregistration.k8s.io", "v1", "mutatingwebhookconfigurations"): true, 144 gvr("admissionregistration.k8s.io", "v1", "validatingwebhookconfigurations"): true, 145 gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"): true, 146 gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies/status"): true, 147 gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicybindings"): true, 148 gvr("admissionregistration.k8s.io", "v1beta1", "validatingadmissionpolicies"): true, 149 gvr("admissionregistration.k8s.io", "v1beta1", "validatingadmissionpolicies/status"): true, 150 gvr("admissionregistration.k8s.io", "v1beta1", "validatingadmissionpolicybindings"): true, 151 } 152 153 parentResources = map[schema.GroupVersionResource]schema.GroupVersionResource{ 154 gvr("extensions", "v1beta1", "replicationcontrollers/scale"): gvr("", "v1", "replicationcontrollers"), 155 } 156 157 // stubDataOverrides holds either non persistent resources' definitions or resources where default stub needs to be overridden. 158 stubDataOverrides = map[schema.GroupVersionResource]string{ 159 // Non persistent Reviews resource 160 gvr("authentication.k8s.io", "v1", "tokenreviews"): `{"metadata": {"name": "tokenreview"}, "spec": {"token": "token", "audience": ["audience1","audience2"]}}`, 161 gvr("authentication.k8s.io", "v1beta1", "tokenreviews"): `{"metadata": {"name": "tokenreview"}, "spec": {"token": "token", "audience": ["audience1","audience2"]}}`, 162 gvr("authentication.k8s.io", "v1alpha1", "selfsubjectreviews"): `{"metadata": {"name": "SelfSubjectReview"},"status":{"userInfo":{}}}`, 163 gvr("authentication.k8s.io", "v1beta1", "selfsubjectreviews"): `{"metadata": {"name": "SelfSubjectReview"},"status":{"userInfo":{}}}`, 164 gvr("authentication.k8s.io", "v1", "selfsubjectreviews"): `{"metadata": {"name": "SelfSubjectReview"},"status":{"userInfo":{}}}`, 165 gvr("authorization.k8s.io", "v1", "localsubjectaccessreviews"): `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"uid": "token", "user": "user1","groups": ["group1","group2"],"resourceAttributes": {"name":"name1","namespace":"` + testNamespace + `"}}}`, 166 gvr("authorization.k8s.io", "v1", "subjectaccessreviews"): `{"metadata": {"name": "", "namespace":""}, "spec": {"user":"user1","resourceAttributes": {"name":"name1", "namespace":"` + testNamespace + `"}}}`, 167 gvr("authorization.k8s.io", "v1", "selfsubjectaccessreviews"): `{"metadata": {"name": "", "namespace":""}, "spec": {"resourceAttributes": {"name":"name1", "namespace":""}}}`, 168 gvr("authorization.k8s.io", "v1", "selfsubjectrulesreviews"): `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"namespace":"` + testNamespace + `"}}`, 169 gvr("authorization.k8s.io", "v1beta1", "localsubjectaccessreviews"): `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"uid": "token", "user": "user1","groups": ["group1","group2"],"resourceAttributes": {"name":"name1","namespace":"` + testNamespace + `"}}}`, 170 gvr("authorization.k8s.io", "v1beta1", "subjectaccessreviews"): `{"metadata": {"name": "", "namespace":""}, "spec": {"user":"user1","resourceAttributes": {"name":"name1", "namespace":"` + testNamespace + `"}}}`, 171 gvr("authorization.k8s.io", "v1beta1", "selfsubjectaccessreviews"): `{"metadata": {"name": "", "namespace":""}, "spec": {"resourceAttributes": {"name":"name1", "namespace":""}}}`, 172 gvr("authorization.k8s.io", "v1beta1", "selfsubjectrulesreviews"): `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"namespace":"` + testNamespace + `"}}`, 173 174 // Other Non persistent resources 175 } 176 ) 177 178 type webhookOptions struct { 179 version string 180 181 // phase indicates whether this is a mutating or validating webhook 182 phase string 183 // converted indicates if this webhook makes use of matchPolicy:equivalent and expects conversion. 184 // if true, recordGVR and expectGVK are mapped through gvrToConvertedGVR/gvrToConvertedGVK. 185 // if false, recordGVR and expectGVK are compared directly to the admission review. 186 converted bool 187 } 188 189 type holder struct { 190 lock sync.RWMutex 191 192 t *testing.T 193 194 warningHandler *warningHandler 195 196 recordGVR metav1.GroupVersionResource 197 recordOperation string 198 recordNamespace string 199 recordName string 200 201 expectGVK schema.GroupVersionKind 202 expectObject bool 203 expectOldObject bool 204 expectOptionsGVK schema.GroupVersionKind 205 expectOptions bool 206 207 // gvrToConvertedGVR maps the GVR submitted to the API server to the expected GVR when converted to the webhook-recognized resource. 208 // When a converted request is recorded, gvrToConvertedGVR[recordGVR] is compared to the GVR seen by the webhook. 209 gvrToConvertedGVR map[metav1.GroupVersionResource]metav1.GroupVersionResource 210 // gvrToConvertedGVR maps the GVR submitted to the API server to the expected GVK when converted to the webhook-recognized resource. 211 // When a converted request is recorded, gvrToConvertedGVR[expectGVK] is compared to the GVK seen by the webhook. 212 gvrToConvertedGVK map[metav1.GroupVersionResource]schema.GroupVersionKind 213 214 recorded map[webhookOptions]*admissionRequest 215 } 216 217 func (h *holder) reset(t *testing.T) { 218 h.lock.Lock() 219 defer h.lock.Unlock() 220 h.t = t 221 h.recordGVR = metav1.GroupVersionResource{} 222 h.expectGVK = schema.GroupVersionKind{} 223 h.recordOperation = "" 224 h.recordName = "" 225 h.recordNamespace = "" 226 h.expectObject = false 227 h.expectOldObject = false 228 h.expectOptionsGVK = schema.GroupVersionKind{} 229 h.expectOptions = false 230 h.warningHandler.reset() 231 232 // Set up the recorded map with nil records for all combinations 233 h.recorded = map[webhookOptions]*admissionRequest{} 234 for _, phase := range []string{mutation, validation} { 235 for _, converted := range []bool{true, false} { 236 for _, version := range []string{"v1", "v1beta1"} { 237 h.recorded[webhookOptions{version: version, phase: phase, converted: converted}] = nil 238 } 239 } 240 } 241 } 242 func (h *holder) expect(gvr schema.GroupVersionResource, gvk, optionsGVK schema.GroupVersionKind, operation v1beta1.Operation, name, namespace string, object, oldObject, options bool) { 243 // Special-case namespaces, since the object name shows up in request attributes 244 if len(namespace) == 0 && gvk.Group == "" && gvk.Version == "v1" && gvk.Kind == "Namespace" { 245 namespace = name 246 } 247 248 h.lock.Lock() 249 defer h.lock.Unlock() 250 h.recordGVR = metav1.GroupVersionResource{Group: gvr.Group, Version: gvr.Version, Resource: gvr.Resource} 251 h.expectGVK = gvk 252 h.recordOperation = string(operation) 253 h.recordName = name 254 h.recordNamespace = namespace 255 h.expectObject = object 256 h.expectOldObject = oldObject 257 h.expectOptionsGVK = optionsGVK 258 h.expectOptions = options 259 h.warningHandler.reset() 260 261 // Set up the recorded map with nil records for all combinations 262 h.recorded = map[webhookOptions]*admissionRequest{} 263 for _, phase := range []string{mutation, validation} { 264 for _, converted := range []bool{true, false} { 265 for _, version := range []string{"v1", "v1beta1"} { 266 h.recorded[webhookOptions{version: version, phase: phase, converted: converted}] = nil 267 } 268 } 269 } 270 } 271 272 type admissionRequest struct { 273 Operation string 274 Resource metav1.GroupVersionResource 275 SubResource string 276 Namespace string 277 Name string 278 Object runtime.RawExtension 279 OldObject runtime.RawExtension 280 Options runtime.RawExtension 281 } 282 283 func (h *holder) record(version string, phase string, converted bool, request *admissionRequest) { 284 h.lock.Lock() 285 defer h.lock.Unlock() 286 287 // this is useful to turn on if items aren't getting recorded and you need to figure out why 288 debug := false 289 if debug { 290 h.t.Logf("%s %#v %v", request.Operation, request.Resource, request.SubResource) 291 } 292 293 resource := request.Resource 294 if len(request.SubResource) > 0 { 295 resource.Resource += "/" + request.SubResource 296 } 297 298 // See if we should record this 299 gvrToRecord := h.recordGVR 300 if converted { 301 // If this is a converted webhook, map to the GVR we expect the webhook to see 302 gvrToRecord = h.gvrToConvertedGVR[h.recordGVR] 303 } 304 if resource != gvrToRecord { 305 if debug { 306 h.t.Log(resource, "!=", gvrToRecord) 307 } 308 return 309 } 310 311 if request.Operation != h.recordOperation { 312 if debug { 313 h.t.Log(request.Operation, "!=", h.recordOperation) 314 } 315 return 316 } 317 if request.Namespace != h.recordNamespace { 318 if debug { 319 h.t.Log(request.Namespace, "!=", h.recordNamespace) 320 } 321 return 322 } 323 324 name := request.Name 325 if name != h.recordName { 326 if debug { 327 h.t.Log(name, "!=", h.recordName) 328 } 329 return 330 } 331 332 if debug { 333 h.t.Logf("recording: %#v = %s %#v %v", webhookOptions{version: version, phase: phase, converted: converted}, request.Operation, request.Resource, request.SubResource) 334 } 335 h.recorded[webhookOptions{version: version, phase: phase, converted: converted}] = request 336 } 337 338 func (h *holder) verify(t *testing.T) { 339 h.lock.Lock() 340 defer h.lock.Unlock() 341 342 for options, value := range h.recorded { 343 if err := h.verifyRequest(options, value); err != nil { 344 t.Errorf("version: %v, phase:%v, converted:%v error: %v", options.version, options.phase, options.converted, err) 345 } 346 } 347 } 348 349 func (h *holder) verifyRequest(webhookOptions webhookOptions, request *admissionRequest) error { 350 converted := webhookOptions.converted 351 352 // Check if current resource should be exempted from Admission processing 353 if admissionExemptResources[gvr(h.recordGVR.Group, h.recordGVR.Version, h.recordGVR.Resource)] { 354 if request == nil { 355 return nil 356 } 357 return fmt.Errorf("admission webhook was called, but not supposed to") 358 } 359 360 if request == nil { 361 return fmt.Errorf("no request received") 362 } 363 364 if h.expectObject { 365 if err := h.verifyObject(converted, request.Object.Object); err != nil { 366 return fmt.Errorf("object error: %v", err) 367 } 368 } else if request.Object.Object != nil { 369 return fmt.Errorf("unexpected object: %#v", request.Object.Object) 370 } 371 372 if h.expectOldObject { 373 if err := h.verifyObject(converted, request.OldObject.Object); err != nil { 374 return fmt.Errorf("old object error: %v", err) 375 } 376 } else if request.OldObject.Object != nil { 377 return fmt.Errorf("unexpected old object: %#v", request.OldObject.Object) 378 } 379 380 if h.expectOptions { 381 if err := h.verifyOptions(request.Options.Object); err != nil { 382 return fmt.Errorf("options error: %v", err) 383 } 384 } else if request.Options.Object != nil { 385 return fmt.Errorf("unexpected options: %#v", request.Options.Object) 386 } 387 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 type warningHandler struct { 420 lock sync.Mutex 421 warnings map[string]bool 422 } 423 424 func (w *warningHandler) reset() { 425 w.lock.Lock() 426 defer w.lock.Unlock() 427 w.warnings = map[string]bool{} 428 } 429 func (w *warningHandler) hasWarning(warning string) bool { 430 w.lock.Lock() 431 defer w.lock.Unlock() 432 return w.warnings[warning] 433 } 434 func makeWarning(version string, phase string, converted bool) string { 435 return fmt.Sprintf("%v/%v/%v", version, phase, converted) 436 } 437 438 func (w *warningHandler) HandleWarningHeader(code int, agent string, message string) { 439 if code != 299 || len(message) == 0 { 440 return 441 } 442 w.lock.Lock() 443 defer w.lock.Unlock() 444 w.warnings[message] = true 445 } 446 447 // TestWebhookAdmissionWithWatchCache tests communication between API server and webhook process. 448 func TestWebhookAdmissionWithWatchCache(t *testing.T) { 449 testWebhookAdmission(t, true) 450 } 451 452 // TestWebhookAdmissionWithoutWatchCache tests communication between API server and webhook process. 453 func TestWebhookAdmissionWithoutWatchCache(t *testing.T) { 454 testWebhookAdmission(t, false) 455 } 456 457 // testWebhookAdmission tests communication between API server and webhook process. 458 func testWebhookAdmission(t *testing.T, watchCache bool) { 459 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.APISelfSubjectReview, true)() 460 461 // holder communicates expectations to webhooks, and results from webhooks 462 holder := &holder{ 463 t: t, 464 warningHandler: &warningHandler{warnings: map[string]bool{}}, 465 gvrToConvertedGVR: map[metav1.GroupVersionResource]metav1.GroupVersionResource{}, 466 gvrToConvertedGVK: map[metav1.GroupVersionResource]schema.GroupVersionKind{}, 467 } 468 469 // set up webhook server 470 roots := x509.NewCertPool() 471 if !roots.AppendCertsFromPEM(localhostCert) { 472 t.Fatal("Failed to append Cert from PEM") 473 } 474 cert, err := tls.X509KeyPair(localhostCert, localhostKey) 475 if err != nil { 476 t.Fatalf("Failed to build cert with error: %+v", err) 477 } 478 479 webhookMux := http.NewServeMux() 480 webhookMux.Handle("/v1beta1/"+mutation, newV1beta1WebhookHandler(t, holder, mutation, false)) 481 webhookMux.Handle("/v1beta1/convert/"+mutation, newV1beta1WebhookHandler(t, holder, mutation, true)) 482 webhookMux.Handle("/v1beta1/"+validation, newV1beta1WebhookHandler(t, holder, validation, false)) 483 webhookMux.Handle("/v1beta1/convert/"+validation, newV1beta1WebhookHandler(t, holder, validation, true)) 484 webhookMux.Handle("/v1/"+mutation, newV1WebhookHandler(t, holder, mutation, false)) 485 webhookMux.Handle("/v1/convert/"+mutation, newV1WebhookHandler(t, holder, mutation, true)) 486 webhookMux.Handle("/v1/"+validation, newV1WebhookHandler(t, holder, validation, false)) 487 webhookMux.Handle("/v1/convert/"+validation, newV1WebhookHandler(t, holder, validation, true)) 488 webhookMux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 489 holder.t.Errorf("unexpected request to %v", req.URL.Path) 490 })) 491 webhookServer := httptest.NewUnstartedServer(webhookMux) 492 webhookServer.TLS = &tls.Config{ 493 RootCAs: roots, 494 Certificates: []tls.Certificate{cert}, 495 } 496 webhookServer.StartTLS() 497 defer webhookServer.Close() 498 499 // start API server 500 etcdConfig := framework.SharedEtcd() 501 server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{ 502 fmt.Sprintf("--watch-cache=%v", watchCache), 503 // turn off admission plugins that add finalizers 504 "--disable-admission-plugins=ServiceAccount,StorageObjectInUseProtection", 505 // force enable all resources so we can check storage. 506 "--runtime-config=api/all=true", 507 // enable feature-gates that protect resources to check their storage, too. 508 // e.g. "--feature-gates=EphemeralContainers=true", 509 }, etcdConfig) 510 defer server.TearDownFn() 511 512 // Configure a client with a distinct user name so that it is easy to distinguish requests 513 // made by the client from requests made by controllers. We use this to filter out requests 514 // before recording them to ensure we don't accidentally mistake requests from controllers 515 // as requests made by the client. 516 clientConfig := rest.CopyConfig(server.ClientConfig) 517 clientConfig.Impersonate.UserName = testClientUsername 518 clientConfig.Impersonate.Groups = []string{"system:masters", "system:authenticated"} 519 clientConfig.WarningHandler = holder.warningHandler 520 client, err := clientset.NewForConfig(clientConfig) 521 if err != nil { 522 t.Fatalf("unexpected error: %v", err) 523 } 524 525 // create CRDs 526 etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...) 527 528 if _, err := client.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}, metav1.CreateOptions{}); err != nil { 529 t.Fatal(err) 530 } 531 532 // gather resources to test 533 dynamicClient, err := dynamic.NewForConfig(clientConfig) 534 if err != nil { 535 t.Fatal(err) 536 } 537 _, resources, err := client.Discovery().ServerGroupsAndResources() 538 if err != nil { 539 t.Fatalf("Failed to get ServerGroupsAndResources with error: %+v", err) 540 } 541 542 gvrsToTest := []schema.GroupVersionResource{} 543 resourcesByGVR := map[schema.GroupVersionResource]metav1.APIResource{} 544 545 for _, list := range resources { 546 defaultGroupVersion, err := schema.ParseGroupVersion(list.GroupVersion) 547 if err != nil { 548 t.Errorf("Failed to get GroupVersion for: %+v", list) 549 continue 550 } 551 for _, resource := range list.APIResources { 552 if resource.Group == "" { 553 resource.Group = defaultGroupVersion.Group 554 } 555 if resource.Version == "" { 556 resource.Version = defaultGroupVersion.Version 557 } 558 gvr := defaultGroupVersion.WithResource(resource.Name) 559 resourcesByGVR[gvr] = resource 560 if shouldTestResource(gvr, resource) { 561 gvrsToTest = append(gvrsToTest, gvr) 562 } 563 } 564 } 565 566 sort.SliceStable(gvrsToTest, func(i, j int) bool { 567 if gvrsToTest[i].Group < gvrsToTest[j].Group { 568 return true 569 } 570 if gvrsToTest[i].Group > gvrsToTest[j].Group { 571 return false 572 } 573 if gvrsToTest[i].Version < gvrsToTest[j].Version { 574 return true 575 } 576 if gvrsToTest[i].Version > gvrsToTest[j].Version { 577 return false 578 } 579 if gvrsToTest[i].Resource < gvrsToTest[j].Resource { 580 return true 581 } 582 if gvrsToTest[i].Resource > gvrsToTest[j].Resource { 583 return false 584 } 585 return true 586 }) 587 588 // map unqualified resource names to the fully qualified resource we will expect to be converted to 589 // Note: this only works because there are no overlapping resource names in-process that are not co-located 590 convertedResources := map[string]schema.GroupVersionResource{} 591 // build the webhook rules enumerating the specific group/version/resources we want 592 convertedV1beta1Rules := []admissionregistrationv1beta1.RuleWithOperations{} 593 convertedV1Rules := []admissionregistrationv1.RuleWithOperations{} 594 for _, gvr := range gvrsToTest { 595 metaGVR := metav1.GroupVersionResource{Group: gvr.Group, Version: gvr.Version, Resource: gvr.Resource} 596 597 convertedGVR, ok := convertedResources[gvr.Resource] 598 if !ok { 599 // this is the first time we've seen this resource 600 // record the fully qualified resource we expect 601 convertedGVR = gvr 602 convertedResources[gvr.Resource] = gvr 603 // add an admission rule indicating we can receive this version 604 convertedV1beta1Rules = append(convertedV1beta1Rules, admissionregistrationv1beta1.RuleWithOperations{ 605 Operations: []admissionregistrationv1beta1.OperationType{admissionregistrationv1beta1.OperationAll}, 606 Rule: admissionregistrationv1beta1.Rule{APIGroups: []string{gvr.Group}, APIVersions: []string{gvr.Version}, Resources: []string{gvr.Resource}}, 607 }) 608 convertedV1Rules = append(convertedV1Rules, admissionregistrationv1.RuleWithOperations{ 609 Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll}, 610 Rule: admissionregistrationv1.Rule{APIGroups: []string{gvr.Group}, APIVersions: []string{gvr.Version}, Resources: []string{gvr.Resource}}, 611 }) 612 } 613 614 // record the expected resource and kind 615 holder.gvrToConvertedGVR[metaGVR] = metav1.GroupVersionResource{Group: convertedGVR.Group, Version: convertedGVR.Version, Resource: convertedGVR.Resource} 616 holder.gvrToConvertedGVK[metaGVR] = schema.GroupVersionKind{Group: resourcesByGVR[convertedGVR].Group, Version: resourcesByGVR[convertedGVR].Version, Kind: resourcesByGVR[convertedGVR].Kind} 617 } 618 619 if err := createV1beta1MutationWebhook(server.EtcdClient, server.EtcdStoragePrefix, client, webhookServer.URL+"/v1beta1/"+mutation, webhookServer.URL+"/v1beta1/convert/"+mutation, convertedV1beta1Rules); err != nil { 620 t.Fatal(err) 621 } 622 if err := createV1beta1ValidationWebhook(server.EtcdClient, server.EtcdStoragePrefix, client, webhookServer.URL+"/v1beta1/"+validation, webhookServer.URL+"/v1beta1/convert/"+validation, convertedV1beta1Rules); err != nil { 623 t.Fatal(err) 624 } 625 if err := createV1MutationWebhook(client, webhookServer.URL+"/v1/"+mutation, webhookServer.URL+"/v1/convert/"+mutation, convertedV1Rules); err != nil { 626 t.Fatal(err) 627 } 628 if err := createV1ValidationWebhook(client, webhookServer.URL+"/v1/"+validation, webhookServer.URL+"/v1/convert/"+validation, convertedV1Rules); err != nil { 629 t.Fatal(err) 630 } 631 632 // Allow the webhook to establish 633 time.Sleep(time.Second) 634 635 start := time.Now() 636 count := 0 637 638 // Test admission on all resources, subresources, and verbs 639 for _, gvr := range gvrsToTest { 640 resource := resourcesByGVR[gvr] 641 t.Run(gvr.Group+"."+gvr.Version+"."+strings.ReplaceAll(resource.Name, "/", "."), func(t *testing.T) { 642 for _, verb := range []string{"create", "update", "patch", "connect", "delete", "deletecollection"} { 643 if shouldTestResourceVerb(gvr, resource, verb) { 644 t.Run(verb, func(t *testing.T) { 645 count++ 646 holder.reset(t) 647 testFunc := getTestFunc(gvr, verb) 648 testFunc(&testContext{ 649 t: t, 650 admissionHolder: holder, 651 client: dynamicClient, 652 clientset: client, 653 verb: verb, 654 gvr: gvr, 655 resource: resource, 656 resources: resourcesByGVR, 657 }) 658 holder.verify(t) 659 }) 660 } 661 } 662 }) 663 } 664 665 duration := time.Since(start) 666 perResourceDuration := time.Duration(int(duration) / count) 667 if perResourceDuration >= 150*time.Millisecond { 668 t.Errorf("expected resources to process in < 150ms, average was %v", perResourceDuration) 669 } 670 } 671 672 // 673 // generic resource testing 674 // 675 676 func testResourceCreate(c *testContext) { 677 stubObj, err := getStubObj(c.gvr, c.resource) 678 if err != nil { 679 c.t.Error(err) 680 return 681 } 682 ns := "" 683 if c.resource.Namespaced { 684 ns = testNamespace 685 } 686 c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkCreateOptions, v1beta1.Create, stubObj.GetName(), ns, true, false, true) 687 _, err = c.client.Resource(c.gvr).Namespace(ns).Create(context.TODO(), stubObj, metav1.CreateOptions{}) 688 if err != nil { 689 c.t.Error(err) 690 return 691 } 692 } 693 694 func testResourceUpdate(c *testContext) { 695 if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { 696 obj, err := createOrGetResource(c.client, c.gvr, c.resource) 697 if err != nil { 698 return err 699 } 700 obj.SetAnnotations(map[string]string{"update": "true"}) 701 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) 702 _, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Update(context.TODO(), obj, metav1.UpdateOptions{}) 703 return err 704 }); err != nil { 705 c.t.Error(err) 706 return 707 } 708 } 709 710 func testResourcePatch(c *testContext) { 711 obj, err := createOrGetResource(c.client, c.gvr, c.resource) 712 if err != nil { 713 c.t.Error(err) 714 return 715 } 716 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) 717 _, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch( 718 context.TODO(), 719 obj.GetName(), 720 types.MergePatchType, 721 []byte(`{"metadata":{"annotations":{"patch":"true"}}}`), 722 metav1.PatchOptions{}) 723 if err != nil { 724 c.t.Error(err) 725 return 726 } 727 } 728 729 func testResourceDelete(c *testContext) { 730 // Verify that an immediate delete triggers the webhook and populates the admisssionRequest.oldObject. 731 obj, err := createOrGetResource(c.client, c.gvr, c.resource) 732 if err != nil { 733 c.t.Error(err) 734 return 735 } 736 background := metav1.DeletePropagationBackground 737 zero := int64(0) 738 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) 739 err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(context.TODO(), obj.GetName(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background}) 740 if err != nil { 741 c.t.Error(err) 742 return 743 } 744 c.admissionHolder.verify(c.t) 745 746 // wait for the item to be gone 747 err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) { 748 obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{}) 749 if apierrors.IsNotFound(err) { 750 return true, nil 751 } 752 if err == nil { 753 c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers()) 754 return false, nil 755 } 756 return false, err 757 }) 758 if err != nil { 759 c.t.Error(err) 760 return 761 } 762 763 // Verify that an update-on-delete triggers the webhook and populates the admisssionRequest.oldObject. 764 obj, err = createOrGetResource(c.client, c.gvr, c.resource) 765 if err != nil { 766 c.t.Error(err) 767 return 768 } 769 // Adding finalizer to the object, then deleting it. 770 // We don't add finalizers by setting DeleteOptions.PropagationPolicy 771 // because some resource (e.g., events) do not support garbage 772 // collector finalizers. 773 _, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch( 774 context.TODO(), 775 obj.GetName(), 776 types.MergePatchType, 777 []byte(`{"metadata":{"finalizers":["test/k8s.io"]}}`), 778 metav1.PatchOptions{}) 779 if err != nil { 780 c.t.Error(err) 781 return 782 } 783 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) 784 err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(context.TODO(), obj.GetName(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background}) 785 if err != nil { 786 c.t.Error(err) 787 return 788 } 789 c.admissionHolder.verify(c.t) 790 791 // wait other finalizers (e.g., crd's customresourcecleanup finalizer) to be removed. 792 err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) { 793 obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{}) 794 if err != nil { 795 return false, err 796 } 797 finalizers := obj.GetFinalizers() 798 if len(finalizers) != 1 { 799 c.t.Logf("waiting for other finalizers on %#v %s to be removed, existing finalizers are %v", c.gvr, obj.GetName(), obj.GetFinalizers()) 800 return false, nil 801 } 802 if finalizers[0] != "test/k8s.io" { 803 return false, fmt.Errorf("expected the single finalizer on %#v %s to be test/k8s.io, got %v", c.gvr, obj.GetName(), obj.GetFinalizers()) 804 } 805 return true, nil 806 }) 807 if err != nil { 808 c.t.Error(err) 809 return 810 } 811 812 // remove the finalizer 813 _, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch( 814 context.TODO(), 815 obj.GetName(), 816 types.MergePatchType, 817 []byte(`{"metadata":{"finalizers":[]}}`), 818 metav1.PatchOptions{}) 819 if err != nil { 820 c.t.Error(err) 821 return 822 } 823 // wait for the item to be gone 824 err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) { 825 obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{}) 826 if apierrors.IsNotFound(err) { 827 return true, nil 828 } 829 if err == nil { 830 c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers()) 831 return false, nil 832 } 833 return false, err 834 }) 835 if err != nil { 836 c.t.Error(err) 837 return 838 } 839 } 840 841 func testResourceDeletecollection(c *testContext) { 842 obj, err := createOrGetResource(c.client, c.gvr, c.resource) 843 if err != nil { 844 c.t.Error(err) 845 return 846 } 847 background := metav1.DeletePropagationBackground 848 zero := int64(0) 849 850 // update the object with a label that matches our selector 851 _, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch( 852 context.TODO(), 853 obj.GetName(), 854 types.MergePatchType, 855 []byte(`{"metadata":{"labels":{"webhooktest":"true"}}}`), 856 metav1.PatchOptions{}) 857 if err != nil { 858 c.t.Error(err) 859 return 860 } 861 862 // set expectations 863 c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkDeleteOptions, v1beta1.Delete, "", obj.GetNamespace(), false, true, true) 864 865 // delete 866 err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).DeleteCollection(context.TODO(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background}, metav1.ListOptions{LabelSelector: "webhooktest=true"}) 867 if err != nil { 868 c.t.Error(err) 869 return 870 } 871 872 // wait for the item to be gone 873 err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) { 874 obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{}) 875 if apierrors.IsNotFound(err) { 876 return true, nil 877 } 878 if err == nil { 879 c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers()) 880 return false, nil 881 } 882 return false, err 883 }) 884 if err != nil { 885 c.t.Error(err) 886 return 887 } 888 } 889 890 func getParentGVR(gvr schema.GroupVersionResource) schema.GroupVersionResource { 891 parentGVR, found := parentResources[gvr] 892 // if no special override is found, just drop the subresource 893 if !found { 894 parentGVR = gvr 895 parentGVR.Resource = strings.Split(parentGVR.Resource, "/")[0] 896 } 897 return parentGVR 898 } 899 900 func testTokenCreate(c *testContext) { 901 saGVR := gvr("", "v1", "serviceaccounts") 902 sa, err := createOrGetResource(c.client, saGVR, c.resources[saGVR]) 903 if err != nil { 904 c.t.Error(err) 905 return 906 } 907 908 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) 909 if err = c.clientset.CoreV1().RESTClient().Post().Namespace(sa.GetNamespace()).Resource("serviceaccounts").Name(sa.GetName()).SubResource("token").Body(&authenticationv1.TokenRequest{ 910 ObjectMeta: metav1.ObjectMeta{Name: sa.GetName()}, 911 Spec: authenticationv1.TokenRequestSpec{ 912 Audiences: []string{"api"}, 913 }, 914 }).Do(context.TODO()).Error(); err != nil { 915 c.t.Error(err) 916 return 917 } 918 c.admissionHolder.verify(c.t) 919 } 920 921 func testSubresourceUpdate(c *testContext) { 922 if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { 923 parentGVR := getParentGVR(c.gvr) 924 parentResource := c.resources[parentGVR] 925 obj, err := createOrGetResource(c.client, parentGVR, parentResource) 926 if err != nil { 927 return err 928 } 929 930 // Save the parent object as what we submit 931 submitObj := obj 932 933 gvrWithoutSubresources := c.gvr 934 gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0] 935 subresources := strings.Split(c.gvr.Resource, "/")[1:] 936 937 // If the subresource supports get, fetch that as the object to submit (namespaces/finalize, */scale, etc) 938 if sets.NewString(c.resource.Verbs...).Has("get") { 939 submitObj, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{}, subresources...) 940 if err != nil { 941 return err 942 } 943 } 944 945 // Modify the object 946 submitObj.SetAnnotations(map[string]string{"subresourceupdate": "true"}) 947 948 // set expectations 949 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) 950 951 _, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Update( 952 context.TODO(), 953 submitObj, 954 metav1.UpdateOptions{}, 955 subresources..., 956 ) 957 return err 958 }); err != nil { 959 c.t.Error(err) 960 } 961 } 962 963 func testSubresourcePatch(c *testContext) { 964 parentGVR := getParentGVR(c.gvr) 965 parentResource := c.resources[parentGVR] 966 obj, err := createOrGetResource(c.client, parentGVR, parentResource) 967 if err != nil { 968 c.t.Error(err) 969 return 970 } 971 972 gvrWithoutSubresources := c.gvr 973 gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0] 974 subresources := strings.Split(c.gvr.Resource, "/")[1:] 975 976 // set expectations 977 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) 978 979 _, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Patch( 980 context.TODO(), 981 obj.GetName(), 982 types.MergePatchType, 983 []byte(`{"metadata":{"annotations":{"subresourcepatch":"true"}}}`), 984 metav1.PatchOptions{}, 985 subresources..., 986 ) 987 if err != nil { 988 c.t.Error(err) 989 return 990 } 991 } 992 993 func unimplemented(c *testContext) { 994 c.t.Errorf("Test function for %+v has not been implemented...", c.gvr) 995 } 996 997 // 998 // custom methods 999 // 1000 1001 // testNamespaceDelete verifies namespace-specific delete behavior: 1002 // - ensures admission is called on first delete (which only sets deletionTimestamp and terminating state) 1003 // - removes finalizer from namespace 1004 // - ensures admission is called on final delete once finalizers are removed 1005 func testNamespaceDelete(c *testContext) { 1006 obj, err := createOrGetResource(c.client, c.gvr, c.resource) 1007 if err != nil { 1008 c.t.Error(err) 1009 return 1010 } 1011 background := metav1.DeletePropagationBackground 1012 zero := int64(0) 1013 1014 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) 1015 err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(context.TODO(), obj.GetName(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background}) 1016 if err != nil { 1017 c.t.Error(err) 1018 return 1019 } 1020 c.admissionHolder.verify(c.t) 1021 1022 // do the finalization so the namespace can be deleted 1023 obj, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{}) 1024 if err != nil { 1025 c.t.Error(err) 1026 return 1027 } 1028 err = unstructured.SetNestedField(obj.Object, nil, "spec", "finalizers") 1029 if err != nil { 1030 c.t.Error(err) 1031 return 1032 } 1033 _, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Update(context.TODO(), obj, metav1.UpdateOptions{}, "finalize") 1034 if err != nil { 1035 c.t.Error(err) 1036 return 1037 } 1038 // verify namespace is gone 1039 obj, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{}) 1040 if err == nil || !apierrors.IsNotFound(err) { 1041 c.t.Errorf("expected namespace to be gone, got %#v, %v", obj, err) 1042 } 1043 } 1044 1045 // testDeploymentRollback verifies rollback-specific behavior: 1046 // - creates a parent deployment 1047 // - creates a rollback object and posts it 1048 func testDeploymentRollback(c *testContext) { 1049 deploymentGVR := gvr("apps", "v1", "deployments") 1050 obj, err := createOrGetResource(c.client, deploymentGVR, c.resources[deploymentGVR]) 1051 if err != nil { 1052 c.t.Error(err) 1053 return 1054 } 1055 1056 gvrWithoutSubresources := c.gvr 1057 gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0] 1058 subresources := strings.Split(c.gvr.Resource, "/")[1:] 1059 1060 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) 1061 1062 var rollbackObj runtime.Object 1063 switch c.gvr { 1064 case gvr("apps", "v1beta1", "deployments/rollback"): 1065 rollbackObj = &appsv1beta1.DeploymentRollback{ 1066 TypeMeta: metav1.TypeMeta{APIVersion: "apps/v1beta1", Kind: "DeploymentRollback"}, 1067 Name: obj.GetName(), 1068 RollbackTo: appsv1beta1.RollbackConfig{Revision: 0}, 1069 } 1070 case gvr("extensions", "v1beta1", "deployments/rollback"): 1071 rollbackObj = &extensionsv1beta1.DeploymentRollback{ 1072 TypeMeta: metav1.TypeMeta{APIVersion: "extensions/v1beta1", Kind: "DeploymentRollback"}, 1073 Name: obj.GetName(), 1074 RollbackTo: extensionsv1beta1.RollbackConfig{Revision: 0}, 1075 } 1076 default: 1077 c.t.Errorf("unknown rollback resource %#v", c.gvr) 1078 return 1079 } 1080 1081 rollbackUnstructuredBody, err := runtime.DefaultUnstructuredConverter.ToUnstructured(rollbackObj) 1082 if err != nil { 1083 c.t.Errorf("ToUnstructured failed: %v", err) 1084 return 1085 } 1086 rollbackUnstructuredObj := &unstructured.Unstructured{Object: rollbackUnstructuredBody} 1087 rollbackUnstructuredObj.SetName(obj.GetName()) 1088 1089 _, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Create(context.TODO(), rollbackUnstructuredObj, metav1.CreateOptions{}, subresources...) 1090 if err != nil { 1091 c.t.Error(err) 1092 return 1093 } 1094 } 1095 1096 // testPodConnectSubresource verifies connect subresources 1097 func testPodConnectSubresource(c *testContext) { 1098 podGVR := gvr("", "v1", "pods") 1099 pod, err := createOrGetResource(c.client, podGVR, c.resources[podGVR]) 1100 if err != nil { 1101 c.t.Error(err) 1102 return 1103 } 1104 1105 // check all upgradeable verbs 1106 for _, httpMethod := range []string{"GET", "POST"} { 1107 c.t.Logf("verifying %v", httpMethod) 1108 1109 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) 1110 var err error 1111 switch c.gvr { 1112 case gvr("", "v1", "pods/exec"): 1113 err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("exec").Do(context.TODO()).Error() 1114 case gvr("", "v1", "pods/attach"): 1115 err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("attach").Do(context.TODO()).Error() 1116 case gvr("", "v1", "pods/portforward"): 1117 err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("portforward").Do(context.TODO()).Error() 1118 default: 1119 c.t.Errorf("unknown subresource %#v", c.gvr) 1120 return 1121 } 1122 1123 if err != nil { 1124 c.t.Logf("debug: result of subresource connect: %v", err) 1125 } 1126 c.admissionHolder.verify(c.t) 1127 1128 } 1129 } 1130 1131 // testPodBindingEviction verifies pod binding and eviction admission 1132 func testPodBindingEviction(c *testContext) { 1133 podGVR := gvr("", "v1", "pods") 1134 pod, err := createOrGetResource(c.client, podGVR, c.resources[podGVR]) 1135 if err != nil { 1136 c.t.Error(err) 1137 return 1138 } 1139 1140 background := metav1.DeletePropagationBackground 1141 zero := int64(0) 1142 forceDelete := metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background} 1143 defer func() { 1144 err := c.clientset.CoreV1().Pods(pod.GetNamespace()).Delete(context.TODO(), pod.GetName(), forceDelete) 1145 if err != nil && !apierrors.IsNotFound(err) { 1146 c.t.Error(err) 1147 return 1148 } 1149 }() 1150 1151 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) 1152 1153 switch c.gvr { 1154 case gvr("", "v1", "bindings"): 1155 err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("bindings").Body(&corev1.Binding{ 1156 ObjectMeta: metav1.ObjectMeta{Name: pod.GetName()}, 1157 Target: corev1.ObjectReference{Name: "foo", Kind: "Node", APIVersion: "v1"}, 1158 }).Do(context.TODO()).Error() 1159 1160 case gvr("", "v1", "pods/binding"): 1161 err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("binding").Body(&corev1.Binding{ 1162 ObjectMeta: metav1.ObjectMeta{Name: pod.GetName()}, 1163 Target: corev1.ObjectReference{Name: "foo", Kind: "Node", APIVersion: "v1"}, 1164 }).Do(context.TODO()).Error() 1165 1166 case gvr("", "v1", "pods/eviction"): 1167 err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("eviction").Body(&policyv1.Eviction{ 1168 ObjectMeta: metav1.ObjectMeta{Name: pod.GetName()}, 1169 DeleteOptions: &forceDelete, 1170 }).Do(context.TODO()).Error() 1171 1172 default: 1173 c.t.Errorf("unhandled resource %#v", c.gvr) 1174 return 1175 } 1176 1177 if err != nil { 1178 c.t.Error(err) 1179 return 1180 } 1181 } 1182 1183 // testSubresourceProxy verifies proxy subresources 1184 func testSubresourceProxy(c *testContext) { 1185 parentGVR := getParentGVR(c.gvr) 1186 parentResource := c.resources[parentGVR] 1187 obj, err := createOrGetResource(c.client, parentGVR, parentResource) 1188 if err != nil { 1189 c.t.Error(err) 1190 return 1191 } 1192 1193 gvrWithoutSubresources := c.gvr 1194 gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0] 1195 subresources := strings.Split(c.gvr.Resource, "/")[1:] 1196 1197 verbToHTTPMethods := map[string][]string{ 1198 "create": {"POST", "GET", "HEAD", "OPTIONS"}, // also test read-only verbs map to Connect admission 1199 "update": {"PUT"}, 1200 "patch": {"PATCH"}, 1201 "delete": {"DELETE"}, 1202 } 1203 httpMethodsToTest, ok := verbToHTTPMethods[c.verb] 1204 if !ok { 1205 c.t.Errorf("unknown verb %v", c.verb) 1206 return 1207 } 1208 1209 for _, httpMethod := range httpMethodsToTest { 1210 c.t.Logf("testing %v", httpMethod) 1211 request := c.clientset.CoreV1().RESTClient().Verb(httpMethod) 1212 1213 // add the namespace if required 1214 if len(obj.GetNamespace()) > 0 { 1215 request = request.Namespace(obj.GetNamespace()) 1216 } 1217 1218 // set expectations 1219 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) 1220 // run the request. we don't actually care if the request is successful, just that admission gets called as expected 1221 err = request.Resource(gvrWithoutSubresources.Resource).Name(obj.GetName()).SubResource(subresources...).Do(context.TODO()).Error() 1222 if err != nil { 1223 c.t.Logf("debug: result of subresource proxy (error expected): %v", err) 1224 } 1225 // verify the result 1226 c.admissionHolder.verify(c.t) 1227 } 1228 } 1229 1230 func testPruningRandomNumbers(c *testContext) { 1231 testResourceCreate(c) 1232 1233 cr2pant, err := c.client.Resource(c.gvr).Get(context.TODO(), "fortytwo", metav1.GetOptions{}) 1234 if err != nil { 1235 c.t.Error(err) 1236 return 1237 } 1238 1239 foo, found, err := unstructured.NestedString(cr2pant.Object, "foo") 1240 if err != nil { 1241 c.t.Error(err) 1242 return 1243 } 1244 if found { 1245 c.t.Errorf("expected .foo to be pruned, but got: %s", foo) 1246 } 1247 } 1248 1249 func testNoPruningCustomFancy(c *testContext) { 1250 testResourceCreate(c) 1251 1252 cr2pant, err := c.client.Resource(c.gvr).Get(context.TODO(), "cr2pant", metav1.GetOptions{}) 1253 if err != nil { 1254 c.t.Error(err) 1255 return 1256 } 1257 1258 foo, _, err := unstructured.NestedString(cr2pant.Object, "foo") 1259 if err != nil { 1260 c.t.Error(err) 1261 return 1262 } 1263 1264 // check that no pruning took place 1265 if expected, got := "test", foo; expected != got { 1266 c.t.Errorf("expected /foo to be %q, got: %q", expected, got) 1267 } 1268 } 1269 1270 // 1271 // utility methods 1272 // 1273 1274 func newV1beta1WebhookHandler(t *testing.T, holder *holder, phase string, converted bool) http.Handler { 1275 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1276 defer r.Body.Close() 1277 data, err := io.ReadAll(r.Body) 1278 if err != nil { 1279 t.Error(err) 1280 return 1281 } 1282 1283 if contentType := r.Header.Get("Content-Type"); contentType != "application/json" { 1284 t.Errorf("contentType=%s, expect application/json", contentType) 1285 return 1286 } 1287 1288 review := v1beta1.AdmissionReview{} 1289 if err := json.Unmarshal(data, &review); err != nil { 1290 t.Errorf("Fail to deserialize object: %s with error: %v", string(data), err) 1291 http.Error(w, err.Error(), 400) 1292 return 1293 } 1294 1295 if review.GetObjectKind().GroupVersionKind() != gvk("admission.k8s.io", "v1beta1", "AdmissionReview") { 1296 t.Errorf("Invalid admission review kind: %#v", review.GetObjectKind().GroupVersionKind()) 1297 http.Error(w, err.Error(), 400) 1298 return 1299 } 1300 1301 if len(review.Request.Object.Raw) > 0 { 1302 u := &unstructured.Unstructured{Object: map[string]interface{}{}} 1303 if err := json.Unmarshal(review.Request.Object.Raw, u); err != nil { 1304 t.Errorf("Fail to deserialize object: %s with error: %v", string(review.Request.Object.Raw), err) 1305 http.Error(w, err.Error(), 400) 1306 return 1307 } 1308 review.Request.Object.Object = u 1309 } 1310 if len(review.Request.OldObject.Raw) > 0 { 1311 u := &unstructured.Unstructured{Object: map[string]interface{}{}} 1312 if err := json.Unmarshal(review.Request.OldObject.Raw, u); err != nil { 1313 t.Errorf("Fail to deserialize object: %s with error: %v", string(review.Request.OldObject.Raw), err) 1314 http.Error(w, err.Error(), 400) 1315 return 1316 } 1317 review.Request.OldObject.Object = u 1318 } 1319 1320 if len(review.Request.Options.Raw) > 0 { 1321 u := &unstructured.Unstructured{Object: map[string]interface{}{}} 1322 if err := json.Unmarshal(review.Request.Options.Raw, u); err != nil { 1323 t.Errorf("Fail to deserialize options object: %s for admission request %#+v with error: %v", string(review.Request.Options.Raw), review.Request, err) 1324 http.Error(w, err.Error(), 400) 1325 return 1326 } 1327 review.Request.Options.Object = u 1328 } 1329 1330 if review.Request.UserInfo.Username == testClientUsername { 1331 // only record requests originating from this integration test's client 1332 reviewRequest := &admissionRequest{ 1333 Operation: string(review.Request.Operation), 1334 Resource: review.Request.Resource, 1335 SubResource: review.Request.SubResource, 1336 Namespace: review.Request.Namespace, 1337 Name: review.Request.Name, 1338 Object: review.Request.Object, 1339 OldObject: review.Request.OldObject, 1340 Options: review.Request.Options, 1341 } 1342 holder.record("v1beta1", phase, converted, reviewRequest) 1343 } 1344 1345 review.Response = &v1beta1.AdmissionResponse{ 1346 Allowed: true, 1347 Result: &metav1.Status{Message: "admitted"}, 1348 } 1349 1350 // v1beta1 webhook handler tolerated these not being set. verify the server continues to accept these as unset. 1351 review.APIVersion = "" 1352 review.Kind = "" 1353 review.Response.UID = "" 1354 1355 // test plumbing warnings back to the client 1356 review.Response.Warnings = []string{makeWarning("v1beta1", phase, converted)} 1357 1358 // If we're mutating, and have an object, return a patch to exercise conversion 1359 if phase == mutation && len(review.Request.Object.Raw) > 0 { 1360 review.Response.Patch = []byte(`[{"op":"add","path":"/foo","value":"test"}]`) 1361 jsonPatch := v1beta1.PatchTypeJSONPatch 1362 review.Response.PatchType = &jsonPatch 1363 } 1364 1365 w.Header().Set("Content-Type", "application/json") 1366 if err := json.NewEncoder(w).Encode(review); err != nil { 1367 t.Errorf("Marshal of response failed with error: %v", err) 1368 } 1369 }) 1370 } 1371 1372 func newV1WebhookHandler(t *testing.T, holder *holder, phase string, converted bool) http.Handler { 1373 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1374 defer r.Body.Close() 1375 data, err := io.ReadAll(r.Body) 1376 if err != nil { 1377 t.Error(err) 1378 return 1379 } 1380 1381 if contentType := r.Header.Get("Content-Type"); contentType != "application/json" { 1382 t.Errorf("contentType=%s, expect application/json", contentType) 1383 return 1384 } 1385 1386 review := admissionreviewv1.AdmissionReview{} 1387 if err := json.Unmarshal(data, &review); err != nil { 1388 t.Errorf("Fail to deserialize object: %s with error: %v", string(data), err) 1389 http.Error(w, err.Error(), 400) 1390 return 1391 } 1392 1393 if review.GetObjectKind().GroupVersionKind() != gvk("admission.k8s.io", "v1", "AdmissionReview") { 1394 err := fmt.Errorf("Invalid admission review kind: %#v", review.GetObjectKind().GroupVersionKind()) 1395 t.Error(err) 1396 http.Error(w, err.Error(), 400) 1397 return 1398 } 1399 1400 if len(review.Request.Object.Raw) > 0 { 1401 u := &unstructured.Unstructured{Object: map[string]interface{}{}} 1402 if err := json.Unmarshal(review.Request.Object.Raw, u); err != nil { 1403 t.Errorf("Fail to deserialize object: %s with error: %v", string(review.Request.Object.Raw), err) 1404 http.Error(w, err.Error(), 400) 1405 return 1406 } 1407 review.Request.Object.Object = u 1408 } 1409 if len(review.Request.OldObject.Raw) > 0 { 1410 u := &unstructured.Unstructured{Object: map[string]interface{}{}} 1411 if err := json.Unmarshal(review.Request.OldObject.Raw, u); err != nil { 1412 t.Errorf("Fail to deserialize object: %s with error: %v", string(review.Request.OldObject.Raw), err) 1413 http.Error(w, err.Error(), 400) 1414 return 1415 } 1416 review.Request.OldObject.Object = u 1417 } 1418 1419 if len(review.Request.Options.Raw) > 0 { 1420 u := &unstructured.Unstructured{Object: map[string]interface{}{}} 1421 if err := json.Unmarshal(review.Request.Options.Raw, u); err != nil { 1422 t.Errorf("Fail to deserialize options object: %s for admission request %#+v with error: %v", string(review.Request.Options.Raw), review.Request, err) 1423 http.Error(w, err.Error(), 400) 1424 return 1425 } 1426 review.Request.Options.Object = u 1427 } 1428 1429 if review.Request.UserInfo.Username == testClientUsername { 1430 // only record requests originating from this integration test's client 1431 reviewRequest := &admissionRequest{ 1432 Operation: string(review.Request.Operation), 1433 Resource: review.Request.Resource, 1434 SubResource: review.Request.SubResource, 1435 Namespace: review.Request.Namespace, 1436 Name: review.Request.Name, 1437 Object: review.Request.Object, 1438 OldObject: review.Request.OldObject, 1439 Options: review.Request.Options, 1440 } 1441 holder.record("v1", phase, converted, reviewRequest) 1442 } 1443 1444 review.Response = &admissionreviewv1.AdmissionResponse{ 1445 Allowed: true, 1446 UID: review.Request.UID, 1447 Result: &metav1.Status{Message: "admitted"}, 1448 1449 // test plumbing warnings back 1450 Warnings: []string{makeWarning("v1", phase, converted)}, 1451 } 1452 // If we're mutating, and have an object, return a patch to exercise conversion 1453 if phase == mutation && len(review.Request.Object.Raw) > 0 { 1454 review.Response.Patch = []byte(`[{"op":"add","path":"/bar","value":"test"}]`) 1455 jsonPatch := admissionreviewv1.PatchTypeJSONPatch 1456 review.Response.PatchType = &jsonPatch 1457 } 1458 1459 w.Header().Set("Content-Type", "application/json") 1460 if err := json.NewEncoder(w).Encode(review); err != nil { 1461 t.Errorf("Marshal of response failed with error: %v", err) 1462 } 1463 }) 1464 } 1465 1466 func getTestFunc(gvr schema.GroupVersionResource, verb string) testFunc { 1467 if f, found := customTestFuncs[gvr][verb]; found { 1468 return f 1469 } 1470 if f, found := customTestFuncs[gvr]["*"]; found { 1471 return f 1472 } 1473 if strings.Contains(gvr.Resource, "/") { 1474 if f, found := defaultSubresourceFuncs[verb]; found { 1475 return f 1476 } 1477 return unimplemented 1478 } 1479 if f, found := defaultResourceFuncs[verb]; found { 1480 return f 1481 } 1482 return unimplemented 1483 } 1484 1485 func getStubObj(gvr schema.GroupVersionResource, resource metav1.APIResource) (*unstructured.Unstructured, error) { 1486 stub := "" 1487 if data, ok := etcd.GetEtcdStorageDataForNamespace(testNamespace)[gvr]; ok { 1488 stub = data.Stub 1489 } 1490 if data, ok := stubDataOverrides[gvr]; ok { 1491 stub = data 1492 } 1493 if len(stub) == 0 { 1494 return nil, fmt.Errorf("no stub data for %#v", gvr) 1495 } 1496 1497 stubObj := &unstructured.Unstructured{Object: map[string]interface{}{}} 1498 if err := json.Unmarshal([]byte(stub), &stubObj.Object); err != nil { 1499 return nil, fmt.Errorf("error unmarshaling stub for %#v: %v", gvr, err) 1500 } 1501 return stubObj, nil 1502 } 1503 1504 func createOrGetResource(client dynamic.Interface, gvr schema.GroupVersionResource, resource metav1.APIResource) (*unstructured.Unstructured, error) { 1505 stubObj, err := getStubObj(gvr, resource) 1506 if err != nil { 1507 return nil, err 1508 } 1509 ns := "" 1510 if resource.Namespaced { 1511 ns = testNamespace 1512 } 1513 obj, err := client.Resource(gvr).Namespace(ns).Get(context.TODO(), stubObj.GetName(), metav1.GetOptions{}) 1514 if err == nil { 1515 return obj, nil 1516 } 1517 if !apierrors.IsNotFound(err) { 1518 return nil, err 1519 } 1520 return client.Resource(gvr).Namespace(ns).Create(context.TODO(), stubObj, metav1.CreateOptions{}) 1521 } 1522 1523 func gvr(group, version, resource string) schema.GroupVersionResource { 1524 return schema.GroupVersionResource{Group: group, Version: version, Resource: resource} 1525 } 1526 func gvk(group, version, kind string) schema.GroupVersionKind { 1527 return schema.GroupVersionKind{Group: group, Version: version, Kind: kind} 1528 } 1529 1530 var ( 1531 gvkCreateOptions = metav1.SchemeGroupVersion.WithKind("CreateOptions") 1532 gvkUpdateOptions = metav1.SchemeGroupVersion.WithKind("UpdateOptions") 1533 gvkDeleteOptions = metav1.SchemeGroupVersion.WithKind("DeleteOptions") 1534 ) 1535 1536 func shouldTestResource(gvr schema.GroupVersionResource, resource metav1.APIResource) bool { 1537 return sets.NewString(resource.Verbs...).HasAny("create", "update", "patch", "connect", "delete", "deletecollection") 1538 } 1539 1540 func shouldTestResourceVerb(gvr schema.GroupVersionResource, resource metav1.APIResource, verb string) bool { 1541 return sets.NewString(resource.Verbs...).Has(verb) 1542 } 1543 1544 // 1545 // webhook registration helpers 1546 // 1547 1548 func createV1beta1ValidationWebhook(etcdClient *clientv3.Client, etcdStoragePrefix string, client clientset.Interface, endpoint, convertedEndpoint string, convertedRules []admissionregistrationv1beta1.RuleWithOperations) error { 1549 fail := admissionregistrationv1beta1.Fail 1550 equivalent := admissionregistrationv1beta1.Equivalent 1551 webhookConfig := &admissionregistrationv1beta1.ValidatingWebhookConfiguration{ 1552 ObjectMeta: metav1.ObjectMeta{Name: "admission.integration.test"}, 1553 Webhooks: []admissionregistrationv1beta1.ValidatingWebhook{ 1554 { 1555 Name: "admission.integration.test", 1556 ClientConfig: admissionregistrationv1beta1.WebhookClientConfig{ 1557 URL: &endpoint, 1558 CABundle: localhostCert, 1559 }, 1560 Rules: []admissionregistrationv1beta1.RuleWithOperations{{ 1561 Operations: []admissionregistrationv1beta1.OperationType{admissionregistrationv1beta1.OperationAll}, 1562 Rule: admissionregistrationv1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}}, 1563 }}, 1564 FailurePolicy: &fail, 1565 AdmissionReviewVersions: []string{"v1beta1"}, 1566 }, 1567 { 1568 Name: "admission.integration.testconversion", 1569 ClientConfig: admissionregistrationv1beta1.WebhookClientConfig{ 1570 URL: &convertedEndpoint, 1571 CABundle: localhostCert, 1572 }, 1573 Rules: convertedRules, 1574 FailurePolicy: &fail, 1575 MatchPolicy: &equivalent, 1576 AdmissionReviewVersions: []string{"v1beta1"}, 1577 }, 1578 }, 1579 } 1580 // run through to get defaulting 1581 apisv1beta1.SetObjectDefaults_ValidatingWebhookConfiguration(webhookConfig) 1582 webhookConfig.TypeMeta.Kind = "ValidatingWebhookConfiguration" 1583 webhookConfig.TypeMeta.APIVersion = "admissionregistration.k8s.io/v1beta1" 1584 1585 // Attaching Mutation webhook to API server 1586 ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceNone) 1587 key := path.Join("/", etcdStoragePrefix, "validatingwebhookconfigurations", webhookConfig.Name) 1588 val, _ := json.Marshal(webhookConfig) 1589 if _, err := etcdClient.Put(ctx, key, string(val)); err != nil { 1590 return err 1591 } 1592 1593 // make sure we can get the webhook 1594 if _, err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Get(context.TODO(), webhookConfig.Name, metav1.GetOptions{}); err != nil { 1595 return err 1596 } 1597 1598 return nil 1599 } 1600 1601 func createV1beta1MutationWebhook(etcdClient *clientv3.Client, etcdStoragePrefix string, client clientset.Interface, endpoint, convertedEndpoint string, convertedRules []admissionregistrationv1beta1.RuleWithOperations) error { 1602 fail := admissionregistrationv1beta1.Fail 1603 equivalent := admissionregistrationv1beta1.Equivalent 1604 webhookConfig := &admissionregistrationv1beta1.MutatingWebhookConfiguration{ 1605 ObjectMeta: metav1.ObjectMeta{Name: "mutation.integration.test"}, 1606 Webhooks: []admissionregistrationv1beta1.MutatingWebhook{ 1607 { 1608 Name: "mutation.integration.test", 1609 ClientConfig: admissionregistrationv1beta1.WebhookClientConfig{ 1610 URL: &endpoint, 1611 CABundle: localhostCert, 1612 }, 1613 Rules: []admissionregistrationv1beta1.RuleWithOperations{{ 1614 Operations: []admissionregistrationv1beta1.OperationType{admissionregistrationv1beta1.OperationAll}, 1615 Rule: admissionregistrationv1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}}, 1616 }}, 1617 FailurePolicy: &fail, 1618 AdmissionReviewVersions: []string{"v1beta1"}, 1619 }, 1620 { 1621 Name: "mutation.integration.testconversion", 1622 ClientConfig: admissionregistrationv1beta1.WebhookClientConfig{ 1623 URL: &convertedEndpoint, 1624 CABundle: localhostCert, 1625 }, 1626 Rules: convertedRules, 1627 FailurePolicy: &fail, 1628 MatchPolicy: &equivalent, 1629 AdmissionReviewVersions: []string{"v1beta1"}, 1630 }, 1631 }, 1632 } 1633 // run through to get defaulting 1634 apisv1beta1.SetObjectDefaults_MutatingWebhookConfiguration(webhookConfig) 1635 webhookConfig.TypeMeta.Kind = "MutatingWebhookConfiguration" 1636 webhookConfig.TypeMeta.APIVersion = "admissionregistration.k8s.io/v1beta1" 1637 1638 // Attaching Mutation webhook to API server 1639 ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceNone) 1640 key := path.Join("/", etcdStoragePrefix, "mutatingwebhookconfigurations", webhookConfig.Name) 1641 val, _ := json.Marshal(webhookConfig) 1642 if _, err := etcdClient.Put(ctx, key, string(val)); err != nil { 1643 return err 1644 } 1645 1646 // make sure we can get the webhook 1647 if _, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Get(context.TODO(), webhookConfig.Name, metav1.GetOptions{}); err != nil { 1648 return err 1649 } 1650 1651 return nil 1652 } 1653 1654 func createV1ValidationWebhook(client clientset.Interface, endpoint, convertedEndpoint string, convertedRules []admissionregistrationv1.RuleWithOperations) error { 1655 fail := admissionregistrationv1.Fail 1656 equivalent := admissionregistrationv1.Equivalent 1657 none := admissionregistrationv1.SideEffectClassNone 1658 // Attaching Admission webhook to API server 1659 _, err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(context.TODO(), &admissionregistrationv1.ValidatingWebhookConfiguration{ 1660 ObjectMeta: metav1.ObjectMeta{Name: "admissionregistrationv1.integration.test"}, 1661 Webhooks: []admissionregistrationv1.ValidatingWebhook{ 1662 { 1663 Name: "admissionregistrationv1.integration.test", 1664 ClientConfig: admissionregistrationv1.WebhookClientConfig{ 1665 URL: &endpoint, 1666 CABundle: localhostCert, 1667 }, 1668 Rules: []admissionregistrationv1.RuleWithOperations{{ 1669 Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll}, 1670 Rule: admissionregistrationv1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}}, 1671 }}, 1672 FailurePolicy: &fail, 1673 AdmissionReviewVersions: []string{"v1", "v1beta1"}, 1674 SideEffects: &none, 1675 }, 1676 { 1677 Name: "admissionregistrationv1.integration.testconversion", 1678 ClientConfig: admissionregistrationv1.WebhookClientConfig{ 1679 URL: &convertedEndpoint, 1680 CABundle: localhostCert, 1681 }, 1682 Rules: convertedRules, 1683 FailurePolicy: &fail, 1684 MatchPolicy: &equivalent, 1685 AdmissionReviewVersions: []string{"v1", "v1beta1"}, 1686 SideEffects: &none, 1687 }, 1688 }, 1689 }, metav1.CreateOptions{}) 1690 return err 1691 } 1692 1693 func createV1MutationWebhook(client clientset.Interface, endpoint, convertedEndpoint string, convertedRules []admissionregistrationv1.RuleWithOperations) error { 1694 fail := admissionregistrationv1.Fail 1695 equivalent := admissionregistrationv1.Equivalent 1696 none := admissionregistrationv1.SideEffectClassNone 1697 // Attaching Mutation webhook to API server 1698 _, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), &admissionregistrationv1.MutatingWebhookConfiguration{ 1699 ObjectMeta: metav1.ObjectMeta{Name: "mutationv1.integration.test"}, 1700 Webhooks: []admissionregistrationv1.MutatingWebhook{ 1701 { 1702 Name: "mutationv1.integration.test", 1703 ClientConfig: admissionregistrationv1.WebhookClientConfig{ 1704 URL: &endpoint, 1705 CABundle: localhostCert, 1706 }, 1707 Rules: []admissionregistrationv1.RuleWithOperations{{ 1708 Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll}, 1709 Rule: admissionregistrationv1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}}, 1710 }}, 1711 FailurePolicy: &fail, 1712 AdmissionReviewVersions: []string{"v1", "v1beta1"}, 1713 SideEffects: &none, 1714 }, 1715 { 1716 Name: "mutationv1.integration.testconversion", 1717 ClientConfig: admissionregistrationv1.WebhookClientConfig{ 1718 URL: &convertedEndpoint, 1719 CABundle: localhostCert, 1720 }, 1721 Rules: convertedRules, 1722 FailurePolicy: &fail, 1723 MatchPolicy: &equivalent, 1724 AdmissionReviewVersions: []string{"v1", "v1beta1"}, 1725 SideEffects: &none, 1726 }, 1727 }, 1728 }, metav1.CreateOptions{}) 1729 return err 1730 } 1731 1732 // localhostCert was generated from crypto/tls/generate_cert.go with the following command: 1733 // 1734 // go run generate_cert.go --rsa-bits 2048 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h 1735 var localhostCert = []byte(`-----BEGIN CERTIFICATE----- 1736 MIIDGDCCAgCgAwIBAgIQTKCKn99d5HhQVCLln2Q+eTANBgkqhkiG9w0BAQsFADAS 1737 MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw 1738 MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 1739 MIIBCgKCAQEA1Z5/aTwqY706M34tn60l8ZHkanWDl8mM1pYf4Q7qg3zA9XqWLX6S 1740 4rTYDYCb4stEasC72lQnbEWHbthiQE76zubP8WOFHdvGR3mjAvHWz4FxvLOTheZ+ 1741 3iDUrl6Aj9UIsYqzmpBJAoY4+vGGf+xHvuukHrVcFqR9ZuBdZuJ/HbbjUyuNr3X9 1742 erNIr5Ha17gVzf17SNbYgNrX9gbCeEB8Z9Ox7dVuJhLDkpF0T/B5Zld3BjyUVY/T 1743 cukU4dTVp6isbWPvCMRCZCCOpb+qIhxEjJ0n6tnPt8nf9lvDl4SWMl6X1bH+2EFa 1744 a8R06G0QI+XhwPyjXUyCR8QEOZPCR5wyqQIDAQABo2gwZjAOBgNVHQ8BAf8EBAMC 1745 AqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAuBgNVHREE 1746 JzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG 1747 9w0BAQsFAAOCAQEAThqgJ/AFqaANsOp48lojDZfZBFxJQ3A4zfR/MgggUoQ9cP3V 1748 rxuKAFWQjze1EZc7J9iO1WvH98lOGVNRY/t2VIrVoSsBiALP86Eew9WucP60tbv2 1749 8/zsBDSfEo9Wl+Q/gwdEh8dgciUKROvCm76EgAwPGicMAgRsxXgwXHhS5e8nnbIE 1750 Ewaqvb5dY++6kh0Oz+adtNT5OqOwXTIRI67WuEe6/B3Z4LNVPQDIj7ZUJGNw8e6L 1751 F4nkUthwlKx4yEJHZBRuFPnO7Z81jNKuwL276+mczRH7piI6z9uyMV/JbEsOIxyL 1752 W6CzB7pZ9Nj1YLpgzc1r6oONHLokMJJIz/IvkQ== 1753 -----END CERTIFICATE-----`) 1754 1755 // localhostKey is the private key for localhostCert. 1756 var localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY----- 1757 MIIEowIBAAKCAQEA1Z5/aTwqY706M34tn60l8ZHkanWDl8mM1pYf4Q7qg3zA9XqW 1758 LX6S4rTYDYCb4stEasC72lQnbEWHbthiQE76zubP8WOFHdvGR3mjAvHWz4FxvLOT 1759 heZ+3iDUrl6Aj9UIsYqzmpBJAoY4+vGGf+xHvuukHrVcFqR9ZuBdZuJ/HbbjUyuN 1760 r3X9erNIr5Ha17gVzf17SNbYgNrX9gbCeEB8Z9Ox7dVuJhLDkpF0T/B5Zld3BjyU 1761 VY/TcukU4dTVp6isbWPvCMRCZCCOpb+qIhxEjJ0n6tnPt8nf9lvDl4SWMl6X1bH+ 1762 2EFaa8R06G0QI+XhwPyjXUyCR8QEOZPCR5wyqQIDAQABAoIBAFAJmb1pMIy8OpFO 1763 hnOcYWoYepe0vgBiIOXJy9n8R7vKQ1X2f0w+b3SHw6eTd1TLSjAhVIEiJL85cdwD 1764 MRTdQrXA30qXOioMzUa8eWpCCHUpD99e/TgfO4uoi2dluw+pBx/WUyLnSqOqfLDx 1765 S66kbeFH0u86jm1hZibki7pfxLbxvu7KQgPe0meO5/13Retztz7/xa/pWIY71Zqd 1766 YC8UckuQdWUTxfuQf0470lAK34GZlDy9tvdVOG/PmNkG4j6OQjy0Kmz4Uk7rewKo 1767 ZbdphaLPJ2A4Rdqfn4WCoyDnxlfV861T922/dEDZEbNWiQpB81G8OfLL+FLHxyIT 1768 LKEu4R0CgYEA4RDj9jatJ/wGkMZBt+UF05mcJlRVMEijqdKgFwR2PP8b924Ka1mj 1769 9zqWsfbxQbdPdwsCeVBZrSlTEmuFSQLeWtqBxBKBTps/tUP0qZf7HjfSmcVI89WE 1770 3ab8LFjfh4PtK/LOq2D1GRZZkFliqi0gKwYdDoK6gxXWwrumXq4c2l8CgYEA8vrX 1771 dMuGCNDjNQkGXx3sr8pyHCDrSNR4Z4FrSlVUkgAW1L7FrCM911BuGh86FcOu9O/1 1772 Ggo0E8ge7qhQiXhB5vOo7hiVzSp0FxxCtGSlpdp4W6wx6ZWK8+Pc+6Moos03XdG7 1773 MKsdPGDciUn9VMOP3r8huX/btFTh90C/L50sH/cCgYAd02wyW8qUqux/0RYydZJR 1774 GWE9Hx3u+SFfRv9aLYgxyyj8oEOXOFjnUYdY7D3KlK1ePEJGq2RG81wD6+XM6Clp 1775 Zt2di0pBjYdi0S+iLfbkaUdqg1+ImLoz2YY/pkNxJQWQNmw2//FbMsAJxh6yKKrD 1776 qNq+6oonBwTf55hDodVHBwKBgEHgEBnyM9ygBXmTgM645jqiwF0v75pHQH2PcO8u 1777 Q0dyDr6PGjiZNWLyw2cBoFXWP9DYXbM5oPTcBMbfizY6DGP5G4uxzqtZHzBE0TDn 1778 OKHGoWr5PG7/xDRrSrZOfe3lhWVCP2XqfnqoKCJwlOYuPws89n+8UmyJttm6DBt0 1779 mUnxAoGBAIvbR87ZFXkvqstLs4KrdqTz4TQIcpzB3wENukHODPA6C1gzWTqp+OEe 1780 GMNltPfGCLO+YmoMQuTpb0kECYV3k4jR3gXO6YvlL9KbY+UOA6P0dDX4ROi2Rklj 1781 yh+lxFLYa1vlzzi9r8B7nkR9hrOGMvkfXF42X89g7lx4uMtu2I4q 1782 -----END RSA PRIVATE KEY-----`)