@@ -67,6 +67,7 @@ const (
6767 errReconcileUpdate = "update failed"
6868 errReconcileDelete = "delete failed"
6969 errRecordChangeLog = "cannot record change log entry"
70+ errProtectProviderConfig = "cannot protect ProviderConfigUsage"
7071 errUntrackProviderConfig = "cannot release ProviderConfigUsage"
7172
7273 errExternalResourceNotExist = "external resource does not exist"
@@ -83,6 +84,8 @@ const (
8384 reasonCannotObserve event.Reason = "CannotObserveExternalResource"
8485 reasonCannotCreate event.Reason = "CannotCreateExternalResource"
8586 reasonCannotDelete event.Reason = "CannotDeleteExternalResource"
87+ reasonCannotProtect event.Reason = "CannotProtectProviderConfigUsage"
88+ reasonCannotUntrack event.Reason = "CannotReleaseProviderConfigUsage"
8689 reasonCannotPublish event.Reason = "CannotPublishConnectionDetails"
8790 reasonCannotUnpublish event.Reason = "CannotUnpublishConnectionDetails"
8891 reasonCannotUpdate event.Reason = "CannotUpdateExternalResource"
@@ -627,6 +630,10 @@ func defaultMRManaged(m manager.Manager) mrManaged {
627630 ReferenceResolver : NewAPISimpleReferenceResolver (m .GetClient ()),
628631 ConnectionPublisher : NewAPISecretPublisher (m .GetClient (), m .GetScheme ()),
629632 LocalConnectionPublisher : NewAPILocalSecretPublisher (m .GetClient (), m .GetScheme ()),
633+
634+ // Providers opt in to ProviderConfigUsage protection by supplying their
635+ // tracker via WithProviderConfigUsageCleaner.
636+ ProviderConfigUsageCleaner : resource .NewNopProviderConfigUsageCleaner (),
630637 }
631638}
632639
@@ -805,6 +812,25 @@ func WithFinalizer(f resource.Finalizer) ReconcilerOption {
805812 }
806813}
807814
815+ // WithProviderConfigUsageCleaner specifies how the Reconciler should protect
816+ // and release the managed resource's ProviderConfigUsage. Managed resources
817+ // that don't use a ProviderConfigUsage don't need this option; the Reconciler
818+ // neither protects nor releases a usage by default.
819+ //
820+ // Supplying a cleaner is what enables ProviderConfig deletion protection. The
821+ // Reconciler adds the usage's finalizer once it has connected, and removes it
822+ // once the managed resource no longer needs its ProviderConfig, so the
823+ // finalizer is only ever added where something is wired up to remove it.
824+ //
825+ // The cleaner need not be the tracker the provider records usages with. It's
826+ // only used to find the usage belonging to a managed resource, so a separately
827+ // constructed one with the same client and usage type behaves identically.
828+ func WithProviderConfigUsageCleaner (c resource.ProviderConfigUsageCleaner ) ReconcilerOption {
829+ return func (r * Reconciler ) {
830+ r .managed .ProviderConfigUsageCleaner = c
831+ }
832+ }
833+
808834// WithReferenceResolver specifies how the Reconciler should resolve any
809835// inter-resource references it encounters while reconciling managed resources.
810836func WithReferenceResolver (rr ReferenceResolver ) ReconcilerOption {
@@ -878,18 +904,12 @@ func (r *Reconciler) effectivePollInterval(o metav1.Object) time.Duration {
878904
879905// NewReconciler returns a Reconciler that reconciles managed resources of the
880906// supplied ManagedKind with resources in an external system such as a cloud
881- // provider API. The usage cleaner should be the tracker used to track the
882- // managed resource's ProviderConfigUsage. Managed resources that do not use a
883- // ProviderConfigUsage should pass resource.NewNopProviderConfigUsageCleaner().
884- // NewReconciler panics if the usage cleaner is nil or the managed resource kind
885- // is not registered with the supplied manager's runtime.Scheme. The returned
886- // Reconciler uses a no-op external system by default; callers should supply an
887- // ExternalConnector that can manage resources in a real system.
888- func NewReconciler (m manager.Manager , of resource.ManagedKind , usage resource.ProviderConfigUsageCleaner , o ... ReconcilerOption ) * Reconciler {
889- if usage == nil {
890- panic ("managed reconciler requires a ProviderConfigUsageCleaner" )
891- }
892-
907+ // provider API. It panics if asked to reconcile a managed resource kind that is
908+ // not registered with the supplied manager's runtime.Scheme. The returned
909+ // Reconciler reconciles with a dummy, no-op 'external system' by default;
910+ // callers should supply an ExternalConnector that returns an ExternalClient
911+ // capable of managing resources in a real system.
912+ func NewReconciler (m manager.Manager , of resource.ManagedKind , o ... ReconcilerOption ) * Reconciler {
893913 nm := func () resource.Managed {
894914 //nolint:forcetypeassert // If this isn't an MR it's a programming error and we want to panic.
895915 return resource .MustCreateObject (schema .GroupVersionKind (of ), m .GetScheme ()).(resource.Managed )
@@ -915,7 +935,6 @@ func NewReconciler(m manager.Manager, of resource.ManagedKind, usage resource.Pr
915935 change : newNopChangeLogger (),
916936 conditions : new (conditions.ObservedGenerationPropagationManager ),
917937 }
918- r .managed .ProviderConfigUsageCleaner = usage
919938
920939 for _ , ro := range o {
921940 ro (r )
@@ -1084,6 +1103,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
10841103 if err := r .managed .Untrack (ctx , managed ); err != nil {
10851104 log .Debug ("Cannot release ProviderConfigUsage" , "error" , err )
10861105 uerr := errors .Wrap (err , errUntrackProviderConfig )
1106+ record .Event (managed , event .Warning (reasonCannotUntrack , uerr ))
10871107 status .MarkConditions (xpv2 .Deleting (), xpv2 .ReconcileError (uerr ))
10881108 if err := updateStatus (); err != nil {
10891109 return reconcile.Result {Requeue : true }, errors .Wrap (err , errUpdateManagedStatus )
@@ -1196,6 +1216,24 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
11961216 }
11971217 }()
11981218
1219+ // We've connected, so the managed resource is using its ProviderConfig and
1220+ // its usage must outlive that need. The Reconciler owns both ends of the
1221+ // usage finalizer - added here, removed by Untrack below - so a provider's
1222+ // tracker never has to, and can't get out of step with us.
1223+ if err := r .managed .Protect (ctx , managed ); err != nil {
1224+ log .Debug ("Cannot protect ProviderConfigUsage" , "error" , err )
1225+
1226+ if kerrors .IsConflict (err ) {
1227+ return reconcile.Result {Requeue : true }, nil
1228+ }
1229+
1230+ perr := errors .Wrap (err , errProtectProviderConfig )
1231+ record .Event (managed , event .Warning (reasonCannotProtect , perr ))
1232+ status .MarkConditions (xpv2 .ReconcileError (perr ))
1233+
1234+ return reconcile.Result {Requeue : true }, errors .Wrap (updateStatus (), errUpdateManagedStatus )
1235+ }
1236+
11991237 observation , err := external .Observe (externalCtx , managed )
12001238 if err != nil {
12011239 // We'll usually hit this case if our Provider credentials are invalid
@@ -1252,7 +1290,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
12521290 if meta .WasDeleted (managed ) {
12531291 log = log .WithValues ("deletion-timestamp" , managed .GetDeletionTimestamp ())
12541292
1255- if numControllerFinalizers ( managed ) > 1 {
1293+ if len ( meta . NonGCFinalizers ( managed ) ) > 1 {
12561294 // There are other controllers monitoring this resource so preserve the external instance
12571295 // until all other finalizers have been removed
12581296 log .Debug ("Delay external deletion until all finalizers have been removed" )
@@ -1337,6 +1375,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
13371375 if err := r .managed .Untrack (ctx , managed ); err != nil {
13381376 log .Debug ("Cannot release ProviderConfigUsage" , "error" , err )
13391377 uerr := errors .Wrap (err , errUntrackProviderConfig )
1378+ record .Event (managed , event .Warning (reasonCannotUntrack , uerr ))
13401379 status .MarkConditions (xpv2 .Deleting (), xpv2 .ReconcileError (uerr ))
13411380 if err := updateStatus (); err != nil {
13421381 return reconcile.Result {Requeue : true }, errors .Wrap (err , errUpdateManagedStatus )
@@ -1609,9 +1648,3 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
16091648
16101649 return reconcile.Result {RequeueAfter : reconcileAfter }, errors .Wrap (updateStatus (), errUpdateManagedStatus )
16111650}
1612-
1613- // numControllerFinalizers returns the number of finalizers not managed by the
1614- // Kubernetes garbage collector.
1615- func numControllerFinalizers (mg resource.Managed ) int {
1616- return len (meta .FinalizersExcludingPropagation (mg ))
1617- }
0 commit comments