// Copyright 2020-2026 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 package validation import ( "context" "fmt" "strings" "k8s.io/apimachinery/pkg/util/validation" "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/pkg/api" 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/template" ) type namespaceMetadataHandler struct{} func NamespaceMetadataHandler() handlers.TypedHandler[*capsulev1beta2.Tenant] { return &namespaceMetadataHandler{} } func (h *namespaceMetadataHandler) OnCreate( _ client.Client, _ client.Reader, tnt *capsulev1beta2.Tenant, _ admission.Decoder, _ events.EventRecorder, ) handlers.Func { return func(_ context.Context, _ admission.Request) *admission.Response { return validateTenantNamespaceMetadata(tnt) } } func (h *namespaceMetadataHandler) OnDelete( client.Client, client.Reader, *capsulev1beta2.Tenant, admission.Decoder, events.EventRecorder, ) handlers.Func { return func(context.Context, admission.Request) *admission.Response { return nil } } func (h *namespaceMetadataHandler) OnUpdate( _ client.Client, _ client.Reader, newTnt *capsulev1beta2.Tenant, _ *capsulev1beta2.Tenant, _ admission.Decoder, _ events.EventRecorder, ) handlers.Func { return func(context.Context, admission.Request) *admission.Response { return validateTenantNamespaceMetadata(newTnt) } } //nolint:staticcheck func validateTenantNamespaceMetadata(tnt *capsulev1beta2.Tenant) *admission.Response { if tnt == nil { return nil } if tnt.Spec.NamespaceOptions == nil { return nil } errs := make([]string, 0, 1+len(tnt.Spec.NamespaceOptions.AdditionalMetadataList)) errs = append( errs, validateAdditionalMetadata( "spec.namespaceOptions.additionalMetadata", //nolint:staticcheck tnt.Spec.NamespaceOptions.AdditionalMetadata, )..., ) for i, item := range tnt.Spec.NamespaceOptions.AdditionalMetadataList { errs = append( errs, validateAdditionalMetadata( fmt.Sprintf("spec.namespaceOptions.additionalMetadataList[%d].additionalMetadata", i), &api.AdditionalMetadataSpec{ Labels: item.Labels, Annotations: item.Annotations, }, )..., ) } if len(errs) > 0 { return ad.Deny(strings.Join(errs, "; ")) } return nil } func validateAdditionalMetadata( fieldPath string, metadata *api.AdditionalMetadataSpec, ) []string { if metadata == nil { return nil } errs := make([]string, 0, len(metadata.Labels)*2+len(metadata.Annotations)*2) errs = append(errs, validateLabelMap(fieldPath+".labels", metadata.Labels)...) errs = append(errs, validateAnnotationMap(fieldPath+".annotations", metadata.Annotations)...) return errs } func validateLabelMap(fieldPath string, labels map[string]string) []string { errs := make([]string, 0, len(labels)*2) for key, value := range labels { errs = append( errs, template.ValidateKubernetesStringOrAllowedTemplates( fmt.Sprintf("%s[%q].key", fieldPath, key), key, validation.IsQualifiedName, )..., ) errs = append( errs, template.ValidateKubernetesStringOrAllowedTemplates( fmt.Sprintf("%s[%q].value", fieldPath, key), value, validation.IsValidLabelValue, )..., ) } return errs } func validateAnnotationMap(fieldPath string, annotations map[string]string) []string { errs := make([]string, 0, len(annotations)*2) for key, value := range annotations { errs = append( errs, template.ValidateKubernetesStringOrAllowedTemplates( fmt.Sprintf("%s[%q].key", fieldPath, key), key, validation.IsQualifiedName, )..., ) errs = append( errs, template.ValidateAllowedTemplatesOnly( fmt.Sprintf("%s[%q].value", fieldPath, key), value, )..., ) } return errs }