// Copyright 2020-2026 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 package mutation import ( "context" "maps" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" "github.com/projectcapsule/capsule/internal/webhook/utils" "github.com/projectcapsule/capsule/pkg/api/meta" 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" ) type metadataHandler struct { cfg configuration.Configuration } func MetadataHandler(cfg configuration.Configuration) handlers.TypedHandlerWithUser[*corev1.Namespace] { return &metadataHandler{ cfg: cfg, } } func (h *metadataHandler) OnCreate( c client.Client, reader client.Reader, user users.AdmissionUser, ns *corev1.Namespace, decoder admission.Decoder, recorder events.EventRecorder, ) handlers.Func { return func(ctx context.Context, req admission.Request) *admission.Response { tnt, errResponse := utils.GetNamespaceTenant(ctx, reader, c, ns, user, h.cfg, recorder) if errResponse != nil { return errResponse } if tnt == nil { return nil } labels, annotations, err := tenant.BuildNamespaceMetadataForTenant(ns, tnt) if err != nil { return ad.ErroredResponse(err) } managedMetadataOnly := tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.ManagedMetadataOnly if !managedMetadataOnly { labels = mergeStringMap(ns.GetLabels(), labels) annotations = mergeStringMap(ns.GetAnnotations(), annotations) } tenant.AddNamespaceNameLabels(labels, ns) tenant.AddTenantNameLabel(labels, tnt) ns.SetLabels(labels) ns.SetAnnotations(annotations) if response := h.handleCordoning(tnt, ns); response != nil { return response } return nil } } func (h *metadataHandler) OnDelete( client.Client, client.Reader, users.AdmissionUser, *corev1.Namespace, admission.Decoder, events.EventRecorder, ) handlers.Func { return func(context.Context, admission.Request) *admission.Response { return nil } } func (h *metadataHandler) OnUpdate( c client.Client, reader client.Reader, user users.AdmissionUser, newNs *corev1.Namespace, oldNs *corev1.Namespace, decoder admission.Decoder, recorder events.EventRecorder, ) handlers.Func { return func(ctx context.Context, req admission.Request) *admission.Response { tnt, errResponse := h.resolveTenantForUpdate(ctx, reader, c, oldNs, newNs, user, recorder) if errResponse != nil { return errResponse } if tnt == nil { return nil } labels, annotations, err := tenant.BuildNamespaceMetadataForTenant(newNs, tnt) if err != nil { return ad.ErroredResponse(err) } managedMetadataOnly := tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.ManagedMetadataOnly if !managedMetadataOnly { labels = mergeStringMap(newNs.GetLabels(), labels) annotations = mergeStringMap(newNs.GetAnnotations(), annotations) } tenant.AddNamespaceNameLabels(labels, newNs) tenant.AddTenantNameLabel(labels, tnt) newNs.SetLabels(labels) newNs.SetAnnotations(annotations) return nil } } func mergeStringMap(dst, src map[string]string) map[string]string { out := maps.Clone(dst) if out == nil { out = map[string]string{} } maps.Copy(out, src) return out } func (h *metadataHandler) handleCordoning( tnt *capsulev1beta2.Tenant, ns *corev1.Namespace, ) *admission.Response { condition := tnt.Status.Conditions.GetConditionByType(meta.CordonedCondition) if condition == nil { return nil } if condition.Status != metav1.ConditionTrue { return nil } labels := ns.GetLabels() if labels == nil { labels = map[string]string{} } if _, ok := labels[meta.CordonedLabel]; ok { return nil } labels[meta.CordonedLabel] = meta.ValueTrue ns.SetLabels(labels) return nil } func (h *metadataHandler) resolveTenantForUpdate( ctx context.Context, reader client.Reader, cache client.Client, oldNs *corev1.Namespace, newNs *corev1.Namespace, user users.AdmissionUser, recorder events.EventRecorder, ) (*capsulev1beta2.Tenant, *admission.Response) { if user.IsAdmin() { tnt, err := tenant.GetTenantByLabels(ctx, reader, newNs) if err != nil { return nil, ad.ErroredResponse(err) } return tnt, nil } tnt, errResponse := utils.GetNamespaceTenant(ctx, reader, cache, oldNs, user, h.cfg, recorder) if errResponse != nil { return nil, errResponse } return tnt, nil }