github.com/argoproj/argo-cd/v3@v3.2.1/controller/appcontroller_test.go (about) 1 package controller 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "testing" 9 "time" 10 11 clustercache "github.com/argoproj/gitops-engine/pkg/cache" 12 "github.com/argoproj/gitops-engine/pkg/health" 13 "github.com/argoproj/gitops-engine/pkg/utils/kube/kubetest" 14 "github.com/sirupsen/logrus" 15 "github.com/stretchr/testify/require" 16 "k8s.io/apimachinery/pkg/api/resource" 17 "k8s.io/apimachinery/pkg/labels" 18 "k8s.io/apimachinery/pkg/util/wait" 19 "k8s.io/client-go/rest" 20 "k8s.io/utils/ptr" 21 22 "github.com/argoproj/argo-cd/v3/common" 23 statecache "github.com/argoproj/argo-cd/v3/controller/cache" 24 "github.com/argoproj/argo-cd/v3/controller/sharding" 25 26 "github.com/argoproj/gitops-engine/pkg/cache/mocks" 27 synccommon "github.com/argoproj/gitops-engine/pkg/sync/common" 28 "github.com/argoproj/gitops-engine/pkg/utils/kube" 29 "github.com/stretchr/testify/assert" 30 "github.com/stretchr/testify/mock" 31 appsv1 "k8s.io/api/apps/v1" 32 corev1 "k8s.io/api/core/v1" 33 apierrors "k8s.io/apimachinery/pkg/api/errors" 34 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 35 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 36 "k8s.io/apimachinery/pkg/runtime" 37 "k8s.io/apimachinery/pkg/runtime/schema" 38 "k8s.io/client-go/kubernetes/fake" 39 kubetesting "k8s.io/client-go/testing" 40 "k8s.io/client-go/tools/cache" 41 "sigs.k8s.io/yaml" 42 43 dbmocks "github.com/argoproj/argo-cd/v3/util/db/mocks" 44 45 mockcommitclient "github.com/argoproj/argo-cd/v3/commitserver/apiclient/mocks" 46 mockstatecache "github.com/argoproj/argo-cd/v3/controller/cache/mocks" 47 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 48 appclientset "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned/fake" 49 "github.com/argoproj/argo-cd/v3/reposerver/apiclient" 50 mockrepoclient "github.com/argoproj/argo-cd/v3/reposerver/apiclient/mocks" 51 "github.com/argoproj/argo-cd/v3/test" 52 "github.com/argoproj/argo-cd/v3/util/argo" 53 "github.com/argoproj/argo-cd/v3/util/argo/normalizers" 54 cacheutil "github.com/argoproj/argo-cd/v3/util/cache" 55 appstatecache "github.com/argoproj/argo-cd/v3/util/cache/appstate" 56 "github.com/argoproj/argo-cd/v3/util/settings" 57 utilTest "github.com/argoproj/argo-cd/v3/util/test" 58 ) 59 60 var testEnableEventList []string = argo.DefaultEnableEventList() 61 62 type namespacedResource struct { 63 v1alpha1.ResourceNode 64 AppName string 65 } 66 67 type fakeData struct { 68 apps []runtime.Object 69 manifestResponse *apiclient.ManifestResponse 70 manifestResponses []*apiclient.ManifestResponse 71 managedLiveObjs map[kube.ResourceKey]*unstructured.Unstructured 72 namespacedResources map[kube.ResourceKey]namespacedResource 73 configMapData map[string]string 74 metricsCacheExpiration time.Duration 75 applicationNamespaces []string 76 updateRevisionForPathsResponse *apiclient.UpdateRevisionForPathsResponse 77 additionalObjs []runtime.Object 78 } 79 80 type MockKubectl struct { 81 kube.Kubectl 82 83 DeletedResources []kube.ResourceKey 84 CreatedResources []*unstructured.Unstructured 85 } 86 87 func (m *MockKubectl) CreateResource(ctx context.Context, config *rest.Config, gvk schema.GroupVersionKind, name string, namespace string, obj *unstructured.Unstructured, createOptions metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) { 88 m.CreatedResources = append(m.CreatedResources, obj) 89 return m.Kubectl.CreateResource(ctx, config, gvk, name, namespace, obj, createOptions, subresources...) 90 } 91 92 func (m *MockKubectl) DeleteResource(ctx context.Context, config *rest.Config, gvk schema.GroupVersionKind, name string, namespace string, deleteOptions metav1.DeleteOptions) error { 93 m.DeletedResources = append(m.DeletedResources, kube.NewResourceKey(gvk.Group, gvk.Kind, namespace, name)) 94 return m.Kubectl.DeleteResource(ctx, config, gvk, name, namespace, deleteOptions) 95 } 96 97 func newFakeController(data *fakeData, repoErr error) *ApplicationController { 98 return newFakeControllerWithResync(data, time.Minute, repoErr, nil) 99 } 100 101 func newFakeControllerWithResync(data *fakeData, appResyncPeriod time.Duration, repoErr, revisionPathsErr error) *ApplicationController { 102 var clust corev1.Secret 103 err := yaml.Unmarshal([]byte(fakeCluster), &clust) 104 if err != nil { 105 panic(err) 106 } 107 108 // Mock out call to GenerateManifest 109 mockRepoClient := mockrepoclient.RepoServerServiceClient{} 110 111 if len(data.manifestResponses) > 0 { 112 for _, response := range data.manifestResponses { 113 if repoErr != nil { 114 mockRepoClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(response, repoErr).Once() 115 } else { 116 mockRepoClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(response, nil).Once() 117 } 118 } 119 } else { 120 if repoErr != nil { 121 mockRepoClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(data.manifestResponse, repoErr).Once() 122 } else { 123 mockRepoClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(data.manifestResponse, nil).Once() 124 } 125 } 126 127 if revisionPathsErr != nil { 128 mockRepoClient.On("UpdateRevisionForPaths", mock.Anything, mock.Anything).Return(nil, revisionPathsErr) 129 } else { 130 mockRepoClient.On("UpdateRevisionForPaths", mock.Anything, mock.Anything).Return(data.updateRevisionForPathsResponse, nil) 131 } 132 133 mockRepoClientset := mockrepoclient.Clientset{RepoServerServiceClient: &mockRepoClient} 134 135 mockCommitClientset := mockcommitclient.Clientset{} 136 137 secret := corev1.Secret{ 138 ObjectMeta: metav1.ObjectMeta{ 139 Name: "argocd-secret", 140 Namespace: test.FakeArgoCDNamespace, 141 }, 142 Data: map[string][]byte{ 143 "admin.password": []byte("test"), 144 "server.secretkey": []byte("test"), 145 }, 146 } 147 cm := corev1.ConfigMap{ 148 ObjectMeta: metav1.ObjectMeta{ 149 Name: "argocd-cm", 150 Namespace: test.FakeArgoCDNamespace, 151 Labels: map[string]string{ 152 "app.kubernetes.io/part-of": "argocd", 153 }, 154 }, 155 Data: data.configMapData, 156 } 157 runtimeObjs := []runtime.Object{&clust, &secret, &cm} 158 runtimeObjs = append(runtimeObjs, data.additionalObjs...) 159 kubeClient := fake.NewClientset(runtimeObjs...) 160 settingsMgr := settings.NewSettingsManager(context.Background(), kubeClient, test.FakeArgoCDNamespace) 161 kubectl := &MockKubectl{Kubectl: &kubetest.MockKubectlCmd{}} 162 ctrl, err := NewApplicationController( 163 test.FakeArgoCDNamespace, 164 settingsMgr, 165 kubeClient, 166 appclientset.NewSimpleClientset(data.apps...), 167 &mockRepoClientset, 168 &mockCommitClientset, 169 appstatecache.NewCache( 170 cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Minute)), 171 1*time.Minute, 172 ), 173 kubectl, 174 appResyncPeriod, 175 time.Hour, 176 time.Second, 177 time.Minute, 178 nil, 179 time.Minute, 180 0, 181 time.Second*10, 182 common.DefaultPortArgoCDMetrics, 183 data.metricsCacheExpiration, 184 []string{}, 185 []string{}, 186 []string{}, 187 0, 188 true, 189 nil, 190 data.applicationNamespaces, 191 nil, 192 false, 193 false, 194 normalizers.IgnoreNormalizerOpts{}, 195 testEnableEventList, 196 false, 197 ) 198 db := &dbmocks.ArgoDB{} 199 db.On("GetApplicationControllerReplicas").Return(1) 200 // Setting a default sharding algorithm for the tests where we cannot set it. 201 ctrl.clusterSharding = sharding.NewClusterSharding(db, 0, 1, common.DefaultShardingAlgorithm) 202 if err != nil { 203 panic(err) 204 } 205 cancelProj := test.StartInformer(ctrl.projInformer) 206 defer cancelProj() 207 cancelApp := test.StartInformer(ctrl.appInformer) 208 defer cancelApp() 209 clusterCacheMock := mocks.ClusterCache{} 210 clusterCacheMock.On("IsNamespaced", mock.Anything).Return(true, nil) 211 clusterCacheMock.On("GetOpenAPISchema").Return(nil, nil) 212 clusterCacheMock.On("GetGVKParser").Return(nil) 213 214 mockStateCache := mockstatecache.LiveStateCache{} 215 ctrl.appStateManager.(*appStateManager).liveStateCache = &mockStateCache 216 ctrl.stateCache = &mockStateCache 217 mockStateCache.On("IsNamespaced", mock.Anything, mock.Anything).Return(true, nil) 218 mockStateCache.On("GetManagedLiveObjs", mock.Anything, mock.Anything, mock.Anything).Return(data.managedLiveObjs, nil) 219 mockStateCache.On("GetVersionsInfo", mock.Anything).Return("v1.2.3", nil, nil) 220 response := make(map[kube.ResourceKey]v1alpha1.ResourceNode) 221 for k, v := range data.namespacedResources { 222 response[k] = v.ResourceNode 223 } 224 mockStateCache.On("GetNamespaceTopLevelResources", mock.Anything, mock.Anything).Return(response, nil) 225 mockStateCache.On("IterateResources", mock.Anything, mock.Anything).Return(nil) 226 mockStateCache.On("GetClusterCache", mock.Anything).Return(&clusterCacheMock, nil) 227 mockStateCache.On("IterateHierarchyV2", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 228 keys := args[1].([]kube.ResourceKey) 229 action := args[2].(func(child v1alpha1.ResourceNode, appName string) bool) 230 for _, key := range keys { 231 appName := "" 232 if res, ok := data.namespacedResources[key]; ok { 233 appName = res.AppName 234 } 235 _ = action(v1alpha1.ResourceNode{ResourceRef: v1alpha1.ResourceRef{Kind: key.Kind, Group: key.Group, Namespace: key.Namespace, Name: key.Name}}, appName) 236 } 237 }).Return(nil) 238 return ctrl 239 } 240 241 var fakeCluster = ` 242 apiVersion: v1 243 data: 244 # {"bearerToken":"fake","tlsClientConfig":{"insecure":true},"awsAuthConfig":null} 245 config: eyJiZWFyZXJUb2tlbiI6ImZha2UiLCJ0bHNDbGllbnRDb25maWciOnsiaW5zZWN1cmUiOnRydWV9LCJhd3NBdXRoQ29uZmlnIjpudWxsfQ== 246 # minikube 247 name: bWluaWt1YmU= 248 # https://localhost:6443 249 server: aHR0cHM6Ly9sb2NhbGhvc3Q6NjQ0Mw== 250 kind: Secret 251 metadata: 252 labels: 253 argocd.argoproj.io/secret-type: cluster 254 name: some-secret 255 namespace: ` + test.FakeArgoCDNamespace + ` 256 type: Opaque 257 ` 258 259 var fakeApp = ` 260 apiVersion: argoproj.io/v1alpha1 261 kind: Application 262 metadata: 263 uid: "123" 264 name: my-app 265 namespace: ` + test.FakeArgoCDNamespace + ` 266 spec: 267 destination: 268 namespace: ` + test.FakeDestNamespace + ` 269 server: https://localhost:6443 270 project: default 271 source: 272 path: some/path 273 repoURL: https://github.com/argoproj/argocd-example-apps.git 274 syncPolicy: 275 automated: {} 276 status: 277 operationState: 278 finishedAt: 2018-09-21T23:50:29Z 279 message: successfully synced 280 operation: 281 sync: 282 revision: HEAD 283 phase: Succeeded 284 startedAt: 2018-09-21T23:50:25Z 285 syncResult: 286 resources: 287 - kind: RoleBinding 288 message: |- 289 rolebinding.rbac.authorization.k8s.io/always-outofsync reconciled 290 rolebinding.rbac.authorization.k8s.io/always-outofsync configured 291 name: always-outofsync 292 namespace: default 293 status: Synced 294 revision: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 295 source: 296 path: some/path 297 repoURL: https://github.com/argoproj/argocd-example-apps.git 298 ` 299 300 var fakeMultiSourceApp = ` 301 apiVersion: argoproj.io/v1alpha1 302 kind: Application 303 metadata: 304 uid: "123" 305 name: my-app 306 namespace: ` + test.FakeArgoCDNamespace + ` 307 spec: 308 destination: 309 namespace: ` + test.FakeDestNamespace + ` 310 server: https://localhost:6443 311 project: default 312 sources: 313 - path: some/path 314 helm: 315 valueFiles: 316 - $values_test/values.yaml 317 repoURL: https://github.com/argoproj/argocd-example-apps.git 318 - path: some/other/path 319 repoURL: https://github.com/argoproj/argocd-example-apps-fake.git 320 - ref: values_test 321 repoURL: https://github.com/argoproj/argocd-example-apps-fake-ref.git 322 syncPolicy: 323 automated: {} 324 status: 325 operationState: 326 finishedAt: 2018-09-21T23:50:29Z 327 message: successfully synced 328 operation: 329 sync: 330 revisions: 331 - HEAD 332 - HEAD 333 - HEAD 334 phase: Succeeded 335 startedAt: 2018-09-21T23:50:25Z 336 syncResult: 337 resources: 338 - kind: RoleBinding 339 message: |- 340 rolebinding.rbac.authorization.k8s.io/always-outofsync reconciled 341 rolebinding.rbac.authorization.k8s.io/always-outofsync configured 342 name: always-outofsync 343 namespace: default 344 status: Synced 345 revisions: 346 - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 347 - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 348 - cccccccccccccccccccccccccccccccccccccccc 349 sources: 350 - path: some/path 351 helm: 352 valueFiles: 353 - $values_test/values.yaml 354 repoURL: https://github.com/argoproj/argocd-example-apps.git 355 - path: some/other/path 356 repoURL: https://github.com/argoproj/argocd-example-apps-fake.git 357 - ref: values_test 358 repoURL: https://github.com/argoproj/argocd-example-apps-fake-ref.git 359 ` 360 361 var fakeAppWithDestName = ` 362 apiVersion: argoproj.io/v1alpha1 363 kind: Application 364 metadata: 365 uid: "123" 366 name: my-app 367 namespace: ` + test.FakeArgoCDNamespace + ` 368 spec: 369 destination: 370 namespace: ` + test.FakeDestNamespace + ` 371 name: minikube 372 project: default 373 source: 374 path: some/path 375 repoURL: https://github.com/argoproj/argocd-example-apps.git 376 syncPolicy: 377 automated: {} 378 ` 379 380 var fakeAppWithDestMismatch = ` 381 apiVersion: argoproj.io/v1alpha1 382 kind: Application 383 metadata: 384 uid: "123" 385 name: my-app 386 namespace: ` + test.FakeArgoCDNamespace + ` 387 spec: 388 destination: 389 namespace: ` + test.FakeDestNamespace + ` 390 name: another-cluster 391 server: https://localhost:6443 392 project: default 393 source: 394 path: some/path 395 repoURL: https://github.com/argoproj/argocd-example-apps.git 396 syncPolicy: 397 automated: {} 398 ` 399 400 var fakeStrayResource = ` 401 apiVersion: v1 402 kind: ConfigMap 403 metadata: 404 name: test-cm 405 namespace: invalid 406 labels: 407 app.kubernetes.io/instance: my-app 408 data: 409 ` 410 411 var fakePostDeleteHook = ` 412 { 413 "apiVersion": "batch/v1", 414 "kind": "Job", 415 "metadata": { 416 "name": "post-delete-hook", 417 "namespace": "default", 418 "labels": { 419 "app.kubernetes.io/instance": "my-app" 420 }, 421 "annotations": { 422 "argocd.argoproj.io/hook": "PostDelete", 423 "argocd.argoproj.io/hook-delete-policy": "HookSucceeded" 424 } 425 }, 426 "spec": { 427 "template": { 428 "metadata": { 429 "name": "post-delete-hook" 430 }, 431 "spec": { 432 "containers": [ 433 { 434 "name": "post-delete-hook", 435 "image": "busybox", 436 "command": [ 437 "/bin/sh", 438 "-c", 439 "sleep 5 && echo hello from the post-delete-hook job" 440 ] 441 } 442 ], 443 "restartPolicy": "Never" 444 } 445 } 446 } 447 } 448 ` 449 450 var fakeServiceAccount = ` 451 { 452 "apiVersion": "v1", 453 "kind": "ServiceAccount", 454 "metadata": { 455 "name": "hook-serviceaccount", 456 "namespace": "default", 457 "annotations": { 458 "argocd.argoproj.io/hook": "PostDelete", 459 "argocd.argoproj.io/hook-delete-policy": "BeforeHookCreation,HookSucceeded" 460 } 461 } 462 } 463 ` 464 465 var fakeRole = ` 466 { 467 "apiVersion": "rbac.authorization.k8s.io/v1", 468 "kind": "Role", 469 "metadata": { 470 "name": "hook-role", 471 "namespace": "default", 472 "annotations": { 473 "argocd.argoproj.io/hook": "PostDelete", 474 "argocd.argoproj.io/hook-delete-policy": "BeforeHookCreation,HookSucceeded" 475 } 476 }, 477 "rules": [ 478 { 479 "apiGroups": [""], 480 "resources": ["secrets"], 481 "verbs": ["get", "delete", "list"] 482 } 483 ] 484 } 485 ` 486 487 var fakeRoleBinding = ` 488 { 489 "apiVersion": "rbac.authorization.k8s.io/v1", 490 "kind": "RoleBinding", 491 "metadata": { 492 "name": "hook-rolebinding", 493 "namespace": "default", 494 "annotations": { 495 "argocd.argoproj.io/hook": "PostDelete", 496 "argocd.argoproj.io/hook-delete-policy": "BeforeHookCreation,HookSucceeded" 497 } 498 }, 499 "roleRef": { 500 "apiGroup": "rbac.authorization.k8s.io", 501 "kind": "Role", 502 "name": "hook-role" 503 }, 504 "subjects": [ 505 { 506 "kind": "ServiceAccount", 507 "name": "hook-serviceaccount", 508 "namespace": "default" 509 } 510 ] 511 } 512 ` 513 514 func newFakeApp() *v1alpha1.Application { 515 return createFakeApp(fakeApp) 516 } 517 518 func newFakeAppWithHealthAndTime(status health.HealthStatusCode, timestamp metav1.Time) *v1alpha1.Application { 519 return createFakeAppWithHealthAndTime(fakeApp, status, timestamp) 520 } 521 522 func newFakeMultiSourceApp() *v1alpha1.Application { 523 return createFakeApp(fakeMultiSourceApp) 524 } 525 526 func createFakeAppWithHealthAndTime(testApp string, status health.HealthStatusCode, timestamp metav1.Time) *v1alpha1.Application { 527 app := createFakeApp(testApp) 528 app.Status.Health = v1alpha1.AppHealthStatus{ 529 Status: status, 530 LastTransitionTime: ×tamp, 531 } 532 return app 533 } 534 535 func newFakeAppWithDestMismatch() *v1alpha1.Application { 536 return createFakeApp(fakeAppWithDestMismatch) 537 } 538 539 func newFakeAppWithDestName() *v1alpha1.Application { 540 return createFakeApp(fakeAppWithDestName) 541 } 542 543 func createFakeApp(testApp string) *v1alpha1.Application { 544 var app v1alpha1.Application 545 err := yaml.Unmarshal([]byte(testApp), &app) 546 if err != nil { 547 panic(err) 548 } 549 return &app 550 } 551 552 func newFakeCM() map[string]any { 553 var cm map[string]any 554 err := yaml.Unmarshal([]byte(fakeStrayResource), &cm) 555 if err != nil { 556 panic(err) 557 } 558 return cm 559 } 560 561 func newFakePostDeleteHook() map[string]any { 562 var hook map[string]any 563 err := yaml.Unmarshal([]byte(fakePostDeleteHook), &hook) 564 if err != nil { 565 panic(err) 566 } 567 return hook 568 } 569 570 func newFakeRoleBinding() map[string]any { 571 var roleBinding map[string]any 572 err := yaml.Unmarshal([]byte(fakeRoleBinding), &roleBinding) 573 if err != nil { 574 panic(err) 575 } 576 return roleBinding 577 } 578 579 func newFakeRole() map[string]any { 580 var role map[string]any 581 err := yaml.Unmarshal([]byte(fakeRole), &role) 582 if err != nil { 583 panic(err) 584 } 585 return role 586 } 587 588 func newFakeServiceAccount() map[string]any { 589 var serviceAccount map[string]any 590 err := yaml.Unmarshal([]byte(fakeServiceAccount), &serviceAccount) 591 if err != nil { 592 panic(err) 593 } 594 return serviceAccount 595 } 596 597 func TestAutoSync(t *testing.T) { 598 app := newFakeApp() 599 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 600 syncStatus := v1alpha1.SyncStatus{ 601 Status: v1alpha1.SyncStatusCodeOutOfSync, 602 Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 603 } 604 cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true) 605 assert.Nil(t, cond) 606 app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{}) 607 require.NoError(t, err) 608 assert.NotNil(t, app.Operation) 609 assert.NotNil(t, app.Operation.Sync) 610 assert.False(t, app.Operation.Sync.Prune) 611 } 612 613 func TestAutoSyncEnabledSetToTrue(t *testing.T) { 614 app := newFakeApp() 615 enable := true 616 app.Spec.SyncPolicy.Automated = &v1alpha1.SyncPolicyAutomated{Enabled: &enable} 617 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 618 syncStatus := v1alpha1.SyncStatus{ 619 Status: v1alpha1.SyncStatusCodeOutOfSync, 620 Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 621 } 622 cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true) 623 assert.Nil(t, cond) 624 app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{}) 625 require.NoError(t, err) 626 assert.NotNil(t, app.Operation) 627 assert.NotNil(t, app.Operation.Sync) 628 assert.False(t, app.Operation.Sync.Prune) 629 } 630 631 func TestAutoSyncMultiSourceWithoutSelfHeal(t *testing.T) { 632 // Simulate OutOfSync caused by object change in cluster 633 // So our Sync Revisions and SyncStatus Revisions should deep equal 634 t.Run("ClusterObjectChangeShouldNotTriggerAutoSync", func(t *testing.T) { 635 app := newFakeMultiSourceApp() 636 app.Spec.SyncPolicy.Automated.SelfHeal = false 637 app.Status.OperationState.SyncResult.Revisions = []string{"z", "x", "v"} 638 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 639 syncStatus := v1alpha1.SyncStatus{ 640 Status: v1alpha1.SyncStatusCodeOutOfSync, 641 Revisions: []string{"z", "x", "v"}, 642 } 643 cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook-1", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true) 644 assert.Nil(t, cond) 645 app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{}) 646 require.NoError(t, err) 647 assert.Nil(t, app.Operation) 648 }) 649 t.Run("NewRevisionChangeShouldTriggerAutoSync", func(t *testing.T) { 650 app := newFakeMultiSourceApp() 651 app.Spec.SyncPolicy.Automated.SelfHeal = false 652 app.Status.OperationState.SyncResult.Revisions = []string{"z", "x", "v"} 653 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 654 syncStatus := v1alpha1.SyncStatus{ 655 Status: v1alpha1.SyncStatusCodeOutOfSync, 656 Revisions: []string{"a", "b", "c"}, 657 } 658 cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook-1", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true) 659 assert.Nil(t, cond) 660 app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{}) 661 require.NoError(t, err) 662 assert.NotNil(t, app.Operation) 663 }) 664 } 665 666 func TestAutoSyncNotAllowEmpty(t *testing.T) { 667 app := newFakeApp() 668 app.Spec.SyncPolicy.Automated.Prune = true 669 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 670 syncStatus := v1alpha1.SyncStatus{ 671 Status: v1alpha1.SyncStatusCodeOutOfSync, 672 Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 673 } 674 cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true) 675 assert.NotNil(t, cond) 676 } 677 678 func TestAutoSyncAllowEmpty(t *testing.T) { 679 app := newFakeApp() 680 app.Spec.SyncPolicy.Automated.Prune = true 681 app.Spec.SyncPolicy.Automated.AllowEmpty = true 682 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 683 syncStatus := v1alpha1.SyncStatus{ 684 Status: v1alpha1.SyncStatusCodeOutOfSync, 685 Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 686 } 687 cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true) 688 assert.Nil(t, cond) 689 } 690 691 func TestSkipAutoSync(t *testing.T) { 692 // Verify we skip when we previously synced to it in our most recent history 693 // Set current to 'aaaaa', desired to 'aaaa' and mark system OutOfSync 694 t.Run("PreviouslySyncedToRevision", func(t *testing.T) { 695 app := newFakeApp() 696 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 697 syncStatus := v1alpha1.SyncStatus{ 698 Status: v1alpha1.SyncStatusCodeOutOfSync, 699 Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 700 } 701 cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true) 702 assert.Nil(t, cond) 703 app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{}) 704 require.NoError(t, err) 705 assert.Nil(t, app.Operation) 706 }) 707 708 // Verify we skip when we are already Synced (even if revision is different) 709 t.Run("AlreadyInSyncedState", func(t *testing.T) { 710 app := newFakeApp() 711 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 712 syncStatus := v1alpha1.SyncStatus{ 713 Status: v1alpha1.SyncStatusCodeSynced, 714 Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 715 } 716 cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true) 717 assert.Nil(t, cond) 718 app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{}) 719 require.NoError(t, err) 720 assert.Nil(t, app.Operation) 721 }) 722 723 // Verify we skip when auto-sync is disabled 724 t.Run("AutoSyncIsDisabled", func(t *testing.T) { 725 app := newFakeApp() 726 app.Spec.SyncPolicy = nil 727 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 728 syncStatus := v1alpha1.SyncStatus{ 729 Status: v1alpha1.SyncStatusCodeOutOfSync, 730 Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 731 } 732 cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true) 733 assert.Nil(t, cond) 734 app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{}) 735 require.NoError(t, err) 736 assert.Nil(t, app.Operation) 737 }) 738 739 // Verify we skip when auto-sync is disabled 740 t.Run("AutoSyncEnableFieldIsSetFalse", func(t *testing.T) { 741 app := newFakeApp() 742 enable := false 743 app.Spec.SyncPolicy.Automated = &v1alpha1.SyncPolicyAutomated{Enabled: &enable} 744 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 745 syncStatus := v1alpha1.SyncStatus{ 746 Status: v1alpha1.SyncStatusCodeOutOfSync, 747 Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 748 } 749 cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true) 750 assert.Nil(t, cond) 751 app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{}) 752 require.NoError(t, err) 753 assert.Nil(t, app.Operation) 754 }) 755 756 // Verify we skip when application is marked for deletion 757 t.Run("ApplicationIsMarkedForDeletion", func(t *testing.T) { 758 app := newFakeApp() 759 now := metav1.Now() 760 app.DeletionTimestamp = &now 761 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 762 syncStatus := v1alpha1.SyncStatus{ 763 Status: v1alpha1.SyncStatusCodeOutOfSync, 764 Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 765 } 766 cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true) 767 assert.Nil(t, cond) 768 app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{}) 769 require.NoError(t, err) 770 assert.Nil(t, app.Operation) 771 }) 772 773 // Verify we skip when previous sync attempt failed and return error condition 774 // Set current to 'aaaaa', desired to 'bbbbb' and add 'bbbbb' to failure history 775 t.Run("PreviousSyncAttemptFailed", func(t *testing.T) { 776 app := newFakeApp() 777 app.Status.OperationState = &v1alpha1.OperationState{ 778 Operation: v1alpha1.Operation{ 779 Sync: &v1alpha1.SyncOperation{}, 780 }, 781 Phase: synccommon.OperationFailed, 782 SyncResult: &v1alpha1.SyncOperationResult{ 783 Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 784 Source: *app.Spec.Source.DeepCopy(), 785 }, 786 } 787 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 788 syncStatus := v1alpha1.SyncStatus{ 789 Status: v1alpha1.SyncStatusCodeOutOfSync, 790 Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 791 } 792 cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true) 793 assert.NotNil(t, cond) 794 app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{}) 795 require.NoError(t, err) 796 assert.Nil(t, app.Operation) 797 }) 798 799 t.Run("PreviousSyncAttemptError", func(t *testing.T) { 800 app := newFakeApp() 801 app.Status.OperationState = &v1alpha1.OperationState{ 802 Operation: v1alpha1.Operation{ 803 Sync: &v1alpha1.SyncOperation{}, 804 }, 805 Phase: synccommon.OperationError, 806 SyncResult: &v1alpha1.SyncOperationResult{ 807 Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 808 Source: *app.Spec.Source.DeepCopy(), 809 }, 810 } 811 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 812 syncStatus := v1alpha1.SyncStatus{ 813 Status: v1alpha1.SyncStatusCodeOutOfSync, 814 Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 815 } 816 cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true) 817 assert.NotNil(t, cond) 818 app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{}) 819 require.NoError(t, err) 820 assert.Nil(t, app.Operation) 821 }) 822 823 t.Run("NeedsToPruneResourcesOnlyButAutomatedPruneDisabled", func(t *testing.T) { 824 app := newFakeApp() 825 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 826 syncStatus := v1alpha1.SyncStatus{ 827 Status: v1alpha1.SyncStatusCodeOutOfSync, 828 Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 829 } 830 cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{ 831 {Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync, RequiresPruning: true}, 832 }, true) 833 assert.Nil(t, cond) 834 app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{}) 835 require.NoError(t, err) 836 assert.Nil(t, app.Operation) 837 }) 838 } 839 840 // TestAutoSyncIndicateError verifies we skip auto-sync and return error condition if previous sync failed 841 func TestAutoSyncIndicateError(t *testing.T) { 842 app := newFakeApp() 843 app.Spec.Source.Helm = &v1alpha1.ApplicationSourceHelm{ 844 Parameters: []v1alpha1.HelmParameter{ 845 { 846 Name: "a", 847 Value: "1", 848 }, 849 }, 850 } 851 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 852 syncStatus := v1alpha1.SyncStatus{ 853 Status: v1alpha1.SyncStatusCodeOutOfSync, 854 Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 855 } 856 app.Status.OperationState = &v1alpha1.OperationState{ 857 Operation: v1alpha1.Operation{ 858 Sync: &v1alpha1.SyncOperation{ 859 Source: app.Spec.Source.DeepCopy(), 860 }, 861 }, 862 Phase: synccommon.OperationFailed, 863 SyncResult: &v1alpha1.SyncOperationResult{ 864 Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 865 Source: *app.Spec.Source.DeepCopy(), 866 }, 867 } 868 cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true) 869 assert.NotNil(t, cond) 870 app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{}) 871 require.NoError(t, err) 872 assert.Nil(t, app.Operation) 873 } 874 875 // TestAutoSyncParameterOverrides verifies we auto-sync if revision is same but parameter overrides are different 876 func TestAutoSyncParameterOverrides(t *testing.T) { 877 t.Run("Single source", func(t *testing.T) { 878 app := newFakeApp() 879 app.Spec.Source.Helm = &v1alpha1.ApplicationSourceHelm{ 880 Parameters: []v1alpha1.HelmParameter{ 881 { 882 Name: "a", 883 Value: "1", 884 }, 885 }, 886 } 887 app.Status.OperationState = &v1alpha1.OperationState{ 888 Operation: v1alpha1.Operation{ 889 Sync: &v1alpha1.SyncOperation{ 890 Source: &v1alpha1.ApplicationSource{ 891 Helm: &v1alpha1.ApplicationSourceHelm{ 892 Parameters: []v1alpha1.HelmParameter{ 893 { 894 Name: "a", 895 Value: "2", // this value changed 896 }, 897 }, 898 }, 899 }, 900 }, 901 }, 902 Phase: synccommon.OperationFailed, 903 SyncResult: &v1alpha1.SyncOperationResult{ 904 Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 905 }, 906 } 907 syncStatus := v1alpha1.SyncStatus{ 908 Status: v1alpha1.SyncStatusCodeOutOfSync, 909 Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 910 } 911 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 912 cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true) 913 assert.Nil(t, cond) 914 app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{}) 915 require.NoError(t, err) 916 assert.NotNil(t, app.Operation) 917 }) 918 919 t.Run("Multi sources", func(t *testing.T) { 920 app := newFakeMultiSourceApp() 921 app.Spec.Sources[0].Helm = &v1alpha1.ApplicationSourceHelm{ 922 Parameters: []v1alpha1.HelmParameter{ 923 { 924 Name: "a", 925 Value: "1", 926 }, 927 }, 928 } 929 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 930 app.Status.OperationState.SyncResult.Revisions = []string{"z", "x", "v"} 931 app.Status.OperationState.SyncResult.Sources[0].Helm = &v1alpha1.ApplicationSourceHelm{ 932 Parameters: []v1alpha1.HelmParameter{ 933 { 934 Name: "a", 935 Value: "2", // this value changed 936 }, 937 }, 938 } 939 syncStatus := v1alpha1.SyncStatus{ 940 Status: v1alpha1.SyncStatusCodeOutOfSync, 941 Revisions: []string{"z", "x", "v"}, 942 } 943 cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true) 944 assert.Nil(t, cond) 945 app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{}) 946 require.NoError(t, err) 947 assert.NotNil(t, app.Operation) 948 }) 949 } 950 951 // TestFinalizeAppDeletion verifies application deletion 952 func TestFinalizeAppDeletion(t *testing.T) { 953 now := metav1.Now() 954 defaultProj := v1alpha1.AppProject{ 955 ObjectMeta: metav1.ObjectMeta{ 956 Name: "default", 957 Namespace: test.FakeArgoCDNamespace, 958 }, 959 Spec: v1alpha1.AppProjectSpec{ 960 SourceRepos: []string{"*"}, 961 Destinations: []v1alpha1.ApplicationDestination{ 962 { 963 Server: "*", 964 Namespace: "*", 965 }, 966 }, 967 }, 968 } 969 970 // Ensure app can be deleted cascading 971 t.Run("CascadingDelete", func(t *testing.T) { 972 app := newFakeApp() 973 app.SetCascadedDeletion(v1alpha1.ResourcesFinalizerName) 974 app.DeletionTimestamp = &now 975 app.Spec.Destination.Namespace = test.FakeArgoCDNamespace 976 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}, managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{}}, nil) 977 patched := false 978 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 979 defaultReactor := fakeAppCs.ReactionChain[0] 980 fakeAppCs.ReactionChain = nil 981 fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 982 return defaultReactor.React(action) 983 }) 984 fakeAppCs.AddReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) { 985 patched = true 986 return true, &v1alpha1.Application{}, nil 987 }) 988 err := ctrl.finalizeApplicationDeletion(app, func(_ string) ([]*v1alpha1.Cluster, error) { 989 return []*v1alpha1.Cluster{}, nil 990 }) 991 require.NoError(t, err) 992 assert.True(t, patched) 993 }) 994 995 // Ensure any stray resources irregularly labeled with instance label of app are not deleted upon deleting, 996 // when app project restriction is in place 997 t.Run("ProjectRestrictionEnforced", func(t *testing.T) { 998 restrictedProj := v1alpha1.AppProject{ 999 ObjectMeta: metav1.ObjectMeta{ 1000 Name: "restricted", 1001 Namespace: test.FakeArgoCDNamespace, 1002 }, 1003 Spec: v1alpha1.AppProjectSpec{ 1004 SourceRepos: []string{"*"}, 1005 Destinations: []v1alpha1.ApplicationDestination{ 1006 { 1007 Server: "*", 1008 Namespace: "my-app", 1009 }, 1010 }, 1011 }, 1012 } 1013 app := newFakeApp() 1014 app.SetCascadedDeletion(v1alpha1.ResourcesFinalizerName) 1015 app.DeletionTimestamp = &now 1016 app.Spec.Destination.Namespace = test.FakeArgoCDNamespace 1017 app.Spec.Project = "restricted" 1018 appObj := kube.MustToUnstructured(&app) 1019 cm := newFakeCM() 1020 strayObj := kube.MustToUnstructured(&cm) 1021 ctrl := newFakeController(&fakeData{ 1022 apps: []runtime.Object{app, &defaultProj, &restrictedProj}, 1023 managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{ 1024 kube.GetResourceKey(appObj): appObj, 1025 kube.GetResourceKey(strayObj): strayObj, 1026 }, 1027 }, nil) 1028 1029 patched := false 1030 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 1031 defaultReactor := fakeAppCs.ReactionChain[0] 1032 fakeAppCs.ReactionChain = nil 1033 fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1034 return defaultReactor.React(action) 1035 }) 1036 fakeAppCs.AddReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1037 patched = true 1038 return true, &v1alpha1.Application{}, nil 1039 }) 1040 err := ctrl.finalizeApplicationDeletion(app, func(_ string) ([]*v1alpha1.Cluster, error) { 1041 return []*v1alpha1.Cluster{}, nil 1042 }) 1043 require.NoError(t, err) 1044 assert.True(t, patched) 1045 objsMap, err := ctrl.stateCache.GetManagedLiveObjs(&v1alpha1.Cluster{Server: "test", Name: "test"}, app, []*unstructured.Unstructured{}) 1046 if err != nil { 1047 require.NoError(t, err) 1048 } 1049 // Managed objects must be empty 1050 assert.Empty(t, objsMap) 1051 1052 // Loop through all deleted objects, ensure that test-cm is none of them 1053 for _, o := range ctrl.kubectl.(*MockKubectl).DeletedResources { 1054 assert.NotEqual(t, "test-cm", o.Name) 1055 } 1056 }) 1057 1058 t.Run("DeleteWithDestinationClusterName", func(t *testing.T) { 1059 app := newFakeAppWithDestName() 1060 app.SetCascadedDeletion(v1alpha1.ResourcesFinalizerName) 1061 app.DeletionTimestamp = &now 1062 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}, managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{}}, nil) 1063 patched := false 1064 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 1065 defaultReactor := fakeAppCs.ReactionChain[0] 1066 fakeAppCs.ReactionChain = nil 1067 fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1068 return defaultReactor.React(action) 1069 }) 1070 fakeAppCs.AddReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1071 patched = true 1072 return true, &v1alpha1.Application{}, nil 1073 }) 1074 err := ctrl.finalizeApplicationDeletion(app, func(_ string) ([]*v1alpha1.Cluster, error) { 1075 return []*v1alpha1.Cluster{}, nil 1076 }) 1077 require.NoError(t, err) 1078 assert.True(t, patched) 1079 }) 1080 1081 // Create an Application with a cluster that doesn't exist 1082 // Ensure it can be deleted. 1083 t.Run("DeleteWithInvalidClusterName", func(t *testing.T) { 1084 appTemplate := newFakeAppWithDestName() 1085 1086 testShouldDelete := func(app *v1alpha1.Application) { 1087 appObj := kube.MustToUnstructured(&app) 1088 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}, managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{ 1089 kube.GetResourceKey(appObj): appObj, 1090 }}, nil) 1091 1092 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 1093 defaultReactor := fakeAppCs.ReactionChain[0] 1094 fakeAppCs.ReactionChain = nil 1095 fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1096 return defaultReactor.React(action) 1097 }) 1098 err := ctrl.finalizeApplicationDeletion(app, func(_ string) ([]*v1alpha1.Cluster, error) { 1099 return []*v1alpha1.Cluster{}, nil 1100 }) 1101 require.NoError(t, err) 1102 } 1103 1104 app1 := appTemplate.DeepCopy() 1105 app1.Spec.Destination.Server = "https://invalid" 1106 testShouldDelete(app1) 1107 1108 app2 := appTemplate.DeepCopy() 1109 app2.Spec.Destination.Name = "invalid" 1110 testShouldDelete(app2) 1111 1112 app3 := appTemplate.DeepCopy() 1113 app3.Spec.Destination.Name = "invalid" 1114 app3.Spec.Destination.Server = "https://invalid" 1115 testShouldDelete(app3) 1116 }) 1117 1118 t.Run("PostDelete_HookIsCreated", func(t *testing.T) { 1119 app := newFakeApp() 1120 app.SetPostDeleteFinalizer() 1121 app.Spec.Destination.Namespace = test.FakeArgoCDNamespace 1122 ctrl := newFakeController(&fakeData{ 1123 manifestResponses: []*apiclient.ManifestResponse{{ 1124 Manifests: []string{fakePostDeleteHook}, 1125 }}, 1126 apps: []runtime.Object{app, &defaultProj}, 1127 managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{}, 1128 }, nil) 1129 1130 patched := false 1131 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 1132 defaultReactor := fakeAppCs.ReactionChain[0] 1133 fakeAppCs.ReactionChain = nil 1134 fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1135 return defaultReactor.React(action) 1136 }) 1137 fakeAppCs.AddReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1138 patched = true 1139 return true, &v1alpha1.Application{}, nil 1140 }) 1141 err := ctrl.finalizeApplicationDeletion(app, func(_ string) ([]*v1alpha1.Cluster, error) { 1142 return []*v1alpha1.Cluster{}, nil 1143 }) 1144 require.NoError(t, err) 1145 // finalizer is not deleted 1146 assert.False(t, patched) 1147 // post-delete hook is created 1148 require.Len(t, ctrl.kubectl.(*MockKubectl).CreatedResources, 1) 1149 require.Equal(t, "post-delete-hook", ctrl.kubectl.(*MockKubectl).CreatedResources[0].GetName()) 1150 }) 1151 1152 t.Run("PostDelete_HookIsExecuted", func(t *testing.T) { 1153 app := newFakeApp() 1154 app.SetPostDeleteFinalizer() 1155 app.Spec.Destination.Namespace = test.FakeArgoCDNamespace 1156 liveHook := &unstructured.Unstructured{Object: newFakePostDeleteHook()} 1157 conditions := []any{ 1158 map[string]any{ 1159 "type": "Complete", 1160 "status": "True", 1161 }, 1162 } 1163 require.NoError(t, unstructured.SetNestedField(liveHook.Object, conditions, "status", "conditions")) 1164 ctrl := newFakeController(&fakeData{ 1165 manifestResponses: []*apiclient.ManifestResponse{{ 1166 Manifests: []string{fakePostDeleteHook}, 1167 }}, 1168 apps: []runtime.Object{app, &defaultProj}, 1169 managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{ 1170 kube.GetResourceKey(liveHook): liveHook, 1171 }, 1172 }, nil) 1173 1174 patched := false 1175 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 1176 defaultReactor := fakeAppCs.ReactionChain[0] 1177 fakeAppCs.ReactionChain = nil 1178 fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1179 return defaultReactor.React(action) 1180 }) 1181 fakeAppCs.AddReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1182 patched = true 1183 return true, &v1alpha1.Application{}, nil 1184 }) 1185 err := ctrl.finalizeApplicationDeletion(app, func(_ string) ([]*v1alpha1.Cluster, error) { 1186 return []*v1alpha1.Cluster{}, nil 1187 }) 1188 require.NoError(t, err) 1189 // finalizer is removed 1190 assert.True(t, patched) 1191 }) 1192 1193 t.Run("PostDelete_HookIsDeleted", func(t *testing.T) { 1194 app := newFakeApp() 1195 app.SetPostDeleteFinalizer("cleanup") 1196 app.Spec.Destination.Namespace = test.FakeArgoCDNamespace 1197 liveRoleBinding := &unstructured.Unstructured{Object: newFakeRoleBinding()} 1198 liveRole := &unstructured.Unstructured{Object: newFakeRole()} 1199 liveServiceAccount := &unstructured.Unstructured{Object: newFakeServiceAccount()} 1200 liveHook := &unstructured.Unstructured{Object: newFakePostDeleteHook()} 1201 conditions := []any{ 1202 map[string]any{ 1203 "type": "Complete", 1204 "status": "True", 1205 }, 1206 } 1207 require.NoError(t, unstructured.SetNestedField(liveHook.Object, conditions, "status", "conditions")) 1208 ctrl := newFakeController(&fakeData{ 1209 manifestResponses: []*apiclient.ManifestResponse{{ 1210 Manifests: []string{fakeRoleBinding, fakeRole, fakeServiceAccount, fakePostDeleteHook}, 1211 }}, 1212 apps: []runtime.Object{app, &defaultProj}, 1213 managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{ 1214 kube.GetResourceKey(liveRoleBinding): liveRoleBinding, 1215 kube.GetResourceKey(liveRole): liveRole, 1216 kube.GetResourceKey(liveServiceAccount): liveServiceAccount, 1217 kube.GetResourceKey(liveHook): liveHook, 1218 }, 1219 }, nil) 1220 1221 patched := false 1222 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 1223 defaultReactor := fakeAppCs.ReactionChain[0] 1224 fakeAppCs.ReactionChain = nil 1225 fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1226 return defaultReactor.React(action) 1227 }) 1228 fakeAppCs.AddReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1229 patched = true 1230 return true, &v1alpha1.Application{}, nil 1231 }) 1232 err := ctrl.finalizeApplicationDeletion(app, func(_ string) ([]*v1alpha1.Cluster, error) { 1233 return []*v1alpha1.Cluster{}, nil 1234 }) 1235 require.NoError(t, err) 1236 // post-delete hooks are deleted 1237 require.Len(t, ctrl.kubectl.(*MockKubectl).DeletedResources, 4) 1238 deletedResources := []string{} 1239 for _, res := range ctrl.kubectl.(*MockKubectl).DeletedResources { 1240 deletedResources = append(deletedResources, res.Name) 1241 } 1242 expectedNames := []string{"hook-rolebinding", "hook-role", "hook-serviceaccount", "post-delete-hook"} 1243 require.ElementsMatch(t, expectedNames, deletedResources, "Deleted resources should match expected names") 1244 // finalizer is not removed 1245 assert.False(t, patched) 1246 }) 1247 } 1248 1249 // TestNormalizeApplication verifies we normalize an application during reconciliation 1250 func TestNormalizeApplication(t *testing.T) { 1251 defaultProj := v1alpha1.AppProject{ 1252 ObjectMeta: metav1.ObjectMeta{ 1253 Name: "default", 1254 Namespace: test.FakeArgoCDNamespace, 1255 }, 1256 Spec: v1alpha1.AppProjectSpec{ 1257 SourceRepos: []string{"*"}, 1258 Destinations: []v1alpha1.ApplicationDestination{ 1259 { 1260 Server: "*", 1261 Namespace: "*", 1262 }, 1263 }, 1264 }, 1265 } 1266 app := newFakeApp() 1267 app.Spec.Project = "" 1268 app.Spec.Source.Kustomize = &v1alpha1.ApplicationSourceKustomize{NamePrefix: "foo-"} 1269 data := fakeData{ 1270 apps: []runtime.Object{app, &defaultProj}, 1271 manifestResponse: &apiclient.ManifestResponse{ 1272 Manifests: []string{}, 1273 Namespace: test.FakeDestNamespace, 1274 Server: test.FakeClusterURL, 1275 Revision: "abc123", 1276 }, 1277 managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured), 1278 } 1279 1280 { 1281 // Verify we normalize the app because project is missing 1282 ctrl := newFakeController(&data, nil) 1283 key, _ := cache.MetaNamespaceKeyFunc(app) 1284 ctrl.appRefreshQueue.AddRateLimited(key) 1285 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 1286 fakeAppCs.ReactionChain = nil 1287 normalized := false 1288 fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1289 if patchAction, ok := action.(kubetesting.PatchAction); ok { 1290 if string(patchAction.GetPatch()) == `{"spec":{"project":"default"}}` { 1291 normalized = true 1292 } 1293 } 1294 return true, &v1alpha1.Application{}, nil 1295 }) 1296 ctrl.processAppRefreshQueueItem() 1297 assert.True(t, normalized) 1298 } 1299 1300 { 1301 // Verify we don't unnecessarily normalize app when project is set 1302 app.Spec.Project = "default" 1303 data.apps[0] = app 1304 ctrl := newFakeController(&data, nil) 1305 key, _ := cache.MetaNamespaceKeyFunc(app) 1306 ctrl.appRefreshQueue.AddRateLimited(key) 1307 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 1308 fakeAppCs.ReactionChain = nil 1309 normalized := false 1310 fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1311 if patchAction, ok := action.(kubetesting.PatchAction); ok { 1312 if string(patchAction.GetPatch()) == `{"spec":{"project":"default"},"status":{"sync":{"comparedTo":{"destination":{},"source":{"repoURL":""}}}}}` { 1313 normalized = true 1314 } 1315 } 1316 return true, &v1alpha1.Application{}, nil 1317 }) 1318 ctrl.processAppRefreshQueueItem() 1319 assert.False(t, normalized) 1320 } 1321 } 1322 1323 func TestHandleAppUpdated(t *testing.T) { 1324 app := newFakeApp() 1325 app.Spec.Destination.Namespace = test.FakeArgoCDNamespace 1326 app.Spec.Destination.Server = v1alpha1.KubernetesInternalAPIServerAddr 1327 proj := defaultProj.DeepCopy() 1328 proj.Spec.SourceNamespaces = []string{test.FakeArgoCDNamespace} 1329 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, proj}}, nil) 1330 1331 ctrl.handleObjectUpdated(map[string]bool{app.InstanceName(ctrl.namespace): true}, kube.GetObjectRef(kube.MustToUnstructured(app))) 1332 isRequested, level := ctrl.isRefreshRequested(app.QualifiedName()) 1333 assert.False(t, isRequested) 1334 assert.Equal(t, ComparisonWithNothing, level) 1335 1336 ctrl.handleObjectUpdated(map[string]bool{app.InstanceName(ctrl.namespace): true}, corev1.ObjectReference{UID: "test", Kind: kube.DeploymentKind, Name: "test", Namespace: "default"}) 1337 isRequested, level = ctrl.isRefreshRequested(app.QualifiedName()) 1338 assert.True(t, isRequested) 1339 assert.Equal(t, CompareWithRecent, level) 1340 } 1341 1342 func TestHandleOrphanedResourceUpdated(t *testing.T) { 1343 app1 := newFakeApp() 1344 app1.Name = "app1" 1345 app1.Spec.Destination.Namespace = test.FakeArgoCDNamespace 1346 app1.Spec.Destination.Server = v1alpha1.KubernetesInternalAPIServerAddr 1347 1348 app2 := newFakeApp() 1349 app2.Name = "app2" 1350 app2.Spec.Destination.Namespace = test.FakeArgoCDNamespace 1351 app2.Spec.Destination.Server = v1alpha1.KubernetesInternalAPIServerAddr 1352 1353 proj := defaultProj.DeepCopy() 1354 proj.Spec.OrphanedResources = &v1alpha1.OrphanedResourcesMonitorSettings{} 1355 1356 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app1, app2, proj}}, nil) 1357 1358 ctrl.handleObjectUpdated(map[string]bool{}, corev1.ObjectReference{UID: "test", Kind: kube.DeploymentKind, Name: "test", Namespace: test.FakeArgoCDNamespace}) 1359 1360 isRequested, level := ctrl.isRefreshRequested(app1.QualifiedName()) 1361 assert.True(t, isRequested) 1362 assert.Equal(t, CompareWithRecent, level) 1363 1364 isRequested, level = ctrl.isRefreshRequested(app2.QualifiedName()) 1365 assert.True(t, isRequested) 1366 assert.Equal(t, CompareWithRecent, level) 1367 } 1368 1369 func TestGetResourceTree_HasOrphanedResources(t *testing.T) { 1370 app := newFakeApp() 1371 proj := defaultProj.DeepCopy() 1372 proj.Spec.OrphanedResources = &v1alpha1.OrphanedResourcesMonitorSettings{} 1373 1374 managedDeploy := v1alpha1.ResourceNode{ 1375 ResourceRef: v1alpha1.ResourceRef{Group: "apps", Kind: "Deployment", Namespace: "default", Name: "nginx-deployment", Version: "v1"}, 1376 Health: &v1alpha1.HealthStatus{ 1377 Status: health.HealthStatusMissing, 1378 }, 1379 } 1380 orphanedDeploy1 := v1alpha1.ResourceNode{ 1381 ResourceRef: v1alpha1.ResourceRef{Group: "apps", Kind: "Deployment", Namespace: "default", Name: "deploy1"}, 1382 } 1383 orphanedDeploy2 := v1alpha1.ResourceNode{ 1384 ResourceRef: v1alpha1.ResourceRef{Group: "apps", Kind: "Deployment", Namespace: "default", Name: "deploy2"}, 1385 } 1386 1387 ctrl := newFakeController(&fakeData{ 1388 apps: []runtime.Object{app, proj}, 1389 namespacedResources: map[kube.ResourceKey]namespacedResource{ 1390 kube.NewResourceKey("apps", "Deployment", "default", "nginx-deployment"): {ResourceNode: managedDeploy}, 1391 kube.NewResourceKey("apps", "Deployment", "default", "deploy1"): {ResourceNode: orphanedDeploy1}, 1392 kube.NewResourceKey("apps", "Deployment", "default", "deploy2"): {ResourceNode: orphanedDeploy2}, 1393 }, 1394 }, nil) 1395 tree, err := ctrl.getResourceTree(&v1alpha1.Cluster{Server: "https://localhost:6443", Name: "fake-cluster"}, app, []*v1alpha1.ResourceDiff{{ 1396 Namespace: "default", 1397 Name: "nginx-deployment", 1398 Kind: "Deployment", 1399 Group: "apps", 1400 LiveState: "null", 1401 TargetState: test.DeploymentManifest, 1402 }}) 1403 1404 require.NoError(t, err) 1405 assert.Equal(t, []v1alpha1.ResourceNode{managedDeploy}, tree.Nodes) 1406 assert.Equal(t, []v1alpha1.ResourceNode{orphanedDeploy1, orphanedDeploy2}, tree.OrphanedNodes) 1407 } 1408 1409 func TestSetOperationStateOnDeletedApp(t *testing.T) { 1410 ctrl := newFakeController(&fakeData{apps: []runtime.Object{}}, nil) 1411 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 1412 fakeAppCs.ReactionChain = nil 1413 patched := false 1414 fakeAppCs.AddReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1415 patched = true 1416 return true, &v1alpha1.Application{}, apierrors.NewNotFound(schema.GroupResource{}, "my-app") 1417 }) 1418 ctrl.setOperationState(newFakeApp(), &v1alpha1.OperationState{Phase: synccommon.OperationSucceeded}) 1419 assert.True(t, patched) 1420 } 1421 1422 func TestSetOperationStateLogRetries(t *testing.T) { 1423 hook := utilTest.LogHook{} 1424 logrus.AddHook(&hook) 1425 t.Cleanup(func() { 1426 logrus.StandardLogger().ReplaceHooks(logrus.LevelHooks{}) 1427 }) 1428 ctrl := newFakeController(&fakeData{apps: []runtime.Object{}}, nil) 1429 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 1430 fakeAppCs.ReactionChain = nil 1431 patched := false 1432 fakeAppCs.AddReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1433 if !patched { 1434 patched = true 1435 return true, &v1alpha1.Application{}, errors.New("fake error") 1436 } 1437 return true, &v1alpha1.Application{}, nil 1438 }) 1439 ctrl.setOperationState(newFakeApp(), &v1alpha1.OperationState{Phase: synccommon.OperationSucceeded}) 1440 assert.True(t, patched) 1441 assert.Contains(t, hook.Entries[0].Message, "fake error") 1442 } 1443 1444 func TestNeedRefreshAppStatus(t *testing.T) { 1445 testCases := []struct { 1446 name string 1447 app *v1alpha1.Application 1448 }{ 1449 { 1450 name: "single-source app", 1451 app: newFakeApp(), 1452 }, 1453 { 1454 name: "multi-source app", 1455 app: newFakeMultiSourceApp(), 1456 }, 1457 } 1458 1459 for _, tc := range testCases { 1460 t.Run(tc.name, func(t *testing.T) { 1461 app := tc.app 1462 now := metav1.Now() 1463 app.Status.ReconciledAt = &now 1464 1465 app.Status.Sync = v1alpha1.SyncStatus{ 1466 Status: v1alpha1.SyncStatusCodeSynced, 1467 ComparedTo: v1alpha1.ComparedTo{ 1468 Destination: app.Spec.Destination, 1469 IgnoreDifferences: app.Spec.IgnoreDifferences, 1470 }, 1471 } 1472 1473 if app.Spec.HasMultipleSources() { 1474 app.Status.Sync.ComparedTo.Sources = app.Spec.Sources 1475 } else { 1476 app.Status.Sync.ComparedTo.Source = app.Spec.GetSource() 1477 } 1478 1479 ctrl := newFakeController(&fakeData{apps: []runtime.Object{}}, nil) 1480 1481 t.Run("no need to refresh just reconciled application", func(t *testing.T) { 1482 needRefresh, _, _ := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour) 1483 assert.False(t, needRefresh) 1484 }) 1485 1486 t.Run("requested refresh is respected", func(t *testing.T) { 1487 needRefresh, _, _ := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour) 1488 assert.False(t, needRefresh) 1489 1490 // use a one-off controller so other tests don't have a manual refresh request 1491 ctrl := newFakeController(&fakeData{apps: []runtime.Object{}}, nil) 1492 1493 // refresh app using the 'deepest' requested comparison level 1494 ctrl.requestAppRefresh(app.Name, CompareWithRecent.Pointer(), nil) 1495 ctrl.requestAppRefresh(app.Name, ComparisonWithNothing.Pointer(), nil) 1496 1497 needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour) 1498 assert.True(t, needRefresh) 1499 assert.Equal(t, v1alpha1.RefreshTypeNormal, refreshType) 1500 assert.Equal(t, CompareWithRecent, compareWith) 1501 }) 1502 1503 t.Run("requesting refresh with delay gives correct compression level", func(t *testing.T) { 1504 needRefresh, _, _ := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour) 1505 assert.False(t, needRefresh) 1506 1507 // use a one-off controller so other tests don't have a manual refresh request 1508 ctrl := newFakeController(&fakeData{apps: []runtime.Object{}}, nil) 1509 1510 // refresh app with a non-nil delay 1511 // use zero-second delay to test the add later logic without waiting in the test 1512 delay := time.Duration(0) 1513 ctrl.requestAppRefresh(app.Name, CompareWithRecent.Pointer(), &delay) 1514 1515 ctrl.processAppComparisonTypeQueueItem() 1516 needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour) 1517 assert.True(t, needRefresh) 1518 assert.Equal(t, v1alpha1.RefreshTypeNormal, refreshType) 1519 assert.Equal(t, CompareWithRecent, compareWith) 1520 }) 1521 1522 t.Run("refresh application which status is not reconciled using latest commit", func(t *testing.T) { 1523 app := app.DeepCopy() 1524 needRefresh, _, _ := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour) 1525 assert.False(t, needRefresh) 1526 app.Status.Sync = v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeUnknown} 1527 1528 needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour) 1529 assert.True(t, needRefresh) 1530 assert.Equal(t, v1alpha1.RefreshTypeNormal, refreshType) 1531 assert.Equal(t, CompareWithLatestForceResolve, compareWith) 1532 }) 1533 1534 t.Run("refresh app using the 'latest' level if comparison expired", func(t *testing.T) { 1535 app := app.DeepCopy() 1536 1537 // use a one-off controller so other tests don't have a manual refresh request 1538 ctrl := newFakeController(&fakeData{apps: []runtime.Object{}}, nil) 1539 1540 needRefresh, _, _ := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour) 1541 assert.False(t, needRefresh) 1542 1543 ctrl.requestAppRefresh(app.Name, CompareWithRecent.Pointer(), nil) 1544 reconciledAt := metav1.NewTime(time.Now().UTC().Add(-1 * time.Hour)) 1545 app.Status.ReconciledAt = &reconciledAt 1546 needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 1*time.Minute, 2*time.Hour) 1547 assert.True(t, needRefresh) 1548 assert.Equal(t, v1alpha1.RefreshTypeNormal, refreshType) 1549 assert.Equal(t, CompareWithLatest, compareWith) 1550 }) 1551 1552 t.Run("refresh app using the 'latest' level if comparison expired for hard refresh", func(t *testing.T) { 1553 app := app.DeepCopy() 1554 app.Status.Sync = v1alpha1.SyncStatus{ 1555 Status: v1alpha1.SyncStatusCodeSynced, 1556 ComparedTo: v1alpha1.ComparedTo{ 1557 Destination: app.Spec.Destination, 1558 IgnoreDifferences: app.Spec.IgnoreDifferences, 1559 }, 1560 } 1561 if app.Spec.HasMultipleSources() { 1562 app.Status.Sync.ComparedTo.Sources = app.Spec.Sources 1563 } else { 1564 app.Status.Sync.ComparedTo.Source = app.Spec.GetSource() 1565 } 1566 1567 // use a one-off controller so other tests don't have a manual refresh request 1568 ctrl := newFakeController(&fakeData{apps: []runtime.Object{}}, nil) 1569 1570 needRefresh, _, _ := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour) 1571 assert.False(t, needRefresh) 1572 ctrl.requestAppRefresh(app.Name, CompareWithRecent.Pointer(), nil) 1573 reconciledAt := metav1.NewTime(time.Now().UTC().Add(-1 * time.Hour)) 1574 app.Status.ReconciledAt = &reconciledAt 1575 needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 2*time.Hour, 1*time.Minute) 1576 assert.True(t, needRefresh) 1577 assert.Equal(t, v1alpha1.RefreshTypeHard, refreshType) 1578 assert.Equal(t, CompareWithLatest, compareWith) 1579 }) 1580 1581 t.Run("execute hard refresh if app has refresh annotation", func(t *testing.T) { 1582 app := app.DeepCopy() 1583 needRefresh, _, _ := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour) 1584 assert.False(t, needRefresh) 1585 reconciledAt := metav1.NewTime(time.Now().UTC().Add(-1 * time.Hour)) 1586 app.Status.ReconciledAt = &reconciledAt 1587 app.Annotations = map[string]string{ 1588 v1alpha1.AnnotationKeyRefresh: string(v1alpha1.RefreshTypeHard), 1589 } 1590 needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour) 1591 assert.True(t, needRefresh) 1592 assert.Equal(t, v1alpha1.RefreshTypeHard, refreshType) 1593 assert.Equal(t, CompareWithLatestForceResolve, compareWith) 1594 }) 1595 1596 t.Run("ensure that CompareWithLatest level is used if application source has changed", func(t *testing.T) { 1597 app := app.DeepCopy() 1598 needRefresh, _, _ := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour) 1599 assert.False(t, needRefresh) 1600 // sample app source change 1601 if app.Spec.HasMultipleSources() { 1602 app.Spec.Sources[0].Helm = &v1alpha1.ApplicationSourceHelm{ 1603 Parameters: []v1alpha1.HelmParameter{{ 1604 Name: "foo", 1605 Value: "bar", 1606 }}, 1607 } 1608 } else { 1609 app.Spec.Source.Helm = &v1alpha1.ApplicationSourceHelm{ 1610 Parameters: []v1alpha1.HelmParameter{{ 1611 Name: "foo", 1612 Value: "bar", 1613 }}, 1614 } 1615 } 1616 1617 needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour) 1618 assert.True(t, needRefresh) 1619 assert.Equal(t, v1alpha1.RefreshTypeNormal, refreshType) 1620 assert.Equal(t, CompareWithLatestForceResolve, compareWith) 1621 }) 1622 1623 t.Run("ensure that CompareWithLatest level is used if ignored differences change", func(t *testing.T) { 1624 app := app.DeepCopy() 1625 needRefresh, _, _ := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour) 1626 assert.False(t, needRefresh) 1627 1628 app.Spec.IgnoreDifferences = []v1alpha1.ResourceIgnoreDifferences{ 1629 { 1630 Group: "apps", 1631 Kind: "Deployment", 1632 JSONPointers: []string{ 1633 "/spec/template/spec/containers/0/image", 1634 }, 1635 }, 1636 } 1637 1638 needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour) 1639 assert.True(t, needRefresh) 1640 assert.Equal(t, v1alpha1.RefreshTypeNormal, refreshType) 1641 assert.Equal(t, CompareWithLatest, compareWith) 1642 }) 1643 }) 1644 } 1645 } 1646 1647 func TestUpdatedManagedNamespaceMetadata(t *testing.T) { 1648 ctrl := newFakeController(&fakeData{apps: []runtime.Object{}}, nil) 1649 app := newFakeApp() 1650 app.Spec.SyncPolicy.ManagedNamespaceMetadata = &v1alpha1.ManagedNamespaceMetadata{ 1651 Labels: map[string]string{ 1652 "foo": "bar", 1653 }, 1654 Annotations: map[string]string{ 1655 "foo": "bar", 1656 }, 1657 } 1658 app.Status.Sync.ComparedTo.Source = app.Spec.GetSource() 1659 app.Status.Sync.ComparedTo.Destination = app.Spec.Destination 1660 1661 // Ensure that hard/soft refresh isn't triggered due to reconciledAt being expired 1662 reconciledAt := metav1.NewTime(time.Now().UTC().Add(15 * time.Minute)) 1663 app.Status.ReconciledAt = &reconciledAt 1664 needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 30*time.Minute, 2*time.Hour) 1665 1666 assert.True(t, needRefresh) 1667 assert.Equal(t, v1alpha1.RefreshTypeNormal, refreshType) 1668 assert.Equal(t, CompareWithLatest, compareWith) 1669 } 1670 1671 func TestUnchangedManagedNamespaceMetadata(t *testing.T) { 1672 ctrl := newFakeController(&fakeData{apps: []runtime.Object{}}, nil) 1673 app := newFakeApp() 1674 app.Spec.SyncPolicy.ManagedNamespaceMetadata = &v1alpha1.ManagedNamespaceMetadata{ 1675 Labels: map[string]string{ 1676 "foo": "bar", 1677 }, 1678 Annotations: map[string]string{ 1679 "foo": "bar", 1680 }, 1681 } 1682 app.Status.Sync.ComparedTo.Source = app.Spec.GetSource() 1683 app.Status.Sync.ComparedTo.Destination = app.Spec.Destination 1684 app.Status.OperationState.SyncResult.ManagedNamespaceMetadata = app.Spec.SyncPolicy.ManagedNamespaceMetadata 1685 1686 // Ensure that hard/soft refresh isn't triggered due to reconciledAt being expired 1687 reconciledAt := metav1.NewTime(time.Now().UTC().Add(15 * time.Minute)) 1688 app.Status.ReconciledAt = &reconciledAt 1689 needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 30*time.Minute, 2*time.Hour) 1690 1691 assert.False(t, needRefresh) 1692 assert.Equal(t, v1alpha1.RefreshTypeNormal, refreshType) 1693 assert.Equal(t, CompareWithLatest, compareWith) 1694 } 1695 1696 func TestRefreshAppConditions(t *testing.T) { 1697 defaultProj := v1alpha1.AppProject{ 1698 ObjectMeta: metav1.ObjectMeta{ 1699 Name: "default", 1700 Namespace: test.FakeArgoCDNamespace, 1701 }, 1702 Spec: v1alpha1.AppProjectSpec{ 1703 SourceRepos: []string{"*"}, 1704 Destinations: []v1alpha1.ApplicationDestination{ 1705 { 1706 Server: "*", 1707 Namespace: "*", 1708 }, 1709 }, 1710 }, 1711 } 1712 1713 t.Run("NoErrorConditions", func(t *testing.T) { 1714 app := newFakeApp() 1715 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}}, nil) 1716 1717 _, hasErrors := ctrl.refreshAppConditions(app) 1718 assert.False(t, hasErrors) 1719 assert.Empty(t, app.Status.Conditions) 1720 }) 1721 1722 t.Run("PreserveExistingWarningCondition", func(t *testing.T) { 1723 app := newFakeApp() 1724 app.Status.SetConditions([]v1alpha1.ApplicationCondition{{Type: v1alpha1.ApplicationConditionExcludedResourceWarning}}, nil) 1725 1726 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}}, nil) 1727 1728 _, hasErrors := ctrl.refreshAppConditions(app) 1729 assert.False(t, hasErrors) 1730 assert.Len(t, app.Status.Conditions, 1) 1731 assert.Equal(t, v1alpha1.ApplicationConditionExcludedResourceWarning, app.Status.Conditions[0].Type) 1732 }) 1733 1734 t.Run("ReplacesSpecErrorCondition", func(t *testing.T) { 1735 app := newFakeApp() 1736 app.Spec.Project = "wrong project" 1737 app.Status.SetConditions([]v1alpha1.ApplicationCondition{{Type: v1alpha1.ApplicationConditionInvalidSpecError, Message: "old message"}}, nil) 1738 1739 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}}, nil) 1740 1741 _, hasErrors := ctrl.refreshAppConditions(app) 1742 assert.True(t, hasErrors) 1743 assert.Len(t, app.Status.Conditions, 1) 1744 assert.Equal(t, v1alpha1.ApplicationConditionInvalidSpecError, app.Status.Conditions[0].Type) 1745 assert.Equal(t, "Application referencing project wrong project which does not exist", app.Status.Conditions[0].Message) 1746 }) 1747 } 1748 1749 func TestUpdateReconciledAt(t *testing.T) { 1750 app := newFakeApp() 1751 reconciledAt := metav1.NewTime(time.Now().Add(-1 * time.Second)) 1752 app.Status = v1alpha1.ApplicationStatus{ReconciledAt: &reconciledAt} 1753 app.Status.Sync = v1alpha1.SyncStatus{ComparedTo: v1alpha1.ComparedTo{Source: app.Spec.GetSource(), Destination: app.Spec.Destination, IgnoreDifferences: app.Spec.IgnoreDifferences}} 1754 ctrl := newFakeController(&fakeData{ 1755 apps: []runtime.Object{app, &defaultProj}, 1756 manifestResponse: &apiclient.ManifestResponse{ 1757 Manifests: []string{}, 1758 Namespace: test.FakeDestNamespace, 1759 Server: test.FakeClusterURL, 1760 Revision: "abc123", 1761 }, 1762 managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured), 1763 }, nil) 1764 key, _ := cache.MetaNamespaceKeyFunc(app) 1765 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 1766 fakeAppCs.ReactionChain = nil 1767 receivedPatch := map[string]any{} 1768 fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1769 if patchAction, ok := action.(kubetesting.PatchAction); ok { 1770 require.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch)) 1771 } 1772 return true, &v1alpha1.Application{}, nil 1773 }) 1774 1775 t.Run("UpdatedOnFullReconciliation", func(t *testing.T) { 1776 receivedPatch = map[string]any{} 1777 ctrl.requestAppRefresh(app.Name, CompareWithLatest.Pointer(), nil) 1778 ctrl.appRefreshQueue.AddRateLimited(key) 1779 1780 ctrl.processAppRefreshQueueItem() 1781 1782 _, updated, err := unstructured.NestedString(receivedPatch, "status", "reconciledAt") 1783 require.NoError(t, err) 1784 assert.True(t, updated) 1785 1786 _, updated, err = unstructured.NestedString(receivedPatch, "status", "observedAt") 1787 require.NoError(t, err) 1788 assert.False(t, updated) 1789 }) 1790 1791 t.Run("NotUpdatedOnPartialReconciliation", func(t *testing.T) { 1792 receivedPatch = map[string]any{} 1793 ctrl.appRefreshQueue.AddRateLimited(key) 1794 ctrl.requestAppRefresh(app.Name, CompareWithRecent.Pointer(), nil) 1795 1796 ctrl.processAppRefreshQueueItem() 1797 1798 _, updated, err := unstructured.NestedString(receivedPatch, "status", "reconciledAt") 1799 require.NoError(t, err) 1800 assert.False(t, updated) 1801 1802 _, updated, err = unstructured.NestedString(receivedPatch, "status", "observedAt") 1803 require.NoError(t, err) 1804 assert.False(t, updated) 1805 }) 1806 } 1807 1808 func TestUpdateHealthStatusTransitionTime(t *testing.T) { 1809 deployment := kube.MustToUnstructured(&appsv1.Deployment{ 1810 TypeMeta: metav1.TypeMeta{ 1811 APIVersion: "apps/v1", 1812 Kind: "Deployment", 1813 }, 1814 ObjectMeta: metav1.ObjectMeta{ 1815 Name: "demo", 1816 Namespace: "default", 1817 }, 1818 }) 1819 testCases := []struct { 1820 name string 1821 app *v1alpha1.Application 1822 configMapData map[string]string 1823 expectedStatus health.HealthStatusCode 1824 }{ 1825 { 1826 name: "Degraded to Missing", 1827 app: newFakeAppWithHealthAndTime(health.HealthStatusDegraded, testTimestamp), 1828 configMapData: map[string]string{ 1829 "resource.customizations": ` 1830 apps/Deployment: 1831 health.lua: | 1832 hs = {} 1833 hs.status = "Missing" 1834 hs.message = "" 1835 return hs`, 1836 }, 1837 expectedStatus: health.HealthStatusMissing, 1838 }, 1839 { 1840 name: "Missing to Progressing", 1841 app: newFakeAppWithHealthAndTime(health.HealthStatusMissing, testTimestamp), 1842 configMapData: map[string]string{ 1843 "resource.customizations": ` 1844 apps/Deployment: 1845 health.lua: | 1846 hs = {} 1847 hs.status = "Progressing" 1848 hs.message = "" 1849 return hs`, 1850 }, 1851 expectedStatus: health.HealthStatusProgressing, 1852 }, 1853 { 1854 name: "Progressing to Healthy", 1855 app: newFakeAppWithHealthAndTime(health.HealthStatusProgressing, testTimestamp), 1856 configMapData: map[string]string{ 1857 "resource.customizations": ` 1858 apps/Deployment: 1859 health.lua: | 1860 hs = {} 1861 hs.status = "Healthy" 1862 hs.message = "" 1863 return hs`, 1864 }, 1865 expectedStatus: health.HealthStatusHealthy, 1866 }, 1867 { 1868 name: "Healthy to Degraded", 1869 app: newFakeAppWithHealthAndTime(health.HealthStatusHealthy, testTimestamp), 1870 configMapData: map[string]string{ 1871 "resource.customizations": ` 1872 apps/Deployment: 1873 health.lua: | 1874 hs = {} 1875 hs.status = "Degraded" 1876 hs.message = "" 1877 return hs`, 1878 }, 1879 expectedStatus: health.HealthStatusDegraded, 1880 }, 1881 } 1882 1883 for _, tc := range testCases { 1884 t.Run(tc.name, func(t *testing.T) { 1885 ctrl := newFakeController(&fakeData{ 1886 apps: []runtime.Object{tc.app, &defaultProj}, 1887 manifestResponse: &apiclient.ManifestResponse{ 1888 Manifests: []string{}, 1889 Namespace: test.FakeDestNamespace, 1890 Server: test.FakeClusterURL, 1891 Revision: "abc123", 1892 }, 1893 managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{ 1894 kube.GetResourceKey(deployment): deployment, 1895 }, 1896 configMapData: tc.configMapData, 1897 }, nil) 1898 1899 ctrl.processAppRefreshQueueItem() 1900 apps, err := ctrl.appLister.List(labels.Everything()) 1901 require.NoError(t, err) 1902 assert.NotEmpty(t, apps) 1903 assert.Equal(t, tc.expectedStatus, apps[0].Status.Health.Status) 1904 assert.NotEqual(t, testTimestamp, *apps[0].Status.Health.LastTransitionTime) 1905 }) 1906 } 1907 } 1908 1909 func TestUpdateHealthStatusProgression(t *testing.T) { 1910 app := newFakeAppWithHealthAndTime(health.HealthStatusDegraded, testTimestamp) 1911 deployment := kube.MustToUnstructured(&appsv1.Deployment{ 1912 TypeMeta: metav1.TypeMeta{ 1913 APIVersion: "apps/v1", 1914 Kind: "Deployment", 1915 }, 1916 ObjectMeta: metav1.ObjectMeta{ 1917 Name: "demo", 1918 Namespace: "default", 1919 }, 1920 Status: appsv1.DeploymentStatus{ 1921 ObservedGeneration: 0, 1922 }, 1923 }) 1924 configMapData := map[string]string{ 1925 "resource.customizations": ` 1926 apps/Deployment: 1927 health.lua: | 1928 hs = {} 1929 hs.status = "" 1930 hs.message = "" 1931 1932 if obj.metadata ~= nil then 1933 if obj.metadata.labels ~= nil then 1934 current_status = obj.metadata.labels["status"] 1935 if current_status == "Degraded" then 1936 hs.status = "Missing" 1937 elseif current_status == "Missing" then 1938 hs.status = "Progressing" 1939 elseif current_status == "Progressing" then 1940 hs.status = "Healthy" 1941 elseif current_status == "Healthy" then 1942 hs.status = "Degraded" 1943 end 1944 end 1945 end 1946 1947 return hs`, 1948 } 1949 ctrl := newFakeControllerWithResync(&fakeData{ 1950 apps: []runtime.Object{app, &defaultProj}, 1951 manifestResponse: &apiclient.ManifestResponse{ 1952 Manifests: []string{}, 1953 Namespace: test.FakeDestNamespace, 1954 Server: test.FakeClusterURL, 1955 Revision: "abc123", 1956 }, 1957 managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{ 1958 kube.GetResourceKey(deployment): deployment, 1959 }, 1960 configMapData: configMapData, 1961 manifestResponses: []*apiclient.ManifestResponse{ 1962 {}, 1963 {}, 1964 {}, 1965 {}, 1966 }, 1967 }, time.Millisecond*10, nil, nil) 1968 1969 testCases := []struct { 1970 name string 1971 initialStatus string 1972 expectedStatus health.HealthStatusCode 1973 }{ 1974 { 1975 name: "Degraded to Missing", 1976 initialStatus: "Degraded", 1977 expectedStatus: health.HealthStatusMissing, 1978 }, 1979 { 1980 name: "Missing to Progressing", 1981 initialStatus: "Missing", 1982 expectedStatus: health.HealthStatusProgressing, 1983 }, 1984 { 1985 name: "Progressing to Healthy", 1986 initialStatus: "Progressing", 1987 expectedStatus: health.HealthStatusHealthy, 1988 }, 1989 { 1990 name: "Healthy to Degraded", 1991 initialStatus: "Healthy", 1992 expectedStatus: health.HealthStatusDegraded, 1993 }, 1994 } 1995 1996 for _, tc := range testCases { 1997 t.Run(tc.name, func(t *testing.T) { 1998 deployment.SetLabels(map[string]string{"status": tc.initialStatus}) 1999 ctrl.processAppRefreshQueueItem() 2000 apps, err := ctrl.appLister.List(labels.Everything()) 2001 require.NoError(t, err) 2002 if assert.NotEmpty(t, apps) { 2003 assert.Equal(t, tc.expectedStatus, apps[0].Status.Health.Status) 2004 assert.NotEqual(t, testTimestamp, *apps[0].Status.Health.LastTransitionTime) 2005 } 2006 2007 ctrl.requestAppRefresh(app.Name, nil, nil) 2008 time.Sleep(time.Millisecond * 15) 2009 }) 2010 } 2011 } 2012 2013 func TestProjectErrorToCondition(t *testing.T) { 2014 app := newFakeApp() 2015 app.Spec.Project = "wrong project" 2016 ctrl := newFakeController(&fakeData{ 2017 apps: []runtime.Object{app, &defaultProj}, 2018 manifestResponse: &apiclient.ManifestResponse{ 2019 Manifests: []string{}, 2020 Namespace: test.FakeDestNamespace, 2021 Server: test.FakeClusterURL, 2022 Revision: "abc123", 2023 }, 2024 managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured), 2025 }, nil) 2026 key, _ := cache.MetaNamespaceKeyFunc(app) 2027 ctrl.appRefreshQueue.AddRateLimited(key) 2028 ctrl.requestAppRefresh(app.Name, CompareWithRecent.Pointer(), nil) 2029 2030 ctrl.processAppRefreshQueueItem() 2031 2032 obj, ok, err := ctrl.appInformer.GetIndexer().GetByKey(key) 2033 assert.True(t, ok) 2034 require.NoError(t, err) 2035 updatedApp := obj.(*v1alpha1.Application) 2036 assert.Equal(t, v1alpha1.ApplicationConditionInvalidSpecError, updatedApp.Status.Conditions[0].Type) 2037 assert.Equal(t, "Application referencing project wrong project which does not exist", updatedApp.Status.Conditions[0].Message) 2038 assert.Equal(t, v1alpha1.ApplicationConditionInvalidSpecError, updatedApp.Status.Conditions[0].Type) 2039 } 2040 2041 func TestFinalizeProjectDeletion_HasApplications(t *testing.T) { 2042 app := newFakeApp() 2043 proj := &v1alpha1.AppProject{ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: test.FakeArgoCDNamespace}} 2044 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, proj}}, nil) 2045 2046 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 2047 patched := false 2048 fakeAppCs.PrependReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) { 2049 patched = true 2050 return true, &v1alpha1.Application{}, nil 2051 }) 2052 2053 err := ctrl.finalizeProjectDeletion(proj) 2054 require.NoError(t, err) 2055 assert.False(t, patched) 2056 } 2057 2058 func TestFinalizeProjectDeletion_DoesNotHaveApplications(t *testing.T) { 2059 proj := &v1alpha1.AppProject{ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: test.FakeArgoCDNamespace}} 2060 ctrl := newFakeController(&fakeData{apps: []runtime.Object{&defaultProj}}, nil) 2061 2062 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 2063 receivedPatch := map[string]any{} 2064 fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 2065 if patchAction, ok := action.(kubetesting.PatchAction); ok { 2066 require.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch)) 2067 } 2068 return true, &v1alpha1.AppProject{}, nil 2069 }) 2070 2071 err := ctrl.finalizeProjectDeletion(proj) 2072 require.NoError(t, err) 2073 assert.Equal(t, map[string]any{ 2074 "metadata": map[string]any{ 2075 "finalizers": nil, 2076 }, 2077 }, receivedPatch) 2078 } 2079 2080 func TestProcessRequestedAppOperation_FailedNoRetries(t *testing.T) { 2081 app := newFakeApp() 2082 app.Spec.Project = "default" 2083 app.Operation = &v1alpha1.Operation{ 2084 Sync: &v1alpha1.SyncOperation{}, 2085 } 2086 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 2087 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 2088 receivedPatch := map[string]any{} 2089 fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 2090 if patchAction, ok := action.(kubetesting.PatchAction); ok { 2091 require.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch)) 2092 } 2093 return true, &v1alpha1.Application{}, nil 2094 }) 2095 2096 ctrl.processRequestedAppOperation(app) 2097 2098 phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase") 2099 message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message") 2100 assert.Equal(t, string(synccommon.OperationError), phase) 2101 assert.Equal(t, "Failed to load application project: error getting app project \"default\": appproject.argoproj.io \"default\" not found", message) 2102 } 2103 2104 func TestProcessRequestedAppOperation_InvalidDestination(t *testing.T) { 2105 app := newFakeAppWithDestMismatch() 2106 app.Spec.Project = "test-project" 2107 app.Operation = &v1alpha1.Operation{ 2108 Sync: &v1alpha1.SyncOperation{}, 2109 } 2110 proj := defaultProj 2111 proj.Name = "test-project" 2112 proj.Spec.SourceNamespaces = []string{test.FakeArgoCDNamespace} 2113 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &proj}}, nil) 2114 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 2115 receivedPatch := map[string]any{} 2116 func() { 2117 fakeAppCs.Lock() 2118 defer fakeAppCs.Unlock() 2119 fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 2120 if patchAction, ok := action.(kubetesting.PatchAction); ok { 2121 require.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch)) 2122 } 2123 return true, &v1alpha1.Application{}, nil 2124 }) 2125 }() 2126 2127 ctrl.processRequestedAppOperation(app) 2128 2129 phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase") 2130 message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message") 2131 assert.Equal(t, string(synccommon.OperationError), phase) 2132 assert.Contains(t, message, "application destination can't have both name and server defined: another-cluster https://localhost:6443") 2133 } 2134 2135 func TestProcessRequestedAppOperation_FailedHasRetries(t *testing.T) { 2136 app := newFakeApp() 2137 app.Spec.Project = "invalid-project" 2138 app.Operation = &v1alpha1.Operation{ 2139 Sync: &v1alpha1.SyncOperation{}, 2140 Retry: v1alpha1.RetryStrategy{Limit: 1}, 2141 } 2142 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 2143 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 2144 receivedPatch := map[string]any{} 2145 fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 2146 if patchAction, ok := action.(kubetesting.PatchAction); ok { 2147 require.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch)) 2148 } 2149 return true, &v1alpha1.Application{}, nil 2150 }) 2151 2152 ctrl.processRequestedAppOperation(app) 2153 2154 phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase") 2155 message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message") 2156 retryCount, _, _ := unstructured.NestedFloat64(receivedPatch, "status", "operationState", "retryCount") 2157 assert.Equal(t, string(synccommon.OperationRunning), phase) 2158 assert.Contains(t, message, "Failed to load application project: error getting app project \"invalid-project\": appproject.argoproj.io \"invalid-project\" not found. Retrying attempt #1") 2159 assert.InEpsilon(t, float64(1), retryCount, 0.0001) 2160 } 2161 2162 func TestProcessRequestedAppOperation_RunningPreviouslyFailed(t *testing.T) { 2163 failedAttemptFinisedAt := time.Now().Add(-time.Minute * 5) 2164 app := newFakeApp() 2165 app.Operation = &v1alpha1.Operation{ 2166 Sync: &v1alpha1.SyncOperation{}, 2167 Retry: v1alpha1.RetryStrategy{Limit: 1}, 2168 } 2169 app.Status.OperationState.Operation = *app.Operation 2170 app.Status.OperationState.Phase = synccommon.OperationRunning 2171 app.Status.OperationState.RetryCount = 1 2172 app.Status.OperationState.FinishedAt = &metav1.Time{Time: failedAttemptFinisedAt} 2173 app.Status.OperationState.SyncResult.Resources = []*v1alpha1.ResourceResult{{ 2174 Name: "guestbook", 2175 Kind: "Deployment", 2176 Group: "apps", 2177 Status: synccommon.ResultCodeSyncFailed, 2178 }} 2179 2180 data := &fakeData{ 2181 apps: []runtime.Object{app, &defaultProj}, 2182 manifestResponse: &apiclient.ManifestResponse{ 2183 Manifests: []string{}, 2184 Namespace: test.FakeDestNamespace, 2185 Server: test.FakeClusterURL, 2186 Revision: "abc123", 2187 }, 2188 } 2189 ctrl := newFakeController(data, nil) 2190 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 2191 receivedPatch := map[string]any{} 2192 fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 2193 if patchAction, ok := action.(kubetesting.PatchAction); ok { 2194 require.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch)) 2195 } 2196 return true, &v1alpha1.Application{}, nil 2197 }) 2198 2199 ctrl.processRequestedAppOperation(app) 2200 2201 phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase") 2202 message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message") 2203 finishedAtStr, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "finishedAt") 2204 finishedAt, err := time.Parse(time.RFC3339, finishedAtStr) 2205 require.NoError(t, err) 2206 assert.Equal(t, string(synccommon.OperationSucceeded), phase) 2207 assert.Equal(t, "successfully synced (no more tasks)", message) 2208 assert.Truef(t, finishedAt.After(failedAttemptFinisedAt), "finishedAt was expected to be updated. The retry was not performed.") 2209 } 2210 2211 func TestProcessRequestedAppOperation_RunningPreviouslyFailedBackoff(t *testing.T) { 2212 failedAttemptFinisedAt := time.Now().Add(-time.Second) 2213 app := newFakeApp() 2214 app.Operation = &v1alpha1.Operation{ 2215 Sync: &v1alpha1.SyncOperation{}, 2216 Retry: v1alpha1.RetryStrategy{ 2217 Limit: 1, 2218 Backoff: &v1alpha1.Backoff{ 2219 Duration: "1h", 2220 Factor: ptr.To(int64(100)), 2221 MaxDuration: "1h", 2222 }, 2223 }, 2224 } 2225 app.Status.OperationState.Operation = *app.Operation 2226 app.Status.OperationState.Phase = synccommon.OperationRunning 2227 app.Status.OperationState.Message = "pending retry" 2228 app.Status.OperationState.RetryCount = 1 2229 app.Status.OperationState.FinishedAt = &metav1.Time{Time: failedAttemptFinisedAt} 2230 app.Status.OperationState.SyncResult.Resources = []*v1alpha1.ResourceResult{{ 2231 Name: "guestbook", 2232 Kind: "Deployment", 2233 Group: "apps", 2234 Status: synccommon.ResultCodeSyncFailed, 2235 }} 2236 2237 data := &fakeData{ 2238 apps: []runtime.Object{app, &defaultProj}, 2239 manifestResponse: &apiclient.ManifestResponse{ 2240 Manifests: []string{}, 2241 Namespace: test.FakeDestNamespace, 2242 Server: test.FakeClusterURL, 2243 Revision: "abc123", 2244 }, 2245 } 2246 ctrl := newFakeController(data, nil) 2247 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 2248 fakeAppCs.PrependReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) { 2249 require.FailNow(t, "A patch should not have been called if the backoff has not passed") 2250 return true, &v1alpha1.Application{}, nil 2251 }) 2252 2253 ctrl.processRequestedAppOperation(app) 2254 } 2255 2256 func TestProcessRequestedAppOperation_HasRetriesTerminated(t *testing.T) { 2257 app := newFakeApp() 2258 app.Operation = &v1alpha1.Operation{ 2259 Sync: &v1alpha1.SyncOperation{}, 2260 Retry: v1alpha1.RetryStrategy{Limit: 10}, 2261 } 2262 app.Status.OperationState.Operation = *app.Operation 2263 app.Status.OperationState.Phase = synccommon.OperationTerminating 2264 2265 data := &fakeData{ 2266 apps: []runtime.Object{app, &defaultProj}, 2267 manifestResponse: &apiclient.ManifestResponse{ 2268 Manifests: []string{}, 2269 Namespace: test.FakeDestNamespace, 2270 Server: test.FakeClusterURL, 2271 Revision: "abc123", 2272 }, 2273 } 2274 ctrl := newFakeController(data, nil) 2275 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 2276 receivedPatch := map[string]any{} 2277 fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 2278 if patchAction, ok := action.(kubetesting.PatchAction); ok { 2279 require.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch)) 2280 } 2281 return true, &v1alpha1.Application{}, nil 2282 }) 2283 2284 ctrl.processRequestedAppOperation(app) 2285 2286 phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase") 2287 message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message") 2288 assert.Equal(t, string(synccommon.OperationFailed), phase) 2289 assert.Equal(t, "Operation terminated", message) 2290 } 2291 2292 func TestProcessRequestedAppOperation_Successful(t *testing.T) { 2293 app := newFakeApp() 2294 app.Spec.Project = "default" 2295 app.Operation = &v1alpha1.Operation{ 2296 Sync: &v1alpha1.SyncOperation{}, 2297 } 2298 ctrl := newFakeController(&fakeData{ 2299 apps: []runtime.Object{app, &defaultProj}, 2300 manifestResponses: []*apiclient.ManifestResponse{{ 2301 Manifests: []string{}, 2302 }}, 2303 }, nil) 2304 fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) 2305 receivedPatch := map[string]any{} 2306 fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 2307 if patchAction, ok := action.(kubetesting.PatchAction); ok { 2308 require.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch)) 2309 } 2310 return true, &v1alpha1.Application{}, nil 2311 }) 2312 2313 ctrl.processRequestedAppOperation(app) 2314 2315 phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase") 2316 message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message") 2317 assert.Equal(t, string(synccommon.OperationSucceeded), phase) 2318 assert.Equal(t, "successfully synced (no more tasks)", message) 2319 ok, level := ctrl.isRefreshRequested(ctrl.toAppKey(app.Name)) 2320 assert.True(t, ok) 2321 assert.Equal(t, CompareWithLatestForceResolve, level) 2322 } 2323 2324 func TestProcessRequestedAppOperation_SyncTimeout(t *testing.T) { 2325 testCases := []struct { 2326 name string 2327 startedSince time.Duration 2328 syncTimeout time.Duration 2329 retryAttempt int 2330 currentPhase synccommon.OperationPhase 2331 expectedPhase synccommon.OperationPhase 2332 expectedMessage string 2333 }{{ 2334 name: "Continue when running operation has not exceeded timeout", 2335 syncTimeout: time.Minute, 2336 startedSince: 30 * time.Second, 2337 currentPhase: synccommon.OperationRunning, 2338 expectedPhase: synccommon.OperationSucceeded, 2339 expectedMessage: "successfully synced (no more tasks)", 2340 }, { 2341 name: "Continue when terminating operation has exceeded timeout", 2342 syncTimeout: time.Minute, 2343 startedSince: 2 * time.Minute, 2344 currentPhase: synccommon.OperationTerminating, 2345 expectedPhase: synccommon.OperationFailed, 2346 expectedMessage: "Operation terminated", 2347 }, { 2348 name: "Terminate when running operation exceeded timeout", 2349 syncTimeout: time.Minute, 2350 startedSince: 2 * time.Minute, 2351 currentPhase: synccommon.OperationRunning, 2352 expectedPhase: synccommon.OperationFailed, 2353 expectedMessage: "Operation terminated, triggered by controller sync timeout", 2354 }, { 2355 name: "Terminate when retried operation exceeded timeout", 2356 syncTimeout: time.Minute, 2357 startedSince: 15 * time.Minute, 2358 currentPhase: synccommon.OperationRunning, 2359 retryAttempt: 1, 2360 expectedPhase: synccommon.OperationFailed, 2361 expectedMessage: "Operation terminated, triggered by controller sync timeout (retried 1 times).", 2362 }} 2363 for i := range testCases { 2364 tc := testCases[i] 2365 t.Run(fmt.Sprintf("case %d: %s", i, tc.name), func(t *testing.T) { 2366 app := newFakeApp() 2367 app.Spec.Project = "default" 2368 app.Operation = &v1alpha1.Operation{ 2369 Sync: &v1alpha1.SyncOperation{ 2370 Revision: "HEAD", 2371 }, 2372 } 2373 ctrl := newFakeController(&fakeData{ 2374 apps: []runtime.Object{app, &defaultProj}, 2375 manifestResponses: []*apiclient.ManifestResponse{{ 2376 Manifests: []string{}, 2377 }}, 2378 }, nil) 2379 2380 ctrl.syncTimeout = tc.syncTimeout 2381 app.Status.OperationState = &v1alpha1.OperationState{ 2382 Operation: *app.Operation, 2383 Phase: tc.currentPhase, 2384 StartedAt: metav1.NewTime(time.Now().Add(-tc.startedSince)), 2385 } 2386 if tc.retryAttempt > 0 { 2387 app.Status.OperationState.FinishedAt = ptr.To(metav1.NewTime(time.Now().Add(-tc.startedSince))) 2388 app.Status.OperationState.RetryCount = int64(tc.retryAttempt) 2389 } 2390 2391 ctrl.processRequestedAppOperation(app) 2392 2393 app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.ObjectMeta.Namespace).Get(t.Context(), app.Name, metav1.GetOptions{}) 2394 require.NoError(t, err) 2395 assert.Equal(t, tc.expectedPhase, app.Status.OperationState.Phase) 2396 assert.Equal(t, tc.expectedMessage, app.Status.OperationState.Message) 2397 }) 2398 } 2399 } 2400 2401 func TestGetAppHosts(t *testing.T) { 2402 app := newFakeApp() 2403 data := &fakeData{ 2404 apps: []runtime.Object{app, &defaultProj}, 2405 manifestResponse: &apiclient.ManifestResponse{ 2406 Manifests: []string{}, 2407 Namespace: test.FakeDestNamespace, 2408 Server: test.FakeClusterURL, 2409 Revision: "abc123", 2410 }, 2411 configMapData: map[string]string{ 2412 "application.allowedNodeLabels": "label1,label2", 2413 }, 2414 } 2415 ctrl := newFakeController(data, nil) 2416 mockStateCache := &mockstatecache.LiveStateCache{} 2417 mockStateCache.On("IterateResources", mock.Anything, mock.MatchedBy(func(callback func(res *clustercache.Resource, info *statecache.ResourceInfo)) bool { 2418 // node resource 2419 callback(&clustercache.Resource{ 2420 Ref: corev1.ObjectReference{Name: "minikube", Kind: "Node", APIVersion: "v1"}, 2421 }, &statecache.ResourceInfo{NodeInfo: &statecache.NodeInfo{ 2422 Name: "minikube", 2423 SystemInfo: corev1.NodeSystemInfo{OSImage: "debian"}, 2424 Capacity: map[corev1.ResourceName]resource.Quantity{corev1.ResourceCPU: resource.MustParse("5")}, 2425 Labels: map[string]string{"label1": "value1", "label2": "value2"}, 2426 }}) 2427 2428 // app pod 2429 callback(&clustercache.Resource{ 2430 Ref: corev1.ObjectReference{Name: "pod1", Kind: kube.PodKind, APIVersion: "v1", Namespace: "default"}, 2431 }, &statecache.ResourceInfo{PodInfo: &statecache.PodInfo{ 2432 NodeName: "minikube", 2433 ResourceRequests: map[corev1.ResourceName]resource.Quantity{corev1.ResourceCPU: resource.MustParse("1")}, 2434 }}) 2435 // neighbor pod 2436 callback(&clustercache.Resource{ 2437 Ref: corev1.ObjectReference{Name: "pod2", Kind: kube.PodKind, APIVersion: "v1", Namespace: "default"}, 2438 }, &statecache.ResourceInfo{PodInfo: &statecache.PodInfo{ 2439 NodeName: "minikube", 2440 ResourceRequests: map[corev1.ResourceName]resource.Quantity{corev1.ResourceCPU: resource.MustParse("2")}, 2441 }}) 2442 return true 2443 })).Return(nil) 2444 ctrl.stateCache = mockStateCache 2445 2446 hosts, err := ctrl.getAppHosts(&v1alpha1.Cluster{Server: "test", Name: "test"}, app, []v1alpha1.ResourceNode{{ 2447 ResourceRef: v1alpha1.ResourceRef{Name: "pod1", Namespace: "default", Kind: kube.PodKind}, 2448 Info: []v1alpha1.InfoItem{{ 2449 Name: "Host", 2450 Value: "Minikube", 2451 }}, 2452 }}) 2453 2454 require.NoError(t, err) 2455 assert.Equal(t, []v1alpha1.HostInfo{{ 2456 Name: "minikube", 2457 SystemInfo: corev1.NodeSystemInfo{OSImage: "debian"}, 2458 ResourcesInfo: []v1alpha1.HostResourceInfo{ 2459 { 2460 ResourceName: corev1.ResourceCPU, Capacity: 5000, RequestedByApp: 1000, RequestedByNeighbors: 2000, 2461 }, 2462 }, 2463 Labels: map[string]string{"label1": "value1", "label2": "value2"}, 2464 }}, hosts) 2465 } 2466 2467 func TestMetricsExpiration(t *testing.T) { 2468 app := newFakeApp() 2469 // Check expiration is disabled by default 2470 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 2471 assert.False(t, ctrl.metricsServer.HasExpiration()) 2472 // Check expiration is enabled if set 2473 ctrl = newFakeController(&fakeData{apps: []runtime.Object{app}, metricsCacheExpiration: 10 * time.Second}, nil) 2474 assert.True(t, ctrl.metricsServer.HasExpiration()) 2475 } 2476 2477 func TestToAppKey(t *testing.T) { 2478 ctrl := newFakeController(&fakeData{}, nil) 2479 tests := []struct { 2480 name string 2481 input string 2482 expected string 2483 }{ 2484 {"From instance name", "foo_bar", "foo/bar"}, 2485 {"From qualified name", "foo/bar", "foo/bar"}, 2486 {"From unqualified name", "bar", ctrl.namespace + "/bar"}, 2487 } 2488 2489 for _, tt := range tests { 2490 t.Run(tt.name, func(t *testing.T) { 2491 assert.Equal(t, tt.expected, ctrl.toAppKey(tt.input)) 2492 }) 2493 } 2494 } 2495 2496 func Test_canProcessApp(t *testing.T) { 2497 app := newFakeApp() 2498 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 2499 ctrl.applicationNamespaces = []string{"good"} 2500 t.Run("without cluster filter, good namespace", func(t *testing.T) { 2501 app.Namespace = "good" 2502 canProcess := ctrl.canProcessApp(app) 2503 assert.True(t, canProcess) 2504 }) 2505 t.Run("without cluster filter, bad namespace", func(t *testing.T) { 2506 app.Namespace = "bad" 2507 canProcess := ctrl.canProcessApp(app) 2508 assert.False(t, canProcess) 2509 }) 2510 t.Run("with cluster filter, good namespace", func(t *testing.T) { 2511 app.Namespace = "good" 2512 canProcess := ctrl.canProcessApp(app) 2513 assert.True(t, canProcess) 2514 }) 2515 t.Run("with cluster filter, bad namespace", func(t *testing.T) { 2516 app.Namespace = "bad" 2517 canProcess := ctrl.canProcessApp(app) 2518 assert.False(t, canProcess) 2519 }) 2520 } 2521 2522 func Test_canProcessAppSkipReconcileAnnotation(t *testing.T) { 2523 appSkipReconcileInvalid := newFakeApp() 2524 appSkipReconcileInvalid.Annotations = map[string]string{common.AnnotationKeyAppSkipReconcile: "invalid-value"} 2525 appSkipReconcileFalse := newFakeApp() 2526 appSkipReconcileFalse.Annotations = map[string]string{common.AnnotationKeyAppSkipReconcile: "false"} 2527 appSkipReconcileTrue := newFakeApp() 2528 appSkipReconcileTrue.Annotations = map[string]string{common.AnnotationKeyAppSkipReconcile: "true"} 2529 ctrl := newFakeController(&fakeData{}, nil) 2530 tests := []struct { 2531 name string 2532 input any 2533 expected bool 2534 }{ 2535 {"No skip reconcile annotation", newFakeApp(), true}, 2536 {"Contains skip reconcile annotation ", appSkipReconcileInvalid, true}, 2537 {"Contains skip reconcile annotation value false", appSkipReconcileFalse, true}, 2538 {"Contains skip reconcile annotation value true", appSkipReconcileTrue, false}, 2539 } 2540 2541 for _, tt := range tests { 2542 t.Run(tt.name, func(t *testing.T) { 2543 assert.Equal(t, tt.expected, ctrl.canProcessApp(tt.input)) 2544 }) 2545 } 2546 } 2547 2548 func Test_syncDeleteOption(t *testing.T) { 2549 app := newFakeApp() 2550 ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) 2551 cm := newFakeCM() 2552 t.Run("without delete option object is deleted", func(t *testing.T) { 2553 cmObj := kube.MustToUnstructured(&cm) 2554 assert.True(t, ctrl.shouldBeDeleted(app, cmObj)) 2555 }) 2556 t.Run("with delete set to false object is retained", func(t *testing.T) { 2557 cmObj := kube.MustToUnstructured(&cm) 2558 cmObj.SetAnnotations(map[string]string{"argocd.argoproj.io/sync-options": "Delete=false"}) 2559 assert.False(t, ctrl.shouldBeDeleted(app, cmObj)) 2560 }) 2561 t.Run("with delete set to false object is retained", func(t *testing.T) { 2562 cmObj := kube.MustToUnstructured(&cm) 2563 cmObj.SetAnnotations(map[string]string{"helm.sh/resource-policy": "keep"}) 2564 assert.False(t, ctrl.shouldBeDeleted(app, cmObj)) 2565 }) 2566 } 2567 2568 func TestAddControllerNamespace(t *testing.T) { 2569 t.Run("set controllerNamespace when the app is in the controller namespace", func(t *testing.T) { 2570 app := newFakeApp() 2571 ctrl := newFakeController(&fakeData{ 2572 apps: []runtime.Object{app, &defaultProj}, 2573 manifestResponse: &apiclient.ManifestResponse{}, 2574 }, nil) 2575 2576 ctrl.processAppRefreshQueueItem() 2577 2578 updatedApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(ctrl.namespace).Get(t.Context(), app.Name, metav1.GetOptions{}) 2579 require.NoError(t, err) 2580 assert.Equal(t, test.FakeArgoCDNamespace, updatedApp.Status.ControllerNamespace) 2581 }) 2582 t.Run("set controllerNamespace when the app is in another namespace than the controller", func(t *testing.T) { 2583 appNamespace := "app-namespace" 2584 2585 app := newFakeApp() 2586 app.Namespace = appNamespace 2587 proj := defaultProj 2588 proj.Spec.SourceNamespaces = []string{appNamespace} 2589 ctrl := newFakeController(&fakeData{ 2590 apps: []runtime.Object{app, &proj}, 2591 manifestResponse: &apiclient.ManifestResponse{}, 2592 applicationNamespaces: []string{appNamespace}, 2593 }, nil) 2594 2595 ctrl.processAppRefreshQueueItem() 2596 2597 updatedApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(appNamespace).Get(t.Context(), app.Name, metav1.GetOptions{}) 2598 require.NoError(t, err) 2599 assert.Equal(t, test.FakeArgoCDNamespace, updatedApp.Status.ControllerNamespace) 2600 }) 2601 } 2602 2603 func TestHelmValuesObjectHasReplaceStrategy(t *testing.T) { 2604 app := v1alpha1.Application{ 2605 Status: v1alpha1.ApplicationStatus{Sync: v1alpha1.SyncStatus{ComparedTo: v1alpha1.ComparedTo{ 2606 Source: v1alpha1.ApplicationSource{ 2607 Helm: &v1alpha1.ApplicationSourceHelm{ 2608 ValuesObject: &runtime.RawExtension{ 2609 Object: &unstructured.Unstructured{Object: map[string]any{"key": []string{"value"}}}, 2610 }, 2611 }, 2612 }, 2613 }}}, 2614 } 2615 2616 appModified := v1alpha1.Application{ 2617 Status: v1alpha1.ApplicationStatus{Sync: v1alpha1.SyncStatus{ComparedTo: v1alpha1.ComparedTo{ 2618 Source: v1alpha1.ApplicationSource{ 2619 Helm: &v1alpha1.ApplicationSourceHelm{ 2620 ValuesObject: &runtime.RawExtension{ 2621 Object: &unstructured.Unstructured{Object: map[string]any{"key": []string{"value-modified1"}}}, 2622 }, 2623 }, 2624 }, 2625 }}}, 2626 } 2627 2628 patch, _, err := createMergePatch( 2629 app, 2630 appModified) 2631 require.NoError(t, err) 2632 assert.JSONEq(t, `{"status":{"sync":{"comparedTo":{"source":{"helm":{"valuesObject":{"key":["value-modified1"]}}}}}}}`, string(patch)) 2633 } 2634 2635 func TestAppStatusIsReplaced(t *testing.T) { 2636 original := &v1alpha1.ApplicationStatus{Sync: v1alpha1.SyncStatus{ 2637 ComparedTo: v1alpha1.ComparedTo{ 2638 Destination: v1alpha1.ApplicationDestination{ 2639 Server: "https://mycluster", 2640 }, 2641 }, 2642 }} 2643 2644 updated := &v1alpha1.ApplicationStatus{Sync: v1alpha1.SyncStatus{ 2645 ComparedTo: v1alpha1.ComparedTo{ 2646 Destination: v1alpha1.ApplicationDestination{ 2647 Name: "mycluster", 2648 }, 2649 }, 2650 }} 2651 2652 patchData, ok, err := createMergePatch(original, updated) 2653 2654 require.NoError(t, err) 2655 require.True(t, ok) 2656 patchObj := map[string]any{} 2657 require.NoError(t, json.Unmarshal(patchData, &patchObj)) 2658 2659 val, has, err := unstructured.NestedFieldNoCopy(patchObj, "sync", "comparedTo", "destination", "server") 2660 require.NoError(t, err) 2661 require.True(t, has) 2662 require.Nil(t, val) 2663 } 2664 2665 func TestAlreadyAttemptSync(t *testing.T) { 2666 app := newFakeApp() 2667 defaultRevision := app.Status.OperationState.SyncResult.Revision 2668 2669 t.Run("no operation state", func(t *testing.T) { 2670 app := app.DeepCopy() 2671 app.Status.OperationState = nil 2672 attempted, _, _ := alreadyAttemptedSync(app, []string{defaultRevision}, true) 2673 assert.False(t, attempted) 2674 }) 2675 2676 t.Run("no sync result for running sync", func(t *testing.T) { 2677 app := app.DeepCopy() 2678 app.Status.OperationState.SyncResult = nil 2679 app.Status.OperationState.Phase = synccommon.OperationRunning 2680 attempted, _, _ := alreadyAttemptedSync(app, []string{defaultRevision}, true) 2681 assert.False(t, attempted) 2682 }) 2683 2684 t.Run("no sync result for completed sync", func(t *testing.T) { 2685 app := app.DeepCopy() 2686 app.Status.OperationState.SyncResult = nil 2687 app.Status.OperationState.Phase = synccommon.OperationError 2688 attempted, _, _ := alreadyAttemptedSync(app, []string{defaultRevision}, true) 2689 assert.True(t, attempted) 2690 }) 2691 2692 t.Run("single source", func(t *testing.T) { 2693 t.Run("no revision", func(t *testing.T) { 2694 attempted, _, _ := alreadyAttemptedSync(app, []string{}, true) 2695 assert.False(t, attempted) 2696 }) 2697 2698 t.Run("empty revision", func(t *testing.T) { 2699 attempted, _, _ := alreadyAttemptedSync(app, []string{""}, true) 2700 assert.False(t, attempted) 2701 }) 2702 2703 t.Run("too many revision", func(t *testing.T) { 2704 app := app.DeepCopy() 2705 app.Status.OperationState.SyncResult.Revision = "sha" 2706 attempted, _, _ := alreadyAttemptedSync(app, []string{"sha", "sha2"}, true) 2707 assert.False(t, attempted) 2708 }) 2709 2710 t.Run("same manifest, same SHA with changes", func(t *testing.T) { 2711 app := app.DeepCopy() 2712 app.Status.OperationState.SyncResult.Revision = "sha" 2713 attempted, _, _ := alreadyAttemptedSync(app, []string{"sha"}, true) 2714 assert.True(t, attempted) 2715 }) 2716 2717 t.Run("same manifest, different SHA with changes", func(t *testing.T) { 2718 app := app.DeepCopy() 2719 app.Status.OperationState.SyncResult.Revision = "sha1" 2720 attempted, _, _ := alreadyAttemptedSync(app, []string{"sha2"}, true) 2721 assert.False(t, attempted) 2722 }) 2723 2724 t.Run("same manifest, different SHA without changes", func(t *testing.T) { 2725 app := app.DeepCopy() 2726 app.Status.OperationState.SyncResult.Revision = "sha1" 2727 attempted, _, _ := alreadyAttemptedSync(app, []string{"sha2"}, false) 2728 assert.True(t, attempted) 2729 }) 2730 2731 t.Run("different manifest, same SHA with changes", func(t *testing.T) { 2732 // This test represents the case where the user changed a source's target revision to a new branch, but it 2733 // points to the same revision as the old branch. We currently do not consider this as having been "already 2734 // attempted." In the future we may want to short-circuit the auto-sync in these cases. 2735 app := app.DeepCopy() 2736 app.Status.OperationState.SyncResult.Source = v1alpha1.ApplicationSource{TargetRevision: "branch1"} 2737 app.Spec.Source = &v1alpha1.ApplicationSource{TargetRevision: "branch2"} 2738 app.Status.OperationState.SyncResult.Revision = "sha" 2739 attempted, _, _ := alreadyAttemptedSync(app, []string{"sha"}, true) 2740 assert.False(t, attempted) 2741 }) 2742 2743 t.Run("different manifest, different SHA with changes", func(t *testing.T) { 2744 app := app.DeepCopy() 2745 app.Status.OperationState.SyncResult.Source = v1alpha1.ApplicationSource{Path: "folder1"} 2746 app.Spec.Source = &v1alpha1.ApplicationSource{Path: "folder2"} 2747 app.Status.OperationState.SyncResult.Revision = "sha1" 2748 attempted, _, _ := alreadyAttemptedSync(app, []string{"sha2"}, true) 2749 assert.False(t, attempted) 2750 }) 2751 2752 t.Run("different manifest, different SHA without changes", func(t *testing.T) { 2753 app := app.DeepCopy() 2754 app.Status.OperationState.SyncResult.Source = v1alpha1.ApplicationSource{Path: "folder1"} 2755 app.Spec.Source = &v1alpha1.ApplicationSource{Path: "folder2"} 2756 app.Status.OperationState.SyncResult.Revision = "sha1" 2757 attempted, _, _ := alreadyAttemptedSync(app, []string{"sha2"}, false) 2758 assert.False(t, attempted) 2759 }) 2760 2761 t.Run("different manifest, same SHA without changes", func(t *testing.T) { 2762 app := app.DeepCopy() 2763 app.Status.OperationState.SyncResult.Source = v1alpha1.ApplicationSource{Path: "folder1"} 2764 app.Spec.Source = &v1alpha1.ApplicationSource{Path: "folder2"} 2765 app.Status.OperationState.SyncResult.Revision = "sha" 2766 attempted, _, _ := alreadyAttemptedSync(app, []string{"sha"}, false) 2767 assert.False(t, attempted) 2768 }) 2769 }) 2770 2771 t.Run("multi-source", func(t *testing.T) { 2772 app := app.DeepCopy() 2773 app.Status.OperationState.SyncResult.Sources = []v1alpha1.ApplicationSource{{Path: "folder1"}, {Path: "folder2"}} 2774 app.Spec.Sources = []v1alpha1.ApplicationSource{{Path: "folder1"}, {Path: "folder2"}} 2775 2776 t.Run("same manifest, same SHAs with changes", func(t *testing.T) { 2777 app := app.DeepCopy() 2778 app.Status.OperationState.SyncResult.Revisions = []string{"sha_a", "sha_b"} 2779 attempted, _, _ := alreadyAttemptedSync(app, []string{"sha_a", "sha_b"}, true) 2780 assert.True(t, attempted) 2781 }) 2782 2783 t.Run("same manifest, different SHAs with changes", func(t *testing.T) { 2784 app := app.DeepCopy() 2785 app.Status.OperationState.SyncResult.Revisions = []string{"sha_a_=", "sha_b_1"} 2786 attempted, _, _ := alreadyAttemptedSync(app, []string{"sha_a_2", "sha_b_2"}, true) 2787 assert.False(t, attempted) 2788 }) 2789 2790 t.Run("same manifest, different SHA without changes", func(t *testing.T) { 2791 app := app.DeepCopy() 2792 app.Status.OperationState.SyncResult.Revisions = []string{"sha_a_=", "sha_b_1"} 2793 attempted, _, _ := alreadyAttemptedSync(app, []string{"sha_a_2", "sha_b_2"}, false) 2794 assert.True(t, attempted) 2795 }) 2796 2797 t.Run("different manifest, same SHA with changes", func(t *testing.T) { 2798 // This test represents the case where the user changed a source's target revision to a new branch, but it 2799 // points to the same revision as the old branch. We currently do not consider this as having been "already 2800 // attempted." In the future we may want to short-circuit the auto-sync in these cases. 2801 app := app.DeepCopy() 2802 app.Status.OperationState.SyncResult.Sources = []v1alpha1.ApplicationSource{{TargetRevision: "branch1"}, {TargetRevision: "branch2"}} 2803 app.Spec.Sources = []v1alpha1.ApplicationSource{{TargetRevision: "branch1"}, {TargetRevision: "branch3"}} 2804 app.Status.OperationState.SyncResult.Revisions = []string{"sha_a_2", "sha_b_2"} 2805 attempted, _, _ := alreadyAttemptedSync(app, []string{"sha_a_2", "sha_b_2"}, false) 2806 assert.False(t, attempted) 2807 }) 2808 2809 t.Run("different manifest, different SHA with changes", func(t *testing.T) { 2810 app := app.DeepCopy() 2811 app.Status.OperationState.SyncResult.Sources = []v1alpha1.ApplicationSource{{Path: "folder1"}, {Path: "folder2"}} 2812 app.Spec.Sources = []v1alpha1.ApplicationSource{{Path: "folder1"}, {Path: "folder3"}} 2813 app.Status.OperationState.SyncResult.Revisions = []string{"sha_a", "sha_b"} 2814 attempted, _, _ := alreadyAttemptedSync(app, []string{"sha_a", "sha_b_2"}, true) 2815 assert.False(t, attempted) 2816 }) 2817 2818 t.Run("different manifest, different SHA without changes", func(t *testing.T) { 2819 app := app.DeepCopy() 2820 app.Status.OperationState.SyncResult.Sources = []v1alpha1.ApplicationSource{{Path: "folder1"}, {Path: "folder2"}} 2821 app.Spec.Sources = []v1alpha1.ApplicationSource{{Path: "folder1"}, {Path: "folder3"}} 2822 app.Status.OperationState.SyncResult.Revisions = []string{"sha_a", "sha_b"} 2823 attempted, _, _ := alreadyAttemptedSync(app, []string{"sha_a", "sha_b_2"}, false) 2824 assert.False(t, attempted) 2825 }) 2826 2827 t.Run("different manifest, same SHA without changes", func(t *testing.T) { 2828 app := app.DeepCopy() 2829 app.Status.OperationState.SyncResult.Sources = []v1alpha1.ApplicationSource{{Path: "folder1"}, {Path: "folder2"}} 2830 app.Spec.Sources = []v1alpha1.ApplicationSource{{Path: "folder1"}, {Path: "folder3"}} 2831 app.Status.OperationState.SyncResult.Revisions = []string{"sha_a", "sha_b"} 2832 attempted, _, _ := alreadyAttemptedSync(app, []string{"sha_a", "sha_b"}, false) 2833 assert.False(t, attempted) 2834 }) 2835 }) 2836 } 2837 2838 func assertDurationAround(t *testing.T, expected time.Duration, actual time.Duration) { 2839 t.Helper() 2840 delta := time.Second / 2 2841 assert.GreaterOrEqual(t, expected, actual-delta) 2842 assert.LessOrEqual(t, expected, actual+delta) 2843 } 2844 2845 func TestSelfHealRemainingBackoff(t *testing.T) { 2846 ctrl := newFakeController(&fakeData{}, nil) 2847 ctrl.selfHealBackoff = &wait.Backoff{ 2848 Factor: 3, 2849 Duration: 2 * time.Second, 2850 Cap: 2 * time.Minute, 2851 } 2852 app := &v1alpha1.Application{ 2853 Status: v1alpha1.ApplicationStatus{ 2854 OperationState: &v1alpha1.OperationState{ 2855 Operation: v1alpha1.Operation{ 2856 Sync: &v1alpha1.SyncOperation{}, 2857 }, 2858 }, 2859 }, 2860 } 2861 2862 testCases := []struct { 2863 attempts int 2864 finishedAt *metav1.Time 2865 expectedDuration time.Duration 2866 shouldSelfHeal bool 2867 }{{ 2868 attempts: 0, 2869 finishedAt: ptr.To(metav1.Now()), 2870 expectedDuration: 0, 2871 shouldSelfHeal: true, 2872 }, { 2873 attempts: 1, 2874 finishedAt: ptr.To(metav1.Now()), 2875 expectedDuration: 2 * time.Second, 2876 shouldSelfHeal: false, 2877 }, { 2878 attempts: 2, 2879 finishedAt: ptr.To(metav1.Now()), 2880 expectedDuration: 6 * time.Second, 2881 shouldSelfHeal: false, 2882 }, { 2883 attempts: 3, 2884 finishedAt: nil, 2885 expectedDuration: 18 * time.Second, 2886 shouldSelfHeal: false, 2887 }, { 2888 attempts: 4, 2889 finishedAt: nil, 2890 expectedDuration: 54 * time.Second, 2891 shouldSelfHeal: false, 2892 }, { 2893 attempts: 5, 2894 finishedAt: nil, 2895 expectedDuration: 120 * time.Second, 2896 shouldSelfHeal: false, 2897 }, { 2898 attempts: 6, 2899 finishedAt: nil, 2900 expectedDuration: 120 * time.Second, 2901 shouldSelfHeal: false, 2902 }, { 2903 attempts: 6, 2904 finishedAt: ptr.To(metav1.Now()), 2905 expectedDuration: 120 * time.Second, 2906 shouldSelfHeal: false, 2907 }, { 2908 attempts: 40, 2909 finishedAt: &metav1.Time{Time: time.Now().Add(-1 * time.Minute)}, 2910 expectedDuration: 60 * time.Second, 2911 shouldSelfHeal: false, 2912 }} 2913 2914 for i := range testCases { 2915 tc := testCases[i] 2916 t.Run(fmt.Sprintf("test case %d", i), func(t *testing.T) { 2917 app.Status.OperationState.FinishedAt = tc.finishedAt 2918 duration := ctrl.selfHealRemainingBackoff(app, tc.attempts) 2919 shouldSelfHeal := duration <= 0 2920 require.Equal(t, tc.shouldSelfHeal, shouldSelfHeal) 2921 assertDurationAround(t, tc.expectedDuration, duration) 2922 }) 2923 } 2924 } 2925 2926 func TestSelfHealBackoffCooldownElapsed(t *testing.T) { 2927 cooldown := time.Second * 30 2928 ctrl := newFakeController(&fakeData{}, nil) 2929 ctrl.selfHealBackoffCooldown = cooldown 2930 2931 app := &v1alpha1.Application{ 2932 Status: v1alpha1.ApplicationStatus{ 2933 OperationState: &v1alpha1.OperationState{ 2934 Phase: synccommon.OperationSucceeded, 2935 }, 2936 }, 2937 } 2938 2939 t.Run("operation not completed", func(t *testing.T) { 2940 app := app.DeepCopy() 2941 app.Status.OperationState.FinishedAt = nil 2942 elapsed := ctrl.selfHealBackoffCooldownElapsed(app) 2943 assert.True(t, elapsed) 2944 }) 2945 2946 t.Run("successful operation finised after cooldown", func(t *testing.T) { 2947 app := app.DeepCopy() 2948 app.Status.OperationState.FinishedAt = &metav1.Time{Time: time.Now().Add(-cooldown)} 2949 elapsed := ctrl.selfHealBackoffCooldownElapsed(app) 2950 assert.True(t, elapsed) 2951 }) 2952 2953 t.Run("unsuccessful operation finised after cooldown", func(t *testing.T) { 2954 app := app.DeepCopy() 2955 app.Status.OperationState.Phase = synccommon.OperationFailed 2956 app.Status.OperationState.FinishedAt = &metav1.Time{Time: time.Now().Add(-cooldown)} 2957 elapsed := ctrl.selfHealBackoffCooldownElapsed(app) 2958 assert.False(t, elapsed) 2959 }) 2960 2961 t.Run("successful operation finised before cooldown", func(t *testing.T) { 2962 app := app.DeepCopy() 2963 app.Status.OperationState.FinishedAt = &metav1.Time{Time: time.Now()} 2964 elapsed := ctrl.selfHealBackoffCooldownElapsed(app) 2965 assert.False(t, elapsed) 2966 }) 2967 }