Skip to content

Commit b663b0b

Browse files
authored
reconciler.Apply: perform deletions late (#174)
1 parent 4de6dd2 commit b663b0b

File tree

1 file changed

+138
-110
lines changed

1 file changed

+138
-110
lines changed

pkg/reconciler/reconciler.go

Lines changed: 138 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj
370370
}
371371
}
372372

373-
// add/update inventory with target objects
373+
// prepare (add/update) new inventory with target objects
374374
// TODO: review this; it would be cleaner to use a DeepCopy method for a []*InventoryItem type (if there would be such a type)
375375
newInventory := slices.Collect(*inventory, func(item *InventoryItem) *InventoryItem { return item.DeepCopy() })
376376
numAdded := 0
@@ -435,7 +435,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj
435435
}
436436
}
437437

438-
// mark obsolete inventory items (clear digest)
438+
// mark obsolete items (clear digest) in new inventory
439439
for _, item := range newInventory {
440440
found := false
441441
for _, object := range objects {
@@ -451,7 +451,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj
451451
}
452452
}
453453

454-
// validate object set:
454+
// validate new inventory:
455455
// - check that all managed instances have apply-order greater than or equal to the according managed type
456456
// - check that all managed instances have delete-order less than or equal to the according managed type
457457
// - check that no managed types are about to be deleted (empty digest) unless all related managed instances are as well
@@ -491,7 +491,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj
491491
}
492492
}
493493

494-
// accept inventory for further processing, put into right order for future deletion
494+
// accept new inventory for further processing, put into right order for future deletion
495495
*inventory = sortObjectsForDelete(newInventory)
496496

497497
// trigger another reconcile if something was added (to be sure that it is persisted)
@@ -504,136 +504,77 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj
504504
// - the persisted inventory at least has the same object keys as the in-memory inventory
505505
// now it is about to synchronize the cluster state with the inventory
506506

