Repository: argoproj/argo-cd
Pull request #23357 (2025-06-11 14:07:00)

fix(sync): auto-sync loop when FailOnSharedResource
This PR fixes a loop when auto-sync is on and the application is configured to `FailOnSharedResource`.

Auto-sync will try to compare the revisions in the sync results. However, the syncResult is not initialized on errors returned this early in SyncAppState.

In general, the errors happening early are validation also performed before auto-sync runs, but not in this case.


The real fix to prevent this from happening is https://github.com/argoproj/argo-cd/pull/23356


Patch:
diff --git a/controller/sync.go b/controller/sync.go
index 3f21b79ca..9aed51682 100644
--- a/controller/sync.go
+++ b/controller/sync.go
@@ -114,15 +114,6 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
 	}
 	syncOp = *state.Operation.Sync
 
-	// validates if it should fail the sync if it finds shared resources
-	hasSharedResource, sharedResourceMessage := hasSharedResourceCondition(app)
-	if syncOp.SyncOptions.HasOption("FailOnSharedResource=true") &&
-		hasSharedResource {
-		state.Phase = common.OperationFailed
-		state.Message = fmt.Sprintf("Shared resource found: %s", sharedResourceMessage)
-		return
-	}
-
 	isMultiSourceRevision := app.Spec.HasMultipleSources()
 	rollback := len(syncOp.Sources) > 0 || syncOp.Source != nil
 	if rollback {
@@ -213,6 +204,15 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
 	syncRes.Revision = compareResult.syncStatus.Revision
 	syncRes.Revisions = compareResult.syncStatus.Revisions
 
+	// validates if it should fail the sync if it finds shared resources
+	hasSharedResource, sharedResourceMessage := hasSharedResourceCondition(app)
+	if syncOp.SyncOptions.HasOption("FailOnSharedResource=true") &&
+		hasSharedResource {
+		state.Phase = common.OperationFailed
+		state.Message = fmt.Sprintf("Shared resource found: %s", sharedResourceMessage)
+		return
+	}
+
 	// If there are any comparison or spec errors error conditions do not perform the operation
 	if errConditions := app.Status.GetConditions(map[v1alpha1.ApplicationConditionType]bool{
 		v1alpha1.ApplicationConditionComparisonError:  true,


Test patch:
diff --git a/controller/sync_test.go b/controller/sync_test.go
index 5560ffc2e..b171fb806 100644
--- a/controller/sync_test.go
+++ b/controller/sync_test.go
@@ -15,6 +15,7 @@ import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
 
+	argocommon "github.com/argoproj/argo-cd/v2/common"
 	"github.com/argoproj/argo-cd/v2/controller/testdata"
 	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
 	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
@@ -190,17 +191,23 @@ func TestSyncComparisonError(t *testing.T) {
 }
 
 func TestAppStateManager_SyncAppState(t *testing.T) {
+	t.Parallel()
+
 	type fixture struct {
-		project     *v1alpha1.AppProject
 		application *v1alpha1.Application
+		project     *v1alpha1.AppProject
 		controller  *ApplicationController
 	}
 
-	setup := func() *fixture {
+	setup := func(liveObjects map[kube.ResourceKey]*unstructured.Unstructured) *fixture {
 		app := newFakeApp()
 		app.Status.OperationState = nil
 		app.Status.History = nil
 
+		if liveObjects == nil {
+			liveObjects = make(map[kube.ResourceKey]*unstructured.Unstructured)
+		}
+
 		project := &v1alpha1.AppProject{
 			ObjectMeta: v1.ObjectMeta{
 				Namespace: test.FakeArgoCDNamespace,
@@ -208,6 +215,12 @@ func TestAppStateManager_SyncAppState(t *testing.T) {
 			},
 			Spec: v1alpha1.AppProjectSpec{
 				SignatureKeys: []v1alpha1.SignatureKey{{KeyID: "test"}},
+				Destinations: []v1alpha1.ApplicationDestination{
+					{
+						Namespace: "*",
+						Server:    "*",
+					},
+				},
 			},
 		}
 		data := fakeData{
@@ -218,13 +231,13 @@ func TestAppStateManager_SyncAppState(t *testing.T) {
 				Server:    test.FakeClusterURL,
 				Revision:  "abc123",
 			},
-			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
+			managedLiveObjs: liveObjects,
 		}
 		ctrl := newFakeController(&data, nil)
 
 		return &fixture{
-			project:     project,
 			application: app,
+			project:     project,
 			controller:  ctrl,
 		}
 	}
@@ -232,13 +245,23 @@ func TestAppStateManager_SyncAppState(t *testing.T) {
 	t.Run("will fail the sync if finds shared resources", func(t *testing.T) {
 		// given
 		t.Parallel()
-		f := setup()
-		syncErrorMsg := "deployment already applied by another application"
-		condition := v1alpha1.ApplicationCondition{
-			Type:    v1alpha1.ApplicationConditionSharedResourceWarning,
-			Message: syncErrorMsg,
-		}
-		f.application.Status.Conditions = append(f.application.Status.Conditions, condition)
+
+		sharedObject := kube.MustToUnstructured(&corev1.ConfigMap{
+			TypeMeta: v1.TypeMeta{
+				APIVersion: "v1",
+				Kind:       "ConfigMap",
+			},
+			ObjectMeta: v1.ObjectMeta{
+				Name:      "configmap1",
+				Namespace: "default",
+				Labels: map[string]string{
+					argocommon.LabelKeyAppInstance: "another-app",
+				},
+			},
+		})
+		liveObjects := make(map[kube.ResourceKey]*unstructured.Unstructured)
+		liveObjects[kube.GetResourceKey(sharedObject)] = sharedObject
+		f := setup(liveObjects)
 
 		// Sync with source unspecified
 		opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
@@ -253,7 +276,7 @@ func TestAppStateManager_SyncAppState(t *testing.T) {
 
 		// then
 		assert.Equal(t, common.OperationFailed, opState.Phase)
-		assert.Contains(t, opState.Message, syncErrorMsg)
+		assert.Contains(t, opState.Message, "ConfigMap/configmap1 is part of applications fake-argocd-ns/my-app and another-app")
 	})
 }