Skip to content

Commit 6485195

Browse files
committed
feat: add support for Prune/Delete=false sync option
For consistency with Prune/Delete=confirm that are now present as application level sync option add Prune/Delete=false as sync option too. Signed-off-by: Arthur Outhenin-Chalandre <[email protected]>
1 parent ed7cef2 commit 6485195

File tree

7 files changed

+119
-1
lines changed

7 files changed

+119
-1
lines changed

controller/appcontroller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,6 +1166,7 @@ func (ctrl *ApplicationController) removeProjectFinalizer(proj *appv1.AppProject
11661166
func (ctrl *ApplicationController) shouldBeDeleted(app *appv1.Application, obj *unstructured.Unstructured) bool {
11671167
return !kube.IsCRD(obj) && !isSelfReferencedApp(app, kube.GetObjectRef(obj)) &&
11681168
!resourceutil.HasAnnotationOption(obj, synccommon.AnnotationSyncOptions, synccommon.SyncOptionDisableDeletion) &&
1169+
(app.Spec.SyncPolicy == nil || !app.Spec.SyncPolicy.SyncOptions.HasOption(synccommon.SyncOptionDisableDeletion)) &&
11691170
!resourceutil.HasAnnotationOption(obj, helm.ResourcePolicyAnnotation, helm.ResourcePolicyKeep)
11701171
}
11711172

controller/appcontroller_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2565,6 +2565,16 @@ func Test_syncDeleteOption(t *testing.T) {
25652565
})
25662566
}
25672567

2568+
func Test_syncDeleteOptionGlobal(t *testing.T) {
2569+
app := newFakeApp()
2570+
app.Spec.SyncPolicy.SyncOptions = []string{"Delete=false"}
2571+
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
2572+
2573+
cm := newFakeCM()
2574+
cmObj := kube.MustToUnstructured(&cm)
2575+
assert.False(t, ctrl.shouldBeDeleted(app, cmObj))
2576+
}
2577+
25682578
func TestAddControllerNamespace(t *testing.T) {
25692579
t.Run("set controllerNamespace when the app is in the controller namespace", func(t *testing.T) {
25702580
app := newFakeApp()

controller/sync.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, project *v1alp
348348
),
349349
sync.WithPruneConfirmed(app.IsDeletionConfirmed(state.StartedAt.Time)),
350350
sync.WithRequiresPruneConfirmation(syncOp.SyncOptions.HasOption(common.SyncOptionPruneRequireConfirm)),
351+
sync.WithPruneDisabled(syncOp.SyncOptions.HasOption(common.SyncOptionDisablePrune)),
351352
sync.WithSkipDryRunOnMissingResource(syncOp.SyncOptions.HasOption(common.SyncOptionSkipDryRunOnMissingResource)),
352353
}
353354

docs/user-guide/sync-options.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,21 @@ metadata:
1414
argocd.argoproj.io/sync-options: Prune=false
1515
```
1616
17+
If you want to prevent any object from being pruned in the Application,
18+
it also can be enabled at the application level like in the example below:
19+
20+
```yaml
21+
apiVersion: argoproj.io/v1alpha1
22+
kind: Application
23+
spec:
24+
syncPolicy:
25+
syncOptions:
26+
- Prune=false
27+
```
28+
29+
Note that setting a Prune sync option on the application level will not override
30+
the same sync option set on a specific resource, both will still be applied.
31+
1732
The sync-status panel shows that pruning was skipped, and why:
1833
1934
![sync option no prune](../assets/sync-option-no-prune-sync-status.png)
@@ -101,6 +116,21 @@ metadata:
101116
argocd.argoproj.io/sync-options: Delete=false
102117
```
103118

119+
If you want to prevent any object from being deleted in the Application,
120+
it also can be enabled at the application level like in the example below:
121+
122+
```yaml
123+
apiVersion: argoproj.io/v1alpha1
124+
kind: Application
125+
spec:
126+
syncPolicy:
127+
syncOptions:
128+
- Delete=false
129+
```
130+
131+
Note that setting a Delete sync option on the application level will not override
132+
the same sync option set on a specific resource, both will still be applied.
133+
104134
## Resource Deletion With Confirmation
105135

106136
Resources such as Namespaces are critical and should not be deleted without confirmation. You can set the `Delete=confirm`

gitops-engine/pkg/sync/sync_context.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@ func WithPruneConfirmed(confirmed bool) SyncOpt {
129129
}
130130
}
131131

