Skip to content

Commit 7268ba4

Browse files
stuggiclaude
andcommitted
[b/r] Add watch predicates and dynamic CRD watches to backup controller
Reduce unnecessary reconciliation by adding predicates to all watched resource types. The controller now only reconciles when: - A resource is created without backup labels - Backup-related annotations change (annotation overrides) - Backup labels are removed from a resource Delete and generic events are ignored entirely. Also adds dynamic watches for CRD instance types (OpenStackControlPlane, MariaDBAccount, etc.) based on the CRD label cache built at setup time using the API reader. This ensures CR instance creates and annotation overrides trigger reconciliation without watching every CRD kind. Additionally scopes findBackupConfigForResource to the resource's namespace instead of listing all BackupConfigs cluster-wide. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e56a018 commit 7268ba4

1 file changed

Lines changed: 152 additions & 12 deletions

File tree

internal/controller/backup/openstackbackupconfig_controller.go

Lines changed: 152 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
402405
func (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.
687755
func (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

Comments
 (0)