From f66f8e13191e7f05d6a2fd4884968938db289e2a Mon Sep 17 00:00:00 2001 From: Arthur Outhenin-Chalandre Date: Mon, 9 Jun 2025 17:17:21 +0200 Subject: [PATCH 1/2] feat: add WithRequiresPruneConfirmation to sync option Add WithRequiresPruneConfirmation so that we can pass this as an Application sync option in ArgoCD Signed-off-by: Arthur Outhenin-Chalandre --- pkg/sync/sync_context.go | 10 ++++++++- pkg/sync/sync_context_test.go | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/pkg/sync/sync_context.go b/pkg/sync/sync_context.go index 8f4d51e4f..3bc5b38ea 100644 --- a/pkg/sync/sync_context.go +++ b/pkg/sync/sync_context.go @@ -115,6 +115,13 @@ func WithPrune(prune bool) SyncOpt { } } +// WithRequiresPruneConfirmation specifies if pruning resources requires a confirmation +func WithRequiresPruneConfirmation(requiresConfirmation bool) SyncOpt { + return func(ctx *syncContext) { + ctx.requiresPruneConfirmation = requiresConfirmation + } +} + // WithPruneConfirmed specifies if prune is confirmed for resources that require confirmation func WithPruneConfirmed(confirmed bool) SyncOpt { return func(ctx *syncContext) { @@ -367,6 +374,7 @@ type syncContext struct { pruneLast bool prunePropagationPolicy *metav1.DeletionPropagation pruneConfirmed bool + requiresPruneConfirmation bool clientSideApplyMigrationManager string enableClientSideApplyMigration bool @@ -1372,7 +1380,7 @@ func (sc *syncContext) runTasks(tasks syncTasks, dryRun bool) runState { if !sc.pruneConfirmed { var resources []string for _, task := range pruneTasks { - if resourceutil.HasAnnotationOption(task.liveObj, common.AnnotationSyncOptions, common.SyncOptionPruneRequireConfirm) { + if sc.requiresPruneConfirmation || resourceutil.HasAnnotationOption(task.liveObj, common.AnnotationSyncOptions, common.SyncOptionPruneRequireConfirm) { resources = append(resources, fmt.Sprintf("%s/%s/%s", task.obj().GetAPIVersion(), task.obj().GetKind(), task.name())) } } diff --git a/pkg/sync/sync_context_test.go b/pkg/sync/sync_context_test.go index 0e8d01ebb..2d6b4dfce 100644 --- a/pkg/sync/sync_context_test.go +++ b/pkg/sync/sync_context_test.go @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httptest" "reflect" + "strconv" "strings" "testing" "time" @@ -769,6 +770,43 @@ func TestDoNotPrunePruneFalse(t *testing.T) { assert.Equal(t, synccommon.OperationSucceeded, phase) } +// make sure that we need confirmation to prune with Prune=confirm +func TestPruneConfirm(t *testing.T) { + for _, appLevelConfirmation := range []bool{true, false} { + t.Run("appLevelConfirmation="+strconv.FormatBool(appLevelConfirmation), func(t *testing.T) { + syncCtx := newTestSyncCtx(nil, WithOperationSettings(false, true, false, false)) + syncCtx.requiresPruneConfirmation = appLevelConfirmation + pod := testingutils.NewPod() + if appLevelConfirmation { + pod.SetAnnotations(map[string]string{synccommon.AnnotationSyncOptions: "Prune=true"}) + } else { + pod.SetAnnotations(map[string]string{synccommon.AnnotationSyncOptions: "Prune=confirm"}) + } + pod.SetNamespace(testingutils.FakeArgoCDNamespace) + syncCtx.resources = groupResources(ReconciliationResult{ + Live: []*unstructured.Unstructured{pod}, + Target: []*unstructured.Unstructured{nil}, + }) + + syncCtx.Sync() + phase, msg, resources := syncCtx.GetState() + + assert.Equal(t, synccommon.OperationRunning, phase) + assert.Empty(t, resources) + assert.Equal(t, "Waiting for pruning confirmation of v1/Pod/my-pod", msg) + + syncCtx.pruneConfirmed = true + syncCtx.Sync() + + phase, _, resources = syncCtx.GetState() + assert.Equal(t, synccommon.OperationSucceeded, phase) + assert.Len(t, resources, 1) + assert.Equal(t, synccommon.ResultCodePruned, resources[0].Status) + assert.Equal(t, "pruned", resources[0].Message) + }) + } +} + // // make sure Validate=false means we don't validate func TestSyncOptionValidate(t *testing.T) { tests := []struct { From a1d18d9806bd6868101b0a74eee137b38712f019 Mon Sep 17 00:00:00 2001 From: Arthur Outhenin-Chalandre Date: Thu, 12 Jun 2025 13:52:50 +0200 Subject: [PATCH 2/2] feat: add WithPruneDisabled to syncoption Signed-off-by: Arthur Outhenin-Chalandre --- pkg/sync/sync_context.go | 10 +++++++++- pkg/sync/sync_context_test.go | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/pkg/sync/sync_context.go b/pkg/sync/sync_context.go index 3bc5b38ea..86dda5a7f 100644 --- a/pkg/sync/sync_context.go +++ b/pkg/sync/sync_context.go @@ -129,6 +129,13 @@ func WithPruneConfirmed(confirmed bool) SyncOpt { } } +// WithPruneDisabled specifies if prune is globally disabled for this application +func WithPruneDisabled(disabled bool) SyncOpt { + return func(ctx *syncContext) { + ctx.pruneDisabled = disabled + } +} + // WithOperationSettings allows to set sync operation settings func WithOperationSettings(dryRun bool, prune bool, force bool, skipHooks bool) SyncOpt { return func(ctx *syncContext) { @@ -375,6 +382,7 @@ type syncContext struct { prunePropagationPolicy *metav1.DeletionPropagation pruneConfirmed bool requiresPruneConfirmation bool + pruneDisabled bool clientSideApplyMigrationManager string enableClientSideApplyMigration bool @@ -1220,7 +1228,7 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R func (sc *syncContext) pruneObject(liveObj *unstructured.Unstructured, prune, dryRun bool) (common.ResultCode, string) { if !prune { return common.ResultCodePruneSkipped, "ignored (requires pruning)" - } else if resourceutil.HasAnnotationOption(liveObj, common.AnnotationSyncOptions, common.SyncOptionDisablePrune) { + } else if resourceutil.HasAnnotationOption(liveObj, common.AnnotationSyncOptions, common.SyncOptionDisablePrune) || sc.pruneDisabled { return common.ResultCodePruneSkipped, "ignored (no prune)" } if dryRun { diff --git a/pkg/sync/sync_context_test.go b/pkg/sync/sync_context_test.go index 2d6b4dfce..4fd6893f1 100644 --- a/pkg/sync/sync_context_test.go +++ b/pkg/sync/sync_context_test.go @@ -768,6 +768,23 @@ func TestDoNotPrunePruneFalse(t *testing.T) { phase, _, _ = syncCtx.GetState() assert.Equal(t, synccommon.OperationSucceeded, phase) + + // test that we can still not prune if prune is disabled on the app level + syncCtx.pruneDisabled = true + pod.SetAnnotations(map[string]string{synccommon.AnnotationSyncOptions: "Prune=true"}) + syncCtx.Sync() + + phase, _, resources = syncCtx.GetState() + + assert.Equal(t, synccommon.OperationSucceeded, phase) + assert.Len(t, resources, 1) + assert.Equal(t, synccommon.ResultCodePruneSkipped, resources[0].Status) + assert.Equal(t, "ignored (no prune)", resources[0].Message) + + syncCtx.Sync() + + phase, _, _ = syncCtx.GetState() + assert.Equal(t, synccommon.OperationSucceeded, phase) } // make sure that we need confirmation to prune with Prune=confirm