From accd9ca038fe8bb4c13feef39e16f162677fcf2e Mon Sep 17 00:00:00 2001 From: Dario Tranchitella Date: Sun, 13 Jun 2021 20:46:40 +0200 Subject: [PATCH] feat: emitting events for policies violations --- controllers/tenant_controller.go | 12 +++--- main.go | 8 ++-- pkg/webhook/handler.go | 7 ++-- pkg/webhook/imagepullpolicy/validating.go | 9 +++-- pkg/webhook/ingress/validating.go | 25 ++++++++++--- pkg/webhook/namespacequota/validating.go | 15 +++----- pkg/webhook/networkpolicies/validating.go | 45 +++++++++++++++-------- pkg/webhook/ownerreference/patching.go | 28 +++++++------- pkg/webhook/podpriority/validating.go | 13 ++++--- pkg/webhook/pvc/validating.go | 15 +++++--- pkg/webhook/registry/validating.go | 13 ++++--- pkg/webhook/router.go | 19 ++++++---- pkg/webhook/services/validating.go | 18 ++++++--- pkg/webhook/tenant/validating.go | 7 ++-- pkg/webhook/tenantprefix/validating.go | 12 +++--- pkg/webhook/utils/in_capsule_groups.go | 13 ++++--- 16 files changed, 153 insertions(+), 106 deletions(-) diff --git a/controllers/tenant_controller.go b/controllers/tenant_controller.go index 1ad3f9e9..7ca7dcd4 100644 --- a/controllers/tenant_controller.go +++ b/controllers/tenant_controller.go @@ -261,7 +261,7 @@ func (r *TenantReconciler) syncAdditionalRoleBindings(tenant *capsulev1alpha1.Te return controllerutil.SetControllerReference(tenant, rb, r.Scheme) }) - r.emitEvent(tenant, rb.GetNamespace(), res, fmt.Sprintf("ensuring additional RoleBinding %s", rb.GetName()), err) + r.emitEvent(tenant, rb.GetNamespace(), res, fmt.Sprintf("Ensuring additional RoleBinding %s", rb.GetName()), err) if err != nil { r.Log.Error(err, "Cannot sync Additional RoleBinding") @@ -394,7 +394,7 @@ func (r *TenantReconciler) syncResourceQuotas(tenant *capsulev1alpha1.Tenant) er return controllerutil.SetControllerReference(tenant, target, r.Scheme) }) - r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("ensuring ResourceQuota %s", target.GetName()), err) + r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring ResourceQuota %s", target.GetName()), err) r.Log.Info("Resource Quota sync result: "+string(res), "name", target.Name, "namespace", target.Namespace) if err != nil { @@ -444,7 +444,7 @@ func (r *TenantReconciler) syncLimitRanges(tenant *capsulev1alpha1.Tenant) error return controllerutil.SetControllerReference(tenant, t, r.Scheme) }) - r.emitEvent(tenant, t.GetNamespace(), res, fmt.Sprintf("ensuring LimitRange %s", t.GetName()), err) + r.emitEvent(tenant, t.GetNamespace(), res, fmt.Sprintf("Ensuring LimitRange %s", t.GetName()), err) r.Log.Info("LimitRange sync result: "+string(res), "name", t.Name, "namespace", t.Namespace) if err != nil { @@ -523,7 +523,7 @@ func (r *TenantReconciler) syncNamespaceMetadata(namespace string, tnt *capsulev return }) - r.emitEvent(tnt, namespace, res, "ensuring Namespace metadata", err) + r.emitEvent(tnt, namespace, res, "Ensuring Namespace metadata", err) return } @@ -585,7 +585,7 @@ func (r *TenantReconciler) syncNetworkPolicies(tenant *capsulev1alpha1.Tenant) e return controllerutil.SetControllerReference(tenant, t, r.Scheme) }) - r.emitEvent(tenant, t.GetNamespace(), res, fmt.Sprintf("ensuring NetworkPolicy %s", t.GetName()), err) + r.emitEvent(tenant, t.GetNamespace(), res, fmt.Sprintf("Ensuring NetworkPolicy %s", t.GetName()), err) r.Log.Info("Network Policy sync result: "+string(res), "name", t.Name, "namespace", t.Namespace) if err != nil { @@ -646,7 +646,7 @@ func (r *TenantReconciler) ownerRoleBinding(tenant *capsulev1alpha1.Tenant) erro return controllerutil.SetControllerReference(tenant, target, r.Scheme) }) - r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("ensuring Capsule RoleBinding %s", target.GetName()), err) + r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring Capsule RoleBinding %s", target.GetName()), err) r.Log.Info("Role Binding sync result: "+string(res), "name", target.Name, "namespace", target.Namespace) if err != nil { diff --git a/main.go b/main.go index f963859b..c5bfd847 100644 --- a/main.go +++ b/main.go @@ -139,8 +139,6 @@ func main() { cfg := configuration.NewCapsuleConfiguration(manager.GetClient(), configurationName) - webhookRecorder := manager.GetEventRecorderFor("tenant-webhook") - // webhooks: the order matters, don't change it and just append webhooksList := append( make([]webhook.Webhook, 0), @@ -149,10 +147,10 @@ func main() { registry.Webhook(registry.Handler()), podpriority.Webhook(podpriority.Handler()), services.Webhook(services.Handler()), - ownerreference.Webhook(utils.InCapsuleGroups(cfg, ownerreference.Handler(cfg, webhookRecorder))), - namespacequota.Webhook(utils.InCapsuleGroups(cfg, namespacequota.Handler(webhookRecorder))), + ownerreference.Webhook(utils.InCapsuleGroups(cfg, ownerreference.Handler(cfg))), + namespacequota.Webhook(utils.InCapsuleGroups(cfg, namespacequota.Handler())), networkpolicies.Webhook(utils.InCapsuleGroups(cfg, networkpolicies.Handler())), - tenantprefix.Webhook(utils.InCapsuleGroups(cfg, tenantprefix.Handler(cfg, webhookRecorder))), + tenantprefix.Webhook(utils.InCapsuleGroups(cfg, tenantprefix.Handler(cfg))), tenant.Webhook(tenant.Handler(cfg)), imagepullpolicy.Webhook(imagepullpolicy.Handler()), ) diff --git a/pkg/webhook/handler.go b/pkg/webhook/handler.go index 368445e5..d0666eb2 100644 --- a/pkg/webhook/handler.go +++ b/pkg/webhook/handler.go @@ -6,6 +6,7 @@ package webhook import ( "context" + "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) @@ -13,7 +14,7 @@ import ( type Func func(ctx context.Context, req admission.Request) admission.Response type Handler interface { - OnCreate(client client.Client, decoder *admission.Decoder) Func - OnDelete(client client.Client, decoder *admission.Decoder) Func - OnUpdate(client client.Client, decoder *admission.Decoder) Func + OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) Func + OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) Func + OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) Func } diff --git a/pkg/webhook/imagepullpolicy/validating.go b/pkg/webhook/imagepullpolicy/validating.go index 5bf7db7b..ef0a77de 100644 --- a/pkg/webhook/imagepullpolicy/validating.go +++ b/pkg/webhook/imagepullpolicy/validating.go @@ -9,6 +9,7 @@ import ( 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" @@ -45,7 +46,7 @@ func Handler() capsulewebhook.Handler { return &handler{} } -func (r *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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{} @@ -77,6 +78,8 @@ func (r *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulew usedPullPolicy := string(container.ImagePullPolicy) if !policy.IsPolicySupported(usedPullPolicy) { + recorder.Eventf(&tnt, corev1.EventTypeWarning, "PullPolicy", "Pod %s/%s pull policy %s is not allowed", req.Namespace, req.Name, usedPullPolicy) + return admission.Denied(NewImagePullPolicyForbidden(usedPullPolicy, container.Name, policy.AllowedPullPolicies()).Error()) } } @@ -85,13 +88,13 @@ func (r *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulew } } -func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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.Client, decoder *admission.Decoder) capsulewebhook.Func { +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/validating.go b/pkg/webhook/ingress/validating.go index b1705c85..eb3f7897 100644 --- a/pkg/webhook/ingress/validating.go +++ b/pkg/webhook/ingress/validating.go @@ -8,12 +8,15 @@ import ( "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" @@ -53,29 +56,29 @@ func Handler(configuration configuration.Configuration) capsulewebhook.Handler { return &handler{configuration: configuration} } -func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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) + return r.validateIngress(ctx, client, ingress, recorder) } } -func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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) + return r.validateIngress(ctx, client, ingress, recorder) } } -func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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("") } @@ -173,7 +176,7 @@ func (r *handler) validateHostnames(tenant v1alpha1.Tenant, hostnames []string) return nil } -func (r *handler) validateIngress(ctx context.Context, c client.Client, ingress Ingress) admission.Response { +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()), @@ -187,14 +190,24 @@ func (r *handler) validateIngress(ctx context.Context, c client.Client, ingress tenant := tenantList.Items[0] if err := r.validateClass(tenant, ingress.IngressClass()); err != nil { + if ic := ingress.IngressClass(); ic != nil { + recorder.Eventf(&tenant, corev1.EventTypeWarning, "InvalidIngressClass", "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 required class for the current Tenant", ingress.Namespace(), ingress.Name()) + } + return admission.Errored(http.StatusBadRequest, err) } if err := r.validateHostnames(tenant, ingress.Hostnames()); err != nil { + recorder.Eventf(&tenant, corev1.EventTypeWarning, "InvalidHostname", "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) } diff --git a/pkg/webhook/namespacequota/validating.go b/pkg/webhook/namespacequota/validating.go index b90b20c7..f9d2ffd2 100644 --- a/pkg/webhook/namespacequota/validating.go +++ b/pkg/webhook/namespacequota/validating.go @@ -42,16 +42,13 @@ func (w *webhook) GetPath() string { } type handler struct { - recorder record.EventRecorder } -func Handler(recorder record.EventRecorder) capsulewebhook.Handler { - return &handler{ - recorder: recorder, - } +func Handler() capsulewebhook.Handler { + return &handler{} } -func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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 { ns := &corev1.Namespace{} if err := decoder.Decode(req, ns); err != nil { @@ -65,7 +62,7 @@ func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder) cap return admission.Errored(http.StatusBadRequest, err) } if tnt.IsFull() { - r.recorder.Eventf(tnt, corev1.EventTypeWarning, "Error", "the Namespace quota has been exceeded, Namespace %s cannot been attached", ns.GetName()) + recorder.Eventf(tnt, corev1.EventTypeWarning, "NamespaceQuota", "Namespace %s cannot be attached, quota exceeded", ns.GetName()) return admission.Denied(NewNamespaceQuotaExceededError().Error()) } @@ -75,13 +72,13 @@ func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder) cap } } -func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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 admission.Allowed("") } diff --git a/pkg/webhook/networkpolicies/validating.go b/pkg/webhook/networkpolicies/validating.go index a59f9150..732b86d8 100644 --- a/pkg/webhook/networkpolicies/validating.go +++ b/pkg/webhook/networkpolicies/validating.go @@ -7,8 +7,10 @@ import ( "context" "net/http" + corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/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" @@ -45,30 +47,44 @@ func Handler() capsulewebhook.Handler { return &handler{} } -func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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) generic(ctx context.Context, req admission.Request, client client.Client, _ *admission.Decoder) (bool, error) { +func (r *handler) generic(ctx context.Context, req admission.Request, client client.Client, _ *admission.Decoder) (*v1alpha1.Tenant, error) { var err error np := &networkingv1.NetworkPolicy{} err = client.Get(ctx, types.NamespacedName{Namespace: req.AdmissionRequest.Namespace, Name: req.AdmissionRequest.Name}, np) if err != nil { - return false, err + return nil, err } - return r.isCapsuleNetworkPolicy(np), nil + tnt := &v1alpha1.Tenant{} + + l, _ := v1alpha1.GetTypeLabel(&v1alpha1.Tenant{}) + if v, ok := np.GetLabels()[l]; ok { + if err = client.Get(ctx, types.NamespacedName{Name: v}, tnt); err != nil { + return nil, err + } + + return tnt, nil + } + + return nil, nil } -func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +// 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 { - ok, err := r.generic(ctx, req, client, decoder) + tnt, err := r.generic(ctx, req, client, decoder) if err != nil { return admission.Errored(http.StatusInternalServerError, err) } - if ok { + 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") } @@ -76,19 +92,16 @@ func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder) cap } } -func (r *handler) isCapsuleNetworkPolicy(np *networkingv1.NetworkPolicy) (ok bool) { - l, _ := v1alpha1.GetTypeLabel(&v1alpha1.Tenant{}) - _, ok = np.GetLabels()[l] - return -} - -func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +// 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 { - ok, err := r.generic(ctx, req, client, decoder) + tnt, err := r.generic(ctx, req, client, decoder) if err != nil { return admission.Errored(http.StatusInternalServerError, err) } - if ok { + 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") } diff --git a/pkg/webhook/ownerreference/patching.go b/pkg/webhook/ownerreference/patching.go index e083f539..d1e01773 100644 --- a/pkg/webhook/ownerreference/patching.go +++ b/pkg/webhook/ownerreference/patching.go @@ -48,18 +48,16 @@ func (w *webhook) GetPath() string { } type handler struct { - cfg configuration.Configuration - recorder record.EventRecorder + cfg configuration.Configuration } -func Handler(cfg configuration.Configuration, recorder record.EventRecorder) capsulewebhook.Handler { +func Handler(cfg configuration.Configuration) capsulewebhook.Handler { return &handler{ - cfg: cfg, - recorder: recorder, + cfg: cfg, } } -func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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 { ns := &corev1.Namespace{} if err := decoder.Decode(req, ns); err != nil { @@ -78,10 +76,12 @@ func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsul } // 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") } // Patching the response - return h.patchResponseForOwnerRef(tnt, ns) + return h.patchResponseForOwnerRef(tnt, ns, recorder) } // If we forceTenantPrefix -> find Tenant from NS name @@ -117,13 +117,13 @@ func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsul } if len(tenants) == 1 { - return h.patchResponseForOwnerRef(&tenants[0], ns) + return h.patchResponseForOwnerRef(&tenants[0], ns, recorder) } if h.cfg.ForceTenantPrefix() { for _, tnt := range tenants { if strings.HasPrefix(ns.GetName(), fmt.Sprintf("%s-", tnt.GetName())) { - return h.patchResponseForOwnerRef(tnt.DeepCopy(), ns) + return h.patchResponseForOwnerRef(tnt.DeepCopy(), ns, recorder) } } admission.Denied("The Namespace prefix used doesn't match any available Tenant") @@ -132,31 +132,31 @@ func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsul return admission.Denied("Unable to assign namespace to tenant. Please use " + ln + " label when creating a namespace") } } -func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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("") } } -func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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") } } -func (h *handler) patchResponseForOwnerRef(tenant *capsulev1alpha1.Tenant, ns *corev1.Namespace) admission.Response { +func (h *handler) patchResponseForOwnerRef(tenant *capsulev1alpha1.Tenant, ns *corev1.Namespace, recorder record.EventRecorder) admission.Response { scheme := runtime.NewScheme() _ = capsulev1alpha1.AddToScheme(scheme) _ = corev1.AddToScheme(scheme) o, _ := json.Marshal(ns.DeepCopy()) if err := controllerutil.SetControllerReference(tenant, ns, scheme); err != nil { - h.recorder.Eventf(tenant, corev1.EventTypeWarning, "Error", "namespace %s cannot be assigned to the desired Tenant", ns.GetName()) + recorder.Eventf(tenant, corev1.EventTypeWarning, "Error", "Namespace %s cannot be assigned to the desired Tenant", ns.GetName()) return admission.Errored(http.StatusInternalServerError, err) } - h.recorder.Eventf(tenant, corev1.EventTypeNormal, "NamespaceCreationWebhook", "namespace %s has been assigned to the desired Tenant", ns.GetName()) + recorder.Eventf(tenant, corev1.EventTypeNormal, "NamespaceCreationWebhook", "Namespace %s has been assigned to the desired Tenant", ns.GetName()) c, _ := json.Marshal(ns) return admission.PatchResponseFromRaw(o, c) diff --git a/pkg/webhook/podpriority/validating.go b/pkg/webhook/podpriority/validating.go index 9155145c..a3e92768 100644 --- a/pkg/webhook/podpriority/validating.go +++ b/pkg/webhook/podpriority/validating.go @@ -7,8 +7,9 @@ import ( "context" "net/http" - v1 "k8s.io/api/core/v1" + 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" @@ -46,9 +47,9 @@ func Handler() capsulewebhook.Handler { return &handler{} } -func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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 = &v1.Pod{} + var pod = &corev1.Pod{} if err := decoder.Decode(req, pod); err != nil { return admission.Errored(http.StatusBadRequest, err) @@ -78,6 +79,8 @@ func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulew // 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, "PriorityClass", "Pod %s/%s is using Priority Class %s not allowed for the current Tenant", pod.Namespace, pod.Name, priorityClassName) + return admission.Errored(http.StatusBadRequest, NewPodPriorityClassForbidden(priorityClassName, *allowed)) default: return admission.Allowed("") @@ -85,13 +88,13 @@ func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulew } } -func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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.Client, decoder *admission.Decoder) capsulewebhook.Func { +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 3483eb5d..a744ae11 100644 --- a/pkg/webhook/pvc/validating.go +++ b/pkg/webhook/pvc/validating.go @@ -7,8 +7,9 @@ import ( "context" "net/http" - v1 "k8s.io/api/core/v1" + 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" @@ -45,10 +46,10 @@ func Handler() capsulewebhook.Handler { return &handler{} } -func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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 valid, matched bool - pvc := &v1.PersistentVolumeClaim{} + pvc := &corev1.PersistentVolumeClaim{} if err := decoder.Decode(req, pvc); err != nil { return admission.Errored(http.StatusBadRequest, err) @@ -72,6 +73,8 @@ func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulew } if pvc.Spec.StorageClassName == nil { + recorder.Eventf(&tnt, corev1.EventTypeWarning, "StorageClassInvalid", "PersistentVolumeClaim %s/%s is missing StorageClass", req.Namespace, req.Name) + return admission.Errored(http.StatusBadRequest, NewStorageClassNotValid(*tntList.Items[0].Spec.StorageClasses)) } @@ -79,19 +82,21 @@ func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulew valid = tnt.Spec.StorageClasses.ExactMatch(sc) matched = tnt.Spec.StorageClasses.RegexMatch(sc) if !valid && !matched { + recorder.Eventf(&tnt, corev1.EventTypeWarning, "StorageClassForbidden", "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)) } return admission.Allowed("") } } -func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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.Client, decoder *admission.Decoder) capsulewebhook.Func { +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/registry/validating.go b/pkg/webhook/registry/validating.go index 509be300..31cfa94a 100644 --- a/pkg/webhook/registry/validating.go +++ b/pkg/webhook/registry/validating.go @@ -7,8 +7,9 @@ import ( "context" "net/http" - v1 "k8s.io/api/core/v1" + 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" @@ -46,9 +47,9 @@ func Handler() capsulewebhook.Handler { return &handler{} } -func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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 := &v1.Pod{} + pod := &corev1.Pod{} if err := decoder.Decode(req, pod); err != nil { return admission.Errored(http.StatusBadRequest, err) } @@ -72,6 +73,8 @@ func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulew valid = tnt.Spec.ContainerRegistries.ExactMatch(registry.Registry()) matched = tnt.Spec.ContainerRegistries.RegexMatch(registry.Registry()) if !valid && !matched { + recorder.Eventf(&tnt, corev1.EventTypeWarning, "ContainerRegistry", "Pod %s/%s is using a forbidden registry %s", req.Namespace, req.Name, registry.Registry()) + return admission.Errored(http.StatusBadRequest, NewContainerRegistryForbidden(container.Image, *tnt.Spec.ContainerRegistries)) } } @@ -81,13 +84,13 @@ func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulew } } -func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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.Client, decoder *admission.Decoder) capsulewebhook.Func { +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/router.go b/pkg/webhook/router.go index e84a15a2..5a74a095 100644 --- a/pkg/webhook/router.go +++ b/pkg/webhook/router.go @@ -8,6 +8,7 @@ import ( "io/ioutil" admissionv1 "k8s.io/api/admission/v1" + "k8s.io/client-go/tools/record" controllerruntime "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook" @@ -21,11 +22,14 @@ func Register(manager controllerruntime.Manager, webhookList ...Webhook) error { return nil } + recorder := manager.GetEventRecorderFor("tenant-webhook") + server := manager.GetWebhookServer() for _, wh := range webhookList { server.Register(wh.GetPath(), &webhook.Admission{ Handler: &handlerRouter{ - handler: wh.GetHandler(), + recorder: recorder, + handler: wh.GetHandler(), }, }) } @@ -33,19 +37,20 @@ func Register(manager controllerruntime.Manager, webhookList ...Webhook) error { } type handlerRouter struct { - handler Handler - client client.Client - decoder *admission.Decoder + handler Handler + client client.Client + decoder *admission.Decoder + recorder record.EventRecorder } 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)(ctx, req) + return r.handler.OnCreate(r.client, r.decoder, r.recorder)(ctx, req) case admissionv1.Update: - return r.handler.OnUpdate(r.client, r.decoder)(ctx, req) + return r.handler.OnUpdate(r.client, r.decoder, r.recorder)(ctx, req) case admissionv1.Delete: - return r.handler.OnDelete(r.client, r.decoder)(ctx, req) + return r.handler.OnDelete(r.client, r.decoder, r.recorder)(ctx, req) default: return admission.Allowed("") } diff --git a/pkg/webhook/services/validating.go b/pkg/webhook/services/validating.go index 3a34b93b..5463169a 100644 --- a/pkg/webhook/services/validating.go +++ b/pkg/webhook/services/validating.go @@ -7,9 +7,11 @@ import ( "context" "net" "net/http" + "strings" 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" @@ -49,7 +51,7 @@ func Handler() capsulewebhook.Handler { return &handler{} } -func (r *handler) handleService(ctx context.Context, clt client.Client, decoder *admission.Decoder, req admission.Request) 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) @@ -67,6 +69,8 @@ func (r *handler) handleService(ctx context.Context, clt client.Client, decoder tnt := tntList.Items[0] if svc.Spec.Type == corev1.ServiceTypeNodePort && tnt.GetAnnotations()[enableNodePortsAnnotation] == "false" { + recorder.Eventf(&tnt, corev1.EventTypeWarning, "NodePort", "Service %s/%s cannot be type of NodePort", req.Namespace, req.Name) + return admission.Errored(http.StatusBadRequest, NewNodePortDisabledError()) } @@ -84,22 +88,24 @@ func (r *handler) handleService(ctx context.Context, clt client.Client, decoder } } + recorder.Eventf(&tnt, corev1.EventTypeWarning, "NodePort", "Service %s/%s external IPs %s are not in the expected range for the current Tenant", req.Namespace, req.Name, strings.Join(svc.Spec.ExternalIPs, ",")) + return admission.Errored(http.StatusBadRequest, NewExternalServiceIPForbidden(tnt.Spec.ExternalServiceIPs.Allowed)) } -func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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 r.handleService(ctx, client, decoder, req) + return r.handleService(ctx, client, decoder, req, recorder) } } -func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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 r.handleService(ctx, client, decoder, req) + return r.handleService(ctx, client, decoder, req, recorder) } } -func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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/tenant/validating.go b/pkg/webhook/tenant/validating.go index 12cdab3d..d5815cee 100644 --- a/pkg/webhook/tenant/validating.go +++ b/pkg/webhook/tenant/validating.go @@ -9,6 +9,7 @@ import ( "regexp" "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" @@ -153,7 +154,7 @@ func (h *handler) validateTenantByRegex(tenant *v1alpha1.Tenant) error { return nil } -func (h *handler) OnCreate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +func (h *handler) 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()) @@ -162,13 +163,13 @@ func (h *handler) OnCreate(client client.Client, decoder *admission.Decoder) cap } } -func (h *handler) OnDelete(client.Client, *admission.Decoder) capsulewebhook.Func { +func (h *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { return func(context.Context, admission.Request) admission.Response { return admission.Allowed("") } } -func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +func (h *handler) OnUpdate(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()) diff --git a/pkg/webhook/tenantprefix/validating.go b/pkg/webhook/tenantprefix/validating.go index 3dd81916..2bd3ccf0 100644 --- a/pkg/webhook/tenantprefix/validating.go +++ b/pkg/webhook/tenantprefix/validating.go @@ -46,17 +46,15 @@ func (w *webhook) GetPath() string { type handler struct { configuration configuration.Configuration - recorder record.EventRecorder } -func Handler(configuration configuration.Configuration, recorder record.EventRecorder) capsulewebhook.Handler { +func Handler(configuration configuration.Configuration) capsulewebhook.Handler { return &handler{ configuration: configuration, - recorder: recorder, } } -func (r *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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 { @@ -79,7 +77,7 @@ func (r *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsul 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())) { - r.recorder.Eventf(tnt, corev1.EventTypeWarning, "TenantPrefix", "Namespace %s does not match the expected prefix", ns.GetName()) + recorder.Eventf(tnt, corev1.EventTypeWarning, "TenantPrefix", "Namespace %s does not match the expected Tenant prefix", ns.GetName()) return admission.Denied("The namespace doesn't match the tenant prefix, expected " + e) } @@ -88,13 +86,13 @@ func (r *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsul } } -func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { +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.Client, decoder *admission.Decoder) capsulewebhook.Func { +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/in_capsule_groups.go b/pkg/webhook/utils/in_capsule_groups.go index 9734447f..3554bab6 100644 --- a/pkg/webhook/utils/in_capsule_groups.go +++ b/pkg/webhook/utils/in_capsule_groups.go @@ -6,6 +6,7 @@ package utils import ( "context" + "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" @@ -43,30 +44,30 @@ func (h handler) isCapsuleUser(req admission.Request) bool { return false } -func (h *handler) OnCreate(client client.Client, decoder *admission.Decoder) webhook.Func { +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 { if !h.isCapsuleUser(req) { return admission.Allowed("") } - return h.handler.OnCreate(client, decoder)(ctx, req) + return h.handler.OnCreate(client, decoder, recorder)(ctx, req) } } -func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder) webhook.Func { +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 { if !h.isCapsuleUser(req) { return admission.Allowed("") } - return h.handler.OnDelete(client, decoder)(ctx, req) + return h.handler.OnDelete(client, decoder, recorder)(ctx, req) } } -func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder) webhook.Func { +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 { if !h.isCapsuleUser(req) { return admission.Allowed("") } - return h.handler.OnUpdate(client, decoder)(ctx, req) + return h.handler.OnUpdate(client, decoder, recorder)(ctx, req) } }