Skip to content

Commit ec3489e

Browse files
committed
fix: clean up related resources when primary object is deleted (#116)
When a primary object is deleted, related resources with origin:kcp are now cleaned up before the primary syncer runs. This ensures finalizers on KCP-side related resources are removed, preventing workspaces from getting stuck in deletion. Changes: - Process related resources before primary deletion in syncer.go - Add forceDelete field to objectSyncer for forced cleanup - Remove early return in handleDeletion that skipped related resources - Pass primaryDeleting flag through related resource processing The e2e test added in the previous commit now passes. Fixes #116 Signed-off-by: Igor Fominykh <ifdotpy@gmail.com>
1 parent 294c759 commit ec3489e

3 files changed

Lines changed: 34 additions & 15 deletions

File tree

internal/sync/object_syncer.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ type objectSyncer struct {
6363
// receive events. Since these objects might be created during the sync,
6464
// they cannot be specified here directly.
6565
eventObjSide syncSideType
66+
// forceDelete triggers the deletion flow even when the source object is not
67+
// being deleted; used to clean up related resources when the primary object
68+
// is being deleted.
69+
forceDelete bool
6670
}
6771

6872
type syncSideType int
@@ -93,8 +97,8 @@ func (s *objectSyncer) recordEvent(ctx context.Context, source, dest syncSide, e
9397
}
9498

9599
func (s *objectSyncer) Sync(ctx context.Context, log *zap.SugaredLogger, source, dest syncSide) (requeue bool, err error) {
96-
// handle deletion: if source object is in deletion, delete the destination object (the clone)
97-
if source.object.GetDeletionTimestamp() != nil {
100+
// handle deletion: if source object is in deletion (or forced), delete the destination object (the clone)
101+
if source.object.GetDeletionTimestamp() != nil || s.forceDelete {
98102
return s.handleDeletion(ctx, log, source, dest)
99103
}
100104

@@ -458,16 +462,9 @@ func (s *objectSyncer) handleDeletion(ctx context.Context, log *zap.SugaredLogge
458462
// if we just removed the finalizer, we can requeue the source object
459463
if updated {
460464
s.recordEvent(ctx, source, dest, corev1.EventTypeNormal, "ObjectDeleted", "Object deletion has been completed, finalizer has been removed.")
461-
return true, nil
462465
}
463466

464-
// For now we do not delete related resources; since after this step the destination object is
465-
// gone already, the remaining syncer logic would fail if it attempts to sync relate objects.
466-
// For the MVP it's fine to just leave related resources around, but in the future this behaviour
467-
// might be configurable per PublishedResource, in which case this `return true` here would need
468-
// to go away and the cleanup in general would need to be rethought a bit (maybe owner refs would
469-
// be a good idea?).
470-
return true, nil
467+
return updated, nil
471468
}
472469

473470
func (s *objectSyncer) removeSubresources(obj *unstructured.Unstructured) *unstructured.Unstructured {

internal/sync/syncer.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,19 @@ func (s *ResourceSyncer) Process(ctx context.Context, remoteObj *unstructured.Un
195195
// (i.e. on the service cluster), so that the original and copy are linked
196196
// together and can be found.
197197
metadataOnDestination: true,
198-
eventObjSide: syncSideSource,
198+
eventObjSide: syncSideSource,
199+
}
200+
201+
// When the primary object is being deleted, clean up related resources FIRST,
202+
// while the local object still exists (needed for reference resolution).
203+
if remoteObj.GetDeletionTimestamp() != nil && destSide.object != nil {
204+
relRequeue, relErr := s.processRelatedResources(ctx, log, stateStore, sourceSide, destSide, true)
205+
if relErr != nil {
206+
return false, fmt.Errorf("failed to clean up related resources during primary deletion: %w", relErr)
207+
}
208+
if relRequeue {
209+
return true, nil
210+
}
199211
}
200212

201213
requeue, err = syncer.Sync(ctx, log, sourceSide, destSide)
@@ -215,7 +227,14 @@ func (s *ResourceSyncer) Process(ctx context.Context, remoteObj *unstructured.Un
215227
// it modifies the state of the world, otherwise the objects in
216228
// source/dest.object might be ouf date.
217229

218-
return s.processRelatedResources(ctx, log, stateStore, sourceSide, destSide)
230+
// Guard: related resource resolution requires both sync sides to exist.
231+
// destSide.object can be nil if the primary was in deletion and the local
232+
// copy was already removed. In that case there is nothing left to sync.
233+
if destSide.object == nil {
234+
return false, nil
235+
}
236+
237+
return s.processRelatedResources(ctx, log, stateStore, sourceSide, destSide, false)
219238
}
220239

221240
func (s *ResourceSyncer) findLocalObject(ctx context.Context, objectKey objectKey) (*unstructured.Unstructured, error) {

internal/sync/syncer_related.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ import (
4040
ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
4141
)
4242

43-
func (s *ResourceSyncer) processRelatedResources(ctx context.Context, log *zap.SugaredLogger, stateStore ObjectStateStore, remote, local syncSide) (requeue bool, err error) {
43+
func (s *ResourceSyncer) processRelatedResources(ctx context.Context, log *zap.SugaredLogger, stateStore ObjectStateStore, remote, local syncSide, primaryDeleting bool) (requeue bool, err error) {
4444
for _, relatedResource := range s.pubRes.Spec.Related {
45-
requeue, err := s.processRelatedResource(ctx, log.With("identifier", relatedResource.Identifier), stateStore, remote, local, relatedResource)
45+
requeue, err := s.processRelatedResource(ctx, log.With("identifier", relatedResource.Identifier), stateStore, remote, local, relatedResource, primaryDeleting)
4646
if err != nil {
4747
return false, fmt.Errorf("failed to process related resource %s: %w", relatedResource.Identifier, err)
4848
}
@@ -62,7 +62,7 @@ type relatedObjectAnnotation struct {
6262
Kind string `json:"kind"`
6363
}
6464

65-
func (s *ResourceSyncer) processRelatedResource(ctx context.Context, log *zap.SugaredLogger, stateStore ObjectStateStore, remote, local syncSide, relRes syncagentv1alpha1.RelatedResourceSpec) (requeue bool, err error) {
65+
func (s *ResourceSyncer) processRelatedResource(ctx context.Context, log *zap.SugaredLogger, stateStore ObjectStateStore, remote, local syncSide, relRes syncagentv1alpha1.RelatedResourceSpec, primaryDeleting bool) (requeue bool, err error) {
6666
// decide what direction to sync (local->remote vs. remote->local)
6767
var (
6868
origin syncSide
@@ -163,6 +163,9 @@ func (s *ResourceSyncer) processRelatedResource(ctx context.Context, log *zap.Su
163163
metadataOnDestination: false,
164164
// events are always created on the kcp side
165165
eventObjSide: eventObjSide,
166+
// force deletion of related resources when the primary object is being deleted
167+
// (only for origin:kcp resources that have blockSourceDeletion)
168+
forceDelete: primaryDeleting && relRes.Origin == "kcp",
166169
}
167170

168171
req, err := syncer.Sync(ctx, log, sourceSide, destSide)

0 commit comments

Comments
 (0)