Skip to content

Commit b56489c

Browse files
committed
Move the ProviderConfigUsage finalizer into the managed reconciler
Address review feedback on the deletion-protection change: - Make the usage cleaner a ReconcilerOption, WithProviderConfigUsageCleaner, defaulting to a no-op, instead of a required NewReconciler parameter. - Stop adding ProviderConfigUsageFinalizer in Track. The reconciler now owns both ends of the finalizer: Protect adds it after Connect and Untrack removes it after teardown, so a provider that does not wire the cleaner never gets a finalizer nothing will remove. Track preserves whatever finalizers the usage already carries when it updates the reference. - Rename FinalizersExcludingPropagation to NonGCFinalizers and name the two garbage collector finalizers it excludes. - Emit warning events when protecting or releasing a usage fails. - Release the usage finalizer before deleting a usage that has no owner reference, so it cannot be left terminating forever. - Drop the numControllerFinalizers helper. Signed-off-by: ezgidemirel <ezgidemirel91@gmail.com>
1 parent 34d7896 commit b56489c

9 files changed

Lines changed: 472 additions & 104 deletions

File tree

pkg/meta/meta.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,11 @@ 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 {
208+
// NonGCFinalizers returns the supplied object's finalizers, excluding the two
209+
// the Kubernetes garbage collector uses to implement deletion propagation -
210+
// foregroundDeletion and orphan. Neither belongs to a controller, and neither
211+
// makes a claim on an external system.
212+
func NonGCFinalizers(o metav1.Object) []string {
211213
f := o.GetFinalizers()
212214
out := make([]string, 0, len(f))
213215

pkg/meta/meta_test.go

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -605,31 +605,45 @@ func TestFinalizerExists(t *testing.T) {
605605
}
606606
}
607607

608-
func TestFinalizersExcludingPropagation(t *testing.T) {
608+
func TestNonGCFinalizers(t *testing.T) {
609+
type args struct {
610+
o metav1.Object
611+
}
612+
609613
cases := map[string]struct {
610614
reason string
611-
o metav1.Object
615+
args args
612616
want []string
613617
}{
614618
"NoFinalizers": {
615-
reason: "An object without finalizers has no finalizers after propagation finalizers are excluded.",
616-
o: &corev1.Pod{},
619+
reason: "An object without finalizers has no finalizers after garbage collector finalizers are excluded.",
620+
args: args{o: &corev1.Pod{}},
617621
want: []string{},
618622
},
619-
"OnlyPropagationFinalizers": {
620-
reason: "Kubernetes foreground and orphan propagation finalizers must both be excluded.",
621-
o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{
623+
"OnlyGCFinalizers": {
624+
reason: "The foregroundDeletion and orphan finalizers must both be excluded.",
625+
args: args{o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{
622626
metav1.FinalizerDeleteDependents,
623627
metav1.FinalizerOrphanDependents,
624-
}}},
628+
}}}},
625629
want: []string{},
626630
},
631+
"KeepsControllerFinalizers": {
632+
reason: "Finalizers that don't belong to the garbage collector must be kept, in order.",
633+
args: args{o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{
634+
metav1.FinalizerDeleteDependents,
635+
"example.org/first",
636+
metav1.FinalizerOrphanDependents,
637+
"example.org/second",
638+
}}}},
639+
want: []string{"example.org/first", "example.org/second"},
640+
},
627641
}
628642

629643
for name, tc := range cases {
630644
t.Run(name, func(t *testing.T) {
631-
if diff := cmp.Diff(tc.want, FinalizersExcludingPropagation(tc.o)); diff != "" {
632-
t.Errorf("%s\nFinalizersExcludingPropagation(...): -want, +got:\n%s", tc.reason, diff)
645+
if diff := cmp.Diff(tc.want, NonGCFinalizers(tc.args.o)); diff != "" {
646+
t.Errorf("%s\nNonGCFinalizers(...): -want, +got:\n%s", tc.reason, diff)
633647
}
634648
})
635649
}

pkg/reconciler/managed/reconciler.go

Lines changed: 53 additions & 20 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+
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.
810836
func 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-
}

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, resource.NewNopProviderConfigUsageCleaner(), tc.args.o...)
2168+
r := NewReconciler(tc.args.m, tc.args.mg, 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, resource.NewNopProviderConfigUsageCleaner(), tc.args.o...)
2889+
r := NewReconciler(tc.args.m, tc.args.mg, 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 != "" {

0 commit comments

Comments
 (0)