From 7574335a8a15b8b8f6d5ddd65c65fb6d282504b2 Mon Sep 17 00:00:00 2001 From: Dario Tranchitella Date: Mon, 21 Jun 2021 13:24:34 +0200 Subject: [PATCH] refactor: using separated webhooks for Namespace handling --- main.go | 7 +- .../{namespacequota => namespace}/errors.go | 2 +- pkg/webhook/namespace/freezed.go | 133 ++++++++++++++++++ .../validating.go => namespace/quota.go} | 26 ++-- pkg/webhook/tenant/cordoning.go | 29 +--- 5 files changed, 156 insertions(+), 41 deletions(-) rename pkg/webhook/{namespacequota => namespace}/errors.go (93%) create mode 100644 pkg/webhook/namespace/freezed.go rename pkg/webhook/{namespacequota/validating.go => namespace/quota.go} (73%) diff --git a/main.go b/main.go index 2b4a8a5c..cb05c539 100644 --- a/main.go +++ b/main.go @@ -30,7 +30,7 @@ import ( "github.com/clastix/capsule/pkg/webhook" "github.com/clastix/capsule/pkg/webhook/imagepullpolicy" "github.com/clastix/capsule/pkg/webhook/ingress" - "github.com/clastix/capsule/pkg/webhook/namespacequota" + namespacewebhook "github.com/clastix/capsule/pkg/webhook/namespace" "github.com/clastix/capsule/pkg/webhook/networkpolicies" "github.com/clastix/capsule/pkg/webhook/ownerreference" "github.com/clastix/capsule/pkg/webhook/podpriority" @@ -148,12 +148,13 @@ func main() { podpriority.Webhook(podpriority.Handler()), services.Webhook(services.Handler()), ownerreference.Webhook(utils.InCapsuleGroups(cfg, ownerreference.Handler(cfg))), - namespacequota.Webhook(utils.InCapsuleGroups(cfg, namespacequota.Handler())), + namespacewebhook.QuotaWebhook(utils.InCapsuleGroups(cfg, namespacewebhook.QuotaHandler())), + namespacewebhook.FreezedWebhook(utils.InCapsuleGroups(cfg, namespacewebhook.FreezeHandler(cfg))), networkpolicies.Webhook(utils.InCapsuleGroups(cfg, networkpolicies.Handler())), tenantprefix.Webhook(utils.InCapsuleGroups(cfg, tenantprefix.Handler(cfg))), tenant.Validating(tenant.ValidatingHandler(cfg)), imagepullpolicy.Webhook(imagepullpolicy.Handler()), - tenant.Cordoning(tenant.CordoningHandler()), + tenant.Cordoning(tenant.CordoningHandler(cfg)), ) if err = webhook.Register(manager, webhooksList...); err != nil { setupLog.Error(err, "unable to setup webhooks") diff --git a/pkg/webhook/namespacequota/errors.go b/pkg/webhook/namespace/errors.go similarity index 93% rename from pkg/webhook/namespacequota/errors.go rename to pkg/webhook/namespace/errors.go index 801f679a..6f3f6160 100644 --- a/pkg/webhook/namespacequota/errors.go +++ b/pkg/webhook/namespace/errors.go @@ -1,7 +1,7 @@ // Copyright 2020-2021 Clastix Labs // SPDX-License-Identifier: Apache-2.0 -package namespacequota +package namespace type namespaceQuotaExceededError struct{} diff --git a/pkg/webhook/namespace/freezed.go b/pkg/webhook/namespace/freezed.go new file mode 100644 index 00000000..cfbf8496 --- /dev/null +++ b/pkg/webhook/namespace/freezed.go @@ -0,0 +1,133 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package namespace + +import ( + "context" + "net/http" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1" + "github.com/clastix/capsule/pkg/configuration" + capsulewebhook "github.com/clastix/capsule/pkg/webhook" + "github.com/clastix/capsule/pkg/webhook/utils" +) + +// +kubebuilder:webhook:path=/validate-v1-namespace-freezed,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=namespaces,verbs=create;update;delete,versions=v1,name=freezed.namespace.capsule.clastix.io + +type freezedWebhook struct { + handler capsulewebhook.Handler +} + +func FreezedWebhook(handler capsulewebhook.Handler) capsulewebhook.Webhook { + return &freezedWebhook{ + handler: handler, + } +} + +func (w *freezedWebhook) GetHandler() capsulewebhook.Handler { + return w.handler +} + +func (w *freezedWebhook) GetName() string { + return "NamespaceFreezed" +} + +func (w *freezedWebhook) GetPath() string { + return "/validate-v1-namespace-freezed" +} + +type freezedHandler struct { + configuration configuration.Configuration +} + +func FreezeHandler(configuration configuration.Configuration) capsulewebhook.Handler { + return &freezedHandler{configuration: configuration} +} + +func (r *freezedHandler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) admission.Response { + ns := &corev1.Namespace{} + if err := decoder.Decode(req, ns); err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + for _, objectRef := range ns.ObjectMeta.OwnerReferences { + // retrieving the selected Tenant + tnt := &capsulev1alpha1.Tenant{} + if err := client.Get(ctx, types.NamespacedName{Name: objectRef.Name}, tnt); err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + if tnt.IsCordoned() { + recorder.Eventf(tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespace %s cannot be attached, the current Tenant is freezed", ns.GetName()) + + return admission.Denied("the selected Tenant is freezed") + } + } + // creating NS that is not bounded to any Tenant + return admission.Allowed("") + } +} + +func (r *freezedHandler) OnDelete(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) admission.Response { + tntList := &capsulev1alpha1.TenantList{} + if err := c.List(ctx, tntList, client.MatchingFieldsSelector{ + Selector: fields.OneTermEqualSelector(".status.namespaces", req.Name), + }); err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + if len(tntList.Items) == 0 { + return admission.Allowed("") + } + + tnt := tntList.Items[0] + + if tnt.IsCordoned() && utils.RequestFromOwnerOrSA(tnt, req, r.configuration.UserGroups()) { + recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespaced %s cannot be deleted, the current Tenant is freezed", req.Name) + + return admission.Denied("the selected Tenant is freezed") + } + + return admission.Allowed("") + } +} + +func (r *freezedHandler) OnUpdate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) admission.Response { + ns := &corev1.Namespace{} + if err := decoder.Decode(req, ns); err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + tntList := &capsulev1alpha1.TenantList{} + if err := c.List(ctx, tntList, client.MatchingFieldsSelector{ + Selector: fields.OneTermEqualSelector(".status.namespaces", ns.Name), + }); err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + if len(tntList.Items) == 0 { + return admission.Allowed("") + } + + tnt := tntList.Items[0] + + if tnt.IsCordoned() && utils.RequestFromOwnerOrSA(tnt, req, r.configuration.UserGroups()) { + recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespaced %s cannot be updated, the current Tenant is freezed", ns.GetName()) + + return admission.Denied("the selected Tenant is freezed") + } + + return admission.Allowed("") + } +} diff --git a/pkg/webhook/namespacequota/validating.go b/pkg/webhook/namespace/quota.go similarity index 73% rename from pkg/webhook/namespacequota/validating.go rename to pkg/webhook/namespace/quota.go index 66b3bdaf..beb802a5 100644 --- a/pkg/webhook/namespacequota/validating.go +++ b/pkg/webhook/namespace/quota.go @@ -1,7 +1,7 @@ // Copyright 2020-2021 Clastix Labs // SPDX-License-Identifier: Apache-2.0 -package namespacequota +package namespace import ( "context" @@ -19,36 +19,36 @@ import ( // +kubebuilder:webhook:path=/validate-v1-namespace-quota,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=namespaces,verbs=create,versions=v1,name=quota.namespace.capsule.clastix.io -type webhook struct { +type quotaWebhook struct { handler capsulewebhook.Handler } -func Webhook(handler capsulewebhook.Handler) capsulewebhook.Webhook { - return &webhook{ +func QuotaWebhook(handler capsulewebhook.Handler) capsulewebhook.Webhook { + return "aWebhook{ handler: handler, } } -func (w *webhook) GetHandler() capsulewebhook.Handler { +func (w *quotaWebhook) GetHandler() capsulewebhook.Handler { return w.handler } -func (w *webhook) GetName() string { +func (w *quotaWebhook) GetName() string { return "NamespaceQuota" } -func (w *webhook) GetPath() string { +func (w *quotaWebhook) GetPath() string { return "/validate-v1-namespace-quota" } -type handler struct { +type quotaHandler struct { } -func Handler() capsulewebhook.Handler { - return &handler{} +func QuotaHandler() capsulewebhook.Handler { + return "aHandler{} } -func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { +func (r *quotaHandler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { return func(ctx context.Context, req admission.Request) admission.Response { ns := &corev1.Namespace{} if err := decoder.Decode(req, ns); err != nil { @@ -78,13 +78,13 @@ func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder, rec } } -func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { +func (r *quotaHandler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { return func(ctx context.Context, req admission.Request) admission.Response { return admission.Allowed("") } } -func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { +func (r *quotaHandler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { return func(ctx context.Context, req admission.Request) admission.Response { return admission.Allowed("") } diff --git a/pkg/webhook/tenant/cordoning.go b/pkg/webhook/tenant/cordoning.go index 27957b27..b99ca6bc 100644 --- a/pkg/webhook/tenant/cordoning.go +++ b/pkg/webhook/tenant/cordoning.go @@ -17,8 +17,8 @@ import ( capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1" "github.com/clastix/capsule/pkg/configuration" - "github.com/clastix/capsule/pkg/utils" capsulewebhook "github.com/clastix/capsule/pkg/webhook" + "github.com/clastix/capsule/pkg/webhook/utils" ) // +kubebuilder:webhook:path=/tenant-cordoning,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="*",resources="*",verbs=create;update;delete,versions="*",name=cordoning.tenant.capsule.clastix.io @@ -47,29 +47,10 @@ type cordoningHandler struct { configuration configuration.Configuration } -func CordoningHandler() capsulewebhook.Handler { - return &cordoningHandler{} -} - -func (h *cordoningHandler) requestFromOwnerOrSA(tenant capsulev1alpha1.Tenant, req admission.Request) bool { - switch { - case tenant.Spec.Owner.Kind == "User" && req.UserInfo.Username == tenant.Spec.Owner.Name: - return true - case tenant.Spec.Owner.Kind == "Group": - groupList := utils.NewUserGroupList(req.UserInfo.Groups) - for _, group := range h.configuration.UserGroups() { - if groupList.Find(group) { - return true - } - } - default: - for _, group := range req.UserInfo.Groups { - if len(req.Namespace) > 0 && strings.HasPrefix(group, "system:serviceaccounts:"+req.Namespace) { - return true - } - } +func CordoningHandler(configuration configuration.Configuration) capsulewebhook.Handler { + return &cordoningHandler{ + configuration: configuration, } - return false } func (h *cordoningHandler) cordonHandler(ctx context.Context, clt client.Client, req admission.Request, recorder record.EventRecorder) admission.Response { @@ -88,7 +69,7 @@ func (h *cordoningHandler) cordonHandler(ctx context.Context, clt client.Client, tnt := tntList.Items[0] if tnt.IsCordoned() { - if h.requestFromOwnerOrSA(tnt, req) { + if utils.RequestFromOwnerOrSA(tnt, req, h.configuration.UserGroups()) { recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "%s %s/%s cannot be %sd, current Tenant is freezed", req.Kind.String(), req.Namespace, req.Name, strings.ToLower(string(req.Operation))) return admission.Denied(fmt.Sprintf("tenant %s is freezed: please, reach out to the system administrator", tnt.GetName()))