github.com/argoproj/argo-cd/v3@v3.2.1/controller/sync_test.go (about)

     1  package controller
     2  
     3  import (
     4  	"strconv"
     5  	"testing"
     6  
     7  	"github.com/argoproj/gitops-engine/pkg/sync"
     8  	synccommon "github.com/argoproj/gitops-engine/pkg/sync/common"
     9  	"github.com/argoproj/gitops-engine/pkg/utils/kube"
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  	corev1 "k8s.io/api/core/v1"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    15  	"k8s.io/apimachinery/pkg/runtime"
    16  
    17  	"github.com/argoproj/argo-cd/v3/common"
    18  	"github.com/argoproj/argo-cd/v3/controller/testdata"
    19  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    20  	"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
    21  	"github.com/argoproj/argo-cd/v3/test"
    22  	"github.com/argoproj/argo-cd/v3/util/argo/diff"
    23  	"github.com/argoproj/argo-cd/v3/util/argo/normalizers"
    24  )
    25  
    26  func TestPersistRevisionHistory(t *testing.T) {
    27  	app := newFakeApp()
    28  	app.Status.OperationState = nil
    29  	app.Status.History = nil
    30  
    31  	defaultProject := &v1alpha1.AppProject{
    32  		ObjectMeta: metav1.ObjectMeta{
    33  			Namespace: test.FakeArgoCDNamespace,
    34  			Name:      "default",
    35  		},
    36  	}
    37  	data := fakeData{
    38  		apps: []runtime.Object{app, defaultProject},
    39  		manifestResponse: &apiclient.ManifestResponse{
    40  			Manifests: []string{},
    41  			Namespace: test.FakeDestNamespace,
    42  			Server:    test.FakeClusterURL,
    43  			Revision:  "abc123",
    44  		},
    45  		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
    46  	}
    47  	ctrl := newFakeController(&data, nil)
    48  
    49  	// Sync with source unspecified
    50  	opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
    51  		Sync: &v1alpha1.SyncOperation{},
    52  	}}
    53  	ctrl.appStateManager.SyncAppState(app, defaultProject, opState)
    54  	// Ensure we record spec.source into sync result
    55  	assert.Equal(t, app.Spec.GetSource(), opState.SyncResult.Source)
    56  
    57  	updatedApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(t.Context(), app.Name, metav1.GetOptions{})
    58  	require.NoError(t, err)
    59  	require.Len(t, updatedApp.Status.History, 1)
    60  	assert.Equal(t, app.Spec.GetSource(), updatedApp.Status.History[0].Source)
    61  	assert.Equal(t, "abc123", updatedApp.Status.History[0].Revision)
    62  }
    63  
    64  func TestPersistManagedNamespaceMetadataState(t *testing.T) {
    65  	app := newFakeApp()
    66  	app.Status.OperationState = nil
    67  	app.Status.History = nil
    68  	app.Spec.SyncPolicy.ManagedNamespaceMetadata = &v1alpha1.ManagedNamespaceMetadata{
    69  		Labels: map[string]string{
    70  			"foo": "bar",
    71  		},
    72  		Annotations: map[string]string{
    73  			"foo": "bar",
    74  		},
    75  	}
    76  
    77  	defaultProject := &v1alpha1.AppProject{
    78  		ObjectMeta: metav1.ObjectMeta{
    79  			Namespace: test.FakeArgoCDNamespace,
    80  			Name:      "default",
    81  		},
    82  	}
    83  	data := fakeData{
    84  		apps: []runtime.Object{app, defaultProject},
    85  		manifestResponse: &apiclient.ManifestResponse{
    86  			Manifests: []string{},
    87  			Namespace: test.FakeDestNamespace,
    88  			Server:    test.FakeClusterURL,
    89  			Revision:  "abc123",
    90  		},
    91  		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
    92  	}
    93  	ctrl := newFakeController(&data, nil)
    94  
    95  	// Sync with source unspecified
    96  	opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
    97  		Sync: &v1alpha1.SyncOperation{},
    98  	}}
    99  	ctrl.appStateManager.SyncAppState(app, defaultProject, opState)
   100  	// Ensure we record spec.syncPolicy.managedNamespaceMetadata into sync result
   101  	assert.Equal(t, app.Spec.SyncPolicy.ManagedNamespaceMetadata, opState.SyncResult.ManagedNamespaceMetadata)
   102  }
   103  
   104  func TestPersistRevisionHistoryRollback(t *testing.T) {
   105  	app := newFakeApp()
   106  	app.Status.OperationState = nil
   107  	app.Status.History = nil
   108  	defaultProject := &v1alpha1.AppProject{
   109  		ObjectMeta: metav1.ObjectMeta{
   110  			Namespace: test.FakeArgoCDNamespace,
   111  			Name:      "default",
   112  		},
   113  	}
   114  	data := fakeData{
   115  		apps: []runtime.Object{app, defaultProject},
   116  		manifestResponse: &apiclient.ManifestResponse{
   117  			Manifests: []string{},
   118  			Namespace: test.FakeDestNamespace,
   119  			Server:    test.FakeClusterURL,
   120  			Revision:  "abc123",
   121  		},
   122  		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
   123  	}
   124  	ctrl := newFakeController(&data, nil)
   125  
   126  	// Sync with source specified
   127  	source := v1alpha1.ApplicationSource{
   128  		Helm: &v1alpha1.ApplicationSourceHelm{
   129  			Parameters: []v1alpha1.HelmParameter{
   130  				{
   131  					Name:  "test",
   132  					Value: "123",
   133  				},
   134  			},
   135  		},
   136  	}
   137  	opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
   138  		Sync: &v1alpha1.SyncOperation{
   139  			Source: &source,
   140  		},
   141  	}}
   142  	ctrl.appStateManager.SyncAppState(app, defaultProject, opState)
   143  	// Ensure we record opState's source into sync result
   144  	assert.Equal(t, source, opState.SyncResult.Source)
   145  
   146  	updatedApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(t.Context(), app.Name, metav1.GetOptions{})
   147  	require.NoError(t, err)
   148  	assert.Len(t, updatedApp.Status.History, 1)
   149  	assert.Equal(t, source, updatedApp.Status.History[0].Source)
   150  	assert.Equal(t, "abc123", updatedApp.Status.History[0].Revision)
   151  }
   152  
   153  func TestSyncComparisonError(t *testing.T) {
   154  	app := newFakeApp()
   155  	app.Status.OperationState = nil
   156  	app.Status.History = nil
   157  
   158  	defaultProject := &v1alpha1.AppProject{
   159  		ObjectMeta: metav1.ObjectMeta{
   160  			Namespace: test.FakeArgoCDNamespace,
   161  			Name:      "default",
   162  		},
   163  		Spec: v1alpha1.AppProjectSpec{
   164  			SignatureKeys: []v1alpha1.SignatureKey{{KeyID: "test"}},
   165  		},
   166  	}
   167  	data := fakeData{
   168  		apps: []runtime.Object{app, defaultProject},
   169  		manifestResponse: &apiclient.ManifestResponse{
   170  			Manifests:    []string{},
   171  			Namespace:    test.FakeDestNamespace,
   172  			Server:       test.FakeClusterURL,
   173  			Revision:     "abc123",
   174  			VerifyResult: "something went wrong",
   175  		},
   176  		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
   177  	}
   178  	ctrl := newFakeController(&data, nil)
   179  
   180  	// Sync with source unspecified
   181  	opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
   182  		Sync: &v1alpha1.SyncOperation{},
   183  	}}
   184  	t.Setenv("ARGOCD_GPG_ENABLED", "true")
   185  	ctrl.appStateManager.SyncAppState(app, defaultProject, opState)
   186  
   187  	conditions := app.Status.GetConditions(map[v1alpha1.ApplicationConditionType]bool{v1alpha1.ApplicationConditionComparisonError: true})
   188  	assert.NotEmpty(t, conditions)
   189  	assert.Equal(t, "abc123", opState.SyncResult.Revision)
   190  }
   191  
   192  func TestAppStateManager_SyncAppState(t *testing.T) {
   193  	t.Parallel()
   194  
   195  	type fixture struct {
   196  		application *v1alpha1.Application
   197  		project     *v1alpha1.AppProject
   198  		controller  *ApplicationController
   199  	}
   200  
   201  	setup := func(liveObjects map[kube.ResourceKey]*unstructured.Unstructured) *fixture {
   202  		app := newFakeApp()
   203  		app.Status.OperationState = nil
   204  		app.Status.History = nil
   205  
   206  		if liveObjects == nil {
   207  			liveObjects = make(map[kube.ResourceKey]*unstructured.Unstructured)
   208  		}
   209  
   210  		project := &v1alpha1.AppProject{
   211  			ObjectMeta: metav1.ObjectMeta{
   212  				Namespace: test.FakeArgoCDNamespace,
   213  				Name:      "default",
   214  			},
   215  			Spec: v1alpha1.AppProjectSpec{
   216  				SignatureKeys: []v1alpha1.SignatureKey{{KeyID: "test"}},
   217  				Destinations: []v1alpha1.ApplicationDestination{
   218  					{
   219  						Namespace: "*",
   220  						Server:    "*",
   221  					},
   222  				},
   223  			},
   224  		}
   225  		data := fakeData{
   226  			apps: []runtime.Object{app, project},
   227  			manifestResponse: &apiclient.ManifestResponse{
   228  				Manifests: []string{},
   229  				Namespace: test.FakeDestNamespace,
   230  				Server:    test.FakeClusterURL,
   231  				Revision:  "abc123",
   232  			},
   233  			managedLiveObjs: liveObjects,
   234  		}
   235  		ctrl := newFakeController(&data, nil)
   236  
   237  		return &fixture{
   238  			application: app,
   239  			project:     project,
   240  			controller:  ctrl,
   241  		}
   242  	}
   243  
   244  	t.Run("will fail the sync if finds shared resources", func(t *testing.T) {
   245  		// given
   246  		t.Parallel()
   247  
   248  		sharedObject := kube.MustToUnstructured(&corev1.ConfigMap{
   249  			TypeMeta: metav1.TypeMeta{
   250  				APIVersion: "v1",
   251  				Kind:       "ConfigMap",
   252  			},
   253  			ObjectMeta: metav1.ObjectMeta{
   254  				Name:      "configmap1",
   255  				Namespace: "default",
   256  				Annotations: map[string]string{
   257  					common.AnnotationKeyAppInstance: "guestbook:/ConfigMap:default/configmap1",
   258  				},
   259  			},
   260  		})
   261  		liveObjects := make(map[kube.ResourceKey]*unstructured.Unstructured)
   262  		liveObjects[kube.GetResourceKey(sharedObject)] = sharedObject
   263  		f := setup(liveObjects)
   264  
   265  		// Sync with source unspecified
   266  		opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
   267  			Sync: &v1alpha1.SyncOperation{
   268  				Source:      &v1alpha1.ApplicationSource{},
   269  				SyncOptions: []string{"FailOnSharedResource=true"},
   270  			},
   271  		}}
   272  
   273  		// when
   274  		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)
   275  
   276  		// then
   277  		assert.Equal(t, synccommon.OperationFailed, opState.Phase)
   278  		assert.Contains(t, opState.Message, "ConfigMap/configmap1 is part of applications fake-argocd-ns/my-app and guestbook")
   279  	})
   280  }
   281  
   282  func TestSyncWindowDeniesSync(t *testing.T) {
   283  	t.Parallel()
   284  
   285  	type fixture struct {
   286  		application *v1alpha1.Application
   287  		project     *v1alpha1.AppProject
   288  		controller  *ApplicationController
   289  	}
   290  
   291  	setup := func() *fixture {
   292  		app := newFakeApp()
   293  		app.Status.OperationState = nil
   294  		app.Status.History = nil
   295  
   296  		project := &v1alpha1.AppProject{
   297  			ObjectMeta: metav1.ObjectMeta{
   298  				Namespace: test.FakeArgoCDNamespace,
   299  				Name:      "default",
   300  			},
   301  			Spec: v1alpha1.AppProjectSpec{
   302  				SyncWindows: v1alpha1.SyncWindows{{
   303  					Kind:         "deny",
   304  					Schedule:     "0 0 * * *",
   305  					Duration:     "24h",
   306  					Clusters:     []string{"*"},
   307  					Namespaces:   []string{"*"},
   308  					Applications: []string{"*"},
   309  				}},
   310  			},
   311  		}
   312  		data := fakeData{
   313  			apps: []runtime.Object{app, project},
   314  			manifestResponse: &apiclient.ManifestResponse{
   315  				Manifests: []string{},
   316  				Namespace: test.FakeDestNamespace,
   317  				Server:    test.FakeClusterURL,
   318  				Revision:  "abc123",
   319  			},
   320  			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
   321  		}
   322  		ctrl := newFakeController(&data, nil)
   323  
   324  		return &fixture{
   325  			application: app,
   326  			project:     project,
   327  			controller:  ctrl,
   328  		}
   329  	}
   330  
   331  	t.Run("will keep the sync progressing if a sync window prevents the sync", func(t *testing.T) {
   332  		// given a project with an active deny sync window and an operation in progress
   333  		t.Parallel()
   334  		f := setup()
   335  		opMessage := "Sync operation blocked by sync window"
   336  
   337  		opState := &v1alpha1.OperationState{
   338  			Operation: v1alpha1.Operation{
   339  				Sync: &v1alpha1.SyncOperation{
   340  					Source: &v1alpha1.ApplicationSource{},
   341  				},
   342  			},
   343  			Phase: synccommon.OperationRunning,
   344  		}
   345  		// when
   346  		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)
   347  
   348  		// then
   349  		assert.Equal(t, synccommon.OperationRunning, opState.Phase)
   350  		assert.Contains(t, opState.Message, opMessage)
   351  	})
   352  }
   353  
   354  func TestNormalizeTargetResources(t *testing.T) {
   355  	type fixture struct {
   356  		comparisonResult *comparisonResult
   357  	}
   358  	setup := func(t *testing.T, ignores []v1alpha1.ResourceIgnoreDifferences) *fixture {
   359  		t.Helper()
   360  		dc, err := diff.NewDiffConfigBuilder().
   361  			WithDiffSettings(ignores, nil, true, normalizers.IgnoreNormalizerOpts{}).
   362  			WithNoCache().
   363  			Build()
   364  		require.NoError(t, err)
   365  		live := test.YamlToUnstructured(testdata.LiveDeploymentYaml)
   366  		target := test.YamlToUnstructured(testdata.TargetDeploymentYaml)
   367  		return &fixture{
   368  			&comparisonResult{
   369  				reconciliationResult: sync.ReconciliationResult{
   370  					Live:   []*unstructured.Unstructured{live},
   371  					Target: []*unstructured.Unstructured{target},
   372  				},
   373  				diffConfig: dc,
   374  			},
   375  		}
   376  	}
   377  	t.Run("will modify target resource adding live state in fields it should ignore", func(t *testing.T) {
   378  		// given
   379  		ignore := v1alpha1.ResourceIgnoreDifferences{
   380  			Group:                 "*",
   381  			Kind:                  "*",
   382  			ManagedFieldsManagers: []string{"janitor"},
   383  		}
   384  		ignores := []v1alpha1.ResourceIgnoreDifferences{ignore}
   385  		f := setup(t, ignores)
   386  
   387  		// when
   388  		targets, err := normalizeTargetResources(f.comparisonResult)
   389  
   390  		// then
   391  		require.NoError(t, err)
   392  		require.Len(t, targets, 1)
   393  		iksmVersion := targets[0].GetAnnotations()["iksm-version"]
   394  		assert.Equal(t, "2.0", iksmVersion)
   395  	})
   396  	t.Run("will not modify target resource if ignore difference is not configured", func(t *testing.T) {
   397  		// given
   398  		f := setup(t, []v1alpha1.ResourceIgnoreDifferences{})
   399  
   400  		// when
   401  		targets, err := normalizeTargetResources(f.comparisonResult)
   402  
   403  		// then
   404  		require.NoError(t, err)
   405  		require.Len(t, targets, 1)
   406  		iksmVersion := targets[0].GetAnnotations()["iksm-version"]
   407  		assert.Equal(t, "1.0", iksmVersion)
   408  	})
   409  	t.Run("will remove fields from target if not present in live", func(t *testing.T) {
   410  		ignore := v1alpha1.ResourceIgnoreDifferences{
   411  			Group:        "apps",
   412  			Kind:         "Deployment",
   413  			JSONPointers: []string{"/metadata/annotations/iksm-version"},
   414  		}
   415  		ignores := []v1alpha1.ResourceIgnoreDifferences{ignore}
   416  		f := setup(t, ignores)
   417  		live := f.comparisonResult.reconciliationResult.Live[0]
   418  		unstructured.RemoveNestedField(live.Object, "metadata", "annotations", "iksm-version")
   419  
   420  		// when
   421  		targets, err := normalizeTargetResources(f.comparisonResult)
   422  
   423  		// then
   424  		require.NoError(t, err)
   425  		require.Len(t, targets, 1)
   426  		_, ok := targets[0].GetAnnotations()["iksm-version"]
   427  		assert.False(t, ok)
   428  	})
   429  	t.Run("will correctly normalize with multiple ignore configurations", func(t *testing.T) {
   430  		// given
   431  		ignores := []v1alpha1.ResourceIgnoreDifferences{
   432  			{
   433  				Group:        "apps",
   434  				Kind:         "Deployment",
   435  				JSONPointers: []string{"/spec/replicas"},
   436  			},
   437  			{
   438  				Group:                 "*",
   439  				Kind:                  "*",
   440  				ManagedFieldsManagers: []string{"janitor"},
   441  			},
   442  		}
   443  		f := setup(t, ignores)
   444  
   445  		// when
   446  		targets, err := normalizeTargetResources(f.comparisonResult)
   447  
   448  		// then
   449  		require.NoError(t, err)
   450  		require.Len(t, targets, 1)
   451  		normalized := targets[0]
   452  		iksmVersion, ok := normalized.GetAnnotations()["iksm-version"]
   453  		require.True(t, ok)
   454  		assert.Equal(t, "2.0", iksmVersion)
   455  		replicas, ok, err := unstructured.NestedInt64(normalized.Object, "spec", "replicas")
   456  		require.NoError(t, err)
   457  		require.True(t, ok)
   458  		assert.Equal(t, int64(4), replicas)
   459  	})
   460  	t.Run("will keep new array entries not found in live state if not ignored", func(t *testing.T) {
   461  		t.Skip("limitation in the current implementation")
   462  		// given
   463  		ignores := []v1alpha1.ResourceIgnoreDifferences{
   464  			{
   465  				Group:             "apps",
   466  				Kind:              "Deployment",
   467  				JQPathExpressions: []string{".spec.template.spec.containers[] | select(.name == \"guestbook-ui\")"},
   468  			},
   469  		}
   470  		f := setup(t, ignores)
   471  		target := test.YamlToUnstructured(testdata.TargetDeploymentNewEntries)
   472  		f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}
   473  
   474  		// when
   475  		targets, err := normalizeTargetResources(f.comparisonResult)
   476  
   477  		// then
   478  		require.NoError(t, err)
   479  		require.Len(t, targets, 1)
   480  		containers, ok, err := unstructured.NestedSlice(targets[0].Object, "spec", "template", "spec", "containers")
   481  		require.NoError(t, err)
   482  		require.True(t, ok)
   483  		assert.Len(t, containers, 2)
   484  	})
   485  }
   486  
   487  func TestNormalizeTargetResourcesWithList(t *testing.T) {
   488  	type fixture struct {
   489  		comparisonResult *comparisonResult
   490  	}
   491  	setupHTTPProxy := func(t *testing.T, ignores []v1alpha1.ResourceIgnoreDifferences) *fixture {
   492  		t.Helper()
   493  		dc, err := diff.NewDiffConfigBuilder().
   494  			WithDiffSettings(ignores, nil, true, normalizers.IgnoreNormalizerOpts{}).
   495  			WithNoCache().
   496  			Build()
   497  		require.NoError(t, err)
   498  		live := test.YamlToUnstructured(testdata.LiveHTTPProxy)
   499  		target := test.YamlToUnstructured(testdata.TargetHTTPProxy)
   500  		return &fixture{
   501  			&comparisonResult{
   502  				reconciliationResult: sync.ReconciliationResult{
   503  					Live:   []*unstructured.Unstructured{live},
   504  					Target: []*unstructured.Unstructured{target},
   505  				},
   506  				diffConfig: dc,
   507  			},
   508  		}
   509  	}
   510  
   511  	t.Run("will properly ignore nested fields within arrays", func(t *testing.T) {
   512  		// given
   513  		ignores := []v1alpha1.ResourceIgnoreDifferences{
   514  			{
   515  				Group:             "projectcontour.io",
   516  				Kind:              "HTTPProxy",
   517  				JQPathExpressions: []string{".spec.routes[]"},
   518  				// JSONPointers: []string{"/spec/routes"},
   519  			},
   520  		}
   521  		f := setupHTTPProxy(t, ignores)
   522  		target := test.YamlToUnstructured(testdata.TargetHTTPProxy)
   523  		f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}
   524  
   525  		// when
   526  		patchedTargets, err := normalizeTargetResources(f.comparisonResult)
   527  
   528  		// then
   529  		require.NoError(t, err)
   530  		require.Len(t, f.comparisonResult.reconciliationResult.Live, 1)
   531  		require.Len(t, f.comparisonResult.reconciliationResult.Target, 1)
   532  		require.Len(t, patchedTargets, 1)
   533  
   534  		// live should have 1 entry
   535  		require.Len(t, dig(f.comparisonResult.reconciliationResult.Live[0].Object, "spec", "routes", 0, "rateLimitPolicy", "global", "descriptors"), 1)
   536  		// assert some arbitrary field to show `entries[0]` is not an empty object
   537  		require.Equal(t, "sample-header", dig(f.comparisonResult.reconciliationResult.Live[0].Object, "spec", "routes", 0, "rateLimitPolicy", "global", "descriptors", 0, "entries", 0, "requestHeader", "headerName"))
   538  
   539  		// target has 2 entries
   540  		require.Len(t, dig(f.comparisonResult.reconciliationResult.Target[0].Object, "spec", "routes", 0, "rateLimitPolicy", "global", "descriptors", 0, "entries"), 2)
   541  		// assert some arbitrary field to show `entries[0]` is not an empty object
   542  		require.Equal(t, "sample-header", dig(f.comparisonResult.reconciliationResult.Target[0].Object, "spec", "routes", 0, "rateLimitPolicy", "global", "descriptors", 0, "entries", 0, "requestHeaderValueMatch", "headers", 0, "name"))
   543  
   544  		// It should be *1* entries in the array
   545  		require.Len(t, dig(patchedTargets[0].Object, "spec", "routes", 0, "rateLimitPolicy", "global", "descriptors"), 1)
   546  		// and it should NOT equal an empty object
   547  		require.Len(t, dig(patchedTargets[0].Object, "spec", "routes", 0, "rateLimitPolicy", "global", "descriptors", 0, "entries", 0), 1)
   548  	})
   549  	t.Run("will correctly set array entries if new entries have been added", func(t *testing.T) {
   550  		// given
   551  		ignores := []v1alpha1.ResourceIgnoreDifferences{
   552  			{
   553  				Group:             "apps",
   554  				Kind:              "Deployment",
   555  				JQPathExpressions: []string{".spec.template.spec.containers[].env[] | select(.name == \"SOME_ENV_VAR\")"},
   556  			},
   557  		}
   558  		f := setupHTTPProxy(t, ignores)
   559  		live := test.YamlToUnstructured(testdata.LiveDeploymentEnvVarsYaml)
   560  		target := test.YamlToUnstructured(testdata.TargetDeploymentEnvVarsYaml)
   561  		f.comparisonResult.reconciliationResult.Live = []*unstructured.Unstructured{live}
   562  		f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}
   563  
   564  		// when
   565  		targets, err := normalizeTargetResources(f.comparisonResult)
   566  
   567  		// then
   568  		require.NoError(t, err)
   569  		require.Len(t, targets, 1)
   570  		containers, ok, err := unstructured.NestedSlice(targets[0].Object, "spec", "template", "spec", "containers")
   571  		require.NoError(t, err)
   572  		require.True(t, ok)
   573  		assert.Len(t, containers, 1)
   574  
   575  		ports := containers[0].(map[string]any)["ports"].([]any)
   576  		assert.Len(t, ports, 1)
   577  
   578  		env := containers[0].(map[string]any)["env"].([]any)
   579  		assert.Len(t, env, 3)
   580  
   581  		first := env[0]
   582  		second := env[1]
   583  		third := env[2]
   584  
   585  		// Currently the defined order at this time is the insertion order of the target manifest.
   586  		assert.Equal(t, "SOME_ENV_VAR", first.(map[string]any)["name"])
   587  		assert.Equal(t, "some_value", first.(map[string]any)["value"])
   588  
   589  		assert.Equal(t, "SOME_OTHER_ENV_VAR", second.(map[string]any)["name"])
   590  		assert.Equal(t, "some_other_value", second.(map[string]any)["value"])
   591  
   592  		assert.Equal(t, "YET_ANOTHER_ENV_VAR", third.(map[string]any)["name"])
   593  		assert.Equal(t, "yet_another_value", third.(map[string]any)["value"])
   594  	})
   595  
   596  	t.Run("ignore-deployment-image-replicas-changes-additive", func(t *testing.T) {
   597  		// given
   598  
   599  		ignores := []v1alpha1.ResourceIgnoreDifferences{
   600  			{
   601  				Group:        "apps",
   602  				Kind:         "Deployment",
   603  				JSONPointers: []string{"/spec/replicas"},
   604  			}, {
   605  				Group:             "apps",
   606  				Kind:              "Deployment",
   607  				JQPathExpressions: []string{".spec.template.spec.containers[].image"},
   608  			},
   609  		}
   610  		f := setupHTTPProxy(t, ignores)
   611  		live := test.YamlToUnstructured(testdata.MinimalImageReplicaDeploymentYaml)
   612  		target := test.YamlToUnstructured(testdata.AdditionalImageReplicaDeploymentYaml)
   613  		f.comparisonResult.reconciliationResult.Live = []*unstructured.Unstructured{live}
   614  		f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}
   615  
   616  		// when
   617  		targets, err := normalizeTargetResources(f.comparisonResult)
   618  
   619  		// then
   620  		require.NoError(t, err)
   621  		require.Len(t, targets, 1)
   622  		metadata, ok, err := unstructured.NestedMap(targets[0].Object, "metadata")
   623  		require.NoError(t, err)
   624  		require.True(t, ok)
   625  		labels, ok := metadata["labels"].(map[string]any)
   626  		require.True(t, ok)
   627  		assert.Len(t, labels, 2)
   628  		assert.Equal(t, "web", labels["appProcess"])
   629  
   630  		spec, ok, err := unstructured.NestedMap(targets[0].Object, "spec")
   631  		require.NoError(t, err)
   632  		require.True(t, ok)
   633  
   634  		assert.Equal(t, int64(1), spec["replicas"])
   635  
   636  		template, ok := spec["template"].(map[string]any)
   637  		require.True(t, ok)
   638  
   639  		tMetadata, ok := template["metadata"].(map[string]any)
   640  		require.True(t, ok)
   641  		tLabels, ok := tMetadata["labels"].(map[string]any)
   642  		require.True(t, ok)
   643  		assert.Len(t, tLabels, 2)
   644  		assert.Equal(t, "web", tLabels["appProcess"])
   645  
   646  		tSpec, ok := template["spec"].(map[string]any)
   647  		require.True(t, ok)
   648  		containers, ok, err := unstructured.NestedSlice(tSpec, "containers")
   649  		require.NoError(t, err)
   650  		require.True(t, ok)
   651  		assert.Len(t, containers, 1)
   652  
   653  		first := containers[0].(map[string]any)
   654  		assert.Equal(t, "alpine:3", first["image"])
   655  
   656  		resources, ok := first["resources"].(map[string]any)
   657  		require.True(t, ok)
   658  		requests, ok := resources["requests"].(map[string]any)
   659  		require.True(t, ok)
   660  		assert.Equal(t, "400m", requests["cpu"])
   661  
   662  		env, ok, err := unstructured.NestedSlice(first, "env")
   663  		require.NoError(t, err)
   664  		require.True(t, ok)
   665  		assert.Len(t, env, 1)
   666  
   667  		env0 := env[0].(map[string]any)
   668  		assert.Equal(t, "EV", env0["name"])
   669  		assert.Equal(t, "here", env0["value"])
   670  	})
   671  }
   672  
   673  func TestDeriveServiceAccountMatchingNamespaces(t *testing.T) {
   674  	t.Parallel()
   675  
   676  	type fixture struct {
   677  		project     *v1alpha1.AppProject
   678  		application *v1alpha1.Application
   679  		cluster     *v1alpha1.Cluster
   680  	}
   681  
   682  	setup := func(destinationServiceAccounts []v1alpha1.ApplicationDestinationServiceAccount, destinationNamespace, destinationServerURL, applicationNamespace string) *fixture {
   683  		project := &v1alpha1.AppProject{
   684  			ObjectMeta: metav1.ObjectMeta{
   685  				Namespace: "argocd-ns",
   686  				Name:      "testProj",
   687  			},
   688  			Spec: v1alpha1.AppProjectSpec{
   689  				DestinationServiceAccounts: destinationServiceAccounts,
   690  			},
   691  		}
   692  		app := &v1alpha1.Application{
   693  			ObjectMeta: metav1.ObjectMeta{
   694  				Namespace: applicationNamespace,
   695  				Name:      "testApp",
   696  			},
   697  			Spec: v1alpha1.ApplicationSpec{
   698  				Project: "testProj",
   699  				Destination: v1alpha1.ApplicationDestination{
   700  					Server:    destinationServerURL,
   701  					Namespace: destinationNamespace,
   702  				},
   703  			},
   704  		}
   705  		cluster := &v1alpha1.Cluster{
   706  			Server: "https://kubernetes.svc.local",
   707  			Name:   "test-cluster",
   708  		}
   709  		return &fixture{
   710  			project:     project,
   711  			application: app,
   712  			cluster:     cluster,
   713  		}
   714  	}
   715  
   716  	t.Run("empty destination service accounts", func(t *testing.T) {
   717  		// given an application referring a project with no destination service accounts
   718  		t.Parallel()
   719  		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{}
   720  		destinationNamespace := "testns"
   721  		destinationServerURL := "https://kubernetes.svc.local"
   722  		applicationNamespace := "argocd-ns"
   723  		expectedSA := ""
   724  		expectedErrMsg := "no matching service account found for destination server https://kubernetes.svc.local and namespace testns"
   725  
   726  		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
   727  		// when
   728  		sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
   729  		assert.Equal(t, expectedSA, sa)
   730  
   731  		// then, there should be an error saying no valid match was found
   732  		assert.EqualError(t, err, expectedErrMsg)
   733  	})
   734  
   735  	t.Run("exact match of destination namespace", func(t *testing.T) {
   736  		// given an application referring a project with exactly one destination service account that matches the application destination,
   737  		t.Parallel()
   738  		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
   739  			{
   740  				Server:                "https://kubernetes.svc.local",
   741  				Namespace:             "testns",
   742  				DefaultServiceAccount: "test-sa",
   743  			},
   744  		}
   745  		destinationNamespace := "testns"
   746  		destinationServerURL := "https://kubernetes.svc.local"
   747  		applicationNamespace := "argocd-ns"
   748  		expectedSA := "system:serviceaccount:testns:test-sa"
   749  
   750  		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
   751  		// when
   752  		sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
   753  
   754  		// then, there should be no error and should use the right service account for impersonation
   755  		require.NoError(t, err)
   756  		assert.Equal(t, expectedSA, sa)
   757  	})
   758  
   759  	t.Run("exact one match with multiple destination service accounts", func(t *testing.T) {
   760  		// given an application referring a project with multiple destination service accounts having one exact match for application destination
   761  		t.Parallel()
   762  		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
   763  			{
   764  				Server:                "https://kubernetes.svc.local",
   765  				Namespace:             "guestbook",
   766  				DefaultServiceAccount: "guestbook-sa",
   767  			},
   768  			{
   769  				Server:                "https://kubernetes.svc.local",
   770  				Namespace:             "guestbook-test",
   771  				DefaultServiceAccount: "guestbook-test-sa",
   772  			},
   773  			{
   774  				Server:                "https://kubernetes.svc.local",
   775  				Namespace:             "default",
   776  				DefaultServiceAccount: "default-sa",
   777  			},
   778  			{
   779  				Server:                "https://kubernetes.svc.local",
   780  				Namespace:             "testns",
   781  				DefaultServiceAccount: "test-sa",
   782  			},
   783  		}
   784  		destinationNamespace := "testns"
   785  		destinationServerURL := "https://kubernetes.svc.local"
   786  		applicationNamespace := "argocd-ns"
   787  		expectedSA := "system:serviceaccount:testns:test-sa"
   788  
   789  		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
   790  		// when
   791  		sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
   792  
   793  		// then, there should be no error and should use the right service account for impersonation
   794  		require.NoError(t, err)
   795  		assert.Equal(t, expectedSA, sa)
   796  	})
   797  
   798  	t.Run("first match to be used when multiple matches are available", func(t *testing.T) {
   799  		// given an application referring a project with multiple destination service accounts having multiple match for application destination
   800  		t.Parallel()
   801  		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
   802  			{
   803  				Server:                "https://kubernetes.svc.local",
   804  				Namespace:             "testns",
   805  				DefaultServiceAccount: "test-sa",
   806  			},
   807  			{
   808  				Server:                "https://kubernetes.svc.local",
   809  				Namespace:             "testns",
   810  				DefaultServiceAccount: "test-sa-2",
   811  			},
   812  			{
   813  				Server:                "https://kubernetes.svc.local",
   814  				Namespace:             "testns",
   815  				DefaultServiceAccount: "test-sa-3",
   816  			},
   817  			{
   818  				Server:                "https://kubernetes.svc.local",
   819  				Namespace:             "guestbook",
   820  				DefaultServiceAccount: "guestbook-sa",
   821  			},
   822  		}
   823  		destinationNamespace := "testns"
   824  		destinationServerURL := "https://kubernetes.svc.local"
   825  		applicationNamespace := "argocd-ns"
   826  		expectedSA := "system:serviceaccount:testns:test-sa"
   827  
   828  		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
   829  		// when
   830  		sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
   831  
   832  		// then, there should be no error and it should use the first matching service account for impersonation
   833  		require.NoError(t, err)
   834  		assert.Equal(t, expectedSA, sa)
   835  	})
   836  
   837  	t.Run("first match to be used when glob pattern is used", func(t *testing.T) {
   838  		// given an application referring a project with multiple destination service accounts with glob patterns matching the application destination
   839  		t.Parallel()
   840  		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
   841  			{
   842  				Server:                "https://kubernetes.svc.local",
   843  				Namespace:             "test*",
   844  				DefaultServiceAccount: "test-sa",
   845  			},
   846  			{
   847  				Server:                "https://kubernetes.svc.local",
   848  				Namespace:             "testns",
   849  				DefaultServiceAccount: "test-sa-2",
   850  			},
   851  			{
   852  				Server:                "https://kubernetes.svc.local",
   853  				Namespace:             "default",
   854  				DefaultServiceAccount: "default-sa",
   855  			},
   856  		}
   857  		destinationNamespace := "testns"
   858  		destinationServerURL := "https://kubernetes.svc.local"
   859  		applicationNamespace := "argocd-ns"
   860  		expectedSA := "system:serviceaccount:testns:test-sa"
   861  
   862  		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
   863  		// when
   864  		sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
   865  
   866  		// then, there should not be any error and should use the first matching glob pattern service account for impersonation
   867  		require.NoError(t, err)
   868  		assert.Equal(t, expectedSA, sa)
   869  	})
   870  
   871  	t.Run("no match among a valid list", func(t *testing.T) {
   872  		// given an application referring a project with multiple destination service accounts with no matches for application destination
   873  		t.Parallel()
   874  		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
   875  			{
   876  				Server:                "https://kubernetes.svc.local",
   877  				Namespace:             "test1",
   878  				DefaultServiceAccount: "test-sa",
   879  			},
   880  			{
   881  				Server:                "https://kubernetes.svc.local",
   882  				Namespace:             "test2",
   883  				DefaultServiceAccount: "test-sa-2",
   884  			},
   885  			{
   886  				Server:                "https://kubernetes.svc.local",
   887  				Namespace:             "default",
   888  				DefaultServiceAccount: "default-sa",
   889  			},
   890  		}
   891  		destinationNamespace := "testns"
   892  		destinationServerURL := "https://kubernetes.svc.local"
   893  		applicationNamespace := "argocd-ns"
   894  		expectedSA := ""
   895  		expectedErrMsg := "no matching service account found for destination server https://kubernetes.svc.local and namespace testns"
   896  
   897  		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
   898  		// when
   899  		sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
   900  
   901  		// then, there should be an error saying no match was found
   902  		require.EqualError(t, err, expectedErrMsg)
   903  		assert.Equal(t, expectedSA, sa)
   904  	})
   905  
   906  	t.Run("app destination namespace is empty", func(t *testing.T) {
   907  		// given an application referring a project with multiple destination service accounts with empty application destination namespace
   908  		t.Parallel()
   909  		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
   910  			{
   911  				Server:                "https://kubernetes.svc.local",
   912  				DefaultServiceAccount: "test-sa",
   913  			},
   914  			{
   915  				Server:                "https://kubernetes.svc.local",
   916  				Namespace:             "*",
   917  				DefaultServiceAccount: "test-sa-2",
   918  			},
   919  		}
   920  		destinationNamespace := ""
   921  		destinationServerURL := "https://kubernetes.svc.local"
   922  		applicationNamespace := "argocd-ns"
   923  		expectedSA := "system:serviceaccount:argocd-ns:test-sa"
   924  
   925  		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
   926  		// when
   927  		sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
   928  
   929  		// then, there should not be any error and the service account configured for with empty namespace should be used.
   930  		require.NoError(t, err)
   931  		assert.Equal(t, expectedSA, sa)
   932  	})
   933  
   934  	t.Run("match done via catch all glob pattern", func(t *testing.T) {
   935  		// given an application referring a project with multiple destination service accounts having a catch all glob pattern
   936  		t.Parallel()
   937  		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
   938  			{
   939  				Server:                "https://kubernetes.svc.local",
   940  				Namespace:             "testns1",
   941  				DefaultServiceAccount: "test-sa-2",
   942  			},
   943  			{
   944  				Server:                "https://kubernetes.svc.local",
   945  				Namespace:             "default",
   946  				DefaultServiceAccount: "default-sa",
   947  			},
   948  			{
   949  				Server:                "https://kubernetes.svc.local",
   950  				Namespace:             "*",
   951  				DefaultServiceAccount: "test-sa",
   952  			},
   953  		}
   954  		destinationNamespace := "testns"
   955  		destinationServerURL := "https://kubernetes.svc.local"
   956  		applicationNamespace := "argocd-ns"
   957  		expectedSA := "system:serviceaccount:testns:test-sa"
   958  
   959  		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
   960  		// when
   961  		sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
   962  
   963  		// then, there should not be any error and the catch all service account should be returned
   964  		require.NoError(t, err)
   965  		assert.Equal(t, expectedSA, sa)
   966  	})
   967  
   968  	t.Run("match done via invalid glob pattern", func(t *testing.T) {
   969  		// given an application referring a project with a destination service account having an invalid glob pattern for namespace
   970  		t.Parallel()
   971  		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
   972  			{
   973  				Server:                "https://kubernetes.svc.local",
   974  				Namespace:             "e[[a*",
   975  				DefaultServiceAccount: "test-sa",
   976  			},
   977  		}
   978  		destinationNamespace := "testns"
   979  		destinationServerURL := "https://kubernetes.svc.local"
   980  		applicationNamespace := "argocd-ns"
   981  		expectedSA := ""
   982  
   983  		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
   984  		// when
   985  		sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
   986  
   987  		// then, there must be an error as the glob pattern is invalid.
   988  		require.ErrorContains(t, err, "invalid glob pattern for destination namespace")
   989  		assert.Equal(t, expectedSA, sa)
   990  	})
   991  
   992  	t.Run("sa specified with a namespace", func(t *testing.T) {
   993  		// given an application referring a project with multiple destination service accounts having a matching service account specified with its namespace
   994  		t.Parallel()
   995  		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
   996  			{
   997  				Server:                "https://kubernetes.svc.local",
   998  				Namespace:             "testns",
   999  				DefaultServiceAccount: "myns:test-sa",
  1000  			},
  1001  			{
  1002  				Server:                "https://kubernetes.svc.local",
  1003  				Namespace:             "default",
  1004  				DefaultServiceAccount: "default-sa",
  1005  			},
  1006  			{
  1007  				Server:                "https://kubernetes.svc.local",
  1008  				Namespace:             "*",
  1009  				DefaultServiceAccount: "test-sa",
  1010  			},
  1011  		}
  1012  		destinationNamespace := "testns"
  1013  		destinationServerURL := "https://kubernetes.svc.local"
  1014  		applicationNamespace := "argocd-ns"
  1015  		expectedSA := "system:serviceaccount:myns:test-sa"
  1016  
  1017  		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
  1018  		// when
  1019  		sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
  1020  		assert.Equal(t, expectedSA, sa)
  1021  
  1022  		// then, there should not be any error and the service account with its namespace should be returned.
  1023  		require.NoError(t, err)
  1024  	})
  1025  
  1026  	t.Run("app destination name instead of server URL", func(t *testing.T) {
  1027  		t.Parallel()
  1028  		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
  1029  			{
  1030  				Server:                "https://kubernetes.svc.local",
  1031  				Namespace:             "*",
  1032  				DefaultServiceAccount: "test-sa",
  1033  			},
  1034  		}
  1035  		destinationNamespace := "testns"
  1036  		destinationServerURL := "https://kubernetes.svc.local"
  1037  		applicationNamespace := "argocd-ns"
  1038  		expectedSA := "system:serviceaccount:testns:test-sa"
  1039  
  1040  		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
  1041  
  1042  		// Use destination name instead of server URL
  1043  		f.application.Spec.Destination.Server = ""
  1044  		f.application.Spec.Destination.Name = f.cluster.Name
  1045  
  1046  		// when
  1047  		sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
  1048  		assert.Equal(t, expectedSA, sa)
  1049  
  1050  		// then, there should not be any error and the service account with its namespace should be returned.
  1051  		require.NoError(t, err)
  1052  	})
  1053  }
  1054  
  1055  func TestDeriveServiceAccountMatchingServers(t *testing.T) {
  1056  	t.Parallel()
  1057  
  1058  	type fixture struct {
  1059  		project     *v1alpha1.AppProject
  1060  		application *v1alpha1.Application
  1061  		cluster     *v1alpha1.Cluster
  1062  	}
  1063  
  1064  	setup := func(destinationServiceAccounts []v1alpha1.ApplicationDestinationServiceAccount, destinationNamespace, destinationServerURL, applicationNamespace string) *fixture {
  1065  		project := &v1alpha1.AppProject{
  1066  			ObjectMeta: metav1.ObjectMeta{
  1067  				Namespace: "argocd-ns",
  1068  				Name:      "testProj",
  1069  			},
  1070  			Spec: v1alpha1.AppProjectSpec{
  1071  				DestinationServiceAccounts: destinationServiceAccounts,
  1072  			},
  1073  		}
  1074  		app := &v1alpha1.Application{
  1075  			ObjectMeta: metav1.ObjectMeta{
  1076  				Namespace: applicationNamespace,
  1077  				Name:      "testApp",
  1078  			},
  1079  			Spec: v1alpha1.ApplicationSpec{
  1080  				Project: "testProj",
  1081  				Destination: v1alpha1.ApplicationDestination{
  1082  					Server:    destinationServerURL,
  1083  					Namespace: destinationNamespace,
  1084  				},
  1085  			},
  1086  		}
  1087  		cluster := &v1alpha1.Cluster{
  1088  			Server: "https://kubernetes.svc.local",
  1089  			Name:   "test-cluster",
  1090  		}
  1091  		return &fixture{
  1092  			project:     project,
  1093  			application: app,
  1094  			cluster:     cluster,
  1095  		}
  1096  	}
  1097  
  1098  	t.Run("exact one match with multiple destination service accounts", func(t *testing.T) {
  1099  		// given an application referring a project with multiple destination service accounts and one exact match for application destination
  1100  		t.Parallel()
  1101  		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
  1102  			{
  1103  				Server:                "https://kubernetes.svc.local",
  1104  				Namespace:             "guestbook",
  1105  				DefaultServiceAccount: "guestbook-sa",
  1106  			},
  1107  			{
  1108  				Server:                "https://abc.svc.local",
  1109  				Namespace:             "guestbook",
  1110  				DefaultServiceAccount: "guestbook-test-sa",
  1111  			},
  1112  			{
  1113  				Server:                "https://cde.svc.local",
  1114  				Namespace:             "guestbook",
  1115  				DefaultServiceAccount: "default-sa",
  1116  			},
  1117  			{
  1118  				Server:                "https://kubernetes.svc.local",
  1119  				Namespace:             "testns",
  1120  				DefaultServiceAccount: "test-sa",
  1121  			},
  1122  		}
  1123  		destinationNamespace := "testns"
  1124  		destinationServerURL := "https://kubernetes.svc.local"
  1125  		applicationNamespace := "argocd-ns"
  1126  		expectedSA := "system:serviceaccount:testns:test-sa"
  1127  
  1128  		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
  1129  		// when
  1130  		sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
  1131  
  1132  		// then, there should not be any error and the right service account must be returned.
  1133  		require.NoError(t, err)
  1134  		assert.Equal(t, expectedSA, sa)
  1135  	})
  1136  
  1137  	t.Run("first match to be used when multiple matches are available", func(t *testing.T) {
  1138  		// given an application referring a project with multiple destination service accounts and multiple matches for application destination
  1139  		t.Parallel()
  1140  		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
  1141  			{
  1142  				Server:                "https://kubernetes.svc.local",
  1143  				Namespace:             "testns",
  1144  				DefaultServiceAccount: "test-sa",
  1145  			},
  1146  			{
  1147  				Server:                "https://kubernetes.svc.local",
  1148  				Namespace:             "testns",
  1149  				DefaultServiceAccount: "test-sa-2",
  1150  			},
  1151  			{
  1152  				Server:                "https://kubernetes.svc.local",
  1153  				Namespace:             "default",
  1154  				DefaultServiceAccount: "default-sa",
  1155  			},
  1156  			{
  1157  				Server:                "https://kubernetes.svc.local",
  1158  				Namespace:             "guestbook",
  1159  				DefaultServiceAccount: "guestbook-sa",
  1160  			},
  1161  		}
  1162  		destinationNamespace := "testns"
  1163  		destinationServerURL := "https://kubernetes.svc.local"
  1164  		applicationNamespace := "argocd-ns"
  1165  		expectedSA := "system:serviceaccount:testns:test-sa"
  1166  
  1167  		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
  1168  		// when
  1169  		sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
  1170  
  1171  		// then, there should not be any error and first matching service account should be used
  1172  		require.NoError(t, err)
  1173  		assert.Equal(t, expectedSA, sa)
  1174  	})
  1175  
  1176  	t.Run("first match to be used when glob pattern is used", func(t *testing.T) {
  1177  		// given an application referring a project with multiple destination service accounts with a matching glob pattern and exact match
  1178  		t.Parallel()
  1179  		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
  1180  			{
  1181  				Server:                "https://kubernetes.svc.local",
  1182  				Namespace:             "test*",
  1183  				DefaultServiceAccount: "test-sa",
  1184  			},
  1185  			{
  1186  				Server:                "https://kubernetes.svc.local",
  1187  				Namespace:             "testns",
  1188  				DefaultServiceAccount: "test-sa-2",
  1189  			},
  1190  			{
  1191  				Server:                "https://kubernetes.svc.local",
  1192  				Namespace:             "default",
  1193  				DefaultServiceAccount: "default-sa",
  1194  			},
  1195  		}
  1196  		destinationNamespace := "testns"
  1197  		destinationServerURL := "https://kubernetes.svc.local"
  1198  		applicationNamespace := "argocd-ns"
  1199  		expectedSA := "system:serviceaccount:testns:test-sa"
  1200  
  1201  		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
  1202  		// when
  1203  		sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
  1204  		assert.Equal(t, expectedSA, sa)
  1205  
  1206  		// then, there should not be any error and the service account of the glob pattern, being the first match should be returned.
  1207  		require.NoError(t, err)
  1208  	})
  1209  
  1210  	t.Run("no match among a valid list", func(t *testing.T) {
  1211  		// given an application referring a project with multiple destination service accounts with no match
  1212  		t.Parallel()
  1213  		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
  1214  			{
  1215  				Server:                "https://kubernetes.svc.local",
  1216  				Namespace:             "testns",
  1217  				DefaultServiceAccount: "test-sa",
  1218  			},
  1219  			{
  1220  				Server:                "https://abc.svc.local",
  1221  				Namespace:             "testns",
  1222  				DefaultServiceAccount: "test-sa-2",
  1223  			},
  1224  			{
  1225  				Server:                "https://cde.svc.local",
  1226  				Namespace:             "default",
  1227  				DefaultServiceAccount: "default-sa",
  1228  			},
  1229  		}
  1230  		destinationNamespace := "testns"
  1231  		destinationServerURL := "https://xyz.svc.local"
  1232  		applicationNamespace := "argocd-ns"
  1233  		expectedSA := ""
  1234  		expectedErr := "no matching service account found for destination server https://xyz.svc.local and namespace testns"
  1235  
  1236  		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
  1237  		// when
  1238  		sa, err := deriveServiceAccountToImpersonate(f.project, f.application, &v1alpha1.Cluster{Server: destinationServerURL})
  1239  
  1240  		// then, there an error with appropriate message must be returned
  1241  		require.EqualError(t, err, expectedErr)
  1242  		assert.Equal(t, expectedSA, sa)
  1243  	})
  1244  
  1245  	t.Run("match done via catch all glob pattern", func(t *testing.T) {
  1246  		// given an application referring a project with multiple destination service accounts with matching catch all glob pattern
  1247  		t.Parallel()
  1248  		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
  1249  			{
  1250  				Server:                "https://kubernetes.svc.local",
  1251  				Namespace:             "testns1",
  1252  				DefaultServiceAccount: "test-sa-2",
  1253  			},
  1254  			{
  1255  				Server:                "https://kubernetes.svc.local",
  1256  				Namespace:             "default",
  1257  				DefaultServiceAccount: "default-sa",
  1258  			},
  1259  			{
  1260  				Server:                "*",
  1261  				Namespace:             "*",
  1262  				DefaultServiceAccount: "test-sa",
  1263  			},
  1264  		}
  1265  		destinationNamespace := "testns"
  1266  		destinationServerURL := "https://localhost:6443"
  1267  		applicationNamespace := "argocd-ns"
  1268  		expectedSA := "system:serviceaccount:testns:test-sa"
  1269  
  1270  		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
  1271  		// when
  1272  		sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
  1273  
  1274  		// then, there should not be any error and the service account of the glob pattern match must be returned.
  1275  		require.NoError(t, err)
  1276  		assert.Equal(t, expectedSA, sa)
  1277  	})
  1278  
  1279  	t.Run("match done via invalid glob pattern", func(t *testing.T) {
  1280  		// given an application referring a project with a destination service account having an invalid glob pattern for server
  1281  		t.Parallel()
  1282  		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
  1283  			{
  1284  				Server:                "e[[a*",
  1285  				Namespace:             "test-ns",
  1286  				DefaultServiceAccount: "test-sa",
  1287  			},
  1288  		}
  1289  		destinationNamespace := "testns"
  1290  		destinationServerURL := "https://kubernetes.svc.local"
  1291  		applicationNamespace := "argocd-ns"
  1292  		expectedSA := ""
  1293  
  1294  		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
  1295  		// when
  1296  		sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
  1297  
  1298  		// then, there must be an error as the glob pattern is invalid.
  1299  		require.ErrorContains(t, err, "invalid glob pattern for destination server")
  1300  		assert.Equal(t, expectedSA, sa)
  1301  	})
  1302  
  1303  	t.Run("sa specified with a namespace", func(t *testing.T) {
  1304  		// given app sync impersonation feature is enabled and matching service account is prefixed with a namespace
  1305  		t.Parallel()
  1306  		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
  1307  			{
  1308  				Server:                "https://abc.svc.local",
  1309  				Namespace:             "testns",
  1310  				DefaultServiceAccount: "myns:test-sa",
  1311  			},
  1312  			{
  1313  				Server:                "https://kubernetes.svc.local",
  1314  				Namespace:             "default",
  1315  				DefaultServiceAccount: "default-sa",
  1316  			},
  1317  			{
  1318  				Server:                "*",
  1319  				Namespace:             "*",
  1320  				DefaultServiceAccount: "test-sa",
  1321  			},
  1322  		}
  1323  		destinationNamespace := "testns"
  1324  		destinationServerURL := "https://abc.svc.local"
  1325  		applicationNamespace := "argocd-ns"
  1326  		expectedSA := "system:serviceaccount:myns:test-sa"
  1327  
  1328  		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
  1329  		// when
  1330  		sa, err := deriveServiceAccountToImpersonate(f.project, f.application, &v1alpha1.Cluster{Server: destinationServerURL})
  1331  
  1332  		// then, there should not be any error and the service account with the given namespace prefix must be returned.
  1333  		require.NoError(t, err)
  1334  		assert.Equal(t, expectedSA, sa)
  1335  	})
  1336  
  1337  	t.Run("app destination name instead of server URL", func(t *testing.T) {
  1338  		t.Parallel()
  1339  		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
  1340  			{
  1341  				Server:                "https://kubernetes.svc.local",
  1342  				Namespace:             "*",
  1343  				DefaultServiceAccount: "test-sa",
  1344  			},
  1345  		}
  1346  		destinationNamespace := "testns"
  1347  		destinationServerURL := "https://kubernetes.svc.local"
  1348  		applicationNamespace := "argocd-ns"
  1349  		expectedSA := "system:serviceaccount:testns:test-sa"
  1350  
  1351  		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
  1352  
  1353  		// Use destination name instead of server URL
  1354  		f.application.Spec.Destination.Server = ""
  1355  		f.application.Spec.Destination.Name = f.cluster.Name
  1356  
  1357  		// when
  1358  		sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
  1359  		assert.Equal(t, expectedSA, sa)
  1360  
  1361  		// then, there should not be any error and the service account with its namespace should be returned.
  1362  		require.NoError(t, err)
  1363  	})
  1364  }
  1365  
  1366  func TestSyncWithImpersonate(t *testing.T) {
  1367  	type fixture struct {
  1368  		application *v1alpha1.Application
  1369  		project     *v1alpha1.AppProject
  1370  		controller  *ApplicationController
  1371  	}
  1372  
  1373  	setup := func(impersonationEnabled bool, destinationNamespace, serviceAccountName string) *fixture {
  1374  		app := newFakeApp()
  1375  		app.Status.OperationState = nil
  1376  		app.Status.History = nil
  1377  		project := &v1alpha1.AppProject{
  1378  			ObjectMeta: metav1.ObjectMeta{
  1379  				Namespace: test.FakeArgoCDNamespace,
  1380  				Name:      "default",
  1381  			},
  1382  			Spec: v1alpha1.AppProjectSpec{
  1383  				DestinationServiceAccounts: []v1alpha1.ApplicationDestinationServiceAccount{
  1384  					{
  1385  						Server:                "https://localhost:6443",
  1386  						Namespace:             destinationNamespace,
  1387  						DefaultServiceAccount: serviceAccountName,
  1388  					},
  1389  				},
  1390  			},
  1391  		}
  1392  		additionalObjs := []runtime.Object{}
  1393  		if serviceAccountName != "" {
  1394  			syncServiceAccount := &corev1.ServiceAccount{
  1395  				ObjectMeta: metav1.ObjectMeta{
  1396  					Name:      serviceAccountName,
  1397  					Namespace: test.FakeDestNamespace,
  1398  				},
  1399  			}
  1400  			additionalObjs = append(additionalObjs, syncServiceAccount)
  1401  		}
  1402  		data := fakeData{
  1403  			apps: []runtime.Object{app, project},
  1404  			manifestResponse: &apiclient.ManifestResponse{
  1405  				Manifests: []string{},
  1406  				Namespace: test.FakeDestNamespace,
  1407  				Server:    "https://localhost:6443",
  1408  				Revision:  "abc123",
  1409  			},
  1410  			managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{},
  1411  			configMapData: map[string]string{
  1412  				"application.sync.impersonation.enabled": strconv.FormatBool(impersonationEnabled),
  1413  			},
  1414  			additionalObjs: additionalObjs,
  1415  		}
  1416  		ctrl := newFakeController(&data, nil)
  1417  		return &fixture{
  1418  			application: app,
  1419  			project:     project,
  1420  			controller:  ctrl,
  1421  		}
  1422  	}
  1423  
  1424  	t.Run("sync with impersonation and no matching service account", func(t *testing.T) {
  1425  		// given app sync impersonation feature is enabled with an application referring a project no matching service account
  1426  		f := setup(true, test.FakeArgoCDNamespace, "")
  1427  		opMessage := "failed to find a matching service account to impersonate: no matching service account found for destination server https://localhost:6443 and namespace fake-dest-ns"
  1428  
  1429  		opState := &v1alpha1.OperationState{
  1430  			Operation: v1alpha1.Operation{
  1431  				Sync: &v1alpha1.SyncOperation{
  1432  					Source: &v1alpha1.ApplicationSource{},
  1433  				},
  1434  			},
  1435  			Phase: synccommon.OperationRunning,
  1436  		}
  1437  		// when
  1438  		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)
  1439  
  1440  		// then, app sync should fail with expected error message in operation state
  1441  		assert.Equal(t, synccommon.OperationError, opState.Phase)
  1442  		assert.Contains(t, opState.Message, opMessage)
  1443  	})
  1444  
  1445  	t.Run("sync with impersonation and empty service account match", func(t *testing.T) {
  1446  		// given app sync impersonation feature is enabled with an application referring a project matching service account that is an empty string
  1447  		f := setup(true, test.FakeDestNamespace, "")
  1448  		opMessage := "failed to find a matching service account to impersonate: default service account contains invalid chars ''"
  1449  
  1450  		opState := &v1alpha1.OperationState{
  1451  			Operation: v1alpha1.Operation{
  1452  				Sync: &v1alpha1.SyncOperation{
  1453  					Source: &v1alpha1.ApplicationSource{},
  1454  				},
  1455  			},
  1456  			Phase: synccommon.OperationRunning,
  1457  		}
  1458  		// when
  1459  		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)
  1460  
  1461  		// then app sync should fail with expected error message in operation state
  1462  		assert.Equal(t, synccommon.OperationError, opState.Phase)
  1463  		assert.Contains(t, opState.Message, opMessage)
  1464  	})
  1465  
  1466  	t.Run("sync with impersonation and matching sa", func(t *testing.T) {
  1467  		// given app sync impersonation feature is enabled with an application referring a project matching service account
  1468  		f := setup(true, test.FakeDestNamespace, "test-sa")
  1469  		opMessage := "successfully synced (no more tasks)"
  1470  
  1471  		opState := &v1alpha1.OperationState{
  1472  			Operation: v1alpha1.Operation{
  1473  				Sync: &v1alpha1.SyncOperation{
  1474  					Source: &v1alpha1.ApplicationSource{},
  1475  				},
  1476  			},
  1477  			Phase: synccommon.OperationRunning,
  1478  		}
  1479  		// when
  1480  		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)
  1481  
  1482  		// then app sync should not fail
  1483  		assert.Equal(t, synccommon.OperationSucceeded, opState.Phase)
  1484  		assert.Contains(t, opState.Message, opMessage)
  1485  	})
  1486  
  1487  	t.Run("sync without impersonation", func(t *testing.T) {
  1488  		// given app sync impersonation feature is disabled with an application referring a project matching service account
  1489  		f := setup(false, test.FakeDestNamespace, "")
  1490  		opMessage := "successfully synced (no more tasks)"
  1491  
  1492  		opState := &v1alpha1.OperationState{
  1493  			Operation: v1alpha1.Operation{
  1494  				Sync: &v1alpha1.SyncOperation{
  1495  					Source: &v1alpha1.ApplicationSource{},
  1496  				},
  1497  			},
  1498  			Phase: synccommon.OperationRunning,
  1499  		}
  1500  		// when
  1501  		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)
  1502  
  1503  		// then application sync should pass using the control plane service account
  1504  		assert.Equal(t, synccommon.OperationSucceeded, opState.Phase)
  1505  		assert.Contains(t, opState.Message, opMessage)
  1506  	})
  1507  
  1508  	t.Run("app destination name instead of server URL", func(t *testing.T) {
  1509  		// given app sync impersonation feature is enabled with an application referring a project matching service account
  1510  		f := setup(true, test.FakeDestNamespace, "test-sa")
  1511  		opMessage := "successfully synced (no more tasks)"
  1512  
  1513  		opState := &v1alpha1.OperationState{
  1514  			Operation: v1alpha1.Operation{
  1515  				Sync: &v1alpha1.SyncOperation{
  1516  					Source: &v1alpha1.ApplicationSource{},
  1517  				},
  1518  			},
  1519  			Phase: synccommon.OperationRunning,
  1520  		}
  1521  
  1522  		f.application.Spec.Destination.Server = ""
  1523  		f.application.Spec.Destination.Name = "minikube"
  1524  
  1525  		// when
  1526  		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)
  1527  
  1528  		// then app sync should not fail
  1529  		assert.Equal(t, synccommon.OperationSucceeded, opState.Phase)
  1530  		assert.Contains(t, opState.Message, opMessage)
  1531  	})
  1532  }
  1533  
  1534  func TestClientSideApplyMigration(t *testing.T) {
  1535  	t.Parallel()
  1536  
  1537  	type fixture struct {
  1538  		application *v1alpha1.Application
  1539  		project     *v1alpha1.AppProject
  1540  		controller  *ApplicationController
  1541  	}
  1542  
  1543  	setup := func(disableMigration bool, customManager string) *fixture {
  1544  		app := newFakeApp()
  1545  		app.Status.OperationState = nil
  1546  		app.Status.History = nil
  1547  
  1548  		// Add sync options
  1549  		if disableMigration {
  1550  			app.Spec.SyncPolicy.SyncOptions = append(app.Spec.SyncPolicy.SyncOptions, "DisableClientSideApplyMigration=true")
  1551  		}
  1552  
  1553  		// Add custom manager annotation if specified
  1554  		if customManager != "" {
  1555  			app.Annotations = map[string]string{
  1556  				"argocd.argoproj.io/client-side-apply-migration-manager": customManager,
  1557  			}
  1558  		}
  1559  
  1560  		project := &v1alpha1.AppProject{
  1561  			ObjectMeta: metav1.ObjectMeta{
  1562  				Namespace: test.FakeArgoCDNamespace,
  1563  				Name:      "default",
  1564  			},
  1565  		}
  1566  		data := fakeData{
  1567  			apps: []runtime.Object{app, project},
  1568  			manifestResponse: &apiclient.ManifestResponse{
  1569  				Manifests: []string{},
  1570  				Namespace: test.FakeDestNamespace,
  1571  				Server:    test.FakeClusterURL,
  1572  				Revision:  "abc123",
  1573  			},
  1574  			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
  1575  		}
  1576  		ctrl := newFakeController(&data, nil)
  1577  
  1578  		return &fixture{
  1579  			application: app,
  1580  			project:     project,
  1581  			controller:  ctrl,
  1582  		}
  1583  	}
  1584  
  1585  	t.Run("client-side apply migration enabled by default", func(t *testing.T) {
  1586  		// given
  1587  		t.Parallel()
  1588  		f := setup(false, "")
  1589  
  1590  		// when
  1591  		opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
  1592  			Sync: &v1alpha1.SyncOperation{
  1593  				Source: &v1alpha1.ApplicationSource{},
  1594  			},
  1595  		}}
  1596  		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)
  1597  
  1598  		// then
  1599  		assert.Equal(t, synccommon.OperationSucceeded, opState.Phase)
  1600  		assert.Contains(t, opState.Message, "successfully synced")
  1601  	})
  1602  
  1603  	t.Run("client-side apply migration disabled", func(t *testing.T) {
  1604  		// given
  1605  		t.Parallel()
  1606  		f := setup(true, "")
  1607  
  1608  		// when
  1609  		opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
  1610  			Sync: &v1alpha1.SyncOperation{
  1611  				Source: &v1alpha1.ApplicationSource{},
  1612  			},
  1613  		}}
  1614  		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)
  1615  
  1616  		// then
  1617  		assert.Equal(t, synccommon.OperationSucceeded, opState.Phase)
  1618  		assert.Contains(t, opState.Message, "successfully synced")
  1619  	})
  1620  
  1621  	t.Run("client-side apply migration with custom manager", func(t *testing.T) {
  1622  		// given
  1623  		t.Parallel()
  1624  		f := setup(false, "my-custom-manager")
  1625  
  1626  		// when
  1627  		opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
  1628  			Sync: &v1alpha1.SyncOperation{
  1629  				Source: &v1alpha1.ApplicationSource{},
  1630  			},
  1631  		}}
  1632  		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)
  1633  
  1634  		// then
  1635  		assert.Equal(t, synccommon.OperationSucceeded, opState.Phase)
  1636  		assert.Contains(t, opState.Message, "successfully synced")
  1637  	})
  1638  }
  1639  
  1640  func dig(obj any, path ...any) any {
  1641  	i := obj
  1642  
  1643  	for _, segment := range path {
  1644  		switch segment := segment.(type) {
  1645  		case int:
  1646  			i = i.([]any)[segment]
  1647  		case string:
  1648  			i = i.(map[string]any)[segment]
  1649  		default:
  1650  			panic("invalid path for object")
  1651  		}
  1652  	}
  1653  
  1654  	return i
  1655  }