// Copyright 2020-2026 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 package predicates import ( "reflect" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" rbacv1 "k8s.io/api/rbac/v1" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/predicate" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" capmeta "github.com/projectcapsule/capsule/pkg/api/meta" "github.com/projectcapsule/capsule/pkg/utils" ) func ClassChanged() predicate.Predicate { return predicate.Or( predicate.GenerationChangedPredicate{}, UpdatedMetadataPredicate{}, DeletionChangedPredicate{}, ) } // TenantManagedResourceChangedPredicate admits drift in fields reconciled by // the Tenant controller while filtering status-only updates. Several built-in // resources do not reliably increment metadata.generation when their desired // fields change, so GenerationChangedPredicate alone cannot detect tampering. type TenantManagedResourceChangedPredicate struct{ predicate.Funcs } func (TenantManagedResourceChangedPredicate) Create(event.CreateEvent) bool { return true } func (TenantManagedResourceChangedPredicate) Delete(event.DeleteEvent) bool { return true } func (TenantManagedResourceChangedPredicate) Generic(event.GenericEvent) bool { return false } func (TenantManagedResourceChangedPredicate) Update(e event.UpdateEvent) bool { if e.ObjectOld == nil || e.ObjectNew == nil { return false } if !utils.MapEqual(e.ObjectOld.GetLabels(), e.ObjectNew.GetLabels()) || !utils.MapEqual(e.ObjectOld.GetAnnotations(), e.ObjectNew.GetAnnotations()) || !reflect.DeepEqual(e.ObjectOld.GetOwnerReferences(), e.ObjectNew.GetOwnerReferences()) || !reflect.DeepEqual(e.ObjectOld.GetFinalizers(), e.ObjectNew.GetFinalizers()) || (e.ObjectOld.GetDeletionTimestamp() == nil) != (e.ObjectNew.GetDeletionTimestamp() == nil) { return true } switch oldObject := e.ObjectOld.(type) { case *corev1.LimitRange: newObject, ok := e.ObjectNew.(*corev1.LimitRange) return ok && !reflect.DeepEqual(oldObject.Spec, newObject.Spec) case *corev1.ResourceQuota: newObject, ok := e.ObjectNew.(*corev1.ResourceQuota) return ok && !reflect.DeepEqual(oldObject.Spec, newObject.Spec) case *networkingv1.NetworkPolicy: newObject, ok := e.ObjectNew.(*networkingv1.NetworkPolicy) return ok && !reflect.DeepEqual(oldObject.Spec, newObject.Spec) case *rbacv1.RoleBinding: newObject, ok := e.ObjectNew.(*rbacv1.RoleBinding) return ok && (!reflect.DeepEqual(oldObject.RoleRef, newObject.RoleRef) || !reflect.DeepEqual(oldObject.Subjects, newObject.Subjects)) default: return false } } type NamespaceTenantStateChangedPredicate struct{ predicate.Funcs } func (NamespaceTenantStateChangedPredicate) Create(event.CreateEvent) bool { return true } func (NamespaceTenantStateChangedPredicate) Delete(event.DeleteEvent) bool { return true } func (NamespaceTenantStateChangedPredicate) Generic(event.GenericEvent) bool { return false } func (NamespaceTenantStateChangedPredicate) Update(e event.UpdateEvent) bool { oldNamespace, oldOK := e.ObjectOld.(*corev1.Namespace) newNamespace, newOK := e.ObjectNew.(*corev1.Namespace) if !oldOK || !newOK { return false } return !reflect.DeepEqual(oldNamespace.Labels, newNamespace.Labels) || !reflect.DeepEqual(oldNamespace.Annotations, newNamespace.Annotations) || !reflect.DeepEqual(oldNamespace.OwnerReferences, newNamespace.OwnerReferences) || (oldNamespace.DeletionTimestamp == nil) != (newNamespace.DeletionTimestamp == nil) } type QuantityLedgerWorkChangedPredicate struct{ predicate.Funcs } func (QuantityLedgerWorkChangedPredicate) Create(e event.CreateEvent) bool { ledger, ok := e.Object.(*capsulev1beta2.QuantityLedger) if !ok { return false } // A ledger can be created and receive admission reservations before the // informer observes its first Add event. In that case the Add is the only // work notification; filtering it would strand the reservations forever. return quantityLedgerHasWork(ledger) } func (QuantityLedgerWorkChangedPredicate) Delete(event.DeleteEvent) bool { return true } func (QuantityLedgerWorkChangedPredicate) Generic(event.GenericEvent) bool { return false } func (QuantityLedgerWorkChangedPredicate) Update(e event.UpdateEvent) bool { oldLedger, oldOK := e.ObjectOld.(*capsulev1beta2.QuantityLedger) newLedger, newOK := e.ObjectNew.(*capsulev1beta2.QuantityLedger) if !oldOK || !newOK { return false } if oldLedger.Generation != newLedger.Generation { return true } // Informers may coalesce an intermediate settled state. An update can // therefore replace reservations with pending deletes (or append more // reservations) while both observed snapshots contain work. Admit every // newly introduced or changed work item so it cannot be stranded. Pure // settlement/removal updates remain filtered. return quantityLedgerIntroducesWork(oldLedger, newLedger) } func quantityLedgerHasWork(ledger *capsulev1beta2.QuantityLedger) bool { return len(ledger.Status.Reservations) > 0 || len(ledger.Status.PendingDeletes) > 0 } func quantityLedgerIntroducesWork(oldLedger, newLedger *capsulev1beta2.QuantityLedger) bool { matchedReservations := make([]bool, len(oldLedger.Status.Reservations)) for _, newReservation := range newLedger.Status.Reservations { matched := false for i, oldReservation := range oldLedger.Status.Reservations { if matchedReservations[i] || !reflect.DeepEqual(oldReservation, newReservation) { continue } matchedReservations[i] = true matched = true break } if !matched { return true } } matchedPendingDeletes := make([]bool, len(oldLedger.Status.PendingDeletes)) for _, newPendingDelete := range newLedger.Status.PendingDeletes { matched := false for i, oldPendingDelete := range oldLedger.Status.PendingDeletes { if matchedPendingDeletes[i] || !reflect.DeepEqual(oldPendingDelete, newPendingDelete) { continue } matchedPendingDeletes[i] = true matched = true break } if !matched { return true } } return false } type ProvisionerSubjectsChangedPredicate struct{ predicate.Funcs } func (ProvisionerSubjectsChangedPredicate) Create(event.CreateEvent) bool { return true } func (ProvisionerSubjectsChangedPredicate) Delete(event.DeleteEvent) bool { return false } func (ProvisionerSubjectsChangedPredicate) Generic(event.GenericEvent) bool { return false } func (ProvisionerSubjectsChangedPredicate) Update(e event.UpdateEvent) bool { oldConfig, oldOK := e.ObjectOld.(*capsulev1beta2.CapsuleConfiguration) newConfig, newOK := e.ObjectNew.(*capsulev1beta2.CapsuleConfiguration) if !oldOK || !newOK { return false } return !reflect.DeepEqual(oldConfig.Spec.Administrators, newConfig.Spec.Administrators) || !reflect.DeepEqual(oldConfig.Status.Users, newConfig.Status.Users) || oldConfig.Spec.AllowServiceAccountPromotion != newConfig.Spec.AllowServiceAccountPromotion || !reflect.DeepEqual(oldConfig.Spec.RBAC, newConfig.Spec.RBAC) } type ResourcePoolNamespacesChangedPredicate struct{ predicate.Funcs } func (ResourcePoolNamespacesChangedPredicate) Create(event.CreateEvent) bool { return true } func (ResourcePoolNamespacesChangedPredicate) Delete(event.DeleteEvent) bool { return true } func (ResourcePoolNamespacesChangedPredicate) Generic(event.GenericEvent) bool { return false } func (ResourcePoolNamespacesChangedPredicate) Update(e event.UpdateEvent) bool { oldPool, oldOK := e.ObjectOld.(*capsulev1beta2.ResourcePool) newPool, newOK := e.ObjectNew.(*capsulev1beta2.ResourcePool) if !oldOK || !newOK { return false } return !reflect.DeepEqual(oldPool.Status.Namespaces, newPool.Status.Namespaces) || (oldPool.DeletionTimestamp == nil) != (newPool.DeletionTimestamp == nil) } // ResourceQuotaUsageChangedPredicate admits only usage changes from the quota // controller. ResourcePool claim Bound conditions are derived from status.used, // while status.hard and other status updates do not affect claim scheduling. type ResourceQuotaUsageChangedPredicate struct{ predicate.Funcs } func (ResourceQuotaUsageChangedPredicate) Create(event.CreateEvent) bool { return false } func (ResourceQuotaUsageChangedPredicate) Delete(event.DeleteEvent) bool { return false } func (ResourceQuotaUsageChangedPredicate) Generic(event.GenericEvent) bool { return false } func (ResourceQuotaUsageChangedPredicate) Update(e event.UpdateEvent) bool { oldQuota, oldOK := e.ObjectOld.(*corev1.ResourceQuota) newQuota, newOK := e.ObjectNew.(*corev1.ResourceQuota) if !oldOK || !newOK { return false } return !reflect.DeepEqual(oldQuota.Status.Used, newQuota.Status.Used) } type DependencyStateChangedPredicate struct{ predicate.Funcs } func (DependencyStateChangedPredicate) Create(event.CreateEvent) bool { return true } func (DependencyStateChangedPredicate) Delete(event.DeleteEvent) bool { return true } func (DependencyStateChangedPredicate) Generic(event.GenericEvent) bool { return false } func (DependencyStateChangedPredicate) Update(e event.UpdateEvent) bool { if e.ObjectOld == nil || e.ObjectNew == nil { return false } return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() || (e.ObjectOld.GetDeletionTimestamp() == nil) != (e.ObjectNew.GetDeletionTimestamp() == nil) || !reflect.DeepEqual(resourceReadyCondition(e.ObjectOld), resourceReadyCondition(e.ObjectNew)) } type TenantSelectionChangedPredicate struct{ predicate.Funcs } func (TenantSelectionChangedPredicate) Create(event.CreateEvent) bool { return true } func (TenantSelectionChangedPredicate) Delete(event.DeleteEvent) bool { return true } func (TenantSelectionChangedPredicate) Generic(event.GenericEvent) bool { return false } func (TenantSelectionChangedPredicate) Update(e event.UpdateEvent) bool { oldTenant, oldOK := e.ObjectOld.(*capsulev1beta2.Tenant) newTenant, newOK := e.ObjectNew.(*capsulev1beta2.Tenant) if !oldOK || !newOK { return false } return !utils.MapEqual(oldTenant.Labels, newTenant.Labels) || !reflect.DeepEqual(oldTenant.Status.Namespaces, newTenant.Status.Namespaces) || (oldTenant.DeletionTimestamp == nil) != (newTenant.DeletionTimestamp == nil) } type TenantPodOptionsChangedPredicate struct{ predicate.Funcs } func (TenantPodOptionsChangedPredicate) Create(event.CreateEvent) bool { return false } func (TenantPodOptionsChangedPredicate) Delete(event.DeleteEvent) bool { return false } func (TenantPodOptionsChangedPredicate) Generic(event.GenericEvent) bool { return false } //nolint:staticcheck func (TenantPodOptionsChangedPredicate) Update(e event.UpdateEvent) bool { oldTenant, oldOK := e.ObjectOld.(*capsulev1beta2.Tenant) newTenant, newOK := e.ObjectNew.(*capsulev1beta2.Tenant) return oldOK && newOK && !reflect.DeepEqual(oldTenant.Spec.PodOptions, newTenant.Spec.PodOptions) } type TenantServiceOptionsChangedPredicate struct{ predicate.Funcs } func (TenantServiceOptionsChangedPredicate) Create(event.CreateEvent) bool { return false } func (TenantServiceOptionsChangedPredicate) Delete(event.DeleteEvent) bool { return false } func (TenantServiceOptionsChangedPredicate) Generic(event.GenericEvent) bool { return false } //nolint:staticcheck func (TenantServiceOptionsChangedPredicate) Update(e event.UpdateEvent) bool { oldTenant, oldOK := e.ObjectOld.(*capsulev1beta2.Tenant) newTenant, newOK := e.ObjectNew.(*capsulev1beta2.Tenant) return oldOK && newOK && !reflect.DeepEqual(oldTenant.Spec.ServiceOptions, newTenant.Spec.ServiceOptions) } type TenantNamespacesChangedPredicate struct{ predicate.Funcs } func (TenantNamespacesChangedPredicate) Create(event.CreateEvent) bool { return true } func (TenantNamespacesChangedPredicate) Delete(event.DeleteEvent) bool { return true } func (TenantNamespacesChangedPredicate) Generic(event.GenericEvent) bool { return false } func (TenantNamespacesChangedPredicate) Update(e event.UpdateEvent) bool { oldTenant, oldOK := e.ObjectOld.(*capsulev1beta2.Tenant) newTenant, newOK := e.ObjectNew.(*capsulev1beta2.Tenant) return oldOK && newOK && !reflect.DeepEqual(oldTenant.Status.Namespaces, newTenant.Status.Namespaces) } type NamespaceMetadataChangedPredicate struct{ predicate.Funcs } func (NamespaceMetadataChangedPredicate) Create(event.CreateEvent) bool { return true } func (NamespaceMetadataChangedPredicate) Delete(event.DeleteEvent) bool { return true } func (NamespaceMetadataChangedPredicate) Generic(event.GenericEvent) bool { return false } func (NamespaceMetadataChangedPredicate) Update(e event.UpdateEvent) bool { oldNamespace, oldOK := e.ObjectOld.(*corev1.Namespace) newNamespace, newOK := e.ObjectNew.(*corev1.Namespace) return oldOK && newOK && (!reflect.DeepEqual(oldNamespace.Labels, newNamespace.Labels) || !reflect.DeepEqual(oldNamespace.Annotations, newNamespace.Annotations)) } func resourceReadyCondition(obj any) any { switch typed := obj.(type) { case *capsulev1beta2.GlobalTenantResource: return typed.Status.Conditions.GetConditionByType(capmeta.ReadyCondition) case *capsulev1beta2.TenantResource: return typed.Status.Conditions.GetConditionByType(capmeta.ReadyCondition) default: return nil } }