diff --git a/main.go b/main.go index cb05c539..cf157285 100644 --- a/main.go +++ b/main.go @@ -28,17 +28,15 @@ import ( "github.com/clastix/capsule/pkg/configuration" "github.com/clastix/capsule/pkg/indexer" "github.com/clastix/capsule/pkg/webhook" - "github.com/clastix/capsule/pkg/webhook/imagepullpolicy" "github.com/clastix/capsule/pkg/webhook/ingress" namespacewebhook "github.com/clastix/capsule/pkg/webhook/namespace" - "github.com/clastix/capsule/pkg/webhook/networkpolicies" + "github.com/clastix/capsule/pkg/webhook/networkpolicy" "github.com/clastix/capsule/pkg/webhook/ownerreference" - "github.com/clastix/capsule/pkg/webhook/podpriority" + "github.com/clastix/capsule/pkg/webhook/pod" "github.com/clastix/capsule/pkg/webhook/pvc" - "github.com/clastix/capsule/pkg/webhook/registry" - "github.com/clastix/capsule/pkg/webhook/services" + "github.com/clastix/capsule/pkg/webhook/route" + "github.com/clastix/capsule/pkg/webhook/service" "github.com/clastix/capsule/pkg/webhook/tenant" - "github.com/clastix/capsule/pkg/webhook/tenantprefix" "github.com/clastix/capsule/pkg/webhook/utils" // +kubebuilder:scaffold:imports ) @@ -142,19 +140,15 @@ func main() { // webhooks: the order matters, don't change it and just append webhooksList := append( make([]webhook.Webhook, 0), - ingress.Webhook(ingress.Handler(cfg)), - pvc.Webhook(pvc.Handler()), - registry.Webhook(registry.Handler()), - podpriority.Webhook(podpriority.Handler()), - services.Webhook(services.Handler()), - ownerreference.Webhook(utils.InCapsuleGroups(cfg, ownerreference.Handler(cfg))), - 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(cfg)), + route.Pod(pod.ImagePullPolicy(), pod.ContainerRegistry(), pod.PriorityClass()), + route.Namespace(utils.InCapsuleGroups(cfg, namespacewebhook.QuotaHandler(), namespacewebhook.FreezeHandler(cfg), namespacewebhook.PrefixHandler(cfg))), + route.Ingress(ingress.Class(cfg), ingress.Hostnames(cfg), ingress.Collision(cfg)), + route.PVC(pvc.Handler()), + route.Service(service.Handler()), + route.NetworkPolicy(utils.InCapsuleGroups(cfg, networkpolicy.Handler())), + route.Tenant(tenant.NameHandler(), tenant.IngressClassRegexHandler(), tenant.StorageClassRegexHandler(), tenant.ContainerRegistryRegexHandler(), tenant.HostnameRegexHandler(), tenant.HostnamesCollisionHandler(cfg), tenant.FreezedEmitter()), + route.OwnerReference(utils.InCapsuleGroups(cfg, ownerreference.Handler(cfg))), + route.Cordoning(tenant.CordoningHandler(cfg)), ) if err = webhook.Register(manager, webhooksList...); err != nil { setupLog.Error(err, "unable to setup webhooks") diff --git a/pkg/webhook/handler.go b/pkg/webhook/handler.go index d0666eb2..1415bd34 100644 --- a/pkg/webhook/handler.go +++ b/pkg/webhook/handler.go @@ -11,7 +11,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) -type Func func(ctx context.Context, req admission.Request) admission.Response +type Func func(ctx context.Context, req admission.Request) *admission.Response type Handler interface { OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) Func diff --git a/pkg/webhook/imagepullpolicy/validating.go b/pkg/webhook/imagepullpolicy/validating.go deleted file mode 100644 index a1d83e13..00000000 --- a/pkg/webhook/imagepullpolicy/validating.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2020-2021 Clastix Labs -// SPDX-License-Identifier: Apache-2.0 - -package imagepullpolicy - -import ( - "context" - "net/http" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/fields" - "k8s.io/client-go/tools/record" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - - "github.com/clastix/capsule/api/v1alpha1" - "github.com/clastix/capsule/api/v1alpha1/domain" - capsulewebhook "github.com/clastix/capsule/pkg/webhook" -) - -// +kubebuilder:webhook:path=/validating-imagepullpolicy,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=pods,verbs=create,versions=v1,name=validating-image-pull-policy.capsule.clastix.io - -type webhook struct { - handler capsulewebhook.Handler -} - -func Webhook(handler capsulewebhook.Handler) capsulewebhook.Webhook { - return &webhook{handler: handler} -} - -func (w *webhook) GetHandler() capsulewebhook.Handler { - return w.handler -} - -func (w *webhook) GetName() string { - return "ImagePullPolicy" -} - -func (w *webhook) GetPath() string { - return "/validating-imagepullpolicy" -} - -type handler struct{} - -func Handler() capsulewebhook.Handler { - return &handler{} -} - -func (r *handler) OnCreate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { - var pod = &corev1.Pod{} - - if err := decoder.Decode(req, pod); err != nil { - return admission.Errored(http.StatusBadRequest, err) - } - - var tntList = &v1alpha1.TenantList{} - - if err := c.List(ctx, tntList, client.MatchingFieldsSelector{ - Selector: fields.OneTermEqualSelector(".status.namespaces", pod.Namespace), - }); err != nil { - return admission.Errored(http.StatusBadRequest, err) - } - // the Pod is not running in a Namespace managed by a Tenant - if len(tntList.Items) == 0 { - return admission.Allowed("") - } - - tnt := tntList.Items[0] - - policy := domain.NewImagePullPolicy(&tnt) - // if Tenant doesn't enforce the pull policy, exit - if policy == nil { - return admission.Allowed("") - } - - for _, container := range pod.Spec.Containers { - usedPullPolicy := string(container.ImagePullPolicy) - - if !policy.IsPolicySupported(usedPullPolicy) { - recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenPullPolicy", "Pod %s/%s pull policy %s is forbidden for the current Tenant", req.Namespace, req.Name, usedPullPolicy) - - return admission.Denied(NewImagePullPolicyForbidden(usedPullPolicy, container.Name, policy.AllowedPullPolicies()).Error()) - } - } - - return admission.Allowed("") - } -} - -func (r *handler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { - return admission.Allowed("") - } -} - -func (r *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { - return admission.Allowed("") - } -} diff --git a/pkg/webhook/ingress/types.go b/pkg/webhook/ingress/types.go index 2879773e..3b228ecd 100644 --- a/pkg/webhook/ingress/types.go +++ b/pkg/webhook/ingress/types.go @@ -6,7 +6,7 @@ package ingress import ( extensionsv1beta1 "k8s.io/api/extensions/v1beta1" networkingv1 "k8s.io/api/networking/v1" - networkingv1beta "k8s.io/api/networking/v1beta1" + networkingv1beta1 "k8s.io/api/networking/v1beta1" ) const ( @@ -54,7 +54,7 @@ func (n NetworkingV1) Hostnames() []string { } type NetworkingV1Beta1 struct { - *networkingv1beta.Ingress + *networkingv1beta1.Ingress } func (n NetworkingV1Beta1) Name() string { diff --git a/pkg/webhook/ingress/utils.go b/pkg/webhook/ingress/utils.go new file mode 100644 index 00000000..8fab3f55 --- /dev/null +++ b/pkg/webhook/ingress/utils.go @@ -0,0 +1,61 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package ingress + +import ( + "context" + "fmt" + + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + networkingv1 "k8s.io/api/networking/v1" + networkingv1beta1 "k8s.io/api/networking/v1beta1" + "k8s.io/apimachinery/pkg/fields" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1" +) + +func tenantFromIngress(ctx context.Context, c client.Client, ingress Ingress) (*capsulev1alpha1.Tenant, error) { + tenantList := &capsulev1alpha1.TenantList{} + if err := c.List(ctx, tenantList, client.MatchingFieldsSelector{ + Selector: fields.OneTermEqualSelector(".status.namespaces", ingress.Namespace()), + }); err != nil { + return nil, err + } + + if len(tenantList.Items) == 0 { + return nil, nil + } + + return &tenantList.Items[0], nil +} + +func ingressFromRequest(req admission.Request, decoder *admission.Decoder) (ingress Ingress, err error) { + switch req.Kind.Group { + case "networking.k8s.io": + if req.Kind.Version == "v1" { + ingressObj := &networkingv1.Ingress{} + if err = decoder.Decode(req, ingressObj); err != nil { + return + } + ingress = NetworkingV1{Ingress: ingressObj} + break + } + ingressObj := &networkingv1beta1.Ingress{} + if err = decoder.Decode(req, ingressObj); err != nil { + return + } + ingress = NetworkingV1Beta1{Ingress: ingressObj} + case "extensions": + ingressObj := &extensionsv1beta1.Ingress{} + if err = decoder.Decode(req, ingressObj); err != nil { + return + } + ingress = Extension{Ingress: ingressObj} + default: + err = fmt.Errorf("cannot recognize type %s", req.Kind.Group) + } + return +} diff --git a/pkg/webhook/ingress/validate_class.go b/pkg/webhook/ingress/validate_class.go new file mode 100644 index 00000000..6ee32e73 --- /dev/null +++ b/pkg/webhook/ingress/validate_class.go @@ -0,0 +1,134 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package ingress + +import ( + "context" + + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "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" +) + +type class struct { + configuration configuration.Configuration +} + +func Class(configuration configuration.Configuration) capsulewebhook.Handler { + return &class{configuration: configuration} +} + +func (r *class) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + ingress, err := ingressFromRequest(req, decoder) + if err != nil { + return utils.ErroredResponse(err) + } + + var tenant *v1alpha1.Tenant + + tenant, err = tenantFromIngress(ctx, client, ingress) + if err != nil { + return utils.ErroredResponse(err) + } + + if tenant == nil { + return nil + } + + if err = r.validateClass(*tenant, ingress.IngressClass()); err == nil { + return nil + } + + var forbiddenErr *ingressClassForbidden + + if errors.As(err, &forbiddenErr) { + recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressClassForbidden", "Ingress %s/%s class is forbidden", ingress.Namespace(), ingress.Name()) + } + + var invalidErr *ingressClassNotValid + + if errors.As(err, &invalidErr) { + recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressClassNotValid", "Ingress %s/%s class is invalid", ingress.Namespace(), ingress.Name()) + } + + response := admission.Denied(err.Error()) + + return &response + } +} + +func (r *class) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + ingress, err := ingressFromRequest(req, decoder) + if err != nil { + return utils.ErroredResponse(err) + } + + var tenant *v1alpha1.Tenant + + tenant, err = tenantFromIngress(ctx, client, ingress) + if err != nil { + return utils.ErroredResponse(err) + } + + if tenant == nil { + return nil + } + + err = r.validateClass(*tenant, ingress.IngressClass()) + + var forbiddenErr *ingressClassForbidden + + if errors.As(err, &forbiddenErr) { + recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressClassForbidden", "Ingress %s/%s class is forbidden", ingress.Namespace(), ingress.Name()) + } + + var invalidErr *ingressClassNotValid + + if errors.As(err, &invalidErr) { + recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressClassNotValid", "Ingress %s/%s class is invalid", ingress.Namespace(), ingress.Name()) + } + + response := admission.Denied(err.Error()) + + return &response + } +} + +func (r *class) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil + } +} + +func (r *class) validateClass(tenant v1alpha1.Tenant, ingressClass *string) error { + if tenant.Spec.IngressClasses == nil { + return nil + } + + if ingressClass == nil { + return NewIngressClassNotValid(*tenant.Spec.IngressClasses) + } + + var valid, matched bool + + if len(tenant.Spec.IngressClasses.Exact) > 0 { + valid = tenant.Spec.IngressClasses.ExactMatch(*ingressClass) + } + matched = tenant.Spec.IngressClasses.RegexMatch(*ingressClass) + + if !valid && !matched { + return NewIngressClassForbidden(*ingressClass, *tenant.Spec.IngressClasses) + } + + return nil +} diff --git a/pkg/webhook/ingress/validate_collision.go b/pkg/webhook/ingress/validate_collision.go new file mode 100644 index 00000000..25677ad0 --- /dev/null +++ b/pkg/webhook/ingress/validate_collision.go @@ -0,0 +1,177 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package ingress + +import ( + "context" + + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + networkingv1 "k8s.io/api/networking/v1" + networkingv1beta1 "k8s.io/api/networking/v1beta1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "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" +) + +type collision struct { + configuration configuration.Configuration +} + +func Collision(configuration configuration.Configuration) capsulewebhook.Handler { + return &collision{configuration: configuration} +} + +func (r *collision) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + ingress, err := ingressFromRequest(req, decoder) + if err != nil { + return utils.ErroredResponse(err) + } + + var tenant *v1alpha1.Tenant + + tenant, err = tenantFromIngress(ctx, client, ingress) + if err != nil { + return utils.ErroredResponse(err) + } + + if tenant == nil { + return nil + } + + if err = r.validateCollision(ctx, client, ingress); err == nil { + return nil + } + + var collisionErr *ingressHostnameCollision + + if errors.As(err, &collisionErr) { + recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressHostnameCollision", "Ingress %s/%s hostname is colliding", ingress.Namespace(), ingress.Name()) + } + + response := admission.Denied(err.Error()) + + return &response + } +} + +func (r *collision) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + ingress, err := ingressFromRequest(req, decoder) + if err != nil { + return utils.ErroredResponse(err) + } + + var tenant *v1alpha1.Tenant + + tenant, err = tenantFromIngress(ctx, client, ingress) + if err != nil { + return utils.ErroredResponse(err) + } + + if tenant == nil { + return nil + } + + err = r.validateCollision(ctx, client, ingress) + + var collisionErr *ingressHostnameCollision + + if errors.As(err, &collisionErr) { + recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressHostnameCollision", "Ingress %s/%s hostname is colliding", ingress.Namespace(), ingress.Name()) + } + + response := admission.Denied(err.Error()) + + return &response + } +} + +func (r *collision) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil + } +} + +func (r *collision) validateCollision(ctx context.Context, clt client.Client, ingress Ingress) error { + if r.configuration.AllowIngressHostnameCollision() { + return nil + } + + for _, hostname := range ingress.Hostnames() { + switch ingress.(type) { + case Extension: + ingressObjList := &extensionsv1beta1.IngressList{} + err := clt.List(ctx, ingressObjList, client.MatchingFieldsSelector{ + Selector: fields.OneTermEqualSelector(".spec.rules[*].host", hostname), + }) + if err != nil { + return err + } + + switch len(ingressObjList.Items) { + case 0: + break + case 1: + if ingressObj := ingressObjList.Items[0]; ingressObj.GetName() == ingress.Name() && ingressObj.GetNamespace() == ingress.Namespace() { + break + } + fallthrough + default: + return NewIngressHostnameCollision(hostname) + } + case NetworkingV1: + ingressObjList := &networkingv1.IngressList{} + err := clt.List(ctx, ingressObjList, client.MatchingFieldsSelector{ + Selector: fields.OneTermEqualSelector(".spec.rules[*].host", hostname), + }) + if err != nil { + return errors.Wrap(err, "cannot list *networkingv1.IngressList by MatchingFieldsSelector") + } + + switch len(ingressObjList.Items) { + case 0: + break + case 1: + if ingressObj := ingressObjList.Items[0]; ingressObj.GetName() == ingress.Name() && ingressObj.GetNamespace() == ingress.Namespace() { + break + } + fallthrough + default: + return NewIngressHostnameCollision(hostname) + } + case NetworkingV1Beta1: + ingressObjList := &networkingv1beta1.IngressList{} + err := clt.List(ctx, ingressObjList, client.MatchingFieldsSelector{ + Selector: fields.OneTermEqualSelector(".spec.rules[*].host", hostname), + }) + if err != nil { + return errors.Wrap(err, "cannot list *networkingv1beta1.IngressList by MatchingFieldsSelector") + } + + switch len(ingressObjList.Items) { + case 0: + break + case 1: + if ingressObj := ingressObjList.Items[0]; ingressObj.GetName() == ingress.Name() && ingressObj.GetNamespace() == ingress.Namespace() { + break + } + + fallthrough + default: + return NewIngressHostnameCollision(hostname) + } + } + } + + return nil +} diff --git a/pkg/webhook/ingress/validate_hostnames.go b/pkg/webhook/ingress/validate_hostnames.go new file mode 100644 index 00000000..32b14c1a --- /dev/null +++ b/pkg/webhook/ingress/validate_hostnames.go @@ -0,0 +1,145 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package ingress + +import ( + "context" + "regexp" + + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "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" +) + +type hostnames struct { + configuration configuration.Configuration +} + +func Hostnames(configuration configuration.Configuration) capsulewebhook.Handler { + return &hostnames{configuration: configuration} +} + +func (r *hostnames) OnCreate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + ingress, err := ingressFromRequest(req, decoder) + if err != nil { + return utils.ErroredResponse(err) + } + + var tenant *v1alpha1.Tenant + + tenant, err = tenantFromIngress(ctx, c, ingress) + if err != nil { + return utils.ErroredResponse(err) + } + + if tenant == nil { + return nil + } + + if err = r.validateHostnames(*tenant, ingress.Hostnames()); err == nil { + return nil + } + + var hostnameNotValidErr *ingressHostnameNotValid + + if errors.As(err, &hostnameNotValidErr) { + recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressHostnameNotValid", "Ingress %s/%s hostname is not valid", ingress.Namespace(), ingress.Name()) + + response := admission.Denied(err.Error()) + + return &response + } + + return utils.ErroredResponse(err) + } +} + +func (r *hostnames) OnUpdate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + ingress, err := ingressFromRequest(req, decoder) + if err != nil { + return utils.ErroredResponse(err) + } + + var tenant *v1alpha1.Tenant + + tenant, err = tenantFromIngress(ctx, c, ingress) + if err != nil { + return utils.ErroredResponse(err) + } + + if tenant == nil { + return nil + } + + err = r.validateHostnames(*tenant, ingress.Hostnames()) + + var hostnameNotValidErr *ingressHostnameNotValid + + if errors.As(err, &hostnameNotValidErr) { + recorder.Eventf(tenant, corev1.EventTypeWarning, "IngressHostnameNotValid", "Ingress %s/%s hostname is not valid", ingress.Namespace(), ingress.Name()) + + response := admission.Denied(err.Error()) + + return &response + } + + return utils.ErroredResponse(err) + } +} + +func (r *hostnames) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil + } +} + +func (r *hostnames) validateHostnames(tenant v1alpha1.Tenant, hostnames []string) error { + if tenant.Spec.IngressHostnames == nil { + return nil + } + + var valid, matched bool + + var invalidHostnames []string + if len(hostnames) > 0 { + for _, currentHostname := range hostnames { + isPresent := v1alpha1.IngressHostnamesList(tenant.Spec.IngressHostnames.Exact).IsStringInList(currentHostname) + if !isPresent { + invalidHostnames = append(invalidHostnames, currentHostname) + } + } + if len(invalidHostnames) == 0 { + valid = true + } + } + + var notMatchingHostnames []string + allowedRegex := tenant.Spec.IngressHostnames.Regex + if len(allowedRegex) > 0 { + for _, currentHostname := range hostnames { + matched, _ = regexp.MatchString(tenant.Spec.IngressHostnames.Regex, currentHostname) + if !matched { + notMatchingHostnames = append(notMatchingHostnames, currentHostname) + } + } + if len(notMatchingHostnames) == 0 { + matched = true + } + } + + if !valid && !matched { + return NewIngressHostnamesNotValid(invalidHostnames, notMatchingHostnames, *tenant.Spec.IngressHostnames) + } + + return nil +} diff --git a/pkg/webhook/ingress/validating.go b/pkg/webhook/ingress/validating.go deleted file mode 100644 index 63998b2b..00000000 --- a/pkg/webhook/ingress/validating.go +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright 2020-2021 Clastix Labs -// SPDX-License-Identifier: Apache-2.0 - -package ingress - -import ( - "context" - "fmt" - "net/http" - "regexp" - "strings" - - "github.com/pkg/errors" - corev1 "k8s.io/api/core/v1" - extensionsv1beta1 "k8s.io/api/extensions/v1beta1" - networkingv1 "k8s.io/api/networking/v1" - networkingv1beta1 "k8s.io/api/networking/v1beta1" - "k8s.io/apimachinery/pkg/fields" - "k8s.io/client-go/tools/record" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - - "github.com/clastix/capsule/api/v1alpha1" - "github.com/clastix/capsule/pkg/configuration" - capsulewebhook "github.com/clastix/capsule/pkg/webhook" -) - -// +kubebuilder:webhook:path=/validating-ingress,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups=networking.k8s.io;extensions,resources=ingresses,verbs=create;update,versions=v1beta1,name=ingress-v1beta1.capsule.clastix.io -// +kubebuilder:webhook:path=/validating-ingress,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups=networking.k8s.io,resources=ingresses,verbs=create;update,versions=v1,name=ingress-v1.capsule.clastix.io - -type webhook struct { - handler capsulewebhook.Handler -} - -func Webhook(handler capsulewebhook.Handler) capsulewebhook.Webhook { - return &webhook{handler: handler} -} - -func (w *webhook) GetHandler() capsulewebhook.Handler { - return w.handler -} - -func (w *webhook) GetName() string { - return "NetworkIngress" -} - -func (w *webhook) GetPath() string { - return "/validating-ingress" -} - -type handler struct { - configuration configuration.Configuration -} - -func Handler(configuration configuration.Configuration) capsulewebhook.Handler { - return &handler{configuration: configuration} -} - -func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { - ingress, err := r.ingressFromRequest(req, decoder) - if err != nil { - return admission.Errored(http.StatusBadRequest, err) - } - - return r.validateIngress(ctx, client, ingress, recorder) - } -} - -func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { - ingress, err := r.ingressFromRequest(req, decoder) - if err != nil { - return admission.Errored(http.StatusBadRequest, err) - } - - return r.validateIngress(ctx, client, ingress, recorder) - } -} - -func (r *handler) 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) ingressFromRequest(req admission.Request, decoder *admission.Decoder) (ingress Ingress, err error) { - switch req.Kind.Group { - case "networking.k8s.io": - if req.Kind.Version == "v1" { - ingressObj := &networkingv1.Ingress{} - if err = decoder.Decode(req, ingressObj); err != nil { - return - } - ingress = NetworkingV1{Ingress: ingressObj} - break - } - ingressObj := &networkingv1beta1.Ingress{} - if err = decoder.Decode(req, ingressObj); err != nil { - return - } - ingress = NetworkingV1Beta1{Ingress: ingressObj} - case "extensions": - ingressObj := &extensionsv1beta1.Ingress{} - if err = decoder.Decode(req, ingressObj); err != nil { - return - } - ingress = Extension{Ingress: ingressObj} - default: - err = fmt.Errorf("cannot recognize type %s", req.Kind.Group) - } - return -} - -func (r *handler) validateClass(tenant v1alpha1.Tenant, ingressClass *string) error { - if tenant.Spec.IngressClasses == nil { - return nil - } - - if ingressClass == nil { - return NewIngressClassNotValid(*tenant.Spec.IngressClasses) - } - - var valid, matched bool - - if len(tenant.Spec.IngressClasses.Exact) > 0 { - valid = tenant.Spec.IngressClasses.ExactMatch(*ingressClass) - } - matched = tenant.Spec.IngressClasses.RegexMatch(*ingressClass) - - if !valid && !matched { - return NewIngressClassForbidden(*ingressClass, *tenant.Spec.IngressClasses) - } - - return nil -} - -func (r *handler) validateHostnames(tenant v1alpha1.Tenant, hostnames []string) error { - if tenant.Spec.IngressHostnames == nil { - return nil - } - - var valid, matched bool - - var invalidHostnames []string - if len(hostnames) > 0 { - for _, currentHostname := range hostnames { - isPresent := v1alpha1.IngressHostnamesList(tenant.Spec.IngressHostnames.Exact).IsStringInList(currentHostname) - if !isPresent { - invalidHostnames = append(invalidHostnames, currentHostname) - } - } - if len(invalidHostnames) == 0 { - valid = true - } - } - - var notMatchingHostnames []string - allowedRegex := tenant.Spec.IngressHostnames.Regex - if len(allowedRegex) > 0 { - for _, currentHostname := range hostnames { - matched, _ = regexp.MatchString(tenant.Spec.IngressHostnames.Regex, currentHostname) - if !matched { - notMatchingHostnames = append(notMatchingHostnames, currentHostname) - } - } - if len(notMatchingHostnames) == 0 { - matched = true - } - } - - if !valid && !matched { - return NewIngressHostnamesNotValid(invalidHostnames, notMatchingHostnames, *tenant.Spec.IngressHostnames) - } - - return nil -} - -func (r *handler) validateIngress(ctx context.Context, c client.Client, ingress Ingress, recorder record.EventRecorder) admission.Response { - tenantList := &v1alpha1.TenantList{} - if err := c.List(ctx, tenantList, client.MatchingFieldsSelector{ - Selector: fields.OneTermEqualSelector(".status.namespaces", ingress.Namespace()), - }); err != nil { - return admission.Errored(http.StatusBadRequest, err) - } - - if len(tenantList.Items) == 0 { - return admission.Allowed("") - } - tenant := tenantList.Items[0] - - if err := r.validateClass(tenant, ingress.IngressClass()); err != nil { - if ic := ingress.IngressClass(); ic != nil { - recorder.Eventf(&tenant, corev1.EventTypeWarning, "ForbidenIngressClass", "Ingress %s/%s class %s is forbidden for the current Tenant", ingress.Namespace(), ingress.Name(), *ic) - } else { - recorder.Eventf(&tenant, corev1.EventTypeWarning, "MissingIngressClass", "Ingress %s/%s is missing IngressClass", ingress.Namespace(), ingress.Name()) - } - - return admission.Errored(http.StatusBadRequest, err) - } - - if err := r.validateHostnames(tenant, ingress.Hostnames()); err != nil { - recorder.Eventf(&tenant, corev1.EventTypeWarning, "ForbiddenHostname", "Ingress %s/%s hostnames %s is forbidden for the current Tenant", ingress.Namespace(), ingress.Name(), strings.Join(ingress.Hostnames(), ",")) - - return admission.Errored(http.StatusBadRequest, err) - } - - if err := r.validateCollision(ctx, c, ingress); err != nil { - recorder.Eventf(&tenant, corev1.EventTypeWarning, "HostnameCollision", "Ingress %s/%s hostnames collision for %s ", ingress.Namespace(), ingress.Name(), strings.Join(ingress.Hostnames(), ",")) - - return admission.Errored(http.StatusBadRequest, err) - } - - return admission.Allowed("") -} - -func (r *handler) validateCollision(ctx context.Context, clt client.Client, ingress Ingress) error { - if r.configuration.AllowIngressHostnameCollision() { - return nil - } - for _, hostname := range ingress.Hostnames() { - collisionErr := NewIngressHostnameCollision(hostname) - - var err error - switch ingress.(type) { - case Extension: - ingressObjList := &extensionsv1beta1.IngressList{} - if err = clt.List(ctx, ingressObjList, client.MatchingFieldsSelector{ - Selector: fields.OneTermEqualSelector(".spec.rules[*].host", hostname), - }); err != nil { - return err - } - switch len(ingressObjList.Items) { - case 0: - break - case 1: - if ingressObj := ingressObjList.Items[0]; ingressObj.GetName() == ingress.Name() && ingressObj.GetNamespace() == ingress.Namespace() { - break - } - fallthrough - default: - return collisionErr - } - case NetworkingV1: - ingressObjList := &networkingv1.IngressList{} - err = clt.List(ctx, ingressObjList, client.MatchingFieldsSelector{ - Selector: fields.OneTermEqualSelector(".spec.rules[*].host", hostname), - }) - if err != nil { - return errors.Wrap(err, "cannot list *networkingv1.IngressList by MatchingFieldsSelector") - } - switch len(ingressObjList.Items) { - case 0: - break - case 1: - if ingressObj := ingressObjList.Items[0]; ingressObj.GetName() == ingress.Name() && ingressObj.GetNamespace() == ingress.Namespace() { - break - } - fallthrough - default: - return collisionErr - } - case NetworkingV1Beta1: - ingressObjList := &networkingv1beta1.IngressList{} - err = clt.List(ctx, ingressObjList, client.MatchingFieldsSelector{ - Selector: fields.OneTermEqualSelector(".spec.rules[*].host", hostname), - }) - if err != nil { - return errors.Wrap(err, "cannot list *networkingv1beta1.IngressList by MatchingFieldsSelector") - } - switch len(ingressObjList.Items) { - case 0: - break - case 1: - if ingressObj := ingressObjList.Items[0]; ingressObj.GetName() == ingress.Name() && ingressObj.GetNamespace() == ingress.Namespace() { - break - } - fallthrough - default: - return collisionErr - } - } - } - return nil -} diff --git a/pkg/webhook/namespace/freezed.go b/pkg/webhook/namespace/freezed.go index cfbf8496..c8680409 100644 --- a/pkg/webhook/namespace/freezed.go +++ b/pkg/webhook/namespace/freezed.go @@ -5,7 +5,6 @@ package namespace import ( "context" - "net/http" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/fields" @@ -20,30 +19,6 @@ import ( "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 } @@ -53,81 +28,87 @@ func FreezeHandler(configuration configuration.Configuration) capsulewebhook.Han } 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 { + 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) + return utils.ErroredResponse(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) + return utils.ErroredResponse(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") + response := admission.Denied("the selected Tenant is freezed") + + return &response } } // creating NS that is not bounded to any Tenant - return admission.Allowed("") + return nil } } -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 { +func (r *freezedHandler) OnDelete(c client.Client, _ *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) + return utils.ErroredResponse(err) } if len(tntList.Items) == 0 { - return admission.Allowed("") + return nil } 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) + recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespace %s cannot be deleted, the current Tenant is freezed", req.Name) - return admission.Denied("the selected Tenant is freezed") + response := admission.Denied("the selected Tenant is freezed") + + return &response } - return admission.Allowed("") + return nil } } 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 { + 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) + return utils.ErroredResponse(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) + return utils.ErroredResponse(err) } if len(tntList.Items) == 0 { - return admission.Allowed("") + return nil } 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()) + recorder.Eventf(&tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespace %s cannot be updated, the current Tenant is freezed", ns.GetName()) - return admission.Denied("the selected Tenant is freezed") + response := admission.Denied("the selected Tenant is freezed") + + return &response } - return admission.Allowed("") + return nil } } diff --git a/pkg/webhook/namespace/prefix.go b/pkg/webhook/namespace/prefix.go new file mode 100644 index 00000000..76fb5791 --- /dev/null +++ b/pkg/webhook/namespace/prefix.go @@ -0,0 +1,81 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package namespace + +import ( + "context" + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + "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" + + "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" +) + +type prefixHandler struct { + configuration configuration.Configuration +} + +func PrefixHandler(configuration configuration.Configuration) capsulewebhook.Handler { + return &prefixHandler{ + configuration: configuration, + } +} + +func (r *prefixHandler) OnCreate(clt 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 utils.ErroredResponse(err) + } + + if exp, _ := r.configuration.ProtectedNamespaceRegexp(); exp != nil { + if matched := exp.MatchString(ns.GetName()); matched { + response := admission.Denied(fmt.Sprintf("Creating namespaces with name matching %s regexp is not allowed; please, reach out to the system administrators", exp.String())) + + return &response + } + } + + if r.configuration.ForceTenantPrefix() { + tnt := &v1alpha1.Tenant{} + + for _, or := range ns.ObjectMeta.OwnerReferences { + // retrieving the selected Tenant + if err := clt.Get(ctx, types.NamespacedName{Name: or.Name}, tnt); err != nil { + return utils.ErroredResponse(err) + } + + if e := fmt.Sprintf("%s-%s", tnt.GetName(), ns.GetName()); !strings.HasPrefix(ns.GetName(), fmt.Sprintf("%s-", tnt.GetName())) { + recorder.Eventf(tnt, corev1.EventTypeWarning, "InvalidTenantPrefix", "Namespace %s does not match the expected prefix for the current Tenant", ns.GetName()) + + response := admission.Denied(fmt.Sprintf("The namespace doesn't match the tenant prefix, expected %s", e)) + + return &response + } + } + } + + return nil + } +} + +func (r *prefixHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil + } +} + +func (r *prefixHandler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil + } +} diff --git a/pkg/webhook/namespace/quota.go b/pkg/webhook/namespace/quota.go index beb802a5..715ece55 100644 --- a/pkg/webhook/namespace/quota.go +++ b/pkg/webhook/namespace/quota.go @@ -5,7 +5,6 @@ package namespace import ( "context" - "net/http" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" @@ -15,32 +14,9 @@ import ( capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1" capsulewebhook "github.com/clastix/capsule/pkg/webhook" + "github.com/clastix/capsule/pkg/webhook/utils" ) -// +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 quotaWebhook struct { - handler capsulewebhook.Handler -} - -func QuotaWebhook(handler capsulewebhook.Handler) capsulewebhook.Webhook { - return "aWebhook{ - handler: handler, - } -} - -func (w *quotaWebhook) GetHandler() capsulewebhook.Handler { - return w.handler -} - -func (w *quotaWebhook) GetName() string { - return "NamespaceQuota" -} - -func (w *quotaWebhook) GetPath() string { - return "/validate-v1-namespace-quota" -} - type quotaHandler struct { } @@ -49,43 +25,40 @@ func QuotaHandler() capsulewebhook.Handler { } 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 { + 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) + return utils.ErroredResponse(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) + return utils.ErroredResponse(err) } - switch { - case tnt.IsFull(): + if tnt.IsFull() { recorder.Eventf(tnt, corev1.EventTypeWarning, "NamespaceQuotaExceded", "Namespace %s cannot be attached, quota exceeded for the current Tenant", ns.GetName()) - return admission.Denied(NewNamespaceQuotaExceededError().Error()) - case tnt.IsCordoned(): - recorder.Eventf(tnt, corev1.EventTypeWarning, "TenantFreezed", "Namespace %s cannot be attached, the current Tenant is freezed", ns.GetName()) + response := admission.Denied(NewNamespaceQuotaExceededError().Error()) - return admission.Denied("the selected Tenant is freezed") + return &response } } // creating NS that is not bounded to any Tenant - return admission.Allowed("") + return nil } } -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 *quotaHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil } } -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("") +func (r *quotaHandler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil } } diff --git a/pkg/webhook/networkpolicies/validating.go b/pkg/webhook/networkpolicy/validating.go similarity index 54% rename from pkg/webhook/networkpolicies/validating.go rename to pkg/webhook/networkpolicy/validating.go index 732b86d8..fa0705f2 100644 --- a/pkg/webhook/networkpolicies/validating.go +++ b/pkg/webhook/networkpolicy/validating.go @@ -1,11 +1,10 @@ // Copyright 2020-2021 Clastix Labs // SPDX-License-Identifier: Apache-2.0 -package networkpolicies +package networkpolicy import ( "context" - "net/http" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" @@ -16,30 +15,9 @@ import ( "github.com/clastix/capsule/api/v1alpha1" capsulewebhook "github.com/clastix/capsule/pkg/webhook" + "github.com/clastix/capsule/pkg/webhook/utils" ) -// +kubebuilder:webhook:path=/validating-v1-network-policy,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups=networking.k8s.io,resources=networkpolicies,verbs=create;update;delete,versions=v1,name=validating.network-policy.capsule.clastix.io - -type webhook struct { - handler capsulewebhook.Handler -} - -func Webhook(handler capsulewebhook.Handler) capsulewebhook.Webhook { - return &webhook{handler: handler} -} - -func (w *webhook) GetHandler() capsulewebhook.Handler { - return w.handler -} - -func (w *webhook) GetName() string { - return "NetworkPolicy" -} - -func (w *webhook) GetPath() string { - return "/validating-v1-network-policy" -} - type handler struct { } @@ -47,9 +25,9 @@ func Handler() capsulewebhook.Handler { return &handler{} } -func (r *handler) OnCreate(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) OnCreate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(context.Context, admission.Request) *admission.Response { + return nil } } @@ -75,36 +53,41 @@ func (r *handler) generic(ctx context.Context, req admission.Request, client cli return nil, nil } -// nolint:dupl +//nolint:dupl func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { + return func(ctx context.Context, req admission.Request) *admission.Response { tnt, err := r.generic(ctx, req, client, decoder) if err != nil { - return admission.Errored(http.StatusInternalServerError, err) + return utils.ErroredResponse(err) } if tnt != nil { recorder.Eventf(tnt, corev1.EventTypeWarning, "NetworkPolicyDeletion", "NetworkPolicy %s/%s cannot be deleted", req.Namespace, req.Name) - return admission.Denied("Capsule Network Policies cannot be deleted: please, reach out to the system administrators") + response := admission.Denied("Capsule Network Policies cannot be deleted: please, reach out to the system administrators") + + return &response } - return admission.Allowed("") + return nil } } -// nolint:dupl +//nolint:dupl func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { + return func(ctx context.Context, req admission.Request) *admission.Response { tnt, err := r.generic(ctx, req, client, decoder) if err != nil { - return admission.Errored(http.StatusInternalServerError, err) + return utils.ErroredResponse(err) } + if tnt != nil { recorder.Eventf(tnt, corev1.EventTypeWarning, "NetworkPolicyUpdate", "NetworkPolicy %s/%s cannot be updated", req.Namespace, req.Name) - return admission.Denied("Capsule Network Policies cannot be updated: please, reach out to the system administrators") + response := admission.Denied("Capsule Network Policies cannot be updated: please, reach out to the system administrators") + + return &response } - return admission.Allowed("") + return nil } } diff --git a/pkg/webhook/ownerreference/patching.go b/pkg/webhook/ownerreference/patching.go index d1e01773..7dd52ad6 100644 --- a/pkg/webhook/ownerreference/patching.go +++ b/pkg/webhook/ownerreference/patching.go @@ -25,28 +25,6 @@ import ( capsulewebhook "github.com/clastix/capsule/pkg/webhook" ) -// +kubebuilder:webhook:path=/mutate-v1-namespace-owner-reference,mutating=true,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=namespaces,verbs=create,versions=v1,name=owner.namespace.capsule.clastix.io - -type webhook struct { - handler capsulewebhook.Handler -} - -func Webhook(handler capsulewebhook.Handler) capsulewebhook.Webhook { - return &webhook{handler: handler} -} - -func (w *webhook) GetHandler() capsulewebhook.Handler { - return w.handler -} - -func (w *webhook) GetName() string { - return "OwnerReference" -} - -func (w *webhook) GetPath() string { - return "/mutate-v1-namespace-owner-reference" -} - type handler struct { cfg configuration.Configuration } @@ -58,30 +36,40 @@ func Handler(cfg configuration.Configuration) capsulewebhook.Handler { } func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { + 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) + response := admission.Errored(http.StatusBadRequest, err) + + return &response } ln, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{}) if err != nil { - return admission.Errored(http.StatusBadRequest, err) + response := admission.Errored(http.StatusBadRequest, err) + + return &response } // If we already had TenantName label on NS -> assign to it if label, ok := ns.ObjectMeta.Labels[ln]; ok { // retrieving the selected Tenant tnt := &capsulev1alpha1.Tenant{} - if err := clt.Get(ctx, types.NamespacedName{Name: label}, tnt); err != nil { - return admission.Errored(http.StatusBadRequest, err) + if err = clt.Get(ctx, types.NamespacedName{Name: label}, tnt); err != nil { + response := admission.Errored(http.StatusBadRequest, err) + + return &response } // Tenant owner must adhere to user that asked for NS creation if !h.isTenantOwner(tnt.Spec.Owner, req.UserInfo) { recorder.Eventf(tnt, corev1.EventTypeWarning, "NonOwnedTenant", "Namespace %s cannot be assigned to the current Tenant", ns.GetName()) - return admission.Denied("Cannot assign the desired namespace to a non-owned Tenant") + response := admission.Denied("Cannot assign the desired namespace to a non-owned Tenant") + + return &response } // Patching the response - return h.patchResponseForOwnerRef(tnt, ns, recorder) + response := h.patchResponseForOwnerRef(tnt, ns, recorder) + + return &response } // If we forceTenantPrefix -> find Tenant from NS name @@ -89,10 +77,14 @@ func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder, record // Find tenants belonging to user { - tntList, err := h.listTenantsForOwnerKind(ctx, "User", req.UserInfo.Username, clt) - if err != nil { - return admission.Errored(http.StatusBadRequest, err) + var tntList *capsulev1alpha1.TenantList + + if tntList, err = h.listTenantsForOwnerKind(ctx, "User", req.UserInfo.Username, clt); err != nil { + response := admission.Errored(http.StatusBadRequest, err) + + return &response } + for _, tnt := range tntList.Items { tenants = append(tenants, tnt) } @@ -102,8 +94,11 @@ func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder, record for _, group := range req.UserInfo.Groups { tntList, err := h.listTenantsForOwnerKind(ctx, "Group", group, clt) if err != nil { - return admission.Errored(http.StatusBadRequest, err) + response := admission.Errored(http.StatusBadRequest, err) + + return &response } + for _, tnt := range tntList.Items { tenants = append(tenants, tnt) } @@ -113,34 +108,44 @@ func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder, record sort.Sort(sort.Reverse(tenants)) if len(tenants) == 0 { - return admission.Denied("You do not have any Tenant assigned: please, reach out to the system administrators") + response := admission.Denied("You do not have any Tenant assigned: please, reach out to the system administrators") + + return &response } if len(tenants) == 1 { - return h.patchResponseForOwnerRef(&tenants[0], ns, recorder) + response := h.patchResponseForOwnerRef(&tenants[0], ns, recorder) + + return &response } if h.cfg.ForceTenantPrefix() { for _, tnt := range tenants { if strings.HasPrefix(ns.GetName(), fmt.Sprintf("%s-", tnt.GetName())) { - return h.patchResponseForOwnerRef(tnt.DeepCopy(), ns, recorder) + response := h.patchResponseForOwnerRef(tnt.DeepCopy(), ns, recorder) + + return &response } } - admission.Denied("The Namespace prefix used doesn't match any available Tenant") + response := admission.Denied("The Namespace prefix used doesn't match any available Tenant") + + return &response } - return admission.Denied("Unable to assign namespace to tenant. Please use " + ln + " label when creating a namespace") + response := admission.Denied("Unable to assign namespace to tenant. Please use " + ln + " label when creating a namespace") + + return &response } } func (h *handler) 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("") + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil } } func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { - return admission.Denied("Capsule user cannot update a Namespace") + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil } } diff --git a/pkg/webhook/pod/containerregistry.go b/pkg/webhook/pod/containerregistry.go new file mode 100644 index 00000000..17ccab72 --- /dev/null +++ b/pkg/webhook/pod/containerregistry.go @@ -0,0 +1,82 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package pod + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/fields" + "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/api/v1alpha1/domain" + capsulewebhook "github.com/clastix/capsule/pkg/webhook" + "github.com/clastix/capsule/pkg/webhook/utils" +) + +type containerRegistryHandler struct { +} + +func ContainerRegistry() capsulewebhook.Handler { + return &containerRegistryHandler{} +} + +func (h *containerRegistryHandler) OnCreate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + pod := &corev1.Pod{} + if err := decoder.Decode(req, pod); err != nil { + return utils.ErroredResponse(err) + } + + tntList := &capsulev1alpha1.TenantList{} + if err := c.List(ctx, tntList, client.MatchingFieldsSelector{ + Selector: fields.OneTermEqualSelector(".status.namespaces", pod.Namespace), + }); err != nil { + return utils.ErroredResponse(err) + } + + if len(tntList.Items) == 0 { + return nil + } + + tnt := tntList.Items[0] + + if tnt.Spec.ContainerRegistries != nil { + var valid, matched bool + + for _, container := range pod.Spec.Containers { + registry := domain.NewRegistry(container.Image) + + valid = tnt.Spec.ContainerRegistries.ExactMatch(registry.Registry()) + + matched = tnt.Spec.ContainerRegistries.RegexMatch(registry.Registry()) + + if !valid && !matched { + recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenContainerRegistry", "Pod %s/%s is using a forbidden registry %s is forbidden for the current Tenant", req.Namespace, req.Name, registry.Registry()) + + response := admission.Denied(NewContainerRegistryForbidden(container.Image, *tnt.Spec.ContainerRegistries).Error()) + + return &response + } + } + } + + return nil + } +} + +func (h *containerRegistryHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil + } +} + +func (h *containerRegistryHandler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil + } +} diff --git a/pkg/webhook/registry/errors.go b/pkg/webhook/pod/containerregistry_errors.go similarity index 97% rename from pkg/webhook/registry/errors.go rename to pkg/webhook/pod/containerregistry_errors.go index 78814ce7..0d962837 100644 --- a/pkg/webhook/registry/errors.go +++ b/pkg/webhook/pod/containerregistry_errors.go @@ -1,7 +1,8 @@ // Copyright 2020-2021 Clastix Labs // SPDX-License-Identifier: Apache-2.0 -package registry +// nolint:dupl +package pod import ( "fmt" diff --git a/pkg/webhook/pod/imagepullpolicy.go b/pkg/webhook/pod/imagepullpolicy.go new file mode 100644 index 00000000..0f54849b --- /dev/null +++ b/pkg/webhook/pod/imagepullpolicy.go @@ -0,0 +1,79 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package pod + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/clastix/capsule/api/v1alpha1" + "github.com/clastix/capsule/api/v1alpha1/domain" + capsulewebhook "github.com/clastix/capsule/pkg/webhook" + "github.com/clastix/capsule/pkg/webhook/utils" +) + +type imagePullPolicy struct{} + +func ImagePullPolicy() capsulewebhook.Handler { + return &imagePullPolicy{} +} + +func (r *imagePullPolicy) OnCreate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + var pod = &corev1.Pod{} + if err := decoder.Decode(req, pod); err != nil { + return utils.ErroredResponse(err) + } + + var tntList = &v1alpha1.TenantList{} + if err := c.List(ctx, tntList, client.MatchingFieldsSelector{ + Selector: fields.OneTermEqualSelector(".status.namespaces", pod.Namespace), + }); err != nil { + return utils.ErroredResponse(err) + } + // the Pod is not running in a Namespace managed by a Tenant + if len(tntList.Items) == 0 { + return nil + } + + tnt := tntList.Items[0] + + policy := domain.NewImagePullPolicy(&tnt) + // if Tenant doesn't enforce the pull policy, exit + if policy == nil { + return nil + } + + for _, container := range pod.Spec.Containers { + usedPullPolicy := string(container.ImagePullPolicy) + + if !policy.IsPolicySupported(usedPullPolicy) { + recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenPullPolicy", "Pod %s/%s pull policy %s is forbidden for the current Tenant", req.Namespace, req.Name, usedPullPolicy) + + response := admission.Denied(NewImagePullPolicyForbidden(usedPullPolicy, container.Name, policy.AllowedPullPolicies()).Error()) + + return &response + } + } + + return nil + } +} + +func (r *imagePullPolicy) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil + } +} + +func (r *imagePullPolicy) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil + } +} diff --git a/pkg/webhook/imagepullpolicy/errors.go b/pkg/webhook/pod/imagepullpolicy_errors.go similarity index 79% rename from pkg/webhook/imagepullpolicy/errors.go rename to pkg/webhook/pod/imagepullpolicy_errors.go index fe8b803f..3f63f224 100644 --- a/pkg/webhook/imagepullpolicy/errors.go +++ b/pkg/webhook/pod/imagepullpolicy_errors.go @@ -1,27 +1,27 @@ // Copyright 2020-2021 Clastix Labs // SPDX-License-Identifier: Apache-2.0 -package imagepullpolicy +package pod import ( "fmt" "strings" ) -type podPriorityClassForbidden struct { +type imagePullPolicyForbidden struct { usedPullPolicy string allowedPullPolicies []string containerName string } func NewImagePullPolicyForbidden(usedPullPolicy, containerName string, allowedPullPolicies []string) error { - return &podPriorityClassForbidden{ + return &imagePullPolicyForbidden{ usedPullPolicy: usedPullPolicy, containerName: containerName, allowedPullPolicies: allowedPullPolicies, } } -func (f podPriorityClassForbidden) Error() (err string) { +func (f imagePullPolicyForbidden) Error() (err string) { return fmt.Sprintf("the ImagePullPolicy %s for container %s is forbidden, use one of the followings: %s", f.usedPullPolicy, f.containerName, strings.Join(f.allowedPullPolicies, ", ")) } diff --git a/pkg/webhook/pod/priorityclass.go b/pkg/webhook/pod/priorityclass.go new file mode 100644 index 00000000..6997c590 --- /dev/null +++ b/pkg/webhook/pod/priorityclass.go @@ -0,0 +1,80 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package pod + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/fields" + "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/api/v1alpha1/domain" + capsulewebhook "github.com/clastix/capsule/pkg/webhook" + "github.com/clastix/capsule/pkg/webhook/utils" +) + +type priorityClass struct { +} + +func PriorityClass() capsulewebhook.Handler { + return &priorityClass{} +} + +func (h *priorityClass) OnCreate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + var pod = &corev1.Pod{} + if err := decoder.Decode(req, pod); err != nil { + return utils.ErroredResponse(err) + } + + var tntList = &capsulev1alpha1.TenantList{} + + if err := c.List(ctx, tntList, client.MatchingFieldsSelector{ + Selector: fields.OneTermEqualSelector(".status.namespaces", pod.Namespace), + }); err != nil { + return utils.ErroredResponse(err) + } + + if len(tntList.Items) == 0 { + return nil + } + + var allowed = domain.NewPodPriority(&tntList.Items[0]) + + var priorityClassName = pod.Spec.PriorityClassName + + switch { + case allowed == nil: + // Enforcement is not in place, skipping it at all + return nil + case len(priorityClassName) == 0: + // We don't have to force Pod to specify a Priority Class + return nil + case !allowed.ExactMatch(priorityClassName) && !allowed.RegexMatch(priorityClassName): + recorder.Eventf(&tntList.Items[0], corev1.EventTypeWarning, "ForbiddenPriorityClass", "Pod %s/%s is using Priority Class %s is forbidden for the current Tenant", pod.Namespace, pod.Name, priorityClassName) + + response := admission.Denied(NewPodPriorityClassForbidden(priorityClassName, *allowed).Error()) + + return &response + default: + return nil + } + } +} + +func (h *priorityClass) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil + } +} + +func (h *priorityClass) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil + } +} diff --git a/pkg/webhook/podpriority/errors.go b/pkg/webhook/pod/priorityclass_errors.go similarity index 97% rename from pkg/webhook/podpriority/errors.go rename to pkg/webhook/pod/priorityclass_errors.go index b9bd327b..3cc6e5a8 100644 --- a/pkg/webhook/podpriority/errors.go +++ b/pkg/webhook/pod/priorityclass_errors.go @@ -1,7 +1,8 @@ // Copyright 2020-2021 Clastix Labs // SPDX-License-Identifier: Apache-2.0 -package podpriority +// nolint:dupl +package pod import ( "fmt" diff --git a/pkg/webhook/podpriority/validating.go b/pkg/webhook/podpriority/validating.go deleted file mode 100644 index 006c5009..00000000 --- a/pkg/webhook/podpriority/validating.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2020-2021 Clastix Labs -// SPDX-License-Identifier: Apache-2.0 - -package podpriority - -import ( - "context" - "net/http" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/fields" - "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/api/v1alpha1/domain" - capsulewebhook "github.com/clastix/capsule/pkg/webhook" -) - -// +kubebuilder:webhook:path=/validating-v1-podpriority,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=ignore,groups="",resources=pods,verbs=create,versions=v1,name=podpriority.capsule.clastix.io - -type webhook struct { - handler capsulewebhook.Handler -} - -func Webhook(handler capsulewebhook.Handler) capsulewebhook.Webhook { - return &webhook{handler: handler} -} - -func (w *webhook) GetName() string { - return "podpriority" -} - -func (w *webhook) GetPath() string { - return "/validating-v1-podpriority" -} - -func (w *webhook) GetHandler() capsulewebhook.Handler { - return w.handler -} - -type handler struct { -} - -func Handler() capsulewebhook.Handler { - return &handler{} -} - -func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { - var pod = &corev1.Pod{} - - if err := decoder.Decode(req, pod); err != nil { - return admission.Errored(http.StatusBadRequest, err) - } - - var tntList = &capsulev1alpha1.TenantList{} - - if err := c.List(ctx, tntList, client.MatchingFieldsSelector{ - Selector: fields.OneTermEqualSelector(".status.namespaces", pod.Namespace), - }); err != nil { - return admission.Errored(http.StatusBadRequest, err) - } - - if len(tntList.Items) == 0 { - return admission.Allowed("") - } - - var allowed = domain.NewPodPriority(&tntList.Items[0]) - - var priorityClassName = pod.Spec.PriorityClassName - - switch { - case allowed == nil: - // Enforcement is not in place, skipping it at all - return admission.Allowed("") - case len(priorityClassName) == 0: - // We don't have to force Pod to specify a Priority Class - return admission.Allowed("") - case !allowed.ExactMatch(priorityClassName) && !allowed.RegexMatch(priorityClassName): - recorder.Eventf(&tntList.Items[0], corev1.EventTypeWarning, "ForbiddenPriorityClass", "Pod %s/%s is using Priority Class %s is forbidden for the current Tenant", pod.Namespace, pod.Name, priorityClassName) - - return admission.Errored(http.StatusBadRequest, NewPodPriorityClassForbidden(priorityClassName, *allowed)) - default: - return admission.Allowed("") - } - } -} - -func (h *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { - return admission.Allowed("") - } -} - -func (h *handler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { - return admission.Allowed("") - } -} diff --git a/pkg/webhook/pvc/validating.go b/pkg/webhook/pvc/validating.go index b4225a47..9bea3ae2 100644 --- a/pkg/webhook/pvc/validating.go +++ b/pkg/webhook/pvc/validating.go @@ -5,7 +5,6 @@ package pvc import ( "context" - "net/http" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/fields" @@ -15,30 +14,9 @@ import ( capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1" capsulewebhook "github.com/clastix/capsule/pkg/webhook" + "github.com/clastix/capsule/pkg/webhook/utils" ) -// +kubebuilder:webhook:path=/validating-v1-pvc,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=persistentvolumeclaims,verbs=create,versions=v1,name=pvc.capsule.clastix.io - -type webhook struct { - handler capsulewebhook.Handler -} - -func Webhook(handler capsulewebhook.Handler) capsulewebhook.Webhook { - return &webhook{handler: handler} -} - -func (w *webhook) GetName() string { - return "Pvc" -} - -func (w *webhook) GetPath() string { - return "/validating-v1-pvc" -} - -func (w *webhook) GetHandler() capsulewebhook.Handler { - return w.handler -} - type handler struct { } @@ -47,35 +25,37 @@ func Handler() capsulewebhook.Handler { } func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { + return func(ctx context.Context, req admission.Request) *admission.Response { var valid, matched bool - pvc := &corev1.PersistentVolumeClaim{} + pvc := &corev1.PersistentVolumeClaim{} if err := decoder.Decode(req, pvc); err != nil { - return admission.Errored(http.StatusBadRequest, err) + return utils.ErroredResponse(err) } tntList := &capsulev1alpha1.TenantList{} if err := c.List(ctx, tntList, client.MatchingFieldsSelector{ Selector: fields.OneTermEqualSelector(".status.namespaces", pvc.Namespace), }); err != nil { - return admission.Errored(http.StatusBadRequest, err) + return utils.ErroredResponse(err) } if len(tntList.Items) == 0 { - return admission.Allowed("") + return nil } tnt := tntList.Items[0] if tnt.Spec.StorageClasses == nil { - return admission.Allowed("") + return nil } if pvc.Spec.StorageClassName == nil { recorder.Eventf(&tnt, corev1.EventTypeWarning, "MissingStorageClass", "PersistentVolumeClaim %s/%s is missing StorageClass", req.Namespace, req.Name) - return admission.Errored(http.StatusBadRequest, NewStorageClassNotValid(*tntList.Items[0].Spec.StorageClasses)) + response := admission.Denied(NewStorageClassNotValid(*tntList.Items[0].Spec.StorageClasses).Error()) + + return &response } sc := *pvc.Spec.StorageClassName @@ -84,20 +64,23 @@ func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder, recorder if !valid && !matched { recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenStorageClass", "PersistentVolumeClaim %s/%s StorageClass %s is forbidden for the current Tenant", req.Namespace, req.Name, sc) - return admission.Errored(http.StatusBadRequest, NewStorageClassForbidden(*pvc.Spec.StorageClassName, *tnt.Spec.StorageClasses)) + response := admission.Denied(NewStorageClassForbidden(*pvc.Spec.StorageClassName, *tnt.Spec.StorageClasses).Error()) + + return &response } - return admission.Allowed("") + + return nil } } func (h *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { - return admission.Allowed("") + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil } } func (h *handler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { - return admission.Allowed("") + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil } } diff --git a/pkg/webhook/registry/validating.go b/pkg/webhook/registry/validating.go deleted file mode 100644 index cfecebd9..00000000 --- a/pkg/webhook/registry/validating.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2020-2021 Clastix Labs -// SPDX-License-Identifier: Apache-2.0 - -package registry - -import ( - "context" - "net/http" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/fields" - "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/api/v1alpha1/domain" - capsulewebhook "github.com/clastix/capsule/pkg/webhook" -) - -// +kubebuilder:webhook:path=/validating-v1-registry,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=ignore,groups="",resources=pods,verbs=create,versions=v1,name=pod.capsule.clastix.io - -type webhook struct { - handler capsulewebhook.Handler -} - -func Webhook(handler capsulewebhook.Handler) capsulewebhook.Webhook { - return &webhook{handler: handler} -} - -func (w *webhook) GetName() string { - return "registry" -} - -func (w *webhook) GetPath() string { - return "/validating-v1-registry" -} - -func (w *webhook) GetHandler() capsulewebhook.Handler { - return w.handler -} - -type handler struct { -} - -func Handler() capsulewebhook.Handler { - return &handler{} -} - -func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { - pod := &corev1.Pod{} - if err := decoder.Decode(req, pod); err != nil { - return admission.Errored(http.StatusBadRequest, err) - } - - tntList := &capsulev1alpha1.TenantList{} - if err := c.List(ctx, tntList, client.MatchingFieldsSelector{ - Selector: fields.OneTermEqualSelector(".status.namespaces", pod.Namespace), - }); err != nil { - return admission.Errored(http.StatusBadRequest, err) - } - if len(tntList.Items) == 0 { - return admission.Allowed("") - } - - tnt := tntList.Items[0] - - if tnt.Spec.ContainerRegistries != nil { - var valid, matched bool - for _, container := range pod.Spec.Containers { - registry := domain.NewRegistry(container.Image) - valid = tnt.Spec.ContainerRegistries.ExactMatch(registry.Registry()) - matched = tnt.Spec.ContainerRegistries.RegexMatch(registry.Registry()) - if !valid && !matched { - recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenContainerRegistry", "Pod %s/%s is using a forbidden registry %s is forbidden for the current Tenant", req.Namespace, req.Name, registry.Registry()) - - return admission.Errored(http.StatusBadRequest, NewContainerRegistryForbidden(container.Image, *tnt.Spec.ContainerRegistries)) - } - } - } - - return admission.Allowed("") - } -} - -func (h *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { - return admission.Allowed("") - } -} - -func (h *handler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { - return admission.Allowed("") - } -} diff --git a/pkg/webhook/route/cordoning.go b/pkg/webhook/route/cordoning.go new file mode 100644 index 00000000..4d6357cc --- /dev/null +++ b/pkg/webhook/route/cordoning.go @@ -0,0 +1,26 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package route + +import ( + capsulewebhook "github.com/clastix/capsule/pkg/webhook" +) + +// +kubebuilder:webhook:path=/cordoning,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="*",resources="*",verbs=create;update;delete,versions="*",name=cordoning.tenant.capsule.clastix.io + +type cordoning struct { + handlers []capsulewebhook.Handler +} + +func Cordoning(handlers ...capsulewebhook.Handler) capsulewebhook.Webhook { + return &cordoning{handlers: handlers} +} + +func (w cordoning) GetPath() string { + return "/cordoning" +} + +func (w cordoning) GetHandlers() []capsulewebhook.Handler { + return w.handlers +} diff --git a/pkg/webhook/route/ingresses.go b/pkg/webhook/route/ingresses.go new file mode 100644 index 00000000..980562fc --- /dev/null +++ b/pkg/webhook/route/ingresses.go @@ -0,0 +1,26 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package route + +import ( + capsulewebhook "github.com/clastix/capsule/pkg/webhook" +) + +// +kubebuilder:webhook:path=/ingresses,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups=networking.k8s.io;extensions,resources=ingresses,verbs=create;update,versions=v1beta1;v1,name=ingress.capsule.clastix.io + +type ingress struct { + handlers []capsulewebhook.Handler +} + +func Ingress(handler ...capsulewebhook.Handler) capsulewebhook.Webhook { + return &ingress{handlers: handler} +} + +func (w *ingress) GetHandlers() []capsulewebhook.Handler { + return w.handlers +} + +func (w *ingress) GetPath() string { + return "/ingresses" +} diff --git a/pkg/webhook/route/namespaces.go b/pkg/webhook/route/namespaces.go new file mode 100644 index 00000000..8de32da3 --- /dev/null +++ b/pkg/webhook/route/namespaces.go @@ -0,0 +1,26 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package route + +import ( + capsulewebhook "github.com/clastix/capsule/pkg/webhook" +) + +// +kubebuilder:webhook:path=/namespaces,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=namespaces,verbs=create;update;delete,versions=v1,name=namespaces.capsule.clastix.io + +type namespace struct { + handlers []capsulewebhook.Handler +} + +func Namespace(handler ...capsulewebhook.Handler) capsulewebhook.Webhook { + return &namespace{handlers: handler} +} + +func (w *namespace) GetHandlers() []capsulewebhook.Handler { + return w.handlers +} + +func (w *namespace) GetPath() string { + return "/namespaces" +} diff --git a/pkg/webhook/route/networkpolicies.go b/pkg/webhook/route/networkpolicies.go new file mode 100644 index 00000000..20f92825 --- /dev/null +++ b/pkg/webhook/route/networkpolicies.go @@ -0,0 +1,26 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package route + +import ( + capsulewebhook "github.com/clastix/capsule/pkg/webhook" +) + +// +kubebuilder:webhook:path=/networkpolicies,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="networking.k8s.io",resources=networkpolicies,verbs=update;delete,versions=v1,name=networkpolicies.capsule.clastix.io + +type networkPolicy struct { + handlers []capsulewebhook.Handler +} + +func NetworkPolicy(handler ...capsulewebhook.Handler) capsulewebhook.Webhook { + return &networkPolicy{handlers: handler} +} + +func (w *networkPolicy) GetHandlers() []capsulewebhook.Handler { + return w.handlers +} + +func (w *networkPolicy) GetPath() string { + return "/networkpolicies" +} diff --git a/pkg/webhook/route/ownerreference.go b/pkg/webhook/route/ownerreference.go new file mode 100644 index 00000000..f8413fd4 --- /dev/null +++ b/pkg/webhook/route/ownerreference.go @@ -0,0 +1,23 @@ +package route + +import ( + capsulewebhook "github.com/clastix/capsule/pkg/webhook" +) + +// +kubebuilder:webhook:path=/namespace-owner-reference,mutating=true,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=namespaces,verbs=create,versions=v1,name=owner.namespace.capsule.clastix.io + +type webhook struct { + handlers []capsulewebhook.Handler +} + +func OwnerReference(handlers ...capsulewebhook.Handler) capsulewebhook.Webhook { + return &webhook{handlers: handlers} +} + +func (w *webhook) GetHandlers() []capsulewebhook.Handler { + return w.handlers +} + +func (w *webhook) GetPath() string { + return "/namespace-owner-reference" +} diff --git a/pkg/webhook/route/pods.go b/pkg/webhook/route/pods.go new file mode 100644 index 00000000..577d54de --- /dev/null +++ b/pkg/webhook/route/pods.go @@ -0,0 +1,26 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package route + +import ( + capsulewebhook "github.com/clastix/capsule/pkg/webhook" +) + +// +kubebuilder:webhook:path=/pods,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=pods,verbs=create,versions=v1,name=pods.capsule.clastix.io + +type pod struct { + handlers []capsulewebhook.Handler +} + +func Pod(handler ...capsulewebhook.Handler) capsulewebhook.Webhook { + return &pod{handlers: handler} +} + +func (w *pod) GetHandlers() []capsulewebhook.Handler { + return w.handlers +} + +func (w *pod) GetPath() string { + return "/pods" +} diff --git a/pkg/webhook/route/pvc.go b/pkg/webhook/route/pvc.go new file mode 100644 index 00000000..1e5ebd2e --- /dev/null +++ b/pkg/webhook/route/pvc.go @@ -0,0 +1,25 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package route + +import ( + capsulewebhook "github.com/clastix/capsule/pkg/webhook" +) + +// +kubebuilder:webhook:path=/persistentvolumeclaims,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=persistentvolumeclaims,verbs=create,versions=v1,name=pvc.capsule.clastix.io + +type pvc struct { + handlers []capsulewebhook.Handler +} + +func PVC(handler ...capsulewebhook.Handler) capsulewebhook.Webhook { + return &pvc{handlers: handler} +} + +func (w *pvc) GetHandlers() []capsulewebhook.Handler { + return w.handlers +} +func (w *pvc) GetPath() string { + return "/persistentvolumeclaims" +} diff --git a/pkg/webhook/route/services.go b/pkg/webhook/route/services.go new file mode 100644 index 00000000..c4f8dac8 --- /dev/null +++ b/pkg/webhook/route/services.go @@ -0,0 +1,26 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package route + +import ( + capsulewebhook "github.com/clastix/capsule/pkg/webhook" +) + +// +kubebuilder:webhook:path=/services,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=services,verbs=create;update,versions=v1,name=services.capsule.clastix.io + +type service struct { + handlers []capsulewebhook.Handler +} + +func Service(handler ...capsulewebhook.Handler) capsulewebhook.Webhook { + return &service{handlers: handler} +} + +func (w *service) GetHandlers() []capsulewebhook.Handler { + return w.handlers +} + +func (w *service) GetPath() string { + return "/services" +} diff --git a/pkg/webhook/route/tenants.go b/pkg/webhook/route/tenants.go new file mode 100644 index 00000000..f9a7e980 --- /dev/null +++ b/pkg/webhook/route/tenants.go @@ -0,0 +1,26 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package route + +import ( + capsulewebhook "github.com/clastix/capsule/pkg/webhook" +) + +// +kubebuilder:webhook:path=/tenants,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="capsule.clastix.io",resources=tenants,verbs=create;update;delete,versions=v1alpha1,name=tenants.capsule.clastix.io + +type tenant struct { + handlers []capsulewebhook.Handler +} + +func Tenant(handler ...capsulewebhook.Handler) capsulewebhook.Webhook { + return &tenant{handlers: handler} +} + +func (w *tenant) GetHandlers() []capsulewebhook.Handler { + return w.handlers +} + +func (w *tenant) GetPath() string { + return "/tenants" +} diff --git a/pkg/webhook/router.go b/pkg/webhook/router.go index 5a74a095..6cf55d29 100644 --- a/pkg/webhook/router.go +++ b/pkg/webhook/router.go @@ -25,11 +25,12 @@ func Register(manager controllerruntime.Manager, webhookList ...Webhook) error { recorder := manager.GetEventRecorderFor("tenant-webhook") server := manager.GetWebhookServer() + for _, wh := range webhookList { server.Register(wh.GetPath(), &webhook.Admission{ Handler: &handlerRouter{ recorder: recorder, - handler: wh.GetHandler(), + handlers: wh.GetHandlers(), }, }) } @@ -37,31 +38,46 @@ func Register(manager controllerruntime.Manager, webhookList ...Webhook) error { } type handlerRouter struct { - handler Handler client client.Client decoder *admission.Decoder recorder record.EventRecorder + + handlers []Handler } func (r *handlerRouter) Handle(ctx context.Context, req admission.Request) admission.Response { switch req.Operation { case admissionv1.Create: - return r.handler.OnCreate(r.client, r.decoder, r.recorder)(ctx, req) + for _, h := range r.handlers { + if response := h.OnCreate(r.client, r.decoder, r.recorder)(ctx, req); response != nil { + return *response + } + } case admissionv1.Update: - return r.handler.OnUpdate(r.client, r.decoder, r.recorder)(ctx, req) + for _, h := range r.handlers { + if response := h.OnUpdate(r.client, r.decoder, r.recorder)(ctx, req); response != nil { + return *response + } + } case admissionv1.Delete: - return r.handler.OnDelete(r.client, r.decoder, r.recorder)(ctx, req) - default: - return admission.Allowed("") + for _, h := range r.handlers { + if response := h.OnDelete(r.client, r.decoder, r.recorder)(ctx, req); response != nil { + return *response + } + } } + + return admission.Allowed("") } func (r *handlerRouter) InjectClient(c client.Client) error { r.client = c + return nil } func (r *handlerRouter) InjectDecoder(d *admission.Decoder) error { r.decoder = d + return nil } diff --git a/pkg/webhook/services/errors.go b/pkg/webhook/service/errors.go similarity index 98% rename from pkg/webhook/services/errors.go rename to pkg/webhook/service/errors.go index 645eb502..693455d4 100644 --- a/pkg/webhook/services/errors.go +++ b/pkg/webhook/service/errors.go @@ -1,7 +1,7 @@ // Copyright 2020-2021 Clastix Labs // SPDX-License-Identifier: Apache-2.0 -package services +package service import ( "fmt" diff --git a/pkg/webhook/services/validating.go b/pkg/webhook/service/validating.go similarity index 64% rename from pkg/webhook/services/validating.go rename to pkg/webhook/service/validating.go index a388a2a1..5fc25c2d 100644 --- a/pkg/webhook/services/validating.go +++ b/pkg/webhook/service/validating.go @@ -1,12 +1,11 @@ // Copyright 2020-2021 Clastix Labs // SPDX-License-Identifier: Apache-2.0 -package services +package service import ( "context" "net" - "net/http" "strings" corev1 "k8s.io/api/core/v1" @@ -17,65 +16,48 @@ import ( "github.com/clastix/capsule/api/v1alpha1" capsulewebhook "github.com/clastix/capsule/pkg/webhook" + "github.com/clastix/capsule/pkg/webhook/utils" ) -// +kubebuilder:webhook:path=/validating-external-service-ips,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="",resources=services,verbs=create;update,versions=v1,name=validating-external-service-ips.capsule.clastix.io - const ( enableNodePortsAnnotation = "capsule.clastix.io/enable-node-ports" ) -type webhook struct { - handler capsulewebhook.Handler -} - -func Webhook(handler capsulewebhook.Handler) capsulewebhook.Webhook { - return &webhook{handler: handler} -} - -func (w *webhook) GetHandler() capsulewebhook.Handler { - return w.handler -} - -func (w *webhook) GetName() string { - return "Service" -} - -func (w *webhook) GetPath() string { - return "/validating-external-service-ips" -} - type handler struct{} func Handler() capsulewebhook.Handler { return &handler{} } -func (r *handler) handleService(ctx context.Context, clt client.Client, decoder *admission.Decoder, req admission.Request, recorder record.EventRecorder) admission.Response { +func (r *handler) handleService(ctx context.Context, clt client.Client, decoder *admission.Decoder, req admission.Request, recorder record.EventRecorder) *admission.Response { svc := &corev1.Service{} if err := decoder.Decode(req, svc); err != nil { - return admission.Errored(http.StatusBadRequest, err) + return utils.ErroredResponse(err) } tntList := &v1alpha1.TenantList{} if err := clt.List(ctx, tntList, client.MatchingFieldsSelector{ Selector: fields.OneTermEqualSelector(".status.namespaces", svc.GetNamespace()), }); err != nil { - return admission.Errored(http.StatusBadRequest, err) + return utils.ErroredResponse(err) } + if len(tntList.Items) == 0 { - return admission.Allowed("") + return nil } + tnt := tntList.Items[0] if svc.Spec.Type == corev1.ServiceTypeNodePort && tnt.GetAnnotations()[enableNodePortsAnnotation] == "false" { recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenNodePort", "Service %s/%s cannot be type of NodePort for the current Tenant", req.Namespace, req.Name) - return admission.Errored(http.StatusBadRequest, NewNodePortDisabledError()) + response := admission.Denied(NewNodePortDisabledError().Error()) + + return &response } if svc.Spec.ExternalIPs == nil || tnt.Spec.ExternalServiceIPs == nil { - return admission.Allowed("") + return nil } ipInCIDR := func(ip net.IP) bool { @@ -99,27 +81,29 @@ func (r *handler) handleService(ctx context.Context, clt client.Client, decoder if !ipInCIDR(ip) { recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenExternalServiceIP", "Service %s/%s external IP %s is forbidden for the current Tenant", req.Namespace, req.Name, ip.String()) - return admission.Errored(http.StatusBadRequest, NewExternalServiceIPForbidden(tnt.Spec.ExternalServiceIPs.Allowed)) + response := admission.Denied(NewExternalServiceIPForbidden(tnt.Spec.ExternalServiceIPs.Allowed).Error()) + + return &response } } - return admission.Allowed("") + return nil } func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { + return func(ctx context.Context, req admission.Request) *admission.Response { return r.handleService(ctx, client, decoder, req, recorder) } } func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { + return func(ctx context.Context, req admission.Request) *admission.Response { return r.handleService(ctx, client, decoder, req, recorder) } } func (r *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { - return admission.Allowed("") + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil } } diff --git a/pkg/webhook/tenant/containerregistry_regex.go b/pkg/webhook/tenant/containerregistry_regex.go new file mode 100644 index 00000000..ba424564 --- /dev/null +++ b/pkg/webhook/tenant/containerregistry_regex.go @@ -0,0 +1,68 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +//nolint:dupl +package tenant + +import ( + "context" + "regexp" + + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/clastix/capsule/api/v1alpha1" + capsulewebhook "github.com/clastix/capsule/pkg/webhook" + "github.com/clastix/capsule/pkg/webhook/utils" +) + +type containerRegistryRegexHandler struct { +} + +func ContainerRegistryRegexHandler() capsulewebhook.Handler { + return &containerRegistryRegexHandler{} +} + +func (h *containerRegistryRegexHandler) validate(decoder *admission.Decoder, req admission.Request) *admission.Response { + tenant := &v1alpha1.Tenant{} + if err := decoder.Decode(req, tenant); err != nil { + return utils.ErroredResponse(err) + } + + if tenant.Spec.ContainerRegistries != nil && len(tenant.Spec.ContainerRegistries.Regex) > 0 { + if _, err := regexp.Compile(tenant.Spec.ContainerRegistries.Regex); err != nil { + response := admission.Denied("unable to compile containerRegistries allowedRegex") + + return &response + } + } + + return nil +} + +func (h *containerRegistryRegexHandler) OnCreate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + if err := h.validate(decoder, req); err != nil { + return err + } + + return nil + } +} + +func (h *containerRegistryRegexHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(context.Context, admission.Request) *admission.Response { + return nil + } +} + +func (h *containerRegistryRegexHandler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + if response := h.validate(decoder, req); response != nil { + return response + } + + return nil + } +} diff --git a/pkg/webhook/tenant/cordoning.go b/pkg/webhook/tenant/cordoning.go index b99ca6bc..7f03a486 100644 --- a/pkg/webhook/tenant/cordoning.go +++ b/pkg/webhook/tenant/cordoning.go @@ -6,7 +6,6 @@ package tenant import ( "context" "fmt" - "net/http" "strings" corev1 "k8s.io/api/core/v1" @@ -21,28 +20,6 @@ import ( "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 - -type cordoning struct { - handler capsulewebhook.Handler -} - -func Cordoning(handler capsulewebhook.Handler) capsulewebhook.Webhook { - return &cordoning{handler: handler} -} - -func (w cordoning) GetName() string { - return "TenantCordoning" -} - -func (w cordoning) GetPath() string { - return "/tenant-cordoning" -} - -func (w cordoning) GetHandler() capsulewebhook.Handler { - return w.handler -} - type cordoningHandler struct { configuration configuration.Configuration } @@ -53,17 +30,17 @@ func CordoningHandler(configuration configuration.Configuration) capsulewebhook. } } -func (h *cordoningHandler) cordonHandler(ctx context.Context, clt client.Client, req admission.Request, recorder record.EventRecorder) admission.Response { +func (h *cordoningHandler) cordonHandler(ctx context.Context, clt client.Client, req admission.Request, recorder record.EventRecorder) *admission.Response { tntList := &capsulev1alpha1.TenantList{} if err := clt.List(ctx, tntList, client.MatchingFieldsSelector{ Selector: fields.OneTermEqualSelector(".status.namespaces", req.Namespace), }); err != nil { - return admission.Errored(http.StatusBadRequest, err) + return utils.ErroredResponse(err) } // resource is not inside a Tenant namespace if len(tntList.Items) == 0 { - return admission.Allowed("") + return nil } tnt := tntList.Items[0] @@ -72,27 +49,29 @@ func (h *cordoningHandler) cordonHandler(ctx context.Context, clt client.Client, 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())) + response := admission.Denied(fmt.Sprintf("tenant %s is freezed: please, reach out to the system administrator", tnt.GetName())) + + return &response } } - return admission.Allowed("") + return nil } -func (h *cordoningHandler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { +func (h *cordoningHandler) OnCreate(client client.Client, _ *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { return h.cordonHandler(ctx, client, req, recorder) } } -func (h *cordoningHandler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { +func (h *cordoningHandler) OnDelete(client client.Client, _ *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { return h.cordonHandler(ctx, client, req, recorder) } } -func (h *cordoningHandler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { +func (h *cordoningHandler) OnUpdate(client client.Client, _ *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { return h.cordonHandler(ctx, client, req, recorder) } } diff --git a/pkg/webhook/tenant/freezed_emitter.go b/pkg/webhook/tenant/freezed_emitter.go new file mode 100644 index 00000000..db5a00d0 --- /dev/null +++ b/pkg/webhook/tenant/freezed_emitter.go @@ -0,0 +1,59 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package tenant + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/clastix/capsule/api/v1alpha1" + capsulewebhook "github.com/clastix/capsule/pkg/webhook" + "github.com/clastix/capsule/pkg/webhook/utils" +) + +type freezedEmitterHandler struct { +} + +func FreezedEmitter() capsulewebhook.Handler { + return &freezedEmitterHandler{} +} + +func (h *freezedEmitterHandler) OnCreate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil + } +} + +func (h *freezedEmitterHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(context.Context, admission.Request) *admission.Response { + return nil + } +} + +func (h *freezedEmitterHandler) OnUpdate(_ client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + oldTnt := &v1alpha1.Tenant{} + if err := decoder.DecodeRaw(req.OldObject, oldTnt); err != nil { + return utils.ErroredResponse(err) + } + + newTnt := &v1alpha1.Tenant{} + if err := decoder.Decode(req, newTnt); err != nil { + return utils.ErroredResponse(err) + } + + switch { + case !oldTnt.IsCordoned() && newTnt.IsCordoned(): + recorder.Eventf(newTnt, corev1.EventTypeNormal, "TenantCordoned", "Tenant has been cordoned") + case oldTnt.IsCordoned() && !newTnt.IsCordoned(): + recorder.Eventf(newTnt, corev1.EventTypeNormal, "TenantUncordoned", "Tenant has been uncordoned") + } + + return nil + } +} diff --git a/pkg/webhook/tenant/hostname_regex.go b/pkg/webhook/tenant/hostname_regex.go new file mode 100644 index 00000000..d35d264a --- /dev/null +++ b/pkg/webhook/tenant/hostname_regex.go @@ -0,0 +1,68 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +//nolint:dupl +package tenant + +import ( + "context" + "regexp" + + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/clastix/capsule/api/v1alpha1" + capsulewebhook "github.com/clastix/capsule/pkg/webhook" + "github.com/clastix/capsule/pkg/webhook/utils" +) + +type hostnameRegexHandler struct { +} + +func HostnameRegexHandler() capsulewebhook.Handler { + return &hostnameRegexHandler{} +} + +func (h *hostnameRegexHandler) validate(decoder *admission.Decoder, req admission.Request) *admission.Response { + tenant := &v1alpha1.Tenant{} + if err := decoder.Decode(req, tenant); err != nil { + return utils.ErroredResponse(err) + } + + if tenant.Spec.IngressHostnames != nil && len(tenant.Spec.IngressHostnames.Regex) > 0 { + if _, err := regexp.Compile(tenant.Spec.IngressHostnames.Regex); err != nil { + response := admission.Denied("unable to compile allowedHostnames allowedRegex") + + return &response + } + } + + return nil +} + +func (h *hostnameRegexHandler) OnCreate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + if response := h.validate(decoder, req); response != nil { + return response + } + + return nil + } +} + +func (h *hostnameRegexHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(context.Context, admission.Request) *admission.Response { + return nil + } +} + +func (h *hostnameRegexHandler) OnUpdate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + if err := h.validate(decoder, req); err != nil { + return err + } + + return nil + } +} diff --git a/pkg/webhook/tenant/hostnames_collision.go b/pkg/webhook/tenant/hostnames_collision.go new file mode 100644 index 00000000..2734c3ba --- /dev/null +++ b/pkg/webhook/tenant/hostnames_collision.go @@ -0,0 +1,86 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package tenant + +import ( + "context" + "fmt" + "net/http" + + "k8s.io/apimachinery/pkg/fields" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "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" +) + +type hostnamesCollisionHandler struct { + configuration configuration.Configuration +} + +func HostnamesCollisionHandler(configuration configuration.Configuration) capsulewebhook.Handler { + return &hostnamesCollisionHandler{configuration: configuration} +} + +func (h *hostnamesCollisionHandler) validateTenant(ctx context.Context, req admission.Request, clt client.Client, decoder *admission.Decoder) *admission.Response { + tenant := &v1alpha1.Tenant{} + if err := decoder.Decode(req, tenant); err != nil { + return utils.ErroredResponse(err) + } + + if !h.configuration.AllowTenantIngressHostnamesCollision() && tenant.Spec.IngressHostnames != nil && len(tenant.Spec.IngressHostnames.Exact) > 0 { + for _, h := range tenant.Spec.IngressHostnames.Exact { + tntList := &v1alpha1.TenantList{} + if err := clt.List(ctx, tntList, client.MatchingFieldsSelector{ + Selector: fields.OneTermEqualSelector(".spec.ingressHostnames", h), + }); err != nil { + response := admission.Errored(http.StatusInternalServerError, fmt.Errorf("cannot retrieve Tenant list using .spec.ingressHostnames field selector: %w", err)) + + return &response + } + switch { + case len(tntList.Items) == 1 && tntList.Items[0].GetName() == tenant.GetName(): + continue + case len(tntList.Items) > 0: + response := admission.Denied(fmt.Sprintf("the allowed hostname %s is already used by the Tenant %s, cannot proceed", h, tntList.Items[0].GetName())) + + return &response + default: + continue + } + } + } + + return nil +} + +func (h *hostnamesCollisionHandler) OnCreate(client client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + if response := h.validateTenant(ctx, req, client, decoder); response != nil { + return response + } + + return nil + } +} + +func (h *hostnamesCollisionHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(context.Context, admission.Request) *admission.Response { + return nil + } +} + +func (h *hostnamesCollisionHandler) OnUpdate(client client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + if response := h.validateTenant(ctx, req, client, decoder); response != nil { + return response + } + + return nil + } +} diff --git a/pkg/webhook/tenant/ingressclass_regex.go b/pkg/webhook/tenant/ingressclass_regex.go new file mode 100644 index 00000000..23ca04a8 --- /dev/null +++ b/pkg/webhook/tenant/ingressclass_regex.go @@ -0,0 +1,68 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +//nolint:dupl +package tenant + +import ( + "context" + "regexp" + + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/clastix/capsule/api/v1alpha1" + capsulewebhook "github.com/clastix/capsule/pkg/webhook" + "github.com/clastix/capsule/pkg/webhook/utils" +) + +type ingressClassRegexHandler struct { +} + +func IngressClassRegexHandler() capsulewebhook.Handler { + return &ingressClassRegexHandler{} +} + +func (h *ingressClassRegexHandler) validate(decoder *admission.Decoder, req admission.Request) *admission.Response { + tenant := &v1alpha1.Tenant{} + if err := decoder.Decode(req, tenant); err != nil { + return utils.ErroredResponse(err) + } + + if tenant.Spec.IngressClasses != nil && len(tenant.Spec.IngressClasses.Regex) > 0 { + if _, err := regexp.Compile(tenant.Spec.IngressClasses.Regex); err != nil { + response := admission.Denied("unable to compile ingressClasses allowedRegex") + + return &response + } + } + + return nil +} + +func (h *ingressClassRegexHandler) OnCreate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + if response := h.validate(decoder, req); response != nil { + return response + } + + return nil + } +} + +func (h *ingressClassRegexHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(context.Context, admission.Request) *admission.Response { + return nil + } +} + +func (h *ingressClassRegexHandler) OnUpdate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + if err := h.validate(decoder, req); err != nil { + return err + } + + return nil + } +} diff --git a/pkg/webhook/tenant/name.go b/pkg/webhook/tenant/name.go new file mode 100644 index 00000000..9ca768d6 --- /dev/null +++ b/pkg/webhook/tenant/name.go @@ -0,0 +1,54 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package tenant + +import ( + "context" + "regexp" + + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/clastix/capsule/api/v1alpha1" + capsulewebhook "github.com/clastix/capsule/pkg/webhook" + "github.com/clastix/capsule/pkg/webhook/utils" +) + +type nameHandler struct { +} + +func NameHandler() capsulewebhook.Handler { + return &nameHandler{} +} + +func (h *nameHandler) OnCreate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + tenant := &v1alpha1.Tenant{} + if err := decoder.Decode(req, tenant); err != nil { + return utils.ErroredResponse(err) + } + + matched, _ := regexp.MatchString(`[a-z0-9]([-a-z0-9]*[a-z0-9])?`, tenant.GetName()) + if !matched { + response := admission.Denied("tenant name has forbidden characters") + + return &response + } + + return nil + } +} + +func (h *nameHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(context.Context, admission.Request) *admission.Response { + return nil + } +} + +func (h *nameHandler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + return nil + } +} diff --git a/pkg/webhook/tenant/storageclass_regex.go b/pkg/webhook/tenant/storageclass_regex.go new file mode 100644 index 00000000..874c1dc6 --- /dev/null +++ b/pkg/webhook/tenant/storageclass_regex.go @@ -0,0 +1,68 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +//nolint:dupl +package tenant + +import ( + "context" + "regexp" + + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/clastix/capsule/api/v1alpha1" + capsulewebhook "github.com/clastix/capsule/pkg/webhook" + "github.com/clastix/capsule/pkg/webhook/utils" +) + +type storageClassRegexHandler struct { +} + +func StorageClassRegexHandler() capsulewebhook.Handler { + return &storageClassRegexHandler{} +} + +func (h *storageClassRegexHandler) validate(decoder *admission.Decoder, req admission.Request) *admission.Response { + tenant := &v1alpha1.Tenant{} + if err := decoder.Decode(req, tenant); err != nil { + return utils.ErroredResponse(err) + } + + if tenant.Spec.StorageClasses != nil && len(tenant.Spec.StorageClasses.Regex) > 0 { + if _, err := regexp.Compile(tenant.Spec.StorageClasses.Regex); err != nil { + response := admission.Denied("unable to compile storageClasses allowedRegex") + + return &response + } + } + + return nil +} + +func (h *storageClassRegexHandler) OnCreate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + if err := h.validate(decoder, req); err != nil { + return err + } + + return nil + } +} + +func (h *storageClassRegexHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(context.Context, admission.Request) *admission.Response { + return nil + } +} + +func (h *storageClassRegexHandler) OnUpdate(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + if err := h.validate(decoder, req); err != nil { + return err + } + + return nil + } +} diff --git a/pkg/webhook/tenant/validating.go b/pkg/webhook/tenant/validating.go deleted file mode 100644 index 7e5d4f9f..00000000 --- a/pkg/webhook/tenant/validating.go +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright 2020-2021 Clastix Labs -// SPDX-License-Identifier: Apache-2.0 - -package tenant - -import ( - "context" - "fmt" - "regexp" - - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/fields" - "k8s.io/client-go/tools/record" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - - "github.com/clastix/capsule/api/v1alpha1" - "github.com/clastix/capsule/pkg/configuration" - capsulewebhook "github.com/clastix/capsule/pkg/webhook" -) - -// +kubebuilder:webhook:path=/validating-v1-tenant,mutating=false,sideEffects=None,admissionReviewVersions=v1,failurePolicy=fail,groups="capsule.clastix.io",resources=tenants,verbs=create;update,versions=v1alpha1,name=tenant.capsule.clastix.io - -type validating struct { - handler capsulewebhook.Handler -} - -func Validating(handler capsulewebhook.Handler) capsulewebhook.Webhook { - return &validating{handler: handler} -} - -func (w validating) GetName() string { - return "Tenant" -} - -func (w validating) GetPath() string { - return "/validating-v1-tenant" -} - -func (w validating) GetHandler() capsulewebhook.Handler { - return w.handler -} - -type validatingHandler struct { - configuration configuration.Configuration -} - -func ValidatingHandler(configuration configuration.Configuration) capsulewebhook.Handler { - return &validatingHandler{configuration: configuration} -} - -// Validate Tenant name -func (h *validatingHandler) validateTenantName(tenant *v1alpha1.Tenant) error { - matched, _ := regexp.MatchString(`[a-z0-9]([-a-z0-9]*[a-z0-9])?`, tenant.GetName()) - if !matched { - return fmt.Errorf("tenant name has forbidden characters") - } - return nil -} - -// Validate ingressClasses regexp -func (h *validatingHandler) validateIngressClassesRegex(tenant *v1alpha1.Tenant) error { - if tenant.Spec.IngressClasses != nil && len(tenant.Spec.IngressClasses.Regex) > 0 { - if _, err := regexp.Compile(tenant.Spec.IngressClasses.Regex); err != nil { - return fmt.Errorf("unable to compile ingressClasses allowedRegex") - } - } - return nil -} - -// Validate storageClasses regexp -func (h *validatingHandler) validateStorageClassesRegex(tenant *v1alpha1.Tenant) error { - if tenant.Spec.StorageClasses != nil && len(tenant.Spec.StorageClasses.Regex) > 0 { - if _, err := regexp.Compile(tenant.Spec.StorageClasses.Regex); err != nil { - return fmt.Errorf("unable to compile storageClasses allowedRegex") - } - } - return nil -} - -// Validate containerRegistries regexp -func (h *validatingHandler) validateContainerRegistriesRegex(tenant *v1alpha1.Tenant) error { - if tenant.Spec.ContainerRegistries != nil && len(tenant.Spec.ContainerRegistries.Regex) > 0 { - if _, err := regexp.Compile(tenant.Spec.ContainerRegistries.Regex); err != nil { - return fmt.Errorf("unable to compile containerRegistries allowedRegex") - } - } - return nil -} - -// Validate containerRegistries regexp -func (h *validatingHandler) validateIngressHostnamesRegex(tenant *v1alpha1.Tenant) error { - if tenant.Spec.IngressHostnames != nil && len(tenant.Spec.IngressHostnames.Regex) > 0 { - if _, err := regexp.Compile(tenant.Spec.IngressHostnames.Regex); err != nil { - return fmt.Errorf("unable to compile ingressHostnames allowedRegex") - } - } - return nil -} - -// Check Ingress hostnames collision across all available Tenants -func (h *validatingHandler) validateIngressHostnamesCollision(context context.Context, clt client.Client, tenant *v1alpha1.Tenant) error { - if !h.configuration.AllowTenantIngressHostnamesCollision() && tenant.Spec.IngressHostnames != nil && len(tenant.Spec.IngressHostnames.Exact) > 0 { - for _, h := range tenant.Spec.IngressHostnames.Exact { - tntList := &v1alpha1.TenantList{} - if err := clt.List(context, tntList, client.MatchingFieldsSelector{ - Selector: fields.OneTermEqualSelector(".spec.ingressHostnames", h), - }); err != nil { - return fmt.Errorf("cannot retrieve Tenant list using .spec.ingressHostnames field selector: %w", err) - } - switch { - case len(tntList.Items) == 1 && tntList.Items[0].GetName() == tenant.GetName(): - continue - case len(tntList.Items) > 0: - return fmt.Errorf("the allowed hostname %s is already used by the Tenant %s, cannot proceed", h, tntList.Items[0].GetName()) - default: - continue - } - } - } - return nil -} - -func (h *validatingHandler) validateTenant(ctx context.Context, req admission.Request, client client.Client, decoder *admission.Decoder) error { - tenant := &v1alpha1.Tenant{} - if err := decoder.Decode(req, tenant); err != nil { - return err - } - if err := h.validateTenantByRegex(tenant); err != nil { - return err - } - if err := h.validateIngressHostnamesCollision(ctx, client, tenant); err != nil { - return err - } - return nil -} - -func (h *validatingHandler) validateTenantByRegex(tenant *v1alpha1.Tenant) error { - if err := h.validateTenantName(tenant); err != nil { - return err - } - if err := h.validateIngressClassesRegex(tenant); err != nil { - return err - } - if err := h.validateStorageClassesRegex(tenant); err != nil { - return err - } - if err := h.validateContainerRegistriesRegex(tenant); err != nil { - return err - } - if err := h.validateIngressHostnamesRegex(tenant); err != nil { - return err - } - - return nil -} - -func (h *validatingHandler) OnCreate(client client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { - if err := h.validateTenant(ctx, req, client, decoder); err != nil { - return admission.Denied(err.Error()) - } - return admission.Allowed("") - } -} - -func (h *validatingHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { - return func(context.Context, admission.Request) admission.Response { - return admission.Allowed("") - } -} - -func (h *validatingHandler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { - if err := h.validateTenant(ctx, req, client, decoder); err != nil { - return admission.Denied(err.Error()) - } - - oldTnt := &v1alpha1.Tenant{} - if err := decoder.DecodeRaw(req.OldObject, oldTnt); err != nil { - return admission.Denied(err.Error()) - } - - newTnt := &v1alpha1.Tenant{} - if err := decoder.Decode(req, newTnt); err != nil { - return admission.Denied(err.Error()) - } - - switch { - case !oldTnt.IsCordoned() && newTnt.IsCordoned(): - recorder.Eventf(newTnt, v1.EventTypeNormal, "TenantCordoned", "Tenant has been cordoned") - case oldTnt.IsCordoned() && !newTnt.IsCordoned(): - recorder.Eventf(newTnt, v1.EventTypeNormal, "TenantUncordoned", "Tenant has been uncordoned") - } - - return admission.Allowed("") - } -} diff --git a/pkg/webhook/tenantprefix/validating.go b/pkg/webhook/tenantprefix/validating.go deleted file mode 100644 index 2a5be32f..00000000 --- a/pkg/webhook/tenantprefix/validating.go +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2020-2021 Clastix Labs -// SPDX-License-Identifier: Apache-2.0 - -package tenantprefix - -import ( - "context" - "fmt" - "net/http" - "strings" - - corev1 "k8s.io/api/core/v1" - "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" - - "github.com/clastix/capsule/api/v1alpha1" - "github.com/clastix/capsule/pkg/configuration" - capsulewebhook "github.com/clastix/capsule/pkg/webhook" -) - -// +kubebuilder:webhook:path=/validating-v1-namespace-tenant-prefix,sideEffects=None,admissionReviewVersions=v1,mutating=false,failurePolicy=fail,groups="",resources=namespaces,verbs=create,versions=v1,name=prefix.namespace.capsule.clastix.io - -type webhook struct { - handler capsulewebhook.Handler -} - -func Webhook(handler capsulewebhook.Handler) capsulewebhook.Webhook { - return &webhook{ - handler: handler, - } -} - -func (w *webhook) GetHandler() capsulewebhook.Handler { - return w.handler -} - -func (w *webhook) GetName() string { - return "OwnerReference" -} - -func (w *webhook) GetPath() string { - return "/validating-v1-namespace-tenant-prefix" -} - -type handler struct { - configuration configuration.Configuration -} - -func Handler(configuration configuration.Configuration) capsulewebhook.Handler { - return &handler{ - configuration: configuration, - } -} - -func (r *handler) OnCreate(clt 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) - } - if exp, _ := r.configuration.ProtectedNamespaceRegexp(); exp != nil { - if matched := exp.MatchString(ns.GetName()); matched { - return admission.Denied("Creating namespaces with name matching " + exp.String() + " regexp is not allowed; please, reach out to the system administrators") - } - } - - if !r.configuration.ForceTenantPrefix() { - return admission.Allowed("") - } - - tnt := &v1alpha1.Tenant{} - for _, or := range ns.ObjectMeta.OwnerReferences { - // retrieving the selected Tenant - if err := clt.Get(ctx, types.NamespacedName{Name: or.Name}, tnt); err != nil { - return admission.Errored(http.StatusBadRequest, err) - } - if e := fmt.Sprintf("%s-%s", tnt.GetName(), ns.GetName()); !strings.HasPrefix(ns.GetName(), fmt.Sprintf("%s-", tnt.GetName())) { - recorder.Eventf(tnt, corev1.EventTypeWarning, "InvalidTenantPrefix", "Namespace %s does not match the expected prefix for the current Tenant", ns.GetName()) - - return admission.Denied("The namespace doesn't match the tenant prefix, expected " + e) - } - } - return admission.Allowed("") - } -} - -func (r *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { - return admission.Allowed("") - } -} - -func (r *handler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { - return admission.Allowed("") - } -} diff --git a/pkg/webhook/utils/error.go b/pkg/webhook/utils/error.go new file mode 100644 index 00000000..b4b95f17 --- /dev/null +++ b/pkg/webhook/utils/error.go @@ -0,0 +1,16 @@ +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package utils + +import ( + "net/http" + + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +func ErroredResponse(err error) *admission.Response { + response := admission.Errored(http.StatusInternalServerError, err) + + return &response +} diff --git a/pkg/webhook/utils/in_capsule_groups.go b/pkg/webhook/utils/in_capsule_groups.go index 3554bab6..1c7dade5 100644 --- a/pkg/webhook/utils/in_capsule_groups.go +++ b/pkg/webhook/utils/in_capsule_groups.go @@ -15,16 +15,16 @@ import ( "github.com/clastix/capsule/pkg/webhook" ) -func InCapsuleGroups(configuration configuration.Configuration, webhookHandler webhook.Handler) webhook.Handler { +func InCapsuleGroups(configuration configuration.Configuration, handlers ...webhook.Handler) webhook.Handler { return &handler{ configuration: configuration, - handler: webhookHandler, + handlers: handlers, } } type handler struct { configuration configuration.Configuration - handler webhook.Handler + handlers []webhook.Handler } // If the user performing action is not a Capsule user, can be skipped @@ -45,29 +45,49 @@ func (h handler) isCapsuleUser(req admission.Request) bool { } func (h *handler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) webhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { + return func(ctx context.Context, req admission.Request) *admission.Response { if !h.isCapsuleUser(req) { - return admission.Allowed("") + return nil } - return h.handler.OnCreate(client, decoder, recorder)(ctx, req) + for _, hndl := range h.handlers { + if response := hndl.OnCreate(client, decoder, recorder)(ctx, req); response != nil { + return response + } + } + + return nil } } func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) webhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { + return func(ctx context.Context, req admission.Request) *admission.Response { if !h.isCapsuleUser(req) { - return admission.Allowed("") + return nil } - return h.handler.OnDelete(client, decoder, recorder)(ctx, req) + + for _, hndl := range h.handlers { + if response := hndl.OnDelete(client, decoder, recorder)(ctx, req); response != nil { + return response + } + } + + return nil } } func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) webhook.Func { - return func(ctx context.Context, req admission.Request) admission.Response { + return func(ctx context.Context, req admission.Request) *admission.Response { if !h.isCapsuleUser(req) { - return admission.Allowed("") + return nil } - return h.handler.OnUpdate(client, decoder, recorder)(ctx, req) + + for _, hndl := range h.handlers { + if response := hndl.OnUpdate(client, decoder, recorder)(ctx, req); response != nil { + return response + } + } + + return nil } } diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go index 3c198e22..10bb17bb 100644 --- a/pkg/webhook/webhook.go +++ b/pkg/webhook/webhook.go @@ -4,7 +4,6 @@ package webhook type Webhook interface { - GetName() string GetPath() string - GetHandler() Handler + GetHandlers() []Handler }