diff --git a/pkg/sync/sync_context.go b/pkg/sync/sync_context.go index 8f4d51e4f..86dda5a7f 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) { @@ -122,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) { @@ -367,6 +381,8 @@ type syncContext struct { pruneLast bool prunePropagationPolicy *metav1.DeletionPropagation pruneConfirmed bool + requiresPruneConfirmation bool + pruneDisabled bool clientSideApplyMigrationManager string enableClientSideApplyMigration bool @@ -1212,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 { @@ -1372,7 +1388,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..4fd6893f1 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" @@ -767,6 +768,60 @@ 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 +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