diff --git a/pkg/apis/admissionregistration/validation/validation_test.go b/pkg/apis/admissionregistration/validation/validation_test.go index 33a2e054f62..b7b574a6343 100644 --- a/pkg/apis/admissionregistration/validation/validation_test.go +++ b/pkg/apis/admissionregistration/validation/validation_test.go @@ -45,8 +45,6 @@ func newValidatingWebhookConfiguration(hooks []admissionregistration.ValidatingW } } -// TODO: Add TestValidateMutatingWebhookConfiguration to test validation for mutating webhooks. - func TestValidateValidatingWebhookConfiguration(t *testing.T) { unknownSideEffect := admissionregistration.SideEffectClassUnknown validSideEffect := &unknownSideEffect @@ -977,3 +975,926 @@ func TestValidateValidatingWebhookConfigurationUpdate(t *testing.T) { } } + +func newMutatingWebhookConfiguration(hooks []admissionregistration.MutatingWebhook, defaultAdmissionReviewVersions bool) *admissionregistration.MutatingWebhookConfiguration { + // If the test case did not specify an AdmissionReviewVersions, default it so the test passes as + // this field will be defaulted in production code. + for i := range hooks { + if defaultAdmissionReviewVersions && len(hooks[i].AdmissionReviewVersions) == 0 { + hooks[i].AdmissionReviewVersions = []string{"v1beta1"} + } + } + return &admissionregistration.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Webhooks: hooks, + } +} + +func TestValidateMutatingWebhookConfiguration(t *testing.T) { + unknownSideEffect := admissionregistration.SideEffectClassUnknown + validSideEffect := &unknownSideEffect + validClientConfig := admissionregistration.WebhookClientConfig{ + URL: strPtr("https://example.com"), + } + tests := []struct { + name string + config *admissionregistration.MutatingWebhookConfiguration + gv schema.GroupVersion + expectedError string + }{ + { + name: "AdmissionReviewVersions are required", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + }, + }, false), + expectedError: `webhooks[0].admissionReviewVersions: Required value: must specify one of v1beta1`, + }, { + name: "should fail on bad AdmissionReviewVersion value", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + AdmissionReviewVersions: []string{"0v"}, + }, + }, true), + expectedError: `Invalid value: "0v": a DNS-1035 label`, + }, + { + name: "should pass on valid AdmissionReviewVersion", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + AdmissionReviewVersions: []string{"v1beta1"}, + }, + }, true), + expectedError: ``, + }, + { + name: "should pass on mix of accepted and unaccepted AdmissionReviewVersion", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + AdmissionReviewVersions: []string{"v1beta1", "invalid-version"}, + }, + }, true), + expectedError: ``, + }, + { + name: "should fail on invalid AdmissionReviewVersion", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + AdmissionReviewVersions: []string{"invalidVersion"}, + }, + }, true), + expectedError: `Invalid value: []string{"invalidVersion"}`, + }, + { + name: "should fail on duplicate AdmissionReviewVersion", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + AdmissionReviewVersions: []string{"v1beta1", "v1beta1"}, + }, + }, true), + expectedError: `Invalid value: "v1beta1": duplicate version`, + }, + { + name: "all Webhooks must have a fully qualified name", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + }, + { + Name: "k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + }, + { + Name: "", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + }, + }, true), + 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`, + }, + { + name: "Webhooks must have unique names when not created via v1beta1", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + }, + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + }, + }, true), + gv: schema.GroupVersion{Group: "foo", Version: "bar"}, + expectedError: `webhooks[1].name: Duplicate value: "webhook.k8s.io"`, + }, + { + name: "Webhooks can have duplicate names when created via v1beta1", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + }, + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + }, + }, true), + gv: schema.GroupVersion{Group: "admissionregistration.k8s.io", Version: "v1beta1"}, + expectedError: ``, + }, + { + name: "Operations must not be empty or nil", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + Rules: []admissionregistration.RuleWithOperations{ + { + Operations: []admissionregistration.OperationType{}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + { + Operations: nil, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, true), + expectedError: `webhooks[0].rules[0].operations: Required value, webhooks[0].rules[1].operations: Required value`, + }, + { + name: "\"\" is NOT a valid operation", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + Rules: []admissionregistration.RuleWithOperations{ + { + Operations: []admissionregistration.OperationType{"CREATE", ""}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, true), + expectedError: `Unsupported value: ""`, + }, + { + name: "operation must be either create/update/delete/connect", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + Rules: []admissionregistration.RuleWithOperations{ + { + Operations: []admissionregistration.OperationType{"PATCH"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, true), + expectedError: `Unsupported value: "PATCH"`, + }, + { + name: "wildcard operation cannot be mixed with other strings", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + Rules: []admissionregistration.RuleWithOperations{ + { + Operations: []admissionregistration.OperationType{"CREATE", "*"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }, true), + expectedError: `if '*' is present, must not specify other operations`, + }, + { + name: `resource "*" can co-exist with resources that have subresources`, + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + Rules: []admissionregistration.RuleWithOperations{ + { + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*", "a/b", "a/*", "*/b"}, + }, + }, + }, + }, + }, true), + }, + { + name: `resource "*" cannot mix with resources that don't have subresources`, + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + Rules: []admissionregistration.RuleWithOperations{ + { + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*", "a"}, + }, + }, + }, + }, + }, true), + expectedError: `if '*' is present, must not specify other resources without subresources`, + }, + { + name: "resource a/* cannot mix with a/x", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + Rules: []admissionregistration.RuleWithOperations{ + { + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a/*", "a/x"}, + }, + }, + }, + }, + }, true), + expectedError: `webhooks[0].rules[0].resources[1]: Invalid value: "a/x": if 'a/*' is present, must not specify a/x`, + }, + { + name: "resource a/* can mix with a", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + Rules: []admissionregistration.RuleWithOperations{ + { + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a/*", "a"}, + }, + }, + }, + }, + }, true), + }, + { + name: "resource */a cannot mix with x/a", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + Rules: []admissionregistration.RuleWithOperations{ + { + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*/a", "x/a"}, + }, + }, + }, + }, + }, true), + expectedError: `webhooks[0].rules[0].resources[1]: Invalid value: "x/a": if '*/a' is present, must not specify x/a`, + }, + { + name: "resource */* cannot mix with other resources", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + Rules: []admissionregistration.RuleWithOperations{ + { + Operations: []admissionregistration.OperationType{"CREATE"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"*/*", "a"}, + }, + }, + }, + }, + }, true), + expectedError: `webhooks[0].rules[0].resources: Invalid value: []string{"*/*", "a"}: if '*/*' is present, must not specify other resources`, + }, + { + name: "FailurePolicy can only be \"Ignore\" or \"Fail\"", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.FailurePolicyType("other") + return &r + }(), + }, + }, true), + expectedError: `webhooks[0].failurePolicy: Unsupported value: "other": supported values: "Fail", "Ignore"`, + }, + { + name: "AdmissionReviewVersions are required", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + }, + }, false), + expectedError: `webhooks[0].admissionReviewVersions: Required value: must specify one of v1beta1`, + }, + { + name: "SideEffects are required", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: nil, + }, + }, true), + expectedError: `webhooks[0].sideEffects: Required value: must specify one of None, NoneOnDryRun, Some, Unknown`, + }, + { + name: "SideEffects can only be \"Unknown\", \"None\", \"Some\", or \"NoneOnDryRun\"", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: func() *admissionregistration.SideEffectClass { + r := admissionregistration.SideEffectClass("other") + return &r + }(), + }, + }, true), + expectedError: `webhooks[0].sideEffects: Unsupported value: "other": supported values: "None", "NoneOnDryRun", "Some", "Unknown"`, + }, + { + name: "both service and URL missing", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: admissionregistration.WebhookClientConfig{}, + }, + }, true), + expectedError: `exactly one of`, + }, + { + name: "both service and URL provided", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: admissionregistration.WebhookClientConfig{ + Service: &admissionregistration.ServiceReference{ + Namespace: "ns", + Name: "n", + Port: 443, + }, + URL: strPtr("example.com/k8s/webhook"), + }, + }, + }, true), + expectedError: `[0].clientConfig: Required value: exactly one of url or service is required`, + }, + { + name: "blank URL", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: admissionregistration.WebhookClientConfig{ + URL: strPtr(""), + }, + }, + }, true), + expectedError: `[0].clientConfig.url: Invalid value: "": host must be provided`, + }, + { + name: "wrong scheme", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: admissionregistration.WebhookClientConfig{ + URL: strPtr("http://example.com"), + }, + }, + }, true), + expectedError: `https`, + }, + { + name: "missing host", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: admissionregistration.WebhookClientConfig{ + URL: strPtr("https:///fancy/webhook"), + }, + }, + }, true), + expectedError: `host must be provided`, + }, + { + name: "fragment", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: admissionregistration.WebhookClientConfig{ + URL: strPtr("https://example.com/#bookmark"), + }, + }, + }, true), + expectedError: `"bookmark": fragments are not permitted`, + }, + { + name: "query", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: admissionregistration.WebhookClientConfig{ + URL: strPtr("https://example.com?arg=value"), + }, + }, + }, true), + expectedError: `"arg=value": query parameters are not permitted`, + }, + { + name: "user", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: admissionregistration.WebhookClientConfig{ + URL: strPtr("https://harry.potter@example.com/"), + }, + }, + }, true), + expectedError: `"harry.potter": user information is not permitted`, + }, + { + name: "just totally wrong", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: admissionregistration.WebhookClientConfig{ + URL: strPtr("arg#backwards=thisis?html.index/port:host//:https"), + }, + }, + }, true), + expectedError: `host must be provided`, + }, + { + name: "path must start with slash", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: admissionregistration.WebhookClientConfig{ + Service: &admissionregistration.ServiceReference{ + Namespace: "ns", + Name: "n", + Path: strPtr("foo/"), + Port: 443, + }, + }, + }, + }, true), + expectedError: `clientConfig.service.path: Invalid value: "foo/": must start with a '/'`, + }, + { + name: "path accepts slash", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: admissionregistration.WebhookClientConfig{ + Service: &admissionregistration.ServiceReference{ + Namespace: "ns", + Name: "n", + Path: strPtr("/"), + Port: 443, + }, + }, + SideEffects: validSideEffect, + }, + }, true), + expectedError: ``, + }, + { + name: "path accepts no trailing slash", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: admissionregistration.WebhookClientConfig{ + Service: &admissionregistration.ServiceReference{ + Namespace: "ns", + Name: "n", + Path: strPtr("/foo"), + Port: 443, + }, + }, + SideEffects: validSideEffect, + }, + }, true), + expectedError: ``, + }, + { + name: "path fails //", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: admissionregistration.WebhookClientConfig{ + Service: &admissionregistration.ServiceReference{ + Namespace: "ns", + Name: "n", + Path: strPtr("//"), + Port: 443, + }, + }, + SideEffects: validSideEffect, + }, + }, true), + expectedError: `clientConfig.service.path: Invalid value: "//": segment[0] may not be empty`, + }, + { + name: "path no empty step", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: admissionregistration.WebhookClientConfig{ + Service: &admissionregistration.ServiceReference{ + Namespace: "ns", + Name: "n", + Path: strPtr("/foo//bar/"), + Port: 443, + }, + }, + SideEffects: validSideEffect, + }, + }, true), + expectedError: `clientConfig.service.path: Invalid value: "/foo//bar/": segment[1] may not be empty`, + }, { + name: "path no empty step 2", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: admissionregistration.WebhookClientConfig{ + Service: &admissionregistration.ServiceReference{ + Namespace: "ns", + Name: "n", + Path: strPtr("/foo/bar//"), + Port: 443, + }, + }, + SideEffects: validSideEffect, + }, + }, true), + expectedError: `clientConfig.service.path: Invalid value: "/foo/bar//": segment[2] may not be empty`, + }, + { + name: "path no non-subdomain", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: admissionregistration.WebhookClientConfig{ + Service: &admissionregistration.ServiceReference{ + Namespace: "ns", + Name: "n", + Path: strPtr("/apis/foo.bar/v1alpha1/--bad"), + Port: 443, + }, + }, + SideEffects: validSideEffect, + }, + }, true), + expectedError: `clientConfig.service.path: Invalid value: "/apis/foo.bar/v1alpha1/--bad": segment[3]: a DNS-1123 subdomain`, + }, + { + name: "invalid port 0", + config: newMutatingWebhookConfiguration( + []admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: admissionregistration.WebhookClientConfig{ + Service: &admissionregistration.ServiceReference{ + Namespace: "ns", + Name: "n", + Path: strPtr("https://apis/foo.bar"), + Port: 0, + }, + }, + SideEffects: validSideEffect, + }, + }, true), + expectedError: `Invalid value: 0: port is not valid: must be between 1 and 65535, inclusive`, + }, + { + name: "invalid port >65535", + config: newMutatingWebhookConfiguration( + []admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: admissionregistration.WebhookClientConfig{ + Service: &admissionregistration.ServiceReference{ + Namespace: "ns", + Name: "n", + Path: strPtr("https://apis/foo.bar"), + Port: 65536, + }, + }, + SideEffects: validSideEffect, + }, + }, true), + expectedError: `Invalid value: 65536: port is not valid: must be between 1 and 65535, inclusive`, + }, + { + name: "timeout seconds cannot be greater than 30", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + TimeoutSeconds: int32Ptr(31), + }, + }, true), + expectedError: `webhooks[0].timeoutSeconds: Invalid value: 31: the timeout value must be between 1 and 30 seconds`, + }, + { + name: "timeout seconds cannot be smaller than 1", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + TimeoutSeconds: int32Ptr(0), + }, + }, true), + expectedError: `webhooks[0].timeoutSeconds: Invalid value: 0: the timeout value must be between 1 and 30 seconds`, + }, + { + name: "timeout seconds must be positive", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + TimeoutSeconds: int32Ptr(-1), + }, + }, true), + expectedError: `webhooks[0].timeoutSeconds: Invalid value: -1: the timeout value must be between 1 and 30 seconds`, + }, + { + name: "valid timeout seconds", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + TimeoutSeconds: int32Ptr(1), + }, + { + Name: "webhook2.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + TimeoutSeconds: int32Ptr(15), + }, + { + Name: "webhook3.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + TimeoutSeconds: int32Ptr(30), + }, + }, true), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + errs := ValidateMutatingWebhookConfiguration(test.config, test.gv) + err := errs.ToAggregate() + if err != nil { + if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { + t.Errorf("expected to contain %s, got %s", e, a) + } + } else { + if test.expectedError != "" { + t.Errorf("unexpected no error, expected to contain %s", test.expectedError) + } + } + }) + + } +} + +func TestValidateMutatingWebhookConfigurationUpdate(t *testing.T) { + unknownSideEffect := admissionregistration.SideEffectClassUnknown + validSideEffect := &unknownSideEffect + validClientConfig := admissionregistration.WebhookClientConfig{ + URL: strPtr("https://example.com"), + } + tests := []struct { + name string + oldconfig *admissionregistration.MutatingWebhookConfiguration + config *admissionregistration.MutatingWebhookConfiguration + gv schema.GroupVersion + expectedError string + }{ + { + name: "should pass on valid new AdmissionReviewVersion", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + AdmissionReviewVersions: []string{"v1beta1"}, + }, + }, true), + oldconfig: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + }, + }, true), + expectedError: ``, + }, + { + name: "should pass on invalid AdmissionReviewVersion with invalid previous versions", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + AdmissionReviewVersions: []string{"invalid-v1", "invalid-v2"}, + }, + }, true), + oldconfig: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + AdmissionReviewVersions: []string{"invalid-v0"}, + }, + }, true), + expectedError: ``, + }, + { + name: "should fail on invalid AdmissionReviewVersion with valid previous versions", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + AdmissionReviewVersions: []string{"invalid-v1"}, + }, + }, true), + oldconfig: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + AdmissionReviewVersions: []string{"v1beta1", "invalid-v1"}, + }, + }, true), + expectedError: `Invalid value: []string{"invalid-v1"}`, + }, + { + name: "should fail on invalid AdmissionReviewVersion with missing previous versions", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + AdmissionReviewVersions: []string{"invalid-v1"}, + }, + }, true), + oldconfig: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + }, + }, false), + expectedError: `Invalid value: []string{"invalid-v1"}`, + }, + { + name: "Webhooks can have duplicate names when old config has duplicate names", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + }, + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + }, + }, true), + oldconfig: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + }, + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + }, + }, true), + gv: schema.GroupVersion{Group: "foo", Version: "bar"}, + expectedError: ``, + }, + { + name: "Webhooks can have duplicate names when updated via v1beta1", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + }, + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + }, + }, true), + oldconfig: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: validSideEffect, + }, + }, false), + gv: schema.GroupVersion{Group: "admissionregistration.k8s.io", Version: "v1beta1"}, + expectedError: ``, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + errs := ValidateMutatingWebhookConfigurationUpdate(test.config, test.oldconfig, test.gv) + err := errs.ToAggregate() + if err != nil { + if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { + t.Errorf("expected to contain %s, got %s", e, a) + } + } else { + if test.expectedError != "" { + t.Errorf("unexpected no error, expected to contain %s", test.expectedError) + } + } + }) + + } +}