// Copyright 2020-2026 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 package validation import ( "context" "regexp" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" ad "github.com/projectcapsule/capsule/pkg/runtime/admission" "github.com/projectcapsule/capsule/pkg/runtime/events" "github.com/projectcapsule/capsule/pkg/runtime/handlers" "github.com/projectcapsule/capsule/pkg/users" ) type requiredMetadataHandler struct{} func RequiredMetadataHandler() handlers.TypedHandlerWithTenantUser[*corev1.Namespace] { return &requiredMetadataHandler{} } func (h *requiredMetadataHandler) OnCreate( _ client.Client, _ client.Reader, _ users.AdmissionUser, ns *corev1.Namespace, _ admission.Decoder, _ events.EventRecorder, tnt *capsulev1beta2.Tenant, ) handlers.Func { return func(_ context.Context, _ admission.Request) *admission.Response { no := tnt.Spec.NamespaceOptions if no == nil || no.RequiredMetadata == nil { return nil } rm := no.RequiredMetadata if resp := validateRequiredMapCreate( "label", rm.Labels, ns.GetLabels(), ); resp != nil { return resp } if resp := validateRequiredMapCreate( "annotation", rm.Annotations, ns.GetAnnotations(), ); resp != nil { return resp } return nil } } func (h *requiredMetadataHandler) OnUpdate( _ client.Client, _ client.Reader, _ users.AdmissionUser, newNs *corev1.Namespace, oldNs *corev1.Namespace, _ admission.Decoder, _ events.EventRecorder, tnt *capsulev1beta2.Tenant, ) handlers.Func { return func(_ context.Context, _ admission.Request) *admission.Response { no := tnt.Spec.NamespaceOptions if no == nil || no.RequiredMetadata == nil { return nil } rm := no.RequiredMetadata if resp := validateRequiredMapUpdate( "label", rm.Labels, newNs.GetLabels(), oldNs.GetLabels(), ); resp != nil { return resp } if resp := validateRequiredMapUpdate( "annotation", rm.Annotations, newNs.GetAnnotations(), oldNs.GetAnnotations(), ); resp != nil { return resp } return nil } } func (h *requiredMetadataHandler) OnDelete( client.Client, client.Reader, users.AdmissionUser, *corev1.Namespace, admission.Decoder, events.EventRecorder, *capsulev1beta2.Tenant, ) handlers.Func { return func(context.Context, admission.Request) *admission.Response { return nil } } func validateRequiredMapCreate(kind string, required map[string]string, actual map[string]string) *admission.Response { for key, exp := range required { val, ok := actual[key] if !ok { return ad.Denyf("required %s %q not present", kind, key) } re, reErr := regexp.Compile(exp) if reErr != nil { return ad.Denyf("invalid required %s regex for %q: %q: %v", kind, key, exp, reErr) } if !re.MatchString(val) { return ad.Denyf("required %s %q value %q does not match regex %q", kind, key, val, exp) } } return nil } func validateRequiredMapUpdate(kind string, required map[string]string, newMap, oldMap map[string]string) *admission.Response { mismatchKind := kind if kind == "label" { mismatchKind = "annotation" } for key, exp := range required { valNew, newOK := newMap[key] valOld, oldOK := oldMap[key] if newOK == oldOK && (!newOK || valNew == valOld) { continue } if !newOK { return ad.Denyf("required %s %q not present", kind, key) } re, reErr := regexp.Compile(exp) if reErr != nil { return ad.Denyf("invalid required %s regex for %q: %q: %v", kind, key, exp, reErr) } if !re.MatchString(valNew) { return ad.Denyf("required %s %q value %q does not match regex %q", mismatchKind, key, valNew, exp) } } return nil }