Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion controller/appcontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -1168,8 +1168,13 @@ func (ctrl *ApplicationController) removeProjectFinalizer(proj *appv1.AppProject

// shouldBeDeleted returns whether a given resource obj should be deleted on cascade delete of application app
func (ctrl *ApplicationController) shouldBeDeleted(app *appv1.Application, obj *unstructured.Unstructured) bool {
deleteOption := resourceutil.GetAnnotationOptionValue(obj, synccommon.AnnotationSyncOptions, synccommon.SyncOptionDelete)
if deleteOption == nil && app.Spec.SyncPolicy != nil {
deleteOption = app.Spec.SyncPolicy.SyncOptions.GetOptionValue(synccommon.SyncOptionDelete)
}

return !kube.IsCRD(obj) && !isSelfReferencedApp(app, kube.GetObjectRef(obj)) &&
!resourceutil.HasAnnotationOption(obj, synccommon.AnnotationSyncOptions, synccommon.SyncOptionDisableDeletion) &&
(deleteOption == nil || *deleteOption != synccommon.SyncValueFalse) &&
!resourceutil.HasAnnotationOption(obj, helm.ResourcePolicyAnnotation, helm.ResourcePolicyKeep)
}

Expand Down
15 changes: 15 additions & 0 deletions controller/appcontroller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2985,6 +2985,21 @@ func Test_syncDeleteOption(t *testing.T) {
cmObj.SetAnnotations(map[string]string{"helm.sh/resource-policy": "keep"})
assert.False(t, ctrl.shouldBeDeleted(app, cmObj))
})

t.Run("delete set on the app level", func(t *testing.T) {
newApp := app.DeepCopy()
newApp.Spec.SyncPolicy.SyncOptions = []string{"Delete=false"}
cmObj := kube.MustToUnstructured(&cm)
cmObj.SetAnnotations(map[string]string{})
assert.False(t, ctrl.shouldBeDeleted(newApp, cmObj))
})
t.Run("delete should be overridden on the resource", func(t *testing.T) {
newApp := app.DeepCopy()
newApp.Spec.SyncPolicy.SyncOptions = []string{"Delete=false"}
cmObj := kube.MustToUnstructured(&cm)
cmObj.SetAnnotations(map[string]string{"argocd.argoproj.io/sync-options": "Delete=foo"})
assert.True(t, ctrl.shouldBeDeleted(newApp, cmObj))
})
}

