Skip to content

Commit d96c3d5

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

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
@@ -1265,6 +1265,31 @@ func (sc *syncContext) runTasks(tasks syncTasks, dryRun bool) runState {
12651265
createTasks = append(createTasks, task)
12661266
}
12671267
}
1268+
1269+
// remove finalizers from previous sync on existing hooks to make sure the operation is idempotent
1270+
{
1271+
ss := newStateSync(state)
1272+
existingHooks := tasks.Filter(func(t *syncTask) bool { return t.isHook() && t.pending() && t.liveObj != nil })
1273+
for _, task := range existingHooks {
1274+
t := task
1275+
ss.Go(func(state runState) runState {
1276+
logCtx := sc.log.WithValues("dryRun", dryRun, "task", t)
1277+
logCtx.V(1).Info("Removing finalizers")
1278+
if !dryRun {
1279+
if err := sc.removeHookFinalizer(t); err != nil {
1280+
state = failed
1281+
sc.setResourceResult(t, t.syncStatus, common.OperationError, fmt.Sprintf("failed to remove hook finalizer: %v", err))
1282+
}
1283+
}
1284+
return state
1285+
})
1286+
}
1287+
state = ss.Wait()
1288+
}
1289+
if state != successful {
1290+
return state
1291+
}
1292+
12681293
// prune first
12691294
{
12701295
if !sc.pruneConfirmed {
@@ -1316,15 +1341,19 @@ func (sc *syncContext) runTasks(tasks syncTasks, dryRun bool) runState {
13161341
for _, task := range hooksPendingDeletion {
13171342
t := task
13181343
ss.Go(func(state runState) runState {
1319-
sc.log.WithValues("dryRun", dryRun, "task", t).V(1).Info("Deleting")
1344+
log := sc.log.WithValues("dryRun", dryRun, "task", t).V(1)
1345+
log.Info("Deleting")
13201346
if !dryRun {
13211347
err := sc.deleteResource(t)
13221348
if err != nil {
13231349
// it is possible to get a race condition here, such that the resource does not exist when
1324-
// delete is requested, we treat this as a nop
1350+
// delete is requested, we treat this as a nopand remove the liveObj
13251351
if !apierrors.IsNotFound(err) {
13261352
state = failed
1327-
sc.setResourceResult(t, "", common.OperationError, fmt.Sprintf("failed to delete resource: %v", err))
1353+
sc.setResourceResult(t, t.syncStatus, common.OperationError, fmt.Sprintf("failed to delete resource: %v", err))
1354+
} else {
1355+
log.Info("Resource not found, treating as no-op and removing liveObj")
1356+
t.liveObj = nil
13281357
}
13291358
} else {
13301359
// 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
@@ -45,15 +45,15 @@ func newTestSyncCtx(getResourceFunc *func(ctx context.Context, config *rest.Conf
4545
&metav1.APIResourceList{
4646
GroupVersion: "v1",
4747
APIResources: []metav1.APIResource{
48-
{Kind: "Pod", Group: "", Version: "v1", Namespaced: true, Verbs: standardVerbs},
49-
{Kind: "Service", Group: "", Version: "v1", Namespaced: true, Verbs: standardVerbs},
50-
{Kind: "Namespace", Group: "", Version: "v1", Namespaced: false, Verbs: standardVerbs},
48+
{Name: "pods", Kind: "Pod", Group: "", Version: "v1", Namespaced: true, Verbs: standardVerbs},
49+
{Name: "services", Kind: "Service", Group: "", Version: "v1", Namespaced: true, Verbs: standardVerbs},
50+
{Name: "namespaces", Kind: "Namespace", Group: "", Version: "v1", Namespaced: false, Verbs: standardVerbs},
5151
},
5252
},
5353
&metav1.APIResourceList{
5454
GroupVersion: "apps/v1",
5555
APIResources: []metav1.APIResource{
56-
{Kind: "Deployment", Group: "apps", Version: "v1", Namespaced: true, Verbs: standardVerbs},
56+
{Name: "deployments", Kind: "Deployment", Group: "apps", Version: "v1", Namespaced: true, Verbs: standardVerbs},
5757
},
5858
})
5959
sc := syncContext{
@@ -807,6 +807,39 @@ func withReplaceAndServerSideApplyAnnotations(un *unstructured.Unstructured) *un
807807
return un
808808
}
809809

810+
func TestSync_HookWithReplaceAndBeforeHookCreation_AlreadyDeleted(t *testing.T) {
811+
// This test a race condition when Delete is called on an already deleted object
812+
// LiveObj is set, but then the resource is deleted asynchronously in kubernetes
813+
syncCtx := newTestSyncCtx(nil)
814+
815+
target := withReplaceAnnotation(testingutils.NewPod())
816+
target.SetNamespace(testingutils.FakeArgoCDNamespace)
817+
target = testingutils.Annotate(target, synccommon.AnnotationKeyHookDeletePolicy, string(synccommon.HookDeletePolicyBeforeHookCreation))
818+
target = testingutils.Annotate(target, synccommon.AnnotationKeyHook, string(synccommon.SyncPhasePreSync))
819+
live := target.DeepCopy()
820+
821+
syncCtx.resources = groupResources(ReconciliationResult{
822+
Live: []*unstructured.Unstructured{live},
823+
Target: []*unstructured.Unstructured{target},
824+
})
825+
syncCtx.hooks = []*unstructured.Unstructured{live}
826+
827+
client := fake.NewSimpleDynamicClient(runtime.NewScheme())
828+
deleted := false
829+
client.PrependReactor("delete", "pods", func(_ testcore.Action) (bool, runtime.Object, error) {
830+
deleted = true
831+
// simulate the race conditions where liveObj was not null, but is now deleted in k8s
832+
return true, nil, apierrors.NewNotFound(corev1.Resource("pods"), live.GetName())
833+
})
834+
syncCtx.dynamicIf = client
835+
836+
syncCtx.Sync()
837+
838+
resourceOps, _ := syncCtx.resourceOps.(*kubetest.MockResourceOps)
839+
assert.Equal(t, "create", resourceOps.GetLastResourceCommand(kube.GetResourceKey(target)))
840+
assert.True(t, deleted)
841+
}
842+
810843
func TestSync_ServerSideApply(t *testing.T) {
811844
testCases := []struct {
812845
name string

0 commit comments

Comments
 (0)