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