Skip to content

Commit d77b677

Browse files
committed
Protect ProviderConfigs while managed resources terminate
Keep ProviderConfigUsages alive until their managed resources finish teardown, ignore Kubernetes propagation finalizers during external deletion, and reap terminating usages whose owners are gone or have completed teardown. Signed-off-by: ezgidemirel <ezgidemirel91@gmail.com>
1 parent 1280e79 commit d77b677

9 files changed

Lines changed: 868 additions & 33 deletions

File tree

pkg/meta/meta.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,23 @@ func FinalizerExists(o metav1.Object, finalizer string) bool {
205205
return slices.Contains(f, finalizer)
206206
}
207207

208+
// FinalizersExcludingPropagation returns the supplied object's finalizers
209+
// without Kubernetes deletion propagation finalizers.
210+
func FinalizersExcludingPropagation(o metav1.Object) []string {
211+
f := o.GetFinalizers()
212+
out := make([]string, 0, len(f))
213+
214+
for _, e := range f {
215+
if e == metav1.FinalizerDeleteDependents || e == metav1.FinalizerOrphanDependents {
216+
continue
217+
}
218+
219+
out = append(out, e)
220+
}
221+
222+
return out
223+
}
224+
208225
// AddLabels to the supplied object.
209226
func AddLabels(o metav1.Object, labels map[string]string) {
210227
l := o.GetLabels()

pkg/reconciler/managed/reconciler.go

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const (
6767
errReconcileUpdate = "update failed"
6868
errReconcileDelete = "delete failed"
6969
errRecordChangeLog = "cannot record change log entry"
70+
errUntrackProviderConfig = "cannot release ProviderConfigUsage"
7071

7172
errExternalResourceNotExist = "external resource does not exist"
7273

@@ -615,6 +616,7 @@ type mrManaged struct {
615616
Initializer
616617
ReferenceResolver
617618
LocalConnectionPublisher
619+
resource.ProviderConfigUsageCleaner
618620
}
619621

620622
func defaultMRManaged(m manager.Manager) mrManaged {
@@ -876,12 +878,18 @@ func (r *Reconciler) effectivePollInterval(o metav1.Object) time.Duration {
876878

877879
// NewReconciler returns a Reconciler that reconciles managed resources of the
878880
// supplied ManagedKind with resources in an external system such as a cloud
879-
// provider API. It panics if asked to reconcile a managed resource kind that is
880-
// not registered with the supplied manager's runtime.Scheme. The returned
881-
// Reconciler reconciles with a dummy, no-op 'external system' by default;
882-
// callers should supply an ExternalConnector that returns an ExternalClient
883-
// capable of managing resources in a real system.
884-
func NewReconciler(m manager.Manager, of resource.ManagedKind, o ...ReconcilerOption) *Reconciler {
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+
885893
nm := func() resource.Managed {
886894
//nolint:forcetypeassert // If this isn't an MR it's a programming error and we want to panic.
887895
return resource.MustCreateObject(schema.GroupVersionKind(of), m.GetScheme()).(resource.Managed)
@@ -907,6 +915,7 @@ func NewReconciler(m manager.Manager, of resource.ManagedKind, o ...ReconcilerOp
907915
change: newNopChangeLogger(),
908916
conditions: new(conditions.ObservedGenerationPropagationManager),
909917
}
918+
r.managed.ProviderConfigUsageCleaner = usage
910919

911920
for _, ro := range o {
912921
ro(r)
@@ -1070,6 +1079,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
10701079
return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus)
10711080
}
10721081

1082+
// Release the usage after removing the managed resource finalizer so the
1083+
// ProviderConfig remains available throughout deletion.
1084+
if err := r.managed.Untrack(ctx, managed); err != nil {
1085+
log.Debug("Cannot release ProviderConfigUsage", "error", err)
1086+
status.MarkConditions(xpv2.Deleting(), xpv2.ReconcileError(err))
1087+
return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUntrackProviderConfig)
1088+
}
1089+
10731090
// We've successfully unpublished our managed resource's connection
10741091
// details and removed our finalizer. If we assume we were the only
10751092
// controller that added a finalizer to this resource then it should no
@@ -1231,7 +1248,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
12311248
if meta.WasDeleted(managed) {
12321249
log = log.WithValues("deletion-timestamp", managed.GetDeletionTimestamp())
12331250

1234-
if len(managed.GetFinalizers()) > 1 {
1251+
if numControllerFinalizers(managed) > 1 {
12351252
// There are other controllers monitoring this resource so preserve the external instance
12361253
// until all other finalizers have been removed
12371254
log.Debug("Delay external deletion until all finalizers have been removed")
@@ -1311,6 +1328,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
13111328
return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUpdateManagedStatus)
13121329
}
13131330

1331+
// Release the usage after removing the managed resource finalizer so the
1332+
// ProviderConfig remains available throughout deletion.
1333+
if err := r.managed.Untrack(ctx, managed); err != nil {
1334+
log.Debug("Cannot release ProviderConfigUsage", "error", err)
1335+
status.MarkConditions(xpv2.Deleting(), xpv2.ReconcileError(err))
1336+
return reconcile.Result{Requeue: true}, errors.Wrap(updateStatus(), errUntrackProviderConfig)
1337+
}
1338+
13141339
// We've successfully deleted our external resource (if necessary) and
13151340
// removed our finalizer. If we assume we were the only controller that
13161341
// added a finalizer to this resource then it should no longer exist and
@@ -1576,3 +1601,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu
15761601

15771602
return reconcile.Result{RequeueAfter: reconcileAfter}, errors.Wrap(updateStatus(), errUpdateManagedStatus)
15781603
}
1604+
1605+
// numControllerFinalizers returns the number of finalizers not managed by the
1606+
// Kubernetes garbage collector.
1607+
func numControllerFinalizers(mg resource.Managed) int {
1608+
return len(meta.FinalizersExcludingPropagation(mg))
1609+
}

pkg/reconciler/managed/reconciler_legacy_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2165,7 +2165,7 @@ func TestReconciler(t *testing.T) {
21652165

21662166
for name, tc := range cases {
21672167
t.Run(name, func(t *testing.T) {
2168-
r := NewReconciler(tc.args.m, tc.args.mg, tc.args.o...)
2168+
r := NewReconciler(tc.args.m, tc.args.mg, resource.NewNopProviderConfigUsageCleaner(), tc.args.o...)
21692169

21702170
got, err := r.Reconcile(context.Background(), reconcile.Request{})
21712171
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
@@ -2886,7 +2886,7 @@ func TestLegacyReconcilerChangeLogs(t *testing.T) {
28862886
for name, tc := range cases {
28872887
t.Run(name, func(t *testing.T) {
28882888
tc.args.o = append(tc.args.o, WithChangeLogger(NewGRPCChangeLogger(tc.args.c, WithProviderVersion("provider-cool:v9.99.999"))))
2889-
r := NewReconciler(tc.args.m, tc.args.mg, tc.args.o...)
2889+
r := NewReconciler(tc.args.m, tc.args.mg, resource.NewNopProviderConfigUsageCleaner(), tc.args.o...)
28902890
r.Reconcile(context.Background(), reconcile.Request{})
28912891

28922892
if diff := cmp.Diff(tc.want.callCount, len(tc.args.c.requests)); diff != "" {

pkg/reconciler/managed/reconciler_modern_test.go

Lines changed: 119 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,22 @@ import (
4343

4444
var _ 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+
4656
func 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\nr.Reconcile(...): unexpected error: %v", tc.reason, err)
3221+
}
3222+
3223+
if diff := cmp.Diff(tc.wantDeleteCalled, deleteCalled); diff != "" {
3224+
t.Errorf("%s\nr.Reconcile(...): -want external Delete called, +got:\n%s", tc.reason, diff)
3225+
}
3226+
})
3227+
}
3228+
}

0 commit comments

Comments
 (0)