Files
capsule/internal/webhook/namespace/validation/handler.go
T
Oliver BählerandGitHub 2252c530f4 feat(performance): removed duplicate client calls from all admission paths (#2054)
* chore

* perfromance improvements

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* perfromance improvements

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat(performance): removed duplicate client calls from all admission paths

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat(performance): removed duplicate client calls from all admission paths

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat(performance): removed duplicate client calls from all admission paths

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat(performance): removed duplicate client calls from all admission paths

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat(performance): removed duplicate client calls from all admission paths

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat(performance): removed duplicate client calls from all admission paths

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat(performance): removed duplicate client calls from all admission paths

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

---------

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
2026-07-28 14:32:42 +02:00

322 lines
8.3 KiB
Go

// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package validation
import (
"context"
"fmt"
"reflect"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
webhookutils "github.com/projectcapsule/capsule/internal/webhook/utils"
ad "github.com/projectcapsule/capsule/pkg/runtime/admission"
"github.com/projectcapsule/capsule/pkg/runtime/configuration"
"github.com/projectcapsule/capsule/pkg/runtime/events"
"github.com/projectcapsule/capsule/pkg/runtime/handlers"
"github.com/projectcapsule/capsule/pkg/tenant"
"github.com/projectcapsule/capsule/pkg/users"
)
func NamespaceHandler(configuration configuration.Configuration, hndlers ...handlers.TypedHandlerWithTenantUser[*corev1.Namespace]) handlers.Handler {
return &handler{
cfg: configuration,
handlers: hndlers,
}
}
type handler struct {
cfg configuration.Configuration
handlers []handlers.TypedHandlerWithTenantUser[*corev1.Namespace]
}
func (h *handler) OnCreate(
c client.Client,
reader client.Reader,
decoder admission.Decoder,
recorder events.EventRecorder,
) handlers.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
reader = webhookutils.NewTenantCachingReader(reader)
user := handlers.ResolveAdmissionUser(ctx, c, req, h.cfg)
ns := &corev1.Namespace{}
if err := decoder.Decode(req, ns); err != nil {
return ad.ErroredResponse(err)
}
if !user.IsAdmin() && !user.IsCapsule() && !tenant.HasTenantReference(ns) {
return nil
}
tnt, err := tenant.ResolveNamespaceTenant(ctx, reader, ns)
if err != nil {
return ad.ErroredResponse(err)
}
if !user.IsAdmin() && !user.IsCapsule() && tnt != nil {
return ad.Deny("only tenant owners can create tenant-owned namespaces")
}
if tnt == nil {
return nil
}
if terminating := h.rejectOnTermination(ns, tnt); terminating != nil {
return terminating
}
for _, hndl := range h.handlers {
if response := hndl.OnCreate(c, reader, user, ns, decoder, recorder, tnt)(ctx, req); response != nil {
return response
}
}
return nil
}
}
func (h *handler) OnDelete(
c client.Client,
reader client.Reader,
decoder admission.Decoder,
recorder events.EventRecorder,
) handlers.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
oldNs := &corev1.Namespace{}
if err := decoder.DecodeRaw(req.OldObject, oldNs); err != nil {
return ad.ErroredResponse(err)
}
// A namespace which is already terminating must be allowed to complete
// deletion even if its Tenant metadata became inconsistent meanwhile.
// Kubernetes authorization has already guarded the original delete.
if oldNs.DeletionTimestamp != nil || oldNs.Status.Phase == corev1.NamespaceTerminating {
return nil
}
reader = webhookutils.NewTenantCachingReader(reader)
tnt, err := tenant.ResolveNamespaceTenant(ctx, reader, oldNs)
if apierrors.IsNotFound(err) {
// Kubernetes authorization already controls namespace deletion.
// A stale Tenant reference must not make a namespace undeletable.
return nil
}
user := handlers.ResolveAdmissionUser(ctx, c, req, h.cfg)
if err != nil && !user.IsAdmin() {
return ad.ErroredResponse(err)
}
if tnt == nil {
return nil
}
for _, hndl := range h.handlers {
if response := hndl.OnDelete(c, reader, user, oldNs, decoder, recorder, tnt)(ctx, req); response != nil {
return response
}
}
return nil
}
}
func (h *handler) OnUpdate(
c client.Client,
reader client.Reader,
decoder admission.Decoder,
recorder events.EventRecorder,
) handlers.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
ns := &corev1.Namespace{}
if err := decoder.Decode(req, ns); err != nil {
return ad.ErroredResponse(err)
}
oldNs := &corev1.Namespace{}
if err := decoder.DecodeRaw(req.OldObject, oldNs); err != nil {
return ad.ErroredResponse(err)
}
if response, stop := validateTerminatingNamespaceUpdate(req, oldNs, ns); stop {
return response
}
reader = webhookutils.NewTenantCachingReader(reader)
user := handlers.ResolveAdmissionUser(ctx, c, req, h.cfg)
if response, stop := validateNamespaceTenantReferenceTransition(user, oldNs, ns); stop {
return response
}
oldTenant, err := tenant.ResolveNamespaceTenant(ctx, reader, oldNs)
if err != nil && !user.IsAdmin() {
return ad.ErroredResponse(err)
}
newTenant, err := tenant.ResolveNamespaceTenant(ctx, reader, ns)
if err != nil {
return ad.ErroredResponse(err)
}
if !user.IsAdmin() {
if oldTenant == nil || newTenant == nil {
return ad.Deny("namespace tenant ownership is incomplete")
}
if oldTenant.GetName() != newTenant.GetName() || oldTenant.GetUID() != newTenant.GetUID() {
return ad.Deny("namespace can not be migrated between tenants")
}
if user.IsCapsule() && !tenant.NamespaceIsOwned(ctx, c, h.cfg, oldNs, oldTenant, user) {
recorder.LabeledEvent(
ns,
corev1.EventTypeWarning,
events.ReasonNamespaceHijack,
events.ActionValidationDenied,
"namespace can not be patched",
).
WithRequestAnnotations(req).
Emit(ctx)
return ad.Deny("denied patch request for this namespace")
}
}
adminTenantTransition := user.IsAdmin() && namespaceTenantChanged(oldTenant, newTenant)
tnt := newTenant
if !user.IsAdmin() {
tnt = oldTenant
}
if tnt == nil {
return nil
}
if terminating := h.rejectOnTermination(ns, newTenant); terminating != nil {
return terminating
}
if adminTenantTransition {
return nil
}
for _, hndl := range h.handlers {
if response := hndl.OnUpdate(c, reader, user, ns, oldNs, decoder, recorder, tnt)(ctx, req); response != nil {
return response
}
}
return nil
}
}
func namespaceTenantChanged(oldTenant, newTenant *capsulev1beta2.Tenant) bool {
if oldTenant == nil || newTenant == nil {
return oldTenant != newTenant
}
return oldTenant.GetName() != newTenant.GetName() || oldTenant.GetUID() != newTenant.GetUID()
}
func isTerminatingNamespaceUpdate(
req admission.Request,
oldNs, newNs *corev1.Namespace,
) bool {
return req.SubResource == "finalize" ||
newNs.DeletionTimestamp != nil ||
oldNs.DeletionTimestamp != nil ||
newNs.Status.Phase == corev1.NamespaceTerminating ||
oldNs.Status.Phase == corev1.NamespaceTerminating
}
func validateTerminatingNamespaceUpdate(
req admission.Request,
oldNs, newNs *corev1.Namespace,
) (*admission.Response, bool) {
if !isTerminatingNamespaceUpdate(req, oldNs, newNs) {
return nil, false
}
if namespaceTenantAssignmentChanged(oldNs, newNs) {
return ad.Deny("namespace tenant ownership can not change during termination"), true
}
return nil, true
}
func validateNamespaceTenantReferenceTransition(
user users.AdmissionUser,
oldNs, newNs *corev1.Namespace,
) (*admission.Response, bool) {
if user.IsAdmin() {
if !tenant.HasConsistentTenantReference(newNs) {
return ad.Deny("tenant label and ownerReference must both be set consistently or both be absent"), true
}
return nil, false
}
oldHasTenantReference := tenant.HasTenantReference(oldNs)
newHasTenantReference := tenant.HasTenantReference(newNs)
switch {
case !oldHasTenantReference && newHasTenantReference:
return ad.Deny("namespace can not be patched into a tenant"), true
case oldHasTenantReference && !newHasTenantReference:
return ad.Deny("namespace can not remove tenant ownership"), true
case !oldHasTenantReference && !newHasTenantReference:
return nil, true
default:
return nil, false
}
}
func namespaceTenantAssignmentChanged(oldNs, newNs *corev1.Namespace) bool {
if tenant.TenanLabelValue(oldNs) != tenant.TenanLabelValue(newNs) {
return true
}
return !reflect.DeepEqual(
tenant.TenantOwnerReferences(oldNs),
tenant.TenantOwnerReferences(newNs),
)
}
func (h *handler) rejectOnTermination(
ns *corev1.Namespace,
t *capsulev1beta2.Tenant,
) *admission.Response {
if t == nil {
return nil
}
if t.DeletionTimestamp == nil {
return nil
}
instance := t.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{
Name: ns.GetName(),
UID: ns.GetUID(),
})
if instance != nil {
return nil
}
err := fmt.Errorf("tenant is terminating and does not accept new namespaces")
return ad.ErroredResponse(err)
}