Skip to content
This repository was archived by the owner on Oct 6, 2025. It is now read-only.

Commit b2cf352

Browse files
authored
fix(hooks): always remove finalizers (#762)
Signed-off-by: Alexandre Gaudreault <[email protected]>
1 parent d13ca35 commit b2cf352

File tree

2 files changed

+69
-7
lines changed

2 files changed

+69
-7
lines changed

pkg/sync/sync_context.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1367,6 +1367,31 @@ func (sc *syncContext) runTasks(tasks syncTasks, dryRun bool) runState {
13671367
createTasks = append(createTasks, task)
13681368
}
13691369
}
1370+
1371+
// remove finalizers from previous sync on existing hooks to make sure the operation is idempotent
1372+
{
1373+
ss := newStateSync(state)
1374+
existingHooks := tasks.Filter(func(t *syncTask) bool { return t.isHook() && t.pending() && t.liveObj != nil })
1375+
for _, task := range existingHooks {
1376+
t := task
1377+
ss.Go(func(state runState) runState {
1378+
logCtx := sc.log.WithValues("dryRun", dryRun, "task", t)
1379+
logCtx.V(1).Info("Removing finalizers")
1380+
if !dryRun {
1381+
if err := sc.removeHookFinalizer(t); err != nil {
1382+
state = failed
1383+
sc.setResourceResult(t, t.syncStatus, common.OperationError, fmt.Sprintf("failed to remove hook finalizer: %v", err))
1384+
}
1385+
}
1386+
return state
1387+
})
1388+
}
1389+
state = ss.Wait()
1390+
}
1391+
if state != successful {
1392+
return state
1393+
}
1394+
13701395
// prune first
13711396
{
13721397
if !sc.pruneConfirmed {
@@ -1418,15 +1443,19 @@ func (sc *syncContext) runTasks(tasks syncTasks, dryRun bool) runState {
14181443
for _, task := range hooksPendingDeletion {
14191444
t := task
14201445
ss.Go(func(state runState) runState {
1421-
sc.log.WithValues("dryRun", dryRun, "task", t).V(1).Info("Deleting")
1446+
log := sc.log.WithValues("dryRun", dryRun, "task", t).V(1)
1447+
log.Info("Deleting")
14221448
if !dryRun {
14231449
err := sc.deleteResource(t)
14241450
if err != nil {
14251451
// it is possible to get a race condition here, such that the resource does not exist when
1426-
// delete is requested, we treat this as a nop
1452+
// delete is requested, we treat this as a nopand remove the liveObj
14271453
if !apierrors.IsNotFound(err) {
14281454
state = failed
1429-
sc.setResourceResult(t, "", common.OperationError, fmt.Sprintf("failed to delete resource: %v", err))
1455+
sc.setResourceResult(t, t.syncStatus, common.OperationError, fmt.Sprintf("failed to delete resource: %v", err))
1456+
} else {
1457+
log.Info("Resource not found, treating as no-op and removing liveObj")
1458+
t.liveObj = nil
14301459
}
14311460
} else {
14321461
// if there is anything that needs deleting, we are at best now in pending and

pkg/sync/sync_context_test.go

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,15 @@ func newTestSyncCtx(getResourceFunc *func(ctx context.Context, config *rest.Conf
4646
&metav1.APIResourceList{
4747
GroupVersion: "v1",
4848
APIResources: []metav1.APIResource{
49-
{Kind: "Pod", Group: "", Version: "v1", Namespaced: true, Verbs: standardVerbs},
50-
{Kind: "Service", Group: "", Version: "v1", Namespaced: true, Verbs: standardVerbs},
51-
{Kind: "Namespace", Group: "", Version: "v1", Namespaced: false, Verbs: standardVerbs},
49+
{Name: "pods", Kind: "Pod", Group: "", Version: "v1", Namespaced: true, Verbs: standardVerbs},
50+
{Name: "services", Kind: "Service", Group: "", Version: "v1", Namespaced: true, Verbs: standardVerbs},
51+
{Name: "namespaces", Kind: "Namespace", Group: "", Version: "v1", Namespaced: false, Verbs: standardVerbs},
5252
},
5353
},
5454
&metav1.APIResourceList{
5555
GroupVersion: "apps/v1",
5656
APIResources: []metav1.APIResource{
57-
{Kind: "Deployment", Group: "apps", Version: "v1", Namespaced: true, Verbs: standardVerbs},
57+
{Name: "deployments", Kind: "Deployment", Group: "apps", Version: "v1", Namespaced: true, Verbs: standardVerbs},
5858
},
5959
})
6060
sc := syncContext{
@@ -854,6 +854,39 @@ func withReplaceAndServerSideApplyAnnotations(un *unstructured.Unstructured) *un
854854
return un
855855
}
856856

857+
func TestSync_HookWithReplaceAndBeforeHookCreation_AlreadyDeleted(t *testing.T) {
858+
// This test a race condition when Delete is called on an already deleted object
859+
// LiveObj is set, but then the resource is deleted asynchronously in kubernetes
860+
syncCtx := newTestSyncCtx(nil)
861+
862+
target := withReplaceAnnotation(testingutils.NewPod())
863+
target.SetNamespace(testingutils.FakeArgoCDNamespace)
864+
target = testingutils.Annotate(target, synccommon.AnnotationKeyHookDeletePolicy, string(synccommon.HookDeletePolicyBeforeHookCreation))
865+
target = testingutils.Annotate(target, synccommon.AnnotationKeyHook, string(synccommon.SyncPhasePreSync))
866+
live := target.DeepCopy()
867+
868+
syncCtx.resources = groupResources(ReconciliationResult{
869+
Live: []*unstructured.Unstructured{live},
870+
Target: []*unstructured.Unstructured{target},
871+
})
872+
syncCtx.hooks = []*unstructured.Unstructured{live}
873+
874+
client := fake.NewSimpleDynamicClient(runtime.NewScheme())
875+
deleted := false
876+
client.PrependReactor("delete", "pods", func(_ testcore.Action) (bool, runtime.Object, error) {
877+
deleted = true
878+
// simulate the race conditions where liveObj was not null, but is now deleted in k8s
879+
return true, nil, apierrors.NewNotFound(corev1.Resource("pods"), live.GetName())
880+
})
881+
syncCtx.dynamicIf = client
882+
883+
syncCtx.Sync()
884+
885+
resourceOps, _ := syncCtx.resourceOps.(*kubetest.MockResourceOps)
886+
assert.Equal(t, "create", resourceOps.GetLastResourceCommand(kube.GetResourceKey(target)))
887+
assert.True(t, deleted)
888+
}
889+
857890
func TestSync_ServerSideApply(t *testing.T) {
858891
testCases := []struct {
859892
name string

0 commit comments

Comments
 (0)