132+
// WithPruneDisabled specifies if prune is globally disabled for this application
133+
func WithPruneDisabled(disabled bool) SyncOpt {
134+
return func(ctx *syncContext) {
135+
ctx.pruneDisabled = disabled
136+
}
137+
}
138+
132139
// WithOperationSettings allows to set sync operation settings
133140
func WithOperationSettings(dryRun bool, prune bool, force bool, skipHooks bool) SyncOpt {
134141
return func(ctx *syncContext) {
@@ -375,6 +382,7 @@ type syncContext struct {
375382
prunePropagationPolicy *metav1.DeletionPropagation
376383
pruneConfirmed bool
377384
requiresPruneConfirmation bool
385+
pruneDisabled bool
378386
clientSideApplyMigrationManager string
379387
enableClientSideApplyMigration bool
380388

@@ -1229,7 +1237,7 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R
12291237
func (sc *syncContext) pruneObject(liveObj *unstructured.Unstructured, prune, dryRun bool) (common.ResultCode, string) {
12301238
if !prune {
12311239
return common.ResultCodePruneSkipped, "ignored (requires pruning)"
1232-
} else if resourceutil.HasAnnotationOption(liveObj, common.AnnotationSyncOptions, common.SyncOptionDisablePrune) {
1240+
} else if resourceutil.HasAnnotationOption(liveObj, common.AnnotationSyncOptions, common.SyncOptionDisablePrune) || sc.pruneDisabled {
12331241
return common.ResultCodePruneSkipped, "ignored (no prune)"
12341242
}
12351243
if dryRun {

gitops-engine/pkg/sync/sync_context_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,23 @@ func TestDoNotPrunePruneFalse(t *testing.T) {
835835

836836
phase, _, _ = syncCtx.GetState()
837837
assert.Equal(t, synccommon.OperationSucceeded, phase)
838+
839+
// test that we can still not prune if prune is disabled on the app level
840+
syncCtx.pruneDisabled = true
841+
pod.SetAnnotations(map[string]string{synccommon.AnnotationSyncOptions: "Prune=true"})
842+
syncCtx.Sync()
843+
844+
phase, _, resources = syncCtx.GetState()
845+
846+
assert.Equal(t, synccommon.OperationSucceeded, phase)
847+
assert.Len(t, resources, 1)
848+
assert.Equal(t, synccommon.ResultCodePruneSkipped, resources[0].Status)
849+
assert.Equal(t, "ignored (no prune)", resources[0].Message)
850+
851+
syncCtx.Sync()
852+
853+
phase, _, _ = syncCtx.GetState()
854+
assert.Equal(t, synccommon.OperationSucceeded, phase)
838855
}
839856

840857
// make sure that we need confirmation to prune with Prune=confirm

test/e2e/app_management_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1750,6 +1750,57 @@ func TestSyncOptionPruneFalse(t *testing.T) {
17501750
Expect(ResourceSyncStatusIs("Pod", "pod-1", SyncStatusCodeOutOfSync))
17511751
}
17521752

1753+
// make sure that if we deleted a resource from the app, it is not pruned if the app has the sync option Prune=false
1754+
func TestSyncOptionSyncOptionPruneFalse(t *testing.T) {
1755+
Given(t).
1756+
Path("two-nice-pods").
1757+
When().
1758+
CreateApp().
1759+
PatchApp(`[{
1760+
"op": "add",
1761+
"path": "/spec/syncPolicy",
1762+
"value": { "syncOptions": ["Prune=false"] }
1763+
}]`).
1764+
Sync().
1765+
Then().
1766+
Expect(OperationPhaseIs(OperationSucceeded)).
1767+
Expect(SyncStatusIs(SyncStatusCodeSynced)).
1768+
When().
1769+
DeleteFile("pod-1.yaml").
1770+
Refresh(RefreshTypeHard).
1771+
IgnoreErrors().
1772+
Sync().
1773+
Then().
1774+
Expect(OperationPhaseIs(OperationSucceeded)).
1775+
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
1776+
Expect(ResourceSyncStatusIs("Pod", "pod-1", SyncStatusCodeOutOfSync))
1777+
1778+
// Also check that another setting on a ressource level doesn't override that
1779+
Given(t).
1780+
Path("two-nice-pods").
1781+
When().
1782+
CreateApp().
1783+
PatchApp(`[{
1784+
"op": "add",
1785+
"path": "/spec/syncPolicy",
1786+
"value": { "syncOptions": ["Prune=false"] }
1787+
}]`).
1788+
PatchFile("pod-1.yaml", `[{"op": "add", "path": "/metadata/annotations", "value": {"argocd.argoproj.io/sync-options": "Prune=true"}}]`).
1789+
Sync().
1790+
Then().
1791+
Expect(OperationPhaseIs(OperationSucceeded)).
1792+
Expect(SyncStatusIs(SyncStatusCodeSynced)).
1793+
When().
1794+
DeleteFile("pod-1.yaml").
1795+
Refresh(RefreshTypeHard).
1796+
IgnoreErrors().
1797+
Sync().
1798+
Then().
1799+
Expect(OperationPhaseIs(OperationSucceeded)).
1800+
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
1801+
Expect(ResourceSyncStatusIs("Pod", "pod-1", SyncStatusCodeOutOfSync))
1802+
}
1803+
17531804
// make sure that if we have an invalid manifest, we can add it if we disable validation, we get a server error rather than a client error
17541805
func TestSyncOptionValidateFalse(t *testing.T) {
17551806
Given(t).

0 commit comments

Comments
 (0)