diff --git a/hack/.golint_failures b/hack/.golint_failures index e3ce8377b9f..e1743258e59 100644 --- a/hack/.golint_failures +++ b/hack/.golint_failures @@ -423,6 +423,7 @@ staging/src/k8s.io/apimachinery/pkg/watch staging/src/k8s.io/apiserver/pkg/admission staging/src/k8s.io/apiserver/pkg/admission/configuration staging/src/k8s.io/apiserver/pkg/admission/initializer +staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1 staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts diff --git a/pkg/apis/admissionregistration/fuzzer/fuzzer.go b/pkg/apis/admissionregistration/fuzzer/fuzzer.go index 45f226ff655..6e84fa54502 100644 --- a/pkg/apis/admissionregistration/fuzzer/fuzzer.go +++ b/pkg/apis/admissionregistration/fuzzer/fuzzer.go @@ -33,7 +33,21 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { obj.Scope = &s } }, - func(obj *admissionregistration.Webhook, c fuzz.Continue) { + func(obj *admissionregistration.ValidatingWebhook, c fuzz.Continue) { + c.FuzzNoCustom(obj) // fuzz self without calling this function again + p := admissionregistration.FailurePolicyType("Fail") + obj.FailurePolicy = &p + m := admissionregistration.MatchPolicyType("Exact") + obj.MatchPolicy = &m + s := admissionregistration.SideEffectClassUnknown + obj.SideEffects = &s + if obj.TimeoutSeconds == nil { + i := int32(30) + obj.TimeoutSeconds = &i + } + obj.AdmissionReviewVersions = []string{"v1beta1"} + }, + func(obj *admissionregistration.MutatingWebhook, c fuzz.Continue) { c.FuzzNoCustom(obj) // fuzz self without calling this function again p := admissionregistration.FailurePolicyType("Fail") obj.FailurePolicy = &p diff --git a/pkg/apis/admissionregistration/types.go b/pkg/apis/admissionregistration/types.go index a17ceb205e0..cb1624b59ad 100644 --- a/pkg/apis/admissionregistration/types.go +++ b/pkg/apis/admissionregistration/types.go @@ -123,7 +123,7 @@ type ValidatingWebhookConfiguration struct { metav1.ObjectMeta // Webhooks is a list of webhooks and the affected resources and operations. // +optional - Webhooks []Webhook + Webhooks []ValidatingWebhook } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -149,7 +149,7 @@ type MutatingWebhookConfiguration struct { metav1.ObjectMeta // Webhooks is a list of webhooks and the affected resources and operations. // +optional - Webhooks []Webhook + Webhooks []MutatingWebhook } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -165,8 +165,118 @@ type MutatingWebhookConfigurationList struct { Items []MutatingWebhookConfiguration } -// Webhook describes an admission webhook and the resources and operations it applies to. -type Webhook struct { +// ValidatingWebhook describes an admission webhook and the resources and operations it applies to. +type ValidatingWebhook struct { + // The name of the admission webhook. + // Name should be fully qualified, e.g., imagepolicy.kubernetes.io, where + // "imagepolicy" is the name of the webhook, and kubernetes.io is the name + // of the organization. + // Required. + Name string + + // ClientConfig defines how to communicate with the hook. + // Required + ClientConfig WebhookClientConfig + + // Rules describes what operations on what resources/subresources the webhook cares about. + // The webhook cares about an operation if it matches _any_ Rule. + Rules []RuleWithOperations + + // FailurePolicy defines how unrecognized errors from the admission endpoint are handled - + // allowed values are Ignore or Fail. Defaults to Ignore. + // +optional + FailurePolicy *FailurePolicyType + + // matchPolicy defines how the "rules" list is used to match incoming requests. + // Allowed values are "Exact" or "Equivalent". + // + // - Exact: match a request only if it exactly matches a specified rule. + // For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1, + // but "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`, + // a request to apps/v1beta1 or extensions/v1beta1 would not be sent to the webhook. + // + // - Equivalent: match a request if modifies a resource listed in rules, even via another API group or version. + // For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1, + // and "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`, + // a request to apps/v1beta1 or extensions/v1beta1 would be converted to apps/v1 and sent to the webhook. + // + // +optional + MatchPolicy *MatchPolicyType + + // NamespaceSelector decides whether to run the webhook on an object based + // on whether the namespace for that object matches the selector. If the + // object itself is a namespace, the matching is performed on + // object.metadata.labels. If the object is another cluster scoped resource, + // it never skips the webhook. + // + // For example, to run the webhook on any objects whose namespace is not + // associated with "runlevel" of "0" or "1"; you will set the selector as + // follows: + // "namespaceSelector": { + // "matchExpressions": [ + // { + // "key": "runlevel", + // "operator": "NotIn", + // "values": [ + // "0", + // "1" + // ] + // } + // ] + // } + // + // If instead you want to only run the webhook on any objects whose + // namespace is associated with the "environment" of "prod" or "staging"; + // you will set the selector as follows: + // "namespaceSelector": { + // "matchExpressions": [ + // { + // "key": "environment", + // "operator": "In", + // "values": [ + // "prod", + // "staging" + // ] + // } + // ] + // } + // + // See + // https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + // for more examples of label selectors. + // + // Default to the empty LabelSelector, which matches everything. + // +optional + NamespaceSelector *metav1.LabelSelector + + // SideEffects states whether this webhookk has side effects. + // Acceptable values are: Unknown, None, Some, NoneOnDryRun + // Webhooks with side effects MUST implement a reconciliation system, since a request may be + // rejected by a future step in the admission change and the side effects therefore need to be undone. + // Requests with the dryRun attribute will be auto-rejected if they match a webhook with + // sideEffects == Unknown or Some. Defaults to Unknown. + // +optional + SideEffects *SideEffectClass + + // TimeoutSeconds specifies the timeout for this webhook. After the timeout passes, + // the webhook call will be ignored or the API call will fail based on the + // failure policy. + // The timeout value must be between 1 and 30 seconds. + // +optional + TimeoutSeconds *int32 + + // AdmissionReviewVersions is an ordered list of preferred `AdmissionReview` + // versions the Webhook expects. API server will try to use first version in + // the list which it supports. If none of the versions specified in this list + // supported by API server, validation will fail for this object. + // If the webhook configuration has already been persisted with a version apiserver + // does not understand, calls to the webhook will fail and be subject to the failure policy. + // +optional + AdmissionReviewVersions []string +} + +// MutatingWebhook describes an admission webhook and the resources and operations it applies to. +type MutatingWebhook struct { // The name of the admission webhook. // Name should be fully qualified, e.g., imagepolicy.kubernetes.io, where // "imagepolicy" is the name of the webhook, and kubernetes.io is the name diff --git a/pkg/apis/admissionregistration/v1beta1/defaults.go b/pkg/apis/admissionregistration/v1beta1/defaults.go index 2ae7d62d247..b529dfd00bb 100644 --- a/pkg/apis/admissionregistration/v1beta1/defaults.go +++ b/pkg/apis/admissionregistration/v1beta1/defaults.go @@ -27,7 +27,35 @@ func addDefaultingFuncs(scheme *runtime.Scheme) error { return RegisterDefaults(scheme) } -func SetDefaults_Webhook(obj *admissionregistrationv1beta1.Webhook) { +func SetDefaults_ValidatingWebhook(obj *admissionregistrationv1beta1.ValidatingWebhook) { + if obj.FailurePolicy == nil { + policy := admissionregistrationv1beta1.Ignore + obj.FailurePolicy = &policy + } + if obj.MatchPolicy == nil { + policy := admissionregistrationv1beta1.Exact + obj.MatchPolicy = &policy + } + if obj.NamespaceSelector == nil { + selector := metav1.LabelSelector{} + obj.NamespaceSelector = &selector + } + if obj.SideEffects == nil { + // TODO: revisit/remove this default and possibly make the field required when promoting to v1 + unknown := admissionregistrationv1beta1.SideEffectClassUnknown + obj.SideEffects = &unknown + } + if obj.TimeoutSeconds == nil { + obj.TimeoutSeconds = new(int32) + *obj.TimeoutSeconds = 30 + } + + if len(obj.AdmissionReviewVersions) == 0 { + obj.AdmissionReviewVersions = []string{admissionregistrationv1beta1.SchemeGroupVersion.Version} + } +} + +func SetDefaults_MutatingWebhook(obj *admissionregistrationv1beta1.MutatingWebhook) { if obj.FailurePolicy == nil { policy := admissionregistrationv1beta1.Ignore obj.FailurePolicy = &policy diff --git a/pkg/apis/admissionregistration/validation/validation.go b/pkg/apis/admissionregistration/validation/validation.go index 5cb804a420e..8fc4efe3544 100644 --- a/pkg/apis/admissionregistration/validation/validation.go +++ b/pkg/apis/admissionregistration/validation/validation.go @@ -201,7 +201,7 @@ func ValidateValidatingWebhookConfiguration(e *admissionregistration.ValidatingW func validateValidatingWebhookConfiguration(e *admissionregistration.ValidatingWebhookConfiguration, requireRecognizedVersion bool) field.ErrorList { allErrors := genericvalidation.ValidateObjectMeta(&e.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) for i, hook := range e.Webhooks { - allErrors = append(allErrors, validateWebhook(&hook, field.NewPath("webhooks").Index(i))...) + allErrors = append(allErrors, validateValidatingWebhook(&hook, field.NewPath("webhooks").Index(i))...) allErrors = append(allErrors, validateAdmissionReviewVersions(hook.AdmissionReviewVersions, requireRecognizedVersion, field.NewPath("webhooks").Index(i).Child("admissionReviewVersions"))...) } return allErrors @@ -214,13 +214,50 @@ func ValidateMutatingWebhookConfiguration(e *admissionregistration.MutatingWebho func validateMutatingWebhookConfiguration(e *admissionregistration.MutatingWebhookConfiguration, requireRecognizedVersion bool) field.ErrorList { allErrors := genericvalidation.ValidateObjectMeta(&e.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) for i, hook := range e.Webhooks { - allErrors = append(allErrors, validateWebhook(&hook, field.NewPath("webhooks").Index(i))...) + allErrors = append(allErrors, validateMutatingWebhook(&hook, field.NewPath("webhooks").Index(i))...) allErrors = append(allErrors, validateAdmissionReviewVersions(hook.AdmissionReviewVersions, requireRecognizedVersion, field.NewPath("webhooks").Index(i).Child("admissionReviewVersions"))...) } return allErrors } -func validateWebhook(hook *admissionregistration.Webhook, fldPath *field.Path) field.ErrorList { +func validateValidatingWebhook(hook *admissionregistration.ValidatingWebhook, fldPath *field.Path) field.ErrorList { + var allErrors field.ErrorList + // hook.Name must be fully qualified + allErrors = append(allErrors, utilvalidation.IsFullyQualifiedName(fldPath.Child("name"), hook.Name)...) + + for i, rule := range hook.Rules { + allErrors = append(allErrors, validateRuleWithOperations(&rule, fldPath.Child("rules").Index(i))...) + } + if hook.FailurePolicy != nil && !supportedFailurePolicies.Has(string(*hook.FailurePolicy)) { + allErrors = append(allErrors, field.NotSupported(fldPath.Child("failurePolicy"), *hook.FailurePolicy, supportedFailurePolicies.List())) + } + if hook.MatchPolicy != nil && !supportedMatchPolicies.Has(string(*hook.MatchPolicy)) { + allErrors = append(allErrors, field.NotSupported(fldPath.Child("matchPolicy"), *hook.MatchPolicy, supportedMatchPolicies.List())) + } + if hook.SideEffects != nil && !supportedSideEffectClasses.Has(string(*hook.SideEffects)) { + allErrors = append(allErrors, field.NotSupported(fldPath.Child("sideEffects"), *hook.SideEffects, supportedSideEffectClasses.List())) + } + if hook.TimeoutSeconds != nil && (*hook.TimeoutSeconds > 30 || *hook.TimeoutSeconds < 1) { + allErrors = append(allErrors, field.Invalid(fldPath.Child("timeoutSeconds"), *hook.TimeoutSeconds, "the timeout value must be between 1 and 30 seconds")) + } + + if hook.NamespaceSelector != nil { + allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.NamespaceSelector, fldPath.Child("namespaceSelector"))...) + } + + cc := hook.ClientConfig + switch { + case (cc.URL == nil) == (cc.Service == nil): + allErrors = append(allErrors, field.Required(fldPath.Child("clientConfig"), "exactly one of url or service is required")) + case cc.URL != nil: + allErrors = append(allErrors, webhook.ValidateWebhookURL(fldPath.Child("clientConfig").Child("url"), *cc.URL, true)...) + case cc.Service != nil: + allErrors = append(allErrors, webhook.ValidateWebhookService(fldPath.Child("clientConfig").Child("service"), cc.Service.Name, cc.Service.Namespace, cc.Service.Path, cc.Service.Port)...) + } + return allErrors +} + +func validateMutatingWebhook(hook *admissionregistration.MutatingWebhook, fldPath *field.Path) field.ErrorList { var allErrors field.ErrorList // hook.Name must be fully qualified allErrors = append(allErrors, utilvalidation.IsFullyQualifiedName(fldPath.Child("name"), hook.Name)...) @@ -309,9 +346,27 @@ func validateRuleWithOperations(ruleWithOperations *admissionregistration.RuleWi return allErrors } -// hasAcceptedAdmissionReviewVersions returns true if all webhooks have at least one +// mutatingHasAcceptedAdmissionReviewVersions returns true if all webhooks have at least one // admission review version this apiserver accepts. -func hasAcceptedAdmissionReviewVersions(webhooks []admissionregistration.Webhook) bool { +func mutatingHasAcceptedAdmissionReviewVersions(webhooks []admissionregistration.MutatingWebhook) bool { + for _, hook := range webhooks { + hasRecognizedVersion := false + for _, version := range hook.AdmissionReviewVersions { + if isAcceptedAdmissionReviewVersion(version) { + hasRecognizedVersion = true + break + } + } + if !hasRecognizedVersion && len(hook.AdmissionReviewVersions) > 0 { + return false + } + } + return true +} + +// validatingHasAcceptedAdmissionReviewVersions returns true if all webhooks have at least one +// admission review version this apiserver accepts. +func validatingHasAcceptedAdmissionReviewVersions(webhooks []admissionregistration.ValidatingWebhook) bool { for _, hook := range webhooks { hasRecognizedVersion := false for _, version := range hook.AdmissionReviewVersions { @@ -328,9 +383,9 @@ func hasAcceptedAdmissionReviewVersions(webhooks []admissionregistration.Webhook } func ValidateValidatingWebhookConfigurationUpdate(newC, oldC *admissionregistration.ValidatingWebhookConfiguration) field.ErrorList { - return validateValidatingWebhookConfiguration(newC, hasAcceptedAdmissionReviewVersions(oldC.Webhooks)) + return validateValidatingWebhookConfiguration(newC, validatingHasAcceptedAdmissionReviewVersions(oldC.Webhooks)) } func ValidateMutatingWebhookConfigurationUpdate(newC, oldC *admissionregistration.MutatingWebhookConfiguration) field.ErrorList { - return validateMutatingWebhookConfiguration(newC, hasAcceptedAdmissionReviewVersions(oldC.Webhooks)) + return validateMutatingWebhookConfiguration(newC, mutatingHasAcceptedAdmissionReviewVersions(oldC.Webhooks)) } diff --git a/pkg/apis/admissionregistration/validation/validation_test.go b/pkg/apis/admissionregistration/validation/validation_test.go index e206a549eec..73c08eda43b 100644 --- a/pkg/apis/admissionregistration/validation/validation_test.go +++ b/pkg/apis/admissionregistration/validation/validation_test.go @@ -28,7 +28,7 @@ func strPtr(s string) *string { return &s } func int32Ptr(i int32) *int32 { return &i } -func newValidatingWebhookConfiguration(hooks []admissionregistration.Webhook, defaultAdmissionReviewVersions bool) *admissionregistration.ValidatingWebhookConfiguration { +func newValidatingWebhookConfiguration(hooks []admissionregistration.ValidatingWebhook, defaultAdmissionReviewVersions bool) *admissionregistration.ValidatingWebhookConfiguration { // 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 { @@ -57,7 +57,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }{ { name: "should fail on bad AdmissionReviewVersion value", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -68,7 +68,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "should pass on valid AdmissionReviewVersion", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -79,7 +79,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "should pass on mix of accepted and unaccepted AdmissionReviewVersion", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -90,7 +90,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "should fail on invalid AdmissionReviewVersion", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -101,7 +101,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "should fail on duplicate AdmissionReviewVersion", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -112,7 +112,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "all Webhooks must have a fully qualified name", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -130,7 +130,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "Operations must not be empty or nil", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", Rules: []admissionregistration.RuleWithOperations{ @@ -157,7 +157,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "\"\" is NOT a valid operation", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", Rules: []admissionregistration.RuleWithOperations{ @@ -176,7 +176,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "operation must be either create/update/delete/connect", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", Rules: []admissionregistration.RuleWithOperations{ @@ -195,7 +195,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "wildcard operation cannot be mixed with other strings", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", Rules: []admissionregistration.RuleWithOperations{ @@ -214,7 +214,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: `resource "*" can co-exist with resources that have subresources`, - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -233,7 +233,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: `resource "*" cannot mix with resources that don't have subresources`, - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -253,7 +253,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "resource a/* cannot mix with a/x", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -273,7 +273,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "resource a/* can mix with a", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -292,7 +292,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "resource */a cannot mix with x/a", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -312,7 +312,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "resource */* cannot mix with other resources", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -332,7 +332,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "FailurePolicy can only be \"Ignore\" or \"Fail\"", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -346,7 +346,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "SideEffects can only be \"Unknown\", \"None\", \"Some\", or \"NoneOnDryRun\"", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -360,7 +360,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "both service and URL missing", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: admissionregistration.WebhookClientConfig{}, @@ -370,7 +370,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "both service and URL provided", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: admissionregistration.WebhookClientConfig{ @@ -387,7 +387,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "blank URL", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: admissionregistration.WebhookClientConfig{ @@ -399,7 +399,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "wrong scheme", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: admissionregistration.WebhookClientConfig{ @@ -411,7 +411,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "missing host", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: admissionregistration.WebhookClientConfig{ @@ -423,7 +423,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "fragment", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: admissionregistration.WebhookClientConfig{ @@ -435,7 +435,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "query", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: admissionregistration.WebhookClientConfig{ @@ -447,7 +447,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "user", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: admissionregistration.WebhookClientConfig{ @@ -459,7 +459,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "just totally wrong", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: admissionregistration.WebhookClientConfig{ @@ -471,7 +471,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "path must start with slash", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: admissionregistration.WebhookClientConfig{ @@ -488,7 +488,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "path accepts slash", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: admissionregistration.WebhookClientConfig{ @@ -505,7 +505,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "path accepts no trailing slash", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: admissionregistration.WebhookClientConfig{ @@ -522,7 +522,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "path fails //", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: admissionregistration.WebhookClientConfig{ @@ -539,7 +539,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "path no empty step", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: admissionregistration.WebhookClientConfig{ @@ -555,7 +555,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { expectedError: `clientConfig.service.path: Invalid value: "/foo//bar/": segment[1] may not be empty`, }, { name: "path no empty step 2", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: admissionregistration.WebhookClientConfig{ @@ -572,7 +572,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "path no non-subdomain", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: admissionregistration.WebhookClientConfig{ @@ -590,7 +590,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { { name: "invalid port 0", config: newValidatingWebhookConfiguration( - []admissionregistration.Webhook{ + []admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: admissionregistration.WebhookClientConfig{ @@ -608,7 +608,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { { name: "invalid port >65535", config: newValidatingWebhookConfiguration( - []admissionregistration.Webhook{ + []admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: admissionregistration.WebhookClientConfig{ @@ -625,7 +625,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "timeout seconds cannot be greater than 30", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -636,7 +636,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "timeout seconds cannot be smaller than 1", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -647,7 +647,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "timeout seconds must be positive", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -658,7 +658,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, { name: "valid timeout seconds", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -707,14 +707,14 @@ func TestValidateValidatingWebhookConfigurationUpdate(t *testing.T) { }{ { name: "should pass on valid new AdmissionReviewVersion", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, AdmissionReviewVersions: []string{"v1beta1"}, }, }, true), - oldconfig: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + oldconfig: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -724,14 +724,14 @@ func TestValidateValidatingWebhookConfigurationUpdate(t *testing.T) { }, { name: "should pass on invalid AdmissionReviewVersion with invalid previous versions", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, AdmissionReviewVersions: []string{"invalid-v1", "invalid-v2"}, }, }, true), - oldconfig: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + oldconfig: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -742,14 +742,14 @@ func TestValidateValidatingWebhookConfigurationUpdate(t *testing.T) { }, { name: "should fail on invalid AdmissionReviewVersion with valid previous versions", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, AdmissionReviewVersions: []string{"invalid-v1"}, }, }, true), - oldconfig: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + oldconfig: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, @@ -760,14 +760,14 @@ func TestValidateValidatingWebhookConfigurationUpdate(t *testing.T) { }, { name: "should fail on invalid AdmissionReviewVersion with missing previous versions", - config: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, AdmissionReviewVersions: []string{"invalid-v1"}, }, }, true), - oldconfig: newValidatingWebhookConfiguration([]admissionregistration.Webhook{ + oldconfig: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ { Name: "webhook.k8s.io", ClientConfig: validClientConfig, diff --git a/staging/src/k8s.io/api/admissionregistration/v1beta1/types.go b/staging/src/k8s.io/api/admissionregistration/v1beta1/types.go index d3a034d2e3f..4e2d694d545 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1beta1/types.go +++ b/staging/src/k8s.io/api/admissionregistration/v1beta1/types.go @@ -124,7 +124,7 @@ type ValidatingWebhookConfiguration struct { // +optional // +patchMergeKey=name // +patchStrategy=merge - Webhooks []Webhook `json:"webhooks,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=Webhooks"` + Webhooks []ValidatingWebhook `json:"webhooks,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=Webhooks"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -154,7 +154,7 @@ type MutatingWebhookConfiguration struct { // +optional // +patchMergeKey=name // +patchStrategy=merge - Webhooks []Webhook `json:"webhooks,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=Webhooks"` + Webhooks []MutatingWebhook `json:"webhooks,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=Webhooks"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -170,8 +170,126 @@ type MutatingWebhookConfigurationList struct { Items []MutatingWebhookConfiguration `json:"items" protobuf:"bytes,2,rep,name=items"` } -// Webhook describes an admission webhook and the resources and operations it applies to. -type Webhook struct { +// ValidatingWebhook describes an admission webhook and the resources and operations it applies to. +type ValidatingWebhook struct { + // The name of the admission webhook. + // Name should be fully qualified, e.g., imagepolicy.kubernetes.io, where + // "imagepolicy" is the name of the webhook, and kubernetes.io is the name + // of the organization. + // Required. + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + + // ClientConfig defines how to communicate with the hook. + // Required + ClientConfig WebhookClientConfig `json:"clientConfig" protobuf:"bytes,2,opt,name=clientConfig"` + + // Rules describes what operations on what resources/subresources the webhook cares about. + // The webhook cares about an operation if it matches _any_ Rule. + // However, in order to prevent ValidatingAdmissionWebhooks and MutatingAdmissionWebhooks + // from putting the cluster in a state which cannot be recovered from without completely + // disabling the plugin, ValidatingAdmissionWebhooks and MutatingAdmissionWebhooks are never called + // on admission requests for ValidatingWebhookConfiguration and MutatingWebhookConfiguration objects. + Rules []RuleWithOperations `json:"rules,omitempty" protobuf:"bytes,3,rep,name=rules"` + + // FailurePolicy defines how unrecognized errors from the admission endpoint are handled - + // allowed values are Ignore or Fail. Defaults to Ignore. + // +optional + FailurePolicy *FailurePolicyType `json:"failurePolicy,omitempty" protobuf:"bytes,4,opt,name=failurePolicy,casttype=FailurePolicyType"` + + // matchPolicy defines how the "rules" list is used to match incoming requests. + // Allowed values are "Exact" or "Equivalent". + // + // - Exact: match a request only if it exactly matches a specified rule. + // For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1, + // but "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`, + // a request to apps/v1beta1 or extensions/v1beta1 would not be sent to the webhook. + // + // - Equivalent: match a request if modifies a resource listed in rules, even via another API group or version. + // For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1, + // and "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`, + // a request to apps/v1beta1 or extensions/v1beta1 would be converted to apps/v1 and sent to the webhook. + // + // Defaults to "Exact" + // +optional + MatchPolicy *MatchPolicyType `json:"matchPolicy,omitempty" protobuf:"bytes,9,opt,name=matchPolicy,casttype=MatchPolicyType"` + + // NamespaceSelector decides whether to run the webhook on an object based + // on whether the namespace for that object matches the selector. If the + // object itself is a namespace, the matching is performed on + // object.metadata.labels. If the object is another cluster scoped resource, + // it never skips the webhook. + // + // For example, to run the webhook on any objects whose namespace is not + // associated with "runlevel" of "0" or "1"; you will set the selector as + // follows: + // "namespaceSelector": { + // "matchExpressions": [ + // { + // "key": "runlevel", + // "operator": "NotIn", + // "values": [ + // "0", + // "1" + // ] + // } + // ] + // } + // + // If instead you want to only run the webhook on any objects whose + // namespace is associated with the "environment" of "prod" or "staging"; + // you will set the selector as follows: + // "namespaceSelector": { + // "matchExpressions": [ + // { + // "key": "environment", + // "operator": "In", + // "values": [ + // "prod", + // "staging" + // ] + // } + // ] + // } + // + // See + // https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + // for more examples of label selectors. + // + // Default to the empty LabelSelector, which matches everything. + // +optional + NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty" protobuf:"bytes,5,opt,name=namespaceSelector"` + + // SideEffects states whether this webhookk has side effects. + // Acceptable values are: Unknown, None, Some, NoneOnDryRun + // Webhooks with side effects MUST implement a reconciliation system, since a request may be + // rejected by a future step in the admission change and the side effects therefore need to be undone. + // Requests with the dryRun attribute will be auto-rejected if they match a webhook with + // sideEffects == Unknown or Some. Defaults to Unknown. + // +optional + SideEffects *SideEffectClass `json:"sideEffects,omitempty" protobuf:"bytes,6,opt,name=sideEffects,casttype=SideEffectClass"` + + // TimeoutSeconds specifies the timeout for this webhook. After the timeout passes, + // the webhook call will be ignored or the API call will fail based on the + // failure policy. + // The timeout value must be between 1 and 30 seconds. + // Default to 30 seconds. + // +optional + TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty" protobuf:"varint,7,opt,name=timeoutSeconds"` + + // AdmissionReviewVersions is an ordered list of preferred `AdmissionReview` + // versions the Webhook expects. API server will try to use first version in + // the list which it supports. If none of the versions specified in this list + // supported by API server, validation will fail for this object. + // If a persisted webhook configuration specifies allowed versions and does not + // include any versions known to the API Server, calls to the webhook will fail + // and be subject to the failure policy. + // Default to `['v1beta1']`. + // +optional + AdmissionReviewVersions []string `json:"admissionReviewVersions,omitempty" protobuf:"bytes,8,rep,name=admissionReviewVersions"` +} + +// MutatingWebhook describes an admission webhook and the resources and operations it applies to. +type MutatingWebhook struct { // The name of the admission webhook. // Name should be fully qualified, e.g., imagepolicy.kubernetes.io, where // "imagepolicy" is the name of the webhook, and kubernetes.io is the name diff --git a/staging/src/k8s.io/apiserver/pkg/admission/BUILD b/staging/src/k8s.io/apiserver/pkg/admission/BUILD index 3d2483328e8..f89ee0ec022 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/admission/BUILD @@ -80,18 +80,7 @@ filegroup( "//staging/src/k8s.io/apiserver/pkg/admission/initializer:all-srcs", "//staging/src/k8s.io/apiserver/pkg/admission/metrics:all-srcs", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle:all-srcs", - "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config:all-srcs", - "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors:all-srcs", - "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic:all-srcs", - "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/initializer:all-srcs", - "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating:all-srcs", - "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:all-srcs", - "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request:all-srcs", - "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules:all-srcs", - "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts:all-srcs", - "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testing:all-srcs", - "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/util:all-srcs", - "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:all-srcs", "//staging/src/k8s.io/apiserver/pkg/admission/testing:all-srcs", ], tags = ["automanaged"], diff --git a/staging/src/k8s.io/apiserver/pkg/admission/configuration/BUILD b/staging/src/k8s.io/apiserver/pkg/admission/configuration/BUILD index 34432781415..3b8448659b2 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/configuration/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/admission/configuration/BUILD @@ -39,6 +39,7 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic:go_default_library", "//staging/src/k8s.io/client-go/informers:go_default_library", "//staging/src/k8s.io/client-go/listers/admissionregistration/v1beta1:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/admission/configuration/mutating_webhook_manager.go b/staging/src/k8s.io/apiserver/pkg/admission/configuration/mutating_webhook_manager.go index 4b2256e118b..bacf61722c3 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/configuration/mutating_webhook_manager.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/configuration/mutating_webhook_manager.go @@ -24,6 +24,7 @@ import ( "k8s.io/api/admissionregistration/v1beta1" "k8s.io/apimachinery/pkg/labels" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apiserver/pkg/admission/plugin/webhook" "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" "k8s.io/client-go/informers" admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1beta1" @@ -48,7 +49,7 @@ func NewMutatingWebhookConfigurationManager(f informers.SharedInformerFactory) g } // Start with an empty list - manager.configuration.Store(&v1beta1.MutatingWebhookConfiguration{}) + manager.configuration.Store([]webhook.WebhookAccessor{}) // On any change, rebuild the config informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ @@ -61,8 +62,8 @@ func NewMutatingWebhookConfigurationManager(f informers.SharedInformerFactory) g } // Webhooks returns the merged MutatingWebhookConfiguration. -func (m *mutatingWebhookConfigurationManager) Webhooks() []v1beta1.Webhook { - return m.configuration.Load().(*v1beta1.MutatingWebhookConfiguration).Webhooks +func (m *mutatingWebhookConfigurationManager) Webhooks() []webhook.WebhookAccessor { + return m.configuration.Load().([]webhook.WebhookAccessor) } func (m *mutatingWebhookConfigurationManager) HasSynced() bool { @@ -78,16 +79,18 @@ func (m *mutatingWebhookConfigurationManager) updateConfiguration() { m.configuration.Store(mergeMutatingWebhookConfigurations(configurations)) } -func mergeMutatingWebhookConfigurations(configurations []*v1beta1.MutatingWebhookConfiguration) *v1beta1.MutatingWebhookConfiguration { - var ret v1beta1.MutatingWebhookConfiguration +func mergeMutatingWebhookConfigurations(configurations []*v1beta1.MutatingWebhookConfiguration) []webhook.WebhookAccessor { // The internal order of webhooks for each configuration is provided by the user // but configurations themselves can be in any order. As we are going to run these // webhooks in serial, they are sorted here to have a deterministic order. sort.SliceStable(configurations, MutatingWebhookConfigurationSorter(configurations).ByName) + accessors := []webhook.WebhookAccessor{} for _, c := range configurations { - ret.Webhooks = append(ret.Webhooks, c.Webhooks...) + for i := range c.Webhooks { + accessors = append(accessors, webhook.NewMutatingWebhookAccessor(&c.Webhooks[i])) + } } - return &ret + return accessors } type MutatingWebhookConfigurationSorter []*v1beta1.MutatingWebhookConfiguration diff --git a/staging/src/k8s.io/apiserver/pkg/admission/configuration/mutating_webhook_manager_test.go b/staging/src/k8s.io/apiserver/pkg/admission/configuration/mutating_webhook_manager_test.go index 9bc037f5a40..828c1843025 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/configuration/mutating_webhook_manager_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/configuration/mutating_webhook_manager_test.go @@ -45,7 +45,7 @@ func TestGetMutatingWebhookConfig(t *testing.T) { webhookConfiguration := &v1beta1.MutatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{Name: "webhook1"}, - Webhooks: []v1beta1.Webhook{{Name: "webhook1.1"}}, + Webhooks: []v1beta1.MutatingWebhook{{Name: "webhook1.1"}}, } mutatingInformer := informerFactory.Admissionregistration().V1beta1().MutatingWebhookConfigurations() @@ -57,7 +57,14 @@ func TestGetMutatingWebhookConfig(t *testing.T) { if len(configurations) == 0 { t.Errorf("expected non empty webhooks") } - if !reflect.DeepEqual(configurations, webhookConfiguration.Webhooks) { - t.Errorf("Expected\n%#v\ngot\n%#v", webhookConfiguration.Webhooks, configurations) + for i := range configurations { + h, ok := configurations[i].GetMutatingWebhook() + if !ok { + t.Errorf("Expected mutating webhook") + continue + } + if !reflect.DeepEqual(h, &webhookConfiguration.Webhooks[i]) { + t.Errorf("Expected\n%#v\ngot\n%#v", &webhookConfiguration.Webhooks[i], h) + } } } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/configuration/validating_webhook_manager.go b/staging/src/k8s.io/apiserver/pkg/admission/configuration/validating_webhook_manager.go index 9258258f64b..bcce1e70f92 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/configuration/validating_webhook_manager.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/configuration/validating_webhook_manager.go @@ -24,6 +24,7 @@ import ( "k8s.io/api/admissionregistration/v1beta1" "k8s.io/apimachinery/pkg/labels" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apiserver/pkg/admission/plugin/webhook" "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" "k8s.io/client-go/informers" admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1beta1" @@ -48,7 +49,7 @@ func NewValidatingWebhookConfigurationManager(f informers.SharedInformerFactory) } // Start with an empty list - manager.configuration.Store(&v1beta1.ValidatingWebhookConfiguration{}) + manager.configuration.Store([]webhook.WebhookAccessor{}) // On any change, rebuild the config informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ @@ -61,8 +62,8 @@ func NewValidatingWebhookConfigurationManager(f informers.SharedInformerFactory) } // Webhooks returns the merged ValidatingWebhookConfiguration. -func (v *validatingWebhookConfigurationManager) Webhooks() []v1beta1.Webhook { - return v.configuration.Load().(*v1beta1.ValidatingWebhookConfiguration).Webhooks +func (v *validatingWebhookConfigurationManager) Webhooks() []webhook.WebhookAccessor { + return v.configuration.Load().([]webhook.WebhookAccessor) } // HasSynced returns true if the shared informers have synced. @@ -79,15 +80,15 @@ func (v *validatingWebhookConfigurationManager) updateConfiguration() { v.configuration.Store(mergeValidatingWebhookConfigurations(configurations)) } -func mergeValidatingWebhookConfigurations( - configurations []*v1beta1.ValidatingWebhookConfiguration, -) *v1beta1.ValidatingWebhookConfiguration { +func mergeValidatingWebhookConfigurations(configurations []*v1beta1.ValidatingWebhookConfiguration) []webhook.WebhookAccessor { sort.SliceStable(configurations, ValidatingWebhookConfigurationSorter(configurations).ByName) - var ret v1beta1.ValidatingWebhookConfiguration + accessors := []webhook.WebhookAccessor{} for _, c := range configurations { - ret.Webhooks = append(ret.Webhooks, c.Webhooks...) + for i := range c.Webhooks { + accessors = append(accessors, webhook.NewValidatingWebhookAccessor(&c.Webhooks[i])) + } } - return &ret + return accessors } type ValidatingWebhookConfigurationSorter []*v1beta1.ValidatingWebhookConfiguration diff --git a/staging/src/k8s.io/apiserver/pkg/admission/configuration/validating_webhook_manager_test.go b/staging/src/k8s.io/apiserver/pkg/admission/configuration/validating_webhook_manager_test.go index 153b4df486f..d99d99b46f2 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/configuration/validating_webhook_manager_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/configuration/validating_webhook_manager_test.go @@ -46,7 +46,7 @@ func TestGetValidatingWebhookConfig(t *testing.T) { webhookConfiguration := &v1beta1.ValidatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{Name: "webhook1"}, - Webhooks: []v1beta1.Webhook{{Name: "webhook1.1"}}, + Webhooks: []v1beta1.ValidatingWebhook{{Name: "webhook1.1"}}, } validatingInformer := informerFactory.Admissionregistration().V1beta1().ValidatingWebhookConfigurations() @@ -59,7 +59,14 @@ func TestGetValidatingWebhookConfig(t *testing.T) { if len(configurations) == 0 { t.Errorf("expected non empty webhooks") } - if !reflect.DeepEqual(configurations, webhookConfiguration.Webhooks) { - t.Errorf("Expected\n%#v\ngot\n%#v", webhookConfiguration.Webhooks, configurations) + for i := range configurations { + h, ok := configurations[i].GetValidatingWebhook() + if !ok { + t.Errorf("Expected validating webhook") + continue + } + if !reflect.DeepEqual(h, &webhookConfiguration.Webhooks[i]) { + t.Errorf("Expected\n%#v\ngot\n%#v", &webhookConfiguration.Webhooks[i], h) + } } } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/BUILD b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/BUILD new file mode 100644 index 00000000000..7787247d2c9 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/BUILD @@ -0,0 +1,41 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["accessors.go"], + importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook", + importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/api/admissionregistration/v1beta1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [ + ":package-srcs", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/initializer:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testing:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/util:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating:all-srcs", + ], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/accessors.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/accessors.go new file mode 100644 index 00000000000..362333c50f5 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/accessors.go @@ -0,0 +1,139 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhook + +import ( + "k8s.io/api/admissionregistration/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// WebhookAccessor provides a common interface to both mutating and validating webhook types. +type WebhookAccessor interface { + // GetName gets the webhook Name field. + GetName() string + // GetClientConfig gets the webhook ClientConfig field. + GetClientConfig() v1beta1.WebhookClientConfig + // GetRules gets the webhook Rules field. + GetRules() []v1beta1.RuleWithOperations + // GetFailurePolicy gets the webhook FailurePolicy field. + GetFailurePolicy() *v1beta1.FailurePolicyType + // GetMatchPolicy gets the webhook MatchPolicy field. + GetMatchPolicy() *v1beta1.MatchPolicyType + // GetNamespaceSelector gets the webhook NamespaceSelector field. + GetNamespaceSelector() *metav1.LabelSelector + // GetSideEffects gets the webhook SideEffects field. + GetSideEffects() *v1beta1.SideEffectClass + // GetTimeoutSeconds gets the webhook TimeoutSeconds field. + GetTimeoutSeconds() *int32 + // GetAdmissionReviewVersions gets the webhook AdmissionReviewVersions field. + GetAdmissionReviewVersions() []string + + // GetMutatingWebhook if the accessor contains a MutatingWebhook, returns it and true, else returns false. + GetMutatingWebhook() (*v1beta1.MutatingWebhook, bool) + // GetValidatingWebhook if the accessor contains a ValidatingWebhook, returns it and true, else returns false. + GetValidatingWebhook() (*v1beta1.ValidatingWebhook, bool) +} + +// NewMutatingWebhookAccessor creates an accessor for a MutatingWebhook. +func NewMutatingWebhookAccessor(h *v1beta1.MutatingWebhook) WebhookAccessor { + return mutatingWebhookAccessor{h} +} + +type mutatingWebhookAccessor struct { + *v1beta1.MutatingWebhook +} + +func (m mutatingWebhookAccessor) GetName() string { + return m.Name +} +func (m mutatingWebhookAccessor) GetClientConfig() v1beta1.WebhookClientConfig { + return m.ClientConfig +} +func (m mutatingWebhookAccessor) GetRules() []v1beta1.RuleWithOperations { + return m.Rules +} +func (m mutatingWebhookAccessor) GetFailurePolicy() *v1beta1.FailurePolicyType { + return m.FailurePolicy +} +func (m mutatingWebhookAccessor) GetMatchPolicy() *v1beta1.MatchPolicyType { + return m.MatchPolicy +} +func (m mutatingWebhookAccessor) GetNamespaceSelector() *metav1.LabelSelector { + return m.NamespaceSelector +} +func (m mutatingWebhookAccessor) GetSideEffects() *v1beta1.SideEffectClass { + return m.SideEffects +} +func (m mutatingWebhookAccessor) GetTimeoutSeconds() *int32 { + return m.TimeoutSeconds +} +func (m mutatingWebhookAccessor) GetAdmissionReviewVersions() []string { + return m.AdmissionReviewVersions +} + +func (m mutatingWebhookAccessor) GetMutatingWebhook() (*v1beta1.MutatingWebhook, bool) { + return m.MutatingWebhook, true +} + +func (m mutatingWebhookAccessor) GetValidatingWebhook() (*v1beta1.ValidatingWebhook, bool) { + return nil, false +} + +// NewValidatingWebhookAccessor creates an accessor for a ValidatingWebhook. +func NewValidatingWebhookAccessor(h *v1beta1.ValidatingWebhook) WebhookAccessor { + return validatingWebhookAccessor{h} +} + +type validatingWebhookAccessor struct { + *v1beta1.ValidatingWebhook +} + +func (v validatingWebhookAccessor) GetName() string { + return v.Name +} +func (v validatingWebhookAccessor) GetClientConfig() v1beta1.WebhookClientConfig { + return v.ClientConfig +} +func (v validatingWebhookAccessor) GetRules() []v1beta1.RuleWithOperations { + return v.Rules +} +func (v validatingWebhookAccessor) GetFailurePolicy() *v1beta1.FailurePolicyType { + return v.FailurePolicy +} +func (v validatingWebhookAccessor) GetMatchPolicy() *v1beta1.MatchPolicyType { + return v.MatchPolicy +} +func (v validatingWebhookAccessor) GetNamespaceSelector() *metav1.LabelSelector { + return v.NamespaceSelector +} +func (v validatingWebhookAccessor) GetSideEffects() *v1beta1.SideEffectClass { + return v.SideEffects +} +func (v validatingWebhookAccessor) GetTimeoutSeconds() *int32 { + return v.TimeoutSeconds +} +func (v validatingWebhookAccessor) GetAdmissionReviewVersions() []string { + return v.AdmissionReviewVersions +} + +func (v validatingWebhookAccessor) GetMutatingWebhook() (*v1beta1.MutatingWebhook, bool) { + return nil, false +} + +func (v validatingWebhookAccessor) GetValidatingWebhook() (*v1beta1.ValidatingWebhook, bool) { + return v.ValidatingWebhook, true +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/BUILD b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/BUILD index 7cdf2e46b48..7329da6c251 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/BUILD @@ -18,6 +18,7 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules:go_default_library", @@ -56,6 +57,7 @@ go_test( "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:go_default_library", "//staging/src/k8s.io/apiserver/pkg/apis/example:go_default_library", "//staging/src/k8s.io/apiserver/pkg/apis/example/v1:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/interfaces.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/interfaces.go index c57189543fb..227502de84e 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/interfaces.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/interfaces.go @@ -19,15 +19,15 @@ package generic import ( "context" - "k8s.io/api/admissionregistration/v1beta1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/plugin/webhook" ) // Source can list dynamic webhook plugins. type Source interface { - Webhooks() []v1beta1.Webhook + Webhooks() []webhook.WebhookAccessor HasSynced() bool } @@ -51,8 +51,7 @@ type VersionedAttributes struct { // WebhookInvocation describes how to call a webhook, including the resource and subresource the webhook registered for, // and the kind that should be sent to the webhook. type WebhookInvocation struct { - Webhook *v1beta1.Webhook - + Webhook webhook.WebhookAccessor Resource schema.GroupVersionResource Subresource string Kind schema.GroupVersionKind diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go index 30a9b5e7b5e..99415292cbf 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go @@ -27,10 +27,11 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer" + "k8s.io/apiserver/pkg/admission/plugin/webhook" "k8s.io/apiserver/pkg/admission/plugin/webhook/config" "k8s.io/apiserver/pkg/admission/plugin/webhook/namespace" "k8s.io/apiserver/pkg/admission/plugin/webhook/rules" - "k8s.io/apiserver/pkg/util/webhook" + webhookutil "k8s.io/apiserver/pkg/util/webhook" "k8s.io/client-go/informers" clientset "k8s.io/client-go/kubernetes" ) @@ -42,7 +43,7 @@ type Webhook struct { sourceFactory sourceFactory hookSource Source - clientManager *webhook.ClientManager + clientManager *webhookutil.ClientManager namespaceMatcher *namespace.Matcher dispatcher Dispatcher } @@ -53,7 +54,7 @@ var ( ) type sourceFactory func(f informers.SharedInformerFactory) Source -type dispatcherFactory func(cm *webhook.ClientManager) Dispatcher +type dispatcherFactory func(cm *webhookutil.ClientManager) Dispatcher // NewWebhook creates a new generic admission webhook. func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory sourceFactory, dispatcherFactory dispatcherFactory) (*Webhook, error) { @@ -62,17 +63,17 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory return nil, err } - cm, err := webhook.NewClientManager(admissionv1beta1.SchemeGroupVersion, admissionv1beta1.AddToScheme) + cm, err := webhookutil.NewClientManager(admissionv1beta1.SchemeGroupVersion, admissionv1beta1.AddToScheme) if err != nil { return nil, err } - authInfoResolver, err := webhook.NewDefaultAuthenticationInfoResolver(kubeconfigFile) + authInfoResolver, err := webhookutil.NewDefaultAuthenticationInfoResolver(kubeconfigFile) if err != nil { return nil, err } // Set defaults which may be overridden later. cm.SetAuthenticationInfoResolver(authInfoResolver) - cm.SetServiceResolver(webhook.NewDefaultServiceResolver()) + cm.SetServiceResolver(webhookutil.NewDefaultServiceResolver()) return &Webhook{ Handler: handler, @@ -86,13 +87,13 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory // SetAuthenticationInfoResolverWrapper sets the // AuthenticationInfoResolverWrapper. // TODO find a better way wire this, but keep this pull small for now. -func (a *Webhook) SetAuthenticationInfoResolverWrapper(wrapper webhook.AuthenticationInfoResolverWrapper) { +func (a *Webhook) SetAuthenticationInfoResolverWrapper(wrapper webhookutil.AuthenticationInfoResolverWrapper) { a.clientManager.SetAuthenticationInfoResolverWrapper(wrapper) } // SetServiceResolver sets a service resolver for the webhook admission plugin. // Passing a nil resolver does not have an effect, instead a default one will be used. -func (a *Webhook) SetServiceResolver(sr webhook.ServiceResolver) { +func (a *Webhook) SetServiceResolver(sr webhookutil.ServiceResolver) { a.clientManager.SetServiceResolver(sr) } @@ -128,10 +129,10 @@ func (a *Webhook) ValidateInitialization() error { // shouldCallHook returns invocation details if the webhook should be called, nil if the webhook should not be called, // or an error if an error was encountered during evaluation. -func (a *Webhook) shouldCallHook(h *v1beta1.Webhook, attr admission.Attributes, o admission.ObjectInterfaces) (*WebhookInvocation, *apierrors.StatusError) { +func (a *Webhook) shouldCallHook(h webhook.WebhookAccessor, attr admission.Attributes, o admission.ObjectInterfaces) (*WebhookInvocation, *apierrors.StatusError) { var err *apierrors.StatusError var invocation *WebhookInvocation - for _, r := range h.Rules { + for _, r := range h.GetRules() { m := rules.Matcher{Rule: r, Attr: attr} if m.Matches() { invocation = &WebhookInvocation{ @@ -143,12 +144,12 @@ func (a *Webhook) shouldCallHook(h *v1beta1.Webhook, attr admission.Attributes, break } } - if invocation == nil && h.MatchPolicy != nil && *h.MatchPolicy == v1beta1.Equivalent { + if invocation == nil && h.GetMatchPolicy() != nil && *h.GetMatchPolicy() == v1beta1.Equivalent { attrWithOverride := &attrWithResourceOverride{Attributes: attr} equivalents := o.GetEquivalentResourceMapper().EquivalentResourcesFor(attr.GetResource(), attr.GetSubresource()) // honor earlier rules first OuterLoop: - for _, r := range h.Rules { + for _, r := range h.GetRules() { // see if the rule matches any of the equivalent resources for _, equivalent := range equivalents { if equivalent == attr.GetResource() { @@ -207,7 +208,7 @@ func (a *Webhook) Dispatch(attr admission.Attributes, o admission.ObjectInterfac var relevantHooks []*WebhookInvocation for i := range hooks { - invocation, err := a.shouldCallHook(&hooks[i], attr, o) + invocation, err := a.shouldCallHook(hooks[i], attr, o) if err != nil { return err } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go index b214122ad38..c2ddbb7b1f8 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go @@ -25,6 +25,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/plugin/webhook" "k8s.io/apiserver/pkg/admission/plugin/webhook/namespace" ) @@ -61,7 +62,7 @@ func TestShouldCallHook(t *testing.T) { testcases := []struct { name string - webhook *v1beta1.Webhook + webhook *v1beta1.ValidatingWebhook attrs admission.Attributes expectCall bool @@ -72,13 +73,13 @@ func TestShouldCallHook(t *testing.T) { }{ { name: "no rules (just write)", - webhook: &v1beta1.Webhook{Rules: []v1beta1.RuleWithOperations{}}, + webhook: &v1beta1.ValidatingWebhook{Rules: []v1beta1.RuleWithOperations{}}, attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"apps", "v1", "Deployment"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "", admission.Create, &metav1.CreateOptions{}, false, nil), expectCall: false, }, { name: "invalid kind lookup", - webhook: &v1beta1.Webhook{ + webhook: &v1beta1.ValidatingWebhook{ NamespaceSelector: &metav1.LabelSelector{}, MatchPolicy: &equivalentMatch, Rules: []v1beta1.RuleWithOperations{{ @@ -91,7 +92,7 @@ func TestShouldCallHook(t *testing.T) { }, { name: "wildcard rule, match as requested", - webhook: &v1beta1.Webhook{ + webhook: &v1beta1.ValidatingWebhook{ NamespaceSelector: &metav1.LabelSelector{}, Rules: []v1beta1.RuleWithOperations{{ Operations: []v1beta1.OperationType{"*"}, @@ -105,7 +106,7 @@ func TestShouldCallHook(t *testing.T) { }, { name: "specific rules, prefer exact match", - webhook: &v1beta1.Webhook{ + webhook: &v1beta1.ValidatingWebhook{ NamespaceSelector: &metav1.LabelSelector{}, Rules: []v1beta1.RuleWithOperations{{ Operations: []v1beta1.OperationType{"*"}, @@ -125,7 +126,7 @@ func TestShouldCallHook(t *testing.T) { }, { name: "specific rules, match miss", - webhook: &v1beta1.Webhook{ + webhook: &v1beta1.ValidatingWebhook{ NamespaceSelector: &metav1.LabelSelector{}, Rules: []v1beta1.RuleWithOperations{{ Operations: []v1beta1.OperationType{"*"}, @@ -139,7 +140,7 @@ func TestShouldCallHook(t *testing.T) { }, { name: "specific rules, exact match miss", - webhook: &v1beta1.Webhook{ + webhook: &v1beta1.ValidatingWebhook{ MatchPolicy: &exactMatch, NamespaceSelector: &metav1.LabelSelector{}, Rules: []v1beta1.RuleWithOperations{{ @@ -154,7 +155,7 @@ func TestShouldCallHook(t *testing.T) { }, { name: "specific rules, equivalent match, prefer extensions", - webhook: &v1beta1.Webhook{ + webhook: &v1beta1.ValidatingWebhook{ MatchPolicy: &equivalentMatch, NamespaceSelector: &metav1.LabelSelector{}, Rules: []v1beta1.RuleWithOperations{{ @@ -172,7 +173,7 @@ func TestShouldCallHook(t *testing.T) { }, { name: "specific rules, equivalent match, prefer apps", - webhook: &v1beta1.Webhook{ + webhook: &v1beta1.ValidatingWebhook{ MatchPolicy: &equivalentMatch, NamespaceSelector: &metav1.LabelSelector{}, Rules: []v1beta1.RuleWithOperations{{ @@ -191,7 +192,7 @@ func TestShouldCallHook(t *testing.T) { { name: "specific rules, subresource prefer exact match", - webhook: &v1beta1.Webhook{ + webhook: &v1beta1.ValidatingWebhook{ NamespaceSelector: &metav1.LabelSelector{}, Rules: []v1beta1.RuleWithOperations{{ Operations: []v1beta1.OperationType{"*"}, @@ -211,7 +212,7 @@ func TestShouldCallHook(t *testing.T) { }, { name: "specific rules, subresource match miss", - webhook: &v1beta1.Webhook{ + webhook: &v1beta1.ValidatingWebhook{ NamespaceSelector: &metav1.LabelSelector{}, Rules: []v1beta1.RuleWithOperations{{ Operations: []v1beta1.OperationType{"*"}, @@ -225,7 +226,7 @@ func TestShouldCallHook(t *testing.T) { }, { name: "specific rules, subresource exact match miss", - webhook: &v1beta1.Webhook{ + webhook: &v1beta1.ValidatingWebhook{ MatchPolicy: &exactMatch, NamespaceSelector: &metav1.LabelSelector{}, Rules: []v1beta1.RuleWithOperations{{ @@ -240,7 +241,7 @@ func TestShouldCallHook(t *testing.T) { }, { name: "specific rules, subresource equivalent match, prefer extensions", - webhook: &v1beta1.Webhook{ + webhook: &v1beta1.ValidatingWebhook{ MatchPolicy: &equivalentMatch, NamespaceSelector: &metav1.LabelSelector{}, Rules: []v1beta1.RuleWithOperations{{ @@ -258,7 +259,7 @@ func TestShouldCallHook(t *testing.T) { }, { name: "specific rules, subresource equivalent match, prefer apps", - webhook: &v1beta1.Webhook{ + webhook: &v1beta1.ValidatingWebhook{ MatchPolicy: &equivalentMatch, NamespaceSelector: &metav1.LabelSelector{}, Rules: []v1beta1.RuleWithOperations{{ @@ -278,7 +279,7 @@ func TestShouldCallHook(t *testing.T) { for _, testcase := range testcases { t.Run(testcase.name, func(t *testing.T) { - invocation, err := a.shouldCallHook(testcase.webhook, testcase.attrs, interfaces) + invocation, err := a.shouldCallHook(webhook.NewValidatingWebhookAccessor(testcase.webhook), testcase.attrs, interfaces) if err != nil { if len(testcase.expectErr) == 0 { t.Fatal(err) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/dispatcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/dispatcher.go index 33190ff1214..b206d2a5393 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/dispatcher.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/dispatcher.go @@ -39,16 +39,16 @@ import ( "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" "k8s.io/apiserver/pkg/admission/plugin/webhook/request" "k8s.io/apiserver/pkg/admission/plugin/webhook/util" - "k8s.io/apiserver/pkg/util/webhook" + webhookutil "k8s.io/apiserver/pkg/util/webhook" ) type mutatingDispatcher struct { - cm *webhook.ClientManager + cm *webhookutil.ClientManager plugin *Plugin } -func newMutatingDispatcher(p *Plugin) func(cm *webhook.ClientManager) generic.Dispatcher { - return func(cm *webhook.ClientManager) generic.Dispatcher { +func newMutatingDispatcher(p *Plugin) func(cm *webhookutil.ClientManager) generic.Dispatcher { + return func(cm *webhookutil.ClientManager) generic.Dispatcher { return &mutatingDispatcher{cm, p} } } @@ -58,7 +58,10 @@ var _ generic.Dispatcher = &mutatingDispatcher{} func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, relevantHooks []*generic.WebhookInvocation) error { var versionedAttr *generic.VersionedAttributes for _, invocation := range relevantHooks { - hook := invocation.Webhook + hook, ok := invocation.Webhook.GetMutatingWebhook() + if !ok { + return fmt.Errorf("mutating webhook dispatch requires v1beta1.MutatingWebhook, but got %T", hook) + } if versionedAttr == nil { // First webhook, create versioned attributes var err error @@ -73,14 +76,14 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib } t := time.Now() - err := a.callAttrMutatingHook(ctx, invocation, versionedAttr, o) + err := a.callAttrMutatingHook(ctx, hook, invocation, versionedAttr, o) admissionmetrics.Metrics.ObserveWebhook(time.Since(t), err != nil, versionedAttr.Attributes, "admit", hook.Name) if err == nil { continue } ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1beta1.Ignore - if callErr, ok := err.(*webhook.ErrCallingWebhook); ok { + if callErr, ok := err.(*webhookutil.ErrCallingWebhook); ok { if ignoreClientCallFailures { klog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr) utilruntime.HandleError(callErr) @@ -100,11 +103,11 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib } // note that callAttrMutatingHook updates attr -func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes, o admission.ObjectInterfaces) error { - h := invocation.Webhook + +func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *v1beta1.MutatingWebhook, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes, o admission.ObjectInterfaces) error { if attr.Attributes.IsDryRun() { if h.SideEffects == nil { - return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil")} + return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil")} } if !(*h.SideEffects == v1beta1.SideEffectClassNone || *h.SideEffects == v1beta1.SideEffectClassNoneOnDryRun) { return webhookerrors.NewDryRunUnsupportedErr(h.Name) @@ -113,15 +116,15 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, invocatio // Currently dispatcher only supports `v1beta1` AdmissionReview // TODO: Make the dispatcher capable of sending multiple AdmissionReview versions - if !util.HasAdmissionReviewVersion(v1beta1.SchemeGroupVersion.Version, h) { - return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("webhook does not accept v1beta1 AdmissionReview")} + if !util.HasAdmissionReviewVersion(v1beta1.SchemeGroupVersion.Version, invocation.Webhook) { + return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("webhook does not accept v1beta1 AdmissionReview")} } // Make the webhook request request := request.CreateAdmissionReview(attr, invocation) - client, err := a.cm.HookClient(util.HookClientConfigForWebhook(h)) + client, err := a.cm.HookClient(util.HookClientConfigForWebhook(invocation.Webhook)) if err != nil { - return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: err} + return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err} } response := &admissionv1beta1.AdmissionReview{} r := client.Post().Context(ctx).Body(&request) @@ -129,11 +132,11 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, invocatio r = r.Timeout(time.Duration(*h.TimeoutSeconds) * time.Second) } if err := r.Do().Into(response); err != nil { - return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: err} + return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err} } if response.Response == nil { - return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")} + return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")} } for k, v := range response.Response.AuditAnnotations { diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/plugin_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/plugin_test.go index 4a1a1c328b4..b8f98eedffb 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/plugin_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/plugin_test.go @@ -46,7 +46,7 @@ func TestAdmit(t *testing.T) { defer close(stopCh) testCases := append(webhooktesting.NewMutatingTestCases(serverURL), - webhooktesting.NewNonMutatingTestCases(serverURL)...) + webhooktesting.ConvertToMutatingTestCases(webhooktesting.NewNonMutatingTestCases(serverURL))...) for _, tt := range testCases { wh, err := NewMutatingWebhook(nil) @@ -56,7 +56,7 @@ func TestAdmit(t *testing.T) { } ns := "webhook-test" - client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, true, stopCh) + client, informer := webhooktesting.NewFakeMutatingDataSource(ns, tt.Webhooks, stopCh) wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32)))) wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL)) @@ -136,7 +136,7 @@ func TestAdmitCachedClient(t *testing.T) { for _, tt := range webhooktesting.NewCachedClientTestcases(serverURL) { ns := "webhook-test" - client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, true, stopCh) + client, informer := webhooktesting.NewFakeMutatingDataSource(ns, webhooktesting.ConvertToMutatingWebhooks(tt.Webhooks), stopCh) // override the webhook source. The client cache will stay the same. cacheMisses := new(int32) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/BUILD b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/BUILD index 6950843f754..24198a9ee3f 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/BUILD @@ -10,13 +10,13 @@ go_library( importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/namespace", visibility = ["//visibility:public"], deps = [ - "//staging/src/k8s.io/api/admissionregistration/v1beta1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library", "//staging/src/k8s.io/client-go/kubernetes:go_default_library", "//staging/src/k8s.io/client-go/listers/core/v1:go_default_library", ], @@ -34,6 +34,7 @@ go_test( "//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library", ], ) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/matcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/matcher.go index 78dbd528286..4530de3e608 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/matcher.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/matcher.go @@ -19,13 +19,13 @@ package namespace import ( "fmt" - "k8s.io/api/admissionregistration/v1beta1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/plugin/webhook" clientset "k8s.io/client-go/kubernetes" corelisters "k8s.io/client-go/listers/core/v1" ) @@ -86,7 +86,7 @@ func (m *Matcher) GetNamespaceLabels(attr admission.Attributes) (map[string]stri // MatchNamespaceSelector decideds whether the request matches the // namespaceSelctor of the webhook. Only when they match, the webhook is called. -func (m *Matcher) MatchNamespaceSelector(h *v1beta1.Webhook, attr admission.Attributes) (bool, *apierrors.StatusError) { +func (m *Matcher) MatchNamespaceSelector(h webhook.WebhookAccessor, attr admission.Attributes) (bool, *apierrors.StatusError) { namespaceName := attr.GetNamespace() if len(namespaceName) == 0 && attr.GetResource().Resource != "namespaces" { // If the request is about a cluster scoped resource, and it is not a @@ -96,7 +96,7 @@ func (m *Matcher) MatchNamespaceSelector(h *v1beta1.Webhook, attr admission.Attr return true, nil } // TODO: adding an LRU cache to cache the translation - selector, err := metav1.LabelSelectorAsSelector(h.NamespaceSelector) + selector, err := metav1.LabelSelectorAsSelector(h.GetNamespaceSelector()) if err != nil { return false, apierrors.NewInternalError(err) } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/matcher_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/matcher_test.go index 616ff7bbe31..4633cac8ecc 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/matcher_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/matcher_test.go @@ -27,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/plugin/webhook" ) type fakeNamespaceLister struct { @@ -114,12 +115,12 @@ func TestGetNamespaceLabels(t *testing.T) { } func TestNotExemptClusterScopedResource(t *testing.T) { - hook := ®istrationv1beta1.Webhook{ + hook := ®istrationv1beta1.ValidatingWebhook{ NamespaceSelector: &metav1.LabelSelector{}, } attr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "", "mock-name", schema.GroupVersionResource{Version: "v1", Resource: "nodes"}, "", admission.Create, &metav1.CreateOptions{}, false, nil) matcher := Matcher{} - matches, err := matcher.MatchNamespaceSelector(hook, attr) + matches, err := matcher.MatchNamespaceSelector(webhook.NewValidatingWebhookAccessor(hook), attr) if err != nil { t.Fatal(err) } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testing/testcase.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testing/testcase.go index ee5467478ca..ec8e6b552c7 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testing/testcase.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testing/testcase.go @@ -49,8 +49,8 @@ var sideEffectsNone = registrationv1beta1.SideEffectClassNone var sideEffectsSome = registrationv1beta1.SideEffectClassSome var sideEffectsNoneOnDryRun = registrationv1beta1.SideEffectClassNoneOnDryRun -// NewFakeDataSource returns a mock client and informer returning the given webhooks. -func NewFakeDataSource(name string, webhooks []registrationv1beta1.Webhook, mutating bool, stopCh <-chan struct{}) (clientset kubernetes.Interface, factory informers.SharedInformerFactory) { +// NewFakeValidatingDataSource returns a mock client and informer returning the given webhooks. +func NewFakeValidatingDataSource(name string, webhooks []registrationv1beta1.ValidatingWebhook, stopCh <-chan struct{}) (clientset kubernetes.Interface, factory informers.SharedInformerFactory) { var objs = []runtime.Object{ &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ @@ -61,21 +61,37 @@ func NewFakeDataSource(name string, webhooks []registrationv1beta1.Webhook, muta }, }, } - if mutating { - objs = append(objs, ®istrationv1beta1.MutatingWebhookConfiguration{ + objs = append(objs, ®istrationv1beta1.ValidatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-webhooks", + }, + Webhooks: webhooks, + }) + + client := fakeclientset.NewSimpleClientset(objs...) + informerFactory := informers.NewSharedInformerFactory(client, 0) + + return client, informerFactory +} + +// NewFakeMutatingDataSource returns a mock client and informer returning the given webhooks. +func NewFakeMutatingDataSource(name string, webhooks []registrationv1beta1.MutatingWebhook, stopCh <-chan struct{}) (clientset kubernetes.Interface, factory informers.SharedInformerFactory) { + var objs = []runtime.Object{ + &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-webhooks", + Name: name, + Labels: map[string]string{ + "runlevel": "0", + }, }, - Webhooks: webhooks, - }) - } else { - objs = append(objs, ®istrationv1beta1.ValidatingWebhookConfiguration{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-webhooks", - }, - Webhooks: webhooks, - }) + }, } + objs = append(objs, ®istrationv1beta1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-webhooks", + }, + Webhooks: webhooks, + }) client := fakeclientset.NewSimpleClientset(objs...) informerFactory := informers.NewSharedInformerFactory(client, 0) @@ -181,10 +197,10 @@ func (c urlConfigGenerator) ccfgURL(urlPath string) registrationv1beta1.WebhookC } } -// Test is a webhook test case. -type Test struct { +// ValidatingTest is a validating webhook test case. +type ValidatingTest struct { Name string - Webhooks []registrationv1beta1.Webhook + Webhooks []registrationv1beta1.ValidatingWebhook Path string IsCRD bool IsDryRun bool @@ -196,19 +212,52 @@ type Test struct { ExpectStatusCode int32 } +// MutatingTest is a mutating webhook test case. +type MutatingTest struct { + Name string + Webhooks []registrationv1beta1.MutatingWebhook + Path string + IsCRD bool + IsDryRun bool + AdditionalLabels map[string]string + ExpectLabels map[string]string + ExpectAllow bool + ErrorContains string + ExpectAnnotations map[string]string + ExpectStatusCode int32 +} + +// ConvertToMutatingTestCases converts a validating test case to a mutating one for test purposes. +func ConvertToMutatingTestCases(tests []ValidatingTest) []MutatingTest { + r := make([]MutatingTest, len(tests)) + for i, t := range tests { + r[i] = MutatingTest{t.Name, ConvertToMutatingWebhooks(t.Webhooks), t.Path, t.IsCRD, t.IsDryRun, t.AdditionalLabels, t.ExpectLabels, t.ExpectAllow, t.ErrorContains, t.ExpectAnnotations, t.ExpectStatusCode} + } + return r +} + +// ConvertToMutatingWebhooks converts a validating webhook to a mutating one for test purposes. +func ConvertToMutatingWebhooks(webhooks []registrationv1beta1.ValidatingWebhook) []registrationv1beta1.MutatingWebhook { + mutating := make([]registrationv1beta1.MutatingWebhook, len(webhooks)) + for i, h := range webhooks { + mutating[i] = registrationv1beta1.MutatingWebhook{h.Name, h.ClientConfig, h.Rules, h.FailurePolicy, h.MatchPolicy, h.NamespaceSelector, h.SideEffects, h.TimeoutSeconds, h.AdmissionReviewVersions} + } + return mutating +} + // NewNonMutatingTestCases returns test cases with a given base url. // All test cases in NewNonMutatingTestCases have no Patch set in // AdmissionResponse. The test cases are used by both MutatingAdmissionWebhook // and ValidatingAdmissionWebhook. -func NewNonMutatingTestCases(url *url.URL) []Test { +func NewNonMutatingTestCases(url *url.URL) []ValidatingTest { policyFail := registrationv1beta1.Fail policyIgnore := registrationv1beta1.Ignore ccfgURL := urlConfigGenerator{url}.ccfgURL - return []Test{ + return []ValidatingTest{ { Name: "no match", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "nomatch", ClientConfig: ccfgSVC("disallow"), Rules: []registrationv1beta1.RuleWithOperations{{ @@ -221,7 +270,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test { }, { Name: "match & allow", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "allow.example.com", ClientConfig: ccfgSVC("allow"), Rules: matchEverythingRules, @@ -233,7 +282,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test { }, { Name: "match & disallow", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "disallow", ClientConfig: ccfgSVC("disallow"), Rules: matchEverythingRules, @@ -245,7 +294,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test { }, { Name: "match & disallow ii", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "disallowReason", ClientConfig: ccfgSVC("disallowReason"), Rules: matchEverythingRules, @@ -257,7 +306,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test { }, { Name: "match & disallow & but allowed because namespaceSelector exempt the ns", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "disallow", ClientConfig: ccfgSVC("disallow"), Rules: newMatchEverythingRules(), @@ -275,7 +324,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test { }, { Name: "match & disallow & but allowed because namespaceSelector exempt the ns ii", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "disallow", ClientConfig: ccfgSVC("disallow"), Rules: newMatchEverythingRules(), @@ -292,7 +341,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test { }, { Name: "match & fail (but allow because fail open)", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "internalErr A", ClientConfig: ccfgSVC("internalErr"), Rules: matchEverythingRules, @@ -319,7 +368,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test { }, { Name: "match & fail (but disallow because fail close on nil FailurePolicy)", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "internalErr A", ClientConfig: ccfgSVC("internalErr"), NamespaceSelector: &metav1.LabelSelector{}, @@ -343,7 +392,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test { }, { Name: "match & fail (but fail because fail closed)", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "internalErr A", ClientConfig: ccfgSVC("internalErr"), Rules: matchEverythingRules, @@ -370,7 +419,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test { }, { Name: "match & allow (url)", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "allow.example.com", ClientConfig: ccfgURL("allow"), Rules: matchEverythingRules, @@ -382,7 +431,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test { }, { Name: "match & disallow (url)", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "disallow", ClientConfig: ccfgURL("disallow"), Rules: matchEverythingRules, @@ -393,7 +442,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test { ErrorContains: "without explanation", }, { Name: "absent response and fail open", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "nilResponse", ClientConfig: ccfgURL("nilResponse"), FailurePolicy: &policyIgnore, @@ -405,7 +454,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test { }, { Name: "absent response and fail closed", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "nilResponse", ClientConfig: ccfgURL("nilResponse"), FailurePolicy: &policyFail, @@ -418,7 +467,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test { }, { Name: "no match dry run", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "nomatch", ClientConfig: ccfgSVC("allow"), Rules: []registrationv1beta1.RuleWithOperations{{ @@ -433,7 +482,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test { }, { Name: "match dry run side effects Unknown", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "allow", ClientConfig: ccfgSVC("allow"), Rules: matchEverythingRules, @@ -447,7 +496,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test { }, { Name: "match dry run side effects None", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "allow", ClientConfig: ccfgSVC("allow"), Rules: matchEverythingRules, @@ -461,7 +510,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test { }, { Name: "match dry run side effects Some", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "allow", ClientConfig: ccfgSVC("allow"), Rules: matchEverythingRules, @@ -475,7 +524,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test { }, { Name: "match dry run side effects NoneOnDryRun", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "allow", ClientConfig: ccfgSVC("allow"), Rules: matchEverythingRules, @@ -489,7 +538,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test { }, { Name: "illegal annotation format", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "invalidAnnotation", ClientConfig: ccfgURL("invalidAnnotation"), Rules: matchEverythingRules, @@ -506,11 +555,11 @@ func NewNonMutatingTestCases(url *url.URL) []Test { // NewMutatingTestCases returns test cases with a given base url. // All test cases in NewMutatingTestCases have Patch set in // AdmissionResponse. The test cases are only used by both MutatingAdmissionWebhook. -func NewMutatingTestCases(url *url.URL) []Test { - return []Test{ +func NewMutatingTestCases(url *url.URL) []MutatingTest { + return []MutatingTest{ { Name: "match & remove label", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.MutatingWebhook{{ Name: "removelabel.example.com", ClientConfig: ccfgSVC("removeLabel"), Rules: matchEverythingRules, @@ -524,7 +573,7 @@ func NewMutatingTestCases(url *url.URL) []Test { }, { Name: "match & add label", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.MutatingWebhook{{ Name: "addLabel", ClientConfig: ccfgSVC("addLabel"), Rules: matchEverythingRules, @@ -536,7 +585,7 @@ func NewMutatingTestCases(url *url.URL) []Test { }, { Name: "match CRD & add label", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.MutatingWebhook{{ Name: "addLabel", ClientConfig: ccfgSVC("addLabel"), Rules: matchEverythingRules, @@ -549,7 +598,7 @@ func NewMutatingTestCases(url *url.URL) []Test { }, { Name: "match CRD & remove label", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.MutatingWebhook{{ Name: "removelabel.example.com", ClientConfig: ccfgSVC("removeLabel"), Rules: matchEverythingRules, @@ -564,7 +613,7 @@ func NewMutatingTestCases(url *url.URL) []Test { }, { Name: "match & invalid mutation", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.MutatingWebhook{{ Name: "invalidMutation", ClientConfig: ccfgSVC("invalidMutation"), Rules: matchEverythingRules, @@ -576,7 +625,7 @@ func NewMutatingTestCases(url *url.URL) []Test { }, { Name: "match & remove label dry run unsupported", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.MutatingWebhook{{ Name: "removeLabel", ClientConfig: ccfgSVC("removeLabel"), Rules: matchEverythingRules, @@ -596,7 +645,7 @@ func NewMutatingTestCases(url *url.URL) []Test { // CachedTest is a test case for the client manager. type CachedTest struct { Name string - Webhooks []registrationv1beta1.Webhook + Webhooks []registrationv1beta1.ValidatingWebhook ExpectAllow bool ExpectCacheMiss bool } @@ -609,7 +658,7 @@ func NewCachedClientTestcases(url *url.URL) []CachedTest { return []CachedTest{ { Name: "uncached: service webhook, path 'allow'", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "cache1", ClientConfig: ccfgSVC("allow"), Rules: newMatchEverythingRules(), @@ -622,7 +671,7 @@ func NewCachedClientTestcases(url *url.URL) []CachedTest { }, { Name: "uncached: service webhook, path 'internalErr'", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "cache2", ClientConfig: ccfgSVC("internalErr"), Rules: newMatchEverythingRules(), @@ -635,7 +684,7 @@ func NewCachedClientTestcases(url *url.URL) []CachedTest { }, { Name: "cached: service webhook, path 'allow'", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "cache3", ClientConfig: ccfgSVC("allow"), Rules: newMatchEverythingRules(), @@ -648,7 +697,7 @@ func NewCachedClientTestcases(url *url.URL) []CachedTest { }, { Name: "uncached: url webhook, path 'allow'", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "cache4", ClientConfig: ccfgURL("allow"), Rules: newMatchEverythingRules(), @@ -661,7 +710,7 @@ func NewCachedClientTestcases(url *url.URL) []CachedTest { }, { Name: "cached: service webhook, path 'allow'", - Webhooks: []registrationv1beta1.Webhook{{ + Webhooks: []registrationv1beta1.ValidatingWebhook{{ Name: "cache5", ClientConfig: ccfgURL("allow"), Rules: newMatchEverythingRules(), diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/util/BUILD b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/util/BUILD index dd9ed4916d0..3a9b036f76e 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/util/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/util/BUILD @@ -7,7 +7,7 @@ go_library( importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/util", visibility = ["//visibility:public"], deps = [ - "//staging/src/k8s.io/api/admissionregistration/v1beta1:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library", "//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library", ], ) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/util/client_config.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/util/client_config.go index 8f489639aaa..b9907286fc5 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/util/client_config.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/util/client_config.go @@ -17,38 +17,39 @@ limitations under the License. package util import ( - "k8s.io/api/admissionregistration/v1beta1" - "k8s.io/apiserver/pkg/util/webhook" + "k8s.io/apiserver/pkg/admission/plugin/webhook" + webhookutil "k8s.io/apiserver/pkg/util/webhook" ) -// HookClientConfigForWebhook construct a webhook.ClientConfig using a v1beta1.Webhook API object. -// webhook.ClientConfig is used to create a HookClient and the purpose of the config struct is to -// share that with other packages that need to create a HookClient. -func HookClientConfigForWebhook(w *v1beta1.Webhook) webhook.ClientConfig { - ret := webhook.ClientConfig{Name: w.Name, CABundle: w.ClientConfig.CABundle} - if w.ClientConfig.URL != nil { - ret.URL = *w.ClientConfig.URL +// HookClientConfigForWebhook construct a webhookutil.ClientConfig using a WebhookAccessor to access +// v1beta1.MutatingWebhook and v1beta1.ValidatingWebhook API objects. webhookutil.ClientConfig is used +// to create a HookClient and the purpose of the config struct is to share that with other packages +// that need to create a HookClient. +func HookClientConfigForWebhook(w webhook.WebhookAccessor) webhookutil.ClientConfig { + ret := webhookutil.ClientConfig{Name: w.GetName(), CABundle: w.GetClientConfig().CABundle} + if w.GetClientConfig().URL != nil { + ret.URL = *w.GetClientConfig().URL } - if w.ClientConfig.Service != nil { - ret.Service = &webhook.ClientConfigService{ - Name: w.ClientConfig.Service.Name, - Namespace: w.ClientConfig.Service.Namespace, + if w.GetClientConfig().Service != nil { + ret.Service = &webhookutil.ClientConfigService{ + Name: w.GetClientConfig().Service.Name, + Namespace: w.GetClientConfig().Service.Namespace, } - if w.ClientConfig.Service.Port != nil { - ret.Service.Port = *w.ClientConfig.Service.Port + if w.GetClientConfig().Service.Port != nil { + ret.Service.Port = *w.GetClientConfig().Service.Port } else { ret.Service.Port = 443 } - if w.ClientConfig.Service.Path != nil { - ret.Service.Path = *w.ClientConfig.Service.Path + if w.GetClientConfig().Service.Path != nil { + ret.Service.Path = *w.GetClientConfig().Service.Path } } return ret } // HasAdmissionReviewVersion check whether a version is accepted by a given webhook. -func HasAdmissionReviewVersion(a string, w *v1beta1.Webhook) bool { - for _, b := range w.AdmissionReviewVersions { +func HasAdmissionReviewVersion(a string, w webhook.WebhookAccessor) bool { + for _, b := range w.GetAdmissionReviewVersions() { if b == a { return true } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/dispatcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/dispatcher.go index d7421e2b53b..0505103d26d 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/dispatcher.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/dispatcher.go @@ -22,8 +22,6 @@ import ( "sync" "time" - "k8s.io/klog" - admissionv1beta1 "k8s.io/api/admission/v1beta1" "k8s.io/api/admissionregistration/v1beta1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -35,14 +33,15 @@ import ( "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" "k8s.io/apiserver/pkg/admission/plugin/webhook/request" "k8s.io/apiserver/pkg/admission/plugin/webhook/util" - "k8s.io/apiserver/pkg/util/webhook" + webhookutil "k8s.io/apiserver/pkg/util/webhook" + "k8s.io/klog" ) type validatingDispatcher struct { - cm *webhook.ClientManager + cm *webhookutil.ClientManager } -func newValidatingDispatcher(cm *webhook.ClientManager) generic.Dispatcher { +func newValidatingDispatcher(cm *webhookutil.ClientManager) generic.Dispatcher { return &validatingDispatcher{cm} } @@ -69,18 +68,21 @@ func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attr for i := range relevantHooks { go func(invocation *generic.WebhookInvocation) { defer wg.Done() - hook := invocation.Webhook + hook, ok := invocation.Webhook.GetValidatingWebhook() + if !ok { + utilruntime.HandleError(fmt.Errorf("validating webhook dispatch requires v1beta1.ValidatingWebhook, but got %T", hook)) + return + } versionedAttr := versionedAttrs[invocation.Kind] - t := time.Now() - err := d.callHook(ctx, invocation, versionedAttr) + err := d.callHook(ctx, hook, invocation, versionedAttr) admissionmetrics.Metrics.ObserveWebhook(time.Since(t), err != nil, versionedAttr.Attributes, "validating", hook.Name) if err == nil { return } ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1beta1.Ignore - if callErr, ok := err.(*webhook.ErrCallingWebhook); ok { + if callErr, ok := err.(*webhookutil.ErrCallingWebhook); ok { if ignoreClientCallFailures { klog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr) utilruntime.HandleError(callErr) @@ -115,11 +117,10 @@ func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attr return errs[0] } -func (d *validatingDispatcher) callHook(ctx context.Context, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes) error { - h := invocation.Webhook +func (d *validatingDispatcher) callHook(ctx context.Context, h *v1beta1.ValidatingWebhook, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes) error { if attr.Attributes.IsDryRun() { if h.SideEffects == nil { - return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil")} + return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil")} } if !(*h.SideEffects == v1beta1.SideEffectClassNone || *h.SideEffects == v1beta1.SideEffectClassNoneOnDryRun) { return webhookerrors.NewDryRunUnsupportedErr(h.Name) @@ -128,15 +129,15 @@ func (d *validatingDispatcher) callHook(ctx context.Context, invocation *generic // Currently dispatcher only supports `v1beta1` AdmissionReview // TODO: Make the dispatcher capable of sending multiple AdmissionReview versions - if !util.HasAdmissionReviewVersion(v1beta1.SchemeGroupVersion.Version, h) { - return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("webhook does not accept v1beta1 AdmissionReviewRequest")} + if !util.HasAdmissionReviewVersion(v1beta1.SchemeGroupVersion.Version, invocation.Webhook) { + return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("webhook does not accept v1beta1 AdmissionReviewRequest")} } // Make the webhook request request := request.CreateAdmissionReview(attr, invocation) - client, err := d.cm.HookClient(util.HookClientConfigForWebhook(h)) + client, err := d.cm.HookClient(util.HookClientConfigForWebhook(invocation.Webhook)) if err != nil { - return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: err} + return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err} } response := &admissionv1beta1.AdmissionReview{} r := client.Post().Context(ctx).Body(&request) @@ -144,11 +145,11 @@ func (d *validatingDispatcher) callHook(ctx context.Context, invocation *generic r = r.Timeout(time.Duration(*h.TimeoutSeconds) * time.Second) } if err := r.Do().Into(response); err != nil { - return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: err} + return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err} } if response.Response == nil { - return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")} + return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")} } for k, v := range response.Response.AuditAnnotations { key := h.Name + "/" + k diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/plugin_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/plugin_test.go index 0ed2a9744c3..a2aec191e39 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/plugin_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/plugin_test.go @@ -51,7 +51,7 @@ func TestValidate(t *testing.T) { } ns := "webhook-test" - client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, false, stopCh) + client, informer := webhooktesting.NewFakeValidatingDataSource(ns, tt.Webhooks, stopCh) wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32)))) wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL)) @@ -116,7 +116,7 @@ func TestValidateCachedClient(t *testing.T) { for _, tt := range webhooktesting.NewCachedClientTestcases(serverURL) { ns := "webhook-test" - client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, false, stopCh) + client, informer := webhooktesting.NewFakeValidatingDataSource(ns, tt.Webhooks, stopCh) // override the webhook source. The client cache will stay the same. cacheMisses := new(int32) diff --git a/test/e2e/apimachinery/webhook.go b/test/e2e/apimachinery/webhook.go index 2c4ba66379b..00e7731f891 100644 --- a/test/e2e/apimachinery/webhook.go +++ b/test/e2e/apimachinery/webhook.go @@ -436,7 +436,7 @@ func registerWebhook(f *framework.Framework, context *certContext) func() { ObjectMeta: metav1.ObjectMeta{ Name: configName, }, - Webhooks: []v1beta1.Webhook{ + Webhooks: []v1beta1.ValidatingWebhook{ { Name: "deny-unwanted-pod-container-name-and-label.k8s.io", Rules: []v1beta1.RuleWithOperations{{ @@ -517,7 +517,7 @@ func registerWebhookForAttachingPod(f *framework.Framework, context *certContext ObjectMeta: metav1.ObjectMeta{ Name: configName, }, - Webhooks: []v1beta1.Webhook{ + Webhooks: []v1beta1.ValidatingWebhook{ { Name: "deny-attaching-pod.k8s.io", Rules: []v1beta1.RuleWithOperations{{ @@ -561,7 +561,7 @@ func registerMutatingWebhookForConfigMap(f *framework.Framework, context *certCo ObjectMeta: metav1.ObjectMeta{ Name: configName, }, - Webhooks: []v1beta1.Webhook{ + Webhooks: []v1beta1.MutatingWebhook{ { Name: "adding-configmap-data-stage-1.k8s.io", Rules: []v1beta1.RuleWithOperations{{ @@ -638,7 +638,7 @@ func registerMutatingWebhookForPod(f *framework.Framework, context *certContext) ObjectMeta: metav1.ObjectMeta{ Name: configName, }, - Webhooks: []v1beta1.Webhook{ + Webhooks: []v1beta1.MutatingWebhook{ { Name: "adding-init-container.k8s.io", Rules: []v1beta1.RuleWithOperations{{ @@ -841,8 +841,8 @@ func testAttachingPodWebhook(f *framework.Framework) { // failingWebhook returns a webhook with rule of create configmaps, // but with an invalid client config so that server cannot communicate with it -func failingWebhook(namespace, name string) v1beta1.Webhook { - return v1beta1.Webhook{ +func failingWebhook(namespace, name string) v1beta1.ValidatingWebhook { + return v1beta1.ValidatingWebhook{ Name: name, Rules: []v1beta1.RuleWithOperations{{ Operations: []v1beta1.OperationType{v1beta1.Create}, @@ -889,7 +889,7 @@ func registerFailClosedWebhook(f *framework.Framework, context *certContext) fun ObjectMeta: metav1.ObjectMeta{ Name: configName, }, - Webhooks: []v1beta1.Webhook{ + Webhooks: []v1beta1.ValidatingWebhook{ // Server cannot talk to this webhook, so it always fails. // Because this webhook is configured fail-closed, request should be rejected after the call fails. hook, @@ -945,7 +945,7 @@ func registerValidatingWebhookForWebhookConfigurations(f *framework.Framework, c ObjectMeta: metav1.ObjectMeta{ Name: configName, }, - Webhooks: []v1beta1.Webhook{ + Webhooks: []v1beta1.ValidatingWebhook{ { Name: "deny-webhook-configuration-deletions.k8s.io", Rules: []v1beta1.RuleWithOperations{{ @@ -998,7 +998,7 @@ func registerMutatingWebhookForWebhookConfigurations(f *framework.Framework, con ObjectMeta: metav1.ObjectMeta{ Name: configName, }, - Webhooks: []v1beta1.Webhook{ + Webhooks: []v1beta1.MutatingWebhook{ { Name: "add-label-to-webhook-configurations.k8s.io", Rules: []v1beta1.RuleWithOperations{{ @@ -1050,7 +1050,7 @@ func testWebhooksForWebhookConfigurations(f *framework.Framework) { ObjectMeta: metav1.ObjectMeta{ Name: dummyValidatingWebhookConfigName, }, - Webhooks: []v1beta1.Webhook{ + Webhooks: []v1beta1.ValidatingWebhook{ { Name: "dummy-validating-webhook.k8s.io", Rules: []v1beta1.RuleWithOperations{{ @@ -1098,7 +1098,7 @@ func testWebhooksForWebhookConfigurations(f *framework.Framework) { ObjectMeta: metav1.ObjectMeta{ Name: dummyMutatingWebhookConfigName, }, - Webhooks: []v1beta1.Webhook{ + Webhooks: []v1beta1.MutatingWebhook{ { Name: "dummy-mutating-webhook.k8s.io", Rules: []v1beta1.RuleWithOperations{{ @@ -1306,7 +1306,7 @@ func registerWebhookForCustomResource(f *framework.Framework, context *certConte ObjectMeta: metav1.ObjectMeta{ Name: configName, }, - Webhooks: []v1beta1.Webhook{ + Webhooks: []v1beta1.ValidatingWebhook{ { Name: "deny-unwanted-custom-resource-data.k8s.io", Rules: []v1beta1.RuleWithOperations{{ @@ -1348,7 +1348,7 @@ func registerMutatingWebhookForCustomResource(f *framework.Framework, context *c ObjectMeta: metav1.ObjectMeta{ Name: configName, }, - Webhooks: []v1beta1.Webhook{ + Webhooks: []v1beta1.MutatingWebhook{ { Name: "mutate-custom-resource-data-stage-1.k8s.io", Rules: []v1beta1.RuleWithOperations{{ @@ -1543,7 +1543,7 @@ func registerValidatingWebhookForCRD(f *framework.Framework, context *certContex ObjectMeta: metav1.ObjectMeta{ Name: configName, }, - Webhooks: []v1beta1.Webhook{ + Webhooks: []v1beta1.ValidatingWebhook{ { Name: "deny-crd-with-unwanted-label.k8s.io", Rules: []v1beta1.RuleWithOperations{{ @@ -1649,7 +1649,7 @@ func registerSlowWebhook(f *framework.Framework, context *certContext, policy *v ObjectMeta: metav1.ObjectMeta{ Name: configName, }, - Webhooks: []v1beta1.Webhook{ + Webhooks: []v1beta1.ValidatingWebhook{ { Name: "allow-configmap-with-delay-webhook.k8s.io", Rules: []v1beta1.RuleWithOperations{{ diff --git a/test/integration/apiserver/admissionwebhook/admission_test.go b/test/integration/apiserver/admissionwebhook/admission_test.go index c39ec915abf..b243f5a24df 100644 --- a/test/integration/apiserver/admissionwebhook/admission_test.go +++ b/test/integration/apiserver/admissionwebhook/admission_test.go @@ -283,6 +283,9 @@ func (h *holder) record(phase string, converted bool, request *v1beta1.Admission return } + if debug { + h.t.Logf("recording: %#v = %s %#v %v", webhookOptions{phase: phase, converted: converted}, request.Operation, request.Resource, request.SubResource) + } h.recorded[webhookOptions{phase: phase, converted: converted}] = request } @@ -1287,7 +1290,7 @@ func createV1beta1ValidationWebhook(client clientset.Interface, endpoint, conver // Attaching Admission webhook to API server _, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&admissionv1beta1.ValidatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{Name: "admission.integration.test"}, - Webhooks: []admissionv1beta1.Webhook{ + Webhooks: []admissionv1beta1.ValidatingWebhook{ { Name: "admission.integration.test", ClientConfig: admissionv1beta1.WebhookClientConfig{ @@ -1323,7 +1326,7 @@ func createV1beta1MutationWebhook(client clientset.Interface, endpoint, converte // Attaching Mutation webhook to API server _, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&admissionv1beta1.MutatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{Name: "mutation.integration.test"}, - Webhooks: []admissionv1beta1.Webhook{ + Webhooks: []admissionv1beta1.MutatingWebhook{ { Name: "mutation.integration.test", ClientConfig: admissionv1beta1.WebhookClientConfig{ diff --git a/test/integration/apiserver/admissionwebhook/broken_webhook_test.go b/test/integration/apiserver/admissionwebhook/broken_webhook_test.go index 7a08d4cd4da..061bd07d590 100644 --- a/test/integration/apiserver/admissionwebhook/broken_webhook_test.go +++ b/test/integration/apiserver/admissionwebhook/broken_webhook_test.go @@ -155,7 +155,7 @@ func brokenWebhookConfig(name string) *admissionregistrationv1beta1.ValidatingWe ObjectMeta: metav1.ObjectMeta{ Name: name, }, - Webhooks: []admissionregistrationv1beta1.Webhook{ + Webhooks: []admissionregistrationv1beta1.ValidatingWebhook{ { Name: "broken-webhook.k8s.io", Rules: []admissionregistrationv1beta1.RuleWithOperations{{ diff --git a/test/integration/examples/webhook_test.go b/test/integration/examples/webhook_test.go index 611925a5f96..d5ce5db6bc7 100644 --- a/test/integration/examples/webhook_test.go +++ b/test/integration/examples/webhook_test.go @@ -22,7 +22,7 @@ import ( "time" admissionv1beta1 "k8s.io/api/admissionregistration/v1beta1" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" auditinternal "k8s.io/apiserver/pkg/apis/audit" @@ -65,7 +65,7 @@ func TestWebhookLoopback(t *testing.T) { fail := admissionv1beta1.Fail _, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&admissionv1beta1.MutatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{Name: "webhooktest.example.com"}, - Webhooks: []admissionv1beta1.Webhook{{ + Webhooks: []admissionv1beta1.MutatingWebhook{{ Name: "webhooktest.example.com", ClientConfig: admissionv1beta1.WebhookClientConfig{ Service: &admissionv1beta1.ServiceReference{Namespace: "default", Name: "kubernetes", Path: &webhookPath}, diff --git a/vendor/modules.txt b/vendor/modules.txt index 9ba9d302cfb..5aa65435c93 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1184,6 +1184,7 @@ k8s.io/apiserver/pkg/admission/configuration k8s.io/apiserver/pkg/admission/initializer k8s.io/apiserver/pkg/admission/metrics k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle +k8s.io/apiserver/pkg/admission/plugin/webhook k8s.io/apiserver/pkg/admission/plugin/webhook/config k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1