// Copyright 2020-2026 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 package invalidator import ( "context" "errors" "fmt" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/client-go/rest" "k8s.io/client-go/util/workqueue" 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/event" "sigs.k8s.io/controller-runtime/pkg/handler" "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/cache" "github.com/projectcapsule/capsule/internal/controllers/utils" "github.com/projectcapsule/capsule/internal/metrics" "github.com/projectcapsule/capsule/pkg/runtime/configuration" "github.com/projectcapsule/capsule/pkg/runtime/predicates" ) type CacheInvalidator struct { client.Client Rest *rest.Config Log logr.Logger reader client.Reader configName string metrics *metrics.ConfigRecorder Configuration configuration.Configuration RegistryCache *cache.RegistryRuleSetCache TargetsCache *cache.CompiledTargetsCache[string] JSONPathCache *cache.JSONPathCache CELCache *cache.CELCache ImpersonationCache *cache.ImpersonationCache RegexCache *cache.RegexCache } func (r *CacheInvalidator) NeedLeaderElection() bool { return false } // Start is the Runnable function triggered upon Manager start-up to perform cache population. func (r *CacheInvalidator) Start(ctx context.Context) error { if err := r.rebuildCaches(ctx, r.Log); err != nil { r.Log.Error(err, "cache population failed") return nil } <-ctx.Done() return nil } func (r *CacheInvalidator) SetupWithManager( mgr ctrl.Manager, ctrlConfig utils.ControllerOptions, metrics *metrics.ConfigRecorder, ) (err error) { r.configName = ctrlConfig.ConfigurationName r.reader = mgr.GetAPIReader() r.metrics = metrics err = ctrl.NewControllerManagedBy(mgr). Named("config/caches"). For( &capsulev1beta2.CapsuleConfiguration{}, builder.WithPredicates( predicate.GenerationChangedPredicate{}, predicates.NamesMatchingPredicate{Names: []string{ctrlConfig.ConfigurationName}}, ), ). Watches( &capsulev1beta2.CapsuleConfiguration{}, handler.Funcs{ UpdateFunc: func(ctx context.Context, updateEvent event.TypedUpdateEvent[client.Object], limitingInterface workqueue.TypedRateLimitingInterface[reconcile.Request]) { if err := r.rebuildImpersonationCache(ctx, r.Log); err != nil { r.Log.Error(err, "unable to invalidate impersonation cache") } }, }, builder.WithPredicates( predicates.CapsuleConfigSpecImpersonationChangedPredicate{}, predicates.NamesMatchingPredicate{Names: []string{ctrlConfig.ConfigurationName}}, ), ). WatchesMetadata( &corev1.ServiceAccount{}, handler.Funcs{ DeleteFunc: func( ctx context.Context, e event.TypedDeleteEvent[client.Object], q workqueue.TypedRateLimitingInterface[reconcile.Request], ) { if err := r.invalidateServiceAccount(ctx, e.Object); err != nil { r.Log.Error(err, "unable to invalidate serviceaccount cache", "namespace", e.Object.GetNamespace(), "name", e.Object.GetName(), ) } }, }, builder.WithPredicates(predicate.Funcs{ DeleteFunc: func(e event.DeleteEvent) bool { return true }, CreateFunc: func(e event.CreateEvent) bool { return false }, UpdateFunc: func(e event.UpdateEvent) bool { return false }, GenericFunc: func(e event.GenericEvent) bool { return false }, }, ), ). WithOptions(ctrlConfig.Runtime.ToControllerOptions()). Complete(r) if err != nil { return err } // register Start(ctx) as a manager runnable. return mgr.Add(r) } func (r *CacheInvalidator) Reconcile(ctx context.Context, request reconcile.Request) (res reconcile.Result, err error) { log := r.Log.WithValues("configuration", request.Name) log.V(5).Info("invalidating and rebuilding caches") cfg := configuration.NewCapsuleConfiguration(ctx, r.Client, r.reader, r.Rest, request.Name) instance := &capsulev1beta2.CapsuleConfiguration{} if err = r.Get(ctx, request.NamespacedName, instance); err != nil { if apierrors.IsNotFound(err) { log.V(5).Info("requested object not found, could have been deleted after reconcile request") return reconcile.Result{}, nil } log.Error(err, "error reading the object") return res, err } if err := r.rebuildCaches(ctx, log); err != nil { return res, err } interval := cfg.CacheInvalidation() return reconcile.Result{ Requeue: true, RequeueAfter: interval.Duration, }, err } // invalidateCaches invokes for all caches their invalidation functions. func (r *CacheInvalidator) rebuildCaches( ctx context.Context, log logr.Logger, ) error { var errs []error if err := r.rebuildRegexCache(ctx, log); err != nil { errs = append(errs, fmt.Errorf("rebuild Regex cache: %w", err)) } if err := r.rebuildJSONPathCache(ctx, log); err != nil { errs = append(errs, fmt.Errorf("rebuild JSONPath cache: %w", err)) } if err := r.rebuildTargetsCache(ctx, log); err != nil { errs = append(errs, fmt.Errorf("rebuild targets cache: %w", err)) } if err := r.rebuildRuleStatusRegistryCache(ctx, log); err != nil { errs = append(errs, fmt.Errorf("rebuild registry cache: %w", err)) } if err := r.rebuildImpersonationCache(ctx, log); err != nil { errs = append(errs, fmt.Errorf("rebuild impersonation cache: %w", err)) } if len(errs) > 0 { return errors.Join(errs...) } return nil }