func TestAddControllerNamespace(t *testing.T) {
Expand Down
42 changes: 31 additions & 11 deletions controller/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -882,17 +882,14 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
isSelfReferencedObj := m.isSelfReferencedObj(liveObj, targetObj, app.GetName(), v1alpha1.TrackingMethod(trackingMethod), installationID)

resState := v1alpha1.ResourceStatus{
Namespace: obj.GetNamespace(),
Name: obj.GetName(),
Kind: gvk.Kind,
Version: gvk.Version,
Group: gvk.Group,
Hook: isHook(obj),
RequiresPruning: targetObj == nil && liveObj != nil && isSelfReferencedObj,
RequiresDeletionConfirmation: targetObj != nil && resourceutil.HasAnnotationOption(targetObj, synccommon.AnnotationSyncOptions, synccommon.SyncOptionDeleteRequireConfirm) ||
liveObj != nil && resourceutil.HasAnnotationOption(liveObj, synccommon.AnnotationSyncOptions, synccommon.SyncOptionDeleteRequireConfirm) ||
targetObj != nil && resourceutil.HasAnnotationOption(targetObj, synccommon.AnnotationSyncOptions, synccommon.SyncOptionPruneRequireConfirm) ||
liveObj != nil && resourceutil.HasAnnotationOption(liveObj, synccommon.AnnotationSyncOptions, synccommon.SyncOptionPruneRequireConfirm),
Namespace: obj.GetNamespace(),
Name: obj.GetName(),
Kind: gvk.Kind,
Version: gvk.Version,
Group: gvk.Group,
Hook: isHook(obj),
RequiresPruning: targetObj == nil && liveObj != nil && isSelfReferencedObj,
RequiresDeletionConfirmation: isObjRequiresDeletionConfirmation(targetObj, app) || isObjRequiresDeletionConfirmation(liveObj, app),
}
if targetObj != nil {
resState.SyncWave = int64(syncwaves.Wave(targetObj))
Expand Down Expand Up @@ -1092,6 +1089,29 @@ func specEqualsCompareTo(spec v1alpha1.ApplicationSpec, sources []v1alpha1.Appli
return reflect.DeepEqual(comparedTo, compareToSpec)
}

func isObjRequiresDeletionConfirmation(obj *unstructured.Unstructured, app *v1alpha1.Application) bool {
if obj == nil {
return false
}
deleteOption := resourceutil.GetAnnotationOptionValue(obj, synccommon.AnnotationSyncOptions, synccommon.SyncOptionDelete)
if deleteOption == nil && app.Spec.SyncPolicy != nil {
deleteOption = app.Spec.SyncPolicy.SyncOptions.GetOptionValue(synccommon.SyncOptionDelete)
}
if deleteOption != nil && *deleteOption == synccommon.SyncValueConfirm {
return true
}

pruneOption := resourceutil.GetAnnotationOptionValue(obj, synccommon.AnnotationSyncOptions, synccommon.SyncOptionPrune)
if pruneOption == nil && app.Spec.SyncPolicy != nil {
pruneOption = app.Spec.SyncPolicy.SyncOptions.GetOptionValue(synccommon.SyncOptionPrune)
}
if pruneOption != nil && *pruneOption == synccommon.SyncValueConfirm {
return true
}

return false
}

func (m *appStateManager) persistRevisionHistory(
app *v1alpha1.Application,
revision string,
Expand Down
70 changes: 70 additions & 0 deletions controller/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"os"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -2038,3 +2039,72 @@ func TestCompareAppState_CallUpdateRevisionForPaths_ForMultiSource(t *testing.T)
require.NoError(t, err)
require.False(t, revisionsMayHaveChanges)
}

func Test_isObjRequiresDeletionConfirmation(t *testing.T) {
for _, tt := range []struct {
name string
resourceSyncOptions []string
appSyncOptions []string
expected bool
}{
{
name: "default",
expected: false,
},
{
name: "confirm delete resource",
resourceSyncOptions: []string{"Delete=confirm"},
expected: true,
},
{
name: "confirm delete app",
appSyncOptions: []string{"Delete=confirm"},
expected: true,
},
{
name: "confirm prune resource",
appSyncOptions: []string{"Prune=confirm"},
expected: true,
},
{
name: "confirm app & resource delete",
appSyncOptions: []string{"Delete=confirm"},
resourceSyncOptions: []string{"Delete=confirm"},
expected: true,
},
{
name: "confirm app & resource override",
appSyncOptions: []string{"Delete=confirm"},
resourceSyncOptions: []string{"Delete=foo"},
expected: false,
},
{
name: "confirm app & resource mixed delete and prune",
appSyncOptions: []string{"Prune=confirm"},
resourceSyncOptions: []string{"Delete=confirm"},
expected: true,
},
{
name: "override prune resource",
appSyncOptions: []string{"Prune=confirm"},
resourceSyncOptions: []string{"Prune=foo"},
expected: false,
},
{
name: "override delete resource and additional delete confirm",
appSyncOptions: []string{"Delete=confirm", "Prune=confirm"},
resourceSyncOptions: []string{"Delete=foo"},
expected: true,
},
} {
t.Run(tt.name, func(t *testing.T) {
obj := NewPod()
obj.SetAnnotations(map[string]string{"argocd.argoproj.io/sync-options": strings.Join(tt.resourceSyncOptions, ",")})

app := newFakeApp()
app.Spec.SyncPolicy.SyncOptions = tt.appSyncOptions

require.Equal(t, tt.expected, isObjRequiresDeletionConfirmation(obj, app))
})
}
}
1 change: 1 addition & 0 deletions controller/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, project *v1alp
clientSideApplyManager,
),
sync.WithPruneConfirmed(app.IsDeletionConfirmed(state.StartedAt.Time)),
sync.WithDefaultPruneOption(syncOp.SyncOptions.GetOptionValue(common.SyncOptionPrune)),
sync.WithSkipDryRunOnMissingResource(syncOp.SyncOptions.HasOption(common.SyncOptionSkipDryRunOnMissingResource)),
}

Expand Down
58 changes: 58 additions & 0 deletions docs/user-guide/sync-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ metadata:
argocd.argoproj.io/sync-options: Prune=false
```

It is also possible to set this option as a default option on the application level:

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
syncPolicy:
syncOptions:
- Prune=false
```

Note that setting a Prune sync option on the resource will always override a
Prune sync policy defined in the Application.

The sync-status panel shows that pruning was skipped, and why:

![sync option no prune](../assets/sync-option-no-prune-sync-status.png)
Expand All @@ -39,6 +53,21 @@ confirmed. The UI will look similar to this, with the "Confirm Pruning" button a

![Screenshot of the Argo CD Application UI. The "Last Sync" section shows that the operation is still Syncing. The row of gray action buttons includes an extra "Confirm Pruning" button.](../assets/confirm-prune.png)

It is also possible to set this option as a default option on the application level:


```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
syncPolicy:
syncOptions:
- Prune=confirm
```

Note that setting a Prune sync option on the resource will always override a
Prune sync policy defined in the Application.

## Disable Kubectl Validation

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:
Expand Down Expand Up @@ -92,6 +121,21 @@ metadata:
argocd.argoproj.io/sync-options: Delete=false
```

It is also possible to set this option as a default option on the application level:


```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
syncPolicy:
syncOptions:
- Delete=false
```

Note that setting a Delete sync option on the resource will always override a
Delete sync policy defined in the Application.

## Resource Deletion With Confirmation

Resources such as Namespaces are critical and should not be deleted without confirmation. You can set the `Delete=confirm`
Expand All @@ -106,6 +150,20 @@ metadata:
To confirm the deletion you can use Argo CD UI, CLI or manually apply the `argocd.argoproj.io/deletion-approved: <ISO formatted timestamp>`
annotation to the application.

It is also possible to set this option as a default option on the application level:

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
syncPolicy:
syncOptions:
- Delete=confirm
```

