diff --git a/pkg/builder/webhook.go b/pkg/builder/webhook.go index d24877d303..704792ea68 100644 --- a/pkg/builder/webhook.go +++ b/pkg/builder/webhook.go @@ -17,6 +17,7 @@ limitations under the License. package builder import ( + "errors" "net/http" "net/url" "strings" @@ -33,6 +34,7 @@ import ( // WebhookBuilder builds a Webhook. type WebhookBuilder struct { apiType runtime.Object + forType admission.For gvk schema.GroupVersionKind mgr manager.Manager config *rest.Config @@ -53,6 +55,15 @@ func (blder *WebhookBuilder) For(apiType runtime.Object) *WebhookBuilder { return blder } +// HandlerFor takes a admission.admissionFor interface. +// +// If the given object implements the admission.DefaulterFor interface, a MutatingWebhook will be wired for this type. +// If the given object implements the admission.ValidatorFor interface, a ValidatingWebhook will be wired for this type. +func (blder *WebhookBuilder) HandlerFor(forType admission.For) *WebhookBuilder { + blder.forType = forType + return blder +} + // Complete builds the webhook. func (blder *WebhookBuilder) Complete() error { // Set the Config @@ -69,9 +80,13 @@ func (blder *WebhookBuilder) loadRestConfig() { } func (blder *WebhookBuilder) registerWebhooks() error { + typ, err := blder.getType() + if err != nil { + return err + } + // Create webhook(s) for each type - var err error - blder.gvk, err = apiutil.GVKForObject(blder.apiType, blder.mgr.GetScheme()) + blder.gvk, err = apiutil.GVKForObject(typ, blder.mgr.GetScheme()) if err != nil { return err } @@ -88,12 +103,7 @@ func (blder *WebhookBuilder) registerWebhooks() error { // registerDefaultingWebhook registers a defaulting webhook if th. func (blder *WebhookBuilder) registerDefaultingWebhook() { - defaulter, isDefaulter := blder.apiType.(admission.Defaulter) - if !isDefaulter { - log.Info("skip registering a mutating webhook, admission.Defaulter interface is not implemented", "GVK", blder.gvk) - return - } - mwh := admission.DefaultingWebhookFor(defaulter) + mwh := blder.getDefaultingWebhook() if mwh != nil { path := generateMutatePath(blder.gvk) @@ -108,10 +118,22 @@ func (blder *WebhookBuilder) registerDefaultingWebhook() { } } +func (blder *WebhookBuilder) getDefaultingWebhook() *admission.Webhook { + if defaulter, ok := blder.apiType.(admission.Defaulter); ok { + return admission.DefaultingWebhookFor(defaulter) + } + if defaulter, ok := blder.forType.(admission.DefaulterFor); ok { + return admission.DefaulterForToWebhook(defaulter) + } + log.Info( + "skip registering a mutating webhook, admission.Defaulter or admission.DefaulterFor interface is not implemented", + "GVK", blder.gvk) + return nil +} + func (blder *WebhookBuilder) registerValidatingWebhook() { validator, isValidator := blder.apiType.(admission.Validator) if !isValidator { - log.Info("skip registering a validating webhook, admission.Validator interface is not implemented", "GVK", blder.gvk) return } vwh := admission.ValidatingWebhookFor(validator) @@ -129,6 +151,19 @@ func (blder *WebhookBuilder) registerValidatingWebhook() { } } +func (blder *WebhookBuilder) getValidatingWebhook() *admission.Webhook { + if validator, ok := blder.apiType.(admission.Validator); ok { + return admission.ValidatingWebhookFor(validator) + } + if validator, ok := blder.forType.(admission.ValidatorFor); ok { + return admission.ValidatorForToWebhook(validator) + } + log.Info( + "skip registering a validating webhook, admission.Validator or admission.ValidatorFor interface is not implemented", + "GVK", blder.gvk) + return nil +} + func (blder *WebhookBuilder) registerConversionWebhook() error { ok, err := conversion.IsConvertible(blder.mgr.GetScheme(), blder.apiType) if err != nil { @@ -145,6 +180,16 @@ func (blder *WebhookBuilder) registerConversionWebhook() error { return nil } +func (blder *WebhookBuilder) getType() (runtime.Object, error) { + if blder.apiType != nil { + return blder.apiType, nil + } + if blder.forType != nil { + return blder.forType.For(), nil + } + return nil, errors.New("one of For() or HandlerFor() should be called") +} + func (blder *WebhookBuilder) isAlreadyHandled(path string) bool { if blder.mgr.GetWebhookServer().WebhookMux == nil { return false diff --git a/pkg/webhook/admission/admission_for.go b/pkg/webhook/admission/admission_for.go new file mode 100644 index 0000000000..c07cb25ec0 --- /dev/null +++ b/pkg/webhook/admission/admission_for.go @@ -0,0 +1,8 @@ +package admission + +import "k8s.io/apimachinery/pkg/runtime" + +// For registers a type as an admission handler. +type For interface { + For() runtime.Object +} diff --git a/pkg/webhook/admission/defaulter_handler.go b/pkg/webhook/admission/defaulter_handler.go new file mode 100644 index 0000000000..bf2626bc99 --- /dev/null +++ b/pkg/webhook/admission/defaulter_handler.go @@ -0,0 +1,74 @@ +/* +Copyright 2018 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 admission + +import ( + "context" + "encoding/json" + "net/http" + + "k8s.io/apimachinery/pkg/runtime" +) + +// DefaulterFor defines functions for setting defaults on resources. +type DefaulterFor interface { + For + Default(ctx context.Context, obj runtime.Object) +} + +// DefaulterForToWebhook creates a new Webhook for a DefaulterFor interface. +func DefaulterForToWebhook(defaulter DefaulterFor) *Webhook { + return &Webhook{ + Handler: &defaulterForType{handler: defaulter}, + } +} + +type defaulterForType struct { + handler DefaulterFor + decoder *Decoder + scheme *runtime.Scheme +} + +var _ DecoderInjector = &defaulterForType{} + +func (h *defaulterForType) InjectDecoder(d *Decoder) error { + h.decoder = d + return nil +} + +// Handle handles admission requests. +func (h *defaulterForType) Handle(ctx context.Context, req Request) Response { + if h.handler == nil { + panic("defaulter should never be nil") + } + + // Get the object in the request + obj := h.handler.For().DeepCopyObject() + if err := h.decoder.Decode(req, obj); err != nil { + return Errored(http.StatusBadRequest, err) + } + + // Default the object + h.handler.Default(ctx, obj) + marshalled, err := json.Marshal(obj) + if err != nil { + return Errored(http.StatusInternalServerError, err) + } + + // Create the patch + return PatchResponseFromRaw(req.Object.Raw, marshalled) +} diff --git a/pkg/webhook/admission/validator_handler.go b/pkg/webhook/admission/validator_handler.go new file mode 100644 index 0000000000..22a87bc4a3 --- /dev/null +++ b/pkg/webhook/admission/validator_handler.go @@ -0,0 +1,122 @@ +/* +Copyright 2018 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 admission + +import ( + "context" + goerrors "errors" + "net/http" + + v1 "k8s.io/api/admission/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" +) + +// Validator defines functions for validating an operation. +type ValidatorFor interface { + For + ValidateCreate(ctx context.Context, obj runtime.Object) error + ValidateUpdate(ctx context.Context, old runtime.Object, new runtime.Object) error + ValidateDelete(ctx context.Context, obj runtime.Object) error +} + +// ValidatingWebhookFor creates a new Webhook for validating the provided type. +func ValidatorForToWebhook(validator ValidatorFor) *Webhook { + return &Webhook{ + Handler: &validatorForType{handler: validator}, + } +} + +type validatorForType struct { + handler ValidatorFor + decoder *Decoder +} + +var _ DecoderInjector = &validatorForType{} + +// InjectDecoder injects the decoder into a validatingHandler. +func (h *validatorForType) InjectDecoder(d *Decoder) error { + h.decoder = d + return nil +} + +// Handle handles admission requests. +func (h *validatorForType) Handle(ctx context.Context, req Request) Response { + if h.handler == nil { + panic("handler should never be nil") + } + + // Get the object in the request + obj := h.handler.For().DeepCopyObject() + if req.Operation == v1.Create { + err := h.decoder.Decode(req, obj) + if err != nil { + return Errored(http.StatusBadRequest, err) + } + + err = h.handler.ValidateCreate(ctx, obj) + if err != nil { + var apiStatus apierrors.APIStatus + if goerrors.As(err, &apiStatus) { + return validationResponseFromStatus(false, apiStatus.Status()) + } + return Denied(err.Error()) + } + } + + if req.Operation == v1.Update { + oldObj := obj.DeepCopyObject() + + err := h.decoder.DecodeRaw(req.Object, obj) + if err != nil { + return Errored(http.StatusBadRequest, err) + } + err = h.decoder.DecodeRaw(req.OldObject, oldObj) + if err != nil { + return Errored(http.StatusBadRequest, err) + } + + err = h.handler.ValidateUpdate(ctx, oldObj, obj) + if err != nil { + var apiStatus apierrors.APIStatus + if goerrors.As(err, &apiStatus) { + return validationResponseFromStatus(false, apiStatus.Status()) + } + return Denied(err.Error()) + } + } + + if req.Operation == v1.Delete { + // In reference to PR: https://github.com/kubernetes/kubernetes/pull/76346 + // OldObject contains the object being deleted + err := h.decoder.DecodeRaw(req.OldObject, obj) + if err != nil { + return Errored(http.StatusBadRequest, err) + } + + err = h.handler.ValidateDelete(ctx, obj) + if err != nil { + var apiStatus apierrors.APIStatus + if goerrors.As(err, &apiStatus) { + return validationResponseFromStatus(false, apiStatus.Status()) + } + return Denied(err.Error()) + } + } + + return Allowed("") +}