Skip to content
Open
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 @@ -1165,8 +1165,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
13 changes: 13 additions & 0 deletions controller/appcontroller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2680,6 +2680,19 @@ func Test_syncDeleteOption(t *testing.T) {
cmObj.SetAnnotations(map[string]string{"helm.sh/resource-policy": "keep"})
assert.False(t, ctrl.shouldBeDeleted(app, cmObj))
})

app.Spec.SyncPolicy.SyncOptions = []string{"Delete=false"}
t.Run("delete set on the app level", func(t *testing.T) {
cmObj := kube.MustToUnstructured(&cm)
cmObj.SetAnnotations(map[string]string{})
assert.False(t, ctrl.shouldBeDeleted(app, cmObj))
})

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

func TestAddControllerNamespace(t *testing.T) {
Expand Down
40 changes: 31 additions & 9 deletions controller/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -853,15 +853,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),
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 @@ -1053,6 +1052,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 @@ -1848,3 +1849,72 @@ func TestCompareAppState_DoesNotCallUpdateRevisionForPaths_ForOCI(t *testing.T)
_, _, _, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, sources, "abc123", []string{"123456"}, false, false, false, &defaultProj, false)
require.NoError(t, err)
}

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 resource",
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 @@ -34,6 +48,21 @@ metadata:
To confirm the pruning 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:
- 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 @@ -87,6 +116,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 @@ -101,6 +145,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
15 changes: 15 additions & 0 deletions gitops-engine/pkg/sync/resource/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package resource

import (
"strings"

"k8s.io/utils/ptr"
)

// AnnotationGetter defines the operations required to inspect if a resource
Expand Down Expand Up @@ -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 strings.HasPrefix(item, prefix) {
return ptr.To(strings.TrimPrefix(item, prefix))
}
}
return nil
}
27 changes: 27 additions & 0 deletions gitops-engine/pkg/sync/resource/annotations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/utils/ptr"

testingutils "github.com/argoproj/gitops-engine/pkg/utils/testing"
)
Expand Down Expand Up @@ -36,6 +37,32 @@ 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"}, ptr.To("baz")},
{"DeDup", args{example("bar=baz1,bar=baz2"), "foo", "bar"}, ptr.To("baz1")},
{"Double", args{example("bar=qux,baz=quux"), "foo", "baz"}, ptr.To("quux")},
{"Spaces", args{example("bar=baz "), "foo", "bar"}, ptr.To("baz")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, GetAnnotationOptionValue(tt.args.obj, tt.args.key, tt.args.val))
})
}
}

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