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

     1  package controller
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"os"
     7  	"testing"
     8  	"time"
     9  
    10  	"dario.cat/mergo"
    11  	cachemocks "github.com/argoproj/gitops-engine/pkg/cache/mocks"
    12  	"github.com/argoproj/gitops-engine/pkg/health"
    13  	synccommon "github.com/argoproj/gitops-engine/pkg/sync/common"
    14  	"github.com/argoproj/gitops-engine/pkg/utils/kube"
    15  	. "github.com/argoproj/gitops-engine/pkg/utils/testing"
    16  	"github.com/sirupsen/logrus"
    17  	logrustest "github.com/sirupsen/logrus/hooks/test"
    18  	"github.com/stretchr/testify/assert"
    19  	"github.com/stretchr/testify/mock"
    20  	"github.com/stretchr/testify/require"
    21  	appsv1 "k8s.io/api/apps/v1"
    22  	corev1 "k8s.io/api/core/v1"
    23  	networkingv1 "k8s.io/api/networking/v1"
    24  	rbacv1 "k8s.io/api/rbac/v1"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/utils/ptr"
    29  
    30  	"github.com/argoproj/argo-cd/v3/common"
    31  	"github.com/argoproj/argo-cd/v3/controller/testdata"
    32  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    33  	"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
    34  	"github.com/argoproj/argo-cd/v3/test"
    35  )
    36  
    37  // TestCompareAppStateEmpty tests comparison when both git and live have no objects
    38  func TestCompareAppStateEmpty(t *testing.T) {
    39  	t.Parallel()
    40  
    41  	app := newFakeApp()
    42  	data := fakeData{
    43  		manifestResponse: &apiclient.ManifestResponse{
    44  			Manifests: []string{},
    45  			Namespace: test.FakeDestNamespace,
    46  			Server:    test.FakeClusterURL,
    47  			Revision:  "abc123",
    48  		},
    49  		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
    50  	}
    51  	ctrl := newFakeController(&data, nil)
    52  	sources := make([]v1alpha1.ApplicationSource, 0)
    53  	sources = append(sources, app.Spec.GetSource())
    54  	revisions := make([]string, 0)
    55  	revisions = append(revisions, "")
    56  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
    57  	require.NoError(t, err)
    58  	assert.NotNil(t, compRes)
    59  	assert.NotNil(t, compRes.syncStatus)
    60  	assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
    61  	assert.Empty(t, compRes.resources)
    62  	assert.Empty(t, compRes.managedResources)
    63  	assert.Empty(t, app.Status.Conditions)
    64  }
    65  
    66  // TestCompareAppStateRepoError tests the case when CompareAppState notices a repo error
    67  func TestCompareAppStateRepoError(t *testing.T) {
    68  	app := newFakeApp()
    69  	ctrl := newFakeController(&fakeData{manifestResponses: make([]*apiclient.ManifestResponse, 3)}, errors.New("test repo error"))
    70  	sources := make([]v1alpha1.ApplicationSource, 0)
    71  	sources = append(sources, app.Spec.GetSource())
    72  	revisions := make([]string, 0)
    73  	revisions = append(revisions, "")
    74  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
    75  	assert.Nil(t, compRes)
    76  	require.EqualError(t, err, ErrCompareStateRepo.Error())
    77  
    78  	// expect to still get compare state error to as inside grace period
    79  	compRes, err = ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
    80  	assert.Nil(t, compRes)
    81  	require.EqualError(t, err, ErrCompareStateRepo.Error())
    82  
    83  	time.Sleep(10 * time.Second)
    84  	// expect to not get error as outside of grace period, but status should be unknown
    85  	compRes, err = ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
    86  	assert.NotNil(t, compRes)
    87  	require.NoError(t, err)
    88  	assert.Equal(t, v1alpha1.SyncStatusCodeUnknown, compRes.syncStatus.Status)
    89  }
    90  
    91  // TestCompareAppStateNamespaceMetadataDiffers tests comparison when managed namespace metadata differs
    92  func TestCompareAppStateNamespaceMetadataDiffers(t *testing.T) {
    93  	app := newFakeApp()
    94  	app.Spec.SyncPolicy.ManagedNamespaceMetadata = &v1alpha1.ManagedNamespaceMetadata{
    95  		Labels: map[string]string{
    96  			"foo": "bar",
    97  		},
    98  		Annotations: map[string]string{
    99  			"foo": "bar",
   100  		},
   101  	}
   102  	app.Status.OperationState = &v1alpha1.OperationState{
   103  		SyncResult: &v1alpha1.SyncOperationResult{},
   104  	}
   105  
   106  	data := fakeData{
   107  		manifestResponse: &apiclient.ManifestResponse{
   108  			Manifests: []string{},
   109  			Namespace: test.FakeDestNamespace,
   110  			Server:    test.FakeClusterURL,
   111  			Revision:  "abc123",
   112  		},
   113  		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
   114  	}
   115  	ctrl := newFakeController(&data, nil)
   116  	sources := make([]v1alpha1.ApplicationSource, 0)
   117  	sources = append(sources, app.Spec.GetSource())
   118  	revisions := make([]string, 0)
   119  	revisions = append(revisions, "")
   120  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
   121  	require.NoError(t, err)
   122  	assert.NotNil(t, compRes)
   123  	assert.NotNil(t, compRes.syncStatus)
   124  	assert.Equal(t, v1alpha1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
   125  	assert.Empty(t, compRes.resources)
   126  	assert.Empty(t, compRes.managedResources)
   127  	assert.Empty(t, app.Status.Conditions)
   128  }
   129  
   130  // TestCompareAppStateNamespaceMetadataDiffers tests comparison when managed namespace metadata differs to live and manifest ns
   131  func TestCompareAppStateNamespaceMetadataDiffersToManifest(t *testing.T) {
   132  	ns := NewNamespace()
   133  	ns.SetName(test.FakeDestNamespace)
   134  	ns.SetNamespace(test.FakeDestNamespace)
   135  	ns.SetAnnotations(map[string]string{"bar": "bat"})
   136  
   137  	app := newFakeApp()
   138  	app.Spec.SyncPolicy.ManagedNamespaceMetadata = &v1alpha1.ManagedNamespaceMetadata{
   139  		Labels: map[string]string{
   140  			"foo": "bar",
   141  		},
   142  		Annotations: map[string]string{
   143  			"foo": "bar",
   144  		},
   145  	}
   146  	app.Status.OperationState = &v1alpha1.OperationState{
   147  		SyncResult: &v1alpha1.SyncOperationResult{},
   148  	}
   149  
   150  	liveNs := ns.DeepCopy()
   151  	liveNs.SetAnnotations(nil)
   152  
   153  	data := fakeData{
   154  		manifestResponse: &apiclient.ManifestResponse{
   155  			Manifests: []string{toJSON(t, liveNs)},
   156  			Namespace: test.FakeDestNamespace,
   157  			Server:    test.FakeClusterURL,
   158  			Revision:  "abc123",
   159  		},
   160  		managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
   161  			kube.GetResourceKey(ns): ns,
   162  		},
   163  	}
   164  	ctrl := newFakeController(&data, nil)
   165  	sources := make([]v1alpha1.ApplicationSource, 0)
   166  	sources = append(sources, app.Spec.GetSource())
   167  	revisions := make([]string, 0)
   168  	revisions = append(revisions, "")
   169  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
   170  	require.NoError(t, err)
   171  	assert.NotNil(t, compRes)
   172  	assert.NotNil(t, compRes.syncStatus)
   173  	assert.Equal(t, v1alpha1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
   174  	assert.Len(t, compRes.resources, 1)
   175  	assert.Len(t, compRes.managedResources, 1)
   176  	assert.NotNil(t, compRes.diffResultList)
   177  	assert.Len(t, compRes.diffResultList.Diffs, 1)
   178  
   179  	result := NewNamespace()
   180  	require.NoError(t, json.Unmarshal(compRes.diffResultList.Diffs[0].PredictedLive, result))
   181  
   182  	labels := result.GetLabels()
   183  	delete(labels, "kubernetes.io/metadata.name")
   184  
   185  	assert.Equal(t, map[string]string{}, labels)
   186  	// Manifests override definitions in managedNamespaceMetadata
   187  	assert.Equal(t, map[string]string{"bar": "bat"}, result.GetAnnotations())
   188  	assert.Empty(t, app.Status.Conditions)
   189  }
   190  
   191  // TestCompareAppStateNamespaceMetadata tests comparison when managed namespace metadata differs to live
   192  func TestCompareAppStateNamespaceMetadata(t *testing.T) {
   193  	ns := NewNamespace()
   194  	ns.SetName(test.FakeDestNamespace)
   195  	ns.SetNamespace(test.FakeDestNamespace)
   196  	ns.SetAnnotations(map[string]string{"bar": "bat"})
   197  
   198  	app := newFakeApp()
   199  	app.Spec.SyncPolicy.ManagedNamespaceMetadata = &v1alpha1.ManagedNamespaceMetadata{
   200  		Labels: map[string]string{
   201  			"foo": "bar",
   202  		},
   203  		Annotations: map[string]string{
   204  			"foo": "bar",
   205  		},
   206  	}
   207  	app.Status.OperationState = &v1alpha1.OperationState{
   208  		SyncResult: &v1alpha1.SyncOperationResult{},
   209  	}
   210  
   211  	data := fakeData{
   212  		manifestResponse: &apiclient.ManifestResponse{
   213  			Manifests: []string{},
   214  			Namespace: test.FakeDestNamespace,
   215  			Server:    test.FakeClusterURL,
   216  			Revision:  "abc123",
   217  		},
   218  		managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
   219  			kube.GetResourceKey(ns): ns,
   220  		},
   221  	}
   222  	ctrl := newFakeController(&data, nil)
   223  	sources := make([]v1alpha1.ApplicationSource, 0)
   224  	sources = append(sources, app.Spec.GetSource())
   225  	revisions := make([]string, 0)
   226  	revisions = append(revisions, "")
   227  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
   228  	require.NoError(t, err)
   229  	assert.NotNil(t, compRes)
   230  	assert.NotNil(t, compRes.syncStatus)
   231  	assert.Equal(t, v1alpha1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
   232  	assert.Len(t, compRes.resources, 1)
   233  	assert.Len(t, compRes.managedResources, 1)
   234  	assert.NotNil(t, compRes.diffResultList)
   235  	assert.Len(t, compRes.diffResultList.Diffs, 1)
   236  
   237  	result := NewNamespace()
   238  	require.NoError(t, json.Unmarshal(compRes.diffResultList.Diffs[0].PredictedLive, result))
   239  
   240  	labels := result.GetLabels()
   241  	delete(labels, "kubernetes.io/metadata.name")
   242  
   243  	assert.Equal(t, map[string]string{"foo": "bar"}, labels)
   244  	assert.Equal(t, map[string]string{"argocd.argoproj.io/sync-options": "ServerSideApply=true", "bar": "bat", "foo": "bar"}, result.GetAnnotations())
   245  	assert.Empty(t, app.Status.Conditions)
   246  }
   247  
   248  // TestCompareAppStateNamespaceMetadataIsTheSame tests comparison when managed namespace metadata is the same
   249  func TestCompareAppStateNamespaceMetadataIsTheSame(t *testing.T) {
   250  	app := newFakeApp()
   251  	app.Spec.SyncPolicy.ManagedNamespaceMetadata = &v1alpha1.ManagedNamespaceMetadata{
   252  		Labels: map[string]string{
   253  			"foo": "bar",
   254  		},
   255  		Annotations: map[string]string{
   256  			"foo": "bar",
   257  		},
   258  	}
   259  	app.Status.OperationState = &v1alpha1.OperationState{
   260  		SyncResult: &v1alpha1.SyncOperationResult{
   261  			ManagedNamespaceMetadata: &v1alpha1.ManagedNamespaceMetadata{
   262  				Labels: map[string]string{
   263  					"foo": "bar",
   264  				},
   265  				Annotations: map[string]string{
   266  					"foo": "bar",
   267  				},
   268  			},
   269  		},
   270  	}
   271  
   272  	data := fakeData{
   273  		manifestResponse: &apiclient.ManifestResponse{
   274  			Manifests: []string{},
   275  			Namespace: test.FakeDestNamespace,
   276  			Server:    test.FakeClusterURL,
   277  			Revision:  "abc123",
   278  		},
   279  		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
   280  	}
   281  	ctrl := newFakeController(&data, nil)
   282  	sources := make([]v1alpha1.ApplicationSource, 0)
   283  	sources = append(sources, app.Spec.GetSource())
   284  	revisions := make([]string, 0)
   285  	revisions = append(revisions, "")
   286  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
   287  	require.NoError(t, err)
   288  	assert.NotNil(t, compRes)
   289  	assert.NotNil(t, compRes.syncStatus)
   290  	assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
   291  	assert.Empty(t, compRes.resources)
   292  	assert.Empty(t, compRes.managedResources)
   293  	assert.Empty(t, app.Status.Conditions)
   294  }
   295  
   296  // TestCompareAppStateMissing tests when there is a manifest defined in the repo which doesn't exist in live
   297  func TestCompareAppStateMissing(t *testing.T) {
   298  	app := newFakeApp()
   299  	data := fakeData{
   300  		apps: []runtime.Object{app},
   301  		manifestResponse: &apiclient.ManifestResponse{
   302  			Manifests: []string{PodManifest},
   303  			Namespace: test.FakeDestNamespace,
   304  			Server:    test.FakeClusterURL,
   305  			Revision:  "abc123",
   306  		},
   307  		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
   308  	}
   309  	ctrl := newFakeController(&data, nil)
   310  	sources := make([]v1alpha1.ApplicationSource, 0)
   311  	sources = append(sources, app.Spec.GetSource())
   312  	revisions := make([]string, 0)
   313  	revisions = append(revisions, "")
   314  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
   315  	require.NoError(t, err)
   316  	assert.NotNil(t, compRes)
   317  	assert.NotNil(t, compRes.syncStatus)
   318  	assert.Equal(t, v1alpha1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
   319  	assert.Len(t, compRes.resources, 1)
   320  	assert.Len(t, compRes.managedResources, 1)
   321  	assert.Empty(t, app.Status.Conditions)
   322  }
   323  
   324  // TestCompareAppStateExtra tests when there is an extra object in live but not defined in git
   325  func TestCompareAppStateExtra(t *testing.T) {
   326  	pod := NewPod()
   327  	pod.SetNamespace(test.FakeDestNamespace)
   328  	app := newFakeApp()
   329  	key := kube.ResourceKey{Group: "", Kind: "Pod", Namespace: test.FakeDestNamespace, Name: app.Name}
   330  	data := fakeData{
   331  		manifestResponse: &apiclient.ManifestResponse{
   332  			Manifests: []string{},
   333  			Namespace: test.FakeDestNamespace,
   334  			Server:    test.FakeClusterURL,
   335  			Revision:  "abc123",
   336  		},
   337  		managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
   338  			key: pod,
   339  		},
   340  	}
   341  	ctrl := newFakeController(&data, nil)
   342  	sources := make([]v1alpha1.ApplicationSource, 0)
   343  	sources = append(sources, app.Spec.GetSource())
   344  	revisions := make([]string, 0)
   345  	revisions = append(revisions, "")
   346  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
   347  	require.NoError(t, err)
   348  	assert.NotNil(t, compRes)
   349  	assert.Equal(t, v1alpha1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
   350  	assert.Len(t, compRes.resources, 1)
   351  	assert.Len(t, compRes.managedResources, 1)
   352  	assert.Empty(t, app.Status.Conditions)
   353  }
   354  
   355  // TestCompareAppStateHook checks that hooks are detected during manifest generation, and not
   356  // considered as part of resources when assessing Synced status
   357  func TestCompareAppStateHook(t *testing.T) {
   358  	pod := NewPod()
   359  	pod.SetAnnotations(map[string]string{synccommon.AnnotationKeyHook: "PreSync"})
   360  	podBytes, _ := json.Marshal(pod)
   361  	app := newFakeApp()
   362  	data := fakeData{
   363  		apps: []runtime.Object{app},
   364  		manifestResponse: &apiclient.ManifestResponse{
   365  			Manifests: []string{string(podBytes)},
   366  			Namespace: test.FakeDestNamespace,
   367  			Server:    test.FakeClusterURL,
   368  			Revision:  "abc123",
   369  		},
   370  		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
   371  	}
   372  	ctrl := newFakeController(&data, nil)
   373  	sources := make([]v1alpha1.ApplicationSource, 0)
   374  	sources = append(sources, app.Spec.GetSource())
   375  	revisions := make([]string, 0)
   376  	revisions = append(revisions, "")
   377  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
   378  	require.NoError(t, err)
   379  	assert.NotNil(t, compRes)
   380  	assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
   381  	assert.Empty(t, compRes.resources)
   382  	assert.Empty(t, compRes.managedResources)
   383  	assert.Len(t, compRes.reconciliationResult.Hooks, 1)
   384  	assert.Empty(t, app.Status.Conditions)
   385  }
   386  
   387  // TestCompareAppStateSkipHook checks that skipped resources are detected during manifest generation, and not
   388  // considered as part of resources when assessing Synced status
   389  func TestCompareAppStateSkipHook(t *testing.T) {
   390  	pod := NewPod()
   391  	pod.SetAnnotations(map[string]string{synccommon.AnnotationKeyHook: "Skip"})
   392  	podBytes, _ := json.Marshal(pod)
   393  	app := newFakeApp()
   394  	data := fakeData{
   395  		apps: []runtime.Object{app},
   396  		manifestResponse: &apiclient.ManifestResponse{
   397  			Manifests: []string{string(podBytes)},
   398  			Namespace: test.FakeDestNamespace,
   399  			Server:    test.FakeClusterURL,
   400  			Revision:  "abc123",
   401  		},
   402  		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
   403  	}
   404  	ctrl := newFakeController(&data, nil)
   405  	sources := make([]v1alpha1.ApplicationSource, 0)
   406  	sources = append(sources, app.Spec.GetSource())
   407  	revisions := make([]string, 0)
   408  	revisions = append(revisions, "")
   409  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
   410  	require.NoError(t, err)
   411  	assert.NotNil(t, compRes)
   412  	assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
   413  	assert.Len(t, compRes.resources, 1)
   414  	assert.Len(t, compRes.managedResources, 1)
   415  	assert.Empty(t, compRes.reconciliationResult.Hooks)
   416  	assert.Empty(t, app.Status.Conditions)
   417  }
   418  
   419  // checks that ignore resources are detected, but excluded from status
   420  func TestCompareAppStateCompareOptionIgnoreExtraneous(t *testing.T) {
   421  	pod := NewPod()
   422  	pod.SetAnnotations(map[string]string{common.AnnotationCompareOptions: "IgnoreExtraneous"})
   423  	app := newFakeApp()
   424  	data := fakeData{
   425  		apps: []runtime.Object{app},
   426  		manifestResponse: &apiclient.ManifestResponse{
   427  			Manifests: []string{},
   428  			Namespace: test.FakeDestNamespace,
   429  			Server:    test.FakeClusterURL,
   430  			Revision:  "abc123",
   431  		},
   432  		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
   433  	}
   434  	ctrl := newFakeController(&data, nil)
   435  
   436  	sources := make([]v1alpha1.ApplicationSource, 0)
   437  	sources = append(sources, app.Spec.GetSource())
   438  	revisions := make([]string, 0)
   439  	revisions = append(revisions, "")
   440  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
   441  	require.NoError(t, err)
   442  
   443  	assert.NotNil(t, compRes)
   444  	assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
   445  	assert.Empty(t, compRes.resources)
   446  	assert.Empty(t, compRes.managedResources)
   447  	assert.Empty(t, app.Status.Conditions)
   448  }
   449  
   450  // TestCompareAppStateExtraHook tests when there is an extra _hook_ object in live but not defined in git
   451  func TestCompareAppStateExtraHook(t *testing.T) {
   452  	pod := NewPod()
   453  	pod.SetAnnotations(map[string]string{synccommon.AnnotationKeyHook: "PreSync"})
   454  	pod.SetNamespace(test.FakeDestNamespace)
   455  	app := newFakeApp()
   456  	key := kube.ResourceKey{Group: "", Kind: "Pod", Namespace: test.FakeDestNamespace, Name: app.Name}
   457  	data := fakeData{
   458  		manifestResponse: &apiclient.ManifestResponse{
   459  			Manifests: []string{},
   460  			Namespace: test.FakeDestNamespace,
   461  			Server:    test.FakeClusterURL,
   462  			Revision:  "abc123",
   463  		},
   464  		managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
   465  			key: pod,
   466  		},
   467  	}
   468  	ctrl := newFakeController(&data, nil)
   469  	sources := make([]v1alpha1.ApplicationSource, 0)
   470  	sources = append(sources, app.Spec.GetSource())
   471  	revisions := make([]string, 0)
   472  	revisions = append(revisions, "")
   473  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
   474  	require.NoError(t, err)
   475  
   476  	assert.NotNil(t, compRes)
   477  	assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
   478  	assert.Len(t, compRes.resources, 1)
   479  	assert.Len(t, compRes.managedResources, 1)
   480  	assert.Empty(t, compRes.reconciliationResult.Hooks)
   481  	assert.Empty(t, app.Status.Conditions)
   482  }
   483  
   484  // TestAppRevisions tests that revisions are properly propagated for a single source app
   485  func TestAppRevisionsSingleSource(t *testing.T) {
   486  	obj1 := NewPod()
   487  	obj1.SetNamespace(test.FakeDestNamespace)
   488  	data := fakeData{
   489  		manifestResponse: &apiclient.ManifestResponse{
   490  			Manifests: []string{toJSON(t, obj1)},
   491  			Namespace: test.FakeDestNamespace,
   492  			Server:    test.FakeClusterURL,
   493  			Revision:  "abc123",
   494  		},
   495  		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
   496  	}
   497  	ctrl := newFakeController(&data, nil)
   498  
   499  	app := newFakeApp()
   500  	revisions := make([]string, 0)
   501  	revisions = append(revisions, "")
   502  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, app.Spec.GetSources(), false, false, nil, app.Spec.HasMultipleSources())
   503  	require.NoError(t, err)
   504  	assert.NotNil(t, compRes)
   505  	assert.NotNil(t, compRes.syncStatus)
   506  	assert.NotEmpty(t, compRes.syncStatus.Revision)
   507  	assert.Empty(t, compRes.syncStatus.Revisions)
   508  }
   509  
   510  // TestAppRevisions tests that revisions are properly propagated for a multi source app
   511  func TestAppRevisionsMultiSource(t *testing.T) {
   512  	obj1 := NewPod()
   513  	obj1.SetNamespace(test.FakeDestNamespace)
   514  	data := fakeData{
   515  		manifestResponses: []*apiclient.ManifestResponse{
   516  			{
   517  				Manifests: []string{toJSON(t, obj1)},
   518  				Namespace: test.FakeDestNamespace,
   519  				Server:    test.FakeClusterURL,
   520  				Revision:  "abc123",
   521  			},
   522  			{
   523  				Manifests: []string{toJSON(t, obj1)},
   524  				Namespace: test.FakeDestNamespace,
   525  				Server:    test.FakeClusterURL,
   526  				Revision:  "def456",
   527  			},
   528  			{
   529  				Manifests: []string{},
   530  				Namespace: test.FakeDestNamespace,
   531  				Server:    test.FakeClusterURL,
   532  				Revision:  "ghi789",
   533  			},
   534  		},
   535  		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
   536  	}
   537  	ctrl := newFakeController(&data, nil)
   538  
   539  	app := newFakeMultiSourceApp()
   540  	revisions := make([]string, 0)
   541  	revisions = append(revisions, "")
   542  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, app.Spec.GetSources(), false, false, nil, app.Spec.HasMultipleSources())
   543  	require.NoError(t, err)
   544  	assert.NotNil(t, compRes)
   545  	assert.NotNil(t, compRes.syncStatus)
   546  	assert.Empty(t, compRes.syncStatus.Revision)
   547  	assert.Len(t, compRes.syncStatus.Revisions, 3)
   548  	assert.Equal(t, "abc123", compRes.syncStatus.Revisions[0])
   549  	assert.Equal(t, "def456", compRes.syncStatus.Revisions[1])
   550  	assert.Equal(t, "ghi789", compRes.syncStatus.Revisions[2])
   551  }
   552  
   553  func toJSON(t *testing.T, obj *unstructured.Unstructured) string {
   554  	t.Helper()
   555  	data, err := json.Marshal(obj)
   556  	require.NoError(t, err)
   557  	return string(data)
   558  }
   559  
   560  func TestCompareAppStateDuplicatedNamespacedResources(t *testing.T) {
   561  	obj1 := NewPod()
   562  	obj1.SetNamespace(test.FakeDestNamespace)
   563  	obj2 := NewPod()
   564  	obj3 := NewPod()
   565  	obj3.SetNamespace("kube-system")
   566  	obj4 := NewPod()
   567  	obj4.SetGenerateName("my-pod")
   568  	obj4.SetName("")
   569  	obj5 := NewPod()
   570  	obj5.SetName("")
   571  	obj5.SetGenerateName("my-pod")
   572  
   573  	app := newFakeApp()
   574  	data := fakeData{
   575  		manifestResponse: &apiclient.ManifestResponse{
   576  			Manifests: []string{toJSON(t, obj1), toJSON(t, obj2), toJSON(t, obj3), toJSON(t, obj4), toJSON(t, obj5)},
   577  			Namespace: test.FakeDestNamespace,
   578  			Server:    test.FakeClusterURL,
   579  			Revision:  "abc123",
   580  		},
   581  		managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
   582  			kube.GetResourceKey(obj1): obj1,
   583  			kube.GetResourceKey(obj3): obj3,
   584  		},
   585  	}
   586  	ctrl := newFakeController(&data, nil)
   587  	sources := make([]v1alpha1.ApplicationSource, 0)
   588  	sources = append(sources, app.Spec.GetSource())
   589  	revisions := make([]string, 0)
   590  	revisions = append(revisions, "")
   591  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
   592  	require.NoError(t, err)
   593  
   594  	assert.NotNil(t, compRes)
   595  	assert.Len(t, app.Status.Conditions, 1)
   596  	assert.NotNil(t, app.Status.Conditions[0].LastTransitionTime)
   597  	assert.Equal(t, v1alpha1.ApplicationConditionRepeatedResourceWarning, app.Status.Conditions[0].Type)
   598  	assert.Equal(t, "Resource /Pod/fake-dest-ns/my-pod appeared 2 times among application resources.", app.Status.Conditions[0].Message)
   599  	assert.Len(t, compRes.resources, 4)
   600  }
   601  
   602  func TestCompareAppStateManagedNamespaceMetadataWithLiveNsDoesNotGetPruned(t *testing.T) {
   603  	app := newFakeApp()
   604  	app.Spec.SyncPolicy = &v1alpha1.SyncPolicy{
   605  		ManagedNamespaceMetadata: &v1alpha1.ManagedNamespaceMetadata{
   606  			Labels:      nil,
   607  			Annotations: nil,
   608  		},
   609  	}
   610  
   611  	ns := NewNamespace()
   612  	ns.SetName(test.FakeDestNamespace)
   613  	ns.SetNamespace(test.FakeDestNamespace)
   614  	ns.SetAnnotations(map[string]string{"argocd.argoproj.io/sync-options": "ServerSideApply=true"})
   615  
   616  	data := fakeData{
   617  		manifestResponse: &apiclient.ManifestResponse{
   618  			Manifests: []string{},
   619  			Namespace: test.FakeDestNamespace,
   620  			Server:    test.FakeClusterURL,
   621  			Revision:  "abc123",
   622  		},
   623  		managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
   624  			kube.GetResourceKey(ns): ns,
   625  		},
   626  	}
   627  	ctrl := newFakeController(&data, nil)
   628  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, []string{}, app.Spec.Sources, false, false, nil, false)
   629  	require.NoError(t, err)
   630  
   631  	assert.NotNil(t, compRes)
   632  	assert.Empty(t, app.Status.Conditions)
   633  	assert.NotNil(t, compRes)
   634  	assert.NotNil(t, compRes.syncStatus)
   635  	// Ensure that ns does not get pruned
   636  	assert.NotNil(t, compRes.reconciliationResult.Target[0])
   637  	assert.Equal(t, compRes.reconciliationResult.Target[0].GetName(), ns.GetName())
   638  	assert.Equal(t, compRes.reconciliationResult.Target[0].GetAnnotations(), ns.GetAnnotations())
   639  	assert.Equal(t, compRes.reconciliationResult.Target[0].GetLabels(), ns.GetLabels())
   640  	assert.Len(t, compRes.resources, 1)
   641  	assert.Len(t, compRes.managedResources, 1)
   642  }
   643  
   644  var defaultProj = v1alpha1.AppProject{
   645  	ObjectMeta: metav1.ObjectMeta{
   646  		Name:      "default",
   647  		Namespace: test.FakeArgoCDNamespace,
   648  	},
   649  	Spec: v1alpha1.AppProjectSpec{
   650  		SourceRepos: []string{"*"},
   651  		Destinations: []v1alpha1.ApplicationDestination{
   652  			{
   653  				Server:    "*",
   654  				Namespace: "*",
   655  			},
   656  		},
   657  	},
   658  }
   659  
   660  // TestCompareAppStateWithManifestGeneratePath tests that it compares revisions when the manifest-generate-path annotation is set.
   661  func TestCompareAppStateWithManifestGeneratePath(t *testing.T) {
   662  	app := newFakeApp()
   663  	app.SetAnnotations(map[string]string{v1alpha1.AnnotationKeyManifestGeneratePaths: "."})
   664  	app.Status.Sync = v1alpha1.SyncStatus{
   665  		Revision: "abc123",
   666  		Status:   v1alpha1.SyncStatusCodeSynced,
   667  	}
   668  
   669  	data := fakeData{
   670  		manifestResponse: &apiclient.ManifestResponse{
   671  			Manifests: []string{},
   672  			Namespace: test.FakeDestNamespace,
   673  			Server:    test.FakeClusterURL,
   674  			Revision:  "abc123",
   675  		},
   676  		updateRevisionForPathsResponse: &apiclient.UpdateRevisionForPathsResponse{},
   677  	}
   678  
   679  	ctrl := newFakeController(&data, nil)
   680  	revisions := make([]string, 0)
   681  	revisions = append(revisions, "abc123")
   682  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, app.Spec.GetSources(), false, false, nil, false)
   683  	require.NoError(t, err)
   684  	assert.NotNil(t, compRes)
   685  	assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
   686  	assert.Equal(t, "abc123", compRes.syncStatus.Revision)
   687  }
   688  
   689  func TestSetHealth(t *testing.T) {
   690  	app := newFakeApp()
   691  	deployment := kube.MustToUnstructured(&appsv1.Deployment{
   692  		TypeMeta: metav1.TypeMeta{
   693  			APIVersion: "apps/v1",
   694  			Kind:       "Deployment",
   695  		},
   696  		ObjectMeta: metav1.ObjectMeta{
   697  			Name:      "demo",
   698  			Namespace: "default",
   699  		},
   700  	})
   701  	ctrl := newFakeController(&fakeData{
   702  		apps: []runtime.Object{app, &defaultProj},
   703  		manifestResponse: &apiclient.ManifestResponse{
   704  			Manifests: []string{},
   705  			Namespace: test.FakeDestNamespace,
   706  			Server:    test.FakeClusterURL,
   707  			Revision:  "abc123",
   708  		},
   709  		managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
   710  			kube.GetResourceKey(deployment): deployment,
   711  		},
   712  	}, nil)
   713  
   714  	sources := make([]v1alpha1.ApplicationSource, 0)
   715  	sources = append(sources, app.Spec.GetSource())
   716  	revisions := make([]string, 0)
   717  	revisions = append(revisions, "")
   718  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
   719  	require.NoError(t, err)
   720  
   721  	assert.Equal(t, health.HealthStatusHealthy, compRes.healthStatus)
   722  }
   723  
   724  func TestPreserveStatusTimestamp(t *testing.T) {
   725  	timestamp := metav1.Now()
   726  	app := newFakeAppWithHealthAndTime(health.HealthStatusHealthy, timestamp)
   727  	deployment := kube.MustToUnstructured(&appsv1.Deployment{
   728  		TypeMeta: metav1.TypeMeta{
   729  			APIVersion: "apps/v1",
   730  			Kind:       "Deployment",
   731  		},
   732  		ObjectMeta: metav1.ObjectMeta{
   733  			Name:      "demo",
   734  			Namespace: "default",
   735  		},
   736  	})
   737  	ctrl := newFakeController(&fakeData{
   738  		apps: []runtime.Object{app, &defaultProj},
   739  		manifestResponse: &apiclient.ManifestResponse{
   740  			Manifests: []string{},
   741  			Namespace: test.FakeDestNamespace,
   742  			Server:    test.FakeClusterURL,
   743  			Revision:  "abc123",
   744  		},
   745  		managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
   746  			kube.GetResourceKey(deployment): deployment,
   747  		},
   748  	}, nil)
   749  
   750  	sources := make([]v1alpha1.ApplicationSource, 0)
   751  	sources = append(sources, app.Spec.GetSource())
   752  	revisions := make([]string, 0)
   753  	revisions = append(revisions, "")
   754  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
   755  	require.NoError(t, err)
   756  
   757  	assert.Equal(t, health.HealthStatusHealthy, compRes.healthStatus)
   758  }
   759  
   760  func TestSetHealthSelfReferencedApp(t *testing.T) {
   761  	app := newFakeApp()
   762  	unstructuredApp := kube.MustToUnstructured(app)
   763  	deployment := kube.MustToUnstructured(&appsv1.Deployment{
   764  		TypeMeta: metav1.TypeMeta{
   765  			APIVersion: "apps/v1",
   766  			Kind:       "Deployment",
   767  		},
   768  		ObjectMeta: metav1.ObjectMeta{
   769  			Name:      "demo",
   770  			Namespace: "default",
   771  		},
   772  	})
   773  	ctrl := newFakeController(&fakeData{
   774  		apps: []runtime.Object{app, &defaultProj},
   775  		manifestResponse: &apiclient.ManifestResponse{
   776  			Manifests: []string{},
   777  			Namespace: test.FakeDestNamespace,
   778  			Server:    test.FakeClusterURL,
   779  			Revision:  "abc123",
   780  		},
   781  		managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
   782  			kube.GetResourceKey(deployment):      deployment,
   783  			kube.GetResourceKey(unstructuredApp): unstructuredApp,
   784  		},
   785  	}, nil)
   786  
   787  	sources := make([]v1alpha1.ApplicationSource, 0)
   788  	sources = append(sources, app.Spec.GetSource())
   789  	revisions := make([]string, 0)
   790  	revisions = append(revisions, "")
   791  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
   792  	require.NoError(t, err)
   793  
   794  	assert.Equal(t, health.HealthStatusHealthy, compRes.healthStatus)
   795  }
   796  
   797  func TestSetManagedResourcesWithOrphanedResources(t *testing.T) {
   798  	proj := defaultProj.DeepCopy()
   799  	proj.Spec.OrphanedResources = &v1alpha1.OrphanedResourcesMonitorSettings{}
   800  
   801  	app := newFakeApp()
   802  	ctrl := newFakeController(&fakeData{
   803  		apps: []runtime.Object{app, proj},
   804  		namespacedResources: map[kube.ResourceKey]namespacedResource{
   805  			kube.NewResourceKey("apps", kube.DeploymentKind, app.Namespace, "guestbook"): {
   806  				ResourceNode: v1alpha1.ResourceNode{
   807  					ResourceRef: v1alpha1.ResourceRef{Kind: kube.DeploymentKind, Name: "guestbook", Namespace: app.Namespace},
   808  				},
   809  				AppName: "",
   810  			},
   811  		},
   812  	}, nil)
   813  
   814  	tree, err := ctrl.setAppManagedResources(&v1alpha1.Cluster{Server: "test", Name: "test"}, app, &comparisonResult{managedResources: make([]managedResource, 0)})
   815  
   816  	require.NoError(t, err)
   817  	assert.Len(t, tree.OrphanedNodes, 1)
   818  	assert.Equal(t, "guestbook", tree.OrphanedNodes[0].Name)
   819  	assert.Equal(t, app.Namespace, tree.OrphanedNodes[0].Namespace)
   820  }
   821  
   822  func TestSetManagedResourcesWithResourcesOfAnotherApp(t *testing.T) {
   823  	proj := defaultProj.DeepCopy()
   824  	proj.Spec.OrphanedResources = &v1alpha1.OrphanedResourcesMonitorSettings{}
   825  
   826  	app1 := newFakeApp()
   827  	app1.Name = "app1"
   828  	app2 := newFakeApp()
   829  	app2.Name = "app2"
   830  
   831  	ctrl := newFakeController(&fakeData{
   832  		apps: []runtime.Object{app1, app2, proj},
   833  		namespacedResources: map[kube.ResourceKey]namespacedResource{
   834  			kube.NewResourceKey("apps", kube.DeploymentKind, app2.Namespace, "guestbook"): {
   835  				ResourceNode: v1alpha1.ResourceNode{
   836  					ResourceRef: v1alpha1.ResourceRef{Kind: kube.DeploymentKind, Name: "guestbook", Namespace: app2.Namespace},
   837  				},
   838  				AppName: "app2",
   839  			},
   840  		},
   841  	}, nil)
   842  
   843  	tree, err := ctrl.setAppManagedResources(&v1alpha1.Cluster{Server: "test", Name: "test"}, app1, &comparisonResult{managedResources: make([]managedResource, 0)})
   844  
   845  	require.NoError(t, err)
   846  	assert.Empty(t, tree.OrphanedNodes)
   847  }
   848  
   849  func TestReturnUnknownComparisonStateOnSettingLoadError(t *testing.T) {
   850  	proj := defaultProj.DeepCopy()
   851  	proj.Spec.OrphanedResources = &v1alpha1.OrphanedResourcesMonitorSettings{}
   852  
   853  	app := newFakeApp()
   854  
   855  	ctrl := newFakeController(&fakeData{
   856  		apps: []runtime.Object{app, proj},
   857  		configMapData: map[string]string{
   858  			"resource.customizations": "invalid setting",
   859  		},
   860  	}, nil)
   861  
   862  	sources := make([]v1alpha1.ApplicationSource, 0)
   863  	sources = append(sources, app.Spec.GetSource())
   864  	revisions := make([]string, 0)
   865  	revisions = append(revisions, "")
   866  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
   867  	require.NoError(t, err)
   868  
   869  	assert.Equal(t, health.HealthStatusUnknown, compRes.healthStatus)
   870  	assert.Equal(t, v1alpha1.SyncStatusCodeUnknown, compRes.syncStatus.Status)
   871  }
   872  
   873  func TestSetManagedResourcesKnownOrphanedResourceExceptions(t *testing.T) {
   874  	proj := defaultProj.DeepCopy()
   875  	proj.Spec.OrphanedResources = &v1alpha1.OrphanedResourcesMonitorSettings{}
   876  	proj.Spec.SourceNamespaces = []string{"default"}
   877  
   878  	app := newFakeApp()
   879  	app.Namespace = "default"
   880  
   881  	ctrl := newFakeController(&fakeData{
   882  		apps: []runtime.Object{app, proj},
   883  		namespacedResources: map[kube.ResourceKey]namespacedResource{
   884  			kube.NewResourceKey("apps", kube.DeploymentKind, app.Namespace, "guestbook"): {
   885  				ResourceNode: v1alpha1.ResourceNode{ResourceRef: v1alpha1.ResourceRef{Group: "apps", Kind: kube.DeploymentKind, Name: "guestbook", Namespace: app.Namespace}},
   886  			},
   887  			kube.NewResourceKey("", kube.ServiceAccountKind, app.Namespace, "default"): {
   888  				ResourceNode: v1alpha1.ResourceNode{ResourceRef: v1alpha1.ResourceRef{Kind: kube.ServiceAccountKind, Name: "default", Namespace: app.Namespace}},
   889  			},
   890  			kube.NewResourceKey("", kube.ServiceKind, app.Namespace, "kubernetes"): {
   891  				ResourceNode: v1alpha1.ResourceNode{ResourceRef: v1alpha1.ResourceRef{Kind: kube.ServiceAccountKind, Name: "kubernetes", Namespace: app.Namespace}},
   892  			},
   893  		},
   894  	}, nil)
   895  
   896  	tree, err := ctrl.setAppManagedResources(&v1alpha1.Cluster{Server: "test", Name: "test"}, app, &comparisonResult{managedResources: make([]managedResource, 0)})
   897  
   898  	require.NoError(t, err)
   899  	assert.Len(t, tree.OrphanedNodes, 1)
   900  	assert.Equal(t, "guestbook", tree.OrphanedNodes[0].Name)
   901  }
   902  
   903  func Test_appStateManager_persistRevisionHistory(t *testing.T) {
   904  	app := newFakeApp()
   905  	ctrl := newFakeController(&fakeData{
   906  		apps: []runtime.Object{app},
   907  	}, nil)
   908  	manager := ctrl.appStateManager.(*appStateManager)
   909  	setRevisionHistoryLimit := func(value int) {
   910  		if value < 0 {
   911  			value = 0
   912  		}
   913  		i := int64(value)
   914  		app.Spec.RevisionHistoryLimit = &i
   915  	}
   916  	addHistory := func() {
   917  		err := manager.persistRevisionHistory(app, "my-revision", v1alpha1.ApplicationSource{}, []string{}, []v1alpha1.ApplicationSource{}, false, metav1.Time{}, v1alpha1.OperationInitiator{})
   918  		require.NoError(t, err)
   919  	}
   920  	addHistory()
   921  	assert.Len(t, app.Status.History, 1)
   922  	addHistory()
   923  	assert.Len(t, app.Status.History, 2)
   924  	addHistory()
   925  	assert.Len(t, app.Status.History, 3)
   926  	addHistory()
   927  	assert.Len(t, app.Status.History, 4)
   928  	addHistory()
   929  	assert.Len(t, app.Status.History, 5)
   930  	addHistory()
   931  	assert.Len(t, app.Status.History, 6)
   932  	addHistory()
   933  	assert.Len(t, app.Status.History, 7)
   934  	addHistory()
   935  	assert.Len(t, app.Status.History, 8)
   936  	addHistory()
   937  	assert.Len(t, app.Status.History, 9)
   938  	addHistory()
   939  	assert.Len(t, app.Status.History, 10)
   940  	// default limit is 10
   941  	addHistory()
   942  	assert.Len(t, app.Status.History, 10)
   943  	// increase limit
   944  	setRevisionHistoryLimit(11)
   945  	addHistory()
   946  	assert.Len(t, app.Status.History, 11)
   947  	// decrease limit
   948  	setRevisionHistoryLimit(9)
   949  	addHistory()
   950  	assert.Len(t, app.Status.History, 9)
   951  
   952  	metav1NowTime := metav1.NewTime(time.Now())
   953  	err := manager.persistRevisionHistory(app, "my-revision", v1alpha1.ApplicationSource{}, []string{}, []v1alpha1.ApplicationSource{}, false, metav1NowTime, v1alpha1.OperationInitiator{})
   954  	require.NoError(t, err)
   955  	assert.Equal(t, app.Status.History.LastRevisionHistory().DeployStartedAt, &metav1NowTime)
   956  
   957  	// negative limit to 0
   958  	setRevisionHistoryLimit(-1)
   959  	addHistory()
   960  	assert.Empty(t, app.Status.History)
   961  }
   962  
   963  // helper function to read contents of a file to string
   964  // panics on error
   965  func mustReadFile(path string) string {
   966  	b, err := os.ReadFile(path)
   967  	if err != nil {
   968  		panic(err.Error())
   969  	}
   970  	return string(b)
   971  }
   972  
   973  var signedProj = v1alpha1.AppProject{
   974  	ObjectMeta: metav1.ObjectMeta{
   975  		Name:      "default",
   976  		Namespace: test.FakeArgoCDNamespace,
   977  	},
   978  	Spec: v1alpha1.AppProjectSpec{
   979  		SourceRepos: []string{"*"},
   980  		Destinations: []v1alpha1.ApplicationDestination{
   981  			{
   982  				Server:    "*",
   983  				Namespace: "*",
   984  			},
   985  		},
   986  		SignatureKeys: []v1alpha1.SignatureKey{
   987  			{
   988  				KeyID: "4AEE18F83AFDEB23",
   989  			},
   990  		},
   991  	},
   992  }
   993  
   994  func TestSignedResponseNoSignatureRequired(t *testing.T) {
   995  	t.Setenv("ARGOCD_GPG_ENABLED", "true")
   996  
   997  	// We have a good signature response, but project does not require signed commits
   998  	{
   999  		app := newFakeApp()
  1000  		data := fakeData{
  1001  			manifestResponse: &apiclient.ManifestResponse{
  1002  				Manifests:    []string{},
  1003  				Namespace:    test.FakeDestNamespace,
  1004  				Server:       test.FakeClusterURL,
  1005  				Revision:     "abc123",
  1006  				VerifyResult: mustReadFile("../util/gpg/testdata/good_signature.txt"),
  1007  			},
  1008  			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
  1009  		}
  1010  		ctrl := newFakeController(&data, nil)
  1011  		sources := make([]v1alpha1.ApplicationSource, 0)
  1012  		sources = append(sources, app.Spec.GetSource())
  1013  		revisions := make([]string, 0)
  1014  		revisions = append(revisions, "")
  1015  		compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
  1016  		require.NoError(t, err)
  1017  		assert.NotNil(t, compRes)
  1018  		assert.NotNil(t, compRes.syncStatus)
  1019  		assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
  1020  		assert.Empty(t, compRes.resources)
  1021  		assert.Empty(t, compRes.managedResources)
  1022  		assert.Empty(t, app.Status.Conditions)
  1023  	}
  1024  	// We have a bad signature response, but project does not require signed commits
  1025  	{
  1026  		app := newFakeApp()
  1027  		data := fakeData{
  1028  			manifestResponse: &apiclient.ManifestResponse{
  1029  				Manifests:    []string{},
  1030  				Namespace:    test.FakeDestNamespace,
  1031  				Server:       test.FakeClusterURL,
  1032  				Revision:     "abc123",
  1033  				VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_bad.txt"),
  1034  			},
  1035  			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
  1036  		}
  1037  		ctrl := newFakeController(&data, nil)
  1038  		sources := make([]v1alpha1.ApplicationSource, 0)
  1039  		sources = append(sources, app.Spec.GetSource())
  1040  		revisions := make([]string, 0)
  1041  		revisions = append(revisions, "")
  1042  		compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
  1043  		require.NoError(t, err)
  1044  		assert.NotNil(t, compRes)
  1045  		assert.NotNil(t, compRes.syncStatus)
  1046  		assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
  1047  		assert.Empty(t, compRes.resources)
  1048  		assert.Empty(t, compRes.managedResources)
  1049  		assert.Empty(t, app.Status.Conditions)
  1050  	}
  1051  }
  1052  
  1053  func TestSignedResponseSignatureRequired(t *testing.T) {
  1054  	t.Setenv("ARGOCD_GPG_ENABLED", "true")
  1055  
  1056  	// We have a good signature response, valid key, and signing is required - sync!
  1057  	{
  1058  		app := newFakeApp()
  1059  		data := fakeData{
  1060  			manifestResponse: &apiclient.ManifestResponse{
  1061  				Manifests:    []string{},
  1062  				Namespace:    test.FakeDestNamespace,
  1063  				Server:       test.FakeClusterURL,
  1064  				Revision:     "abc123",
  1065  				VerifyResult: mustReadFile("../util/gpg/testdata/good_signature.txt"),
  1066  			},
  1067  			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
  1068  		}
  1069  		ctrl := newFakeController(&data, nil)
  1070  		sources := make([]v1alpha1.ApplicationSource, 0)
  1071  		sources = append(sources, app.Spec.GetSource())
  1072  		revisions := make([]string, 0)
  1073  		revisions = append(revisions, "")
  1074  		compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, nil, false)
  1075  		require.NoError(t, err)
  1076  		assert.NotNil(t, compRes)
  1077  		assert.NotNil(t, compRes.syncStatus)
  1078  		assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
  1079  		assert.Empty(t, compRes.resources)
  1080  		assert.Empty(t, compRes.managedResources)
  1081  		assert.Empty(t, app.Status.Conditions)
  1082  	}
  1083  	// We have a bad signature response and signing is required - do not sync
  1084  	{
  1085  		app := newFakeApp()
  1086  		data := fakeData{
  1087  			manifestResponse: &apiclient.ManifestResponse{
  1088  				Manifests:    []string{},
  1089  				Namespace:    test.FakeDestNamespace,
  1090  				Server:       test.FakeClusterURL,
  1091  				Revision:     "abc123",
  1092  				VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_bad.txt"),
  1093  			},
  1094  			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
  1095  		}
  1096  		ctrl := newFakeController(&data, nil)
  1097  		sources := make([]v1alpha1.ApplicationSource, 0)
  1098  		sources = append(sources, app.Spec.GetSource())
  1099  		revisions := make([]string, 0)
  1100  		revisions = append(revisions, "abc123")
  1101  		compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, nil, false)
  1102  		require.NoError(t, err)
  1103  		assert.NotNil(t, compRes)
  1104  		assert.NotNil(t, compRes.syncStatus)
  1105  		assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
  1106  		assert.Empty(t, compRes.resources)
  1107  		assert.Empty(t, compRes.managedResources)
  1108  		assert.Len(t, app.Status.Conditions, 1)
  1109  	}
  1110  	// We have a malformed signature response and signing is required - do not sync
  1111  	{
  1112  		app := newFakeApp()
  1113  		data := fakeData{
  1114  			manifestResponse: &apiclient.ManifestResponse{
  1115  				Manifests:    []string{},
  1116  				Namespace:    test.FakeDestNamespace,
  1117  				Server:       test.FakeClusterURL,
  1118  				Revision:     "abc123",
  1119  				VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_malformed1.txt"),
  1120  			},
  1121  			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
  1122  		}
  1123  		ctrl := newFakeController(&data, nil)
  1124  		sources := make([]v1alpha1.ApplicationSource, 0)
  1125  		sources = append(sources, app.Spec.GetSource())
  1126  		revisions := make([]string, 0)
  1127  		revisions = append(revisions, "abc123")
  1128  		compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, nil, false)
  1129  		require.NoError(t, err)
  1130  		assert.NotNil(t, compRes)
  1131  		assert.NotNil(t, compRes.syncStatus)
  1132  		assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
  1133  		assert.Empty(t, compRes.resources)
  1134  		assert.Empty(t, compRes.managedResources)
  1135  		assert.Len(t, app.Status.Conditions, 1)
  1136  	}
  1137  	// We have no signature response (no signature made) and signing is required - do not sync
  1138  	{
  1139  		app := newFakeApp()
  1140  		data := fakeData{
  1141  			manifestResponse: &apiclient.ManifestResponse{
  1142  				Manifests:    []string{},
  1143  				Namespace:    test.FakeDestNamespace,
  1144  				Server:       test.FakeClusterURL,
  1145  				Revision:     "abc123",
  1146  				VerifyResult: "",
  1147  			},
  1148  			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
  1149  		}
  1150  		ctrl := newFakeController(&data, nil)
  1151  		sources := make([]v1alpha1.ApplicationSource, 0)
  1152  		sources = append(sources, app.Spec.GetSource())
  1153  		revisions := make([]string, 0)
  1154  		revisions = append(revisions, "abc123")
  1155  		compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, nil, false)
  1156  		require.NoError(t, err)
  1157  		assert.NotNil(t, compRes)
  1158  		assert.NotNil(t, compRes.syncStatus)
  1159  		assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
  1160  		assert.Empty(t, compRes.resources)
  1161  		assert.Empty(t, compRes.managedResources)
  1162  		assert.Len(t, app.Status.Conditions, 1)
  1163  	}
  1164  
  1165  	// We have a good signature and signing is required, but key is not allowed - do not sync
  1166  	{
  1167  		app := newFakeApp()
  1168  		data := fakeData{
  1169  			manifestResponse: &apiclient.ManifestResponse{
  1170  				Manifests:    []string{},
  1171  				Namespace:    test.FakeDestNamespace,
  1172  				Server:       test.FakeClusterURL,
  1173  				Revision:     "abc123",
  1174  				VerifyResult: mustReadFile("../util/gpg/testdata/good_signature.txt"),
  1175  			},
  1176  			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
  1177  		}
  1178  		ctrl := newFakeController(&data, nil)
  1179  		testProj := signedProj
  1180  		testProj.Spec.SignatureKeys[0].KeyID = "4AEE18F83AFDEB24"
  1181  		sources := make([]v1alpha1.ApplicationSource, 0)
  1182  		sources = append(sources, app.Spec.GetSource())
  1183  		revisions := make([]string, 0)
  1184  		revisions = append(revisions, "abc123")
  1185  		compRes, err := ctrl.appStateManager.CompareAppState(app, &testProj, revisions, sources, false, false, nil, false)
  1186  		require.NoError(t, err)
  1187  		assert.NotNil(t, compRes)
  1188  		assert.NotNil(t, compRes.syncStatus)
  1189  		assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
  1190  		assert.Empty(t, compRes.resources)
  1191  		assert.Empty(t, compRes.managedResources)
  1192  		assert.Len(t, app.Status.Conditions, 1)
  1193  		assert.Contains(t, app.Status.Conditions[0].Message, "key is not allowed")
  1194  	}
  1195  	// Signature required and local manifests supplied - do not sync
  1196  	{
  1197  		app := newFakeApp()
  1198  		data := fakeData{
  1199  			manifestResponse: &apiclient.ManifestResponse{
  1200  				Manifests:    []string{},
  1201  				Namespace:    test.FakeDestNamespace,
  1202  				Server:       test.FakeClusterURL,
  1203  				Revision:     "abc123",
  1204  				VerifyResult: "",
  1205  			},
  1206  			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
  1207  		}
  1208  		// it doesn't matter for our test whether local manifests are valid
  1209  		localManifests := []string{"foobar"}
  1210  		ctrl := newFakeController(&data, nil)
  1211  		sources := make([]v1alpha1.ApplicationSource, 0)
  1212  		sources = append(sources, app.Spec.GetSource())
  1213  		revisions := make([]string, 0)
  1214  		revisions = append(revisions, "abc123")
  1215  		compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, localManifests, false)
  1216  		require.NoError(t, err)
  1217  		assert.NotNil(t, compRes)
  1218  		assert.NotNil(t, compRes.syncStatus)
  1219  		assert.Equal(t, v1alpha1.SyncStatusCodeUnknown, compRes.syncStatus.Status)
  1220  		assert.Empty(t, compRes.resources)
  1221  		assert.Empty(t, compRes.managedResources)
  1222  		assert.Len(t, app.Status.Conditions, 1)
  1223  		assert.Contains(t, app.Status.Conditions[0].Message, "Cannot use local manifests")
  1224  	}
  1225  
  1226  	t.Setenv("ARGOCD_GPG_ENABLED", "false")
  1227  	// We have a bad signature response and signing would be required, but GPG subsystem is disabled - sync
  1228  	{
  1229  		app := newFakeApp()
  1230  		data := fakeData{
  1231  			manifestResponse: &apiclient.ManifestResponse{
  1232  				Manifests:    []string{},
  1233  				Namespace:    test.FakeDestNamespace,
  1234  				Server:       test.FakeClusterURL,
  1235  				Revision:     "abc123",
  1236  				VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_bad.txt"),
  1237  			},
  1238  			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
  1239  		}
  1240  		ctrl := newFakeController(&data, nil)
  1241  		sources := make([]v1alpha1.ApplicationSource, 0)
  1242  		sources = append(sources, app.Spec.GetSource())
  1243  		revisions := make([]string, 0)
  1244  		revisions = append(revisions, "abc123")
  1245  		compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, nil, false)
  1246  		require.NoError(t, err)
  1247  		assert.NotNil(t, compRes)
  1248  		assert.NotNil(t, compRes.syncStatus)
  1249  		assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
  1250  		assert.Empty(t, compRes.resources)
  1251  		assert.Empty(t, compRes.managedResources)
  1252  		assert.Empty(t, app.Status.Conditions)
  1253  	}
  1254  
  1255  	// Signature required and local manifests supplied and GPG subsystem is disabled - sync
  1256  	{
  1257  		app := newFakeApp()
  1258  		data := fakeData{
  1259  			manifestResponse: &apiclient.ManifestResponse{
  1260  				Manifests:    []string{},
  1261  				Namespace:    test.FakeDestNamespace,
  1262  				Server:       test.FakeClusterURL,
  1263  				Revision:     "abc123",
  1264  				VerifyResult: "",
  1265  			},
  1266  			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
  1267  		}
  1268  		// it doesn't matter for our test whether local manifests are valid
  1269  		localManifests := []string{""}
  1270  		ctrl := newFakeController(&data, nil)
  1271  		sources := make([]v1alpha1.ApplicationSource, 0)
  1272  		sources = append(sources, app.Spec.GetSource())
  1273  		revisions := make([]string, 0)
  1274  		revisions = append(revisions, "abc123")
  1275  		compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, localManifests, false)
  1276  		require.NoError(t, err)
  1277  		assert.NotNil(t, compRes)
  1278  		assert.NotNil(t, compRes.syncStatus)
  1279  		assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
  1280  		assert.Empty(t, compRes.resources)
  1281  		assert.Empty(t, compRes.managedResources)
  1282  		assert.Empty(t, app.Status.Conditions)
  1283  	}
  1284  }
  1285  
  1286  func TestComparisonResult_GetHealthStatus(t *testing.T) {
  1287  	status := health.HealthStatusMissing
  1288  	res := comparisonResult{
  1289  		healthStatus: status,
  1290  	}
  1291  
  1292  	assert.Equal(t, status, res.GetHealthStatus())
  1293  }
  1294  
  1295  func TestComparisonResult_GetSyncStatus(t *testing.T) {
  1296  	status := &v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeOutOfSync}
  1297  	res := comparisonResult{
  1298  		syncStatus: status,
  1299  	}
  1300  
  1301  	assert.Equal(t, status, res.GetSyncStatus())
  1302  }
  1303  
  1304  func TestIsLiveResourceManaged(t *testing.T) {
  1305  	t.Parallel()
  1306  
  1307  	managedObj := kube.MustToUnstructured(&corev1.ConfigMap{
  1308  		TypeMeta: metav1.TypeMeta{
  1309  			APIVersion: "v1",
  1310  			Kind:       "ConfigMap",
  1311  		},
  1312  		ObjectMeta: metav1.ObjectMeta{
  1313  			Name:      "configmap1",
  1314  			Namespace: "default",
  1315  			Annotations: map[string]string{
  1316  				common.AnnotationKeyAppInstance: "guestbook:/ConfigMap:default/configmap1",
  1317  			},
  1318  		},
  1319  	})
  1320  	managedObjWithLabel := kube.MustToUnstructured(&corev1.ConfigMap{
  1321  		TypeMeta: metav1.TypeMeta{
  1322  			APIVersion: "v1",
  1323  			Kind:       "ConfigMap",
  1324  		},
  1325  		ObjectMeta: metav1.ObjectMeta{
  1326  			Name:      "configmap1",
  1327  			Namespace: "default",
  1328  			Labels: map[string]string{
  1329  				common.LabelKeyAppInstance: "guestbook",
  1330  			},
  1331  		},
  1332  	})
  1333  	unmanagedObjWrongName := kube.MustToUnstructured(&corev1.ConfigMap{
  1334  		TypeMeta: metav1.TypeMeta{
  1335  			APIVersion: "v1",
  1336  			Kind:       "ConfigMap",
  1337  		},
  1338  		ObjectMeta: metav1.ObjectMeta{
  1339  			Name:      "configmap2",
  1340  			Namespace: "default",
  1341  			Annotations: map[string]string{
  1342  				common.AnnotationKeyAppInstance: "guestbook:/ConfigMap:default/configmap1",
  1343  			},
  1344  		},
  1345  	})
  1346  	unmanagedObjWrongKind := kube.MustToUnstructured(&corev1.ConfigMap{
  1347  		TypeMeta: metav1.TypeMeta{
  1348  			APIVersion: "v1",
  1349  			Kind:       "ConfigMap",
  1350  		},
  1351  		ObjectMeta: metav1.ObjectMeta{
  1352  			Name:      "configmap2",
  1353  			Namespace: "default",
  1354  			Annotations: map[string]string{
  1355  				common.AnnotationKeyAppInstance: "guestbook:/Service:default/configmap2",
  1356  			},
  1357  		},
  1358  	})
  1359  	unmanagedObjWrongGroup := kube.MustToUnstructured(&corev1.ConfigMap{
  1360  		TypeMeta: metav1.TypeMeta{
  1361  			APIVersion: "v1",
  1362  			Kind:       "ConfigMap",
  1363  		},
  1364  		ObjectMeta: metav1.ObjectMeta{
  1365  			Name:      "configmap2",
  1366  			Namespace: "default",
  1367  			Annotations: map[string]string{
  1368  				common.AnnotationKeyAppInstance: "guestbook:apps/ConfigMap:default/configmap2",
  1369  			},
  1370  		},
  1371  	})
  1372  	unmanagedObjWrongNamespace := kube.MustToUnstructured(&corev1.ConfigMap{
  1373  		TypeMeta: metav1.TypeMeta{
  1374  			APIVersion: "v1",
  1375  			Kind:       "ConfigMap",
  1376  		},
  1377  		ObjectMeta: metav1.ObjectMeta{
  1378  			Name:      "configmap2",
  1379  			Namespace: "default",
  1380  			Annotations: map[string]string{
  1381  				common.AnnotationKeyAppInstance: "guestbook:/ConfigMap:fakens/configmap2",
  1382  			},
  1383  		},
  1384  	})
  1385  	managedWrongAPIGroup := kube.MustToUnstructured(&networkingv1.Ingress{
  1386  		TypeMeta: metav1.TypeMeta{
  1387  			APIVersion: "networking.k8s.io/v1",
  1388  			Kind:       "Ingress",
  1389  		},
  1390  		ObjectMeta: metav1.ObjectMeta{
  1391  			Name:      "some-ingress",
  1392  			Namespace: "default",
  1393  			Annotations: map[string]string{
  1394  				common.AnnotationKeyAppInstance: "guestbook:extensions/Ingress:default/some-ingress",
  1395  			},
  1396  		},
  1397  	})
  1398  	ctrl := newFakeController(&fakeData{
  1399  		apps: []runtime.Object{app, &defaultProj},
  1400  		manifestResponse: &apiclient.ManifestResponse{
  1401  			Manifests: []string{},
  1402  			Namespace: test.FakeDestNamespace,
  1403  			Server:    test.FakeClusterURL,
  1404  			Revision:  "abc123",
  1405  		},
  1406  		managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
  1407  			kube.GetResourceKey(managedObj):                 managedObj,
  1408  			kube.GetResourceKey(unmanagedObjWrongName):      unmanagedObjWrongName,
  1409  			kube.GetResourceKey(unmanagedObjWrongKind):      unmanagedObjWrongKind,
  1410  			kube.GetResourceKey(unmanagedObjWrongGroup):     unmanagedObjWrongGroup,
  1411  			kube.GetResourceKey(unmanagedObjWrongNamespace): unmanagedObjWrongNamespace,
  1412  		},
  1413  	}, nil)
  1414  
  1415  	manager := ctrl.appStateManager.(*appStateManager)
  1416  	appName := "guestbook"
  1417  
  1418  	t.Run("will return true if trackingid matches the resource", func(t *testing.T) {
  1419  		// given
  1420  		t.Parallel()
  1421  		configObj := managedObj.DeepCopy()
  1422  
  1423  		// then
  1424  		assert.True(t, manager.isSelfReferencedObj(managedObj, configObj, appName, v1alpha1.TrackingMethodLabel, ""))
  1425  		assert.True(t, manager.isSelfReferencedObj(managedObj, configObj, appName, v1alpha1.TrackingMethodAnnotation, ""))
  1426  	})
  1427  	t.Run("will return true if tracked with label", func(t *testing.T) {
  1428  		// given
  1429  		t.Parallel()
  1430  		configObj := managedObjWithLabel.DeepCopy()
  1431  
  1432  		// then
  1433  		assert.True(t, manager.isSelfReferencedObj(managedObjWithLabel, configObj, appName, v1alpha1.TrackingMethodLabel, ""))
  1434  	})
  1435  	t.Run("will handle if trackingId has wrong resource name and config is nil", func(t *testing.T) {
  1436  		// given
  1437  		t.Parallel()
  1438  
  1439  		// then
  1440  		assert.True(t, manager.isSelfReferencedObj(unmanagedObjWrongName, nil, appName, v1alpha1.TrackingMethodLabel, ""))
  1441  		assert.False(t, manager.isSelfReferencedObj(unmanagedObjWrongName, nil, appName, v1alpha1.TrackingMethodAnnotation, ""))
  1442  	})
  1443  	t.Run("will handle if trackingId has wrong resource group and config is nil", func(t *testing.T) {
  1444  		// given
  1445  		t.Parallel()
  1446  
  1447  		// then
  1448  		assert.True(t, manager.isSelfReferencedObj(unmanagedObjWrongGroup, nil, appName, v1alpha1.TrackingMethodLabel, ""))
  1449  		assert.False(t, manager.isSelfReferencedObj(unmanagedObjWrongGroup, nil, appName, v1alpha1.TrackingMethodAnnotation, ""))
  1450  	})
  1451  	t.Run("will handle if trackingId has wrong kind and config is nil", func(t *testing.T) {
  1452  		// given
  1453  		t.Parallel()
  1454  
  1455  		// then
  1456  		assert.True(t, manager.isSelfReferencedObj(unmanagedObjWrongKind, nil, appName, v1alpha1.TrackingMethodLabel, ""))
  1457  		assert.False(t, manager.isSelfReferencedObj(unmanagedObjWrongKind, nil, appName, v1alpha1.TrackingMethodAnnotation, ""))
  1458  	})
  1459  	t.Run("will handle if trackingId has wrong namespace and config is nil", func(t *testing.T) {
  1460  		// given
  1461  		t.Parallel()
  1462  
  1463  		// then
  1464  		assert.True(t, manager.isSelfReferencedObj(unmanagedObjWrongNamespace, nil, appName, v1alpha1.TrackingMethodLabel, ""))
  1465  		assert.False(t, manager.isSelfReferencedObj(unmanagedObjWrongNamespace, nil, appName, v1alpha1.TrackingMethodAnnotationAndLabel, ""))
  1466  	})
  1467  	t.Run("will return true if live is nil", func(t *testing.T) {
  1468  		t.Parallel()
  1469  		assert.True(t, manager.isSelfReferencedObj(nil, nil, appName, v1alpha1.TrackingMethodAnnotation, ""))
  1470  	})
  1471  
  1472  	t.Run("will handle upgrade in desired state APIGroup", func(t *testing.T) {
  1473  		// given
  1474  		t.Parallel()
  1475  		config := managedWrongAPIGroup.DeepCopy()
  1476  		delete(config.GetAnnotations(), common.AnnotationKeyAppInstance)
  1477  
  1478  		// then
  1479  		assert.True(t, manager.isSelfReferencedObj(managedWrongAPIGroup, config, appName, v1alpha1.TrackingMethodAnnotation, ""))
  1480  	})
  1481  }
  1482  
  1483  func TestUseDiffCache(t *testing.T) {
  1484  	t.Parallel()
  1485  	type fixture struct {
  1486  		testName             string
  1487  		noCache              bool
  1488  		manifestInfos        []*apiclient.ManifestResponse
  1489  		sources              []v1alpha1.ApplicationSource
  1490  		app                  *v1alpha1.Application
  1491  		manifestRevisions    []string
  1492  		statusRefreshTimeout time.Duration
  1493  		expectedUseCache     bool
  1494  		serverSideDiff       bool
  1495  	}
  1496  	manifestInfos := func(revision string) []*apiclient.ManifestResponse {
  1497  		return []*apiclient.ManifestResponse{
  1498  			{
  1499  				Manifests: []string{
  1500  					"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"labels\":{\"app.kubernetes.io/instance\":\"httpbin\"},\"name\":\"httpbin-svc\",\"namespace\":\"httpbin\"},\"spec\":{\"ports\":[{\"name\":\"http-port\",\"port\":7777,\"targetPort\":80},{\"name\":\"test\",\"port\":333}],\"selector\":{\"app\":\"httpbin\"}}}",
  1501  					"{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"labels\":{\"app.kubernetes.io/instance\":\"httpbin\"},\"name\":\"httpbin-deployment\",\"namespace\":\"httpbin\"},\"spec\":{\"replicas\":2,\"selector\":{\"matchLabels\":{\"app\":\"httpbin\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"httpbin\"}},\"spec\":{\"containers\":[{\"image\":\"kennethreitz/httpbin\",\"imagePullPolicy\":\"Always\",\"name\":\"httpbin\",\"ports\":[{\"containerPort\":80}]}]}}}}",
  1502  				},
  1503  				Namespace:    "",
  1504  				Server:       "",
  1505  				Revision:     revision,
  1506  				SourceType:   "Kustomize",
  1507  				VerifyResult: "",
  1508  			},
  1509  		}
  1510  	}
  1511  	source := func() v1alpha1.ApplicationSource {
  1512  		return v1alpha1.ApplicationSource{
  1513  			RepoURL:        "https://some-repo.com",
  1514  			Path:           "argocd/httpbin",
  1515  			TargetRevision: "HEAD",
  1516  		}
  1517  	}
  1518  	sources := func() []v1alpha1.ApplicationSource {
  1519  		return []v1alpha1.ApplicationSource{source()}
  1520  	}
  1521  
  1522  	app := func(namespace string, revision string, refresh bool, a *v1alpha1.Application) *v1alpha1.Application {
  1523  		app := &v1alpha1.Application{
  1524  			ObjectMeta: metav1.ObjectMeta{
  1525  				Name:      "httpbin",
  1526  				Namespace: namespace,
  1527  			},
  1528  			Spec: v1alpha1.ApplicationSpec{
  1529  				Source: ptr.To(source()),
  1530  				Destination: v1alpha1.ApplicationDestination{
  1531  					Server:    "https://kubernetes.default.svc",
  1532  					Namespace: "httpbin",
  1533  				},
  1534  				Project: "default",
  1535  				SyncPolicy: &v1alpha1.SyncPolicy{
  1536  					SyncOptions: []string{
  1537  						"CreateNamespace=true",
  1538  						"ServerSideApply=true",
  1539  					},
  1540  				},
  1541  			},
  1542  			Status: v1alpha1.ApplicationStatus{
  1543  				Resources: []v1alpha1.ResourceStatus{},
  1544  				Sync: v1alpha1.SyncStatus{
  1545  					Status: v1alpha1.SyncStatusCodeSynced,
  1546  					ComparedTo: v1alpha1.ComparedTo{
  1547  						Source: source(),
  1548  						Destination: v1alpha1.ApplicationDestination{
  1549  							Server:    "https://kubernetes.default.svc",
  1550  							Namespace: "httpbin",
  1551  						},
  1552  					},
  1553  					Revision:  revision,
  1554  					Revisions: []string{},
  1555  				},
  1556  				ReconciledAt: &metav1.Time{
  1557  					Time: time.Now().Add(-time.Hour),
  1558  				},
  1559  			},
  1560  		}
  1561  		if refresh {
  1562  			annotations := make(map[string]string)
  1563  			annotations[v1alpha1.AnnotationKeyRefresh] = string(v1alpha1.RefreshTypeNormal)
  1564  			app.SetAnnotations(annotations)
  1565  		}
  1566  		if a != nil {
  1567  			err := mergo.Merge(app, a, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue)
  1568  			require.NoErrorf(t, err, "error merging app")
  1569  		}
  1570  		return app
  1571  	}
  1572  	cases := []fixture{
  1573  		{
  1574  			testName:             "will use diff cache",
  1575  			noCache:              false,
  1576  			manifestInfos:        manifestInfos("rev1"),
  1577  			sources:              sources(),
  1578  			app:                  app("httpbin", "rev1", false, nil),
  1579  			manifestRevisions:    []string{"rev1"},
  1580  			statusRefreshTimeout: time.Hour * 24,
  1581  			expectedUseCache:     true,
  1582  			serverSideDiff:       false,
  1583  		},
  1584  		{
  1585  			testName:             "will use diff cache with sync policy",
  1586  			noCache:              false,
  1587  			manifestInfos:        manifestInfos("rev1"),
  1588  			sources:              []v1alpha1.ApplicationSource{test.YamlToApplication(testdata.DiffCacheYaml).Status.Sync.ComparedTo.Source},
  1589  			app:                  test.YamlToApplication(testdata.DiffCacheYaml),
  1590  			manifestRevisions:    []string{"rev1"},
  1591  			statusRefreshTimeout: time.Hour * 24,
  1592  			expectedUseCache:     true,
  1593  			serverSideDiff:       true,
  1594  		},
  1595  		{
  1596  			testName:      "will use diff cache for multisource",
  1597  			noCache:       false,
  1598  			manifestInfos: append(manifestInfos("rev1"), manifestInfos("rev2")...),
  1599  			sources: v1alpha1.ApplicationSources{
  1600  				{
  1601  					RepoURL: "multisource repo1",
  1602  				},
  1603  				{
  1604  					RepoURL: "multisource repo2",
  1605  				},
  1606  			},
  1607  			app: app("httpbin", "", false, &v1alpha1.Application{
  1608  				Spec: v1alpha1.ApplicationSpec{
  1609  					Source: nil,
  1610  					Sources: v1alpha1.ApplicationSources{
  1611  						{
  1612  							RepoURL: "multisource repo1",
  1613  						},
  1614  						{
  1615  							RepoURL: "multisource repo2",
  1616  						},
  1617  					},
  1618  				},
  1619  				Status: v1alpha1.ApplicationStatus{
  1620  					Resources: []v1alpha1.ResourceStatus{},
  1621  					Sync: v1alpha1.SyncStatus{
  1622  						Status: v1alpha1.SyncStatusCodeSynced,
  1623  						ComparedTo: v1alpha1.ComparedTo{
  1624  							Source: v1alpha1.ApplicationSource{},
  1625  							Sources: v1alpha1.ApplicationSources{
  1626  								{
  1627  									RepoURL: "multisource repo1",
  1628  								},
  1629  								{
  1630  									RepoURL: "multisource repo2",
  1631  								},
  1632  							},
  1633  						},
  1634  						Revisions: []string{"rev1", "rev2"},
  1635  					},
  1636  					ReconciledAt: &metav1.Time{
  1637  						Time: time.Now().Add(-time.Hour),
  1638  					},
  1639  				},
  1640  			}),
  1641  			manifestRevisions:    []string{"rev1", "rev2"},
  1642  			statusRefreshTimeout: time.Hour * 24,
  1643  			expectedUseCache:     true,
  1644  			serverSideDiff:       false,
  1645  		},
  1646  		{
  1647  			testName:             "will return false if nocache is true",
  1648  			noCache:              true,
  1649  			manifestInfos:        manifestInfos("rev1"),
  1650  			sources:              sources(),
  1651  			app:                  app("httpbin", "rev1", false, nil),
  1652  			manifestRevisions:    []string{"rev1"},
  1653  			statusRefreshTimeout: time.Hour * 24,
  1654  			expectedUseCache:     false,
  1655  			serverSideDiff:       false,
  1656  		},
  1657  		{
  1658  			testName:             "will return false if requested refresh",
  1659  			noCache:              false,
  1660  			manifestInfos:        manifestInfos("rev1"),
  1661  			sources:              sources(),
  1662  			app:                  app("httpbin", "rev1", true, nil),
  1663  			manifestRevisions:    []string{"rev1"},
  1664  			statusRefreshTimeout: time.Hour * 24,
  1665  			expectedUseCache:     false,
  1666  			serverSideDiff:       false,
  1667  		},
  1668  		{
  1669  			testName:             "will return false if status expired",
  1670  			noCache:              false,
  1671  			manifestInfos:        manifestInfos("rev1"),
  1672  			sources:              sources(),
  1673  			app:                  app("httpbin", "rev1", false, nil),
  1674  			manifestRevisions:    []string{"rev1"},
  1675  			statusRefreshTimeout: time.Minute,
  1676  			expectedUseCache:     false,
  1677  			serverSideDiff:       false,
  1678  		},
  1679  		{
  1680  			testName:             "will return true if status expired and server-side diff",
  1681  			noCache:              false,
  1682  			manifestInfos:        manifestInfos("rev1"),
  1683  			sources:              sources(),
  1684  			app:                  app("httpbin", "rev1", false, nil),
  1685  			manifestRevisions:    []string{"rev1"},
  1686  			statusRefreshTimeout: time.Minute,
  1687  			expectedUseCache:     true,
  1688  			serverSideDiff:       true,
  1689  		},
  1690  		{
  1691  			testName:             "will return false if there is a new revision",
  1692  			noCache:              false,
  1693  			manifestInfos:        manifestInfos("rev1"),
  1694  			sources:              sources(),
  1695  			app:                  app("httpbin", "rev1", false, nil),
  1696  			manifestRevisions:    []string{"rev2"},
  1697  			statusRefreshTimeout: time.Hour * 24,
  1698  			expectedUseCache:     false,
  1699  			serverSideDiff:       false,
  1700  		},
  1701  		{
  1702  			testName:      "will return false if app spec repo changed",
  1703  			noCache:       false,
  1704  			manifestInfos: manifestInfos("rev1"),
  1705  			sources:       sources(),
  1706  			app: app("httpbin", "rev1", false, &v1alpha1.Application{
  1707  				Spec: v1alpha1.ApplicationSpec{
  1708  					Source: &v1alpha1.ApplicationSource{
  1709  						RepoURL: "new-repo",
  1710  					},
  1711  				},
  1712  			}),
  1713  			manifestRevisions:    []string{"rev1"},
  1714  			statusRefreshTimeout: time.Hour * 24,
  1715  			expectedUseCache:     false,
  1716  			serverSideDiff:       false,
  1717  		},
  1718  		{
  1719  			testName:      "will return false if app spec IgnoreDifferences changed",
  1720  			noCache:       false,
  1721  			manifestInfos: manifestInfos("rev1"),
  1722  			sources:       sources(),
  1723  			app: app("httpbin", "rev1", false, &v1alpha1.Application{
  1724  				Spec: v1alpha1.ApplicationSpec{
  1725  					IgnoreDifferences: []v1alpha1.ResourceIgnoreDifferences{
  1726  						{
  1727  							Group:             "app/v1",
  1728  							Kind:              "application",
  1729  							Name:              "httpbin",
  1730  							Namespace:         "httpbin",
  1731  							JQPathExpressions: []string{"."},
  1732  						},
  1733  					},
  1734  				},
  1735  			}),
  1736  			manifestRevisions:    []string{"rev1"},
  1737  			statusRefreshTimeout: time.Hour * 24,
  1738  			expectedUseCache:     false,
  1739  			serverSideDiff:       false,
  1740  		},
  1741  	}
  1742  
  1743  	for _, tc := range cases {
  1744  		t.Run(tc.testName, func(t *testing.T) {
  1745  			// Given
  1746  			t.Parallel()
  1747  			logger, _ := logrustest.NewNullLogger()
  1748  			log := logrus.NewEntry(logger)
  1749  			// When
  1750  			useDiffCache := useDiffCache(tc.noCache, tc.manifestInfos, tc.sources, tc.app, tc.manifestRevisions, tc.statusRefreshTimeout, tc.serverSideDiff, log)
  1751  			// Then
  1752  			assert.Equal(t, tc.expectedUseCache, useDiffCache)
  1753  		})
  1754  	}
  1755  }
  1756  
  1757  func TestCompareAppStateDefaultRevisionUpdated(t *testing.T) {
  1758  	app := newFakeApp()
  1759  	data := fakeData{
  1760  		manifestResponse: &apiclient.ManifestResponse{
  1761  			Manifests: []string{},
  1762  			Namespace: test.FakeDestNamespace,
  1763  			Server:    test.FakeClusterURL,
  1764  			Revision:  "abc123",
  1765  		},
  1766  		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
  1767  	}
  1768  	ctrl := newFakeController(&data, nil)
  1769  	sources := make([]v1alpha1.ApplicationSource, 0)
  1770  	sources = append(sources, app.Spec.GetSource())
  1771  	revisions := make([]string, 0)
  1772  	revisions = append(revisions, "")
  1773  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
  1774  	require.NoError(t, err)
  1775  	assert.NotNil(t, compRes)
  1776  	assert.NotNil(t, compRes.syncStatus)
  1777  	assert.True(t, compRes.revisionsMayHaveChanges)
  1778  }
  1779  
  1780  func TestCompareAppStateRevisionUpdatedWithHelmSource(t *testing.T) {
  1781  	app := newFakeMultiSourceApp()
  1782  	data := fakeData{
  1783  		manifestResponse: &apiclient.ManifestResponse{
  1784  			Manifests: []string{},
  1785  			Namespace: test.FakeDestNamespace,
  1786  			Server:    test.FakeClusterURL,
  1787  			Revision:  "abc123",
  1788  		},
  1789  		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
  1790  	}
  1791  	ctrl := newFakeController(&data, nil)
  1792  	sources := make([]v1alpha1.ApplicationSource, 0)
  1793  	sources = append(sources, app.Spec.GetSource())
  1794  	revisions := make([]string, 0)
  1795  	revisions = append(revisions, "")
  1796  	compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
  1797  	require.NoError(t, err)
  1798  	assert.NotNil(t, compRes)
  1799  	assert.NotNil(t, compRes.syncStatus)
  1800  	assert.True(t, compRes.revisionsMayHaveChanges)
  1801  }
  1802  
  1803  func Test_normalizeClusterScopeTracking(t *testing.T) {
  1804  	obj := kube.MustToUnstructured(&rbacv1.ClusterRole{
  1805  		ObjectMeta: metav1.ObjectMeta{
  1806  			Name:      "test",
  1807  			Namespace: "test",
  1808  		},
  1809  	})
  1810  	c := cachemocks.ClusterCache{}
  1811  	c.On("IsNamespaced", mock.Anything).Return(false, nil)
  1812  	var called bool
  1813  	err := normalizeClusterScopeTracking([]*unstructured.Unstructured{obj}, &c, func(u *unstructured.Unstructured) error {
  1814  		// We expect that the normalization function will call this callback with an obj that has had the namespace set
  1815  		// to empty.
  1816  		called = true
  1817  		assert.Empty(t, u.GetNamespace())
  1818  		return nil
  1819  	})
  1820  	require.NoError(t, err)
  1821  	require.True(t, called, "normalization function should have called the callback function")
  1822  }
  1823  
  1824  func TestCompareAppState_DoesNotCallUpdateRevisionForPaths_ForOCI(t *testing.T) {
  1825  	app := newFakeApp()
  1826  	// Enable the manifest-generate-paths annotation and set a synced revision
  1827  	app.SetAnnotations(map[string]string{v1alpha1.AnnotationKeyManifestGeneratePaths: "."})
  1828  	app.Status.Sync = v1alpha1.SyncStatus{
  1829  		Revision: "abc123",
  1830  		Status:   v1alpha1.SyncStatusCodeSynced,
  1831  	}
  1832  
  1833  	data := fakeData{
  1834  		manifestResponse: &apiclient.ManifestResponse{
  1835  			Manifests: []string{},
  1836  			Namespace: test.FakeDestNamespace,
  1837  			Server:    test.FakeClusterURL,
  1838  			Revision:  "abc123",
  1839  		},
  1840  	}
  1841  	ctrl := newFakeControllerWithResync(&data, time.Minute, nil, errors.New("this should not be called"))
  1842  
  1843  	source := app.Spec.GetSource()
  1844  	source.RepoURL = "oci://example.com/argo/argo-cd"
  1845  	sources := make([]v1alpha1.ApplicationSource, 0)
  1846  	sources = append(sources, source)
  1847  
  1848  	_, _, _, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, sources, "abc123", []string{"123456"}, false, false, false, &defaultProj, false)
  1849  	require.NoError(t, err)
  1850  }