@@ -44,8 +44,11 @@ import (
4444 "k8s.io/apimachinery/pkg/runtime/schema"
4545 "k8s.io/apimachinery/pkg/types"
4646 ctrl "sigs.k8s.io/controller-runtime"
47+ "sigs.k8s.io/controller-runtime/pkg/builder"
4748 "sigs.k8s.io/controller-runtime/pkg/client"
49+ "sigs.k8s.io/controller-runtime/pkg/event"
4850 "sigs.k8s.io/controller-runtime/pkg/handler"
51+ "sigs.k8s.io/controller-runtime/pkg/predicate"
4952 "sigs.k8s.io/controller-runtime/pkg/reconcile"
5053)
5154
@@ -400,7 +403,7 @@ func (r *OpenStackBackupConfigReconciler) labelIssuers(ctx context.Context, log
400403// This labels CRs like OpenStackControlPlane, OpenStackVersion, MariaDBAccount, etc.
401404// based on their CRD's backup/restore configuration.
402405func (r * OpenStackBackupConfigReconciler ) labelCRInstances (ctx context.Context , log logr.Logger , instance * backupv1beta1.OpenStackBackupConfig ) (int , error ) {
403- // Build cache lazily on first use (the manager's client cache is ready by reconcile time)
406+ // Fallback: build cache if not populated at setup time
404407 if len (r .CRDLabelCache ) == 0 {
405408 cache , err := backup .BuildCRDLabelCache (ctx , r .Client )
406409 if err != nil {
@@ -683,14 +686,79 @@ func (r *OpenStackBackupConfigReconciler) Reconcile(ctx context.Context, req ctr
683686 return ctrl.Result {}, nil
684687}
685688
689+ // backupLabelKeys are the label keys managed by this controller.
690+ var backupLabelKeys = []string {
691+ backup .BackupLabel ,
692+ backup .BackupRestoreLabel ,
693+ backup .BackupRestoreOrderLabel ,
694+ backup .BackupCategoryLabel ,
695+ }
696+
697+ // backupAnnotationKeys are the annotation keys used for overrides.
698+ var backupAnnotationKeys = []string {
699+ backup .BackupRestoreLabel ,
700+ backup .BackupRestoreOrderLabel ,
701+ }
702+
703+ // needsBackupLabeling returns true if a resource does not yet have backup labels.
704+ func needsBackupLabeling (labels map [string ]string ) bool {
705+ _ , hasBackup := labels [backup .BackupLabel ]
706+ _ , hasRestore := labels [backup .BackupRestoreLabel ]
707+ return ! hasBackup && ! hasRestore
708+ }
709+
710+ // backupAnnotationsChanged returns true if backup-related annotations differ between old and new.
711+ func backupAnnotationsChanged (oldAnnotations , newAnnotations map [string ]string ) bool {
712+ for _ , key := range backupAnnotationKeys {
713+ if oldAnnotations [key ] != newAnnotations [key ] {
714+ return true
715+ }
716+ }
717+ return false
718+ }
719+
720+ // backupLabelsRemoved returns true if any backup labels were present on old but removed from new.
721+ func backupLabelsRemoved (oldLabels , newLabels map [string ]string ) bool {
722+ for _ , key := range backupLabelKeys {
723+ if _ , hadIt := oldLabels [key ]; hadIt {
724+ if _ , hasIt := newLabels [key ]; ! hasIt {
725+ return true
726+ }
727+ }
728+ }
729+ return false
730+ }
731+
732+ // backupResourcePredicate filters events to only reconcile when backup labeling is needed.
733+ // Triggers on:
734+ // - Create: resource has no backup labels yet
735+ // - Update: backup annotations changed OR backup labels were removed
736+ //
737+ // Ignores deletes and generic events entirely.
738+ var backupResourcePredicate = predicate.Funcs {
739+ CreateFunc : func (e event.CreateEvent ) bool {
740+ return needsBackupLabeling (e .Object .GetLabels ())
741+ },
742+ UpdateFunc : func (e event.UpdateEvent ) bool {
743+ return backupAnnotationsChanged (e .ObjectOld .GetAnnotations (), e .ObjectNew .GetAnnotations ()) ||
744+ backupLabelsRemoved (e .ObjectOld .GetLabels (), e .ObjectNew .GetLabels ())
745+ },
746+ DeleteFunc : func (_ event.DeleteEvent ) bool {
747+ return false
748+ },
749+ GenericFunc : func (_ event.GenericEvent ) bool {
750+ return false
751+ },
752+ }
753+
686754// SetupWithManager sets up the controller with the Manager.
687755func (r * OpenStackBackupConfigReconciler ) SetupWithManager (mgr ctrl.Manager ) error {
756+ Log := ctrl .Log .WithName ("backup" ).WithName ("setup" )
757+
688758 // findBackupConfigForResource maps a resource back to the BackupConfig that should process it
689- findBackupConfigForResource := func (ctx context.Context , _ client.Object ) []reconcile.Request {
690- // For now, reconcile all BackupConfigs when any resource changes
691- // TODO: optimize to only reconcile the BackupConfig for the resource's namespace
759+ findBackupConfigForResource := func (ctx context.Context , obj client.Object ) []reconcile.Request {
692760 configList := & backupv1beta1.OpenStackBackupConfigList {}
693- if err := mgr .GetClient ().List (ctx , configList ); err != nil {
761+ if err := mgr .GetClient ().List (ctx , configList , client . InNamespace ( obj . GetNamespace ()) ); err != nil {
694762 return []reconcile.Request {}
695763 }
696764
@@ -706,12 +774,84 @@ func (r *OpenStackBackupConfigReconciler) SetupWithManager(mgr ctrl.Manager) err
706774 return requests
707775 }
708776
709- return ctrl .NewControllerManagedBy (mgr ).
777+ bldr := ctrl .NewControllerManagedBy (mgr ).
710778 For (& backupv1beta1.OpenStackBackupConfig {}).
711- Watches (& corev1.Secret {}, handler .EnqueueRequestsFromMapFunc (findBackupConfigForResource )).
712- Watches (& corev1.ConfigMap {}, handler .EnqueueRequestsFromMapFunc (findBackupConfigForResource )).
713- Watches (& k8s_networkingv1.NetworkAttachmentDefinition {}, handler .EnqueueRequestsFromMapFunc (findBackupConfigForResource )).
714- Watches (& certmgrv1.Issuer {}, handler .EnqueueRequestsFromMapFunc (findBackupConfigForResource )).
715- Named ("openstackbackupconfig" ).
716- Complete (r )
779+ Watches (& corev1.Secret {}, handler .EnqueueRequestsFromMapFunc (findBackupConfigForResource ), builder .WithPredicates (backupResourcePredicate )).
780+ Watches (& corev1.ConfigMap {}, handler .EnqueueRequestsFromMapFunc (findBackupConfigForResource ), builder .WithPredicates (backupResourcePredicate )).
781+ Watches (& k8s_networkingv1.NetworkAttachmentDefinition {}, handler .EnqueueRequestsFromMapFunc (findBackupConfigForResource ), builder .WithPredicates (backupResourcePredicate )).
782+ Watches (& certmgrv1.Issuer {}, handler .EnqueueRequestsFromMapFunc (findBackupConfigForResource ), builder .WithPredicates (backupResourcePredicate ))
783+
784+ // Build CRD label cache and add watches for CRD instance types.
785+ // Uses the API reader since the manager's cache is not started yet.
786+ apiReader := mgr .GetAPIReader ()
787+ cache , err := buildCRDLabelCacheFromReader (apiReader )
788+ if err != nil {
789+ Log .Error (err , "Failed to build CRD label cache, CR instances will not be watched" )
790+ } else {
791+ r .CRDLabelCache = cache
792+ for crdName := range cache {
793+ gvk , err := getGVKFromCRDUsingReader (apiReader , crdName )
794+ if err != nil {
795+ Log .Error (err , "Failed to get GVK for CRD, skipping watch" , "crd" , crdName )
796+ continue
797+ }
798+ obj := & metav1.PartialObjectMetadata {}
799+ obj .SetGroupVersionKind (gvk )
800+ bldr = bldr .Watches (obj , handler .EnqueueRequestsFromMapFunc (findBackupConfigForResource ), builder .WithPredicates (backupResourcePredicate ))
801+ Log .Info ("Added watch for CRD instances" , "crd" , crdName , "gvk" , gvk )
802+ }
803+ }
804+
805+ return bldr .Named ("openstackbackupconfig" ).Complete (r )
806+ }
807+
808+ // buildCRDLabelCacheFromReader builds the CRD label cache using a client.Reader.
809+ // Used at setup time when the manager's cache is not started.
810+ func buildCRDLabelCacheFromReader (reader client.Reader ) (backup.CRDLabelCache , error ) {
811+ cache := make (backup.CRDLabelCache )
812+
813+ crdList := & apiextensionsv1.CustomResourceDefinitionList {}
814+ if err := reader .List (context .Background (), crdList ); err != nil {
815+ return nil , err
816+ }
817+
818+ for _ , crd := range crdList .Items {
819+ labels := crd .GetLabels ()
820+ if labels == nil || labels [backup .BackupRestoreLabel ] != "true" {
821+ continue
822+ }
823+ cache [crd .Name ] = backup.BackupConfig {
824+ Enabled : true ,
825+ RestoreOrder : labels [backup .BackupRestoreOrderLabel ],
826+ Category : labels [backup .BackupCategoryLabel ],
827+ }
828+ }
829+
830+ return cache , nil
831+ }
832+
833+ // getGVKFromCRDUsingReader looks up a CRD by name using a reader and returns its GVK.
834+ // Used at setup time when the manager's cache is not started.
835+ func getGVKFromCRDUsingReader (reader client.Reader , crdName string ) (schema.GroupVersionKind , error ) {
836+ crd := & apiextensionsv1.CustomResourceDefinition {}
837+ if err := reader .Get (context .Background (), types.NamespacedName {Name : crdName }, crd ); err != nil {
838+ return schema.GroupVersionKind {}, err
839+ }
840+
841+ var version string
842+ for _ , v := range crd .Spec .Versions {
843+ if v .Storage {
844+ version = v .Name
845+ break
846+ }
847+ if v .Served && version == "" {
848+ version = v .Name
849+ }
850+ }
851+
852+ return schema.GroupVersionKind {
853+ Group : crd .Spec .Group ,
854+ Version : version ,
855+ Kind : crd .Spec .Names .Kind ,
856+ }, nil
717857}
0 commit comments