Skip to content

Commit afa5bae

Browse files
committed
feat: add support for Prune/Delete=confirm sync option
As deletion confirmation acts on the whole Application it could makes sense to add it as a sync option rather than a resource annotation. It also could be more convenient to integrate in certain workflows for users. Signed-off-by: Arthur Outhenin-Chalandre <[email protected]>
1 parent d8675e9 commit afa5bae

File tree

5 files changed

+85
-4
lines changed

5 files changed

+85
-4
lines changed

controller/appcontroller.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,6 +1217,11 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
12171217
return err
12181218
}
12191219

1220+
if app.Spec.SyncPolicy != nil && app.Spec.SyncPolicy.SyncOptions.HasOption(synccommon.SyncOptionDeleteRequireConfirm) && !deletionApproved {
1221+
logCtx.Infof("Application requires manual confirmation to be deleted")
1222+
return nil
1223+
}
1224+
12201225
for k := range objsMap {
12211226
// Wait for objects pending deletion to complete before proceeding with next sync wave
12221227
if objsMap[k].GetDeletionTimestamp() != nil {

controller/sync.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
377377
sync.WithServerSideApply(syncOp.SyncOptions.HasOption(common.SyncOptionServerSideApply)),
378378
sync.WithServerSideApplyManager(cdcommon.ArgoCDSSAManager),
379379
sync.WithPruneConfirmed(app.IsDeletionConfirmed(state.StartedAt.Time)),
380+
sync.WithRequiresPruneConfirmation(syncOp.SyncOptions.HasOption(common.SyncOptionPruneRequireConfirm)),
380381
sync.WithSkipDryRunOnMissingResource(syncOp.SyncOptions.HasOption(common.SyncOptionSkipDryRunOnMissingResource)),
381382
}
382383

docs/user-guide/sync-options.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ metadata:
3434
To confirm the pruning you can use Argo CD UI, CLI or manually apply the `argocd.argoproj.io/deletion-approved: <ISO formatted timestamp>`
3535
annotation to the application.
3636

37+
It also can be enabled at the application level like in the example below:
38+
39+
```yaml
40+
apiVersion: argoproj.io/v1alpha1
41+
kind: Application
42+
spec:
43+
syncPolicy:
44+
syncOptions:
45+
- Prune=confirm
46+
```
47+
48+
Note that setting a Prune sync option on the application level will not override
49+
the same sync option set on a specific resource, both will still be applied.
50+
3751
## Disable Kubectl Validation
3852

3953
For a certain class of objects, it is necessary to `kubectl apply` them using the `--validate=false` flag. Examples of this are Kubernetes types which uses `RawExtension`, such as [ServiceCatalog](https://github.com/kubernetes-incubator/service-catalog/blob/master/pkg/apis/servicecatalog/v1beta1/types.go#L497). You can do that using this annotation:
@@ -101,6 +115,20 @@ metadata:
101115
To confirm the deletion you can use Argo CD UI, CLI or manually apply the `argocd.argoproj.io/deletion-approved: <ISO formatted timestamp>`
102116
annotation to the application.
103117

118+
It also can be enabled at the application level like in the example below:
119+
120+
```yaml
121+
apiVersion: argoproj.io/v1alpha1
122+
kind: Application
123+
spec:
124+
syncPolicy:
125+
syncOptions:
126+
- Delete=confirm
127+
```
128+
129+
Note that setting a Delete sync option on the application level will not override
130+
the same sync option set on a specific resource, both will still be applied.
131+
104132
## Selective Sync
105133

106134
Currently, when syncing using auto sync Argo CD applies every object in the application.

test/e2e/app_management_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3030,6 +3030,42 @@ func TestDeletionConfirmation(t *testing.T) {
30303030
Then().Expect(DoesNotExist())
30313031
}
30323032

3033+
func TestDeletionConfirmationSyncOption(t *testing.T) {
3034+
ctx := Given(t)
3035+
ctx.
3036+
And(func() {
3037+
_, err := fixture.KubeClientset.CoreV1().ConfigMaps(fixture.DeploymentNamespace()).Create(
3038+
t.Context(), &corev1.ConfigMap{
3039+
ObjectMeta: metav1.ObjectMeta{
3040+
Name: "test-configmap",
3041+
Labels: map[string]string{
3042+
common.LabelKeyAppInstance: ctx.AppName(),
3043+
},
3044+
},
3045+
}, metav1.CreateOptions{})
3046+
require.NoError(t, err)
3047+
}).
3048+
Path(guestbookPath).
3049+
Async(true).
3050+
When().
3051+
CreateApp().Then().When().
3052+
PatchApp(`[{
3053+
"op": "add",
3054+
"path": "/spec/syncPolicy",
3055+
"value": { "syncOptions": ["Delete=confirm", "Prune=confirm"] }
3056+
}]`).Sync().
3057+
Then().Expect(OperationPhaseIs(OperationRunning)).
3058+
When().ConfirmDeletion().
3059+
Then().Expect(OperationPhaseIs(OperationSucceeded)).
3060+
When().Delete(true).
3061+
Then().
3062+
And(func(app *Application) {
3063+
assert.NotNil(t, app.DeletionTimestamp)
3064+
}).
3065+
When().ConfirmDeletion().
3066+
Then().Expect(DoesNotExist())
3067+
}
3068+
30333069
func TestLastTransitionTimeUnchangedError(t *testing.T) {
30343070
// Ensure that, if the health status hasn't changed, the lastTransitionTime is not updated.
30353071

ui/src/app/applications/components/application-details/application-details.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,19 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
10391039
iconClassName: ext.iconClassName
10401040
};
10411041
}
1042+
1043+
private requiresDeletionConfirmation(app: appModels.Application): boolean {
1044+
if (app.status.resources.find(r => r.requiresDeletionConfirmation)) {
1045+
return true;
1046+
}
1047+
1048+
if (app.spec.syncPolicy?.syncOptions?.includes('Delete=confirm') || app.spec.syncPolicy?.syncOptions?.includes('Prune=confirm')) {
1049+
return true;
1050+
}
1051+
1052+
return false;
1053+
}
1054+
10421055
private getApplicationActionMenu(app: appModels.Application, needOverlapLabelOnNarrowScreen: boolean) {
10431056
const refreshing = app.metadata.annotations && app.metadata.annotations[appModels.AnnotationRefreshKey];
10441057
const fullName = AppUtils.nodeKey({group: 'argoproj.io', kind: app.kind, name: app.metadata.name, namespace: app.metadata.namespace});
@@ -1064,7 +1077,7 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
10641077
action: () => AppUtils.showDeploy('all', null, this.appContext.apis),
10651078
disabled: !app.spec.source && (!app.spec.sources || app.spec.sources.length === 0) && !app.spec.sourceHydrator
10661079
},
1067-
...(app.status?.operationState?.phase === 'Running' && app.status.resources.find(r => r.requiresDeletionConfirmation)
1080+
...(app.status?.operationState?.phase === 'Running' && this.requiresDeletionConfirmation(app)
10681081
? [
10691082
{
10701083
iconClassName: 'fa fa-check',
@@ -1087,9 +1100,7 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
10871100
},
10881101
disabled: !app.status.operationState
10891102
},
1090-
app.metadata.deletionTimestamp &&
1091-
app.status.resources.find(r => r.requiresDeletionConfirmation) &&
1092-
!((app.metadata.annotations || {})[appModels.AppDeletionConfirmedAnnotation] == 'true')
1103+
app.metadata.deletionTimestamp && this.requiresDeletionConfirmation(app) && !((app.metadata.annotations || {})[appModels.AppDeletionConfirmedAnnotation] == 'true')
10931104
? {
10941105
iconClassName: 'fa fa-check',
10951106
title: <ActionMenuItem actionLabel='Confirm Deletion' />,

0 commit comments

Comments
 (0)