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
40 changes: 27 additions & 13 deletions controller/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
gitopsDiff "github.com/argoproj/argo-cd/gitops-engine/pkg/diff"
"github.com/argoproj/argo-cd/gitops-engine/pkg/sync"
"github.com/argoproj/argo-cd/gitops-engine/pkg/sync/common"
syncresource "github.com/argoproj/argo-cd/gitops-engine/pkg/sync/resource"
"github.com/argoproj/argo-cd/gitops-engine/pkg/utils/kube"
jsonpatch "github.com/evanphx/json-patch"
log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -232,18 +233,15 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, project *v1alp

reconciliationResult := compareResult.reconciliationResult

// if RespectIgnoreDifferences is enabled, it should normalize the target
// resources which in this case applies the live values in the configured
// ignore differences fields.
if syncOp.SyncOptions.HasOption("RespectIgnoreDifferences=true") {
patchedTargets, err := normalizeTargetResources(compareResult)
if err != nil {
state.Phase = common.OperationError
state.Message = fmt.Sprintf("Failed to normalize target resources: %s", err)
return
}
reconciliationResult.Target = patchedTargets
// if RespectIgnoreDifferences is enabled (at the app or resource level), normalize the
// target resources so that ignored fields are not patched back to the desired state.
patchedTargets, err := normalizeTargetResources(compareResult, syncOp.SyncOptions)
if err != nil {
state.Phase = common.OperationError
state.Message = fmt.Sprintf("Failed to normalize target resources: %s", err)
return
}
reconciliationResult.Target = patchedTargets

installationID, err := m.settingsMgr.GetInstallationID()
if err != nil {
Expand Down Expand Up @@ -396,8 +394,12 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, project *v1alp
// - applies normalization to the target resources based on the live resources
// - copies ignored fields from the matching live resources: apply normalizer to the live resource,
// calculates the patch performed by normalizer and applies the patch to the target resource
func normalizeTargetResources(cr *comparisonResult) ([]*unstructured.Unstructured, error) {
// normalize live and target resources
//
// Whether a resource is normalized is decided per-resource: the app-level syncOption sets the default,
// and individual resources can override in either direction via the argocd.argoproj.io/sync-options annotation.
func normalizeTargetResources(cr *comparisonResult, syncOptions v1alpha1.SyncOptions) ([]*unstructured.Unstructured, error) {
appLevel := syncOptions.HasOption(common.SyncOptionRespectIgnoreDifferences)

normalized, err := diff.Normalize(cr.reconciliationResult.Live, cr.reconciliationResult.Target, cr.diffConfig)
if err != nil {
return nil, err
Expand All @@ -415,6 +417,18 @@ func normalizeTargetResources(cr *comparisonResult) ([]*unstructured.Unstructure
continue
}

// per-resource annotation overrides app-level in either direction
shouldNormalize := appLevel
if syncresource.HasAnnotationOption(originalTarget, common.AnnotationSyncOptions, common.SyncOptionRespectIgnoreDifferences) {
shouldNormalize = true
} else if syncresource.HasAnnotationOption(originalTarget, common.AnnotationSyncOptions, common.SyncOptionDisableRespectIgnoreDifferences) {
shouldNormalize = false
}
if !shouldNormalize {
patchedTargets = append(patchedTargets, originalTarget)
continue
}

var lookupPatchMeta *strategicpatch.PatchMetaFromStruct
versionedObject, err := scheme.Scheme.New(normalizedTarget.GroupVersionKind())
if err == nil {
Expand Down
121 changes: 107 additions & 14 deletions controller/sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ import (
"github.com/argoproj/argo-cd/v3/util/settings"
)

var syncOptsRespectIgnoreDiffs = v1alpha1.SyncOptions{synccommon.SyncOptionRespectIgnoreDifferences}

func withRespectIgnoreDiffs(obj *unstructured.Unstructured) *unstructured.Unstructured {
annotations := obj.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
annotations[synccommon.AnnotationSyncOptions] = synccommon.SyncOptionRespectIgnoreDifferences
obj.SetAnnotations(annotations)
return obj
}

func TestPersistRevisionHistory(t *testing.T) {
app := newFakeApp()
app.Status.OperationState = nil
Expand Down Expand Up @@ -368,7 +380,7 @@ func TestNormalizeTargetResources(t *testing.T) {
Build()
require.NoError(t, err)
live := test.YamlToUnstructured(testdata.LiveDeploymentYaml)
target := test.YamlToUnstructured(testdata.TargetDeploymentYaml)
target := withRespectIgnoreDiffs(test.YamlToUnstructured(testdata.TargetDeploymentYaml))
return &fixture{
&comparisonResult{
reconciliationResult: sync.ReconciliationResult{
Expand All @@ -390,7 +402,7 @@ func TestNormalizeTargetResources(t *testing.T) {
f := setup(t, ignores)

// when
targets, err := normalizeTargetResources(f.comparisonResult)
targets, err := normalizeTargetResources(f.comparisonResult, nil)

// then
require.NoError(t, err)
Expand All @@ -403,7 +415,7 @@ func TestNormalizeTargetResources(t *testing.T) {
f := setup(t, []v1alpha1.ResourceIgnoreDifferences{})

// when
targets, err := normalizeTargetResources(f.comparisonResult)
targets, err := normalizeTargetResources(f.comparisonResult, nil)

// then
require.NoError(t, err)
Expand All @@ -423,7 +435,7 @@ func TestNormalizeTargetResources(t *testing.T) {
unstructured.RemoveNestedField(live.Object, "metadata", "annotations", "iksm-version")

// when
targets, err := normalizeTargetResources(f.comparisonResult)
targets, err := normalizeTargetResources(f.comparisonResult, nil)

// then
require.NoError(t, err)
Expand All @@ -448,7 +460,7 @@ func TestNormalizeTargetResources(t *testing.T) {
f := setup(t, ignores)

// when
targets, err := normalizeTargetResources(f.comparisonResult)
targets, err := normalizeTargetResources(f.comparisonResult, nil)

// then
require.NoError(t, err)
Expand All @@ -473,11 +485,11 @@ func TestNormalizeTargetResources(t *testing.T) {
},
}
f := setup(t, ignores)
target := test.YamlToUnstructured(testdata.TargetDeploymentNewEntries)
target := withRespectIgnoreDiffs(test.YamlToUnstructured(testdata.TargetDeploymentNewEntries))
f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}

// when
targets, err := normalizeTargetResources(f.comparisonResult)
targets, err := normalizeTargetResources(f.comparisonResult, nil)

// then
require.NoError(t, err)
Expand All @@ -489,6 +501,87 @@ func TestNormalizeTargetResources(t *testing.T) {
})
}

func TestNormalizeTargetResourcesPerResource(t *testing.T) {
ignoreReplicas := v1alpha1.ResourceIgnoreDifferences{
Group: "apps",
Kind: "Deployment",
JSONPointers: []string{"/spec/replicas"},
}
dc, err := diff.NewDiffConfigBuilder().
WithDiffSettings([]v1alpha1.ResourceIgnoreDifferences{ignoreReplicas}, nil, true, normalizers.IgnoreNormalizerOpts{}).
WithNoCache().
Build()
require.NoError(t, err)

annotated := test.YamlToUnstructured(testdata.TargetDeploymentYaml)
annotated.SetAnnotations(map[string]string{synccommon.AnnotationSyncOptions: synccommon.SyncOptionRespectIgnoreDifferences})
unannotated := test.YamlToUnstructured(testdata.TargetDeploymentYaml)

live1 := test.YamlToUnstructured(testdata.LiveDeploymentYaml)
live2 := test.YamlToUnstructured(testdata.LiveDeploymentYaml)
cr := &comparisonResult{
reconciliationResult: sync.ReconciliationResult{
Live: []*unstructured.Unstructured{live1, live2},
Target: []*unstructured.Unstructured{annotated, unannotated},
},
diffConfig: dc,
}

t.Run("normalizes annotated resource, skips unannotated", func(t *testing.T) {
targets, err := normalizeTargetResources(cr, nil)
require.NoError(t, err)
require.Len(t, targets, 2)

// annotated: gets live replicas (4) via JSON merge patch
replicas0, ok, err := unstructured.NestedInt64(targets[0].Object, "spec", "replicas")
require.NoError(t, err)
require.True(t, ok)
assert.Equal(t, int64(4), replicas0)

// unannotated: returned as-is from YAML (float64 from yaml.Unmarshal)
replicas1, ok, err := unstructured.NestedFieldNoCopy(targets[1].Object, "spec", "replicas")
require.NoError(t, err)
require.True(t, ok)
assert.EqualValues(t, 1, replicas1)
})
t.Run("no-op when no annotation and app-level disabled", func(t *testing.T) {
onlyUnannotated := &comparisonResult{
reconciliationResult: sync.ReconciliationResult{
Live: []*unstructured.Unstructured{live1},
Target: []*unstructured.Unstructured{unannotated},
},
diffConfig: dc,
}
targets, err := normalizeTargetResources(onlyUnannotated, nil)
require.NoError(t, err)
assert.Same(t, unannotated, targets[0])
})
t.Run("per-resource disable annotation overrides app-level enabled", func(t *testing.T) {
disabled := test.YamlToUnstructured(testdata.TargetDeploymentYaml)
disabled.SetAnnotations(map[string]string{synccommon.AnnotationSyncOptions: synccommon.SyncOptionDisableRespectIgnoreDifferences})

cr := &comparisonResult{
reconciliationResult: sync.ReconciliationResult{
Live: []*unstructured.Unstructured{live1, live2},
Target: []*unstructured.Unstructured{disabled, unannotated},
},
diffConfig: dc,
}
targets, err := normalizeTargetResources(cr, syncOptsRespectIgnoreDiffs)
require.NoError(t, err)
require.Len(t, targets, 2)

// disabled: annotation overrides app-level; returned as-is
assert.Same(t, disabled, targets[0])

// unannotated: no override, app-level applies; gets live replicas (4)
replicas1, ok, err := unstructured.NestedInt64(targets[1].Object, "spec", "replicas")
require.NoError(t, err)
require.True(t, ok)
assert.Equal(t, int64(4), replicas1)
})
}

func TestNormalizeTargetResourcesWithList(t *testing.T) {
type fixture struct {
comparisonResult *comparisonResult
Expand All @@ -501,7 +594,7 @@ func TestNormalizeTargetResourcesWithList(t *testing.T) {
Build()
require.NoError(t, err)
live := test.YamlToUnstructured(testdata.LiveHTTPProxy)
target := test.YamlToUnstructured(testdata.TargetHTTPProxy)
target := withRespectIgnoreDiffs(test.YamlToUnstructured(testdata.TargetHTTPProxy))
return &fixture{
&comparisonResult{
reconciliationResult: sync.ReconciliationResult{
Expand All @@ -524,11 +617,11 @@ func TestNormalizeTargetResourcesWithList(t *testing.T) {
},
}
f := setupHTTPProxy(t, ignores)
target := test.YamlToUnstructured(testdata.TargetHTTPProxy)
target := withRespectIgnoreDiffs(test.YamlToUnstructured(testdata.TargetHTTPProxy))
f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}

// when
patchedTargets, err := normalizeTargetResources(f.comparisonResult)
patchedTargets, err := normalizeTargetResources(f.comparisonResult, nil)

// then
require.NoError(t, err)
Expand Down Expand Up @@ -562,12 +655,12 @@ func TestNormalizeTargetResourcesWithList(t *testing.T) {
}
f := setupHTTPProxy(t, ignores)
live := test.YamlToUnstructured(testdata.LiveDeploymentEnvVarsYaml)
target := test.YamlToUnstructured(testdata.TargetDeploymentEnvVarsYaml)
target := withRespectIgnoreDiffs(test.YamlToUnstructured(testdata.TargetDeploymentEnvVarsYaml))
f.comparisonResult.reconciliationResult.Live = []*unstructured.Unstructured{live}
f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}

// when
targets, err := normalizeTargetResources(f.comparisonResult)
targets, err := normalizeTargetResources(f.comparisonResult, nil)

// then
require.NoError(t, err)
Expand Down Expand Up @@ -614,12 +707,12 @@ func TestNormalizeTargetResourcesWithList(t *testing.T) {
}
f := setupHTTPProxy(t, ignores)
live := test.YamlToUnstructured(testdata.MinimalImageReplicaDeploymentYaml)
target := test.YamlToUnstructured(testdata.AdditionalImageReplicaDeploymentYaml)
target := withRespectIgnoreDiffs(test.YamlToUnstructured(testdata.AdditionalImageReplicaDeploymentYaml))
f.comparisonResult.reconciliationResult.Live = []*unstructured.Unstructured{live}
f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}

// when
targets, err := normalizeTargetResources(f.comparisonResult)
targets, err := normalizeTargetResources(f.comparisonResult, nil)

// then
require.NoError(t, err)
Expand Down
11 changes: 11 additions & 0 deletions docs/user-guide/sync-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,17 @@ spec:

The example above shows how an Argo CD Application can be configured so it will ignore the `spec.replicas` field from the desired state (git) during the sync stage. This is achieved by calculating and pre-patching the desired state before applying it in the cluster. Note that the `RespectIgnoreDifferences` sync option is only effective when the resource is already created in the cluster. If the Application is being created and no live state exists, the desired state is applied as-is.

This can also be configured at individual resource level using the `argocd.argoproj.io/sync-options` annotation. The per-resource annotation overrides the application-level setting in either direction:

- `RespectIgnoreDifferences=true` enables normalization for a specific resource even when the app-level option is not set.
- `RespectIgnoreDifferences=false` disables normalization for a specific resource even when the app-level option is enabled. It has no effect when the app-level option is not set.

```yaml
metadata:
annotations:
argocd.argoproj.io/sync-options: RespectIgnoreDifferences=true
```

## Create Namespace

```yaml
Expand Down
4 changes: 4 additions & 0 deletions gitops-engine/pkg/sync/common/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ const (
SyncOptionClientSideApplyMigration = "ClientSideApplyMigration=true"
// Sync option that disables client-side apply migration
SyncOptionDisableClientSideApplyMigration = "ClientSideApplyMigration=false"
// Sync option that respects ignoreDifferences configs during the sync stage
SyncOptionRespectIgnoreDifferences = "RespectIgnoreDifferences=true"
// Sync option that disables RespectIgnoreDifferences for a specific resource
SyncOptionDisableRespectIgnoreDifferences = "RespectIgnoreDifferences=false"

// Default field manager for client-side apply migration
DefaultClientSideApplyMigrationManager = "kubectl-client-side-apply"
Expand Down
Loading