// Copyright 2020-2026 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 //nolint:dupl package admission import ( "context" "maps" "sort" "github.com/go-logr/logr" admissionv1 "k8s.io/api/admissionregistration/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" "github.com/projectcapsule/capsule/internal/controllers/utils" "github.com/projectcapsule/capsule/pkg/api/meta" clt "github.com/projectcapsule/capsule/pkg/runtime/client" "github.com/projectcapsule/capsule/pkg/runtime/configuration" "github.com/projectcapsule/capsule/pkg/runtime/predicates" ) type mutatingReconciler struct { client client.Client configuration configuration.Configuration log logr.Logger } func (r *mutatingReconciler) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.ControllerOptions) error { return ctrl.NewControllerManagedBy(mgr). Named("capsule/admission/mutating"). For( &capsulev1beta2.CapsuleConfiguration{}, builder.WithPredicates( predicate.GenerationChangedPredicate{}, predicates.NamesMatchingPredicate{Names: []string{ctrlConfig.ConfigurationName}}, ), ). WithOptions(controller.Options{MaxConcurrentReconciles: ctrlConfig.MaxConcurrentReconciles}). Complete(r) } func (r *mutatingReconciler) Reconcile(ctx context.Context, request reconcile.Request) (res reconcile.Result, err error) { err = r.reconcileConfiguration(ctx, r.configuration.Admission().Mutating) return res, err } func (r *mutatingReconciler) reconcileConfiguration( ctx context.Context, cfg capsulev1beta2.DynamicAdmissionConfig, ) error { desiredName := string(cfg.Name) hooks, err := r.webhooks(ctx, cfg) if err != nil { return err } if len(hooks) == 0 { managed, err := r.listManagedWebhookConfigs(ctx) if err != nil { return err } for i := range managed { if err := r.deleteWebhookConfig(ctx, managed[i].Name); err != nil { return err } } return nil } obj := &admissionv1.MutatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{Name: string(cfg.Name)}, } sort.Slice(hooks, func(i, j int) bool { return hooks[i].Name < hooks[j].Name }) labels := obj.GetLabels() if labels == nil { labels = make(map[string]string) } maps.Copy(labels, cfg.Labels) labels[meta.CreatedByCapsuleLabel] = meta.ControllerValue obj.SetLabels(labels) annotations := obj.GetAnnotations() if annotations == nil { annotations = make(map[string]string) } maps.Copy(annotations, cfg.Annotations) obj.SetAnnotations(annotations) if err := clt.CreateOrPatch(ctx, r.client, obj, meta.FieldManagerCapsuleController, true); err != nil { return err } // Garbage-collect any old managed validating webhook configs with different name managed, err := r.listManagedWebhookConfigs(ctx) if err != nil { return err } for i := range managed { if managed[i].Name == desiredName { continue } if err := r.deleteWebhookConfig(ctx, managed[i].Name); err != nil { return err } } return nil } func (r *mutatingReconciler) listManagedWebhookConfigs(ctx context.Context) ([]admissionv1.MutatingWebhookConfiguration, error) { list := &admissionv1.MutatingWebhookConfigurationList{} if err := r.client.List(ctx, list, client.MatchingLabels{ meta.CreatedByCapsuleLabel: meta.ControllerValue, }); err != nil { return nil, err } return list.Items, nil } func (r *mutatingReconciler) deleteWebhookConfig(ctx context.Context, name string) error { if name == "" { return nil } obj := &admissionv1.MutatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{Name: name}, } err := r.client.Delete(ctx, obj) if apierrors.IsNotFound(err) { return nil } return err } func (r *mutatingReconciler) webhooks( ctx context.Context, cfg capsulev1beta2.DynamicAdmissionConfig, ) (hooks []admissionv1.MutatingWebhook, err error) { return }