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