k8s.io/kubernetes@v1.29.3/test/integration/controlplane/audit/audit_test.go (about) 1 /* 2 Copyright 2018 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 audit 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "net/http" 24 "os" 25 "strings" 26 "testing" 27 "time" 28 29 "k8s.io/api/admission/v1beta1" 30 admissionregistrationv1 "k8s.io/api/admissionregistration/v1" 31 appsv1 "k8s.io/api/apps/v1" 32 authenticationv1 "k8s.io/api/authentication/v1" 33 autoscalingv1 "k8s.io/api/autoscaling/v1" 34 apiv1 "k8s.io/api/core/v1" 35 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 36 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 37 "k8s.io/apimachinery/pkg/runtime/schema" 38 "k8s.io/apimachinery/pkg/types" 39 "k8s.io/apimachinery/pkg/util/wait" 40 "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating" 41 auditinternal "k8s.io/apiserver/pkg/apis/audit" 42 auditv1 "k8s.io/apiserver/pkg/apis/audit/v1" 43 clientset "k8s.io/client-go/kubernetes" 44 utiltesting "k8s.io/client-go/util/testing" 45 kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" 46 "k8s.io/kubernetes/test/integration/framework" 47 "k8s.io/kubernetes/test/utils" 48 49 jsonpatch "github.com/evanphx/json-patch" 50 ) 51 52 const ( 53 testWebhookConfigurationName = "auditmutation.integration.test" 54 testWebhookName = "auditmutation.integration.test" 55 ) 56 57 var ( 58 auditPolicyPattern = ` 59 apiVersion: {version} 60 kind: Policy 61 rules: 62 - level: RequestResponse 63 namespaces: ["no-webhook-namespace"] 64 resources: 65 - group: "" # core 66 resources: ["configmaps"] 67 - level: Metadata 68 namespaces: ["webhook-audit-metadata"] 69 resources: 70 - group: "" # core 71 resources: ["configmaps"] 72 - level: Request 73 namespaces: ["webhook-audit-request"] 74 resources: 75 - group: "" # core 76 resources: ["configmaps"] 77 - level: RequestResponse 78 namespaces: ["webhook-audit-response"] 79 resources: 80 - group: "" # core 81 resources: ["configmaps"] 82 - level: Request 83 namespaces: ["create-audit-request"] 84 resources: 85 - group: "" # core 86 resources: ["serviceaccounts/token"] 87 - level: RequestResponse 88 namespaces: ["create-audit-response"] 89 resources: 90 - group: "" # core 91 resources: ["serviceaccounts/token"] 92 - level: Request 93 namespaces: ["update-audit-request"] 94 resources: 95 - group: "apps" 96 resources: ["deployments/scale"] 97 - level: RequestResponse 98 namespaces: ["update-audit-response"] 99 resources: 100 - group: "apps" 101 resources: ["deployments/scale"] 102 103 ` 104 nonAdmissionWebhookNamespace = "no-webhook-namespace" 105 watchTestTimeout int64 = 1 106 watchOptions = metav1.ListOptions{TimeoutSeconds: &watchTestTimeout} 107 patch, _ = json.Marshal(jsonpatch.Patch{}) 108 auditTestUser = "system:apiserver" 109 versions = map[string]schema.GroupVersion{ 110 "audit.k8s.io/v1": auditv1.SchemeGroupVersion, 111 } 112 113 expectedEvents = []utils.AuditEvent{ 114 { 115 Level: auditinternal.LevelRequestResponse, 116 Stage: auditinternal.StageResponseComplete, 117 RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps", nonAdmissionWebhookNamespace), 118 Verb: "create", 119 Code: 201, 120 User: auditTestUser, 121 Resource: "configmaps", 122 Namespace: nonAdmissionWebhookNamespace, 123 RequestObject: true, 124 ResponseObject: true, 125 AuthorizeDecision: "allow", 126 }, { 127 Level: auditinternal.LevelRequestResponse, 128 Stage: auditinternal.StageResponseComplete, 129 RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps/audit-configmap", nonAdmissionWebhookNamespace), 130 Verb: "get", 131 Code: 200, 132 User: auditTestUser, 133 Resource: "configmaps", 134 Namespace: nonAdmissionWebhookNamespace, 135 RequestObject: false, 136 ResponseObject: true, 137 AuthorizeDecision: "allow", 138 }, { 139 Level: auditinternal.LevelRequestResponse, 140 Stage: auditinternal.StageResponseComplete, 141 RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps", nonAdmissionWebhookNamespace), 142 Verb: "list", 143 Code: 200, 144 User: auditTestUser, 145 Resource: "configmaps", 146 Namespace: nonAdmissionWebhookNamespace, 147 RequestObject: false, 148 ResponseObject: true, 149 AuthorizeDecision: "allow", 150 }, { 151 Level: auditinternal.LevelRequestResponse, 152 Stage: auditinternal.StageResponseStarted, 153 RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps?timeout=%ds&timeoutSeconds=%d&watch=true", nonAdmissionWebhookNamespace, watchTestTimeout, watchTestTimeout), 154 Verb: "watch", 155 Code: 200, 156 User: auditTestUser, 157 Resource: "configmaps", 158 Namespace: nonAdmissionWebhookNamespace, 159 RequestObject: false, 160 ResponseObject: false, 161 AuthorizeDecision: "allow", 162 }, { 163 Level: auditinternal.LevelRequestResponse, 164 Stage: auditinternal.StageResponseComplete, 165 RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps?timeout=%ds&timeoutSeconds=%d&watch=true", nonAdmissionWebhookNamespace, watchTestTimeout, watchTestTimeout), 166 Verb: "watch", 167 Code: 200, 168 User: auditTestUser, 169 Resource: "configmaps", 170 Namespace: nonAdmissionWebhookNamespace, 171 RequestObject: false, 172 ResponseObject: false, 173 AuthorizeDecision: "allow", 174 }, { 175 Level: auditinternal.LevelRequestResponse, 176 Stage: auditinternal.StageResponseComplete, 177 RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps/audit-configmap", nonAdmissionWebhookNamespace), 178 Verb: "update", 179 Code: 200, 180 User: auditTestUser, 181 Resource: "configmaps", 182 Namespace: nonAdmissionWebhookNamespace, 183 RequestObject: true, 184 ResponseObject: true, 185 AuthorizeDecision: "allow", 186 }, { 187 Level: auditinternal.LevelRequestResponse, 188 Stage: auditinternal.StageResponseComplete, 189 RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps/audit-configmap", nonAdmissionWebhookNamespace), 190 Verb: "patch", 191 Code: 200, 192 User: auditTestUser, 193 Resource: "configmaps", 194 Namespace: nonAdmissionWebhookNamespace, 195 RequestObject: true, 196 ResponseObject: true, 197 AuthorizeDecision: "allow", 198 }, { 199 Level: auditinternal.LevelRequestResponse, 200 Stage: auditinternal.StageResponseComplete, 201 RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps/audit-configmap", nonAdmissionWebhookNamespace), 202 Verb: "delete", 203 Code: 200, 204 User: auditTestUser, 205 Resource: "configmaps", 206 Namespace: nonAdmissionWebhookNamespace, 207 RequestObject: true, 208 ResponseObject: true, 209 AuthorizeDecision: "allow", 210 }, 211 } 212 ) 213 214 // TestAudit ensures that both v1beta1 and v1 version audit api could work. 215 func TestAudit(t *testing.T) { 216 for version := range versions { 217 runTestWithVersion(t, version) 218 } 219 } 220 221 func runTestWithVersion(t *testing.T, version string) { 222 webhookMux := http.NewServeMux() 223 webhookMux.Handle("/mutation", utils.AdmissionWebhookHandler(t, admitFunc)) 224 url, closeFunc, err := utils.NewAdmissionWebhookServer(webhookMux) 225 defer closeFunc() 226 if err != nil { 227 t.Fatalf("%v", err) 228 } 229 230 // prepare audit policy file 231 auditPolicy := strings.Replace(auditPolicyPattern, "{version}", version, 1) 232 policyFile, err := os.CreateTemp("", "audit-policy.yaml") 233 if err != nil { 234 t.Fatalf("Failed to create audit policy file: %v", err) 235 } 236 defer os.Remove(policyFile.Name()) 237 if _, err := policyFile.Write([]byte(auditPolicy)); err != nil { 238 t.Fatalf("Failed to write audit policy file: %v", err) 239 } 240 if err := policyFile.Close(); err != nil { 241 t.Fatalf("Failed to close audit policy file: %v", err) 242 } 243 244 // prepare audit log file 245 logFile, err := os.CreateTemp("", "audit.log") 246 if err != nil { 247 t.Fatalf("Failed to create audit log file: %v", err) 248 } 249 defer utiltesting.CloseAndRemove(t, logFile) 250 251 // start api server 252 result := kubeapiservertesting.StartTestServerOrDie(t, nil, 253 []string{ 254 "--audit-policy-file", policyFile.Name(), 255 "--audit-log-version", version, 256 "--audit-log-mode", "blocking", 257 "--audit-log-path", logFile.Name()}, 258 framework.SharedEtcd()) 259 defer result.TearDownFn() 260 261 kubeclient, err := clientset.NewForConfig(result.ClientConfig) 262 if err != nil { 263 t.Fatalf("Unexpected error: %v", err) 264 } 265 266 if err := createMutationWebhook(kubeclient, url+"/mutation"); err != nil { 267 t.Fatal(err) 268 } 269 270 tcs := []struct { 271 auditLevel auditinternal.Level 272 enableMutatingWebhook bool 273 namespace string 274 }{ 275 { 276 auditLevel: auditinternal.LevelRequestResponse, 277 enableMutatingWebhook: false, 278 namespace: nonAdmissionWebhookNamespace, 279 }, 280 { 281 auditLevel: auditinternal.LevelMetadata, 282 enableMutatingWebhook: true, 283 namespace: "webhook-audit-metadata", 284 }, 285 { 286 auditLevel: auditinternal.LevelRequest, 287 enableMutatingWebhook: true, 288 namespace: "webhook-audit-request", 289 }, 290 { 291 auditLevel: auditinternal.LevelRequestResponse, 292 enableMutatingWebhook: true, 293 namespace: "webhook-audit-response", 294 }, 295 } 296 297 crossGroupTestCases := []struct { 298 auditLevel auditinternal.Level 299 expEvents []utils.AuditEvent 300 namespace string 301 }{ 302 { 303 auditLevel: auditinternal.LevelRequest, 304 namespace: "create-audit-request", 305 expEvents: []utils.AuditEvent{ 306 { 307 Level: auditinternal.LevelRequest, 308 Stage: auditinternal.StageResponseComplete, 309 RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/serviceaccounts/%s/token", "create-audit-request", "audit-serviceaccount"), 310 Verb: "create", 311 Code: 201, 312 User: auditTestUser, 313 Resource: "serviceaccounts", 314 Namespace: "create-audit-request", 315 RequestObject: true, 316 ResponseObject: false, 317 AuthorizeDecision: "allow", 318 }, 319 }, 320 }, 321 { 322 auditLevel: auditinternal.LevelRequestResponse, 323 namespace: "create-audit-response", 324 expEvents: []utils.AuditEvent{ 325 { 326 Level: auditinternal.LevelRequestResponse, 327 Stage: auditinternal.StageResponseComplete, 328 RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/serviceaccounts/%s/token", "create-audit-response", "audit-serviceaccount"), 329 Verb: "create", 330 Code: 201, 331 User: auditTestUser, 332 Resource: "serviceaccounts", 333 Namespace: "create-audit-response", 334 RequestObject: true, 335 ResponseObject: true, 336 AuthorizeDecision: "allow", 337 }, 338 }, 339 }, 340 { 341 auditLevel: auditinternal.LevelRequest, 342 namespace: "update-audit-request", 343 expEvents: []utils.AuditEvent{ 344 { 345 Level: auditinternal.LevelRequest, 346 Stage: auditinternal.StageResponseComplete, 347 RequestURI: fmt.Sprintf("/apis/apps/v1/namespaces/%s/deployments/%s/scale", "update-audit-request", "audit-deployment"), 348 Verb: "update", 349 Code: 200, 350 User: auditTestUser, 351 Resource: "deployments", 352 Namespace: "update-audit-request", 353 RequestObject: true, 354 ResponseObject: false, 355 AuthorizeDecision: "allow", 356 }, 357 }, 358 }, 359 { 360 auditLevel: auditinternal.LevelRequestResponse, 361 namespace: "update-audit-response", 362 expEvents: []utils.AuditEvent{ 363 { 364 Level: auditinternal.LevelRequestResponse, 365 Stage: auditinternal.StageResponseComplete, 366 RequestURI: fmt.Sprintf("/apis/apps/v1/namespaces/%s/deployments/%s/scale", "update-audit-response", "audit-deployment"), 367 Verb: "update", 368 Code: 200, 369 User: auditTestUser, 370 Resource: "deployments", 371 Namespace: "update-audit-response", 372 RequestObject: true, 373 ResponseObject: true, 374 AuthorizeDecision: "allow", 375 }, 376 }, 377 }, 378 } 379 380 for _, tc := range tcs { 381 t.Run(fmt.Sprintf("%s.%s.%t", version, tc.auditLevel, tc.enableMutatingWebhook), func(t *testing.T) { 382 testAudit(t, version, tc.auditLevel, tc.enableMutatingWebhook, tc.namespace, kubeclient, logFile) 383 }) 384 } 385 386 // cross-group subResources 387 for _, tc := range crossGroupTestCases { 388 t.Run(fmt.Sprintf("cross-group-%s.%s.%s", version, tc.auditLevel, tc.namespace), func(t *testing.T) { 389 testAuditCrossGroupSubResource(t, version, tc.expEvents, tc.namespace, kubeclient, logFile) 390 }) 391 } 392 } 393 394 func testAudit(t *testing.T, version string, level auditinternal.Level, enableMutatingWebhook bool, namespace string, kubeclient clientset.Interface, logFile *os.File) { 395 var lastMissingReport string 396 createNamespace(t, kubeclient, namespace) 397 398 if err := wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) { 399 // perform configmap operations 400 configMapOperations(t, kubeclient, namespace) 401 402 // check for corresponding audit logs 403 stream, err := os.Open(logFile.Name()) 404 if err != nil { 405 return false, fmt.Errorf("unexpected error: %v", err) 406 } 407 defer stream.Close() 408 missingReport, err := utils.CheckAuditLines(stream, getExpectedEvents(level, enableMutatingWebhook, namespace), versions[version]) 409 if err != nil { 410 return false, fmt.Errorf("unexpected error: %v", err) 411 } 412 if len(missingReport.MissingEvents) > 0 { 413 lastMissingReport = missingReport.String() 414 return false, nil 415 } 416 return true, nil 417 }); err != nil { 418 t.Fatalf("failed to get expected events -- missingReport: %s, error: %v", lastMissingReport, err) 419 } 420 } 421 422 func testAuditCrossGroupSubResource(t *testing.T, version string, expEvents []utils.AuditEvent, namespace string, kubeclient clientset.Interface, logFile *os.File) { 423 var ( 424 lastMissingReport string 425 sa *apiv1.ServiceAccount 426 deploy *appsv1.Deployment 427 ) 428 429 createNamespace(t, kubeclient, namespace) 430 switch expEvents[0].Resource { 431 case "serviceaccounts": 432 sa = createServiceAccount(t, kubeclient, namespace) 433 case "deployments": 434 deploy = createDeployment(t, kubeclient, namespace) 435 default: 436 t.Fatalf("%v resource has no cross-group sub-resources", expEvents[0].Resource) 437 } 438 439 if err := wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) { 440 // perform cross-group subresources operations 441 if sa != nil { 442 tokenRequestOperations(t, kubeclient, sa.Namespace, sa.Name) 443 } 444 if deploy != nil { 445 scaleOperations(t, kubeclient, deploy.Namespace, deploy.Name) 446 } 447 448 // check for corresponding audit logs 449 stream, err := os.Open(logFile.Name()) 450 if err != nil { 451 return false, fmt.Errorf("unexpected error: %v", err) 452 } 453 defer stream.Close() 454 missingReport, err := utils.CheckAuditLines(stream, expEvents, versions[version]) 455 if err != nil { 456 return false, fmt.Errorf("unexpected error: %v", err) 457 } 458 if len(missingReport.MissingEvents) > 0 { 459 lastMissingReport = missingReport.String() 460 return false, nil 461 } 462 return true, nil 463 }); err != nil { 464 t.Fatalf("failed to get expected events -- missingReport: %s, error: %v", lastMissingReport, err) 465 } 466 } 467 468 func getExpectedEvents(level auditinternal.Level, enableMutatingWebhook bool, namespace string) []utils.AuditEvent { 469 if !enableMutatingWebhook { 470 return expectedEvents 471 } 472 473 var webhookMutationAnnotations, webhookPatchAnnotations map[string]string 474 var requestObject, responseObject bool 475 if level.GreaterOrEqual(auditinternal.LevelMetadata) { 476 // expect mutation existence annotation 477 webhookMutationAnnotations = map[string]string{} 478 webhookMutationAnnotations[mutating.MutationAuditAnnotationPrefix+"round_0_index_0"] = fmt.Sprintf(`{"configuration":"%s","webhook":"%s","mutated":%t}`, testWebhookConfigurationName, testWebhookName, true) 479 } 480 if level.GreaterOrEqual(auditinternal.LevelRequest) { 481 // expect actual patch annotation 482 webhookPatchAnnotations = map[string]string{} 483 webhookPatchAnnotations[mutating.PatchAuditAnnotationPrefix+"round_0_index_0"] = strings.Replace(fmt.Sprintf(`{"configuration": "%s", "webhook": "%s", "patch": %s, "patchType": "JSONPatch"}`, testWebhookConfigurationName, testWebhookName, `[{"op":"add","path":"/data","value":{"test":"dummy"}}]`), " ", "", -1) 484 // expect request object in audit log 485 requestObject = true 486 } 487 if level.GreaterOrEqual(auditinternal.LevelRequestResponse) { 488 // expect response obect in audit log 489 responseObject = true 490 } 491 return []utils.AuditEvent{ 492 { 493 // expect CREATE audit event with webhook in effect 494 Level: level, 495 Stage: auditinternal.StageResponseComplete, 496 RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps", namespace), 497 Verb: "create", 498 Code: 201, 499 User: auditTestUser, 500 Resource: "configmaps", 501 Namespace: namespace, 502 AuthorizeDecision: "allow", 503 RequestObject: requestObject, 504 ResponseObject: responseObject, 505 AdmissionWebhookMutationAnnotations: webhookMutationAnnotations, 506 AdmissionWebhookPatchAnnotations: webhookPatchAnnotations, 507 }, { 508 // expect UPDATE audit event with webhook in effect 509 Level: level, 510 Stage: auditinternal.StageResponseComplete, 511 RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps/audit-configmap", namespace), 512 Verb: "update", 513 Code: 200, 514 User: auditTestUser, 515 Resource: "configmaps", 516 Namespace: namespace, 517 AuthorizeDecision: "allow", 518 RequestObject: requestObject, 519 ResponseObject: responseObject, 520 AdmissionWebhookMutationAnnotations: webhookMutationAnnotations, 521 AdmissionWebhookPatchAnnotations: webhookPatchAnnotations, 522 }, 523 } 524 } 525 526 // configMapOperations is a set of known operations performed on the configmap type 527 // which correspond to the expected events. 528 // This is shared by the dynamic test 529 func configMapOperations(t *testing.T, kubeclient clientset.Interface, namespace string) { 530 // create, get, watch, update, patch, list and delete configmap. 531 configMap := &apiv1.ConfigMap{ 532 ObjectMeta: metav1.ObjectMeta{ 533 Name: "audit-configmap", 534 Namespace: namespace, 535 }, 536 Data: map[string]string{ 537 "map-key": "map-value", 538 }, 539 } 540 // add admission label to config maps that are to be sent to webhook 541 if namespace != nonAdmissionWebhookNamespace { 542 configMap.Labels = map[string]string{ 543 "admission": "true", 544 } 545 } 546 547 _, err := kubeclient.CoreV1().ConfigMaps(namespace).Create(context.TODO(), configMap, metav1.CreateOptions{}) 548 expectNoError(t, err, "failed to create audit-configmap") 549 550 _, err = kubeclient.CoreV1().ConfigMaps(namespace).Get(context.TODO(), configMap.Name, metav1.GetOptions{}) 551 expectNoError(t, err, "failed to get audit-configmap") 552 553 configMapChan, err := kubeclient.CoreV1().ConfigMaps(namespace).Watch(context.TODO(), watchOptions) 554 expectNoError(t, err, "failed to create watch for config maps") 555 for range configMapChan.ResultChan() { 556 // Block until watchOptions.TimeoutSeconds expires. 557 // If the test finishes before watchOptions.TimeoutSeconds expires, the watch audit 558 // event at stage ResponseComplete will not be generated. 559 } 560 561 _, err = kubeclient.CoreV1().ConfigMaps(namespace).Update(context.TODO(), configMap, metav1.UpdateOptions{}) 562 expectNoError(t, err, "failed to update audit-configmap") 563 564 _, err = kubeclient.CoreV1().ConfigMaps(namespace).Patch(context.TODO(), configMap.Name, types.JSONPatchType, patch, metav1.PatchOptions{}) 565 expectNoError(t, err, "failed to patch configmap") 566 567 _, err = kubeclient.CoreV1().ConfigMaps(namespace).List(context.TODO(), metav1.ListOptions{}) 568 expectNoError(t, err, "failed to list config maps") 569 570 err = kubeclient.CoreV1().ConfigMaps(namespace).Delete(context.TODO(), configMap.Name, metav1.DeleteOptions{}) 571 expectNoError(t, err, "failed to delete audit-configmap") 572 } 573 574 func tokenRequestOperations(t *testing.T, kubeClient clientset.Interface, namespace, name string) { 575 var ( 576 treq = &authenticationv1.TokenRequest{ 577 Spec: authenticationv1.TokenRequestSpec{ 578 Audiences: []string{"api"}, 579 }, 580 } 581 ) 582 // create tokenRequest 583 _, err := kubeClient.CoreV1().ServiceAccounts(namespace).CreateToken(context.TODO(), name, treq, metav1.CreateOptions{}) 584 expectNoError(t, err, "failed to create audit-tokenRequest") 585 } 586 587 func scaleOperations(t *testing.T, kubeClient clientset.Interface, namespace, name string) { 588 var ( 589 scale = &autoscalingv1.Scale{ 590 ObjectMeta: metav1.ObjectMeta{ 591 Name: "audit-deployment", 592 Namespace: namespace, 593 }, 594 Spec: autoscalingv1.ScaleSpec{ 595 Replicas: 2, 596 }, 597 } 598 ) 599 600 // update scale 601 _, err := kubeClient.AppsV1().Deployments(namespace).UpdateScale(context.TODO(), name, scale, metav1.UpdateOptions{}) 602 expectNoError(t, err, fmt.Sprintf("failed to update scale %v", scale)) 603 } 604 605 func expectNoError(t *testing.T, err error, msg string) { 606 if err != nil { 607 t.Fatalf("%s: %v", msg, err) 608 } 609 } 610 611 func admitFunc(review *v1beta1.AdmissionReview) error { 612 gvk := schema.GroupVersionKind{Group: "admission.k8s.io", Version: "v1beta1", Kind: "AdmissionReview"} 613 if review.GetObjectKind().GroupVersionKind() != gvk { 614 return fmt.Errorf("invalid admission review kind: %#v", review.GetObjectKind().GroupVersionKind()) 615 } 616 if len(review.Request.Object.Raw) > 0 { 617 u := &unstructured.Unstructured{Object: map[string]interface{}{}} 618 if err := json.Unmarshal(review.Request.Object.Raw, u); err != nil { 619 return fmt.Errorf("failed to deserialize object: %s with error: %v", string(review.Request.Object.Raw), err) 620 } 621 review.Request.Object.Object = u 622 } 623 if len(review.Request.OldObject.Raw) > 0 { 624 u := &unstructured.Unstructured{Object: map[string]interface{}{}} 625 if err := json.Unmarshal(review.Request.OldObject.Raw, u); err != nil { 626 return fmt.Errorf("failed to deserialize object: %s with error: %v", string(review.Request.OldObject.Raw), err) 627 } 628 review.Request.OldObject.Object = u 629 } 630 631 review.Response = &v1beta1.AdmissionResponse{ 632 Allowed: true, 633 UID: review.Request.UID, 634 Result: &metav1.Status{Message: "admitted"}, 635 } 636 review.Response.Patch = []byte(`[{"op":"add","path":"/data","value":{"test":"dummy"}}]`) 637 jsonPatch := v1beta1.PatchTypeJSONPatch 638 review.Response.PatchType = &jsonPatch 639 return nil 640 } 641 642 func createMutationWebhook(client clientset.Interface, endpoint string) error { 643 fail := admissionregistrationv1.Fail 644 noSideEffects := admissionregistrationv1.SideEffectClassNone 645 // Attaching Mutation webhook to API server 646 _, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), &admissionregistrationv1.MutatingWebhookConfiguration{ 647 ObjectMeta: metav1.ObjectMeta{Name: testWebhookConfigurationName}, 648 Webhooks: []admissionregistrationv1.MutatingWebhook{{ 649 Name: testWebhookName, 650 ClientConfig: admissionregistrationv1.WebhookClientConfig{ 651 URL: &endpoint, 652 CABundle: utils.LocalhostCert, 653 }, 654 Rules: []admissionregistrationv1.RuleWithOperations{{ 655 Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update}, 656 Rule: admissionregistrationv1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}}, 657 }}, 658 ObjectSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"admission": "true"}}, 659 FailurePolicy: &fail, 660 AdmissionReviewVersions: []string{"v1beta1"}, 661 SideEffects: &noSideEffects, 662 }}, 663 }, metav1.CreateOptions{}) 664 return err 665 } 666 667 func createNamespace(t *testing.T, kubeclient clientset.Interface, namespace string) { 668 ns := &apiv1.Namespace{ 669 ObjectMeta: metav1.ObjectMeta{ 670 Name: namespace, 671 }, 672 } 673 _, err := kubeclient.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{}) 674 expectNoError(t, err, fmt.Sprintf("failed to create namespace ns %s", namespace)) 675 } 676 677 func createServiceAccount(t *testing.T, cs clientset.Interface, namespace string) *apiv1.ServiceAccount { 678 sa := &apiv1.ServiceAccount{ 679 ObjectMeta: metav1.ObjectMeta{ 680 Name: "audit-serviceaccount", 681 Namespace: namespace, 682 }, 683 } 684 _, err := cs.CoreV1().ServiceAccounts(sa.Namespace).Create(context.TODO(), sa, metav1.CreateOptions{}) 685 expectNoError(t, err, fmt.Sprintf("failed to create serviceaccount %v", sa)) 686 return sa 687 } 688 689 func createDeployment(t *testing.T, cs clientset.Interface, namespace string) *appsv1.Deployment { 690 deploy := &appsv1.Deployment{ 691 ObjectMeta: metav1.ObjectMeta{ 692 Name: "audit-deployment", 693 Namespace: namespace, 694 }, 695 Spec: appsv1.DeploymentSpec{ 696 Selector: &metav1.LabelSelector{ 697 MatchLabels: map[string]string{"app": "test"}, 698 }, 699 Template: apiv1.PodTemplateSpec{ 700 Spec: apiv1.PodSpec{ 701 Containers: []apiv1.Container{ 702 { 703 Name: "foo", 704 Image: "foo/bar", 705 }, 706 }, 707 }, 708 ObjectMeta: metav1.ObjectMeta{ 709 Name: "audit-deployment-scale", 710 Namespace: namespace, 711 Labels: map[string]string{"app": "test"}, 712 }, 713 }, 714 }, 715 } 716 _, err := cs.AppsV1().Deployments(deploy.Namespace).Create(context.TODO(), deploy, metav1.CreateOptions{}) 717 expectNoError(t, err, fmt.Sprintf("failed to create deployment %v", deploy)) 718 return deploy 719 }