507-
// delete redundant objects and maintain inventory;
508-
// objects are deleted in waves according to their delete order;
509-
// that means, only if all redundant objects of a wave are gone or comppleted, the next
510-
// wave will be processed; within each wave, objects which are instances of managed
511-
// types are deleted before all other objects, and namespaces will only be deleted
512-
// if they are not used by any object in the inventory (note that this may cause deadlocks)
513-
numManagedToBeDeleted := 0
514-
numToBeDeleted := 0
515-
for k, item := range *inventory {
516-
// if this is the first object of an order, then
517-
// count instances of managed types in this wave which are about to be deleted
518-
if k == 0 || (*inventory)[k-1].DeleteOrder < item.DeleteOrder {
519-
log.V(2).Info("begin of deletion wave", "order", item.DeleteOrder)
520-
numManagedToBeDeleted = 0
521-
for j := k; j < len(*inventory) && (*inventory)[j].DeleteOrder == item.DeleteOrder; j++ {
522-
_item := (*inventory)[j]
523-
if (_item.Phase == PhaseScheduledForDeletion || _item.Phase == PhaseScheduledForCompletion || _item.Phase == PhaseDeleting || _item.Phase == PhaseCompleting) && isInstanceOfManagedType(*inventory, _item) {
524-
numManagedToBeDeleted++
507+
// note: after this point, it is also guaranteed that objects is contained in the persisted inventory;
508+
// the inventory therefore consists of two parts:
509+
// - items which are contained in objects
510+
// these items can have one of the following phases:
511+
// - PhaseScheduledForApplication
512+
// - PhaseCreating
513+
// - PhaseUpdating
514+
// - PhaseReady
515+
// - PhaseScheduledForCompletion
516+
// - PhaseCompleting
517+
// - PhaseCompleted
518+
// - items which are not contained in objects
519+
// their phase is one of the following:
520+
// - PhaseScheduledForDeletion
521+
// - PhaseDeleting
522+
523+
// create missing namespaces
524+
if r.createMissingNamespaces {
525+
for _, namespace := range findMissingNamespaces(objects) {
526+
if err := r.client.Get(ctx, apitypes.NamespacedName{Name: namespace}, &corev1.Namespace{}); err != nil {
527+
if !apierrors.IsNotFound(err) {
528+
return false, errors.Wrapf(err, "error reading namespace %s", namespace)
529+
}
530+
if err := r.client.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, client.FieldOwner(r.name)); err != nil {
531+
return false, errors.Wrapf(err, "error creating namespace %s", namespace)
525532
}
526533
}
527534
}
535+
}
528536

529-
if item.Phase == PhaseScheduledForDeletion || item.Phase == PhaseScheduledForCompletion || item.Phase == PhaseDeleting || item.Phase == PhaseCompleting {
530-
// fetch object (if existing)
537+
// put objects into right order for applying
538+
objects = sortObjectsForApply(objects, getApplyOrder)
539+
540+
// finish due completions
541+
// note that completions do not honor delete-order or delete-policy
542+
// however, due to the way how PhaseScheduledForCompletion is set, the affected objects will
543+
// always be in one and the same apply order
544+
// in addition deletions are triggered in the canonical deletion order (but not waited for)
545+
numToBeCompleted := 0
546+
for _, item := range *inventory {
547+
if item.Phase == PhaseScheduledForCompletion || item.Phase == PhaseCompleting {
531548
existingObject, err := r.readObject(ctx, item)
532549
if err != nil {
533550
return false, errors.Wrapf(err, "error reading object %s", item)
534551
}
535552

536-
orphan := item.DeletePolicy == DeletePolicyOrphan
537-
538553
switch item.Phase {
539-
case PhaseScheduledForDeletion:
540-
// delete namespaces after all contained inventory items
541-
// delete all instances of managed types before remaining objects; this ensures that no objects are prematurely
542-
// deleted which are needed for the deletion of the managed instances, such as webhook servers, api servers, ...
543-
if (!isNamespace(item) || !isNamespaceUsed(*inventory, item.Name)) && (numManagedToBeDeleted == 0 || isInstanceOfManagedType(*inventory, item)) {
544-
if orphan {
545-
item.Phase = ""
546-
} else {
547-
// note: here is a theoretical risk that we delete an existing foreign object, because informers are not yet synced
548-
// however not sending the delete request is also not an option, because this might lead to orphaned own dependents
549-
// TODO: perform an additional owner id check
550-
if err := r.deleteObject(ctx, item, existingObject); err != nil {
551-
return false, errors.Wrapf(err, "error deleting object %s", item)
552-
}
553-
item.Phase = PhaseDeleting
554-
item.Status = status.TerminatingStatus
555-
numToBeDeleted++
556-
}
557-
} else {
558-
numToBeDeleted++
559-
}
560554
case PhaseScheduledForCompletion:
561-
// delete namespaces after all contained inventory items
562-
// delete all instances of managed types before remaining objects; this ensures that no objects are prematurely
563-
// deleted which are needed for the deletion of the managed instances, such as webhook servers, api servers, ...
564-
if (!isNamespace(item) || !isNamespaceUsed(*inventory, item.Name)) && (numManagedToBeDeleted == 0 || isInstanceOfManagedType(*inventory, item)) {
565-
if orphan {
566-
return false, fmt.Errorf("invalid usage of deletion policy: object %s is scheduled for completion and therefore cannot be orphaned", item)
567-
} else {
568-
// note: here is a theoretical risk that we delete an existing foreign object, because informers are not yet synced
569-
// however not sending the delete request is also not an option, because this might lead to orphaned own dependents
570-
// TODO: perform an additional owner id check
571-
if err := r.deleteObject(ctx, item, existingObject); err != nil {
572-
return false, errors.Wrapf(err, "error deleting object %s", item)
573-
}
574-
item.Phase = PhaseCompleting
575-
item.Status = status.TerminatingStatus
576-
numToBeDeleted++
577-
}
578-
} else {
579-
numToBeDeleted++
580-
}
581-
case PhaseDeleting:
582-
// if object is gone, we can remove it from inventory
583-
if existingObject == nil {
584-
item.Phase = ""
585-
} else {
586-
numToBeDeleted++
555+
if err := r.deleteObject(ctx, item, existingObject); err != nil {
556+
return false, errors.Wrapf(err, "error deleting object %s", item)
587557
}
558+
item.Phase = PhaseCompleting
559+
item.Status = status.TerminatingStatus
560+
numToBeCompleted++
588561
case PhaseCompleting:
589-
// if object is gone, it is set to completed, and kept in inventory
590562
if existingObject == nil {
591563
item.Phase = PhaseCompleted
592564
item.Status = ""
593565
} else {
594-
numToBeDeleted++
566+
numToBeCompleted++
595567
}
596-
default:
597-
// note: any other phase value would indicate a severe code problem, so we want to see the panic in that case
598-
panic("this cannot happen")
599-
}
600-
}
601-
602-
// trigger another reconcile if this is the last object of the wave, and some deletions are not yet completed
603-
if k == len(*inventory)-1 || (*inventory)[k+1].DeleteOrder > item.DeleteOrder {
604-
log.V(2).Info("end of deletion wave", "order", item.DeleteOrder)
605-
if numToBeDeleted > 0 {
606-
break
607568
}
608569
}
609570
}
610571

611-
*inventory = slices.Select(*inventory, func(item *InventoryItem) bool { return item.Phase != "" })
612-
613-
// trigger another reconcile
614-
if numToBeDeleted > 0 {
572+
// trigger another reconcile if any to-be-completed objects are left
573+
if numToBeCompleted > 0 {
615574
return false, nil
616575
}
617576

618-
// note: after this point, PhaseScheduledForDeletion, PhaseScheduledForCompletion, PhaseDeleting, PhaseCompleting cannot occur anymore in inventory
619-
// in other words: the inventory and objects contains the same resources
620-
621-
// create missing namespaces
622-
if r.createMissingNamespaces {
623-
for _, namespace := range findMissingNamespaces(objects) {
624-
if err := r.client.Get(ctx, apitypes.NamespacedName{Name: namespace}, &corev1.Namespace{}); err != nil {
625-
if !apierrors.IsNotFound(err) {
626-
return false, errors.Wrapf(err, "error reading namespace %s", namespace)
627-
}
628-
if err := r.client.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, client.FieldOwner(r.name)); err != nil {
629-
return false, errors.Wrapf(err, "error creating namespace %s", namespace)
630-
}
631-
}
632-
}
633-
}
634-
635-
// put objects into right order for applying
636-
objects = sortObjectsForApply(objects, getApplyOrder)
577+
// note: after this point, PhaseScheduledForCompletion, PhaseCompleting cannot occur anymore in inventory
637578

638579
// apply objects and maintain inventory;
639580
// objects are applied (i.e. created/updated) in waves according to their apply order;
@@ -748,7 +689,94 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj
748689
}
749690
}
750691

751-
return numUnready == 0, nil
692+
// trigger another reconcile if any unready objects are left
693+
if numUnready > 0 {
694+
return false, nil
695+
}
696+
697+
// delete redundant objects and maintain inventory;
698+
// objects are deleted in waves according to their delete order;
699+
// that means, only if all redundant objects of a wave are gone , the next
700+
// wave will be processed; within each wave, objects which are instances of managed
701+
// types are deleted before all other objects, and namespaces will only be deleted
702+
// if they are not used by any object in the inventory (note that this may cause deadlocks)
703+
numManagedToBeDeleted := 0
704+
numToBeDeleted := 0
705+
for k, item := range *inventory {
706+
// if this is the first object of an order, then
707+
// count instances of managed types in this wave which are about to be deleted
708+
if k == 0 || (*inventory)[k-1].DeleteOrder < item.DeleteOrder {
709+
log.V(2).Info("begin of deletion wave", "order", item.DeleteOrder)
710+
numManagedToBeDeleted = 0
711+
for j := k; j < len(*inventory) && (*inventory)[j].DeleteOrder == item.DeleteOrder; j++ {
712+
_item := (*inventory)[j]
713+
if (_item.Phase == PhaseScheduledForDeletion || _item.Phase == PhaseDeleting) && isInstanceOfManagedType(*inventory, _item) {
714+
numManagedToBeDeleted++
715+
}
716+
}
717+
}
718+
719+
if item.Phase == PhaseScheduledForDeletion || item.Phase == PhaseDeleting {
720+
// fetch object (if existing)
721+
existingObject, err := r.readObject(ctx, item)
722+
if err != nil {
723+
return false, errors.Wrapf(err, "error reading object %s", item)
724+
}
725+
726+
orphan := item.DeletePolicy == DeletePolicyOrphan
727+
728+
switch item.Phase {
729+
case PhaseScheduledForDeletion:
730+
// delete namespaces after all contained inventory items
731+
// delete all instances of managed types before remaining objects; this ensures that no objects are prematurely
732+
// deleted which are needed for the deletion of the managed instances, such as webhook servers, api servers, ...
733+
if (!isNamespace(item) || !isNamespaceUsed(*inventory, item.Name)) && (numManagedToBeDeleted == 0 || isInstanceOfManagedType(*inventory, item)) {
734+
if orphan {
735+
item.Phase = ""
736+
} else {
737+
// note: here is a theoretical risk that we delete an existing foreign object, because informers are not yet synced
738+
// however not sending the delete request is also not an option, because this might lead to orphaned own dependents
739+
// TODO: perform an additional owner id check
740+
if err := r.deleteObject(ctx, item, existingObject); err != nil {
741+
return false, errors.Wrapf(err, "error deleting object %s", item)
742+
}
743+
item.Phase = PhaseDeleting
744+
item.Status = status.TerminatingStatus
745+
numToBeDeleted++
746+
}
747+
} else {
748+
numToBeDeleted++
749+
}
750+
case PhaseDeleting:
751+
// if object is gone, we can remove it from inventory
752+
if existingObject == nil {
753+
item.Phase = ""
754+
} else {
755+
numToBeDeleted++
756+
}
757+
default:
758+
// note: any other phase value would indicate a severe code problem, so we want to see the panic in that case
759+
panic("this cannot happen")
760+
}
761+
}
762+
763+
// trigger another reconcile if this is the last object of the wave, and some deletions are not yet finished
764+
if k == len(*inventory)-1 || (*inventory)[k+1].DeleteOrder > item.DeleteOrder {
765+
log.V(2).Info("end of deletion wave", "order", item.DeleteOrder)
766+
if numToBeDeleted > 0 {
767+
break
768+
}
769+
}
770+
}
771+
772+
*inventory = slices.Select(*inventory, func(item *InventoryItem) bool { return item.Phase != "" })
773+
774+
// trigger another reconcile if any to-be-deleted objects are left
775+
if numToBeDeleted > 0 {
776+
return false, nil
777+
}
778+
779+
return true, nil
752780
}
753781

754782
// Delete objects stored in the inventory from the target cluster and maintain inventory.

0 commit comments

Comments
 (0)