@@ -43,11 +43,22 @@ import (
4343
4444var _ reconcile.Reconciler = & Reconciler {}
4545
46+ func TestNewReconcilerRequiresProviderConfigUsageCleaner (t * testing.T ) {
47+ defer func () {
48+ if recover () == nil {
49+ t .Error ("NewReconciler(...): expected a panic when the ProviderConfigUsageCleaner is nil" )
50+ }
51+ }()
52+
53+ NewReconciler (nil , resource.ManagedKind {}, nil )
54+ }
55+
4656func TestModernReconciler (t * testing.T ) {
4757 type args struct {
48- m manager.Manager
49- mg resource.ManagedKind
50- o []ReconcilerOption
58+ m manager.Manager
59+ mg resource.ManagedKind
60+ usage resource.ProviderConfigUsageCleaner
61+ o []ReconcilerOption
5162 }
5263
5364 type want struct {
@@ -58,6 +69,7 @@ func TestModernReconciler(t *testing.T) {
5869
5970 errBoom := errors .New ("boom" )
6071 now := metav1 .Now ()
72+ finalizerRemoved := false
6173
6274 cases := map [string ]struct {
6375 reason string
@@ -180,9 +192,18 @@ func TestModernReconciler(t *testing.T) {
180192 Scheme : fake .SchemeWith (& fake.ModernManaged {}),
181193 },
182194 mg : resource .ManagedKind (fake .GVK (& fake.ModernManaged {})),
195+ usage : resource .ProviderConfigUsageCleanerFn (func (_ context.Context , _ resource.Managed ) error {
196+ if ! finalizerRemoved {
197+ t .Error ("ProviderConfigUsage released before managed resource finalizer was removed" )
198+ }
199+ return nil
200+ }),
183201 o : []ReconcilerOption {
184202 WithManagementPolicies (),
185- WithFinalizer (resource.FinalizerFns {RemoveFinalizerFn : func (_ context.Context , _ resource.Object ) error { return nil }}),
203+ WithFinalizer (resource.FinalizerFns {RemoveFinalizerFn : func (_ context.Context , _ resource.Object ) error {
204+ finalizerRemoved = true
205+ return nil
206+ }}),
186207 },
187208 },
188209 want : want {result : reconcile.Result {Requeue : false }},
@@ -2171,7 +2192,11 @@ func TestModernReconciler(t *testing.T) {
21712192
21722193 for name , tc := range cases {
21732194 t .Run (name , func (t * testing.T ) {
2174- r := NewReconciler (tc .args .m , tc .args .mg , tc .args .o ... )
2195+ usage := tc .args .usage
2196+ if usage == nil {
2197+ usage = resource .NewNopProviderConfigUsageCleaner ()
2198+ }
2199+ r := NewReconciler (tc .args .m , tc .args .mg , usage , tc .args .o ... )
21752200
21762201 got , err := r .Reconcile (context .Background (), reconcile.Request {})
21772202 if diff := cmp .Diff (tc .want .err , err , test .EquateErrors ()); diff != "" {
@@ -2837,7 +2862,7 @@ func TestReconcilerChangeLogs(t *testing.T) {
28372862 for name , tc := range cases {
28382863 t .Run (name , func (t * testing.T ) {
28392864 tc .args .o = append (tc .args .o , WithChangeLogger (NewGRPCChangeLogger (tc .args .c , WithProviderVersion ("provider-cool:v9.99.999" ))))
2840- r := NewReconciler (tc .args .m , tc .args .mg , tc .args .o ... )
2865+ r := NewReconciler (tc .args .m , tc .args .mg , resource . NewNopProviderConfigUsageCleaner (), tc .args .o ... )
28412866 r .Reconcile (context .Background (), reconcile.Request {})
28422867
28432868 if diff := cmp .Diff (tc .want .callCount , len (tc .args .c .requests )); diff != "" {
@@ -2981,7 +3006,7 @@ func TestReconcilePollIntervalAnnotation(t *testing.T) {
29813006 }),
29823007 },
29833008 Scheme : fake .SchemeWith (& fake.ModernManaged {}),
2984- }, resource .ManagedKind (fake .GVK (& fake.ModernManaged {})),
3009+ }, resource .ManagedKind (fake .GVK (& fake.ModernManaged {})), resource . NewNopProviderConfigUsageCleaner (),
29853010 WithPollInterval (tc .pollInterval ),
29863011 WithMinPollInterval (tc .minPollInterval ),
29873012 WithInitializers (),
@@ -3070,7 +3095,7 @@ func TestReconcileRequestAnnotation(t *testing.T) {
30703095 }),
30713096 },
30723097 Scheme : fake .SchemeWith (& fake.ModernManaged {}),
3073- }, resource .ManagedKind (fake .GVK (& fake.ModernManaged {})),
3098+ }, resource .ManagedKind (fake .GVK (& fake.ModernManaged {})), resource . NewNopProviderConfigUsageCleaner (),
30743099 WithInitializers (),
30753100 WithReferenceResolver (ReferenceResolverFn (func (_ context.Context , _ resource.Managed ) error { return nil })),
30763101 WithExternalConnector (ExternalConnectorFn (func (_ context.Context , _ resource.Managed ) (ExternalClient , error ) {
@@ -3115,3 +3140,89 @@ func modernManagedMockGetFn(err error, generation int64) test.MockGetFn {
31153140 return nil
31163141 })
31173142}
3143+
3144+ // TestReconcilePropagationFinalizersDoNotDelayDelete ensures Kubernetes
3145+ // propagation finalizers do not prevent external deletion.
3146+ func TestReconcilePropagationFinalizersDoNotDelayDelete (t * testing.T ) {
3147+ errBoom := errors .New ("boom" )
3148+ now := metav1 .Now ()
3149+
3150+ cases := map [string ]struct {
3151+ reason string
3152+ finalizers []string
3153+ wantDeleteCalled bool
3154+ }{
3155+ "ForegroundDeletion" : {
3156+ reason : "Kubernetes' foregroundDeletion finalizer must not delay the external delete." ,
3157+ finalizers : []string {FinalizerName , metav1 .FinalizerDeleteDependents },
3158+ wantDeleteCalled : true ,
3159+ },
3160+ "OrphanDependents" : {
3161+ reason : "Kubernetes' orphan finalizer must not delay the external delete." ,
3162+ finalizers : []string {FinalizerName , metav1 .FinalizerOrphanDependents },
3163+ wantDeleteCalled : true ,
3164+ },
3165+ "BothPropagationFinalizers" : {
3166+ reason : "Neither propagation finalizer must delay the external delete." ,
3167+ finalizers : []string {FinalizerName , metav1 .FinalizerDeleteDependents , metav1 .FinalizerOrphanDependents },
3168+ wantDeleteCalled : true ,
3169+ },
3170+ "ForeignFinalizer" : {
3171+ reason : "A finalizer belonging to another controller must still delay the external delete." ,
3172+ finalizers : []string {FinalizerName , "example.org/another-controller" },
3173+ wantDeleteCalled : false ,
3174+ },
3175+ "ForeignFinalizerAlongsidePropagation" : {
3176+ reason : "A foreign finalizer must delay the external delete even when a propagation finalizer is also present." ,
3177+ finalizers : []string {FinalizerName , metav1 .FinalizerDeleteDependents , "example.org/another-controller" },
3178+ wantDeleteCalled : false ,
3179+ },
3180+ }
3181+
3182+ for name , tc := range cases {
3183+ t .Run (name , func (t * testing.T ) {
3184+ deleteCalled := false
3185+
3186+ m := & fake.Manager {
3187+ Client : & test.MockClient {
3188+ MockGet : test .NewMockGetFn (nil , func (obj client.Object ) error {
3189+ mg := asModernManaged (obj , 42 )
3190+ mg .SetDeletionTimestamp (& now )
3191+ mg .SetFinalizers (tc .finalizers )
3192+
3193+ return nil
3194+ }),
3195+ MockUpdate : test .NewMockUpdateFn (nil ),
3196+ MockStatusUpdate : test .NewMockSubResourceUpdateFn (nil ),
3197+ },
3198+ Scheme : fake .SchemeWith (& fake.ModernManaged {}),
3199+ }
3200+
3201+ r := NewReconciler (m , resource .ManagedKind (fake .GVK (& fake.ModernManaged {})), resource .NewNopProviderConfigUsageCleaner (),
3202+ WithInitializers (),
3203+ WithReferenceResolver (ReferenceResolverFn (func (_ context.Context , _ resource.Managed ) error { return nil })),
3204+ WithExternalConnector (ExternalConnectorFn (func (_ context.Context , _ resource.Managed ) (ExternalClient , error ) {
3205+ return & ExternalClientFns {
3206+ ObserveFn : func (_ context.Context , _ resource.Managed ) (ExternalObservation , error ) {
3207+ return ExternalObservation {ResourceExists : true }, nil
3208+ },
3209+ // Stop after recording whether Delete was called.
3210+ DeleteFn : func (_ context.Context , _ resource.Managed ) (ExternalDelete , error ) {
3211+ deleteCalled = true
3212+ return ExternalDelete {}, errBoom
3213+ },
3214+ DisconnectFn : func (_ context.Context ) error { return nil },
3215+ }, nil
3216+ })),
3217+ )
3218+
3219+ if _ , err := r .Reconcile (context .Background (), reconcile.Request {}); err != nil {
3220+ t .Errorf ("%s\n r.Reconcile(...): unexpected error: %v" , tc .reason , err )
3221+ }
3222+
3223+ if diff := cmp .Diff (tc .wantDeleteCalled , deleteCalled ); diff != "" {
3224+ t .Errorf ("%s\n r.Reconcile(...): -want external Delete called, +got:\n %s" , tc .reason , diff )
3225+ }
3226+ })
3227+ }
3228+ }
0 commit comments