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