k8s.io/kubernetes@v1.29.3/pkg/apis/admissionregistration/validation/validation_test.go (about) 1 /* 2 Copyright 2017 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 validation 18 19 import ( 20 "fmt" 21 "strings" 22 "testing" 23 24 "github.com/google/cel-go/cel" 25 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/util/validation/field" 28 "k8s.io/apimachinery/pkg/util/version" 29 plugincel "k8s.io/apiserver/pkg/admission/plugin/cel" 30 "k8s.io/apiserver/pkg/cel/environment" 31 "k8s.io/apiserver/pkg/cel/library" 32 "k8s.io/kubernetes/pkg/apis/admissionregistration" 33 ) 34 35 func ptr[T any](v T) *T { return &v } 36 37 func strPtr(s string) *string { return &s } 38 39 func int32Ptr(i int32) *int32 { return &i } 40 41 func newValidatingWebhookConfiguration(hooks []admissionregistration.ValidatingWebhook, defaultAdmissionReviewVersions bool) *admissionregistration.ValidatingWebhookConfiguration { 42 // If the test case did not specify an AdmissionReviewVersions, default it so the test passes as 43 // this field will be defaulted in production code. 44 for i := range hooks { 45 if defaultAdmissionReviewVersions && len(hooks[i].AdmissionReviewVersions) == 0 { 46 hooks[i].AdmissionReviewVersions = []string{"v1beta1"} 47 } 48 } 49 return &admissionregistration.ValidatingWebhookConfiguration{ 50 ObjectMeta: metav1.ObjectMeta{ 51 Name: "config", 52 }, 53 Webhooks: hooks, 54 } 55 } 56 57 func TestValidateValidatingWebhookConfiguration(t *testing.T) { 58 noSideEffect := admissionregistration.SideEffectClassNone 59 unknownSideEffect := admissionregistration.SideEffectClassUnknown 60 validClientConfig := admissionregistration.WebhookClientConfig{ 61 URL: strPtr("https://example.com"), 62 } 63 tests := []struct { 64 name string 65 config *admissionregistration.ValidatingWebhookConfiguration 66 expectedError string 67 }{{ 68 name: "AdmissionReviewVersions are required", 69 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 70 Name: "webhook.k8s.io", 71 ClientConfig: validClientConfig, 72 SideEffects: &unknownSideEffect, 73 }, 74 }, false), 75 expectedError: `webhooks[0].admissionReviewVersions: Required value: must specify one of v1, v1beta1`, 76 }, { 77 name: "should fail on bad AdmissionReviewVersion value", 78 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 79 Name: "webhook.k8s.io", 80 ClientConfig: validClientConfig, 81 AdmissionReviewVersions: []string{"0v"}, 82 }, 83 }, true), 84 expectedError: `Invalid value: "0v": a DNS-1035 label`, 85 }, { 86 name: "should pass on valid AdmissionReviewVersion", 87 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 88 Name: "webhook.k8s.io", 89 ClientConfig: validClientConfig, 90 SideEffects: &noSideEffect, 91 AdmissionReviewVersions: []string{"v1beta1"}, 92 }, 93 }, true), 94 expectedError: ``, 95 }, { 96 name: "should pass on mix of accepted and unaccepted AdmissionReviewVersion", 97 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 98 Name: "webhook.k8s.io", 99 ClientConfig: validClientConfig, 100 SideEffects: &noSideEffect, 101 AdmissionReviewVersions: []string{"v1beta1", "invalid-version"}, 102 }, 103 }, true), 104 expectedError: ``, 105 }, { 106 name: "should fail on invalid AdmissionReviewVersion", 107 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 108 Name: "webhook.k8s.io", 109 ClientConfig: validClientConfig, 110 AdmissionReviewVersions: []string{"invalidVersion"}, 111 }, 112 }, true), 113 expectedError: `Invalid value: []string{"invalidVersion"}`, 114 }, { 115 name: "should fail on duplicate AdmissionReviewVersion", 116 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 117 Name: "webhook.k8s.io", 118 ClientConfig: validClientConfig, 119 AdmissionReviewVersions: []string{"v1beta1", "v1beta1"}, 120 }, 121 }, true), 122 expectedError: `Invalid value: "v1beta1": duplicate version`, 123 }, { 124 name: "all Webhooks must have a fully qualified name", 125 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 126 Name: "webhook.k8s.io", 127 ClientConfig: validClientConfig, 128 SideEffects: &noSideEffect, 129 }, { 130 Name: "k8s.io", 131 ClientConfig: validClientConfig, 132 SideEffects: &noSideEffect, 133 }, { 134 Name: "", 135 ClientConfig: validClientConfig, 136 SideEffects: &noSideEffect, 137 }, 138 }, true), 139 expectedError: `webhooks[1].name: Invalid value: "k8s.io": should be a domain with at least three segments separated by dots, webhooks[2].name: Required value`, 140 }, { 141 name: "Webhooks must have unique names when created", 142 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 143 Name: "webhook.k8s.io", 144 ClientConfig: validClientConfig, 145 SideEffects: &unknownSideEffect, 146 }, { 147 Name: "webhook.k8s.io", 148 ClientConfig: validClientConfig, 149 SideEffects: &unknownSideEffect, 150 }, 151 }, true), 152 expectedError: `webhooks[1].name: Duplicate value: "webhook.k8s.io"`, 153 }, { 154 name: "Operations must not be empty or nil", 155 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 156 Name: "webhook.k8s.io", 157 Rules: []admissionregistration.RuleWithOperations{{ 158 Operations: []admissionregistration.OperationType{}, 159 Rule: admissionregistration.Rule{ 160 APIGroups: []string{"a"}, 161 APIVersions: []string{"a"}, 162 Resources: []string{"a"}, 163 }, 164 }, { 165 Operations: nil, 166 Rule: admissionregistration.Rule{ 167 APIGroups: []string{"a"}, 168 APIVersions: []string{"a"}, 169 Resources: []string{"a"}, 170 }, 171 }}, 172 }, 173 }, true), 174 expectedError: `webhooks[0].rules[0].operations: Required value, webhooks[0].rules[1].operations: Required value`, 175 }, { 176 name: "\"\" is NOT a valid operation", 177 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 178 Name: "webhook.k8s.io", 179 Rules: []admissionregistration.RuleWithOperations{{ 180 Operations: []admissionregistration.OperationType{"CREATE", ""}, 181 Rule: admissionregistration.Rule{ 182 APIGroups: []string{"a"}, 183 APIVersions: []string{"a"}, 184 Resources: []string{"a"}, 185 }, 186 }}, 187 }, 188 }, true), 189 expectedError: `Unsupported value: ""`, 190 }, { 191 name: "operation must be either create/update/delete/connect", 192 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 193 Name: "webhook.k8s.io", 194 Rules: []admissionregistration.RuleWithOperations{{ 195 Operations: []admissionregistration.OperationType{"PATCH"}, 196 Rule: admissionregistration.Rule{ 197 APIGroups: []string{"a"}, 198 APIVersions: []string{"a"}, 199 Resources: []string{"a"}, 200 }, 201 }}, 202 }, 203 }, true), 204 expectedError: `Unsupported value: "PATCH"`, 205 }, { 206 name: "wildcard operation cannot be mixed with other strings", 207 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 208 Name: "webhook.k8s.io", 209 Rules: []admissionregistration.RuleWithOperations{{ 210 Operations: []admissionregistration.OperationType{"CREATE", "*"}, 211 Rule: admissionregistration.Rule{ 212 APIGroups: []string{"a"}, 213 APIVersions: []string{"a"}, 214 Resources: []string{"a"}, 215 }, 216 }}, 217 }, 218 }, true), 219 expectedError: `if '*' is present, must not specify other operations`, 220 }, { 221 name: `resource "*" can co-exist with resources that have subresources`, 222 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 223 Name: "webhook.k8s.io", 224 ClientConfig: validClientConfig, 225 SideEffects: &noSideEffect, 226 Rules: []admissionregistration.RuleWithOperations{{ 227 Operations: []admissionregistration.OperationType{"CREATE"}, 228 Rule: admissionregistration.Rule{ 229 APIGroups: []string{"a"}, 230 APIVersions: []string{"a"}, 231 Resources: []string{"*", "a/b", "a/*", "*/b"}, 232 }, 233 }}, 234 }, 235 }, true), 236 }, { 237 name: `resource "*" cannot mix with resources that don't have subresources`, 238 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 239 Name: "webhook.k8s.io", 240 ClientConfig: validClientConfig, 241 SideEffects: &unknownSideEffect, 242 Rules: []admissionregistration.RuleWithOperations{{ 243 Operations: []admissionregistration.OperationType{"CREATE"}, 244 Rule: admissionregistration.Rule{ 245 APIGroups: []string{"a"}, 246 APIVersions: []string{"a"}, 247 Resources: []string{"*", "a"}, 248 }, 249 }}, 250 }, 251 }, true), 252 expectedError: `if '*' is present, must not specify other resources without subresources`, 253 }, { 254 name: "resource a/* cannot mix with a/x", 255 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 256 Name: "webhook.k8s.io", 257 ClientConfig: validClientConfig, 258 SideEffects: &unknownSideEffect, 259 Rules: []admissionregistration.RuleWithOperations{{ 260 Operations: []admissionregistration.OperationType{"CREATE"}, 261 Rule: admissionregistration.Rule{ 262 APIGroups: []string{"a"}, 263 APIVersions: []string{"a"}, 264 Resources: []string{"a/*", "a/x"}, 265 }, 266 }}, 267 }, 268 }, true), 269 expectedError: `webhooks[0].rules[0].resources[1]: Invalid value: "a/x": if 'a/*' is present, must not specify a/x`, 270 }, { 271 name: "resource a/* can mix with a", 272 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 273 Name: "webhook.k8s.io", 274 ClientConfig: validClientConfig, 275 SideEffects: &noSideEffect, 276 Rules: []admissionregistration.RuleWithOperations{{ 277 Operations: []admissionregistration.OperationType{"CREATE"}, 278 Rule: admissionregistration.Rule{ 279 APIGroups: []string{"a"}, 280 APIVersions: []string{"a"}, 281 Resources: []string{"a/*", "a"}, 282 }, 283 }}, 284 }, 285 }, true), 286 }, { 287 name: "resource */a cannot mix with x/a", 288 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 289 Name: "webhook.k8s.io", 290 ClientConfig: validClientConfig, 291 SideEffects: &unknownSideEffect, 292 Rules: []admissionregistration.RuleWithOperations{{ 293 Operations: []admissionregistration.OperationType{"CREATE"}, 294 Rule: admissionregistration.Rule{ 295 APIGroups: []string{"a"}, 296 APIVersions: []string{"a"}, 297 Resources: []string{"*/a", "x/a"}, 298 }, 299 }}, 300 }, 301 }, true), 302 expectedError: `webhooks[0].rules[0].resources[1]: Invalid value: "x/a": if '*/a' is present, must not specify x/a`, 303 }, { 304 name: "resource */* cannot mix with other resources", 305 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 306 Name: "webhook.k8s.io", 307 ClientConfig: validClientConfig, 308 SideEffects: &unknownSideEffect, 309 Rules: []admissionregistration.RuleWithOperations{{ 310 Operations: []admissionregistration.OperationType{"CREATE"}, 311 Rule: admissionregistration.Rule{ 312 APIGroups: []string{"a"}, 313 APIVersions: []string{"a"}, 314 Resources: []string{"*/*", "a"}, 315 }, 316 }}, 317 }, 318 }, true), 319 expectedError: `webhooks[0].rules[0].resources: Invalid value: []string{"*/*", "a"}: if '*/*' is present, must not specify other resources`, 320 }, { 321 name: "FailurePolicy can only be \"Ignore\" or \"Fail\"", 322 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 323 Name: "webhook.k8s.io", 324 ClientConfig: validClientConfig, 325 SideEffects: &unknownSideEffect, 326 FailurePolicy: func() *admissionregistration.FailurePolicyType { 327 r := admissionregistration.FailurePolicyType("other") 328 return &r 329 }(), 330 }, 331 }, true), 332 expectedError: `webhooks[0].failurePolicy: Unsupported value: "other": supported values: "Fail", "Ignore"`, 333 }, { 334 name: "AdmissionReviewVersions are required", 335 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 336 Name: "webhook.k8s.io", 337 ClientConfig: validClientConfig, 338 SideEffects: &unknownSideEffect, 339 }, 340 }, false), 341 expectedError: `webhooks[0].admissionReviewVersions: Required value: must specify one of v1, v1beta1`, 342 }, { 343 name: "SideEffects are required", 344 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 345 Name: "webhook.k8s.io", 346 ClientConfig: validClientConfig, 347 SideEffects: nil, 348 }, 349 }, true), 350 expectedError: `webhooks[0].sideEffects: Required value: must specify one of None, NoneOnDryRun`, 351 }, { 352 name: "SideEffects can only be \"None\" or \"NoneOnDryRun\" when created", 353 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 354 Name: "webhook.k8s.io", 355 ClientConfig: validClientConfig, 356 SideEffects: func() *admissionregistration.SideEffectClass { 357 r := admissionregistration.SideEffectClass("other") 358 return &r 359 }(), 360 }, 361 }, true), 362 expectedError: `webhooks[0].sideEffects: Unsupported value: "other": supported values: "None", "NoneOnDryRun"`, 363 }, { 364 name: "both service and URL missing", 365 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 366 Name: "webhook.k8s.io", 367 ClientConfig: admissionregistration.WebhookClientConfig{}, 368 }, 369 }, true), 370 expectedError: `exactly one of`, 371 }, { 372 name: "both service and URL provided", 373 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 374 Name: "webhook.k8s.io", 375 ClientConfig: admissionregistration.WebhookClientConfig{ 376 Service: &admissionregistration.ServiceReference{ 377 Namespace: "ns", 378 Name: "n", 379 Port: 443, 380 }, 381 URL: strPtr("example.com/k8s/webhook"), 382 }, 383 }, 384 }, true), 385 expectedError: `[0].clientConfig: Required value: exactly one of url or service is required`, 386 }, { 387 name: "blank URL", 388 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 389 Name: "webhook.k8s.io", 390 ClientConfig: admissionregistration.WebhookClientConfig{ 391 URL: strPtr(""), 392 }, 393 }, 394 }, true), 395 expectedError: `[0].clientConfig.url: Invalid value: "": host must be specified`, 396 }, { 397 name: "wrong scheme", 398 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 399 Name: "webhook.k8s.io", 400 ClientConfig: admissionregistration.WebhookClientConfig{ 401 URL: strPtr("http://example.com"), 402 }, 403 }, 404 }, true), 405 expectedError: `https`, 406 }, { 407 name: "missing host", 408 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 409 Name: "webhook.k8s.io", 410 ClientConfig: admissionregistration.WebhookClientConfig{ 411 URL: strPtr("https:///fancy/webhook"), 412 }, 413 }, 414 }, true), 415 expectedError: `host must be specified`, 416 }, { 417 name: "fragment", 418 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 419 Name: "webhook.k8s.io", 420 ClientConfig: admissionregistration.WebhookClientConfig{ 421 URL: strPtr("https://example.com/#bookmark"), 422 }, 423 }, 424 }, true), 425 expectedError: `"bookmark": fragments are not permitted`, 426 }, { 427 name: "query", 428 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 429 Name: "webhook.k8s.io", 430 ClientConfig: admissionregistration.WebhookClientConfig{ 431 URL: strPtr("https://example.com?arg=value"), 432 }, 433 }, 434 }, true), 435 expectedError: `"arg=value": query parameters are not permitted`, 436 }, { 437 name: "user", 438 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 439 Name: "webhook.k8s.io", 440 ClientConfig: admissionregistration.WebhookClientConfig{ 441 URL: strPtr("https://harry.potter@example.com/"), 442 }, 443 }, 444 }, true), 445 expectedError: `"harry.potter": user information is not permitted`, 446 }, { 447 name: "just totally wrong", 448 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 449 Name: "webhook.k8s.io", 450 ClientConfig: admissionregistration.WebhookClientConfig{ 451 URL: strPtr("arg#backwards=thisis?html.index/port:host//:https"), 452 }, 453 }, 454 }, true), 455 expectedError: `host must be specified`, 456 }, { 457 name: "path must start with slash", 458 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 459 Name: "webhook.k8s.io", 460 ClientConfig: admissionregistration.WebhookClientConfig{ 461 Service: &admissionregistration.ServiceReference{ 462 Namespace: "ns", 463 Name: "n", 464 Path: strPtr("foo/"), 465 Port: 443, 466 }, 467 }, 468 }, 469 }, true), 470 expectedError: `clientConfig.service.path: Invalid value: "foo/": must start with a '/'`, 471 }, { 472 name: "path accepts slash", 473 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 474 Name: "webhook.k8s.io", 475 ClientConfig: admissionregistration.WebhookClientConfig{ 476 Service: &admissionregistration.ServiceReference{ 477 Namespace: "ns", 478 Name: "n", 479 Path: strPtr("/"), 480 Port: 443, 481 }, 482 }, 483 SideEffects: &noSideEffect, 484 }, 485 }, true), 486 expectedError: ``, 487 }, { 488 name: "path accepts no trailing slash", 489 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 490 Name: "webhook.k8s.io", 491 ClientConfig: admissionregistration.WebhookClientConfig{ 492 Service: &admissionregistration.ServiceReference{ 493 Namespace: "ns", 494 Name: "n", 495 Path: strPtr("/foo"), 496 Port: 443, 497 }, 498 }, 499 SideEffects: &noSideEffect, 500 }, 501 }, true), 502 expectedError: ``, 503 }, { 504 name: "path fails //", 505 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 506 Name: "webhook.k8s.io", 507 ClientConfig: admissionregistration.WebhookClientConfig{ 508 Service: &admissionregistration.ServiceReference{ 509 Namespace: "ns", 510 Name: "n", 511 Path: strPtr("//"), 512 Port: 443, 513 }, 514 }, 515 SideEffects: &noSideEffect, 516 }, 517 }, true), 518 expectedError: `clientConfig.service.path: Invalid value: "//": segment[0] may not be empty`, 519 }, { 520 name: "path no empty step", 521 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 522 Name: "webhook.k8s.io", 523 ClientConfig: admissionregistration.WebhookClientConfig{ 524 Service: &admissionregistration.ServiceReference{ 525 Namespace: "ns", 526 Name: "n", 527 Path: strPtr("/foo//bar/"), 528 Port: 443, 529 }, 530 }, 531 SideEffects: &unknownSideEffect, 532 }, 533 }, true), 534 expectedError: `clientConfig.service.path: Invalid value: "/foo//bar/": segment[1] may not be empty`, 535 }, { 536 name: "path no empty step 2", 537 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 538 Name: "webhook.k8s.io", 539 ClientConfig: admissionregistration.WebhookClientConfig{ 540 Service: &admissionregistration.ServiceReference{ 541 Namespace: "ns", 542 Name: "n", 543 Path: strPtr("/foo/bar//"), 544 Port: 443, 545 }, 546 }, 547 SideEffects: &unknownSideEffect, 548 }, 549 }, true), 550 expectedError: `clientConfig.service.path: Invalid value: "/foo/bar//": segment[2] may not be empty`, 551 }, { 552 name: "path no non-subdomain", 553 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 554 Name: "webhook.k8s.io", 555 ClientConfig: admissionregistration.WebhookClientConfig{ 556 Service: &admissionregistration.ServiceReference{ 557 Namespace: "ns", 558 Name: "n", 559 Path: strPtr("/apis/foo.bar/v1alpha1/--bad"), 560 Port: 443, 561 }, 562 }, 563 SideEffects: &unknownSideEffect, 564 }, 565 }, true), 566 expectedError: `clientConfig.service.path: Invalid value: "/apis/foo.bar/v1alpha1/--bad": segment[3]: a lowercase RFC 1123 subdomain`, 567 }, { 568 name: "invalid port 0", 569 config: newValidatingWebhookConfiguration( 570 []admissionregistration.ValidatingWebhook{{ 571 Name: "webhook.k8s.io", 572 ClientConfig: admissionregistration.WebhookClientConfig{ 573 Service: &admissionregistration.ServiceReference{ 574 Namespace: "ns", 575 Name: "n", 576 Path: strPtr("https://apis/foo.bar"), 577 Port: 0, 578 }, 579 }, 580 SideEffects: &unknownSideEffect, 581 }, 582 }, true), 583 expectedError: `Invalid value: 0: port is not valid: must be between 1 and 65535, inclusive`, 584 }, { 585 name: "invalid port >65535", 586 config: newValidatingWebhookConfiguration( 587 []admissionregistration.ValidatingWebhook{{ 588 Name: "webhook.k8s.io", 589 ClientConfig: admissionregistration.WebhookClientConfig{ 590 Service: &admissionregistration.ServiceReference{ 591 Namespace: "ns", 592 Name: "n", 593 Path: strPtr("https://apis/foo.bar"), 594 Port: 65536, 595 }, 596 }, 597 SideEffects: &unknownSideEffect, 598 }, 599 }, true), 600 expectedError: `Invalid value: 65536: port is not valid: must be between 1 and 65535, inclusive`, 601 }, { 602 name: "timeout seconds cannot be greater than 30", 603 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 604 Name: "webhook.k8s.io", 605 ClientConfig: validClientConfig, 606 SideEffects: &unknownSideEffect, 607 TimeoutSeconds: int32Ptr(31), 608 }, 609 }, true), 610 expectedError: `webhooks[0].timeoutSeconds: Invalid value: 31: the timeout value must be between 1 and 30 seconds`, 611 }, { 612 name: "timeout seconds cannot be smaller than 1", 613 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 614 Name: "webhook.k8s.io", 615 ClientConfig: validClientConfig, 616 SideEffects: &unknownSideEffect, 617 TimeoutSeconds: int32Ptr(0), 618 }, 619 }, true), 620 expectedError: `webhooks[0].timeoutSeconds: Invalid value: 0: the timeout value must be between 1 and 30 seconds`, 621 }, { 622 name: "timeout seconds must be positive", 623 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 624 Name: "webhook.k8s.io", 625 ClientConfig: validClientConfig, 626 SideEffects: &unknownSideEffect, 627 TimeoutSeconds: int32Ptr(-1), 628 }, 629 }, true), 630 expectedError: `webhooks[0].timeoutSeconds: Invalid value: -1: the timeout value must be between 1 and 30 seconds`, 631 }, { 632 name: "valid timeout seconds", 633 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 634 Name: "webhook.k8s.io", 635 ClientConfig: validClientConfig, 636 SideEffects: &noSideEffect, 637 TimeoutSeconds: int32Ptr(1), 638 }, { 639 Name: "webhook2.k8s.io", 640 ClientConfig: validClientConfig, 641 SideEffects: &noSideEffect, 642 TimeoutSeconds: int32Ptr(15), 643 }, { 644 Name: "webhook3.k8s.io", 645 ClientConfig: validClientConfig, 646 SideEffects: &noSideEffect, 647 TimeoutSeconds: int32Ptr(30), 648 }, 649 }, true), 650 }, { 651 name: "single match condition must have a name", 652 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 653 Name: "webhook.k8s.io", 654 ClientConfig: validClientConfig, 655 SideEffects: &noSideEffect, 656 MatchConditions: []admissionregistration.MatchCondition{{ 657 Expression: "true", 658 }}, 659 }, 660 }, true), 661 expectedError: `webhooks[0].matchConditions[0].name: Required value`, 662 }, { 663 name: "all match conditions must have a name", 664 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 665 Name: "webhook.k8s.io", 666 ClientConfig: validClientConfig, 667 SideEffects: &noSideEffect, 668 MatchConditions: []admissionregistration.MatchCondition{{ 669 Expression: "true", 670 }, { 671 Expression: "true", 672 }}, 673 }, { 674 Name: "webhook.k8s.io", 675 ClientConfig: validClientConfig, 676 SideEffects: &noSideEffect, 677 MatchConditions: []admissionregistration.MatchCondition{{ 678 Name: "", 679 Expression: "true", 680 }}, 681 }, 682 }, true), 683 expectedError: `webhooks[0].matchConditions[0].name: Required value, webhooks[0].matchConditions[1].name: Required value, webhooks[1].matchConditions[0].name: Required value`, 684 }, { 685 name: "single match condition must have a qualified name", 686 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 687 Name: "webhook.k8s.io", 688 ClientConfig: validClientConfig, 689 SideEffects: &noSideEffect, 690 MatchConditions: []admissionregistration.MatchCondition{{ 691 Name: "-hello", 692 Expression: "true", 693 }}, 694 }, 695 }, true), 696 expectedError: `webhooks[0].matchConditions[0].name: Invalid value: "-hello": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')`, 697 }, { 698 name: "all match conditions must have qualified names", 699 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 700 Name: "webhook.k8s.io", 701 ClientConfig: validClientConfig, 702 SideEffects: &noSideEffect, 703 MatchConditions: []admissionregistration.MatchCondition{{ 704 Name: ".io", 705 Expression: "true", 706 }, { 707 Name: "thing.test.com", 708 Expression: "true", 709 }}, 710 }, { 711 Name: "webhook2.k8s.io", 712 ClientConfig: validClientConfig, 713 SideEffects: &noSideEffect, 714 MatchConditions: []admissionregistration.MatchCondition{{ 715 Name: "some name", 716 Expression: "true", 717 }}, 718 }, 719 }, true), 720 expectedError: `[webhooks[0].matchConditions[0].name: Invalid value: ".io": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]'), webhooks[1].matchConditions[0].name: Invalid value: "some name": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')]`, 721 }, { 722 name: "expression is required", 723 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 724 Name: "webhook.k8s.io", 725 ClientConfig: validClientConfig, 726 SideEffects: &noSideEffect, 727 MatchConditions: []admissionregistration.MatchCondition{{ 728 Name: "webhook.k8s.io", 729 }}, 730 }, 731 }, true), 732 expectedError: `webhooks[0].matchConditions[0].expression: Required value`, 733 }, { 734 name: "expression is required to have some value", 735 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 736 Name: "webhook.k8s.io", 737 ClientConfig: validClientConfig, 738 SideEffects: &noSideEffect, 739 MatchConditions: []admissionregistration.MatchCondition{{ 740 Name: "webhook.k8s.io", 741 Expression: "", 742 }}, 743 }, 744 }, true), 745 expectedError: `webhooks[0].matchConditions[0].expression: Required value`, 746 }, { 747 name: "invalid expression", 748 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 749 Name: "webhook.k8s.io", 750 ClientConfig: validClientConfig, 751 SideEffects: &noSideEffect, 752 MatchConditions: []admissionregistration.MatchCondition{{ 753 Name: "webhook.k8s.io", 754 Expression: "object.x in [1, 2, ", 755 }}, 756 }, 757 }, true), 758 expectedError: `webhooks[0].matchConditions[0].expression: Invalid value: "object.x in [1, 2,": compilation failed: ERROR: <input>:1:19: Syntax error: missing ']' at '<EOF>'`, 759 }, { 760 name: "unique names same hook", 761 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 762 Name: "webhook.k8s.io", 763 ClientConfig: validClientConfig, 764 SideEffects: &noSideEffect, 765 MatchConditions: []admissionregistration.MatchCondition{{ 766 Name: "webhook.k8s.io", 767 Expression: "true", 768 }, { 769 Name: "webhook.k8s.io", 770 Expression: "true", 771 }}, 772 }, 773 }, true), 774 expectedError: `matchConditions[1].name: Duplicate value: "webhook.k8s.io"`, 775 }, { 776 name: "repeat names allowed across different hooks", 777 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 778 Name: "webhook.k8s.io", 779 ClientConfig: validClientConfig, 780 SideEffects: &noSideEffect, 781 MatchConditions: []admissionregistration.MatchCondition{{ 782 Name: "webhook.k8s.io", 783 Expression: "true", 784 }}, 785 }, { 786 Name: "webhook2.k8s.io", 787 ClientConfig: validClientConfig, 788 SideEffects: &noSideEffect, 789 MatchConditions: []admissionregistration.MatchCondition{{ 790 Name: "webhook.k8s.io", 791 Expression: "true", 792 }}, 793 }, 794 }, true), 795 expectedError: ``, 796 }, { 797 name: "must evaluate to bool", 798 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 799 Name: "webhook.k8s.io", 800 ClientConfig: validClientConfig, 801 SideEffects: &noSideEffect, 802 MatchConditions: []admissionregistration.MatchCondition{{ 803 Name: "webhook.k8s.io", 804 Expression: "6", 805 }}, 806 }, 807 }, true), 808 expectedError: `webhooks[0].matchConditions[0].expression: Invalid value: "6": must evaluate to bool`, 809 }, { 810 name: "max of 64 match conditions", 811 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 812 Name: "webhook.k8s.io", 813 ClientConfig: validClientConfig, 814 SideEffects: &noSideEffect, 815 MatchConditions: get65MatchConditions(), 816 }, 817 }, true), 818 expectedError: `webhooks[0].matchConditions: Too many: 65: must have at most 64 items`, 819 }} 820 for _, test := range tests { 821 t.Run(test.name, func(t *testing.T) { 822 errs := ValidateValidatingWebhookConfiguration(test.config) 823 err := errs.ToAggregate() 824 if err != nil { 825 if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { 826 t.Errorf("expected to contain:\n %s\ngot:\n %s", e, a) 827 } 828 } else { 829 if test.expectedError != "" { 830 t.Errorf("unexpected no error, expected to contain:\n %s", test.expectedError) 831 } 832 } 833 }) 834 835 } 836 } 837 838 func TestValidateValidatingWebhookConfigurationUpdate(t *testing.T) { 839 noSideEffect := admissionregistration.SideEffectClassNone 840 unknownSideEffect := admissionregistration.SideEffectClassUnknown 841 validClientConfig := admissionregistration.WebhookClientConfig{ 842 URL: strPtr("https://example.com"), 843 } 844 tests := []struct { 845 name string 846 oldconfig *admissionregistration.ValidatingWebhookConfiguration 847 config *admissionregistration.ValidatingWebhookConfiguration 848 expectedError string 849 }{{ 850 name: "should pass on valid new AdmissionReviewVersion", 851 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 852 Name: "webhook.k8s.io", 853 ClientConfig: validClientConfig, 854 SideEffects: &unknownSideEffect, 855 AdmissionReviewVersions: []string{"v1beta1"}, 856 }, 857 }, true), 858 oldconfig: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 859 Name: "webhook.k8s.io", 860 ClientConfig: validClientConfig, 861 SideEffects: &unknownSideEffect, 862 }, 863 }, true), 864 expectedError: ``, 865 }, { 866 name: "should pass on invalid AdmissionReviewVersion with invalid previous versions", 867 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 868 Name: "webhook.k8s.io", 869 ClientConfig: validClientConfig, 870 SideEffects: &unknownSideEffect, 871 AdmissionReviewVersions: []string{"invalid-v1", "invalid-v2"}, 872 }, 873 }, true), 874 oldconfig: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 875 Name: "webhook.k8s.io", 876 ClientConfig: validClientConfig, 877 SideEffects: &unknownSideEffect, 878 AdmissionReviewVersions: []string{"invalid-v0"}, 879 }, 880 }, true), 881 expectedError: ``, 882 }, { 883 name: "should fail on invalid AdmissionReviewVersion with valid previous versions", 884 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 885 Name: "webhook.k8s.io", 886 ClientConfig: validClientConfig, 887 SideEffects: &unknownSideEffect, 888 AdmissionReviewVersions: []string{"invalid-v1"}, 889 }, 890 }, true), 891 oldconfig: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 892 Name: "webhook.k8s.io", 893 ClientConfig: validClientConfig, 894 SideEffects: &unknownSideEffect, 895 AdmissionReviewVersions: []string{"v1beta1", "invalid-v1"}, 896 }, 897 }, true), 898 expectedError: `Invalid value: []string{"invalid-v1"}`, 899 }, { 900 name: "should fail on invalid AdmissionReviewVersion with missing previous versions", 901 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 902 Name: "webhook.k8s.io", 903 ClientConfig: validClientConfig, 904 SideEffects: &unknownSideEffect, 905 AdmissionReviewVersions: []string{"invalid-v1"}, 906 }, 907 }, true), 908 oldconfig: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 909 Name: "webhook.k8s.io", 910 ClientConfig: validClientConfig, 911 SideEffects: &unknownSideEffect, 912 }, 913 }, false), 914 expectedError: `Invalid value: []string{"invalid-v1"}`, 915 }, { 916 name: "Webhooks must have unique names when old config has unique names", 917 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 918 Name: "webhook.k8s.io", 919 ClientConfig: validClientConfig, 920 SideEffects: &unknownSideEffect, 921 }, { 922 Name: "webhook.k8s.io", 923 ClientConfig: validClientConfig, 924 SideEffects: &unknownSideEffect, 925 }, 926 }, true), 927 oldconfig: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 928 Name: "webhook.k8s.io", 929 ClientConfig: validClientConfig, 930 SideEffects: &unknownSideEffect, 931 }, 932 }, false), 933 expectedError: `webhooks[1].name: Duplicate value: "webhook.k8s.io"`, 934 }, { 935 name: "Webhooks can have duplicate names when old config has duplicate names", 936 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 937 Name: "webhook.k8s.io", 938 ClientConfig: validClientConfig, 939 SideEffects: &unknownSideEffect, 940 }, { 941 Name: "webhook.k8s.io", 942 ClientConfig: validClientConfig, 943 SideEffects: &unknownSideEffect, 944 }, 945 }, true), 946 oldconfig: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 947 Name: "webhook.k8s.io", 948 ClientConfig: validClientConfig, 949 SideEffects: &unknownSideEffect, 950 }, { 951 Name: "webhook.k8s.io", 952 ClientConfig: validClientConfig, 953 SideEffects: &unknownSideEffect, 954 }, 955 }, true), 956 expectedError: ``, 957 }, { 958 name: "Webhooks must compile CEL expressions with StoredExpression environment if unchanged", 959 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 960 Name: "webhook.k8s.io", 961 ClientConfig: validClientConfig, 962 SideEffects: &noSideEffect, 963 MatchConditions: []admissionregistration.MatchCondition{{ 964 Name: "checkStorage", 965 Expression: "test() == true", 966 }}, 967 }, 968 }, true), 969 oldconfig: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 970 Name: "webhook.k8s.io", 971 ClientConfig: validClientConfig, 972 SideEffects: &noSideEffect, 973 MatchConditions: []admissionregistration.MatchCondition{{ 974 Name: "checkStorage", 975 Expression: "test() == true", 976 }}}, 977 }, true), 978 }, { 979 name: "Webhooks must compile CEL expressions with NewExpression environment type if changed", 980 config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 981 Name: "webhook.k8s.io", 982 ClientConfig: validClientConfig, 983 SideEffects: &noSideEffect, 984 MatchConditions: []admissionregistration.MatchCondition{{ 985 Name: "checkStorage", 986 Expression: "test() == true", 987 }}}, 988 }, true), 989 oldconfig: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{ 990 Name: "webhook.k8s.io", 991 ClientConfig: validClientConfig, 992 SideEffects: &noSideEffect, 993 MatchConditions: []admissionregistration.MatchCondition{{ 994 Name: "checkStorage", 995 Expression: "true", 996 }}}, 997 }, true), 998 expectedError: `undeclared reference to 'test'`, 999 }} 1000 for _, test := range tests { 1001 t.Run(test.name, func(t *testing.T) { 1002 errs := ValidateValidatingWebhookConfigurationUpdate(test.config, test.oldconfig) 1003 err := errs.ToAggregate() 1004 if err != nil { 1005 if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { 1006 t.Errorf("expected to contain:\n %s\ngot:\n %s", e, a) 1007 } 1008 } else { 1009 if test.expectedError != "" { 1010 t.Errorf("unexpected no error, expected to contain:\n %s", test.expectedError) 1011 } 1012 } 1013 }) 1014 1015 } 1016 } 1017 1018 func newMutatingWebhookConfiguration(hooks []admissionregistration.MutatingWebhook, defaultAdmissionReviewVersions bool) *admissionregistration.MutatingWebhookConfiguration { 1019 // If the test case did not specify an AdmissionReviewVersions, default it so the test passes as 1020 // this field will be defaulted in production code. 1021 for i := range hooks { 1022 if defaultAdmissionReviewVersions && len(hooks[i].AdmissionReviewVersions) == 0 { 1023 hooks[i].AdmissionReviewVersions = []string{"v1beta1"} 1024 } 1025 } 1026 return &admissionregistration.MutatingWebhookConfiguration{ 1027 ObjectMeta: metav1.ObjectMeta{ 1028 Name: "config", 1029 }, 1030 Webhooks: hooks, 1031 } 1032 } 1033 1034 func TestValidateMutatingWebhookConfiguration(t *testing.T) { 1035 noSideEffect := admissionregistration.SideEffectClassNone 1036 unknownSideEffect := admissionregistration.SideEffectClassUnknown 1037 validClientConfig := admissionregistration.WebhookClientConfig{ 1038 URL: strPtr("https://example.com"), 1039 } 1040 tests := []struct { 1041 name string 1042 config *admissionregistration.MutatingWebhookConfiguration 1043 expectedError string 1044 }{{ 1045 name: "AdmissionReviewVersions are required", 1046 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1047 Name: "webhook.k8s.io", 1048 ClientConfig: validClientConfig, 1049 SideEffects: &unknownSideEffect, 1050 }, 1051 }, false), 1052 expectedError: `webhooks[0].admissionReviewVersions: Required value: must specify one of v1, v1beta1`, 1053 }, { 1054 name: "should fail on bad AdmissionReviewVersion value", 1055 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1056 Name: "webhook.k8s.io", 1057 ClientConfig: validClientConfig, 1058 AdmissionReviewVersions: []string{"0v"}, 1059 }, 1060 }, true), 1061 expectedError: `Invalid value: "0v": a DNS-1035 label`, 1062 }, { 1063 name: "should pass on valid AdmissionReviewVersion", 1064 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1065 Name: "webhook.k8s.io", 1066 ClientConfig: validClientConfig, 1067 SideEffects: &noSideEffect, 1068 AdmissionReviewVersions: []string{"v1beta1"}, 1069 }, 1070 }, true), 1071 expectedError: ``, 1072 }, { 1073 name: "should pass on mix of accepted and unaccepted AdmissionReviewVersion", 1074 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1075 Name: "webhook.k8s.io", 1076 ClientConfig: validClientConfig, 1077 SideEffects: &noSideEffect, 1078 AdmissionReviewVersions: []string{"v1beta1", "invalid-version"}, 1079 }, 1080 }, true), 1081 expectedError: ``, 1082 }, { 1083 name: "should fail on invalid AdmissionReviewVersion", 1084 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1085 Name: "webhook.k8s.io", 1086 ClientConfig: validClientConfig, 1087 AdmissionReviewVersions: []string{"invalidVersion"}, 1088 }, 1089 }, true), 1090 expectedError: `Invalid value: []string{"invalidVersion"}`, 1091 }, { 1092 name: "should fail on duplicate AdmissionReviewVersion", 1093 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1094 Name: "webhook.k8s.io", 1095 ClientConfig: validClientConfig, 1096 AdmissionReviewVersions: []string{"v1beta1", "v1beta1"}, 1097 }, 1098 }, true), 1099 expectedError: `Invalid value: "v1beta1": duplicate version`, 1100 }, { 1101 name: "all Webhooks must have a fully qualified name", 1102 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1103 Name: "webhook.k8s.io", 1104 ClientConfig: validClientConfig, 1105 SideEffects: &noSideEffect, 1106 }, { 1107 Name: "k8s.io", 1108 ClientConfig: validClientConfig, 1109 SideEffects: &noSideEffect, 1110 }, { 1111 Name: "", 1112 ClientConfig: validClientConfig, 1113 SideEffects: &noSideEffect, 1114 }, 1115 }, true), 1116 expectedError: `webhooks[1].name: Invalid value: "k8s.io": should be a domain with at least three segments separated by dots, webhooks[2].name: Required value`, 1117 }, { 1118 name: "Webhooks must have unique names when created", 1119 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1120 Name: "webhook.k8s.io", 1121 ClientConfig: validClientConfig, 1122 SideEffects: &unknownSideEffect, 1123 }, { 1124 Name: "webhook.k8s.io", 1125 ClientConfig: validClientConfig, 1126 SideEffects: &unknownSideEffect, 1127 }, 1128 }, true), 1129 expectedError: `webhooks[1].name: Duplicate value: "webhook.k8s.io"`, 1130 }, { 1131 name: "Operations must not be empty or nil", 1132 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1133 Name: "webhook.k8s.io", 1134 Rules: []admissionregistration.RuleWithOperations{{ 1135 Operations: []admissionregistration.OperationType{}, 1136 Rule: admissionregistration.Rule{ 1137 APIGroups: []string{"a"}, 1138 APIVersions: []string{"a"}, 1139 Resources: []string{"a"}, 1140 }, 1141 }, { 1142 Operations: nil, 1143 Rule: admissionregistration.Rule{ 1144 APIGroups: []string{"a"}, 1145 APIVersions: []string{"a"}, 1146 Resources: []string{"a"}, 1147 }, 1148 }}, 1149 }, 1150 }, true), 1151 expectedError: `webhooks[0].rules[0].operations: Required value, webhooks[0].rules[1].operations: Required value`, 1152 }, { 1153 name: "\"\" is NOT a valid operation", 1154 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1155 Name: "webhook.k8s.io", 1156 Rules: []admissionregistration.RuleWithOperations{{ 1157 Operations: []admissionregistration.OperationType{"CREATE", ""}, 1158 Rule: admissionregistration.Rule{ 1159 APIGroups: []string{"a"}, 1160 APIVersions: []string{"a"}, 1161 Resources: []string{"a"}, 1162 }, 1163 }}, 1164 }, 1165 }, true), 1166 expectedError: `Unsupported value: ""`, 1167 }, { 1168 name: "operation must be either create/update/delete/connect", 1169 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1170 Name: "webhook.k8s.io", 1171 Rules: []admissionregistration.RuleWithOperations{{ 1172 Operations: []admissionregistration.OperationType{"PATCH"}, 1173 Rule: admissionregistration.Rule{ 1174 APIGroups: []string{"a"}, 1175 APIVersions: []string{"a"}, 1176 Resources: []string{"a"}, 1177 }, 1178 }}, 1179 }, 1180 }, true), 1181 expectedError: `Unsupported value: "PATCH"`, 1182 }, { 1183 name: "wildcard operation cannot be mixed with other strings", 1184 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1185 Name: "webhook.k8s.io", 1186 Rules: []admissionregistration.RuleWithOperations{{ 1187 Operations: []admissionregistration.OperationType{"CREATE", "*"}, 1188 Rule: admissionregistration.Rule{ 1189 APIGroups: []string{"a"}, 1190 APIVersions: []string{"a"}, 1191 Resources: []string{"a"}, 1192 }, 1193 }}, 1194 }, 1195 }, true), 1196 expectedError: `if '*' is present, must not specify other operations`, 1197 }, { 1198 name: `resource "*" can co-exist with resources that have subresources`, 1199 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1200 Name: "webhook.k8s.io", 1201 ClientConfig: validClientConfig, 1202 SideEffects: &noSideEffect, 1203 Rules: []admissionregistration.RuleWithOperations{{ 1204 Operations: []admissionregistration.OperationType{"CREATE"}, 1205 Rule: admissionregistration.Rule{ 1206 APIGroups: []string{"a"}, 1207 APIVersions: []string{"a"}, 1208 Resources: []string{"*", "a/b", "a/*", "*/b"}, 1209 }, 1210 }}, 1211 }, 1212 }, true), 1213 }, { 1214 name: `resource "*" cannot mix with resources that don't have subresources`, 1215 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1216 Name: "webhook.k8s.io", 1217 ClientConfig: validClientConfig, 1218 SideEffects: &unknownSideEffect, 1219 Rules: []admissionregistration.RuleWithOperations{{ 1220 Operations: []admissionregistration.OperationType{"CREATE"}, 1221 Rule: admissionregistration.Rule{ 1222 APIGroups: []string{"a"}, 1223 APIVersions: []string{"a"}, 1224 Resources: []string{"*", "a"}, 1225 }, 1226 }}, 1227 }, 1228 }, true), 1229 expectedError: `if '*' is present, must not specify other resources without subresources`, 1230 }, { 1231 name: "resource a/* cannot mix with a/x", 1232 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1233 Name: "webhook.k8s.io", 1234 ClientConfig: validClientConfig, 1235 SideEffects: &unknownSideEffect, 1236 Rules: []admissionregistration.RuleWithOperations{{ 1237 Operations: []admissionregistration.OperationType{"CREATE"}, 1238 Rule: admissionregistration.Rule{ 1239 APIGroups: []string{"a"}, 1240 APIVersions: []string{"a"}, 1241 Resources: []string{"a/*", "a/x"}, 1242 }, 1243 }}, 1244 }, 1245 }, true), 1246 expectedError: `webhooks[0].rules[0].resources[1]: Invalid value: "a/x": if 'a/*' is present, must not specify a/x`, 1247 }, { 1248 name: "resource a/* can mix with a", 1249 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1250 Name: "webhook.k8s.io", 1251 ClientConfig: validClientConfig, 1252 SideEffects: &noSideEffect, 1253 Rules: []admissionregistration.RuleWithOperations{{ 1254 Operations: []admissionregistration.OperationType{"CREATE"}, 1255 Rule: admissionregistration.Rule{ 1256 APIGroups: []string{"a"}, 1257 APIVersions: []string{"a"}, 1258 Resources: []string{"a/*", "a"}, 1259 }, 1260 }}, 1261 }, 1262 }, true), 1263 }, { 1264 name: "resource */a cannot mix with x/a", 1265 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1266 Name: "webhook.k8s.io", 1267 ClientConfig: validClientConfig, 1268 SideEffects: &unknownSideEffect, 1269 Rules: []admissionregistration.RuleWithOperations{{ 1270 Operations: []admissionregistration.OperationType{"CREATE"}, 1271 Rule: admissionregistration.Rule{ 1272 APIGroups: []string{"a"}, 1273 APIVersions: []string{"a"}, 1274 Resources: []string{"*/a", "x/a"}, 1275 }, 1276 }}, 1277 }, 1278 }, true), 1279 expectedError: `webhooks[0].rules[0].resources[1]: Invalid value: "x/a": if '*/a' is present, must not specify x/a`, 1280 }, { 1281 name: "resource */* cannot mix with other resources", 1282 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1283 Name: "webhook.k8s.io", 1284 ClientConfig: validClientConfig, 1285 SideEffects: &unknownSideEffect, 1286 Rules: []admissionregistration.RuleWithOperations{{ 1287 Operations: []admissionregistration.OperationType{"CREATE"}, 1288 Rule: admissionregistration.Rule{ 1289 APIGroups: []string{"a"}, 1290 APIVersions: []string{"a"}, 1291 Resources: []string{"*/*", "a"}, 1292 }, 1293 }}, 1294 }, 1295 }, true), 1296 expectedError: `webhooks[0].rules[0].resources: Invalid value: []string{"*/*", "a"}: if '*/*' is present, must not specify other resources`, 1297 }, { 1298 name: "FailurePolicy can only be \"Ignore\" or \"Fail\"", 1299 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1300 Name: "webhook.k8s.io", 1301 ClientConfig: validClientConfig, 1302 SideEffects: &unknownSideEffect, 1303 FailurePolicy: func() *admissionregistration.FailurePolicyType { 1304 r := admissionregistration.FailurePolicyType("other") 1305 return &r 1306 }(), 1307 }, 1308 }, true), 1309 expectedError: `webhooks[0].failurePolicy: Unsupported value: "other": supported values: "Fail", "Ignore"`, 1310 }, { 1311 name: "AdmissionReviewVersions are required", 1312 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1313 Name: "webhook.k8s.io", 1314 ClientConfig: validClientConfig, 1315 SideEffects: &unknownSideEffect, 1316 }, 1317 }, false), 1318 expectedError: `webhooks[0].admissionReviewVersions: Required value: must specify one of v1, v1beta1`, 1319 }, { 1320 name: "SideEffects are required", 1321 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1322 Name: "webhook.k8s.io", 1323 ClientConfig: validClientConfig, 1324 SideEffects: nil, 1325 }, 1326 }, true), 1327 expectedError: `webhooks[0].sideEffects: Required value: must specify one of None, NoneOnDryRun`, 1328 }, { 1329 name: "SideEffects can only be \"None\" or \"NoneOnDryRun\" when created", 1330 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1331 Name: "webhook.k8s.io", 1332 ClientConfig: validClientConfig, 1333 SideEffects: func() *admissionregistration.SideEffectClass { 1334 r := admissionregistration.SideEffectClass("other") 1335 return &r 1336 }(), 1337 }, 1338 }, true), 1339 expectedError: `webhooks[0].sideEffects: Unsupported value: "other": supported values: "None", "NoneOnDryRun"`, 1340 }, { 1341 name: "both service and URL missing", 1342 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1343 Name: "webhook.k8s.io", 1344 ClientConfig: admissionregistration.WebhookClientConfig{}, 1345 }, 1346 }, true), 1347 expectedError: `exactly one of`, 1348 }, { 1349 name: "both service and URL provided", 1350 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1351 Name: "webhook.k8s.io", 1352 ClientConfig: admissionregistration.WebhookClientConfig{ 1353 Service: &admissionregistration.ServiceReference{ 1354 Namespace: "ns", 1355 Name: "n", 1356 Port: 443, 1357 }, 1358 URL: strPtr("example.com/k8s/webhook"), 1359 }, 1360 }, 1361 }, true), 1362 expectedError: `[0].clientConfig: Required value: exactly one of url or service is required`, 1363 }, { 1364 name: "blank URL", 1365 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1366 Name: "webhook.k8s.io", 1367 ClientConfig: admissionregistration.WebhookClientConfig{ 1368 URL: strPtr(""), 1369 }, 1370 }, 1371 }, true), 1372 expectedError: `[0].clientConfig.url: Invalid value: "": host must be specified`, 1373 }, { 1374 name: "wrong scheme", 1375 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1376 Name: "webhook.k8s.io", 1377 ClientConfig: admissionregistration.WebhookClientConfig{ 1378 URL: strPtr("http://example.com"), 1379 }, 1380 }, 1381 }, true), 1382 expectedError: `https`, 1383 }, { 1384 name: "missing host", 1385 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1386 Name: "webhook.k8s.io", 1387 ClientConfig: admissionregistration.WebhookClientConfig{ 1388 URL: strPtr("https:///fancy/webhook"), 1389 }, 1390 }, 1391 }, true), 1392 expectedError: `host must be specified`, 1393 }, { 1394 name: "fragment", 1395 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1396 Name: "webhook.k8s.io", 1397 ClientConfig: admissionregistration.WebhookClientConfig{ 1398 URL: strPtr("https://example.com/#bookmark"), 1399 }, 1400 }, 1401 }, true), 1402 expectedError: `"bookmark": fragments are not permitted`, 1403 }, { 1404 name: "query", 1405 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1406 Name: "webhook.k8s.io", 1407 ClientConfig: admissionregistration.WebhookClientConfig{ 1408 URL: strPtr("https://example.com?arg=value"), 1409 }, 1410 }, 1411 }, true), 1412 expectedError: `"arg=value": query parameters are not permitted`, 1413 }, { 1414 name: "user", 1415 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1416 Name: "webhook.k8s.io", 1417 ClientConfig: admissionregistration.WebhookClientConfig{ 1418 URL: strPtr("https://harry.potter@example.com/"), 1419 }, 1420 }, 1421 }, true), 1422 expectedError: `"harry.potter": user information is not permitted`, 1423 }, { 1424 name: "just totally wrong", 1425 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1426 Name: "webhook.k8s.io", 1427 ClientConfig: admissionregistration.WebhookClientConfig{ 1428 URL: strPtr("arg#backwards=thisis?html.index/port:host//:https"), 1429 }, 1430 }, 1431 }, true), 1432 expectedError: `host must be specified`, 1433 }, { 1434 name: "path must start with slash", 1435 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1436 Name: "webhook.k8s.io", 1437 ClientConfig: admissionregistration.WebhookClientConfig{ 1438 Service: &admissionregistration.ServiceReference{ 1439 Namespace: "ns", 1440 Name: "n", 1441 Path: strPtr("foo/"), 1442 Port: 443, 1443 }, 1444 }, 1445 }, 1446 }, true), 1447 expectedError: `clientConfig.service.path: Invalid value: "foo/": must start with a '/'`, 1448 }, { 1449 name: "path accepts slash", 1450 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1451 Name: "webhook.k8s.io", 1452 ClientConfig: admissionregistration.WebhookClientConfig{ 1453 Service: &admissionregistration.ServiceReference{ 1454 Namespace: "ns", 1455 Name: "n", 1456 Path: strPtr("/"), 1457 Port: 443, 1458 }, 1459 }, 1460 SideEffects: &noSideEffect, 1461 }, 1462 }, true), 1463 expectedError: ``, 1464 }, { 1465 name: "path accepts no trailing slash", 1466 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1467 Name: "webhook.k8s.io", 1468 ClientConfig: admissionregistration.WebhookClientConfig{ 1469 Service: &admissionregistration.ServiceReference{ 1470 Namespace: "ns", 1471 Name: "n", 1472 Path: strPtr("/foo"), 1473 Port: 443, 1474 }, 1475 }, 1476 SideEffects: &noSideEffect, 1477 }, 1478 }, true), 1479 expectedError: ``, 1480 }, { 1481 name: "path fails //", 1482 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1483 Name: "webhook.k8s.io", 1484 ClientConfig: admissionregistration.WebhookClientConfig{ 1485 Service: &admissionregistration.ServiceReference{ 1486 Namespace: "ns", 1487 Name: "n", 1488 Path: strPtr("//"), 1489 Port: 443, 1490 }, 1491 }, 1492 SideEffects: &noSideEffect, 1493 }, 1494 }, true), 1495 expectedError: `clientConfig.service.path: Invalid value: "//": segment[0] may not be empty`, 1496 }, { 1497 name: "path no empty step", 1498 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1499 Name: "webhook.k8s.io", 1500 ClientConfig: admissionregistration.WebhookClientConfig{ 1501 Service: &admissionregistration.ServiceReference{ 1502 Namespace: "ns", 1503 Name: "n", 1504 Path: strPtr("/foo//bar/"), 1505 Port: 443, 1506 }, 1507 }, 1508 SideEffects: &unknownSideEffect, 1509 }, 1510 }, true), 1511 expectedError: `clientConfig.service.path: Invalid value: "/foo//bar/": segment[1] may not be empty`, 1512 }, { 1513 name: "path no empty step 2", 1514 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1515 Name: "webhook.k8s.io", 1516 ClientConfig: admissionregistration.WebhookClientConfig{ 1517 Service: &admissionregistration.ServiceReference{ 1518 Namespace: "ns", 1519 Name: "n", 1520 Path: strPtr("/foo/bar//"), 1521 Port: 443, 1522 }, 1523 }, 1524 SideEffects: &unknownSideEffect, 1525 }, 1526 }, true), 1527 expectedError: `clientConfig.service.path: Invalid value: "/foo/bar//": segment[2] may not be empty`, 1528 }, { 1529 name: "path no non-subdomain", 1530 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1531 Name: "webhook.k8s.io", 1532 ClientConfig: admissionregistration.WebhookClientConfig{ 1533 Service: &admissionregistration.ServiceReference{ 1534 Namespace: "ns", 1535 Name: "n", 1536 Path: strPtr("/apis/foo.bar/v1alpha1/--bad"), 1537 Port: 443, 1538 }, 1539 }, 1540 SideEffects: &unknownSideEffect, 1541 }, 1542 }, true), 1543 expectedError: `clientConfig.service.path: Invalid value: "/apis/foo.bar/v1alpha1/--bad": segment[3]: a lowercase RFC 1123 subdomain`, 1544 }, { 1545 name: "invalid port 0", 1546 config: newMutatingWebhookConfiguration( 1547 []admissionregistration.MutatingWebhook{{ 1548 Name: "webhook.k8s.io", 1549 ClientConfig: admissionregistration.WebhookClientConfig{ 1550 Service: &admissionregistration.ServiceReference{ 1551 Namespace: "ns", 1552 Name: "n", 1553 Path: strPtr("https://apis/foo.bar"), 1554 Port: 0, 1555 }, 1556 }, 1557 SideEffects: &unknownSideEffect, 1558 }, 1559 }, true), 1560 expectedError: `Invalid value: 0: port is not valid: must be between 1 and 65535, inclusive`, 1561 }, { 1562 name: "invalid port >65535", 1563 config: newMutatingWebhookConfiguration( 1564 []admissionregistration.MutatingWebhook{{ 1565 Name: "webhook.k8s.io", 1566 ClientConfig: admissionregistration.WebhookClientConfig{ 1567 Service: &admissionregistration.ServiceReference{ 1568 Namespace: "ns", 1569 Name: "n", 1570 Path: strPtr("https://apis/foo.bar"), 1571 Port: 65536, 1572 }, 1573 }, 1574 SideEffects: &unknownSideEffect, 1575 }, 1576 }, true), 1577 expectedError: `Invalid value: 65536: port is not valid: must be between 1 and 65535, inclusive`, 1578 }, { 1579 name: "timeout seconds cannot be greater than 30", 1580 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1581 Name: "webhook.k8s.io", 1582 ClientConfig: validClientConfig, 1583 SideEffects: &unknownSideEffect, 1584 TimeoutSeconds: int32Ptr(31), 1585 }, 1586 }, true), 1587 expectedError: `webhooks[0].timeoutSeconds: Invalid value: 31: the timeout value must be between 1 and 30 seconds`, 1588 }, { 1589 name: "timeout seconds cannot be smaller than 1", 1590 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1591 Name: "webhook.k8s.io", 1592 ClientConfig: validClientConfig, 1593 SideEffects: &unknownSideEffect, 1594 TimeoutSeconds: int32Ptr(0), 1595 }, 1596 }, true), 1597 expectedError: `webhooks[0].timeoutSeconds: Invalid value: 0: the timeout value must be between 1 and 30 seconds`, 1598 }, { 1599 name: "timeout seconds must be positive", 1600 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1601 Name: "webhook.k8s.io", 1602 ClientConfig: validClientConfig, 1603 SideEffects: &unknownSideEffect, 1604 TimeoutSeconds: int32Ptr(-1), 1605 }, 1606 }, true), 1607 expectedError: `webhooks[0].timeoutSeconds: Invalid value: -1: the timeout value must be between 1 and 30 seconds`, 1608 }, { 1609 name: "valid timeout seconds", 1610 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1611 Name: "webhook.k8s.io", 1612 ClientConfig: validClientConfig, 1613 SideEffects: &noSideEffect, 1614 TimeoutSeconds: int32Ptr(1), 1615 }, { 1616 Name: "webhook2.k8s.io", 1617 ClientConfig: validClientConfig, 1618 SideEffects: &noSideEffect, 1619 TimeoutSeconds: int32Ptr(15), 1620 }, { 1621 Name: "webhook3.k8s.io", 1622 ClientConfig: validClientConfig, 1623 SideEffects: &noSideEffect, 1624 TimeoutSeconds: int32Ptr(30), 1625 }, 1626 }, true), 1627 }, { 1628 name: "single match condition must have a name", 1629 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1630 Name: "webhook.k8s.io", 1631 ClientConfig: validClientConfig, 1632 SideEffects: &noSideEffect, 1633 MatchConditions: []admissionregistration.MatchCondition{{ 1634 Expression: "true", 1635 }}, 1636 }, 1637 }, true), 1638 expectedError: `webhooks[0].matchConditions[0].name: Required value`, 1639 }, { 1640 name: "all match conditions must have a name", 1641 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1642 Name: "webhook.k8s.io", 1643 ClientConfig: validClientConfig, 1644 SideEffects: &noSideEffect, 1645 MatchConditions: []admissionregistration.MatchCondition{{ 1646 Expression: "true", 1647 }, { 1648 Expression: "true", 1649 }}, 1650 }, { 1651 Name: "webhook.k8s.io", 1652 ClientConfig: validClientConfig, 1653 SideEffects: &noSideEffect, 1654 MatchConditions: []admissionregistration.MatchCondition{{ 1655 Name: "", 1656 Expression: "true", 1657 }}, 1658 }, 1659 }, true), 1660 expectedError: `webhooks[0].matchConditions[0].name: Required value, webhooks[0].matchConditions[1].name: Required value, webhooks[1].matchConditions[0].name: Required value`, 1661 }, { 1662 name: "single match condition must have a qualified name", 1663 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1664 Name: "webhook.k8s.io", 1665 ClientConfig: validClientConfig, 1666 SideEffects: &noSideEffect, 1667 MatchConditions: []admissionregistration.MatchCondition{{ 1668 Name: "-hello", 1669 Expression: "true", 1670 }}, 1671 }, 1672 }, true), 1673 expectedError: `webhooks[0].matchConditions[0].name: Invalid value: "-hello": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')`, 1674 }, { 1675 name: "all match conditions must have qualified names", 1676 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1677 Name: "webhook.k8s.io", 1678 ClientConfig: validClientConfig, 1679 SideEffects: &noSideEffect, 1680 MatchConditions: []admissionregistration.MatchCondition{{ 1681 Name: ".io", 1682 Expression: "true", 1683 }, { 1684 Name: "thing.test.com", 1685 Expression: "true", 1686 }}, 1687 }, { 1688 Name: "webhook2.k8s.io", 1689 ClientConfig: validClientConfig, 1690 SideEffects: &noSideEffect, 1691 MatchConditions: []admissionregistration.MatchCondition{{ 1692 Name: "some name", 1693 Expression: "true", 1694 }}, 1695 }, 1696 }, true), 1697 expectedError: `[webhooks[0].matchConditions[0].name: Invalid value: ".io": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]'), webhooks[1].matchConditions[0].name: Invalid value: "some name": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')]`, 1698 }, { 1699 name: "expression is required", 1700 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1701 Name: "webhook.k8s.io", 1702 ClientConfig: validClientConfig, 1703 SideEffects: &noSideEffect, 1704 MatchConditions: []admissionregistration.MatchCondition{{ 1705 Name: "webhook.k8s.io", 1706 }}, 1707 }, 1708 }, true), 1709 expectedError: `webhooks[0].matchConditions[0].expression: Required value`, 1710 }, { 1711 name: "expression is required to have some value", 1712 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1713 Name: "webhook.k8s.io", 1714 ClientConfig: validClientConfig, 1715 SideEffects: &noSideEffect, 1716 MatchConditions: []admissionregistration.MatchCondition{{ 1717 Name: "webhook.k8s.io", 1718 Expression: "", 1719 }}, 1720 }, 1721 }, true), 1722 expectedError: `webhooks[0].matchConditions[0].expression: Required value`, 1723 }, { 1724 name: "invalid expression", 1725 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1726 Name: "webhook.k8s.io", 1727 ClientConfig: validClientConfig, 1728 SideEffects: &noSideEffect, 1729 MatchConditions: []admissionregistration.MatchCondition{{ 1730 Name: "webhook.k8s.io", 1731 Expression: "object.x in [1, 2, ", 1732 }}, 1733 }, 1734 }, true), 1735 expectedError: `webhooks[0].matchConditions[0].expression: Invalid value: "object.x in [1, 2,": compilation failed: ERROR: <input>:1:19: Syntax error: missing ']' at '<EOF>'`, 1736 }, { 1737 name: "unique names same hook", 1738 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1739 Name: "webhook.k8s.io", 1740 ClientConfig: validClientConfig, 1741 SideEffects: &noSideEffect, 1742 MatchConditions: []admissionregistration.MatchCondition{{ 1743 Name: "webhook.k8s.io", 1744 Expression: "true", 1745 }, { 1746 Name: "webhook.k8s.io", 1747 Expression: "true", 1748 }}, 1749 }, 1750 }, true), 1751 expectedError: `matchConditions[1].name: Duplicate value: "webhook.k8s.io"`, 1752 }, { 1753 name: "repeat names allowed across different hooks", 1754 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1755 Name: "webhook.k8s.io", 1756 ClientConfig: validClientConfig, 1757 SideEffects: &noSideEffect, 1758 MatchConditions: []admissionregistration.MatchCondition{{ 1759 Name: "webhook.k8s.io", 1760 Expression: "true", 1761 }}, 1762 }, { 1763 Name: "webhook2.k8s.io", 1764 ClientConfig: validClientConfig, 1765 SideEffects: &noSideEffect, 1766 MatchConditions: []admissionregistration.MatchCondition{{ 1767 Name: "webhook.k8s.io", 1768 Expression: "true", 1769 }}, 1770 }, 1771 }, true), 1772 expectedError: ``, 1773 }, { 1774 name: "must evaluate to bool", 1775 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1776 Name: "webhook.k8s.io", 1777 ClientConfig: validClientConfig, 1778 SideEffects: &noSideEffect, 1779 MatchConditions: []admissionregistration.MatchCondition{{ 1780 Name: "webhook.k8s.io", 1781 Expression: "6", 1782 }}, 1783 }, 1784 }, true), 1785 expectedError: `webhooks[0].matchConditions[0].expression: Invalid value: "6": must evaluate to bool`, 1786 }, { 1787 name: "max of 64 match conditions", 1788 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1789 Name: "webhook.k8s.io", 1790 ClientConfig: validClientConfig, 1791 SideEffects: &noSideEffect, 1792 MatchConditions: get65MatchConditions(), 1793 }, 1794 }, true), 1795 expectedError: `webhooks[0].matchConditions: Too many: 65: must have at most 64 items`, 1796 }} 1797 for _, test := range tests { 1798 t.Run(test.name, func(t *testing.T) { 1799 errs := ValidateMutatingWebhookConfiguration(test.config) 1800 err := errs.ToAggregate() 1801 if err != nil { 1802 if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { 1803 t.Errorf("expected to contain:\n %s\ngot:\n %s", e, a) 1804 } 1805 } else { 1806 if test.expectedError != "" { 1807 t.Errorf("unexpected no error, expected to contain:\n %s", test.expectedError) 1808 } 1809 } 1810 }) 1811 1812 } 1813 } 1814 1815 func TestValidateMutatingWebhookConfigurationUpdate(t *testing.T) { 1816 unknownSideEffect := admissionregistration.SideEffectClassUnknown 1817 noSideEffect := admissionregistration.SideEffectClassNone 1818 validClientConfig := admissionregistration.WebhookClientConfig{ 1819 URL: strPtr("https://example.com"), 1820 } 1821 tests := []struct { 1822 name string 1823 oldconfig *admissionregistration.MutatingWebhookConfiguration 1824 config *admissionregistration.MutatingWebhookConfiguration 1825 expectedError string 1826 }{{ 1827 name: "should pass on valid new AdmissionReviewVersion (v1beta1)", 1828 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1829 Name: "webhook.k8s.io", 1830 ClientConfig: validClientConfig, 1831 SideEffects: &unknownSideEffect, 1832 AdmissionReviewVersions: []string{"v1beta1"}, 1833 }, 1834 }, true), 1835 oldconfig: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1836 Name: "webhook.k8s.io", 1837 ClientConfig: validClientConfig, 1838 SideEffects: &unknownSideEffect, 1839 }, 1840 }, true), 1841 expectedError: ``, 1842 }, { 1843 name: "should pass on valid new AdmissionReviewVersion (v1)", 1844 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1845 Name: "webhook.k8s.io", 1846 ClientConfig: validClientConfig, 1847 SideEffects: &unknownSideEffect, 1848 AdmissionReviewVersions: []string{"v1"}, 1849 }, 1850 }, true), 1851 oldconfig: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1852 Name: "webhook.k8s.io", 1853 ClientConfig: validClientConfig, 1854 SideEffects: &unknownSideEffect, 1855 }, 1856 }, true), 1857 expectedError: ``, 1858 }, { 1859 name: "should pass on invalid AdmissionReviewVersion with invalid previous versions", 1860 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1861 Name: "webhook.k8s.io", 1862 ClientConfig: validClientConfig, 1863 SideEffects: &unknownSideEffect, 1864 AdmissionReviewVersions: []string{"invalid-v1", "invalid-v2"}, 1865 }, 1866 }, true), 1867 oldconfig: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1868 Name: "webhook.k8s.io", 1869 ClientConfig: validClientConfig, 1870 SideEffects: &unknownSideEffect, 1871 AdmissionReviewVersions: []string{"invalid-v0"}, 1872 }, 1873 }, true), 1874 expectedError: ``, 1875 }, { 1876 name: "should fail on invalid AdmissionReviewVersion with valid previous versions", 1877 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1878 Name: "webhook.k8s.io", 1879 ClientConfig: validClientConfig, 1880 SideEffects: &unknownSideEffect, 1881 AdmissionReviewVersions: []string{"invalid-v1"}, 1882 }, 1883 }, true), 1884 oldconfig: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1885 Name: "webhook.k8s.io", 1886 ClientConfig: validClientConfig, 1887 SideEffects: &unknownSideEffect, 1888 AdmissionReviewVersions: []string{"v1beta1", "invalid-v1"}, 1889 }, 1890 }, true), 1891 expectedError: `Invalid value: []string{"invalid-v1"}`, 1892 }, { 1893 name: "should fail on invalid AdmissionReviewVersion with missing previous versions", 1894 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1895 Name: "webhook.k8s.io", 1896 ClientConfig: validClientConfig, 1897 SideEffects: &unknownSideEffect, 1898 AdmissionReviewVersions: []string{"invalid-v1"}, 1899 }, 1900 }, true), 1901 oldconfig: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1902 Name: "webhook.k8s.io", 1903 ClientConfig: validClientConfig, 1904 SideEffects: &unknownSideEffect, 1905 }, 1906 }, false), 1907 expectedError: `Invalid value: []string{"invalid-v1"}`, 1908 }, { 1909 name: "Webhooks can have duplicate names when old config has duplicate names", 1910 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1911 Name: "webhook.k8s.io", 1912 ClientConfig: validClientConfig, 1913 SideEffects: &unknownSideEffect, 1914 }, { 1915 Name: "webhook.k8s.io", 1916 ClientConfig: validClientConfig, 1917 SideEffects: &unknownSideEffect, 1918 }, 1919 }, true), 1920 oldconfig: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1921 Name: "webhook.k8s.io", 1922 ClientConfig: validClientConfig, 1923 SideEffects: &unknownSideEffect, 1924 }, { 1925 Name: "webhook.k8s.io", 1926 ClientConfig: validClientConfig, 1927 SideEffects: &unknownSideEffect, 1928 }, 1929 }, true), 1930 expectedError: ``, 1931 }, { 1932 name: "Webhooks can't have side effects when old config has no side effects", 1933 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1934 Name: "webhook.k8s.io", 1935 ClientConfig: validClientConfig, 1936 SideEffects: &unknownSideEffect, 1937 }, 1938 }, true), 1939 oldconfig: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1940 Name: "webhook.k8s.io", 1941 ClientConfig: validClientConfig, 1942 SideEffects: &noSideEffect, 1943 }, 1944 }, true), 1945 expectedError: `Unsupported value: "Unknown": supported values: "None", "NoneOnDryRun"`, 1946 }, { 1947 name: "Webhooks can have side effects when old config has side effects", 1948 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1949 Name: "webhook.k8s.io", 1950 ClientConfig: validClientConfig, 1951 SideEffects: &unknownSideEffect, 1952 }, 1953 }, true), 1954 oldconfig: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1955 Name: "webhook.k8s.io", 1956 ClientConfig: validClientConfig, 1957 SideEffects: &unknownSideEffect, 1958 }, 1959 }, true), 1960 expectedError: ``, 1961 }, { 1962 name: "Webhooks must compile CEL expressions with StoredExpression environment if unchanged", 1963 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1964 Name: "webhook.k8s.io", 1965 ClientConfig: validClientConfig, 1966 SideEffects: &noSideEffect, 1967 MatchConditions: []admissionregistration.MatchCondition{{ 1968 Name: "checkStorage", 1969 Expression: "test() == true", 1970 }}, 1971 }, 1972 }, true), 1973 oldconfig: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1974 Name: "webhook.k8s.io", 1975 ClientConfig: validClientConfig, 1976 SideEffects: &noSideEffect, 1977 MatchConditions: []admissionregistration.MatchCondition{{ 1978 Name: "checkStorage", 1979 Expression: "test() == true", 1980 }}, 1981 }, 1982 }, true), 1983 }, 1984 { 1985 name: "Webhooks must compile CEL expressions with NewExpression environment if changed", 1986 config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1987 Name: "webhook.k8s.io", 1988 ClientConfig: validClientConfig, 1989 SideEffects: &noSideEffect, 1990 MatchConditions: []admissionregistration.MatchCondition{{ 1991 Name: "checkStorage", 1992 Expression: "test() == true", 1993 }, 1994 }}, 1995 }, true), 1996 oldconfig: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{ 1997 Name: "webhook.k8s.io", 1998 ClientConfig: validClientConfig, 1999 SideEffects: &noSideEffect, 2000 MatchConditions: []admissionregistration.MatchCondition{{ 2001 Name: "checkStorage", 2002 Expression: "true", 2003 }, 2004 }}, 2005 }, true), 2006 expectedError: `undeclared reference to 'test'`, 2007 }} 2008 for _, test := range tests { 2009 t.Run(test.name, func(t *testing.T) { 2010 errs := ValidateMutatingWebhookConfigurationUpdate(test.config, test.oldconfig) 2011 err := errs.ToAggregate() 2012 if err != nil { 2013 if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { 2014 t.Errorf("expected to contain:\n %s\ngot:\n %s", e, a) 2015 } 2016 } else { 2017 if test.expectedError != "" { 2018 t.Errorf("unexpected no error, expected to contain:\n %s", test.expectedError) 2019 } 2020 } 2021 }) 2022 2023 } 2024 } 2025 2026 func TestValidateValidatingAdmissionPolicy(t *testing.T) { 2027 tests := []struct { 2028 name string 2029 config *admissionregistration.ValidatingAdmissionPolicy 2030 expectedError string 2031 }{{ 2032 name: "metadata.name validation", 2033 config: &admissionregistration.ValidatingAdmissionPolicy{ 2034 ObjectMeta: metav1.ObjectMeta{ 2035 Name: "!!!!", 2036 }, 2037 }, 2038 expectedError: `metadata.name: Invalid value: "!!!!":`, 2039 }, { 2040 name: "failure policy validation", 2041 config: &admissionregistration.ValidatingAdmissionPolicy{ 2042 ObjectMeta: metav1.ObjectMeta{ 2043 Name: "config", 2044 }, 2045 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2046 FailurePolicy: func() *admissionregistration.FailurePolicyType { 2047 r := admissionregistration.FailurePolicyType("other") 2048 return &r 2049 }(), 2050 Validations: []admissionregistration.Validation{{ 2051 Expression: "object.x < 100", 2052 }}, 2053 }, 2054 }, 2055 expectedError: `spec.failurePolicy: Unsupported value: "other": supported values: "Fail", "Ignore"`, 2056 }, { 2057 name: "failure policy validation", 2058 config: &admissionregistration.ValidatingAdmissionPolicy{ 2059 ObjectMeta: metav1.ObjectMeta{ 2060 Name: "config", 2061 }, 2062 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2063 FailurePolicy: func() *admissionregistration.FailurePolicyType { 2064 r := admissionregistration.FailurePolicyType("other") 2065 return &r 2066 }(), 2067 }, 2068 }, 2069 expectedError: `spec.failurePolicy: Unsupported value: "other": supported values: "Fail", "Ignore"`, 2070 }, { 2071 name: "API version is required in ParamKind", 2072 config: &admissionregistration.ValidatingAdmissionPolicy{ 2073 ObjectMeta: metav1.ObjectMeta{ 2074 Name: "config", 2075 }, 2076 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2077 Validations: []admissionregistration.Validation{{ 2078 Expression: "object.x < 100", 2079 }}, 2080 ParamKind: &admissionregistration.ParamKind{ 2081 Kind: "Example", 2082 APIVersion: "test.example.com", 2083 }, 2084 }, 2085 }, 2086 expectedError: `spec.paramKind.apiVersion: Invalid value: "test.example.com"`, 2087 }, { 2088 name: "API kind is required in ParamKind", 2089 config: &admissionregistration.ValidatingAdmissionPolicy{ 2090 ObjectMeta: metav1.ObjectMeta{ 2091 Name: "config", 2092 }, 2093 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2094 Validations: []admissionregistration.Validation{{ 2095 Expression: "object.x < 100", 2096 }}, 2097 ParamKind: &admissionregistration.ParamKind{ 2098 APIVersion: "test.example.com/v1", 2099 }, 2100 }, 2101 }, 2102 expectedError: `spec.paramKind.kind: Required value`, 2103 }, { 2104 name: "API version format in ParamKind", 2105 config: &admissionregistration.ValidatingAdmissionPolicy{ 2106 ObjectMeta: metav1.ObjectMeta{ 2107 Name: "config", 2108 }, 2109 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2110 Validations: []admissionregistration.Validation{{ 2111 Expression: "object.x < 100", 2112 }}, 2113 ParamKind: &admissionregistration.ParamKind{ 2114 Kind: "Example", 2115 APIVersion: "test.example.com/!!!", 2116 }, 2117 }, 2118 }, 2119 expectedError: `pec.paramKind.apiVersion: Invalid value: "!!!":`, 2120 }, { 2121 name: "API group format in ParamKind", 2122 config: &admissionregistration.ValidatingAdmissionPolicy{ 2123 ObjectMeta: metav1.ObjectMeta{ 2124 Name: "config", 2125 }, 2126 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2127 Validations: []admissionregistration.Validation{{ 2128 Expression: "object.x < 100", 2129 }}, 2130 ParamKind: &admissionregistration.ParamKind{ 2131 APIVersion: "!!!/v1", 2132 Kind: "ReplicaLimit", 2133 }, 2134 }, 2135 }, 2136 expectedError: `pec.paramKind.apiVersion: Invalid value: "!!!":`, 2137 }, { 2138 name: "Validations is required", 2139 config: &admissionregistration.ValidatingAdmissionPolicy{ 2140 ObjectMeta: metav1.ObjectMeta{ 2141 Name: "config", 2142 }, 2143 Spec: admissionregistration.ValidatingAdmissionPolicySpec{}, 2144 }, 2145 2146 expectedError: `spec.validations: Required value: validations or auditAnnotations must contain at least one item`, 2147 }, { 2148 name: "Invalid Validations Reason", 2149 config: &admissionregistration.ValidatingAdmissionPolicy{ 2150 ObjectMeta: metav1.ObjectMeta{ 2151 Name: "config", 2152 }, 2153 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2154 Validations: []admissionregistration.Validation{{ 2155 Expression: "object.x < 100", 2156 Reason: func() *metav1.StatusReason { 2157 r := metav1.StatusReason("other") 2158 return &r 2159 }(), 2160 }}, 2161 }, 2162 }, 2163 2164 expectedError: `spec.validations[0].reason: Unsupported value: "other"`, 2165 }, { 2166 name: "MatchConstraints is required", 2167 config: &admissionregistration.ValidatingAdmissionPolicy{ 2168 ObjectMeta: metav1.ObjectMeta{ 2169 Name: "config", 2170 }, 2171 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2172 Validations: []admissionregistration.Validation{{ 2173 Expression: "object.x < 100", 2174 }}, 2175 }, 2176 }, 2177 2178 expectedError: `spec.matchConstraints: Required value`, 2179 }, { 2180 name: "matchConstraints.resourceRules is required", 2181 config: &admissionregistration.ValidatingAdmissionPolicy{ 2182 ObjectMeta: metav1.ObjectMeta{ 2183 Name: "config", 2184 }, 2185 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2186 Validations: []admissionregistration.Validation{{ 2187 Expression: "object.x < 100", 2188 }}, 2189 MatchConstraints: &admissionregistration.MatchResources{}, 2190 }, 2191 }, 2192 expectedError: `spec.matchConstraints.resourceRules: Required value`, 2193 }, { 2194 name: "matchConstraints.resourceRules has at least one explicit rule", 2195 config: &admissionregistration.ValidatingAdmissionPolicy{ 2196 ObjectMeta: metav1.ObjectMeta{ 2197 Name: "config", 2198 }, 2199 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2200 Validations: []admissionregistration.Validation{{ 2201 Expression: "object.x < 100", 2202 }}, 2203 MatchConstraints: &admissionregistration.MatchResources{ 2204 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 2205 RuleWithOperations: admissionregistration.RuleWithOperations{ 2206 Rule: admissionregistration.Rule{}, 2207 }, 2208 ResourceNames: []string{"/./."}, 2209 }}, 2210 }, 2211 }, 2212 }, 2213 expectedError: `spec.matchConstraints.resourceRules[0].apiVersions: Required value`, 2214 }, { 2215 name: "expression is required", 2216 config: &admissionregistration.ValidatingAdmissionPolicy{ 2217 ObjectMeta: metav1.ObjectMeta{ 2218 Name: "config", 2219 }, 2220 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2221 Validations: []admissionregistration.Validation{{}}, 2222 }, 2223 }, 2224 2225 expectedError: `spec.validations[0].expression: Required value: expression is not specified`, 2226 }, { 2227 name: "matchResources resourceNames check", 2228 config: &admissionregistration.ValidatingAdmissionPolicy{ 2229 ObjectMeta: metav1.ObjectMeta{ 2230 Name: "config", 2231 }, 2232 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2233 Validations: []admissionregistration.Validation{{ 2234 Expression: "object.x < 100", 2235 }}, 2236 MatchConstraints: &admissionregistration.MatchResources{ 2237 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 2238 ResourceNames: []string{"/./."}, 2239 }}, 2240 }, 2241 }, 2242 }, 2243 expectedError: `spec.matchConstraints.resourceRules[0].resourceNames[0]: Invalid value: "/./."`, 2244 }, { 2245 name: "matchResources resourceNames cannot duplicate", 2246 config: &admissionregistration.ValidatingAdmissionPolicy{ 2247 ObjectMeta: metav1.ObjectMeta{ 2248 Name: "config", 2249 }, 2250 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2251 Validations: []admissionregistration.Validation{{ 2252 Expression: "object.x < 100", 2253 }}, 2254 MatchConstraints: &admissionregistration.MatchResources{ 2255 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 2256 ResourceNames: []string{"test", "test"}, 2257 }}, 2258 }, 2259 }, 2260 }, 2261 expectedError: `spec.matchConstraints.resourceRules[0].resourceNames[1]: Duplicate value: "test"`, 2262 }, { 2263 name: "matchResources validation: matchPolicy", 2264 config: &admissionregistration.ValidatingAdmissionPolicy{ 2265 ObjectMeta: metav1.ObjectMeta{ 2266 Name: "config", 2267 }, 2268 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2269 Validations: []admissionregistration.Validation{{ 2270 Expression: "object.x < 100", 2271 }}, 2272 MatchConstraints: &admissionregistration.MatchResources{ 2273 MatchPolicy: func() *admissionregistration.MatchPolicyType { 2274 r := admissionregistration.MatchPolicyType("other") 2275 return &r 2276 }(), 2277 }, 2278 }, 2279 }, 2280 expectedError: `spec.matchConstraints.matchPolicy: Unsupported value: "other": supported values: "Equivalent", "Exact"`, 2281 }, { 2282 name: "Operations must not be empty or nil", 2283 config: &admissionregistration.ValidatingAdmissionPolicy{ 2284 ObjectMeta: metav1.ObjectMeta{ 2285 Name: "config", 2286 }, 2287 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2288 Validations: []admissionregistration.Validation{{ 2289 Expression: "object.x < 100", 2290 }}, 2291 FailurePolicy: func() *admissionregistration.FailurePolicyType { 2292 r := admissionregistration.FailurePolicyType("Fail") 2293 return &r 2294 }(), 2295 MatchConstraints: &admissionregistration.MatchResources{ 2296 NamespaceSelector: &metav1.LabelSelector{ 2297 MatchLabels: map[string]string{"a": "b"}, 2298 }, 2299 ObjectSelector: &metav1.LabelSelector{ 2300 MatchLabels: map[string]string{"a": "b"}, 2301 }, 2302 MatchPolicy: func() *admissionregistration.MatchPolicyType { 2303 r := admissionregistration.MatchPolicyType("Exact") 2304 return &r 2305 }(), 2306 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 2307 RuleWithOperations: admissionregistration.RuleWithOperations{ 2308 Operations: []admissionregistration.OperationType{}, 2309 Rule: admissionregistration.Rule{ 2310 APIGroups: []string{"a"}, 2311 APIVersions: []string{"a"}, 2312 Resources: []string{"a"}, 2313 }, 2314 }, 2315 }, { 2316 RuleWithOperations: admissionregistration.RuleWithOperations{ 2317 Operations: nil, 2318 Rule: admissionregistration.Rule{ 2319 APIGroups: []string{"a"}, 2320 APIVersions: []string{"a"}, 2321 Resources: []string{"a"}, 2322 }, 2323 }, 2324 }}, 2325 ExcludeResourceRules: []admissionregistration.NamedRuleWithOperations{{ 2326 RuleWithOperations: admissionregistration.RuleWithOperations{ 2327 Operations: []admissionregistration.OperationType{}, 2328 Rule: admissionregistration.Rule{ 2329 APIGroups: []string{"a"}, 2330 APIVersions: []string{"a"}, 2331 Resources: []string{"a"}, 2332 }, 2333 }, 2334 }, { 2335 RuleWithOperations: admissionregistration.RuleWithOperations{ 2336 Operations: nil, 2337 Rule: admissionregistration.Rule{ 2338 APIGroups: []string{"a"}, 2339 APIVersions: []string{"a"}, 2340 Resources: []string{"a"}, 2341 }, 2342 }, 2343 }}, 2344 }, 2345 }, 2346 }, 2347 expectedError: `spec.matchConstraints.resourceRules[0].operations: Required value, spec.matchConstraints.resourceRules[1].operations: Required value, spec.matchConstraints.excludeResourceRules[0].operations: Required value, spec.matchConstraints.excludeResourceRules[1].operations: Required value`, 2348 }, { 2349 name: "\"\" is NOT a valid operation", 2350 config: &admissionregistration.ValidatingAdmissionPolicy{ 2351 ObjectMeta: metav1.ObjectMeta{ 2352 Name: "config", 2353 }, 2354 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2355 Validations: []admissionregistration.Validation{{ 2356 Expression: "object.x < 100", 2357 }}, 2358 MatchConstraints: &admissionregistration.MatchResources{ 2359 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 2360 RuleWithOperations: admissionregistration.RuleWithOperations{ 2361 Operations: []admissionregistration.OperationType{"CREATE", ""}, 2362 Rule: admissionregistration.Rule{ 2363 APIGroups: []string{"a"}, 2364 APIVersions: []string{"a"}, 2365 Resources: []string{"a"}, 2366 }, 2367 }, 2368 }}, 2369 }, 2370 }, 2371 }, 2372 expectedError: `Unsupported value: ""`, 2373 }, { 2374 name: "operation must be either create/update/delete/connect", 2375 config: &admissionregistration.ValidatingAdmissionPolicy{ 2376 ObjectMeta: metav1.ObjectMeta{ 2377 Name: "config", 2378 }, 2379 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2380 Validations: []admissionregistration.Validation{{ 2381 Expression: "object.x < 100", 2382 }}, 2383 MatchConstraints: &admissionregistration.MatchResources{ 2384 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 2385 RuleWithOperations: admissionregistration.RuleWithOperations{ 2386 Operations: []admissionregistration.OperationType{"PATCH"}, 2387 Rule: admissionregistration.Rule{ 2388 APIGroups: []string{"a"}, 2389 APIVersions: []string{"a"}, 2390 Resources: []string{"a"}, 2391 }, 2392 }, 2393 }}, 2394 }, 2395 }, 2396 }, 2397 expectedError: `Unsupported value: "PATCH"`, 2398 }, { 2399 name: "wildcard operation cannot be mixed with other strings", 2400 config: &admissionregistration.ValidatingAdmissionPolicy{ 2401 ObjectMeta: metav1.ObjectMeta{ 2402 Name: "config", 2403 }, 2404 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2405 Validations: []admissionregistration.Validation{{ 2406 Expression: "object.x < 100", 2407 }}, 2408 MatchConstraints: &admissionregistration.MatchResources{ 2409 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 2410 RuleWithOperations: admissionregistration.RuleWithOperations{ 2411 Operations: []admissionregistration.OperationType{"CREATE", "*"}, 2412 Rule: admissionregistration.Rule{ 2413 APIGroups: []string{"a"}, 2414 APIVersions: []string{"a"}, 2415 Resources: []string{"a"}, 2416 }, 2417 }, 2418 }}, 2419 }, 2420 }, 2421 }, 2422 expectedError: `if '*' is present, must not specify other operations`, 2423 }, { 2424 name: `resource "*" can co-exist with resources that have subresources`, 2425 config: &admissionregistration.ValidatingAdmissionPolicy{ 2426 ObjectMeta: metav1.ObjectMeta{ 2427 Name: "config", 2428 }, 2429 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2430 FailurePolicy: func() *admissionregistration.FailurePolicyType { 2431 r := admissionregistration.FailurePolicyType("Fail") 2432 return &r 2433 }(), 2434 Validations: []admissionregistration.Validation{{ 2435 Expression: "object.x < 100", 2436 }}, 2437 MatchConstraints: &admissionregistration.MatchResources{ 2438 MatchPolicy: func() *admissionregistration.MatchPolicyType { 2439 r := admissionregistration.MatchPolicyType("Exact") 2440 return &r 2441 }(), 2442 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 2443 RuleWithOperations: admissionregistration.RuleWithOperations{ 2444 Operations: []admissionregistration.OperationType{"CREATE"}, 2445 Rule: admissionregistration.Rule{ 2446 APIGroups: []string{"a"}, 2447 APIVersions: []string{"a"}, 2448 Resources: []string{"*", "a/b", "a/*", "*/b"}, 2449 }, 2450 }, 2451 }}, 2452 NamespaceSelector: &metav1.LabelSelector{ 2453 MatchLabels: map[string]string{"a": "b"}, 2454 }, 2455 ObjectSelector: &metav1.LabelSelector{ 2456 MatchLabels: map[string]string{"a": "b"}, 2457 }, 2458 }, 2459 }, 2460 }, 2461 }, { 2462 name: `resource "*" cannot mix with resources that don't have subresources`, 2463 config: &admissionregistration.ValidatingAdmissionPolicy{ 2464 ObjectMeta: metav1.ObjectMeta{ 2465 Name: "config", 2466 }, 2467 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2468 Validations: []admissionregistration.Validation{{ 2469 Expression: "object.x < 100", 2470 }}, 2471 MatchConstraints: &admissionregistration.MatchResources{ 2472 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 2473 RuleWithOperations: admissionregistration.RuleWithOperations{ 2474 Operations: []admissionregistration.OperationType{"CREATE"}, 2475 Rule: admissionregistration.Rule{ 2476 APIGroups: []string{"a"}, 2477 APIVersions: []string{"a"}, 2478 Resources: []string{"*", "a"}, 2479 }, 2480 }, 2481 }}, 2482 }, 2483 }, 2484 }, 2485 expectedError: `if '*' is present, must not specify other resources without subresources`, 2486 }, { 2487 name: "resource a/* cannot mix with a/x", 2488 config: &admissionregistration.ValidatingAdmissionPolicy{ 2489 ObjectMeta: metav1.ObjectMeta{ 2490 Name: "config", 2491 }, 2492 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2493 Validations: []admissionregistration.Validation{{ 2494 Expression: "object.x < 100", 2495 }}, 2496 MatchConstraints: &admissionregistration.MatchResources{ 2497 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 2498 RuleWithOperations: admissionregistration.RuleWithOperations{ 2499 Operations: []admissionregistration.OperationType{"CREATE"}, 2500 Rule: admissionregistration.Rule{ 2501 APIGroups: []string{"a"}, 2502 APIVersions: []string{"a"}, 2503 Resources: []string{"a/*", "a/x"}, 2504 }, 2505 }, 2506 }}, 2507 }, 2508 }, 2509 }, 2510 expectedError: `spec.matchConstraints.resourceRules[0].resources[1]: Invalid value: "a/x": if 'a/*' is present, must not specify a/x`, 2511 }, { 2512 name: "resource a/* can mix with a", 2513 config: &admissionregistration.ValidatingAdmissionPolicy{ 2514 ObjectMeta: metav1.ObjectMeta{ 2515 Name: "config", 2516 }, 2517 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2518 FailurePolicy: func() *admissionregistration.FailurePolicyType { 2519 r := admissionregistration.FailurePolicyType("Fail") 2520 return &r 2521 }(), 2522 Validations: []admissionregistration.Validation{{ 2523 Expression: "object.x < 100", 2524 }}, 2525 MatchConstraints: &admissionregistration.MatchResources{ 2526 MatchPolicy: func() *admissionregistration.MatchPolicyType { 2527 r := admissionregistration.MatchPolicyType("Exact") 2528 return &r 2529 }(), 2530 NamespaceSelector: &metav1.LabelSelector{ 2531 MatchLabels: map[string]string{"a": "b"}, 2532 }, 2533 ObjectSelector: &metav1.LabelSelector{ 2534 MatchLabels: map[string]string{"a": "b"}, 2535 }, 2536 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 2537 RuleWithOperations: admissionregistration.RuleWithOperations{ 2538 Operations: []admissionregistration.OperationType{"CREATE"}, 2539 Rule: admissionregistration.Rule{ 2540 APIGroups: []string{"a"}, 2541 APIVersions: []string{"a"}, 2542 Resources: []string{"a/*", "a"}, 2543 }, 2544 }, 2545 }}, 2546 }, 2547 }, 2548 }, 2549 }, { 2550 name: "resource */a cannot mix with x/a", 2551 config: &admissionregistration.ValidatingAdmissionPolicy{ 2552 ObjectMeta: metav1.ObjectMeta{ 2553 Name: "config", 2554 }, 2555 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2556 Validations: []admissionregistration.Validation{{ 2557 Expression: "object.x < 100", 2558 }}, 2559 MatchConstraints: &admissionregistration.MatchResources{ 2560 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 2561 RuleWithOperations: admissionregistration.RuleWithOperations{ 2562 Operations: []admissionregistration.OperationType{"CREATE"}, 2563 Rule: admissionregistration.Rule{ 2564 APIGroups: []string{"a"}, 2565 APIVersions: []string{"a"}, 2566 Resources: []string{"*/a", "x/a"}, 2567 }, 2568 }, 2569 }}, 2570 }, 2571 }, 2572 }, 2573 expectedError: `spec.matchConstraints.resourceRules[0].resources[1]: Invalid value: "x/a": if '*/a' is present, must not specify x/a`, 2574 }, { 2575 name: "resource */* cannot mix with other resources", 2576 config: &admissionregistration.ValidatingAdmissionPolicy{ 2577 ObjectMeta: metav1.ObjectMeta{ 2578 Name: "config", 2579 }, 2580 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2581 Validations: []admissionregistration.Validation{{ 2582 Expression: "object.x < 100", 2583 }}, 2584 MatchConstraints: &admissionregistration.MatchResources{ 2585 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 2586 RuleWithOperations: admissionregistration.RuleWithOperations{ 2587 Operations: []admissionregistration.OperationType{"CREATE"}, 2588 Rule: admissionregistration.Rule{ 2589 APIGroups: []string{"a"}, 2590 APIVersions: []string{"a"}, 2591 Resources: []string{"*/*", "a"}, 2592 }, 2593 }, 2594 }}, 2595 }, 2596 }, 2597 }, 2598 expectedError: `spec.matchConstraints.resourceRules[0].resources: Invalid value: []string{"*/*", "a"}: if '*/*' is present, must not specify other resources`, 2599 }, { 2600 name: "invalid expression", 2601 config: &admissionregistration.ValidatingAdmissionPolicy{ 2602 ObjectMeta: metav1.ObjectMeta{ 2603 Name: "config", 2604 }, 2605 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2606 Validations: []admissionregistration.Validation{{ 2607 Expression: "object.x in [1, 2, ", 2608 }}, 2609 MatchConstraints: &admissionregistration.MatchResources{ 2610 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 2611 RuleWithOperations: admissionregistration.RuleWithOperations{ 2612 Operations: []admissionregistration.OperationType{"CREATE"}, 2613 Rule: admissionregistration.Rule{ 2614 APIGroups: []string{"a"}, 2615 APIVersions: []string{"a"}, 2616 Resources: []string{"*/*"}, 2617 }, 2618 }, 2619 }}, 2620 }, 2621 }, 2622 }, 2623 expectedError: `spec.validations[0].expression: Invalid value: "object.x in [1, 2, ": compilation failed: ERROR: <input>:1:20: Syntax error: missing ']' at '<EOF>`, 2624 }, { 2625 name: "invalid messageExpression", 2626 config: &admissionregistration.ValidatingAdmissionPolicy{ 2627 ObjectMeta: metav1.ObjectMeta{ 2628 Name: "config", 2629 }, 2630 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2631 Validations: []admissionregistration.Validation{{ 2632 Expression: "true", 2633 MessageExpression: "object.x in [1, 2, ", 2634 }}, 2635 MatchConstraints: &admissionregistration.MatchResources{ 2636 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 2637 RuleWithOperations: admissionregistration.RuleWithOperations{ 2638 Operations: []admissionregistration.OperationType{"CREATE"}, 2639 Rule: admissionregistration.Rule{ 2640 APIGroups: []string{"a"}, 2641 APIVersions: []string{"a"}, 2642 Resources: []string{"*/*"}, 2643 }, 2644 }, 2645 }}, 2646 }, 2647 }, 2648 }, 2649 expectedError: `spec.validations[0].messageExpression: Invalid value: "object.x in [1, 2, ": compilation failed: ERROR: <input>:1:20: Syntax error: missing ']' at '<EOF>`, 2650 }, { 2651 name: "messageExpression of wrong type", 2652 config: &admissionregistration.ValidatingAdmissionPolicy{ 2653 ObjectMeta: metav1.ObjectMeta{ 2654 Name: "config", 2655 }, 2656 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2657 Validations: []admissionregistration.Validation{{ 2658 Expression: "true", 2659 MessageExpression: "0 == 0", 2660 }}, 2661 MatchConstraints: &admissionregistration.MatchResources{ 2662 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 2663 RuleWithOperations: admissionregistration.RuleWithOperations{ 2664 Operations: []admissionregistration.OperationType{"CREATE"}, 2665 Rule: admissionregistration.Rule{ 2666 APIGroups: []string{"a"}, 2667 APIVersions: []string{"a"}, 2668 Resources: []string{"*/*"}, 2669 }, 2670 }, 2671 }}, 2672 }, 2673 }, 2674 }, 2675 expectedError: `spec.validations[0].messageExpression: Invalid value: "0 == 0": must evaluate to string`, 2676 }, { 2677 name: "invalid auditAnnotations key due to key name", 2678 config: &admissionregistration.ValidatingAdmissionPolicy{ 2679 ObjectMeta: metav1.ObjectMeta{ 2680 Name: "config", 2681 }, 2682 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2683 AuditAnnotations: []admissionregistration.AuditAnnotation{{ 2684 Key: "@", 2685 ValueExpression: "value", 2686 }}, 2687 }, 2688 }, 2689 expectedError: `spec.auditAnnotations[0].key: Invalid value: "config/@": name part must consist of alphanumeric characters`, 2690 }, { 2691 name: "auditAnnotations keys must be unique", 2692 config: &admissionregistration.ValidatingAdmissionPolicy{ 2693 ObjectMeta: metav1.ObjectMeta{ 2694 Name: "config", 2695 }, 2696 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2697 AuditAnnotations: []admissionregistration.AuditAnnotation{{ 2698 Key: "a", 2699 ValueExpression: "'1'", 2700 }, { 2701 Key: "a", 2702 ValueExpression: "'2'", 2703 }}, 2704 }, 2705 }, 2706 expectedError: `spec.auditAnnotations[1].key: Duplicate value: "a"`, 2707 }, { 2708 name: "invalid auditAnnotations key due to metadata.name", 2709 config: &admissionregistration.ValidatingAdmissionPolicy{ 2710 ObjectMeta: metav1.ObjectMeta{ 2711 Name: "nope!", 2712 }, 2713 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2714 AuditAnnotations: []admissionregistration.AuditAnnotation{{ 2715 Key: "key", 2716 ValueExpression: "'value'", 2717 }}, 2718 }, 2719 }, 2720 expectedError: `spec.auditAnnotations[0].key: Invalid value: "nope!/key": prefix part a lowercase RFC 1123 subdomain`, 2721 }, { 2722 name: "invalid auditAnnotations key due to length", 2723 config: &admissionregistration.ValidatingAdmissionPolicy{ 2724 ObjectMeta: metav1.ObjectMeta{ 2725 Name: "this-is-a-long-name-for-a-admission-policy-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 2726 }, 2727 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2728 AuditAnnotations: []admissionregistration.AuditAnnotation{{ 2729 Key: "this-is-a-long-name-for-an-audit-annotation-key-xxxxxxxxxxxxxxxxxxxxxxxxxx", 2730 ValueExpression: "'value'", 2731 }}, 2732 }, 2733 }, 2734 expectedError: `spec.auditAnnotations[0].key: Invalid value`, 2735 }, { 2736 name: "invalid auditAnnotations valueExpression type", 2737 config: &admissionregistration.ValidatingAdmissionPolicy{ 2738 ObjectMeta: metav1.ObjectMeta{ 2739 Name: "config", 2740 }, 2741 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2742 AuditAnnotations: []admissionregistration.AuditAnnotation{{ 2743 Key: "something", 2744 ValueExpression: "true", 2745 }}, 2746 }, 2747 }, 2748 expectedError: `spec.auditAnnotations[0].valueExpression: Invalid value: "true": must evaluate to one of [string null_type]`, 2749 }, { 2750 name: "invalid auditAnnotations valueExpression", 2751 config: &admissionregistration.ValidatingAdmissionPolicy{ 2752 ObjectMeta: metav1.ObjectMeta{ 2753 Name: "config", 2754 }, 2755 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2756 AuditAnnotations: []admissionregistration.AuditAnnotation{{ 2757 Key: "something", 2758 ValueExpression: "object.x in [1, 2, ", 2759 }}, 2760 }, 2761 }, 2762 expectedError: `spec.auditAnnotations[0].valueExpression: Invalid value: "object.x in [1, 2, ": compilation failed: ERROR: <input>:1:19: Syntax error: missing ']' at '<EOF>`, 2763 }, { 2764 name: "single match condition must have a name", 2765 config: &admissionregistration.ValidatingAdmissionPolicy{ 2766 ObjectMeta: metav1.ObjectMeta{ 2767 Name: "config", 2768 }, 2769 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2770 MatchConditions: []admissionregistration.MatchCondition{{ 2771 Expression: "true", 2772 }}, 2773 Validations: []admissionregistration.Validation{{ 2774 Expression: "object.x < 100", 2775 }}, 2776 }, 2777 }, 2778 expectedError: `spec.matchConditions[0].name: Required value`, 2779 }, { 2780 name: "match condition with parameters allowed", 2781 config: &admissionregistration.ValidatingAdmissionPolicy{ 2782 ObjectMeta: metav1.ObjectMeta{ 2783 Name: "config", 2784 }, 2785 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2786 ParamKind: &admissionregistration.ParamKind{ 2787 Kind: "Foo", 2788 APIVersion: "foobar/v1alpha1", 2789 }, 2790 MatchConstraints: &admissionregistration.MatchResources{ 2791 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 2792 RuleWithOperations: admissionregistration.RuleWithOperations{ 2793 Operations: []admissionregistration.OperationType{"*"}, 2794 Rule: admissionregistration.Rule{ 2795 APIGroups: []string{"a"}, 2796 APIVersions: []string{"a"}, 2797 Resources: []string{"a"}, 2798 }, 2799 }, 2800 }}, 2801 NamespaceSelector: &metav1.LabelSelector{ 2802 MatchLabels: map[string]string{"a": "b"}, 2803 }, 2804 ObjectSelector: &metav1.LabelSelector{ 2805 MatchLabels: map[string]string{"a": "b"}, 2806 }, 2807 MatchPolicy: func() *admissionregistration.MatchPolicyType { 2808 r := admissionregistration.MatchPolicyType("Exact") 2809 return &r 2810 }(), 2811 }, 2812 FailurePolicy: func() *admissionregistration.FailurePolicyType { 2813 r := admissionregistration.FailurePolicyType("Fail") 2814 return &r 2815 }(), 2816 MatchConditions: []admissionregistration.MatchCondition{{ 2817 Name: "hasParams", 2818 Expression: `params.foo == "okay"`, 2819 }}, 2820 Validations: []admissionregistration.Validation{{ 2821 Expression: "object.x < 100", 2822 }}, 2823 }, 2824 }, 2825 expectedError: "", 2826 }, { 2827 name: "match condition with parameters not allowed if no param kind", 2828 config: &admissionregistration.ValidatingAdmissionPolicy{ 2829 ObjectMeta: metav1.ObjectMeta{ 2830 Name: "config", 2831 }, 2832 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2833 MatchConstraints: &admissionregistration.MatchResources{ 2834 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 2835 RuleWithOperations: admissionregistration.RuleWithOperations{ 2836 Operations: []admissionregistration.OperationType{"*"}, 2837 Rule: admissionregistration.Rule{ 2838 APIGroups: []string{"a"}, 2839 APIVersions: []string{"a"}, 2840 Resources: []string{"a"}, 2841 }, 2842 }, 2843 }}, 2844 NamespaceSelector: &metav1.LabelSelector{ 2845 MatchLabels: map[string]string{"a": "b"}, 2846 }, 2847 ObjectSelector: &metav1.LabelSelector{ 2848 MatchLabels: map[string]string{"a": "b"}, 2849 }, 2850 MatchPolicy: func() *admissionregistration.MatchPolicyType { 2851 r := admissionregistration.MatchPolicyType("Exact") 2852 return &r 2853 }(), 2854 }, 2855 FailurePolicy: func() *admissionregistration.FailurePolicyType { 2856 r := admissionregistration.FailurePolicyType("Fail") 2857 return &r 2858 }(), 2859 MatchConditions: []admissionregistration.MatchCondition{{ 2860 Name: "hasParams", 2861 Expression: `params.foo == "okay"`, 2862 }}, 2863 Validations: []admissionregistration.Validation{{ 2864 Expression: "object.x < 100", 2865 }}, 2866 }, 2867 }, 2868 expectedError: `undeclared reference to 'params'`, 2869 }, { 2870 name: "variable composition empty name", 2871 config: &admissionregistration.ValidatingAdmissionPolicy{ 2872 ObjectMeta: metav1.ObjectMeta{ 2873 Name: "config", 2874 }, 2875 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2876 Variables: []admissionregistration.Variable{ 2877 { 2878 Name: " ", 2879 Expression: "true", 2880 }, 2881 }, 2882 Validations: []admissionregistration.Validation{ 2883 { 2884 Expression: "true", 2885 }, 2886 }, 2887 }, 2888 }, 2889 expectedError: `spec.variables[0].name: Required value: name is not specified`, 2890 }, { 2891 name: "variable composition name is not a valid identifier", 2892 config: &admissionregistration.ValidatingAdmissionPolicy{ 2893 ObjectMeta: metav1.ObjectMeta{ 2894 Name: "config", 2895 }, 2896 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2897 Variables: []admissionregistration.Variable{ 2898 { 2899 Name: "4ever", 2900 Expression: "true", 2901 }, 2902 }, 2903 Validations: []admissionregistration.Validation{ 2904 { 2905 Expression: "true", 2906 }, 2907 }, 2908 }, 2909 }, 2910 expectedError: `spec.variables[0].name: Invalid value: "4ever": name is not a valid CEL identifier`, 2911 }, { 2912 name: "variable composition cannot compile", 2913 config: &admissionregistration.ValidatingAdmissionPolicy{ 2914 ObjectMeta: metav1.ObjectMeta{ 2915 Name: "config", 2916 }, 2917 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2918 Variables: []admissionregistration.Variable{ 2919 { 2920 Name: "foo", 2921 Expression: "114 + '514'", // compile error: type confusion 2922 }, 2923 }, 2924 Validations: []admissionregistration.Validation{ 2925 { 2926 Expression: "true", 2927 }, 2928 }, 2929 }, 2930 }, 2931 expectedError: `spec.variables[0].expression: Invalid value: "114 + '514'": compilation failed: ERROR: <input>:1:5: found no matching overload for '_+_' applied to '(int, string)`, 2932 }, { 2933 name: "validation referred to non-existing variable", 2934 config: &admissionregistration.ValidatingAdmissionPolicy{ 2935 ObjectMeta: metav1.ObjectMeta{ 2936 Name: "config", 2937 }, 2938 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2939 Variables: []admissionregistration.Variable{ 2940 { 2941 Name: "foo", 2942 Expression: "1 + 1", 2943 }, 2944 { 2945 Name: "bar", 2946 Expression: "variables.foo + 1", 2947 }, 2948 }, 2949 Validations: []admissionregistration.Validation{ 2950 { 2951 Expression: "variables.foo > 1", // correct 2952 }, 2953 { 2954 Expression: "variables.replicas == 2", // replicas undefined 2955 }, 2956 }, 2957 }, 2958 }, 2959 expectedError: `spec.validations[1].expression: Invalid value: "variables.replicas == 2": compilation failed: ERROR: <input>:1:10: undefined field 'replicas'`, 2960 }, { 2961 name: "variables wrong order", 2962 config: &admissionregistration.ValidatingAdmissionPolicy{ 2963 ObjectMeta: metav1.ObjectMeta{ 2964 Name: "config", 2965 }, 2966 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 2967 Variables: []admissionregistration.Variable{ 2968 { 2969 Name: "correct", 2970 Expression: "object", 2971 }, 2972 { 2973 Name: "bar", // should go below foo 2974 Expression: "variables.foo + 1", 2975 }, 2976 { 2977 Name: "foo", 2978 Expression: "1 + 1", 2979 }, 2980 }, 2981 Validations: []admissionregistration.Validation{ 2982 { 2983 Expression: "variables.foo > 1", // correct 2984 }, 2985 }, 2986 }, 2987 }, 2988 expectedError: `spec.variables[1].expression: Invalid value: "variables.foo + 1": compilation failed: ERROR: <input>:1:10: undefined field 'foo'`, 2989 }, 2990 } 2991 for _, test := range tests { 2992 t.Run(test.name, func(t *testing.T) { 2993 errs := ValidateValidatingAdmissionPolicy(test.config) 2994 err := errs.ToAggregate() 2995 if err != nil { 2996 if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { 2997 t.Errorf("expected to contain %s, got %s", e, a) 2998 } 2999 } else { 3000 if test.expectedError != "" { 3001 t.Errorf("unexpected no error, expected to contain %s", test.expectedError) 3002 } 3003 } 3004 }) 3005 } 3006 } 3007 3008 func TestValidateValidatingAdmissionPolicyUpdate(t *testing.T) { 3009 tests := []struct { 3010 name string 3011 oldconfig *admissionregistration.ValidatingAdmissionPolicy 3012 config *admissionregistration.ValidatingAdmissionPolicy 3013 expectedError string 3014 }{{ 3015 name: "should pass on valid new ValidatingAdmissionPolicy", 3016 config: &admissionregistration.ValidatingAdmissionPolicy{ 3017 ObjectMeta: metav1.ObjectMeta{ 3018 Name: "config", 3019 }, 3020 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 3021 FailurePolicy: func() *admissionregistration.FailurePolicyType { 3022 r := admissionregistration.FailurePolicyType("Fail") 3023 return &r 3024 }(), 3025 Validations: []admissionregistration.Validation{{ 3026 Expression: "object.x < 100", 3027 }}, 3028 MatchConstraints: &admissionregistration.MatchResources{ 3029 NamespaceSelector: &metav1.LabelSelector{ 3030 MatchLabels: map[string]string{"a": "b"}, 3031 }, 3032 ObjectSelector: &metav1.LabelSelector{ 3033 MatchLabels: map[string]string{"a": "b"}, 3034 }, 3035 MatchPolicy: func() *admissionregistration.MatchPolicyType { 3036 r := admissionregistration.MatchPolicyType("Exact") 3037 return &r 3038 }(), 3039 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 3040 RuleWithOperations: admissionregistration.RuleWithOperations{ 3041 Operations: []admissionregistration.OperationType{"CREATE"}, 3042 Rule: admissionregistration.Rule{ 3043 APIGroups: []string{"a"}, 3044 APIVersions: []string{"a"}, 3045 Resources: []string{"a"}, 3046 }, 3047 }, 3048 }}, 3049 }, 3050 }, 3051 }, 3052 oldconfig: &admissionregistration.ValidatingAdmissionPolicy{ 3053 ObjectMeta: metav1.ObjectMeta{ 3054 Name: "config", 3055 }, 3056 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 3057 Validations: []admissionregistration.Validation{{ 3058 Expression: "object.x < 100", 3059 }}, 3060 MatchConstraints: &admissionregistration.MatchResources{ 3061 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 3062 RuleWithOperations: admissionregistration.RuleWithOperations{ 3063 Operations: []admissionregistration.OperationType{"CREATE"}, 3064 Rule: admissionregistration.Rule{ 3065 APIGroups: []string{"a"}, 3066 APIVersions: []string{"a"}, 3067 Resources: []string{"a"}, 3068 }, 3069 }, 3070 }}, 3071 }, 3072 }, 3073 }, 3074 }, { 3075 name: "should pass on valid new ValidatingAdmissionPolicy with invalid old ValidatingAdmissionPolicy", 3076 config: &admissionregistration.ValidatingAdmissionPolicy{ 3077 ObjectMeta: metav1.ObjectMeta{ 3078 Name: "config", 3079 }, 3080 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 3081 FailurePolicy: func() *admissionregistration.FailurePolicyType { 3082 r := admissionregistration.FailurePolicyType("Fail") 3083 return &r 3084 }(), 3085 Validations: []admissionregistration.Validation{{ 3086 Expression: "object.x < 100", 3087 }}, 3088 MatchConstraints: &admissionregistration.MatchResources{ 3089 NamespaceSelector: &metav1.LabelSelector{ 3090 MatchLabels: map[string]string{"a": "b"}, 3091 }, 3092 ObjectSelector: &metav1.LabelSelector{ 3093 MatchLabels: map[string]string{"a": "b"}, 3094 }, 3095 MatchPolicy: func() *admissionregistration.MatchPolicyType { 3096 r := admissionregistration.MatchPolicyType("Exact") 3097 return &r 3098 }(), 3099 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 3100 RuleWithOperations: admissionregistration.RuleWithOperations{ 3101 Operations: []admissionregistration.OperationType{"CREATE"}, 3102 Rule: admissionregistration.Rule{ 3103 APIGroups: []string{"a"}, 3104 APIVersions: []string{"a"}, 3105 Resources: []string{"a"}, 3106 }, 3107 }, 3108 }}, 3109 }, 3110 }, 3111 }, 3112 oldconfig: &admissionregistration.ValidatingAdmissionPolicy{ 3113 ObjectMeta: metav1.ObjectMeta{ 3114 Name: "!!!", 3115 }, 3116 Spec: admissionregistration.ValidatingAdmissionPolicySpec{}, 3117 }, 3118 }, { 3119 name: "match conditions re-checked if paramKind changes", 3120 oldconfig: &admissionregistration.ValidatingAdmissionPolicy{ 3121 ObjectMeta: metav1.ObjectMeta{ 3122 Name: "config", 3123 }, 3124 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 3125 ParamKind: &admissionregistration.ParamKind{ 3126 Kind: "Foo", 3127 APIVersion: "foobar/v1alpha1", 3128 }, 3129 MatchConstraints: &admissionregistration.MatchResources{ 3130 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 3131 RuleWithOperations: admissionregistration.RuleWithOperations{ 3132 Operations: []admissionregistration.OperationType{"*"}, 3133 Rule: admissionregistration.Rule{ 3134 APIGroups: []string{"a"}, 3135 APIVersions: []string{"a"}, 3136 Resources: []string{"a"}, 3137 }, 3138 }, 3139 }}, 3140 NamespaceSelector: &metav1.LabelSelector{ 3141 MatchLabels: map[string]string{"a": "b"}, 3142 }, 3143 ObjectSelector: &metav1.LabelSelector{ 3144 MatchLabels: map[string]string{"a": "b"}, 3145 }, 3146 MatchPolicy: func() *admissionregistration.MatchPolicyType { 3147 r := admissionregistration.MatchPolicyType("Exact") 3148 return &r 3149 }(), 3150 }, 3151 FailurePolicy: func() *admissionregistration.FailurePolicyType { 3152 r := admissionregistration.FailurePolicyType("Fail") 3153 return &r 3154 }(), 3155 MatchConditions: []admissionregistration.MatchCondition{{ 3156 Name: "hasParams", 3157 Expression: `params.foo == "okay"`, 3158 }}, 3159 Validations: []admissionregistration.Validation{{ 3160 Expression: "object.x < 100", 3161 }}, 3162 }, 3163 }, 3164 config: &admissionregistration.ValidatingAdmissionPolicy{ 3165 ObjectMeta: metav1.ObjectMeta{ 3166 Name: "config", 3167 }, 3168 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 3169 MatchConstraints: &admissionregistration.MatchResources{ 3170 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 3171 RuleWithOperations: admissionregistration.RuleWithOperations{ 3172 Operations: []admissionregistration.OperationType{"*"}, 3173 Rule: admissionregistration.Rule{ 3174 APIGroups: []string{"a"}, 3175 APIVersions: []string{"a"}, 3176 Resources: []string{"a"}, 3177 }, 3178 }, 3179 }}, 3180 NamespaceSelector: &metav1.LabelSelector{ 3181 MatchLabels: map[string]string{"a": "b"}, 3182 }, 3183 ObjectSelector: &metav1.LabelSelector{ 3184 MatchLabels: map[string]string{"a": "b"}, 3185 }, 3186 MatchPolicy: func() *admissionregistration.MatchPolicyType { 3187 r := admissionregistration.MatchPolicyType("Exact") 3188 return &r 3189 }(), 3190 }, 3191 FailurePolicy: func() *admissionregistration.FailurePolicyType { 3192 r := admissionregistration.FailurePolicyType("Fail") 3193 return &r 3194 }(), 3195 MatchConditions: []admissionregistration.MatchCondition{{ 3196 Name: "hasParams", 3197 Expression: `params.foo == "okay"`, 3198 }}, 3199 Validations: []admissionregistration.Validation{{ 3200 Expression: "object.x < 100", 3201 }}, 3202 }, 3203 }, 3204 expectedError: `undeclared reference to 'params'`, 3205 }, { 3206 name: "match conditions not re-checked if no change to paramKind or matchConditions", 3207 oldconfig: &admissionregistration.ValidatingAdmissionPolicy{ 3208 ObjectMeta: metav1.ObjectMeta{ 3209 Name: "config", 3210 }, 3211 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 3212 MatchConstraints: &admissionregistration.MatchResources{ 3213 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 3214 RuleWithOperations: admissionregistration.RuleWithOperations{ 3215 Operations: []admissionregistration.OperationType{"*"}, 3216 Rule: admissionregistration.Rule{ 3217 APIGroups: []string{"a"}, 3218 APIVersions: []string{"a"}, 3219 Resources: []string{"a"}, 3220 }, 3221 }, 3222 }}, 3223 NamespaceSelector: &metav1.LabelSelector{ 3224 MatchLabels: map[string]string{"a": "b"}, 3225 }, 3226 ObjectSelector: &metav1.LabelSelector{ 3227 MatchLabels: map[string]string{"a": "b"}, 3228 }, 3229 MatchPolicy: func() *admissionregistration.MatchPolicyType { 3230 r := admissionregistration.MatchPolicyType("Exact") 3231 return &r 3232 }(), 3233 }, 3234 FailurePolicy: func() *admissionregistration.FailurePolicyType { 3235 r := admissionregistration.FailurePolicyType("Fail") 3236 return &r 3237 }(), 3238 MatchConditions: []admissionregistration.MatchCondition{{ 3239 Name: "hasParams", 3240 Expression: `params.foo == "okay"`, 3241 }}, 3242 Validations: []admissionregistration.Validation{{ 3243 Expression: "object.x < 100", 3244 }}, 3245 }, 3246 }, 3247 config: &admissionregistration.ValidatingAdmissionPolicy{ 3248 ObjectMeta: metav1.ObjectMeta{ 3249 Name: "config", 3250 }, 3251 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 3252 MatchConstraints: &admissionregistration.MatchResources{ 3253 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 3254 RuleWithOperations: admissionregistration.RuleWithOperations{ 3255 Operations: []admissionregistration.OperationType{"*"}, 3256 Rule: admissionregistration.Rule{ 3257 APIGroups: []string{"a"}, 3258 APIVersions: []string{"a"}, 3259 Resources: []string{"a"}, 3260 }, 3261 }, 3262 }}, 3263 NamespaceSelector: &metav1.LabelSelector{ 3264 MatchLabels: map[string]string{"a": "b"}, 3265 }, 3266 ObjectSelector: &metav1.LabelSelector{ 3267 MatchLabels: map[string]string{"a": "b"}, 3268 }, 3269 MatchPolicy: func() *admissionregistration.MatchPolicyType { 3270 r := admissionregistration.MatchPolicyType("Exact") 3271 return &r 3272 }(), 3273 }, 3274 FailurePolicy: func() *admissionregistration.FailurePolicyType { 3275 r := admissionregistration.FailurePolicyType("Ignore") 3276 return &r 3277 }(), 3278 MatchConditions: []admissionregistration.MatchCondition{{ 3279 Name: "hasParams", 3280 Expression: `params.foo == "okay"`, 3281 }}, 3282 Validations: []admissionregistration.Validation{{ 3283 Expression: "object.x < 50", 3284 }}, 3285 }, 3286 }, 3287 expectedError: "", 3288 }, 3289 { 3290 name: "expressions that are not changed must be compiled using the StoredExpression environment", 3291 oldconfig: validatingAdmissionPolicyWithExpressions( 3292 []admissionregistration.MatchCondition{ 3293 { 3294 Name: "checkEnvironmentMode", 3295 Expression: `test() == true`, 3296 }, 3297 }, 3298 []admissionregistration.Validation{ 3299 { 3300 Expression: `test() == true`, 3301 MessageExpression: "string(test())", 3302 }, 3303 }, 3304 []admissionregistration.AuditAnnotation{ 3305 { 3306 Key: "checkEnvironmentMode", 3307 ValueExpression: "string(test())", 3308 }, 3309 }), 3310 config: validatingAdmissionPolicyWithExpressions( 3311 []admissionregistration.MatchCondition{ 3312 { 3313 Name: "checkEnvironmentMode", 3314 Expression: `test() == true`, 3315 }, 3316 }, 3317 []admissionregistration.Validation{ 3318 { 3319 Expression: `test() == true`, 3320 MessageExpression: "string(test())", 3321 }, 3322 }, 3323 []admissionregistration.AuditAnnotation{ 3324 { 3325 Key: "checkEnvironmentMode", 3326 ValueExpression: "string(test())", 3327 }, 3328 }), 3329 expectedError: "", 3330 }, 3331 { 3332 name: "matchCondition expressions that are changed must be compiled using the NewExpression environment", 3333 oldconfig: validatingAdmissionPolicyWithExpressions( 3334 []admissionregistration.MatchCondition{ 3335 { 3336 Name: "checkEnvironmentMode", 3337 Expression: `true`, 3338 }, 3339 }, 3340 nil, nil), 3341 config: validatingAdmissionPolicyWithExpressions( 3342 []admissionregistration.MatchCondition{ 3343 { 3344 Name: "checkEnvironmentMode", 3345 Expression: `test() == true`, 3346 }, 3347 }, 3348 nil, nil), 3349 expectedError: `undeclared reference to 'test'`, 3350 }, 3351 { 3352 name: "validation expressions that are changed must be compiled using the NewExpression environment", 3353 oldconfig: validatingAdmissionPolicyWithExpressions( 3354 nil, 3355 []admissionregistration.Validation{ 3356 { 3357 Expression: `true`, 3358 }, 3359 }, 3360 nil), 3361 config: validatingAdmissionPolicyWithExpressions( 3362 nil, 3363 []admissionregistration.Validation{ 3364 { 3365 Expression: `test() == true`, 3366 }, 3367 }, 3368 nil), 3369 expectedError: `undeclared reference to 'test'`, 3370 }, 3371 { 3372 name: "validation messageExpressions that are changed must be compiled using the NewExpression environment", 3373 oldconfig: validatingAdmissionPolicyWithExpressions( 3374 nil, 3375 []admissionregistration.Validation{ 3376 { 3377 Expression: `true`, 3378 MessageExpression: "'test'", 3379 }, 3380 }, 3381 nil), 3382 config: validatingAdmissionPolicyWithExpressions( 3383 nil, 3384 []admissionregistration.Validation{ 3385 { 3386 Expression: `true`, 3387 MessageExpression: "string(test())", 3388 }, 3389 }, 3390 nil), 3391 expectedError: `undeclared reference to 'test'`, 3392 }, 3393 { 3394 name: "auditAnnotation valueExpressions that are changed must be compiled using the NewExpression environment", 3395 oldconfig: validatingAdmissionPolicyWithExpressions( 3396 nil, nil, 3397 []admissionregistration.AuditAnnotation{ 3398 { 3399 Key: "checkEnvironmentMode", 3400 ValueExpression: "'test'", 3401 }, 3402 }), 3403 config: validatingAdmissionPolicyWithExpressions( 3404 nil, nil, 3405 []admissionregistration.AuditAnnotation{ 3406 { 3407 Key: "checkEnvironmentMode", 3408 ValueExpression: "string(test())", 3409 }, 3410 }), 3411 expectedError: `undeclared reference to 'test'`, 3412 }, 3413 // TODO: CustomAuditAnnotations: string valueExpression with {oldObject} is allowed 3414 } 3415 // Include the test library, which includes the test() function in the storage environment during test 3416 base := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()) 3417 extended, err := base.Extend(environment.VersionedOptions{ 3418 IntroducedVersion: version.MustParseGeneric("1.999"), 3419 EnvOptions: []cel.EnvOption{library.Test()}, 3420 }) 3421 if err != nil { 3422 t.Fatal(err) 3423 } 3424 statelessCELCompiler = plugincel.NewCompiler(extended) 3425 defer func() { 3426 statelessCELCompiler = plugincel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) 3427 }() 3428 3429 for _, test := range tests { 3430 t.Run(test.name, func(t *testing.T) { 3431 errs := ValidateValidatingAdmissionPolicyUpdate(test.config, test.oldconfig) 3432 err := errs.ToAggregate() 3433 if err != nil { 3434 if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { 3435 t.Errorf("expected to contain %s, got %s", e, a) 3436 } 3437 } else { 3438 if test.expectedError != "" { 3439 t.Errorf("unexpected no error, expected to contain %s", test.expectedError) 3440 } 3441 } 3442 }) 3443 3444 } 3445 } 3446 3447 func validatingAdmissionPolicyWithExpressions( 3448 matchConditions []admissionregistration.MatchCondition, 3449 validations []admissionregistration.Validation, 3450 auditAnnotations []admissionregistration.AuditAnnotation) *admissionregistration.ValidatingAdmissionPolicy { 3451 return &admissionregistration.ValidatingAdmissionPolicy{ 3452 ObjectMeta: metav1.ObjectMeta{ 3453 Name: "config", 3454 }, 3455 Spec: admissionregistration.ValidatingAdmissionPolicySpec{ 3456 MatchConstraints: &admissionregistration.MatchResources{ 3457 ResourceRules: []admissionregistration.NamedRuleWithOperations{ 3458 { 3459 RuleWithOperations: admissionregistration.RuleWithOperations{ 3460 Operations: []admissionregistration.OperationType{"*"}, 3461 Rule: admissionregistration.Rule{ 3462 APIGroups: []string{"a"}, 3463 APIVersions: []string{"a"}, 3464 Resources: []string{"a"}, 3465 }, 3466 }, 3467 }, 3468 }, 3469 NamespaceSelector: &metav1.LabelSelector{ 3470 MatchLabels: map[string]string{"a": "b"}, 3471 }, 3472 ObjectSelector: &metav1.LabelSelector{ 3473 MatchLabels: map[string]string{"a": "b"}, 3474 }, 3475 MatchPolicy: func() *admissionregistration.MatchPolicyType { 3476 r := admissionregistration.MatchPolicyType("Exact") 3477 return &r 3478 }(), 3479 }, 3480 FailurePolicy: func() *admissionregistration.FailurePolicyType { 3481 r := admissionregistration.FailurePolicyType("Ignore") 3482 return &r 3483 }(), 3484 MatchConditions: matchConditions, 3485 Validations: validations, 3486 AuditAnnotations: auditAnnotations, 3487 }, 3488 } 3489 } 3490 3491 func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) { 3492 tests := []struct { 3493 name string 3494 config *admissionregistration.ValidatingAdmissionPolicyBinding 3495 expectedError string 3496 }{{ 3497 name: "metadata.name validation", 3498 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3499 ObjectMeta: metav1.ObjectMeta{ 3500 Name: "!!!!", 3501 }, 3502 }, 3503 expectedError: `metadata.name: Invalid value: "!!!!":`, 3504 }, { 3505 name: "PolicyName is required", 3506 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3507 ObjectMeta: metav1.ObjectMeta{ 3508 Name: "config", 3509 }, 3510 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{}, 3511 }, 3512 expectedError: `spec.policyName: Required value`, 3513 }, { 3514 name: "matchResources validation: matchPolicy", 3515 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3516 ObjectMeta: metav1.ObjectMeta{ 3517 Name: "config", 3518 }, 3519 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 3520 PolicyName: "xyzlimit-scale.example.com", 3521 ParamRef: &admissionregistration.ParamRef{ 3522 Name: "xyzlimit-scale-setting.example.com", 3523 ParameterNotFoundAction: ptr(admissionregistration.DenyAction), 3524 }, 3525 MatchResources: &admissionregistration.MatchResources{ 3526 MatchPolicy: func() *admissionregistration.MatchPolicyType { 3527 r := admissionregistration.MatchPolicyType("other") 3528 return &r 3529 }(), 3530 }, 3531 }, 3532 }, 3533 expectedError: `spec.matchResouces.matchPolicy: Unsupported value: "other": supported values: "Equivalent", "Exact"`, 3534 }, { 3535 name: "Operations must not be empty or nil", 3536 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3537 ObjectMeta: metav1.ObjectMeta{ 3538 Name: "config", 3539 }, 3540 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 3541 PolicyName: "xyzlimit-scale.example.com", 3542 ParamRef: &admissionregistration.ParamRef{ 3543 Name: "xyzlimit-scale-setting.example.com", 3544 ParameterNotFoundAction: ptr(admissionregistration.DenyAction), 3545 }, 3546 MatchResources: &admissionregistration.MatchResources{ 3547 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 3548 RuleWithOperations: admissionregistration.RuleWithOperations{ 3549 Operations: []admissionregistration.OperationType{}, 3550 Rule: admissionregistration.Rule{ 3551 APIGroups: []string{"a"}, 3552 APIVersions: []string{"a"}, 3553 Resources: []string{"a"}, 3554 }, 3555 }, 3556 }, { 3557 RuleWithOperations: admissionregistration.RuleWithOperations{ 3558 Operations: nil, 3559 Rule: admissionregistration.Rule{ 3560 APIGroups: []string{"a"}, 3561 APIVersions: []string{"a"}, 3562 Resources: []string{"a"}, 3563 }, 3564 }, 3565 }}, 3566 ExcludeResourceRules: []admissionregistration.NamedRuleWithOperations{{ 3567 RuleWithOperations: admissionregistration.RuleWithOperations{ 3568 Operations: []admissionregistration.OperationType{}, 3569 Rule: admissionregistration.Rule{ 3570 APIGroups: []string{"a"}, 3571 APIVersions: []string{"a"}, 3572 Resources: []string{"a"}, 3573 }, 3574 }, 3575 }, { 3576 RuleWithOperations: admissionregistration.RuleWithOperations{ 3577 Operations: nil, 3578 Rule: admissionregistration.Rule{ 3579 APIGroups: []string{"a"}, 3580 APIVersions: []string{"a"}, 3581 Resources: []string{"a"}, 3582 }, 3583 }, 3584 }}, 3585 }, 3586 }, 3587 }, 3588 expectedError: `spec.matchResouces.resourceRules[0].operations: Required value, spec.matchResouces.resourceRules[1].operations: Required value, spec.matchResouces.excludeResourceRules[0].operations: Required value, spec.matchResouces.excludeResourceRules[1].operations: Required value`, 3589 }, { 3590 name: "\"\" is NOT a valid operation", 3591 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3592 ObjectMeta: metav1.ObjectMeta{ 3593 Name: "config", 3594 }, 3595 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 3596 PolicyName: "xyzlimit-scale.example.com", 3597 ParamRef: &admissionregistration.ParamRef{ 3598 Name: "xyzlimit-scale-setting.example.com", 3599 ParameterNotFoundAction: ptr(admissionregistration.DenyAction), 3600 }, MatchResources: &admissionregistration.MatchResources{ 3601 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 3602 RuleWithOperations: admissionregistration.RuleWithOperations{ 3603 Operations: []admissionregistration.OperationType{"CREATE", ""}, 3604 Rule: admissionregistration.Rule{ 3605 APIGroups: []string{"a"}, 3606 APIVersions: []string{"a"}, 3607 Resources: []string{"a"}, 3608 }, 3609 }, 3610 }}, 3611 }, 3612 }, 3613 }, 3614 expectedError: `Unsupported value: ""`, 3615 }, { 3616 name: "operation must be either create/update/delete/connect", 3617 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3618 ObjectMeta: metav1.ObjectMeta{ 3619 Name: "config", 3620 }, 3621 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 3622 PolicyName: "xyzlimit-scale.example.com", 3623 ParamRef: &admissionregistration.ParamRef{ 3624 Name: "xyzlimit-scale-setting.example.com", 3625 ParameterNotFoundAction: ptr(admissionregistration.DenyAction), 3626 }, MatchResources: &admissionregistration.MatchResources{ 3627 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 3628 RuleWithOperations: admissionregistration.RuleWithOperations{ 3629 Operations: []admissionregistration.OperationType{"PATCH"}, 3630 Rule: admissionregistration.Rule{ 3631 APIGroups: []string{"a"}, 3632 APIVersions: []string{"a"}, 3633 Resources: []string{"a"}, 3634 }, 3635 }, 3636 }}, 3637 }, 3638 }, 3639 }, 3640 expectedError: `Unsupported value: "PATCH"`, 3641 }, { 3642 name: "wildcard operation cannot be mixed with other strings", 3643 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3644 ObjectMeta: metav1.ObjectMeta{ 3645 Name: "config", 3646 }, 3647 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 3648 PolicyName: "xyzlimit-scale.example.com", 3649 ParamRef: &admissionregistration.ParamRef{ 3650 Name: "xyzlimit-scale-setting.example.com", 3651 ParameterNotFoundAction: ptr(admissionregistration.DenyAction), 3652 }, 3653 ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny}, 3654 MatchResources: &admissionregistration.MatchResources{ 3655 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 3656 RuleWithOperations: admissionregistration.RuleWithOperations{ 3657 Operations: []admissionregistration.OperationType{"CREATE", "*"}, 3658 Rule: admissionregistration.Rule{ 3659 APIGroups: []string{"a"}, 3660 APIVersions: []string{"a"}, 3661 Resources: []string{"a"}, 3662 }, 3663 }, 3664 }}, 3665 }, 3666 }, 3667 }, 3668 expectedError: `if '*' is present, must not specify other operations`, 3669 }, { 3670 name: `resource "*" can co-exist with resources that have subresources`, 3671 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3672 ObjectMeta: metav1.ObjectMeta{ 3673 Name: "config", 3674 }, 3675 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 3676 PolicyName: "xyzlimit-scale.example.com", 3677 ParamRef: &admissionregistration.ParamRef{ 3678 Name: "xyzlimit-scale-setting.example.com", 3679 ParameterNotFoundAction: ptr(admissionregistration.DenyAction), 3680 }, 3681 ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny}, 3682 MatchResources: &admissionregistration.MatchResources{ 3683 NamespaceSelector: &metav1.LabelSelector{ 3684 MatchLabels: map[string]string{"a": "b"}, 3685 }, 3686 ObjectSelector: &metav1.LabelSelector{ 3687 MatchLabels: map[string]string{"a": "b"}, 3688 }, 3689 MatchPolicy: func() *admissionregistration.MatchPolicyType { 3690 r := admissionregistration.MatchPolicyType("Exact") 3691 return &r 3692 }(), 3693 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 3694 RuleWithOperations: admissionregistration.RuleWithOperations{ 3695 Operations: []admissionregistration.OperationType{"CREATE"}, 3696 Rule: admissionregistration.Rule{ 3697 APIGroups: []string{"a"}, 3698 APIVersions: []string{"a"}, 3699 Resources: []string{"*", "a/b", "a/*", "*/b"}, 3700 }, 3701 }, 3702 }}, 3703 }, 3704 }, 3705 }, 3706 }, { 3707 name: `resource "*" cannot mix with resources that don't have subresources`, 3708 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3709 ObjectMeta: metav1.ObjectMeta{ 3710 Name: "config", 3711 }, 3712 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 3713 PolicyName: "xyzlimit-scale.example.com", 3714 ParamRef: &admissionregistration.ParamRef{ 3715 Name: "xyzlimit-scale-setting.example.com", 3716 ParameterNotFoundAction: ptr(admissionregistration.DenyAction), 3717 }, 3718 ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny}, 3719 MatchResources: &admissionregistration.MatchResources{ 3720 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 3721 RuleWithOperations: admissionregistration.RuleWithOperations{ 3722 Operations: []admissionregistration.OperationType{"CREATE"}, 3723 Rule: admissionregistration.Rule{ 3724 APIGroups: []string{"a"}, 3725 APIVersions: []string{"a"}, 3726 Resources: []string{"*", "a"}, 3727 }, 3728 }, 3729 }}, 3730 }, 3731 }, 3732 }, 3733 expectedError: `if '*' is present, must not specify other resources without subresources`, 3734 }, { 3735 name: "resource a/* cannot mix with a/x", 3736 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3737 ObjectMeta: metav1.ObjectMeta{ 3738 Name: "config", 3739 }, 3740 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 3741 PolicyName: "xyzlimit-scale.example.com", 3742 ParamRef: &admissionregistration.ParamRef{ 3743 Name: "xyzlimit-scale-setting.example.com", 3744 ParameterNotFoundAction: ptr(admissionregistration.DenyAction), 3745 }, 3746 ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny}, 3747 MatchResources: &admissionregistration.MatchResources{ 3748 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 3749 RuleWithOperations: admissionregistration.RuleWithOperations{ 3750 Operations: []admissionregistration.OperationType{"CREATE"}, 3751 Rule: admissionregistration.Rule{ 3752 APIGroups: []string{"a"}, 3753 APIVersions: []string{"a"}, 3754 Resources: []string{"a/*", "a/x"}, 3755 }, 3756 }, 3757 }}, 3758 }, 3759 }, 3760 }, 3761 expectedError: `spec.matchResouces.resourceRules[0].resources[1]: Invalid value: "a/x": if 'a/*' is present, must not specify a/x`, 3762 }, { 3763 name: "resource a/* can mix with a", 3764 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3765 ObjectMeta: metav1.ObjectMeta{ 3766 Name: "config", 3767 }, 3768 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 3769 PolicyName: "xyzlimit-scale.example.com", 3770 ParamRef: &admissionregistration.ParamRef{ 3771 Name: "xyzlimit-scale-setting.example.com", 3772 ParameterNotFoundAction: ptr(admissionregistration.DenyAction), 3773 }, 3774 ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny}, 3775 MatchResources: &admissionregistration.MatchResources{ 3776 NamespaceSelector: &metav1.LabelSelector{ 3777 MatchLabels: map[string]string{"a": "b"}, 3778 }, 3779 ObjectSelector: &metav1.LabelSelector{ 3780 MatchLabels: map[string]string{"a": "b"}, 3781 }, 3782 MatchPolicy: func() *admissionregistration.MatchPolicyType { 3783 r := admissionregistration.MatchPolicyType("Exact") 3784 return &r 3785 }(), 3786 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 3787 RuleWithOperations: admissionregistration.RuleWithOperations{ 3788 Operations: []admissionregistration.OperationType{"CREATE"}, 3789 Rule: admissionregistration.Rule{ 3790 APIGroups: []string{"a"}, 3791 APIVersions: []string{"a"}, 3792 Resources: []string{"a/*", "a"}, 3793 }, 3794 }, 3795 }}, 3796 }, 3797 }, 3798 }, 3799 }, { 3800 name: "resource */a cannot mix with x/a", 3801 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3802 ObjectMeta: metav1.ObjectMeta{ 3803 Name: "config", 3804 }, 3805 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 3806 PolicyName: "xyzlimit-scale.example.com", 3807 ParamRef: &admissionregistration.ParamRef{ 3808 Name: "xyzlimit-scale-setting.example.com", 3809 ParameterNotFoundAction: ptr(admissionregistration.DenyAction), 3810 }, 3811 ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny}, 3812 MatchResources: &admissionregistration.MatchResources{ 3813 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 3814 RuleWithOperations: admissionregistration.RuleWithOperations{ 3815 Operations: []admissionregistration.OperationType{"CREATE"}, 3816 Rule: admissionregistration.Rule{ 3817 APIGroups: []string{"a"}, 3818 APIVersions: []string{"a"}, 3819 Resources: []string{"*/a", "x/a"}, 3820 }, 3821 }, 3822 }}, 3823 }, 3824 }, 3825 }, 3826 expectedError: `spec.matchResouces.resourceRules[0].resources[1]: Invalid value: "x/a": if '*/a' is present, must not specify x/a`, 3827 }, { 3828 name: "resource */* cannot mix with other resources", 3829 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3830 ObjectMeta: metav1.ObjectMeta{ 3831 Name: "config", 3832 }, 3833 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 3834 PolicyName: "xyzlimit-scale.example.com", 3835 ParamRef: &admissionregistration.ParamRef{ 3836 Name: "xyzlimit-scale-setting.example.com", 3837 ParameterNotFoundAction: ptr(admissionregistration.DenyAction), 3838 }, 3839 ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny}, 3840 MatchResources: &admissionregistration.MatchResources{ 3841 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 3842 RuleWithOperations: admissionregistration.RuleWithOperations{ 3843 Operations: []admissionregistration.OperationType{"CREATE"}, 3844 Rule: admissionregistration.Rule{ 3845 APIGroups: []string{"a"}, 3846 APIVersions: []string{"a"}, 3847 Resources: []string{"*/*", "a"}, 3848 }, 3849 }, 3850 }}, 3851 }, 3852 }, 3853 }, 3854 expectedError: `spec.matchResouces.resourceRules[0].resources: Invalid value: []string{"*/*", "a"}: if '*/*' is present, must not specify other resources`, 3855 }, { 3856 name: "validationActions must be unique", 3857 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3858 ObjectMeta: metav1.ObjectMeta{ 3859 Name: "config", 3860 }, 3861 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 3862 PolicyName: "xyzlimit-scale.example.com", 3863 ParamRef: &admissionregistration.ParamRef{ 3864 Name: "xyzlimit-scale-setting.example.com", 3865 ParameterNotFoundAction: ptr(admissionregistration.DenyAction), 3866 }, 3867 ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny, admissionregistration.Deny}, 3868 }, 3869 }, 3870 expectedError: `spec.validationActions[1]: Duplicate value: "Deny"`, 3871 }, { 3872 name: "validationActions must contain supported values", 3873 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3874 ObjectMeta: metav1.ObjectMeta{ 3875 Name: "config", 3876 }, 3877 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 3878 PolicyName: "xyzlimit-scale.example.com", 3879 ParamRef: &admissionregistration.ParamRef{ 3880 Name: "xyzlimit-scale-setting.example.com", 3881 ParameterNotFoundAction: ptr(admissionregistration.DenyAction), 3882 }, 3883 ValidationActions: []admissionregistration.ValidationAction{admissionregistration.ValidationAction("illegal")}, 3884 }, 3885 }, 3886 expectedError: `Unsupported value: "illegal": supported values: "Audit", "Deny", "Warn"`, 3887 }, { 3888 name: "paramRef selector must not be set when name is set", 3889 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3890 ObjectMeta: metav1.ObjectMeta{ 3891 Name: "config", 3892 }, 3893 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 3894 PolicyName: "xyzlimit-scale.example.com", 3895 ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny}, 3896 ParamRef: &admissionregistration.ParamRef{ 3897 Name: "xyzlimit-scale-setting.example.com", 3898 Selector: &metav1.LabelSelector{ 3899 MatchLabels: map[string]string{ 3900 "label": "value", 3901 }, 3902 }, 3903 ParameterNotFoundAction: ptr(admissionregistration.DenyAction), 3904 }, 3905 }, 3906 }, 3907 expectedError: `spec.paramRef.name: Forbidden: name and selector are mutually exclusive, spec.paramRef.selector: Forbidden: name and selector are mutually exclusive`, 3908 }, { 3909 name: "paramRef parameterNotFoundAction must be set", 3910 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3911 ObjectMeta: metav1.ObjectMeta{ 3912 Name: "config", 3913 }, 3914 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 3915 PolicyName: "xyzlimit-scale.example.com", 3916 ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny}, 3917 ParamRef: &admissionregistration.ParamRef{ 3918 Name: "xyzlimit-scale-setting.example.com", 3919 }, 3920 }, 3921 }, 3922 expectedError: "spec.paramRef.parameterNotFoundAction: Required value", 3923 }, { 3924 name: "paramRef parameterNotFoundAction must be an valid value", 3925 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3926 ObjectMeta: metav1.ObjectMeta{ 3927 Name: "config", 3928 }, 3929 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 3930 PolicyName: "xyzlimit-scale.example.com", 3931 ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny}, 3932 ParamRef: &admissionregistration.ParamRef{ 3933 Name: "xyzlimit-scale-setting.example.com", 3934 ParameterNotFoundAction: ptr(admissionregistration.ParameterNotFoundActionType("invalid")), 3935 }, 3936 }, 3937 }, 3938 expectedError: `spec.paramRef.parameterNotFoundAction: Unsupported value: "invalid": supported values: "Deny", "Allow"`, 3939 }, { 3940 name: "paramRef one of name or selector", 3941 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3942 ObjectMeta: metav1.ObjectMeta{ 3943 Name: "config", 3944 }, 3945 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 3946 PolicyName: "xyzlimit-scale.example.com", 3947 ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny}, 3948 ParamRef: &admissionregistration.ParamRef{ 3949 ParameterNotFoundAction: ptr(admissionregistration.DenyAction), 3950 }, 3951 }, 3952 }, 3953 expectedError: `one of name or selector must be specified`, 3954 }} 3955 for _, test := range tests { 3956 t.Run(test.name, func(t *testing.T) { 3957 errs := ValidateValidatingAdmissionPolicyBinding(test.config) 3958 err := errs.ToAggregate() 3959 if err != nil { 3960 if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { 3961 t.Errorf("expected to contain %s, got %s", e, a) 3962 } 3963 } else { 3964 if test.expectedError != "" { 3965 t.Errorf("unexpected no error, expected to contain %s", test.expectedError) 3966 } 3967 } 3968 }) 3969 3970 } 3971 } 3972 3973 func TestValidateValidatingAdmissionPolicyBindingUpdate(t *testing.T) { 3974 tests := []struct { 3975 name string 3976 oldconfig *admissionregistration.ValidatingAdmissionPolicyBinding 3977 config *admissionregistration.ValidatingAdmissionPolicyBinding 3978 expectedError string 3979 }{{ 3980 name: "should pass on valid new ValidatingAdmissionPolicyBinding", 3981 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 3982 ObjectMeta: metav1.ObjectMeta{ 3983 Name: "config", 3984 }, 3985 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 3986 PolicyName: "xyzlimit-scale.example.com", 3987 ParamRef: &admissionregistration.ParamRef{ 3988 Name: "xyzlimit-scale-setting.example.com", 3989 ParameterNotFoundAction: ptr(admissionregistration.DenyAction), 3990 }, 3991 ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny}, 3992 MatchResources: &admissionregistration.MatchResources{ 3993 NamespaceSelector: &metav1.LabelSelector{ 3994 MatchLabels: map[string]string{"a": "b"}, 3995 }, 3996 ObjectSelector: &metav1.LabelSelector{ 3997 MatchLabels: map[string]string{"a": "b"}, 3998 }, 3999 MatchPolicy: func() *admissionregistration.MatchPolicyType { 4000 r := admissionregistration.MatchPolicyType("Exact") 4001 return &r 4002 }(), 4003 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 4004 RuleWithOperations: admissionregistration.RuleWithOperations{ 4005 Operations: []admissionregistration.OperationType{"CREATE"}, 4006 Rule: admissionregistration.Rule{ 4007 APIGroups: []string{"a"}, 4008 APIVersions: []string{"a"}, 4009 Resources: []string{"a"}, 4010 }, 4011 }, 4012 }}, 4013 }, 4014 }, 4015 }, 4016 oldconfig: &admissionregistration.ValidatingAdmissionPolicyBinding{ 4017 ObjectMeta: metav1.ObjectMeta{ 4018 Name: "config", 4019 }, 4020 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 4021 PolicyName: "xyzlimit-scale.example.com", 4022 ParamRef: &admissionregistration.ParamRef{ 4023 Name: "xyzlimit-scale-setting.example.com", 4024 ParameterNotFoundAction: ptr(admissionregistration.DenyAction), 4025 }, 4026 ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny}, 4027 MatchResources: &admissionregistration.MatchResources{ 4028 NamespaceSelector: &metav1.LabelSelector{ 4029 MatchLabels: map[string]string{"a": "b"}, 4030 }, 4031 ObjectSelector: &metav1.LabelSelector{ 4032 MatchLabels: map[string]string{"a": "b"}, 4033 }, 4034 MatchPolicy: func() *admissionregistration.MatchPolicyType { 4035 r := admissionregistration.MatchPolicyType("Exact") 4036 return &r 4037 }(), 4038 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 4039 RuleWithOperations: admissionregistration.RuleWithOperations{ 4040 Operations: []admissionregistration.OperationType{"CREATE"}, 4041 Rule: admissionregistration.Rule{ 4042 APIGroups: []string{"a"}, 4043 APIVersions: []string{"a"}, 4044 Resources: []string{"a"}, 4045 }, 4046 }, 4047 }}, 4048 }, 4049 }, 4050 }, 4051 }, { 4052 name: "should pass on valid new ValidatingAdmissionPolicyBinding with invalid old ValidatingAdmissionPolicyBinding", 4053 config: &admissionregistration.ValidatingAdmissionPolicyBinding{ 4054 ObjectMeta: metav1.ObjectMeta{ 4055 Name: "config", 4056 }, 4057 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{ 4058 PolicyName: "xyzlimit-scale.example.com", 4059 ParamRef: &admissionregistration.ParamRef{ 4060 Name: "xyzlimit-scale-setting.example.com", 4061 ParameterNotFoundAction: ptr(admissionregistration.DenyAction), 4062 }, 4063 ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny}, 4064 MatchResources: &admissionregistration.MatchResources{ 4065 NamespaceSelector: &metav1.LabelSelector{ 4066 MatchLabels: map[string]string{"a": "b"}, 4067 }, 4068 ObjectSelector: &metav1.LabelSelector{ 4069 MatchLabels: map[string]string{"a": "b"}, 4070 }, 4071 MatchPolicy: func() *admissionregistration.MatchPolicyType { 4072 r := admissionregistration.MatchPolicyType("Exact") 4073 return &r 4074 }(), 4075 ResourceRules: []admissionregistration.NamedRuleWithOperations{{ 4076 RuleWithOperations: admissionregistration.RuleWithOperations{ 4077 Operations: []admissionregistration.OperationType{"CREATE"}, 4078 Rule: admissionregistration.Rule{ 4079 APIGroups: []string{"a"}, 4080 APIVersions: []string{"a"}, 4081 Resources: []string{"a"}, 4082 }, 4083 }, 4084 }}, 4085 }, 4086 }, 4087 }, 4088 oldconfig: &admissionregistration.ValidatingAdmissionPolicyBinding{ 4089 ObjectMeta: metav1.ObjectMeta{ 4090 Name: "!!!", 4091 }, 4092 Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{}, 4093 }, 4094 }} 4095 for _, test := range tests { 4096 t.Run(test.name, func(t *testing.T) { 4097 errs := ValidateValidatingAdmissionPolicyBindingUpdate(test.config, test.oldconfig) 4098 err := errs.ToAggregate() 4099 if err != nil { 4100 if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { 4101 t.Errorf("expected to contain %s, got %s", e, a) 4102 } 4103 } else { 4104 if test.expectedError != "" { 4105 t.Errorf("unexpected no error, expected to contain %s", test.expectedError) 4106 } 4107 } 4108 }) 4109 4110 } 4111 } 4112 4113 func TestValidateValidatingAdmissionPolicyStatus(t *testing.T) { 4114 for _, tc := range []struct { 4115 name string 4116 status *admissionregistration.ValidatingAdmissionPolicyStatus 4117 expectedError string 4118 }{{ 4119 name: "empty", 4120 status: &admissionregistration.ValidatingAdmissionPolicyStatus{}, 4121 }, { 4122 name: "type checking", 4123 status: &admissionregistration.ValidatingAdmissionPolicyStatus{ 4124 TypeChecking: &admissionregistration.TypeChecking{ 4125 ExpressionWarnings: []admissionregistration.ExpressionWarning{{ 4126 FieldRef: "spec.validations[0].expression", 4127 Warning: "message", 4128 }}, 4129 }, 4130 }, 4131 }, { 4132 name: "type checking bad json path", 4133 status: &admissionregistration.ValidatingAdmissionPolicyStatus{ 4134 TypeChecking: &admissionregistration.TypeChecking{ 4135 ExpressionWarnings: []admissionregistration.ExpressionWarning{{ 4136 FieldRef: "spec[foo]", 4137 Warning: "message", 4138 }}, 4139 }, 4140 }, 4141 expectedError: "invalid JSONPath: invalid array index foo", 4142 }, { 4143 name: "type checking missing warning", 4144 status: &admissionregistration.ValidatingAdmissionPolicyStatus{ 4145 TypeChecking: &admissionregistration.TypeChecking{ 4146 ExpressionWarnings: []admissionregistration.ExpressionWarning{{ 4147 FieldRef: "spec.validations[0].expression", 4148 }}, 4149 }, 4150 }, 4151 expectedError: "Required value", 4152 }, { 4153 name: "type checking missing fieldRef", 4154 status: &admissionregistration.ValidatingAdmissionPolicyStatus{ 4155 TypeChecking: &admissionregistration.TypeChecking{ 4156 ExpressionWarnings: []admissionregistration.ExpressionWarning{{ 4157 Warning: "message", 4158 }}, 4159 }, 4160 }, 4161 expectedError: "Required value", 4162 }, 4163 } { 4164 t.Run(tc.name, func(t *testing.T) { 4165 errs := validateValidatingAdmissionPolicyStatus(tc.status, field.NewPath("status")) 4166 err := errs.ToAggregate() 4167 if err != nil { 4168 if e, a := tc.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { 4169 t.Errorf("expected to contain %s, got %s", e, a) 4170 } 4171 } else { 4172 if tc.expectedError != "" { 4173 t.Errorf("unexpected no error, expected to contain %s", tc.expectedError) 4174 } 4175 } 4176 }) 4177 } 4178 } 4179 4180 func get65MatchConditions() []admissionregistration.MatchCondition { 4181 result := []admissionregistration.MatchCondition{} 4182 for i := 0; i < 65; i++ { 4183 result = append(result, admissionregistration.MatchCondition{ 4184 Name: fmt.Sprintf("test%v", i), 4185 Expression: "true", 4186 }) 4187 } 4188 return result 4189 }