Note that setting a Delete sync option on the resource will always override a
Delete sync policy defined in the Application.

## Selective Sync

Currently, when syncing using auto sync Argo CD applies every object in the application.
Expand Down
16 changes: 8 additions & 8 deletions gitops-engine/pkg/sync/common/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ const (

// Sync option that disables dry run in resource is missing in the cluster
SyncOptionSkipDryRunOnMissingResource = "SkipDryRunOnMissingResource=true"
// Sync option that disables resource pruning
SyncOptionDisablePrune = "Prune=false"
// Sync option that disables resource validation
SyncOptionsDisableValidation = "Validate=false"
// Sync option that enables pruneLast
Expand All @@ -36,16 +34,18 @@ const (
SyncOptionServerSideApply = "ServerSideApply=true"
// Sync option that disables use of --server-side flag instead of client-side
SyncOptionDisableServerSideApply = "ServerSideApply=false"
// Sync option that disables resource deletion
SyncOptionDisableDeletion = "Delete=false"
// Sync option that sync only out of sync resources
SyncOptionApplyOutOfSyncOnly = "ApplyOutOfSyncOnly=true"
// Sync option that disables sync only out of sync resources
SyncOptionDisableApplyOutOfSyncOnly = "ApplyOutOfSyncOnly=false"
// Sync option that requires confirmation before deleting the resource
SyncOptionDeleteRequireConfirm = "Delete=confirm"
// Sync option that requires confirmation before deleting the resource
SyncOptionPruneRequireConfirm = "Prune=confirm"
// Sync option that controls resource deletion
SyncOptionDelete = "Delete"
// Sync option that controls resource pruning
SyncOptionPrune = "Prune"
// Sync value to confirm a delete or prune operation
SyncValueConfirm = "confirm"
// Sync value to disable a delete or prune operation
SyncValueFalse = "false"
// Sync option that enables client-side apply migration
SyncOptionClientSideApplyMigration = "ClientSideApplyMigration=true"
// Sync option that disables client-side apply migration
Expand Down
29 changes: 22 additions & 7 deletions gitops-engine/pkg/sync/resource/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ type AnnotationGetter interface {
// the given key. If the annotation has comma separated values, the returned
// list will contain all deduped values.
func GetAnnotationCSVs(obj AnnotationGetter, key string) []string {
// may for de-duping
valuesToBool := make(map[string]bool)
// map for de-duping
seen := make(map[string]bool)
var values []string
for _, item := range strings.Split(obj.GetAnnotations()[key], ",") {
val := strings.TrimSpace(item)
if val != "" {
valuesToBool[val] = true
if val == "" {
continue
}
}
var values []string
for val := range valuesToBool {
if seen[val] {
continue
}
seen[val] = true
values = append(values, val)
}
return values
Expand All @@ -39,3 +41,16 @@ func HasAnnotationOption(obj AnnotationGetter, key, val string) bool {
}
return false
}

// GetAnnotationOptionValue will return the value of an option inside the
// annotation defined as the given key.
// This function only support options that are defined as key=value and not standalone.
func GetAnnotationOptionValue(obj AnnotationGetter, annotation, optionKey string) *string {
prefix := optionKey + "="
for _, item := range GetAnnotationCSVs(obj, annotation) {
if val, found := strings.CutPrefix(item, prefix); found {
return new(val)
}
}
return nil
}
32 changes: 32 additions & 0 deletions gitops-engine/pkg/sync/resource/annotations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,38 @@ func TestHasAnnotationOption(t *testing.T) {
}
}

func TestGetAnnotationOptionValue(t *testing.T) {
type args struct {
obj *unstructured.Unstructured
key string
val string
}
tests := []struct {
name string
args args
want *string
}{
{"Nil", args{testingutils.NewPod(), "foo", "bar"}, nil},
{"Empty", args{example(""), "foo", "bar"}, nil},
{"Standalone", args{example("bar"), "foo", "bar"}, nil},
{"Single", args{example("bar=baz"), "foo", "bar"}, new("baz")},
{"DeDup", args{example("bar=baz1,bar=baz2"), "foo", "bar"}, new("baz1")},
{"Double", args{example("bar=qux,baz=quux"), "foo", "baz"}, new("quux")},
{"Spaces", args{example("bar=baz "), "foo", "bar"}, new("baz")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GetAnnotationOptionValue(tt.args.obj, tt.args.key, tt.args.val)
if tt.want == nil {
assert.Nil(t, got)
} else {
assert.NotNil(t, got)
assert.Equal(t, *tt.want, *got)
}
})
}
}

func example(val string) *unstructured.Unstructured {
return testingutils.Annotate(testingutils.NewPod(), "foo", val)
}
Loading
Loading