github.com/argoproj/argo-cd/v3@v3.2.1/test/e2e/app_management_test.go (about) 1 package e2e 2 3 import ( 4 "context" 5 "fmt" 6 "reflect" 7 "testing" 8 "time" 9 10 "github.com/argoproj/gitops-engine/pkg/diff" 11 "github.com/argoproj/gitops-engine/pkg/health" 12 . "github.com/argoproj/gitops-engine/pkg/sync/common" 13 "github.com/argoproj/gitops-engine/pkg/utils/kube" 14 log "github.com/sirupsen/logrus" 15 "github.com/stretchr/testify/assert" 16 "github.com/stretchr/testify/require" 17 corev1 "k8s.io/api/core/v1" 18 networkingv1 "k8s.io/api/networking/v1" 19 rbacv1 "k8s.io/api/rbac/v1" 20 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 22 "k8s.io/apimachinery/pkg/runtime" 23 "k8s.io/apimachinery/pkg/runtime/schema" 24 "k8s.io/apimachinery/pkg/types" 25 "k8s.io/apimachinery/pkg/util/intstr" 26 "k8s.io/utils/ptr" 27 28 "github.com/argoproj/argo-cd/v3/common" 29 applicationpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/application" 30 . "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 31 "github.com/argoproj/argo-cd/v3/test/e2e/fixture" 32 accountFixture "github.com/argoproj/argo-cd/v3/test/e2e/fixture/account" 33 . "github.com/argoproj/argo-cd/v3/test/e2e/fixture/app" 34 clusterFixture "github.com/argoproj/argo-cd/v3/test/e2e/fixture/cluster" 35 projectFixture "github.com/argoproj/argo-cd/v3/test/e2e/fixture/project" 36 repoFixture "github.com/argoproj/argo-cd/v3/test/e2e/fixture/repos" 37 "github.com/argoproj/argo-cd/v3/test/e2e/testdata" 38 "github.com/argoproj/argo-cd/v3/util/argo" 39 "github.com/argoproj/argo-cd/v3/util/errors" 40 utilio "github.com/argoproj/argo-cd/v3/util/io" 41 "github.com/argoproj/argo-cd/v3/util/settings" 42 43 "github.com/argoproj/argo-cd/v3/pkg/apis/application" 44 ) 45 46 const ( 47 guestbookPath = "guestbook" 48 guestbookPathLocal = "./testdata/guestbook_local" 49 globalWithNoNameSpace = "global-with-no-namespace" 50 guestbookWithNamespace = "guestbook-with-namespace" 51 resourceActions = "resource-actions" 52 appLogsRetryCount = 5 53 ) 54 55 // This empty test is here only for clarity, to conform to logs rbac tests structure in account. This exact usecase is covered in the TestAppLogs test 56 func TestGetLogsAllowNoSwitch(_ *testing.T) { 57 } 58 59 func TestGetLogsDeny(t *testing.T) { 60 fixture.SkipOnEnv(t, "OPENSHIFT") 61 62 accountFixture.Given(t). 63 Name("test"). 64 When(). 65 Create(). 66 Login(). 67 SetPermissions([]fixture.ACL{ 68 { 69 Resource: "applications", 70 Action: "create", 71 Scope: "*", 72 }, 73 { 74 Resource: "applications", 75 Action: "get", 76 Scope: "*", 77 }, 78 { 79 Resource: "applications", 80 Action: "sync", 81 Scope: "*", 82 }, 83 { 84 Resource: "projects", 85 Action: "get", 86 Scope: "*", 87 }, 88 }, "app-creator") 89 90 GivenWithSameState(t). 91 Path("guestbook-logs"). 92 When(). 93 CreateApp(). 94 Sync(). 95 Then(). 96 Expect(HealthIs(health.HealthStatusHealthy)). 97 And(func(app *Application) { 98 _, err := fixture.RunCliWithRetry(appLogsRetryCount, "app", "logs", app.Name, "--kind", "Deployment", "--group", "", "--name", "guestbook-ui") 99 assert.ErrorContains(t, err, "permission denied") 100 }) 101 } 102 103 func TestGetLogsAllow(t *testing.T) { 104 fixture.SkipOnEnv(t, "OPENSHIFT") 105 106 accountFixture.Given(t). 107 Name("test"). 108 When(). 109 Create(). 110 Login(). 111 SetPermissions([]fixture.ACL{ 112 { 113 Resource: "applications", 114 Action: "create", 115 Scope: "*", 116 }, 117 { 118 Resource: "applications", 119 Action: "get", 120 Scope: "*", 121 }, 122 { 123 Resource: "applications", 124 Action: "sync", 125 Scope: "*", 126 }, 127 { 128 Resource: "projects", 129 Action: "get", 130 Scope: "*", 131 }, 132 { 133 Resource: "logs", 134 Action: "get", 135 Scope: "*", 136 }, 137 }, "app-creator") 138 139 GivenWithSameState(t). 140 Path("guestbook-logs"). 141 When(). 142 CreateApp(). 143 Sync(). 144 Then(). 145 Expect(HealthIs(health.HealthStatusHealthy)). 146 And(func(app *Application) { 147 out, err := fixture.RunCliWithRetry(appLogsRetryCount, "app", "logs", app.Name, "--kind", "Deployment", "--group", "", "--name", "guestbook-ui") 148 require.NoError(t, err) 149 assert.Contains(t, out, "Hi") 150 }). 151 And(func(app *Application) { 152 out, err := fixture.RunCliWithRetry(appLogsRetryCount, "app", "logs", app.Name, "--kind", "Pod") 153 require.NoError(t, err) 154 assert.Contains(t, out, "Hi") 155 }). 156 And(func(app *Application) { 157 out, err := fixture.RunCliWithRetry(appLogsRetryCount, "app", "logs", app.Name, "--kind", "Service") 158 require.NoError(t, err) 159 assert.NotContains(t, out, "Hi") 160 }) 161 } 162 163 func TestSyncToUnsignedCommit(t *testing.T) { 164 fixture.SkipOnEnv(t, "GPG") 165 Given(t). 166 Project("gpg"). 167 Path(guestbookPath). 168 When(). 169 IgnoreErrors(). 170 CreateApp(). 171 Sync(). 172 Then(). 173 Expect(OperationPhaseIs(OperationError)). 174 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 175 Expect(HealthIs(health.HealthStatusMissing)) 176 } 177 178 func TestSyncToSignedCommitWithoutKnownKey(t *testing.T) { 179 fixture.SkipOnEnv(t, "GPG") 180 Given(t). 181 Project("gpg"). 182 Path(guestbookPath). 183 When(). 184 AddSignedFile("test.yaml", "null"). 185 IgnoreErrors(). 186 CreateApp(). 187 Sync(). 188 Then(). 189 Expect(OperationPhaseIs(OperationError)). 190 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 191 Expect(HealthIs(health.HealthStatusMissing)) 192 } 193 194 func TestSyncToSignedCommitWithKnownKey(t *testing.T) { 195 fixture.SkipOnEnv(t, "GPG") 196 Given(t). 197 Project("gpg"). 198 Path(guestbookPath). 199 GPGPublicKeyAdded(). 200 Sleep(2). 201 When(). 202 AddSignedFile("test.yaml", "null"). 203 IgnoreErrors(). 204 CreateApp(). 205 Sync(). 206 Then(). 207 Expect(OperationPhaseIs(OperationSucceeded)). 208 Expect(SyncStatusIs(SyncStatusCodeSynced)). 209 Expect(HealthIs(health.HealthStatusHealthy)) 210 } 211 212 func TestSyncToSignedBranchWithKnownKey(t *testing.T) { 213 fixture.SkipOnEnv(t, "GPG") 214 Given(t). 215 Project("gpg"). 216 Path(guestbookPath). 217 Revision("master"). 218 GPGPublicKeyAdded(). 219 Sleep(2). 220 When(). 221 AddSignedFile("test.yaml", "null"). 222 IgnoreErrors(). 223 CreateApp(). 224 Sync(). 225 Then(). 226 Expect(OperationPhaseIs(OperationSucceeded)). 227 Expect(SyncStatusIs(SyncStatusCodeSynced)). 228 Expect(HealthIs(health.HealthStatusHealthy)) 229 } 230 231 func TestSyncToSignedBranchWithUnknownKey(t *testing.T) { 232 fixture.SkipOnEnv(t, "GPG") 233 Given(t). 234 Project("gpg"). 235 Path(guestbookPath). 236 Revision("master"). 237 Sleep(2). 238 When(). 239 AddSignedFile("test.yaml", "null"). 240 IgnoreErrors(). 241 CreateApp(). 242 Sync(). 243 Then(). 244 Expect(OperationPhaseIs(OperationError)). 245 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 246 Expect(HealthIs(health.HealthStatusMissing)) 247 } 248 249 func TestSyncToUnsignedBranch(t *testing.T) { 250 fixture.SkipOnEnv(t, "GPG") 251 Given(t). 252 Project("gpg"). 253 Revision("master"). 254 Path(guestbookPath). 255 GPGPublicKeyAdded(). 256 Sleep(2). 257 When(). 258 IgnoreErrors(). 259 CreateApp(). 260 Sync(). 261 Then(). 262 Expect(OperationPhaseIs(OperationError)). 263 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 264 Expect(HealthIs(health.HealthStatusMissing)) 265 } 266 267 func TestSyncToSignedTagWithKnownKey(t *testing.T) { 268 fixture.SkipOnEnv(t, "GPG") 269 Given(t). 270 Project("gpg"). 271 Revision("signed-tag"). 272 Path(guestbookPath). 273 GPGPublicKeyAdded(). 274 Sleep(2). 275 When(). 276 AddSignedTag("signed-tag"). 277 IgnoreErrors(). 278 CreateApp(). 279 Sync(). 280 Then(). 281 Expect(OperationPhaseIs(OperationSucceeded)). 282 Expect(SyncStatusIs(SyncStatusCodeSynced)). 283 Expect(HealthIs(health.HealthStatusHealthy)) 284 } 285 286 func TestSyncToSignedTagWithUnknownKey(t *testing.T) { 287 fixture.SkipOnEnv(t, "GPG") 288 Given(t). 289 Project("gpg"). 290 Revision("signed-tag"). 291 Path(guestbookPath). 292 Sleep(2). 293 When(). 294 AddSignedTag("signed-tag"). 295 IgnoreErrors(). 296 CreateApp(). 297 Sync(). 298 Then(). 299 Expect(OperationPhaseIs(OperationError)). 300 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 301 Expect(HealthIs(health.HealthStatusMissing)) 302 } 303 304 func TestSyncToUnsignedTag(t *testing.T) { 305 fixture.SkipOnEnv(t, "GPG") 306 Given(t). 307 Project("gpg"). 308 Revision("unsigned-tag"). 309 Path(guestbookPath). 310 GPGPublicKeyAdded(). 311 Sleep(2). 312 When(). 313 AddTag("unsigned-tag"). 314 IgnoreErrors(). 315 CreateApp(). 316 Sync(). 317 Then(). 318 Expect(OperationPhaseIs(OperationError)). 319 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 320 Expect(HealthIs(health.HealthStatusMissing)) 321 } 322 323 func TestAppCreation(t *testing.T) { 324 ctx := Given(t) 325 ctx. 326 Path(guestbookPath). 327 When(). 328 CreateApp(). 329 Then(). 330 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 331 And(func(app *Application) { 332 assert.Equal(t, fixture.Name(), app.Name) 333 assert.Equal(t, fixture.RepoURL(fixture.RepoURLTypeFile), app.Spec.GetSource().RepoURL) 334 assert.Equal(t, guestbookPath, app.Spec.GetSource().Path) 335 assert.Equal(t, fixture.DeploymentNamespace(), app.Spec.Destination.Namespace) 336 assert.Equal(t, KubernetesInternalAPIServerAddr, app.Spec.Destination.Server) 337 }). 338 Expect(Event(argo.EventReasonResourceCreated, "create")). 339 And(func(_ *Application) { 340 // app should be listed 341 output, err := fixture.RunCli("app", "list") 342 require.NoError(t, err) 343 assert.Contains(t, output, fixture.Name()) 344 }). 345 When(). 346 // ensure that create is idempotent 347 CreateApp(). 348 Then(). 349 Given(). 350 Revision("master"). 351 When(). 352 // ensure that update replaces spec and merge labels and annotations 353 And(func() { 354 errors.NewHandler(t).FailOnErr(fixture.AppClientset.ArgoprojV1alpha1().Applications(fixture.TestNamespace()).Patch(t.Context(), 355 ctx.GetName(), types.MergePatchType, []byte(`{"metadata": {"labels": { "test": "label" }, "annotations": { "test": "annotation" }}}`), metav1.PatchOptions{})) 356 }). 357 CreateApp("--upsert"). 358 Then(). 359 And(func(app *Application) { 360 assert.Equal(t, "label", app.Labels["test"]) 361 assert.Equal(t, "annotation", app.Annotations["test"]) 362 assert.Equal(t, "master", app.Spec.GetSource().TargetRevision) 363 }) 364 } 365 366 func TestAppCreationWithoutForceUpdate(t *testing.T) { 367 ctx := Given(t) 368 369 ctx. 370 Path(guestbookPath). 371 DestName("in-cluster"). 372 When(). 373 CreateApp(). 374 Then(). 375 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 376 And(func(app *Application) { 377 assert.Equal(t, ctx.AppName(), app.Name) 378 assert.Equal(t, fixture.RepoURL(fixture.RepoURLTypeFile), app.Spec.GetSource().RepoURL) 379 assert.Equal(t, guestbookPath, app.Spec.GetSource().Path) 380 assert.Equal(t, fixture.DeploymentNamespace(), app.Spec.Destination.Namespace) 381 assert.Equal(t, "in-cluster", app.Spec.Destination.Name) 382 }). 383 Expect(Event(argo.EventReasonResourceCreated, "create")). 384 And(func(_ *Application) { 385 // app should be listed 386 output, err := fixture.RunCli("app", "list") 387 require.NoError(t, err) 388 assert.Contains(t, output, fixture.Name()) 389 }). 390 When(). 391 IgnoreErrors(). 392 CreateApp("--dest-server", KubernetesInternalAPIServerAddr). 393 Then(). 394 Expect(Error("", "existing application spec is different, use upsert flag to force update")) 395 } 396 397 // Test designed to cover #15126. 398 // The issue occurs in the controller, when a valuesObject field that contains non-strings (eg, a nested map) gets 399 // merged/patched. 400 // Note: Failure is observed by the test timing out, because the controller cannot 'merge' the patch. 401 func TestPatchValuesObject(t *testing.T) { 402 Given(t). 403 Timeout(30). 404 Path("helm"). 405 When(). 406 // app should be auto-synced once created 407 CreateFromFile(func(app *Application) { 408 app.Spec.Source.Helm = &ApplicationSourceHelm{ 409 ValuesObject: &runtime.RawExtension{ 410 // Setup by using nested YAML objects, which is what causes the patch error: 411 // "unable to find api field in struct RawExtension for the json field "some"" 412 Raw: []byte(`{"some": {"foo": "bar"}}`), 413 }, 414 } 415 }). 416 Then(). 417 When(). 418 PatchApp(`[{ 419 "op": "add", 420 "path": "/spec/source/helm/valuesObject", 421 "value": {"some":{"foo":"bar","new":"field"}} 422 }]`). 423 Refresh(RefreshTypeNormal). 424 Sync(). 425 Then(). 426 Expect(Success("")). 427 Expect(OperationPhaseIs(OperationSucceeded)). 428 Expect(SyncStatusIs(SyncStatusCodeSynced)). 429 Expect(NoConditions()). 430 And(func(app *Application) { 431 // Check that the patch was a success. 432 assert.JSONEq(t, `{"some":{"foo":"bar","new":"field"}}`, string(app.Spec.Source.Helm.ValuesObject.Raw)) 433 }) 434 } 435 436 func TestDeleteAppResource(t *testing.T) { 437 ctx := Given(t) 438 439 ctx. 440 Path(guestbookPath). 441 When(). 442 CreateApp(). 443 Sync(). 444 Then(). 445 Expect(SyncStatusIs(SyncStatusCodeSynced)). 446 And(func(_ *Application) { 447 // app should be listed 448 if _, err := fixture.RunCli("app", "delete-resource", fixture.Name(), "--kind", "Service", "--resource-name", "guestbook-ui"); err != nil { 449 require.NoError(t, err) 450 } 451 }). 452 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 453 Expect(HealthIs(health.HealthStatusMissing)) 454 } 455 456 // Fix for issue #2677, support PATCH in HTTP service 457 func TestPatchHttp(t *testing.T) { 458 ctx := Given(t) 459 460 ctx. 461 Path(guestbookPath). 462 When(). 463 CreateApp(). 464 Sync(). 465 PatchAppHttp(`{"metadata": {"labels": { "test": "patch" }, "annotations": { "test": "patch" }}}`). 466 Then(). 467 And(func(app *Application) { 468 assert.Equal(t, "patch", app.Labels["test"]) 469 assert.Equal(t, "patch", app.Annotations["test"]) 470 }) 471 } 472 473 // demonstrate that we cannot use a standard sync when an immutable field is changed, we must use "force" 474 func TestImmutableChange(t *testing.T) { 475 fixture.SkipOnEnv(t, "OPENSHIFT") 476 Given(t). 477 Path("secrets"). 478 When(). 479 CreateApp(). 480 PatchFile("secrets.yaml", `[{"op": "add", "path": "/data/new-field", "value": "dGVzdA=="}, {"op": "add", "path": "/immutable", "value": true}]`). 481 Sync(). 482 Then(). 483 Expect(OperationPhaseIs(OperationSucceeded)). 484 Expect(SyncStatusIs(SyncStatusCodeSynced)). 485 Expect(HealthIs(health.HealthStatusHealthy)). 486 When(). 487 PatchFile("secrets.yaml", `[{"op": "add", "path": "/data/new-field", "value": "dGVzdDI="}]`). 488 IgnoreErrors(). 489 Sync(). 490 DoNotIgnoreErrors(). 491 Then(). 492 Expect(OperationPhaseIs(OperationFailed)). 493 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 494 Expect(ResourceResultNumbering(1)). 495 Expect(ResourceResultMatches(ResourceResult{ 496 Kind: "Secret", 497 Version: "v1", 498 Namespace: fixture.DeploymentNamespace(), 499 Name: "test-secret", 500 SyncPhase: "Sync", 501 Status: "SyncFailed", 502 HookPhase: "Failed", 503 Message: `Secret "test-secret" is invalid`, 504 })). 505 // now we can do this will a force 506 Given(). 507 Force(). 508 When(). 509 Sync(). 510 Then(). 511 Expect(OperationPhaseIs(OperationSucceeded)). 512 Expect(SyncStatusIs(SyncStatusCodeSynced)). 513 Expect(HealthIs(health.HealthStatusHealthy)) 514 } 515 516 func TestInvalidAppProject(t *testing.T) { 517 Given(t). 518 Path(guestbookPath). 519 Project("does-not-exist"). 520 When(). 521 IgnoreErrors(). 522 CreateApp(). 523 Then(). 524 // We're not allowed to infer whether the project exists based on this error message. Instead, we get a generic 525 // permission denied error. 526 Expect(Error("", "is not allowed")) 527 } 528 529 func TestAppDeletion(t *testing.T) { 530 Given(t). 531 Path(guestbookPath). 532 When(). 533 CreateApp(). 534 Then(). 535 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 536 When(). 537 Delete(true). 538 Then(). 539 Expect(DoesNotExist()). 540 Expect(Event(argo.EventReasonResourceDeleted, "delete")) 541 542 output, err := fixture.RunCli("app", "list") 543 require.NoError(t, err) 544 assert.NotContains(t, output, fixture.Name()) 545 } 546 547 func TestAppLabels(t *testing.T) { 548 Given(t). 549 Path("config-map"). 550 When(). 551 CreateApp("-l", "foo=bar"). 552 Then(). 553 And(func(_ *Application) { 554 assert.Contains(t, errors.NewHandler(t).FailOnErr(fixture.RunCli("app", "list")), fixture.Name()) 555 assert.Contains(t, errors.NewHandler(t).FailOnErr(fixture.RunCli("app", "list", "-l", "foo=bar")), fixture.Name()) 556 assert.NotContains(t, errors.NewHandler(t).FailOnErr(fixture.RunCli("app", "list", "-l", "foo=rubbish")), fixture.Name()) 557 }). 558 Given(). 559 // remove both name and replace labels means nothing will sync 560 Name(""). 561 When(). 562 IgnoreErrors(). 563 Sync("-l", "foo=rubbish"). 564 DoNotIgnoreErrors(). 565 Then(). 566 Expect(Error("", "No matching apps found for filter: selector foo=rubbish")). 567 // check we can update the app and it is then sync'd 568 Given(). 569 When(). 570 Sync("-l", "foo=bar") 571 } 572 573 func TestTrackAppStateAndSyncApp(t *testing.T) { 574 Given(t). 575 Path(guestbookPath). 576 When(). 577 CreateApp(). 578 Sync(). 579 Wait(). 580 Then(). 581 Expect(OperationPhaseIs(OperationSucceeded)). 582 Expect(SyncStatusIs(SyncStatusCodeSynced)). 583 Expect(HealthIs(health.HealthStatusHealthy)). 584 Expect(Success(fmt.Sprintf("Service %s guestbook-ui Synced ", fixture.DeploymentNamespace()))). 585 Expect(Success(fmt.Sprintf("apps Deployment %s guestbook-ui Synced", fixture.DeploymentNamespace()))). 586 Expect(Event(argo.EventReasonResourceUpdated, "sync")). 587 And(func(app *Application) { 588 assert.NotNil(t, app.Status.OperationState.SyncResult) 589 }) 590 } 591 592 func TestAppRollbackSuccessful(t *testing.T) { 593 Given(t). 594 Path(guestbookPath). 595 When(). 596 CreateApp(). 597 Sync(). 598 Then(). 599 Expect(SyncStatusIs(SyncStatusCodeSynced)). 600 And(func(app *Application) { 601 assert.NotEmpty(t, app.Status.Sync.Revision) 602 }). 603 And(func(app *Application) { 604 appWithHistory := app.DeepCopy() 605 appWithHistory.Status.History = []RevisionHistory{{ 606 ID: 1, 607 Revision: app.Status.Sync.Revision, 608 DeployedAt: metav1.Time{Time: metav1.Now().UTC().Add(-1 * time.Minute)}, 609 Source: app.Spec.GetSource(), 610 }, { 611 ID: 2, 612 Revision: "cdb", 613 DeployedAt: metav1.Time{Time: metav1.Now().UTC().Add(-2 * time.Minute)}, 614 Source: app.Spec.GetSource(), 615 }} 616 patch, _, err := diff.CreateTwoWayMergePatch(app, appWithHistory, &Application{}) 617 require.NoError(t, err) 618 app, err = fixture.AppClientset.ArgoprojV1alpha1().Applications(fixture.TestNamespace()).Patch(t.Context(), app.Name, types.MergePatchType, patch, metav1.PatchOptions{}) 619 require.NoError(t, err) 620 621 // sync app and make sure it reaches InSync state 622 _, err = fixture.RunCli("app", "rollback", app.Name, "1") 623 require.NoError(t, err) 624 }). 625 Expect(Event(argo.EventReasonOperationStarted, "rollback")). 626 Expect(SyncStatusIs(SyncStatusCodeSynced)). 627 And(func(app *Application) { 628 assert.Equal(t, SyncStatusCodeSynced, app.Status.Sync.Status) 629 require.NotNil(t, app.Status.OperationState.SyncResult) 630 assert.Len(t, app.Status.OperationState.SyncResult.Resources, 2) 631 assert.Equal(t, OperationSucceeded, app.Status.OperationState.Phase) 632 assert.Len(t, app.Status.History, 3) 633 }) 634 } 635 636 func TestComparisonFailsIfClusterNotAdded(t *testing.T) { 637 Given(t). 638 Path(guestbookPath). 639 DestServer("https://not-registered-cluster/api"). 640 When(). 641 IgnoreErrors(). 642 CreateApp(). 643 Then(). 644 Expect(DoesNotExist()) 645 } 646 647 func TestComparisonFailsIfDestinationClusterIsInvalid(t *testing.T) { 648 clusterActions := clusterFixture.Given(t). 649 Name("temp-cluster"). 650 Server(KubernetesInternalAPIServerAddr). 651 When(). 652 Create() 653 654 GivenWithSameState(t). 655 Path(guestbookPath). 656 DestName("temp-cluster"). 657 When(). 658 CreateApp(). 659 Refresh(RefreshTypeNormal). 660 Sync(). 661 Then(). 662 Expect(Success("")). 663 Expect(HealthIs(health.HealthStatusHealthy)). 664 Expect(SyncStatusIs(SyncStatusCodeSynced)). 665 When(). 666 And(func() { 667 clusterActions.DeleteByName() 668 }). 669 Refresh(RefreshTypeNormal). 670 Then(). 671 Expect(Success("")). 672 Expect(HealthIs(health.HealthStatusUnknown)). 673 Expect(SyncStatusIs(SyncStatusCodeUnknown)). 674 Expect(Condition(ApplicationConditionInvalidSpecError, "there are no clusters with this name")) 675 } 676 677 func TestComparisonFailsIfInClusterDisabled(t *testing.T) { 678 Given(t). 679 Path(guestbookPath). 680 DestServer(KubernetesInternalAPIServerAddr). 681 When(). 682 CreateApp(). 683 Refresh(RefreshTypeNormal). 684 Sync(). 685 Then(). 686 Expect(Success("")). 687 Expect(HealthIs(health.HealthStatusHealthy)). 688 Expect(SyncStatusIs(SyncStatusCodeSynced)). 689 When(). 690 SetParamInSettingConfigMap("cluster.inClusterEnabled", "false"). 691 Refresh(RefreshTypeNormal). 692 Then(). 693 Expect(Success("")). 694 Expect(HealthIs(health.HealthStatusUnknown)). 695 Expect(SyncStatusIs(SyncStatusCodeUnknown)). 696 Expect(Condition(ApplicationConditionInvalidSpecError, fmt.Sprintf("cluster %q is disabled", KubernetesInternalAPIServerAddr))) 697 } 698 699 func TestCannotSetInvalidPath(t *testing.T) { 700 Given(t). 701 Path(guestbookPath). 702 When(). 703 CreateApp(). 704 IgnoreErrors(). 705 AppSet("--path", "garbage"). 706 Then(). 707 Expect(Error("", "app path does not exist")) 708 } 709 710 func TestManipulateApplicationResources(t *testing.T) { 711 Given(t). 712 Path(guestbookPath). 713 When(). 714 CreateApp(). 715 Sync(). 716 Then(). 717 Expect(SyncStatusIs(SyncStatusCodeSynced)). 718 And(func(app *Application) { 719 manifests, err := fixture.RunCli("app", "manifests", app.Name, "--source", "live") 720 require.NoError(t, err) 721 resources, err := kube.SplitYAML([]byte(manifests)) 722 require.NoError(t, err) 723 724 index := -1 725 for i := range resources { 726 if resources[i].GetKind() == kube.DeploymentKind { 727 index = i 728 break 729 } 730 } 731 assert.Greater(t, index, -1) 732 733 deployment := resources[index] 734 735 closer, client, err := fixture.ArgoCDClientset.NewApplicationClient() 736 require.NoError(t, err) 737 defer utilio.Close(closer) 738 739 _, err = client.DeleteResource(t.Context(), &applicationpkg.ApplicationResourceDeleteRequest{ 740 Name: &app.Name, 741 Group: ptr.To(deployment.GroupVersionKind().Group), 742 Kind: ptr.To(deployment.GroupVersionKind().Kind), 743 Version: ptr.To(deployment.GroupVersionKind().Version), 744 Namespace: ptr.To(deployment.GetNamespace()), 745 ResourceName: ptr.To(deployment.GetName()), 746 }) 747 require.NoError(t, err) 748 }). 749 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)) 750 } 751 752 func assetSecretDataHidden(t *testing.T, manifest string) { 753 t.Helper() 754 secret, err := UnmarshalToUnstructured(manifest) 755 require.NoError(t, err) 756 757 _, hasStringData, err := unstructured.NestedMap(secret.Object, "stringData") 758 require.NoError(t, err) 759 assert.False(t, hasStringData) 760 761 secretData, hasData, err := unstructured.NestedMap(secret.Object, "data") 762 require.NoError(t, err) 763 assert.True(t, hasData) 764 for _, v := range secretData { 765 assert.Regexp(t, `[*]*`, v) 766 } 767 var lastAppliedConfigAnnotation string 768 annotations := secret.GetAnnotations() 769 if annotations != nil { 770 lastAppliedConfigAnnotation = annotations[corev1.LastAppliedConfigAnnotation] 771 } 772 if lastAppliedConfigAnnotation != "" { 773 assetSecretDataHidden(t, lastAppliedConfigAnnotation) 774 } 775 } 776 777 func TestAppWithSecrets(t *testing.T) { 778 closer, client, err := fixture.ArgoCDClientset.NewApplicationClient() 779 require.NoError(t, err) 780 defer utilio.Close(closer) 781 782 Given(t). 783 Path("secrets"). 784 When(). 785 CreateApp(). 786 Sync(). 787 Then(). 788 Expect(SyncStatusIs(SyncStatusCodeSynced)). 789 And(func(app *Application) { 790 res := errors.NewHandler(t).FailOnErr(client.GetResource(t.Context(), &applicationpkg.ApplicationResourceRequest{ 791 Namespace: &app.Spec.Destination.Namespace, 792 Kind: ptr.To(kube.SecretKind), 793 Group: ptr.To(""), 794 Name: &app.Name, 795 Version: ptr.To("v1"), 796 ResourceName: ptr.To("test-secret"), 797 })).(*applicationpkg.ApplicationResourceResponse) 798 assetSecretDataHidden(t, res.GetManifest()) 799 800 manifests, err := client.GetManifests(t.Context(), &applicationpkg.ApplicationManifestQuery{Name: &app.Name}) 801 require.NoError(t, err) 802 803 for _, manifest := range manifests.Manifests { 804 assetSecretDataHidden(t, manifest) 805 } 806 807 diffOutput := errors.NewHandler(t).FailOnErr(fixture.RunCli("app", "diff", app.Name)).(string) 808 assert.Empty(t, diffOutput) 809 810 // make sure resource update error does not print secret details 811 _, err = fixture.RunCli("app", "patch-resource", "test-app-with-secrets", "--resource-name", "test-secret", 812 "--kind", "Secret", "--patch", `{"op": "add", "path": "/data", "value": "hello"}'`, 813 "--patch-type", "application/json-patch+json") 814 require.ErrorContains(t, err, fmt.Sprintf("failed to patch Secret %s/test-secret", fixture.DeploymentNamespace())) 815 assert.NotContains(t, err.Error(), "username") 816 assert.NotContains(t, err.Error(), "password") 817 818 // patch secret and make sure app is out of sync and diff detects the change 819 errors.NewHandler(t).FailOnErr(fixture.KubeClientset.CoreV1().Secrets(fixture.DeploymentNamespace()).Patch(t.Context(), 820 "test-secret", types.JSONPatchType, []byte(`[ 821 {"op": "remove", "path": "/data/username"}, 822 {"op": "add", "path": "/stringData", "value": {"password": "foo"}} 823 ]`), metav1.PatchOptions{})) 824 }). 825 When(). 826 Refresh(RefreshTypeNormal). 827 Then(). 828 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 829 And(func(app *Application) { 830 diffOutput, err := fixture.RunCli("app", "diff", app.Name) 831 require.Error(t, err) 832 assert.Contains(t, diffOutput, "username: ++++++++") 833 assert.Contains(t, diffOutput, "password: ++++++++++++") 834 835 // local diff should ignore secrets 836 diffOutput = errors.NewHandler(t).FailOnErr(fixture.RunCli("app", "diff", app.Name, "--local", "testdata", "--server-side-generate")).(string) 837 assert.Empty(t, diffOutput) 838 839 // ignore missing field and make sure diff shows no difference 840 app.Spec.IgnoreDifferences = []ResourceIgnoreDifferences{{ 841 Kind: kube.SecretKind, JSONPointers: []string{"/data"}, 842 }} 843 errors.NewHandler(t).FailOnErr(client.UpdateSpec(t.Context(), &applicationpkg.ApplicationUpdateSpecRequest{Name: &app.Name, Spec: &app.Spec})) 844 }). 845 When(). 846 Refresh(RefreshTypeNormal). 847 Then(). 848 Expect(OperationPhaseIs(OperationSucceeded)). 849 Expect(SyncStatusIs(SyncStatusCodeSynced)). 850 And(func(app *Application) { 851 diffOutput := errors.NewHandler(t).FailOnErr(fixture.RunCli("app", "diff", app.Name)).(string) 852 assert.Empty(t, diffOutput) 853 }). 854 // verify not committed secret also ignore during diffing 855 When(). 856 WriteFile("secret3.yaml", ` 857 apiVersion: v1 858 kind: Secret 859 metadata: 860 name: test-secret3 861 stringData: 862 username: test-username`). 863 Then(). 864 And(func(app *Application) { 865 diffOutput := errors.NewHandler(t).FailOnErr(fixture.RunCli("app", "diff", app.Name, "--local", "testdata", "--server-side-generate")).(string) 866 assert.Empty(t, diffOutput) 867 }) 868 } 869 870 func TestResourceDiffing(t *testing.T) { 871 Given(t). 872 Path(guestbookPath). 873 When(). 874 CreateApp(). 875 Sync(). 876 Then(). 877 Expect(SyncStatusIs(SyncStatusCodeSynced)). 878 And(func(_ *Application) { 879 // Patch deployment 880 _, err := fixture.KubeClientset.AppsV1().Deployments(fixture.DeploymentNamespace()).Patch(t.Context(), 881 "guestbook-ui", types.JSONPatchType, []byte(`[{ "op": "replace", "path": "/spec/template/spec/containers/0/image", "value": "test" }]`), metav1.PatchOptions{}) 882 require.NoError(t, err) 883 }). 884 When(). 885 Refresh(RefreshTypeNormal). 886 Then(). 887 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 888 And(func(app *Application) { 889 diffOutput, err := fixture.RunCli("app", "diff", app.Name, "--local", "testdata", "--server-side-generate") 890 require.Error(t, err) 891 assert.Contains(t, diffOutput, fmt.Sprintf("===== apps/Deployment %s/guestbook-ui ======", fixture.DeploymentNamespace())) 892 }). 893 Given(). 894 ResourceOverrides(map[string]ResourceOverride{"apps/Deployment": { 895 IgnoreDifferences: OverrideIgnoreDiff{JSONPointers: []string{"/spec/template/spec/containers/0/image"}}, 896 }}). 897 When(). 898 Refresh(RefreshTypeNormal). 899 Then(). 900 Expect(SyncStatusIs(SyncStatusCodeSynced)). 901 And(func(app *Application) { 902 diffOutput, err := fixture.RunCli("app", "diff", app.Name, "--local", "testdata", "--server-side-generate") 903 require.NoError(t, err) 904 assert.Empty(t, diffOutput) 905 }). 906 Given(). 907 When(). 908 // Now we migrate from client-side apply to server-side apply 909 // This is necessary, as starting with kubectl 1.26, all previously 910 // client-side owned fields have ownership migrated to the manager from 911 // the first ssa. 912 // More details: https://github.com/kubernetes/kubectl/issues/1337 913 PatchApp(`[{ 914 "op": "add", 915 "path": "/spec/syncPolicy", 916 "value": { "syncOptions": ["ServerSideApply=true"] } 917 }]`). 918 Sync(). 919 And(func() { 920 output, err := fixture.RunWithStdin(testdata.SSARevisionHistoryDeployment, "", "kubectl", "apply", "-n", fixture.DeploymentNamespace(), "--server-side=true", "--field-manager=revision-history-manager", "--validate=false", "--force-conflicts", "-f", "-") 921 require.NoError(t, err) 922 assert.Contains(t, output, "serverside-applied") 923 }). 924 Refresh(RefreshTypeNormal). 925 Then(). 926 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 927 Given(). 928 ResourceOverrides(map[string]ResourceOverride{"apps/Deployment": { 929 IgnoreDifferences: OverrideIgnoreDiff{ 930 ManagedFieldsManagers: []string{"revision-history-manager"}, 931 JSONPointers: []string{"/spec/template/spec/containers/0/image"}, 932 }, 933 }}). 934 When(). 935 Refresh(RefreshTypeNormal). 936 Then(). 937 Expect(SyncStatusIs(SyncStatusCodeSynced)). 938 Given(). 939 When(). 940 Sync(). 941 PatchApp(`[{ 942 "op": "add", 943 "path": "/spec/syncPolicy", 944 "value": { "syncOptions": ["RespectIgnoreDifferences=true"] } 945 }]`). 946 And(func() { 947 deployment, err := fixture.KubeClientset.AppsV1().Deployments(fixture.DeploymentNamespace()).Get(t.Context(), "guestbook-ui", metav1.GetOptions{}) 948 require.NoError(t, err) 949 assert.Equal(t, int32(3), *deployment.Spec.RevisionHistoryLimit) 950 }). 951 And(func() { 952 output, err := fixture.RunWithStdin(testdata.SSARevisionHistoryDeployment, "", "kubectl", "apply", "-n", fixture.DeploymentNamespace(), "--server-side=true", "--field-manager=revision-history-manager", "--validate=false", "--force-conflicts", "-f", "-") 953 require.NoError(t, err) 954 assert.Contains(t, output, "serverside-applied") 955 }). 956 Then(). 957 When().Refresh(RefreshTypeNormal). 958 Then(). 959 Expect(SyncStatusIs(SyncStatusCodeSynced)). 960 And(func(_ *Application) { 961 deployment, err := fixture.KubeClientset.AppsV1().Deployments(fixture.DeploymentNamespace()).Get(t.Context(), "guestbook-ui", metav1.GetOptions{}) 962 require.NoError(t, err) 963 assert.Equal(t, int32(1), *deployment.Spec.RevisionHistoryLimit) 964 }). 965 When().Sync().Then().Expect(SyncStatusIs(SyncStatusCodeSynced)). 966 And(func(_ *Application) { 967 deployment, err := fixture.KubeClientset.AppsV1().Deployments(fixture.DeploymentNamespace()).Get(t.Context(), "guestbook-ui", metav1.GetOptions{}) 968 require.NoError(t, err) 969 assert.Equal(t, int32(1), *deployment.Spec.RevisionHistoryLimit) 970 }) 971 } 972 973 func TestCRDs(t *testing.T) { 974 testEdgeCasesApplicationResources(t, "crd-creation", health.HealthStatusHealthy) 975 } 976 977 func TestKnownTypesInCRDDiffing(t *testing.T) { 978 dummiesGVR := schema.GroupVersionResource{Group: application.Group, Version: "v1alpha1", Resource: "dummies"} 979 980 Given(t). 981 Path("crd-creation"). 982 When().CreateApp().Sync().Then(). 983 Expect(OperationPhaseIs(OperationSucceeded)).Expect(SyncStatusIs(SyncStatusCodeSynced)). 984 When(). 985 And(func() { 986 dummyResIf := fixture.DynamicClientset.Resource(dummiesGVR).Namespace(fixture.DeploymentNamespace()) 987 patchData := []byte(`{"spec":{"cpu": "2"}}`) 988 errors.NewHandler(t).FailOnErr(dummyResIf.Patch(t.Context(), "dummy-crd-instance", types.MergePatchType, patchData, metav1.PatchOptions{})) 989 }).Refresh(RefreshTypeNormal). 990 Then(). 991 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 992 When(). 993 And(func() { 994 require.NoError(t, fixture.SetResourceOverrides(map[string]ResourceOverride{ 995 "argoproj.io/Dummy": { 996 KnownTypeFields: []KnownTypeField{{ 997 Field: "spec", 998 Type: "core/v1/ResourceList", 999 }}, 1000 }, 1001 })) 1002 }). 1003 Refresh(RefreshTypeNormal). 1004 Then(). 1005 Expect(SyncStatusIs(SyncStatusCodeSynced)) 1006 } 1007 1008 func TestDuplicatedClusterResourcesAnnotationTracking(t *testing.T) { 1009 // This test will fail if the controller fails to fix the tracking annotation for malformed cluster resources 1010 // (i.e. resources where metadata.namespace is set). Before the bugfix, this test would fail with a diff in the 1011 // tracking annotation. 1012 Given(t). 1013 SetTrackingMethod(string(TrackingMethodAnnotation)). 1014 Path("duplicated-resources"). 1015 When(). 1016 CreateApp(). 1017 Sync(). 1018 Then(). 1019 Expect(OperationPhaseIs(OperationSucceeded)). 1020 Expect(SyncStatusIs(SyncStatusCodeSynced)). 1021 Expect(HealthIs(health.HealthStatusHealthy)). 1022 And(func(app *Application) { 1023 diffOutput, err := fixture.RunCli("app", "diff", app.Name, "--local", "testdata", "--server-side-generate") 1024 assert.Empty(t, diffOutput) 1025 require.NoError(t, err) 1026 }) 1027 } 1028 1029 func TestDuplicatedResources(t *testing.T) { 1030 testEdgeCasesApplicationResources(t, "duplicated-resources", health.HealthStatusHealthy) 1031 } 1032 1033 func TestConfigMap(t *testing.T) { 1034 testEdgeCasesApplicationResources(t, "config-map", health.HealthStatusHealthy, "my-map Synced configmap/my-map created") 1035 } 1036 1037 func testEdgeCasesApplicationResources(t *testing.T, appPath string, statusCode health.HealthStatusCode, message ...string) { 1038 t.Helper() 1039 expect := Given(t). 1040 Path(appPath). 1041 When(). 1042 CreateApp(). 1043 Sync(). 1044 Then(). 1045 Expect(OperationPhaseIs(OperationSucceeded)). 1046 Expect(SyncStatusIs(SyncStatusCodeSynced)) 1047 for i := range message { 1048 expect = expect.Expect(Success(message[i])) 1049 } 1050 expect. 1051 Expect(HealthIs(statusCode)). 1052 And(func(app *Application) { 1053 diffOutput, err := fixture.RunCli("app", "diff", app.Name, "--local", "testdata", "--server-side-generate") 1054 assert.Empty(t, diffOutput) 1055 require.NoError(t, err) 1056 }) 1057 } 1058 1059 const actionsConfig = `discovery.lua: return { sample = {} } 1060 definitions: 1061 - name: sample 1062 action.lua: | 1063 obj.metadata.labels.sample = 'test' 1064 return obj` 1065 1066 func TestOldStyleResourceAction(t *testing.T) { 1067 Given(t). 1068 Path(guestbookPath). 1069 ResourceOverrides(map[string]ResourceOverride{"apps/Deployment": {Actions: actionsConfig}}). 1070 When(). 1071 CreateApp(). 1072 Sync(). 1073 Then(). 1074 And(func(app *Application) { 1075 closer, client, err := fixture.ArgoCDClientset.NewApplicationClient() 1076 require.NoError(t, err) 1077 defer utilio.Close(closer) 1078 1079 actions, err := client.ListResourceActions(t.Context(), &applicationpkg.ApplicationResourceRequest{ 1080 Name: &app.Name, 1081 Group: ptr.To("apps"), 1082 Kind: ptr.To("Deployment"), 1083 Version: ptr.To("v1"), 1084 Namespace: ptr.To(fixture.DeploymentNamespace()), 1085 ResourceName: ptr.To("guestbook-ui"), 1086 }) 1087 require.NoError(t, err) 1088 assert.Equal(t, []*ResourceAction{{Name: "sample", Disabled: false}}, actions.Actions) 1089 1090 _, err = client.RunResourceActionV2(t.Context(), &applicationpkg.ResourceActionRunRequestV2{ 1091 Name: &app.Name, 1092 Group: ptr.To("apps"), 1093 Kind: ptr.To("Deployment"), 1094 Version: ptr.To("v1"), 1095 Namespace: ptr.To(fixture.DeploymentNamespace()), 1096 ResourceName: ptr.To("guestbook-ui"), 1097 Action: ptr.To("sample"), 1098 }) 1099 require.NoError(t, err) 1100 1101 deployment, err := fixture.KubeClientset.AppsV1().Deployments(fixture.DeploymentNamespace()).Get(t.Context(), "guestbook-ui", metav1.GetOptions{}) 1102 require.NoError(t, err) 1103 1104 assert.Equal(t, "test", deployment.Labels["sample"]) 1105 }) 1106 } 1107 1108 const newStyleActionsConfig = `discovery.lua: return { sample = {} } 1109 definitions: 1110 - name: sample 1111 action.lua: | 1112 local os = require("os") 1113 1114 function deepCopy(object) 1115 local lookup_table = {} 1116 local function _copy(obj) 1117 if type(obj) ~= "table" then 1118 return obj 1119 elseif lookup_table[obj] then 1120 return lookup_table[obj] 1121 elseif next(obj) == nil then 1122 return nil 1123 else 1124 local new_table = {} 1125 lookup_table[obj] = new_table 1126 for key, value in pairs(obj) do 1127 new_table[_copy(key)] = _copy(value) 1128 end 1129 return setmetatable(new_table, getmetatable(obj)) 1130 end 1131 end 1132 return _copy(object) 1133 end 1134 1135 job = {} 1136 job.apiVersion = "batch/v1" 1137 job.kind = "Job" 1138 1139 job.metadata = {} 1140 job.metadata.name = obj.metadata.name .. "-123" 1141 job.metadata.namespace = obj.metadata.namespace 1142 1143 ownerRef = {} 1144 ownerRef.apiVersion = obj.apiVersion 1145 ownerRef.kind = obj.kind 1146 ownerRef.name = obj.metadata.name 1147 ownerRef.uid = obj.metadata.uid 1148 job.metadata.ownerReferences = {} 1149 job.metadata.ownerReferences[1] = ownerRef 1150 1151 job.spec = {} 1152 job.spec.suspend = false 1153 job.spec.template = {} 1154 job.spec.template.spec = deepCopy(obj.spec.jobTemplate.spec.template.spec) 1155 1156 impactedResource = {} 1157 impactedResource.operation = "create" 1158 impactedResource.resource = job 1159 result = {} 1160 result[1] = impactedResource 1161 1162 return result` 1163 1164 func TestNewStyleResourceActionPermitted(t *testing.T) { 1165 Given(t). 1166 Path(resourceActions). 1167 ResourceOverrides(map[string]ResourceOverride{"batch/CronJob": {Actions: newStyleActionsConfig}}). 1168 ProjectSpec(AppProjectSpec{ 1169 SourceRepos: []string{"*"}, 1170 Destinations: []ApplicationDestination{{Namespace: "*", Server: "*"}}, 1171 NamespaceResourceWhitelist: []metav1.GroupKind{ 1172 {Group: "batch", Kind: "Job"}, 1173 {Group: "batch", Kind: "CronJob"}, 1174 }, 1175 }). 1176 When(). 1177 CreateApp(). 1178 Sync(). 1179 Wait(). 1180 Then(). 1181 And(func(app *Application) { 1182 closer, client, err := fixture.ArgoCDClientset.NewApplicationClient() 1183 require.NoError(t, err) 1184 defer utilio.Close(closer) 1185 1186 actions, err := client.ListResourceActions(t.Context(), &applicationpkg.ApplicationResourceRequest{ 1187 Name: &app.Name, 1188 Group: ptr.To("batch"), 1189 Kind: ptr.To("CronJob"), 1190 Version: ptr.To("v1"), 1191 Namespace: ptr.To(fixture.DeploymentNamespace()), 1192 ResourceName: ptr.To("hello"), 1193 }) 1194 require.NoError(t, err) 1195 assert.Equal(t, []*ResourceAction{{Name: "sample", Disabled: false}}, actions.Actions) 1196 1197 _, err = client.RunResourceActionV2(t.Context(), &applicationpkg.ResourceActionRunRequestV2{ 1198 Name: &app.Name, 1199 Group: ptr.To("batch"), 1200 Kind: ptr.To("CronJob"), 1201 Version: ptr.To("v1"), 1202 Namespace: ptr.To(fixture.DeploymentNamespace()), 1203 ResourceName: ptr.To("hello"), 1204 Action: ptr.To("sample"), 1205 }) 1206 require.NoError(t, err) 1207 1208 _, err = fixture.KubeClientset.BatchV1().Jobs(fixture.DeploymentNamespace()).Get(t.Context(), "hello-123", metav1.GetOptions{}) 1209 require.NoError(t, err) 1210 }) 1211 } 1212 1213 const newStyleActionsConfigMixedOk = `discovery.lua: return { sample = {} } 1214 definitions: 1215 - name: sample 1216 action.lua: | 1217 local os = require("os") 1218 1219 function deepCopy(object) 1220 local lookup_table = {} 1221 local function _copy(obj) 1222 if type(obj) ~= "table" then 1223 return obj 1224 elseif lookup_table[obj] then 1225 return lookup_table[obj] 1226 elseif next(obj) == nil then 1227 return nil 1228 else 1229 local new_table = {} 1230 lookup_table[obj] = new_table 1231 for key, value in pairs(obj) do 1232 new_table[_copy(key)] = _copy(value) 1233 end 1234 return setmetatable(new_table, getmetatable(obj)) 1235 end 1236 end 1237 return _copy(object) 1238 end 1239 1240 job = {} 1241 job.apiVersion = "batch/v1" 1242 job.kind = "Job" 1243 1244 job.metadata = {} 1245 job.metadata.name = obj.metadata.name .. "-123" 1246 job.metadata.namespace = obj.metadata.namespace 1247 1248 ownerRef = {} 1249 ownerRef.apiVersion = obj.apiVersion 1250 ownerRef.kind = obj.kind 1251 ownerRef.name = obj.metadata.name 1252 ownerRef.uid = obj.metadata.uid 1253 job.metadata.ownerReferences = {} 1254 job.metadata.ownerReferences[1] = ownerRef 1255 1256 job.spec = {} 1257 job.spec.suspend = false 1258 job.spec.template = {} 1259 job.spec.template.spec = deepCopy(obj.spec.jobTemplate.spec.template.spec) 1260 1261 impactedResource1 = {} 1262 impactedResource1.operation = "create" 1263 impactedResource1.resource = job 1264 result = {} 1265 result[1] = impactedResource1 1266 1267 obj.metadata.labels = {} 1268 obj.metadata.labels["aKey"] = 'aValue' 1269 impactedResource2 = {} 1270 impactedResource2.operation = "patch" 1271 impactedResource2.resource = obj 1272 1273 result[2] = impactedResource2 1274 1275 return result` 1276 1277 func TestNewStyleResourceActionMixedOk(t *testing.T) { 1278 Given(t). 1279 Path(resourceActions). 1280 ResourceOverrides(map[string]ResourceOverride{"batch/CronJob": {Actions: newStyleActionsConfigMixedOk}}). 1281 ProjectSpec(AppProjectSpec{ 1282 SourceRepos: []string{"*"}, 1283 Destinations: []ApplicationDestination{{Namespace: "*", Server: "*"}}, 1284 NamespaceResourceWhitelist: []metav1.GroupKind{ 1285 {Group: "batch", Kind: "Job"}, 1286 {Group: "batch", Kind: "CronJob"}, 1287 }, 1288 }). 1289 When(). 1290 CreateApp(). 1291 Sync(). 1292 Wait(). 1293 Then(). 1294 And(func(app *Application) { 1295 closer, client, err := fixture.ArgoCDClientset.NewApplicationClient() 1296 require.NoError(t, err) 1297 defer utilio.Close(closer) 1298 1299 actions, err := client.ListResourceActions(t.Context(), &applicationpkg.ApplicationResourceRequest{ 1300 Name: &app.Name, 1301 Group: ptr.To("batch"), 1302 Kind: ptr.To("CronJob"), 1303 Version: ptr.To("v1"), 1304 Namespace: ptr.To(fixture.DeploymentNamespace()), 1305 ResourceName: ptr.To("hello"), 1306 }) 1307 require.NoError(t, err) 1308 assert.Equal(t, []*ResourceAction{{Name: "sample", Disabled: false}}, actions.Actions) 1309 1310 _, err = client.RunResourceActionV2(t.Context(), &applicationpkg.ResourceActionRunRequestV2{ 1311 Name: &app.Name, 1312 Group: ptr.To("batch"), 1313 Kind: ptr.To("CronJob"), 1314 Version: ptr.To("v1"), 1315 Namespace: ptr.To(fixture.DeploymentNamespace()), 1316 ResourceName: ptr.To("hello"), 1317 Action: ptr.To("sample"), 1318 }) 1319 require.NoError(t, err) 1320 1321 // Assert new Job was created 1322 _, err = fixture.KubeClientset.BatchV1().Jobs(fixture.DeploymentNamespace()).Get(t.Context(), "hello-123", metav1.GetOptions{}) 1323 require.NoError(t, err) 1324 // Assert the original CronJob was patched 1325 cronJob, err := fixture.KubeClientset.BatchV1().CronJobs(fixture.DeploymentNamespace()).Get(t.Context(), "hello", metav1.GetOptions{}) 1326 assert.Equal(t, "aValue", cronJob.Labels["aKey"]) 1327 require.NoError(t, err) 1328 }) 1329 } 1330 1331 func TestSyncResourceByLabel(t *testing.T) { 1332 Given(t). 1333 Path(guestbookPath). 1334 When(). 1335 CreateApp(). 1336 Sync(). 1337 Then(). 1338 And(func(app *Application) { 1339 _, _ = fixture.RunCli("app", "sync", app.Name, "--label", "app.kubernetes.io/instance="+app.Name) 1340 }). 1341 Expect(SyncStatusIs(SyncStatusCodeSynced)). 1342 And(func(app *Application) { 1343 _, err := fixture.RunCli("app", "sync", app.Name, "--label", "this-label=does-not-exist") 1344 assert.ErrorContains(t, err, "\"level\":\"fatal\"") 1345 }) 1346 } 1347 1348 func TestSyncResourceByProject(t *testing.T) { 1349 Given(t). 1350 Path(guestbookPath). 1351 When(). 1352 CreateApp(). 1353 Sync(). 1354 Then(). 1355 And(func(app *Application) { 1356 _, _ = fixture.RunCli("app", "sync", app.Name, "--project", app.Spec.Project) 1357 }). 1358 Expect(SyncStatusIs(SyncStatusCodeSynced)). 1359 And(func(app *Application) { 1360 _, err := fixture.RunCli("app", "sync", app.Name, "--project", "this-project-does-not-exist") 1361 assert.ErrorContains(t, err, "\"level\":\"fatal\"") 1362 }) 1363 } 1364 1365 func TestLocalManifestSync(t *testing.T) { 1366 Given(t). 1367 Path(guestbookPath). 1368 When(). 1369 CreateApp(). 1370 Sync(). 1371 Then(). 1372 And(func(app *Application) { 1373 res, _ := fixture.RunCli("app", "manifests", app.Name) 1374 assert.Contains(t, res, "containerPort: 80") 1375 assert.Contains(t, res, "image: quay.io/argoprojlabs/argocd-e2e-container:0.2") 1376 }). 1377 Given(). 1378 LocalPath(guestbookPathLocal). 1379 When(). 1380 Sync("--local-repo-root", "."). 1381 Then(). 1382 Expect(SyncStatusIs(SyncStatusCodeSynced)). 1383 And(func(app *Application) { 1384 res, _ := fixture.RunCli("app", "manifests", app.Name) 1385 assert.Contains(t, res, "containerPort: 81") 1386 assert.Contains(t, res, "image: quay.io/argoprojlabs/argocd-e2e-container:0.3") 1387 }). 1388 Given(). 1389 LocalPath(""). 1390 When(). 1391 Sync(). 1392 Then(). 1393 Expect(SyncStatusIs(SyncStatusCodeSynced)). 1394 And(func(app *Application) { 1395 res, _ := fixture.RunCli("app", "manifests", app.Name) 1396 assert.Contains(t, res, "containerPort: 80") 1397 assert.Contains(t, res, "image: quay.io/argoprojlabs/argocd-e2e-container:0.2") 1398 }) 1399 } 1400 1401 func TestLocalSync(t *testing.T) { 1402 Given(t). 1403 // we've got to use Helm as this uses kubeVersion 1404 Path("helm"). 1405 When(). 1406 CreateApp(). 1407 Then(). 1408 And(func(app *Application) { 1409 errors.NewHandler(t).FailOnErr(fixture.RunCli("app", "sync", app.Name, "--local", "testdata/helm")) 1410 }) 1411 } 1412 1413 func TestNoLocalSyncWithAutosyncEnabled(t *testing.T) { 1414 Given(t). 1415 Path(guestbookPath). 1416 When(). 1417 CreateApp(). 1418 Sync(). 1419 Then(). 1420 And(func(app *Application) { 1421 _, err := fixture.RunCli("app", "set", app.Name, "--sync-policy", "automated") 1422 require.NoError(t, err) 1423 1424 _, err = fixture.RunCli("app", "sync", app.Name, "--local", guestbookPathLocal) 1425 require.Error(t, err) 1426 }) 1427 } 1428 1429 func TestLocalSyncDryRunWithAutosyncEnabled(t *testing.T) { 1430 Given(t). 1431 Path(guestbookPath). 1432 When(). 1433 CreateApp(). 1434 Sync(). 1435 Then(). 1436 And(func(app *Application) { 1437 _, err := fixture.RunCli("app", "set", app.Name, "--sync-policy", "automated") 1438 require.NoError(t, err) 1439 1440 appBefore := app.DeepCopy() 1441 _, err = fixture.RunCli("app", "sync", app.Name, "--dry-run", "--local-repo-root", ".", "--local", guestbookPathLocal) 1442 require.NoError(t, err) 1443 1444 appAfter := app.DeepCopy() 1445 assert.True(t, reflect.DeepEqual(appBefore, appAfter)) 1446 }) 1447 } 1448 1449 func TestSyncAsync(t *testing.T) { 1450 Given(t). 1451 Path(guestbookPath). 1452 Async(true). 1453 When(). 1454 CreateApp(). 1455 Sync(). 1456 Then(). 1457 Expect(Success("")). 1458 Expect(OperationPhaseIs(OperationSucceeded)). 1459 Expect(SyncStatusIs(SyncStatusCodeSynced)) 1460 } 1461 1462 // assertResourceActions verifies if view/modify resource actions are successful/failing for given application 1463 func assertResourceActions(t *testing.T, appName string, successful bool) { 1464 t.Helper() 1465 assertError := func(err error, message string) { 1466 if successful { 1467 require.NoError(t, err) 1468 } else { 1469 assert.ErrorContains(t, err, message) 1470 } 1471 } 1472 1473 closer, cdClient := fixture.ArgoCDClientset.NewApplicationClientOrDie() 1474 defer utilio.Close(closer) 1475 1476 deploymentResource, err := fixture.KubeClientset.AppsV1().Deployments(fixture.DeploymentNamespace()).Get(t.Context(), "guestbook-ui", metav1.GetOptions{}) 1477 require.NoError(t, err) 1478 1479 logs, err := cdClient.PodLogs(t.Context(), &applicationpkg.ApplicationPodLogsQuery{ 1480 Group: ptr.To("apps"), 1481 Kind: ptr.To("Deployment"), 1482 Name: &appName, 1483 Namespace: ptr.To(fixture.DeploymentNamespace()), 1484 Container: ptr.To(""), 1485 SinceSeconds: ptr.To(int64(0)), 1486 TailLines: ptr.To(int64(0)), 1487 Follow: ptr.To(false), 1488 }) 1489 require.NoError(t, err) 1490 _, err = logs.Recv() 1491 assertError(err, "EOF") 1492 1493 expectedError := "Deployment apps guestbook-ui not found as part of application " + appName 1494 1495 _, err = cdClient.ListResourceEvents(t.Context(), &applicationpkg.ApplicationResourceEventsQuery{ 1496 Name: &appName, 1497 ResourceName: ptr.To("guestbook-ui"), 1498 ResourceNamespace: ptr.To(fixture.DeploymentNamespace()), 1499 ResourceUID: ptr.To(string(deploymentResource.UID)), 1500 }) 1501 assertError(err, fmt.Sprintf("%s not found as part of application %s", "guestbook-ui", appName)) 1502 1503 _, err = cdClient.GetResource(t.Context(), &applicationpkg.ApplicationResourceRequest{ 1504 Name: &appName, 1505 ResourceName: ptr.To("guestbook-ui"), 1506 Namespace: ptr.To(fixture.DeploymentNamespace()), 1507 Version: ptr.To("v1"), 1508 Group: ptr.To("apps"), 1509 Kind: ptr.To("Deployment"), 1510 }) 1511 assertError(err, expectedError) 1512 1513 _, err = cdClient.RunResourceActionV2(t.Context(), &applicationpkg.ResourceActionRunRequestV2{ 1514 Name: &appName, 1515 ResourceName: ptr.To("guestbook-ui"), 1516 Namespace: ptr.To(fixture.DeploymentNamespace()), 1517 Version: ptr.To("v1"), 1518 Group: ptr.To("apps"), 1519 Kind: ptr.To("Deployment"), 1520 Action: ptr.To("restart"), 1521 }) 1522 assertError(err, expectedError) 1523 1524 _, err = cdClient.DeleteResource(t.Context(), &applicationpkg.ApplicationResourceDeleteRequest{ 1525 Name: &appName, 1526 ResourceName: ptr.To("guestbook-ui"), 1527 Namespace: ptr.To(fixture.DeploymentNamespace()), 1528 Version: ptr.To("v1"), 1529 Group: ptr.To("apps"), 1530 Kind: ptr.To("Deployment"), 1531 }) 1532 assertError(err, expectedError) 1533 } 1534 1535 func TestPermissions(t *testing.T) { 1536 appCtx := Given(t) 1537 projName := "argo-project" 1538 projActions := projectFixture. 1539 GivenWithSameState(t). 1540 Name(projName). 1541 When(). 1542 Create() 1543 1544 sourceError := fmt.Sprintf("application repo %s is not permitted in project 'argo-project'", fixture.RepoURL(fixture.RepoURLTypeFile)) 1545 destinationError := fmt.Sprintf("application destination server '%s' and namespace '%s' do not match any of the allowed destinations in project 'argo-project'", KubernetesInternalAPIServerAddr, fixture.DeploymentNamespace()) 1546 1547 appCtx. 1548 Path("guestbook-logs"). 1549 Project(projName). 1550 When(). 1551 IgnoreErrors(). 1552 // ensure app is not created if project permissions are missing 1553 CreateApp(). 1554 Then(). 1555 Expect(Error("", sourceError)). 1556 Expect(Error("", destinationError)). 1557 When(). 1558 DoNotIgnoreErrors(). 1559 // add missing permissions, create and sync app 1560 And(func() { 1561 projActions.AddDestination("*", "*") 1562 projActions.AddSource("*") 1563 }). 1564 CreateApp(). 1565 Sync(). 1566 Then(). 1567 // make sure application resource actiions are successful 1568 And(func(app *Application) { 1569 assertResourceActions(t, app.Name, true) 1570 }). 1571 When(). 1572 // remove projet permissions and "refresh" app 1573 And(func() { 1574 projActions.UpdateProject(func(proj *AppProject) { 1575 proj.Spec.Destinations = nil 1576 proj.Spec.SourceRepos = nil 1577 }) 1578 }). 1579 Refresh(RefreshTypeNormal). 1580 Then(). 1581 // ensure app resource tree is empty when source/destination permissions are missing 1582 Expect(Condition(ApplicationConditionInvalidSpecError, destinationError)). 1583 Expect(Condition(ApplicationConditionInvalidSpecError, sourceError)). 1584 And(func(app *Application) { 1585 closer, cdClient := fixture.ArgoCDClientset.NewApplicationClientOrDie() 1586 defer utilio.Close(closer) 1587 appName, appNs := argo.ParseFromQualifiedName(app.Name, "") 1588 fmt.Printf("APP NAME: %s\n", appName) 1589 tree, err := cdClient.ResourceTree(t.Context(), &applicationpkg.ResourcesQuery{ApplicationName: &appName, AppNamespace: &appNs}) 1590 require.NoError(t, err) 1591 assert.Empty(t, tree.Nodes) 1592 assert.Empty(t, tree.OrphanedNodes) 1593 }). 1594 When(). 1595 // add missing permissions but deny management of Deployment kind 1596 And(func() { 1597 projActions. 1598 AddDestination("*", "*"). 1599 AddSource("*"). 1600 UpdateProject(func(proj *AppProject) { 1601 proj.Spec.NamespaceResourceBlacklist = []metav1.GroupKind{{Group: "*", Kind: "Deployment"}} 1602 }) 1603 }). 1604 Refresh(RefreshTypeNormal). 1605 Then(). 1606 // make sure application resource actiions are failing 1607 And(func(_ *Application) { 1608 assertResourceActions(t, "test-permissions", false) 1609 }) 1610 } 1611 1612 func TestPermissionWithScopedRepo(t *testing.T) { 1613 projName := "argo-project" 1614 fixture.EnsureCleanState(t) 1615 projectFixture. 1616 Given(t). 1617 Name(projName). 1618 Destination("*,*"). 1619 When(). 1620 Create(). 1621 AddSource("*") 1622 1623 repoFixture.GivenWithSameState(t). 1624 When(). 1625 Path(fixture.RepoURL(fixture.RepoURLTypeFile)). 1626 Project(projName). 1627 Create() 1628 1629 GivenWithSameState(t). 1630 Project(projName). 1631 RepoURLType(fixture.RepoURLTypeFile). 1632 Path("two-nice-pods"). 1633 When(). 1634 PatchFile("pod-1.yaml", `[{"op": "add", "path": "/metadata/annotations", "value": {"argocd.argoproj.io/sync-options": "Prune=false"}}]`). 1635 CreateApp(). 1636 Sync(). 1637 Then(). 1638 Expect(OperationPhaseIs(OperationSucceeded)). 1639 Expect(SyncStatusIs(SyncStatusCodeSynced)). 1640 When(). 1641 DeleteFile("pod-1.yaml"). 1642 Refresh(RefreshTypeHard). 1643 IgnoreErrors(). 1644 Sync(). 1645 Then(). 1646 Expect(OperationPhaseIs(OperationSucceeded)). 1647 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 1648 Expect(ResourceSyncStatusIs("Pod", "pod-1", SyncStatusCodeOutOfSync)) 1649 } 1650 1651 func TestPermissionDeniedWithScopedRepo(t *testing.T) { 1652 projName := "argo-project" 1653 projectFixture. 1654 Given(t). 1655 Name(projName). 1656 Destination("*,*"). 1657 When(). 1658 Create() 1659 1660 repoFixture.GivenWithSameState(t). 1661 When(). 1662 Path(fixture.RepoURL(fixture.RepoURLTypeFile)). 1663 Create() 1664 1665 GivenWithSameState(t). 1666 Project(projName). 1667 RepoURLType(fixture.RepoURLTypeFile). 1668 Path("two-nice-pods"). 1669 When(). 1670 PatchFile("pod-1.yaml", `[{"op": "add", "path": "/metadata/annotations", "value": {"argocd.argoproj.io/sync-options": "Prune=false"}}]`). 1671 IgnoreErrors(). 1672 CreateApp(). 1673 Then(). 1674 Expect(Error("", "is not permitted in project")) 1675 } 1676 1677 func TestPermissionDeniedWithNegatedNamespace(t *testing.T) { 1678 projName := "argo-project" 1679 projectFixture. 1680 Given(t). 1681 Name(projName). 1682 Destination("*,!*test-permission-denied-with-negated-namespace*"). 1683 When(). 1684 Create() 1685 1686 repoFixture.GivenWithSameState(t). 1687 When(). 1688 Path(fixture.RepoURL(fixture.RepoURLTypeFile)). 1689 Project(projName). 1690 Create() 1691 1692 GivenWithSameState(t). 1693 Project(projName). 1694 RepoURLType(fixture.RepoURLTypeFile). 1695 Path("two-nice-pods"). 1696 When(). 1697 PatchFile("pod-1.yaml", `[{"op": "add", "path": "/metadata/annotations", "value": {"argocd.argoproj.io/sync-options": "Prune=false"}}]`). 1698 IgnoreErrors(). 1699 CreateApp(). 1700 Then(). 1701 Expect(Error("", "do not match any of the allowed destinations in project")) 1702 } 1703 1704 func TestPermissionDeniedWithNegatedServer(t *testing.T) { 1705 projName := "argo-project" 1706 projectFixture. 1707 Given(t). 1708 Name(projName). 1709 Destination("!https://kubernetes.default.svc,*"). 1710 When(). 1711 Create() 1712 1713 repoFixture.GivenWithSameState(t). 1714 When(). 1715 Path(fixture.RepoURL(fixture.RepoURLTypeFile)). 1716 Project(projName). 1717 Create() 1718 1719 GivenWithSameState(t). 1720 Project(projName). 1721 RepoURLType(fixture.RepoURLTypeFile). 1722 Path("two-nice-pods"). 1723 When(). 1724 PatchFile("pod-1.yaml", `[{"op": "add", "path": "/metadata/annotations", "value": {"argocd.argoproj.io/sync-options": "Prune=false"}}]`). 1725 IgnoreErrors(). 1726 CreateApp(). 1727 Then(). 1728 Expect(Error("", "do not match any of the allowed destinations in project")) 1729 } 1730 1731 // make sure that if we deleted a resource from the app, it is not pruned if annotated with Prune=false 1732 func TestSyncOptionPruneFalse(t *testing.T) { 1733 Given(t). 1734 Path("two-nice-pods"). 1735 When(). 1736 PatchFile("pod-1.yaml", `[{"op": "add", "path": "/metadata/annotations", "value": {"argocd.argoproj.io/sync-options": "Prune=false"}}]`). 1737 CreateApp(). 1738 Sync(). 1739 Then(). 1740 Expect(OperationPhaseIs(OperationSucceeded)). 1741 Expect(SyncStatusIs(SyncStatusCodeSynced)). 1742 When(). 1743 DeleteFile("pod-1.yaml"). 1744 Refresh(RefreshTypeHard). 1745 IgnoreErrors(). 1746 Sync(). 1747 Then(). 1748 Expect(OperationPhaseIs(OperationSucceeded)). 1749 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 1750 Expect(ResourceSyncStatusIs("Pod", "pod-1", SyncStatusCodeOutOfSync)) 1751 } 1752 1753 // make sure that if we have an invalid manifest, we can add it if we disable validation, we get a server error rather than a client error 1754 func TestSyncOptionValidateFalse(t *testing.T) { 1755 Given(t). 1756 Path("crd-validation"). 1757 When(). 1758 CreateApp(). 1759 Then(). 1760 Expect(Success("")). 1761 When(). 1762 IgnoreErrors(). 1763 Sync(). 1764 Then(). 1765 // client error. K8s API changed error message w/ 1.25, so for now, we need to check both 1766 Expect(ErrorRegex("error validating data|of type int32", "")). 1767 When(). 1768 PatchFile("deployment.yaml", `[{"op": "add", "path": "/metadata/annotations", "value": {"argocd.argoproj.io/sync-options": "Validate=false"}}]`). 1769 Sync(). 1770 Then(). 1771 // server error 1772 Expect(Error("cannot be handled as a Deployment", "")) 1773 } 1774 1775 // make sure that, if we have a resource that needs pruning, but we're ignoring it, the app is in-sync 1776 func TestCompareOptionIgnoreExtraneous(t *testing.T) { 1777 Given(t). 1778 Prune(false). 1779 Path("two-nice-pods"). 1780 When(). 1781 PatchFile("pod-1.yaml", `[{"op": "add", "path": "/metadata/annotations", "value": {"argocd.argoproj.io/compare-options": "IgnoreExtraneous"}}]`). 1782 CreateApp(). 1783 Sync(). 1784 Then(). 1785 Expect(OperationPhaseIs(OperationSucceeded)). 1786 Expect(SyncStatusIs(SyncStatusCodeSynced)). 1787 When(). 1788 DeleteFile("pod-1.yaml"). 1789 Refresh(RefreshTypeHard). 1790 Then(). 1791 Expect(SyncStatusIs(SyncStatusCodeSynced)). 1792 And(func(app *Application) { 1793 assert.Len(t, app.Status.Resources, 2) 1794 statusByName := map[string]SyncStatusCode{} 1795 for _, r := range app.Status.Resources { 1796 statusByName[r.Name] = r.Status 1797 } 1798 assert.Equal(t, SyncStatusCodeOutOfSync, statusByName["pod-1"]) 1799 assert.Equal(t, SyncStatusCodeSynced, statusByName["pod-2"]) 1800 }). 1801 When(). 1802 Sync(). 1803 Then(). 1804 Expect(OperationPhaseIs(OperationSucceeded)). 1805 Expect(SyncStatusIs(SyncStatusCodeSynced)) 1806 } 1807 1808 func TestSourceNamespaceCanBeMigratedToManagedNamespaceWithoutBeingPrunedOrOutOfSync(t *testing.T) { 1809 Given(t). 1810 Prune(true). 1811 Path("guestbook-with-plain-namespace-manifest"). 1812 When(). 1813 PatchFile("guestbook-ui-namespace.yaml", fmt.Sprintf(`[{"op": "replace", "path": "/metadata/name", "value": %q}]`, fixture.DeploymentNamespace())). 1814 CreateApp(). 1815 Sync(). 1816 Then(). 1817 Expect(OperationPhaseIs(OperationSucceeded)). 1818 Expect(SyncStatusIs(SyncStatusCodeSynced)). 1819 When(). 1820 PatchApp(`[{ 1821 "op": "add", 1822 "path": "/spec/syncPolicy", 1823 "value": { "prune": true, "syncOptions": ["PrunePropagationPolicy=foreground"], "managedNamespaceMetadata": { "labels": { "foo": "bar" } } } 1824 }]`). 1825 Sync(). 1826 Then(). 1827 Expect(OperationPhaseIs(OperationSucceeded)). 1828 Expect(SyncStatusIs(SyncStatusCodeSynced)). 1829 And(func(app *Application) { 1830 assert.Equal(t, &ManagedNamespaceMetadata{Labels: map[string]string{"foo": "bar"}}, app.Spec.SyncPolicy.ManagedNamespaceMetadata) 1831 }). 1832 When(). 1833 DeleteFile("guestbook-ui-namespace.yaml"). 1834 Refresh(RefreshTypeHard). 1835 Sync(). 1836 Wait(). 1837 Then(). 1838 Expect(OperationPhaseIs(OperationSucceeded)). 1839 Expect(SyncStatusIs(SyncStatusCodeSynced)) 1840 } 1841 1842 func TestSelfManagedApps(t *testing.T) { 1843 Given(t). 1844 Path("self-managed-app"). 1845 When(). 1846 PatchFile("resources.yaml", fmt.Sprintf(`[{"op": "replace", "path": "/spec/source/repoURL", "value": %q}]`, fixture.RepoURL(fixture.RepoURLTypeFile))). 1847 CreateApp(). 1848 Sync(). 1849 Then(). 1850 Expect(OperationPhaseIs(OperationSucceeded)). 1851 Expect(SyncStatusIs(SyncStatusCodeSynced)). 1852 And(func(a *Application) { 1853 ctx, cancel := context.WithTimeout(t.Context(), time.Second*3) 1854 defer cancel() 1855 1856 reconciledCount := 0 1857 var lastReconciledAt *metav1.Time 1858 for event := range fixture.ArgoCDClientset.WatchApplicationWithRetry(ctx, a.Name, a.ResourceVersion) { 1859 reconciledAt := event.Application.Status.ReconciledAt 1860 if reconciledAt == nil { 1861 reconciledAt = &metav1.Time{} 1862 } 1863 if lastReconciledAt != nil && !lastReconciledAt.Equal(reconciledAt) { 1864 reconciledCount = reconciledCount + 1 1865 } 1866 lastReconciledAt = reconciledAt 1867 } 1868 1869 assert.Less(t, reconciledCount, 3, "Application was reconciled too many times") 1870 }) 1871 } 1872 1873 func TestExcludedResource(t *testing.T) { 1874 Given(t). 1875 ResourceOverrides(map[string]ResourceOverride{"apps/Deployment": {Actions: actionsConfig}}). 1876 Path(guestbookPath). 1877 ResourceFilter(settings.ResourcesFilter{ 1878 ResourceExclusions: []settings.FilteredResource{{Kinds: []string{kube.DeploymentKind}}}, 1879 }). 1880 When(). 1881 CreateApp(). 1882 Sync(). 1883 Refresh(RefreshTypeNormal). 1884 Then(). 1885 Expect(Condition(ApplicationConditionExcludedResourceWarning, "Resource apps/Deployment guestbook-ui is excluded in the settings")) 1886 } 1887 1888 func TestRevisionHistoryLimit(t *testing.T) { 1889 Given(t). 1890 Path("config-map"). 1891 When(). 1892 CreateApp(). 1893 Sync(). 1894 Then(). 1895 Expect(OperationPhaseIs(OperationSucceeded)). 1896 Expect(SyncStatusIs(SyncStatusCodeSynced)). 1897 And(func(app *Application) { 1898 assert.Len(t, app.Status.History, 1) 1899 }). 1900 When(). 1901 AppSet("--revision-history-limit", "1"). 1902 Sync(). 1903 Then(). 1904 Expect(OperationPhaseIs(OperationSucceeded)). 1905 Expect(SyncStatusIs(SyncStatusCodeSynced)). 1906 And(func(app *Application) { 1907 assert.Len(t, app.Status.History, 1) 1908 }) 1909 } 1910 1911 func TestOrphanedResource(t *testing.T) { 1912 fixture.SkipOnEnv(t, "OPENSHIFT") 1913 Given(t). 1914 ProjectSpec(AppProjectSpec{ 1915 SourceRepos: []string{"*"}, 1916 Destinations: []ApplicationDestination{{Namespace: "*", Server: "*"}}, 1917 OrphanedResources: &OrphanedResourcesMonitorSettings{Warn: ptr.To(true)}, 1918 }). 1919 Path(guestbookPath). 1920 When(). 1921 CreateApp(). 1922 Sync(). 1923 Then(). 1924 Expect(SyncStatusIs(SyncStatusCodeSynced)). 1925 Expect(NoConditions()). 1926 When(). 1927 And(func() { 1928 errors.NewHandler(t).FailOnErr(fixture.KubeClientset.CoreV1().ConfigMaps(fixture.DeploymentNamespace()).Create(t.Context(), &corev1.ConfigMap{ 1929 ObjectMeta: metav1.ObjectMeta{ 1930 Name: "orphaned-configmap", 1931 }, 1932 }, metav1.CreateOptions{})) 1933 }). 1934 Refresh(RefreshTypeNormal). 1935 Then(). 1936 Expect(Condition(ApplicationConditionOrphanedResourceWarning, "Application has 1 orphaned resources")). 1937 And(func(app *Application) { 1938 output, err := fixture.RunCli("app", "resources", app.Name) 1939 require.NoError(t, err) 1940 assert.Contains(t, output, "orphaned-configmap") 1941 }). 1942 Given(). 1943 ProjectSpec(AppProjectSpec{ 1944 SourceRepos: []string{"*"}, 1945 Destinations: []ApplicationDestination{{Namespace: "*", Server: "*"}}, 1946 OrphanedResources: &OrphanedResourcesMonitorSettings{Warn: ptr.To(true), Ignore: []OrphanedResourceKey{{Group: "Test", Kind: "ConfigMap"}}}, 1947 }). 1948 When(). 1949 Refresh(RefreshTypeNormal). 1950 Then(). 1951 Expect(Condition(ApplicationConditionOrphanedResourceWarning, "Application has 1 orphaned resources")). 1952 And(func(app *Application) { 1953 output, err := fixture.RunCli("app", "resources", app.Name) 1954 require.NoError(t, err) 1955 assert.Contains(t, output, "orphaned-configmap") 1956 }). 1957 Given(). 1958 ProjectSpec(AppProjectSpec{ 1959 SourceRepos: []string{"*"}, 1960 Destinations: []ApplicationDestination{{Namespace: "*", Server: "*"}}, 1961 OrphanedResources: &OrphanedResourcesMonitorSettings{Warn: ptr.To(true), Ignore: []OrphanedResourceKey{{Kind: "ConfigMap"}}}, 1962 }). 1963 When(). 1964 Refresh(RefreshTypeNormal). 1965 Then(). 1966 Expect(SyncStatusIs(SyncStatusCodeSynced)). 1967 Expect(NoConditions()). 1968 And(func(app *Application) { 1969 output, err := fixture.RunCli("app", "resources", app.Name) 1970 require.NoError(t, err) 1971 assert.NotContains(t, output, "orphaned-configmap") 1972 }). 1973 Given(). 1974 ProjectSpec(AppProjectSpec{ 1975 SourceRepos: []string{"*"}, 1976 Destinations: []ApplicationDestination{{Namespace: "*", Server: "*"}}, 1977 OrphanedResources: &OrphanedResourcesMonitorSettings{Warn: ptr.To(true), Ignore: []OrphanedResourceKey{{Kind: "ConfigMap", Name: "orphaned-configmap"}}}, 1978 }). 1979 When(). 1980 Refresh(RefreshTypeNormal). 1981 Then(). 1982 Expect(SyncStatusIs(SyncStatusCodeSynced)). 1983 Expect(NoConditions()). 1984 And(func(app *Application) { 1985 output, err := fixture.RunCli("app", "resources", app.Name) 1986 require.NoError(t, err) 1987 assert.NotContains(t, output, "orphaned-configmap") 1988 }). 1989 Given(). 1990 ProjectSpec(AppProjectSpec{ 1991 SourceRepos: []string{"*"}, 1992 Destinations: []ApplicationDestination{{Namespace: "*", Server: "*"}}, 1993 OrphanedResources: nil, 1994 }). 1995 When(). 1996 Refresh(RefreshTypeNormal). 1997 Then(). 1998 Expect(SyncStatusIs(SyncStatusCodeSynced)). 1999 Expect(NoConditions()) 2000 } 2001 2002 func TestNotPermittedResources(t *testing.T) { 2003 ctx := Given(t) 2004 2005 pathType := networkingv1.PathTypePrefix 2006 ingress := &networkingv1.Ingress{ 2007 ObjectMeta: metav1.ObjectMeta{ 2008 Name: "sample-ingress", 2009 Labels: map[string]string{ 2010 common.LabelKeyAppInstance: ctx.GetName(), 2011 }, 2012 }, 2013 Spec: networkingv1.IngressSpec{ 2014 Rules: []networkingv1.IngressRule{{ 2015 IngressRuleValue: networkingv1.IngressRuleValue{ 2016 HTTP: &networkingv1.HTTPIngressRuleValue{ 2017 Paths: []networkingv1.HTTPIngressPath{{ 2018 Path: "/", 2019 Backend: networkingv1.IngressBackend{ 2020 Service: &networkingv1.IngressServiceBackend{ 2021 Name: "guestbook-ui", 2022 Port: networkingv1.ServiceBackendPort{Number: 80}, 2023 }, 2024 }, 2025 PathType: &pathType, 2026 }}, 2027 }, 2028 }, 2029 }}, 2030 }, 2031 } 2032 defer func() { 2033 log.Infof("Ingress 'sample-ingress' deleted from %s", fixture.TestNamespace()) 2034 require.NoError(t, fixture.KubeClientset.NetworkingV1().Ingresses(fixture.TestNamespace()).Delete(t.Context(), "sample-ingress", metav1.DeleteOptions{})) 2035 }() 2036 2037 svc := &corev1.Service{ 2038 ObjectMeta: metav1.ObjectMeta{ 2039 Name: "guestbook-ui", 2040 Labels: map[string]string{ 2041 common.LabelKeyAppInstance: ctx.GetName(), 2042 }, 2043 }, 2044 Spec: corev1.ServiceSpec{ 2045 Ports: []corev1.ServicePort{{ 2046 Port: 80, 2047 TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: 80}, 2048 }}, 2049 Selector: map[string]string{ 2050 "app": "guestbook-ui", 2051 }, 2052 }, 2053 } 2054 2055 ctx.ProjectSpec(AppProjectSpec{ 2056 SourceRepos: []string{"*"}, 2057 Destinations: []ApplicationDestination{{Namespace: fixture.DeploymentNamespace(), Server: "*"}}, 2058 NamespaceResourceBlacklist: []metav1.GroupKind{ 2059 {Group: "", Kind: "Service"}, 2060 }, 2061 }). 2062 And(func() { 2063 errors.NewHandler(t).FailOnErr(fixture.KubeClientset.NetworkingV1().Ingresses(fixture.TestNamespace()).Create(t.Context(), ingress, metav1.CreateOptions{})) 2064 errors.NewHandler(t).FailOnErr(fixture.KubeClientset.CoreV1().Services(fixture.DeploymentNamespace()).Create(t.Context(), svc, metav1.CreateOptions{})) 2065 }). 2066 Path(guestbookPath). 2067 When(). 2068 CreateApp(). 2069 Then(). 2070 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 2071 And(func(app *Application) { 2072 statusByKind := make(map[string]ResourceStatus) 2073 for _, res := range app.Status.Resources { 2074 statusByKind[res.Kind] = res 2075 } 2076 _, hasIngress := statusByKind[kube.IngressKind] 2077 assert.False(t, hasIngress, "Ingress is prohibited not managed object and should be even visible to user") 2078 serviceStatus := statusByKind[kube.ServiceKind] 2079 assert.Equal(t, SyncStatusCodeUnknown, serviceStatus.Status, "Service is prohibited managed resource so should be set to Unknown") 2080 deploymentStatus := statusByKind[kube.DeploymentKind] 2081 assert.Equal(t, SyncStatusCodeOutOfSync, deploymentStatus.Status) 2082 }). 2083 When(). 2084 Delete(true). 2085 Then(). 2086 Expect(DoesNotExist()) 2087 2088 // Make sure prohibited resources are not deleted during application deletion 2089 errors.NewHandler(t).FailOnErr(fixture.KubeClientset.NetworkingV1().Ingresses(fixture.TestNamespace()).Get(t.Context(), "sample-ingress", metav1.GetOptions{})) 2090 errors.NewHandler(t).FailOnErr(fixture.KubeClientset.CoreV1().Services(fixture.DeploymentNamespace()).Get(t.Context(), "guestbook-ui", metav1.GetOptions{})) 2091 } 2092 2093 func TestSyncWithInfos(t *testing.T) { 2094 expectedInfo := make([]*Info, 2) 2095 expectedInfo[0] = &Info{Name: "name1", Value: "val1"} 2096 expectedInfo[1] = &Info{Name: "name2", Value: "val2"} 2097 2098 Given(t). 2099 Path(guestbookPath). 2100 When(). 2101 CreateApp(). 2102 Then(). 2103 And(func(app *Application) { 2104 _, err := fixture.RunCli("app", "sync", app.Name, 2105 "--info", fmt.Sprintf("%s=%s", expectedInfo[0].Name, expectedInfo[0].Value), 2106 "--info", fmt.Sprintf("%s=%s", expectedInfo[1].Name, expectedInfo[1].Value)) 2107 require.NoError(t, err) 2108 }). 2109 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2110 And(func(app *Application) { 2111 assert.ElementsMatch(t, app.Status.OperationState.Operation.Info, expectedInfo) 2112 }) 2113 } 2114 2115 // TestSyncWithRetryAndRefreshEnabled verifies that sync+refresh picks up new commits automatically on the original source 2116 // at the time the sync was triggered 2117 func TestSyncWithRetryAndRefreshEnabled(t *testing.T) { 2118 Given(t). 2119 Timeout(2). // Quick timeout since Sync operation is expected to retry forever 2120 Path(guestbookPath). 2121 When(). 2122 CreateFromFile(func(app *Application) { 2123 app.Spec.SyncPolicy = &SyncPolicy{ 2124 Retry: &RetryStrategy{ 2125 Limit: -1, // Repeat forever 2126 Refresh: true, 2127 Backoff: &Backoff{ 2128 Duration: time.Second.String(), 2129 Factor: ptr.To(int64(1)), 2130 MaxDuration: time.Second.String(), 2131 }, 2132 }, 2133 } 2134 }). 2135 Sync(). 2136 Then(). 2137 Expect(OperationPhaseIs(OperationSucceeded)). 2138 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2139 When(). 2140 PatchFile("guestbook-ui-deployment.yaml", `[{"op": "replace", "path": "/spec/revisionHistoryLimit", "value": "badValue"}]`). 2141 IgnoreErrors(). 2142 Sync(). 2143 DoNotIgnoreErrors(). 2144 Then(). 2145 Expect(OperationPhaseIs(OperationRunning)). 2146 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 2147 Expect(OperationRetriedMinimumTimes(1)). 2148 When(). 2149 PatchApp(`[{"op": "add", "path": "/spec/source/path", "value": "failure-during-sync"}]`). 2150 // push a fixed commit on HEAD branch 2151 PatchFile("guestbook-ui-deployment.yaml", `[{"op": "replace", "path": "/spec/revisionHistoryLimit", "value": 42}]`). 2152 IgnoreErrors(). 2153 Sync(). 2154 DoNotIgnoreErrors(). 2155 Then(). 2156 Expect(Status(func(status ApplicationStatus) (bool, string) { 2157 // Validate that the history contains the sync to the previous sources 2158 // The history will only contain successful sync 2159 if len(status.History) != 2 { 2160 return false, "expected len to be 2" 2161 } 2162 if status.History[1].Source.Path != guestbookPath { 2163 return false, fmt.Sprintf("expected source path to be '%s'", guestbookPath) 2164 } 2165 return true, "" 2166 })) 2167 } 2168 2169 // Given: argocd app create does not provide --dest-namespace 2170 // 2171 // Manifest contains resource console which does not require namespace 2172 // 2173 // Expect: no app.Status.Conditions 2174 func TestCreateAppWithNoNameSpaceForGlobalResource(t *testing.T) { 2175 Given(t). 2176 Path(globalWithNoNameSpace). 2177 When(). 2178 CreateWithNoNameSpace(). 2179 Then(). 2180 And(func(app *Application) { 2181 app, err := fixture.AppClientset.ArgoprojV1alpha1().Applications(fixture.TestNamespace()).Get(t.Context(), app.Name, metav1.GetOptions{}) 2182 require.NoError(t, err) 2183 assert.Empty(t, app.Status.Conditions) 2184 }) 2185 } 2186 2187 // Given: argocd app create does not provide --dest-namespace 2188 // 2189 // Manifest contains resource deployment, and service which requires namespace 2190 // Deployment and service do not have namespace in manifest 2191 // 2192 // Expect: app.Status.Conditions for deployment ans service which does not have namespace in manifest 2193 func TestCreateAppWithNoNameSpaceWhenRequired(t *testing.T) { 2194 Given(t). 2195 Path(guestbookPath). 2196 When(). 2197 CreateWithNoNameSpace(). 2198 Refresh(RefreshTypeNormal). 2199 Then(). 2200 And(func(app *Application) { 2201 updatedApp, err := fixture.AppClientset.ArgoprojV1alpha1().Applications(fixture.TestNamespace()).Get(t.Context(), app.Name, metav1.GetOptions{}) 2202 require.NoError(t, err) 2203 2204 assert.Len(t, updatedApp.Status.Conditions, 2) 2205 assert.Equal(t, ApplicationConditionInvalidSpecError, updatedApp.Status.Conditions[0].Type) 2206 assert.Equal(t, ApplicationConditionInvalidSpecError, updatedApp.Status.Conditions[1].Type) 2207 }) 2208 } 2209 2210 // Given: argocd app create does not provide --dest-namespace 2211 // 2212 // Manifest contains resource deployment, and service which requires namespace 2213 // Some deployment and service has namespace in manifest 2214 // Some deployment and service does not have namespace in manifest 2215 // 2216 // Expect: app.Status.Conditions for deployment and service which does not have namespace in manifest 2217 func TestCreateAppWithNoNameSpaceWhenRequired2(t *testing.T) { 2218 Given(t). 2219 Path(guestbookWithNamespace). 2220 When(). 2221 CreateWithNoNameSpace(). 2222 Refresh(RefreshTypeNormal). 2223 Then(). 2224 And(func(app *Application) { 2225 updatedApp, err := fixture.AppClientset.ArgoprojV1alpha1().Applications(fixture.TestNamespace()).Get(t.Context(), app.Name, metav1.GetOptions{}) 2226 require.NoError(t, err) 2227 2228 assert.Len(t, updatedApp.Status.Conditions, 2) 2229 assert.Equal(t, ApplicationConditionInvalidSpecError, updatedApp.Status.Conditions[0].Type) 2230 assert.Equal(t, ApplicationConditionInvalidSpecError, updatedApp.Status.Conditions[1].Type) 2231 }) 2232 } 2233 2234 func TestCreateAppWithInClusterDisabled(t *testing.T) { 2235 Given(t). 2236 Path(guestbookPath). 2237 DestServer(KubernetesInternalAPIServerAddr). 2238 When(). 2239 SetParamInSettingConfigMap("cluster.inClusterEnabled", "false"). 2240 IgnoreErrors(). 2241 CreateApp(). 2242 Then(). 2243 // RPC error messages are quoted: time="2024-12-18T04:13:58Z" level=fatal msg="<Quoted value>" 2244 Expect(Error("", fmt.Sprintf(`cluster \"%s\" is disabled`, KubernetesInternalAPIServerAddr))) 2245 } 2246 2247 func TestListResource(t *testing.T) { 2248 fixture.SkipOnEnv(t, "OPENSHIFT") 2249 Given(t). 2250 ProjectSpec(AppProjectSpec{ 2251 SourceRepos: []string{"*"}, 2252 Destinations: []ApplicationDestination{{Namespace: "*", Server: "*"}}, 2253 OrphanedResources: &OrphanedResourcesMonitorSettings{Warn: ptr.To(true)}, 2254 }). 2255 Path(guestbookPath). 2256 When(). 2257 CreateApp(). 2258 Sync(). 2259 Then(). 2260 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2261 Expect(NoConditions()). 2262 When(). 2263 And(func() { 2264 errors.NewHandler(t).FailOnErr(fixture.KubeClientset.CoreV1().ConfigMaps(fixture.DeploymentNamespace()).Create(t.Context(), &corev1.ConfigMap{ 2265 ObjectMeta: metav1.ObjectMeta{ 2266 Name: "orphaned-configmap", 2267 }, 2268 }, metav1.CreateOptions{})) 2269 }). 2270 Refresh(RefreshTypeNormal). 2271 Then(). 2272 Expect(Condition(ApplicationConditionOrphanedResourceWarning, "Application has 1 orphaned resources")). 2273 And(func(app *Application) { 2274 output, err := fixture.RunCli("app", "resources", app.Name) 2275 require.NoError(t, err) 2276 assert.Contains(t, output, "orphaned-configmap") 2277 assert.Contains(t, output, "guestbook-ui") 2278 }). 2279 And(func(app *Application) { 2280 output, err := fixture.RunCli("app", "resources", app.Name, "--orphaned=true") 2281 require.NoError(t, err) 2282 assert.Contains(t, output, "orphaned-configmap") 2283 assert.NotContains(t, output, "guestbook-ui") 2284 }). 2285 And(func(app *Application) { 2286 output, err := fixture.RunCli("app", "resources", app.Name, "--orphaned=false") 2287 require.NoError(t, err) 2288 assert.NotContains(t, output, "orphaned-configmap") 2289 assert.Contains(t, output, "guestbook-ui") 2290 }). 2291 Given(). 2292 ProjectSpec(AppProjectSpec{ 2293 SourceRepos: []string{"*"}, 2294 Destinations: []ApplicationDestination{{Namespace: "*", Server: "*"}}, 2295 OrphanedResources: nil, 2296 }). 2297 When(). 2298 Refresh(RefreshTypeNormal). 2299 Then(). 2300 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2301 Expect(NoConditions()) 2302 } 2303 2304 // Given application is set with --sync-option CreateNamespace=true 2305 // 2306 // application --dest-namespace does not exist 2307 // 2308 // Verity application --dest-namespace is created 2309 // 2310 // application sync successful 2311 // when application is deleted, --dest-namespace is not deleted 2312 func TestNamespaceAutoCreation(t *testing.T) { 2313 fixture.SkipOnEnv(t, "OPENSHIFT") 2314 updatedNamespace := getNewNamespace(t) 2315 defer func() { 2316 if !t.Skipped() { 2317 _, err := fixture.Run("", "kubectl", "delete", "namespace", updatedNamespace) 2318 require.NoError(t, err) 2319 } 2320 }() 2321 Given(t). 2322 Timeout(30). 2323 Path("guestbook"). 2324 When(). 2325 CreateApp("--sync-option", "CreateNamespace=true"). 2326 Then(). 2327 And(func(_ *Application) { 2328 // Make sure the namespace we are about to update to does not exist 2329 _, err := fixture.Run("", "kubectl", "get", "namespace", updatedNamespace) 2330 assert.ErrorContains(t, err, "not found") 2331 }). 2332 When(). 2333 AppSet("--dest-namespace", updatedNamespace). 2334 Sync(). 2335 Then(). 2336 Expect(Success("")). 2337 Expect(OperationPhaseIs(OperationSucceeded)).Expect(ResourceHealthWithNamespaceIs("Deployment", "guestbook-ui", updatedNamespace, health.HealthStatusHealthy)). 2338 Expect(ResourceHealthWithNamespaceIs("Deployment", "guestbook-ui", updatedNamespace, health.HealthStatusHealthy)). 2339 Expect(ResourceSyncStatusWithNamespaceIs("Deployment", "guestbook-ui", updatedNamespace, SyncStatusCodeSynced)). 2340 Expect(ResourceSyncStatusWithNamespaceIs("Deployment", "guestbook-ui", updatedNamespace, SyncStatusCodeSynced)). 2341 When(). 2342 Delete(true). 2343 Then(). 2344 Expect(Success("")). 2345 And(func(_ *Application) { 2346 // Verify delete app does not delete the namespace auto created 2347 output, err := fixture.Run("", "kubectl", "get", "namespace", updatedNamespace) 2348 require.NoError(t, err) 2349 assert.Contains(t, output, updatedNamespace) 2350 }) 2351 } 2352 2353 func TestFailedSyncWithRetry(t *testing.T) { 2354 Given(t). 2355 Path("hook"). 2356 When(). 2357 PatchFile("hook.yaml", `[{"op": "replace", "path": "/metadata/annotations", "value": {"argocd.argoproj.io/hook": "PreSync"}}]`). 2358 // make hook fail 2359 PatchFile("hook.yaml", `[{"op": "replace", "path": "/spec/containers/0/command", "value": ["false"]}]`). 2360 CreateApp(). 2361 IgnoreErrors(). 2362 Sync("--retry-limit=1", "--retry-backoff-duration=1s"). 2363 Then(). 2364 Expect(OperationPhaseIs(OperationFailed)). 2365 Expect(OperationMessageContains("retried 1 times")) 2366 } 2367 2368 func TestCreateDisableValidation(t *testing.T) { 2369 Given(t). 2370 Path("baddir"). 2371 When(). 2372 CreateApp("--validate=false"). 2373 Then(). 2374 And(func(app *Application) { 2375 _, err := fixture.RunCli("app", "create", app.Name, "--upsert", "--validate=false", "--repo", fixture.RepoURL(fixture.RepoURLTypeFile), 2376 "--path", "baddir2", "--project", app.Spec.Project, "--dest-server", KubernetesInternalAPIServerAddr, "--dest-namespace", fixture.DeploymentNamespace()) 2377 require.NoError(t, err) 2378 }). 2379 When(). 2380 AppSet("--path", "baddir3", "--validate=false") 2381 } 2382 2383 func TestCreateFromPartialFile(t *testing.T) { 2384 partialApp := `metadata: 2385 labels: 2386 labels.local/from-file: file 2387 labels.local/from-args: file 2388 annotations: 2389 annotations.local/from-file: file 2390 finalizers: 2391 - resources-finalizer.argocd.argoproj.io 2392 spec: 2393 syncPolicy: 2394 automated: 2395 prune: true 2396 ` 2397 2398 path := "helm-values" 2399 Given(t). 2400 When(). 2401 // app should be auto-synced once created 2402 CreateFromPartialFile(partialApp, "--path", path, "-l", "labels.local/from-args=args", "--helm-set", "foo=foo"). 2403 Then(). 2404 Expect(Success("")). 2405 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2406 Expect(NoConditions()). 2407 And(func(app *Application) { 2408 assert.Equal(t, map[string]string{"labels.local/from-file": "file", "labels.local/from-args": "args"}, app.Labels) 2409 assert.Equal(t, map[string]string{"annotations.local/from-file": "file"}, app.Annotations) 2410 assert.Equal(t, []string{ResourcesFinalizerName}, app.Finalizers) 2411 assert.Equal(t, path, app.Spec.GetSource().Path) 2412 assert.Equal(t, []HelmParameter{{Name: "foo", Value: "foo"}}, app.Spec.GetSource().Helm.Parameters) 2413 }) 2414 } 2415 2416 // Ensure actions work when using a resource action that modifies status and/or spec 2417 func TestCRDStatusSubresourceAction(t *testing.T) { 2418 actions := ` 2419 discovery.lua: | 2420 actions = {} 2421 actions["update-spec"] = {["disabled"] = false} 2422 actions["update-status"] = {["disabled"] = false} 2423 actions["update-both"] = {["disabled"] = false} 2424 return actions 2425 definitions: 2426 - name: update-both 2427 action.lua: | 2428 obj.spec = {} 2429 obj.spec.foo = "update-both" 2430 obj.status = {} 2431 obj.status.bar = "update-both" 2432 return obj 2433 - name: update-spec 2434 action.lua: | 2435 obj.spec = {} 2436 obj.spec.foo = "update-spec" 2437 return obj 2438 - name: update-status 2439 action.lua: | 2440 obj.status = {} 2441 obj.status.bar = "update-status" 2442 return obj 2443 ` 2444 Given(t). 2445 Path("crd-subresource"). 2446 And(func() { 2447 require.NoError(t, fixture.SetResourceOverrides(map[string]ResourceOverride{ 2448 "argoproj.io/StatusSubResource": { 2449 Actions: actions, 2450 }, 2451 "argoproj.io/NonStatusSubResource": { 2452 Actions: actions, 2453 }, 2454 })) 2455 }). 2456 When().CreateApp().Sync().Then(). 2457 Expect(OperationPhaseIs(OperationSucceeded)).Expect(SyncStatusIs(SyncStatusCodeSynced)). 2458 When(). 2459 Refresh(RefreshTypeNormal). 2460 Then(). 2461 // tests resource actions on a CRD using status subresource 2462 And(func(app *Application) { 2463 _, err := fixture.RunCli("app", "actions", "run", app.Name, "--kind", "StatusSubResource", "update-both") 2464 require.NoError(t, err) 2465 text := errors.NewHandler(t).FailOnErr(fixture.Run(".", "kubectl", "-n", app.Spec.Destination.Namespace, "get", "statussubresources", "status-subresource", "-o", "jsonpath={.spec.foo}")).(string) 2466 assert.Equal(t, "update-both", text) 2467 text = errors.NewHandler(t).FailOnErr(fixture.Run(".", "kubectl", "-n", app.Spec.Destination.Namespace, "get", "statussubresources", "status-subresource", "-o", "jsonpath={.status.bar}")).(string) 2468 assert.Equal(t, "update-both", text) 2469 2470 _, err = fixture.RunCli("app", "actions", "run", app.Name, "--kind", "StatusSubResource", "update-spec") 2471 require.NoError(t, err) 2472 text = errors.NewHandler(t).FailOnErr(fixture.Run(".", "kubectl", "-n", app.Spec.Destination.Namespace, "get", "statussubresources", "status-subresource", "-o", "jsonpath={.spec.foo}")).(string) 2473 assert.Equal(t, "update-spec", text) 2474 2475 _, err = fixture.RunCli("app", "actions", "run", app.Name, "--kind", "StatusSubResource", "update-status") 2476 require.NoError(t, err) 2477 text = errors.NewHandler(t).FailOnErr(fixture.Run(".", "kubectl", "-n", app.Spec.Destination.Namespace, "get", "statussubresources", "status-subresource", "-o", "jsonpath={.status.bar}")).(string) 2478 assert.Equal(t, "update-status", text) 2479 }). 2480 // tests resource actions on a CRD *not* using status subresource 2481 And(func(app *Application) { 2482 _, err := fixture.RunCli("app", "actions", "run", app.Name, "--kind", "NonStatusSubResource", "update-both") 2483 require.NoError(t, err) 2484 text := errors.NewHandler(t).FailOnErr(fixture.Run(".", "kubectl", "-n", app.Spec.Destination.Namespace, "get", "nonstatussubresources", "non-status-subresource", "-o", "jsonpath={.spec.foo}")).(string) 2485 assert.Equal(t, "update-both", text) 2486 text = errors.NewHandler(t).FailOnErr(fixture.Run(".", "kubectl", "-n", app.Spec.Destination.Namespace, "get", "nonstatussubresources", "non-status-subresource", "-o", "jsonpath={.status.bar}")).(string) 2487 assert.Equal(t, "update-both", text) 2488 2489 _, err = fixture.RunCli("app", "actions", "run", app.Name, "--kind", "NonStatusSubResource", "update-spec") 2490 require.NoError(t, err) 2491 text = errors.NewHandler(t).FailOnErr(fixture.Run(".", "kubectl", "-n", app.Spec.Destination.Namespace, "get", "nonstatussubresources", "non-status-subresource", "-o", "jsonpath={.spec.foo}")).(string) 2492 assert.Equal(t, "update-spec", text) 2493 2494 _, err = fixture.RunCli("app", "actions", "run", app.Name, "--kind", "NonStatusSubResource", "update-status") 2495 require.NoError(t, err) 2496 text = errors.NewHandler(t).FailOnErr(fixture.Run(".", "kubectl", "-n", app.Spec.Destination.Namespace, "get", "nonstatussubresources", "non-status-subresource", "-o", "jsonpath={.status.bar}")).(string) 2497 assert.Equal(t, "update-status", text) 2498 }) 2499 } 2500 2501 func TestAppLogs(t *testing.T) { 2502 t.SkipNow() // Too flaky. https://github.com/argoproj/argo-cd/issues/13834 2503 fixture.SkipOnEnv(t, "OPENSHIFT") 2504 Given(t). 2505 Path("guestbook-logs"). 2506 When(). 2507 CreateApp(). 2508 Sync(). 2509 Then(). 2510 Expect(HealthIs(health.HealthStatusHealthy)). 2511 And(func(app *Application) { 2512 out, err := fixture.RunCliWithRetry(appLogsRetryCount, "app", "logs", app.Name, "--kind", "Deployment", "--group", "", "--name", "guestbook-ui") 2513 require.NoError(t, err) 2514 assert.Contains(t, out, "Hi") 2515 }). 2516 And(func(app *Application) { 2517 out, err := fixture.RunCliWithRetry(appLogsRetryCount, "app", "logs", app.Name, "--kind", "Pod") 2518 require.NoError(t, err) 2519 assert.Contains(t, out, "Hi") 2520 }). 2521 And(func(app *Application) { 2522 out, err := fixture.RunCliWithRetry(appLogsRetryCount, "app", "logs", app.Name, "--kind", "Service") 2523 require.NoError(t, err) 2524 assert.NotContains(t, out, "Hi") 2525 }) 2526 } 2527 2528 func TestAppWaitOperationInProgress(t *testing.T) { 2529 ctx := Given(t) 2530 ctx. 2531 And(func() { 2532 require.NoError(t, fixture.SetResourceOverrides(map[string]ResourceOverride{ 2533 "batch/Job": { 2534 HealthLua: `return { status = 'Running' }`, 2535 }, 2536 "apps/Deployment": { 2537 HealthLua: `return { status = 'Suspended' }`, 2538 }, 2539 })) 2540 }). 2541 Async(true). 2542 Path("hook-and-deployment"). 2543 When(). 2544 CreateApp(). 2545 Sync(). 2546 Then(). 2547 // stuck in running state 2548 Expect(OperationPhaseIs(OperationRunning)). 2549 When(). 2550 And(func() { 2551 _, err := fixture.RunCli("app", "wait", ctx.AppName(), "--suspended") 2552 require.NoError(t, err) 2553 }) 2554 } 2555 2556 func TestSyncOptionReplace(t *testing.T) { 2557 Given(t). 2558 Path("config-map"). 2559 When(). 2560 PatchFile("config-map.yaml", `[{"op": "add", "path": "/metadata/annotations", "value": {"argocd.argoproj.io/sync-options": "Replace=true"}}]`). 2561 CreateApp(). 2562 Sync(). 2563 Then(). 2564 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2565 And(func(app *Application) { 2566 assert.Equal(t, "configmap/my-map created", app.Status.OperationState.SyncResult.Resources[0].Message) 2567 }). 2568 When(). 2569 Sync(). 2570 Then(). 2571 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2572 And(func(app *Application) { 2573 assert.Equal(t, "configmap/my-map replaced", app.Status.OperationState.SyncResult.Resources[0].Message) 2574 }) 2575 } 2576 2577 func TestSyncOptionReplaceFromCLI(t *testing.T) { 2578 Given(t). 2579 Path("config-map"). 2580 Replace(). 2581 When(). 2582 CreateApp(). 2583 Sync(). 2584 Then(). 2585 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2586 And(func(app *Application) { 2587 assert.Equal(t, "configmap/my-map created", app.Status.OperationState.SyncResult.Resources[0].Message) 2588 }). 2589 When(). 2590 Sync(). 2591 Then(). 2592 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2593 And(func(app *Application) { 2594 assert.Equal(t, "configmap/my-map replaced", app.Status.OperationState.SyncResult.Resources[0].Message) 2595 }) 2596 } 2597 2598 func TestDiscoverNewCommit(t *testing.T) { 2599 var sha string 2600 Given(t). 2601 Path("config-map"). 2602 When(). 2603 CreateApp(). 2604 Sync(). 2605 Then(). 2606 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2607 And(func(app *Application) { 2608 sha = app.Status.Sync.Revision 2609 assert.NotEmpty(t, sha) 2610 }). 2611 When(). 2612 PatchFile("config-map.yaml", `[{"op": "replace", "path": "/data/foo", "value": "hello"}]`). 2613 Then(). 2614 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2615 // make sure new commit is not discovered immediately after push 2616 And(func(app *Application) { 2617 assert.Equal(t, sha, app.Status.Sync.Revision) 2618 }). 2619 When(). 2620 // make sure new commit is not discovered after refresh is requested 2621 Refresh(RefreshTypeNormal). 2622 Then(). 2623 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 2624 And(func(app *Application) { 2625 assert.NotEqual(t, sha, app.Status.Sync.Revision) 2626 }) 2627 } 2628 2629 func TestDisableManifestGeneration(t *testing.T) { 2630 Given(t). 2631 Path("guestbook"). 2632 When(). 2633 CreateApp(). 2634 Refresh(RefreshTypeHard). 2635 Then(). 2636 And(func(app *Application) { 2637 assert.Equal(t, ApplicationSourceTypeKustomize, app.Status.SourceType) 2638 }). 2639 When(). 2640 And(func() { 2641 require.NoError(t, fixture.SetEnableManifestGeneration(map[ApplicationSourceType]bool{ 2642 ApplicationSourceTypeKustomize: false, 2643 })) 2644 }). 2645 Refresh(RefreshTypeHard). 2646 Then(). 2647 And(func(_ *Application) { 2648 // Wait for refresh to complete 2649 time.Sleep(1 * time.Second) 2650 }). 2651 And(func(app *Application) { 2652 assert.Equal(t, ApplicationSourceTypeDirectory, app.Status.SourceType) 2653 }) 2654 } 2655 2656 func TestSwitchTrackingMethod(t *testing.T) { 2657 ctx := Given(t) 2658 2659 ctx. 2660 SetTrackingMethod(string(TrackingMethodAnnotation)). 2661 Path("deployment"). 2662 When(). 2663 CreateApp(). 2664 Sync(). 2665 Refresh(RefreshTypeNormal). 2666 Then(). 2667 Expect(OperationPhaseIs(OperationSucceeded)). 2668 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2669 Expect(HealthIs(health.HealthStatusHealthy)). 2670 When(). 2671 And(func() { 2672 // Add resource with tracking annotation. This should put the 2673 // application OutOfSync. 2674 errors.NewHandler(t).FailOnErr(fixture.KubeClientset.CoreV1().ConfigMaps(fixture.DeploymentNamespace()).Create(t.Context(), &corev1.ConfigMap{ 2675 ObjectMeta: metav1.ObjectMeta{ 2676 Name: "other-configmap", 2677 Annotations: map[string]string{ 2678 common.AnnotationKeyAppInstance: fmt.Sprintf("%s:/ConfigMap:%s/other-configmap", fixture.Name(), fixture.DeploymentNamespace()), 2679 }, 2680 }, 2681 }, metav1.CreateOptions{})) 2682 }). 2683 Then(). 2684 Expect(OperationPhaseIs(OperationSucceeded)). 2685 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 2686 Expect(HealthIs(health.HealthStatusHealthy)). 2687 When(). 2688 And(func() { 2689 // Delete resource to bring application back in sync 2690 errors.NewHandler(t).FailOnErr(nil, fixture.KubeClientset.CoreV1().ConfigMaps(fixture.DeploymentNamespace()).Delete(t.Context(), "other-configmap", metav1.DeleteOptions{})) 2691 }). 2692 Then(). 2693 Expect(OperationPhaseIs(OperationSucceeded)). 2694 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2695 Expect(HealthIs(health.HealthStatusHealthy)). 2696 When(). 2697 SetTrackingMethod(string(TrackingMethodLabel)). 2698 Sync(). 2699 Then(). 2700 Expect(OperationPhaseIs(OperationSucceeded)). 2701 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2702 Expect(HealthIs(health.HealthStatusHealthy)). 2703 When(). 2704 And(func() { 2705 // Add a resource with a tracking annotation. This should not 2706 // affect the application, because we now use the tracking method 2707 // "label". 2708 errors.NewHandler(t).FailOnErr(fixture.KubeClientset.CoreV1().ConfigMaps(fixture.DeploymentNamespace()).Create(t.Context(), &corev1.ConfigMap{ 2709 ObjectMeta: metav1.ObjectMeta{ 2710 Name: "other-configmap", 2711 Annotations: map[string]string{ 2712 common.AnnotationKeyAppInstance: fmt.Sprintf("%s:/ConfigMap:%s/other-configmap", fixture.Name(), fixture.DeploymentNamespace()), 2713 }, 2714 }, 2715 }, metav1.CreateOptions{})) 2716 }). 2717 Then(). 2718 Expect(OperationPhaseIs(OperationSucceeded)). 2719 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2720 Expect(HealthIs(health.HealthStatusHealthy)). 2721 When(). 2722 And(func() { 2723 // Add a resource with the tracking label. The app should become 2724 // OutOfSync. 2725 errors.NewHandler(t).FailOnErr(fixture.KubeClientset.CoreV1().ConfigMaps(fixture.DeploymentNamespace()).Create(t.Context(), &corev1.ConfigMap{ 2726 ObjectMeta: metav1.ObjectMeta{ 2727 Name: "extra-configmap", 2728 Labels: map[string]string{ 2729 common.LabelKeyAppInstance: fixture.Name(), 2730 }, 2731 }, 2732 }, metav1.CreateOptions{})) 2733 }). 2734 Then(). 2735 Expect(OperationPhaseIs(OperationSucceeded)). 2736 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 2737 Expect(HealthIs(health.HealthStatusHealthy)). 2738 When(). 2739 And(func() { 2740 // Delete resource to bring application back in sync 2741 errors.NewHandler(t).FailOnErr(nil, fixture.KubeClientset.CoreV1().ConfigMaps(fixture.DeploymentNamespace()).Delete(t.Context(), "extra-configmap", metav1.DeleteOptions{})) 2742 }). 2743 Then(). 2744 Expect(OperationPhaseIs(OperationSucceeded)). 2745 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2746 Expect(HealthIs(health.HealthStatusHealthy)) 2747 } 2748 2749 func TestSwitchTrackingLabel(t *testing.T) { 2750 ctx := Given(t) 2751 2752 require.NoError(t, fixture.SetTrackingMethod(string(TrackingMethodLabel))) 2753 ctx. 2754 Path("deployment"). 2755 When(). 2756 CreateApp(). 2757 Sync(). 2758 Refresh(RefreshTypeNormal). 2759 Then(). 2760 Expect(OperationPhaseIs(OperationSucceeded)). 2761 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2762 Expect(HealthIs(health.HealthStatusHealthy)). 2763 When(). 2764 And(func() { 2765 // Add extra resource that carries the default tracking label 2766 // We expect the app to go out of sync. 2767 errors.NewHandler(t).FailOnErr(fixture.KubeClientset.CoreV1().ConfigMaps(fixture.DeploymentNamespace()).Create(t.Context(), &corev1.ConfigMap{ 2768 ObjectMeta: metav1.ObjectMeta{ 2769 Name: "other-configmap", 2770 Labels: map[string]string{ 2771 common.LabelKeyAppInstance: fixture.Name(), 2772 }, 2773 }, 2774 }, metav1.CreateOptions{})) 2775 }). 2776 Then(). 2777 Expect(OperationPhaseIs(OperationSucceeded)). 2778 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 2779 Expect(HealthIs(health.HealthStatusHealthy)). 2780 When(). 2781 And(func() { 2782 // Delete resource to bring application back in sync 2783 errors.NewHandler(t).FailOnErr(nil, fixture.KubeClientset.CoreV1().ConfigMaps(fixture.DeploymentNamespace()).Delete(t.Context(), "other-configmap", metav1.DeleteOptions{})) 2784 }). 2785 Then(). 2786 Expect(OperationPhaseIs(OperationSucceeded)). 2787 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2788 Expect(HealthIs(health.HealthStatusHealthy)). 2789 When(). 2790 // Change tracking label 2791 SetTrackingLabel("argocd.tracking"). 2792 Sync(). 2793 Then(). 2794 Expect(OperationPhaseIs(OperationSucceeded)). 2795 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2796 Expect(HealthIs(health.HealthStatusHealthy)). 2797 When(). 2798 And(func() { 2799 // Create resource with the new tracking label, the application 2800 // is expected to go out of sync 2801 errors.NewHandler(t).FailOnErr(fixture.KubeClientset.CoreV1().ConfigMaps(fixture.DeploymentNamespace()).Create(t.Context(), &corev1.ConfigMap{ 2802 ObjectMeta: metav1.ObjectMeta{ 2803 Name: "other-configmap", 2804 Labels: map[string]string{ 2805 "argocd.tracking": fixture.Name(), 2806 }, 2807 }, 2808 }, metav1.CreateOptions{})) 2809 }). 2810 Then(). 2811 Expect(OperationPhaseIs(OperationSucceeded)). 2812 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 2813 Expect(HealthIs(health.HealthStatusHealthy)). 2814 When(). 2815 And(func() { 2816 // Delete resource to bring application back in sync 2817 errors.NewHandler(t).FailOnErr(nil, fixture.KubeClientset.CoreV1().ConfigMaps(fixture.DeploymentNamespace()).Delete(t.Context(), "other-configmap", metav1.DeleteOptions{})) 2818 }). 2819 Then(). 2820 Expect(OperationPhaseIs(OperationSucceeded)). 2821 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2822 Expect(HealthIs(health.HealthStatusHealthy)). 2823 When(). 2824 And(func() { 2825 // Add extra resource that carries the default tracking label 2826 // We expect the app to stay in sync, because the configured 2827 // label is different. 2828 errors.NewHandler(t).FailOnErr(fixture.KubeClientset.CoreV1().ConfigMaps(fixture.DeploymentNamespace()).Create(t.Context(), &corev1.ConfigMap{ 2829 ObjectMeta: metav1.ObjectMeta{ 2830 Name: "other-configmap", 2831 Labels: map[string]string{ 2832 common.LabelKeyAppInstance: fixture.Name(), 2833 }, 2834 }, 2835 }, metav1.CreateOptions{})) 2836 }). 2837 Then(). 2838 Expect(OperationPhaseIs(OperationSucceeded)). 2839 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2840 Expect(HealthIs(health.HealthStatusHealthy)) 2841 } 2842 2843 func TestAnnotationTrackingExtraResources(t *testing.T) { 2844 ctx := Given(t) 2845 2846 require.NoError(t, fixture.SetTrackingMethod(string(TrackingMethodAnnotation))) 2847 ctx. 2848 Path("deployment"). 2849 When(). 2850 CreateApp(). 2851 Sync(). 2852 Refresh(RefreshTypeNormal). 2853 Then(). 2854 Expect(OperationPhaseIs(OperationSucceeded)). 2855 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2856 Expect(HealthIs(health.HealthStatusHealthy)). 2857 When(). 2858 And(func() { 2859 // Add a resource with an annotation that is not referencing the 2860 // resource. 2861 errors.NewHandler(t).FailOnErr(fixture.KubeClientset.CoreV1().ConfigMaps(fixture.DeploymentNamespace()).Create(t.Context(), &corev1.ConfigMap{ 2862 ObjectMeta: metav1.ObjectMeta{ 2863 Name: "extra-configmap", 2864 Annotations: map[string]string{ 2865 common.AnnotationKeyAppInstance: fmt.Sprintf("%s:apps/Deployment:%s/guestbook-cm", fixture.Name(), fixture.DeploymentNamespace()), 2866 }, 2867 }, 2868 }, metav1.CreateOptions{})) 2869 }). 2870 Refresh(RefreshTypeNormal). 2871 Then(). 2872 Expect(OperationPhaseIs(OperationSucceeded)). 2873 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2874 Expect(HealthIs(health.HealthStatusHealthy)). 2875 When(). 2876 Sync("--prune"). 2877 And(func() { 2878 // The extra configmap must not be pruned, because it's not tracked 2879 cm, err := fixture.KubeClientset.CoreV1().ConfigMaps(fixture.DeploymentNamespace()).Get(t.Context(), "extra-configmap", metav1.GetOptions{}) 2880 require.NoError(t, err) 2881 require.Equal(t, "extra-configmap", cm.Name) 2882 }). 2883 And(func() { 2884 // Add a resource with an annotation that is self-referencing the 2885 // resource. 2886 errors.NewHandler(t).FailOnErr(fixture.KubeClientset.CoreV1().ConfigMaps(fixture.DeploymentNamespace()).Create(t.Context(), &corev1.ConfigMap{ 2887 ObjectMeta: metav1.ObjectMeta{ 2888 Name: "other-configmap", 2889 Annotations: map[string]string{ 2890 common.AnnotationKeyAppInstance: fmt.Sprintf("%s:/ConfigMap:%s/other-configmap", fixture.Name(), fixture.DeploymentNamespace()), 2891 }, 2892 }, 2893 }, metav1.CreateOptions{})) 2894 }). 2895 Refresh(RefreshTypeNormal). 2896 Then(). 2897 Expect(OperationPhaseIs(OperationSucceeded)). 2898 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 2899 Expect(HealthIs(health.HealthStatusHealthy)). 2900 When(). 2901 Sync("--prune"). 2902 And(func() { 2903 // The extra configmap must be pruned now, because it's tracked 2904 cm, err := fixture.KubeClientset.CoreV1().ConfigMaps(fixture.DeploymentNamespace()).Get(t.Context(), "other-configmap", metav1.GetOptions{}) 2905 require.Error(t, err) 2906 require.Empty(t, cm.Name) 2907 }). 2908 Then(). 2909 Expect(OperationPhaseIs(OperationSucceeded)). 2910 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2911 Expect(HealthIs(health.HealthStatusHealthy)). 2912 When(). 2913 And(func() { 2914 // Add a cluster-scoped resource that is not referencing itself 2915 errors.NewHandler(t).FailOnErr(fixture.KubeClientset.RbacV1().ClusterRoles().Create(t.Context(), &rbacv1.ClusterRole{ 2916 ObjectMeta: metav1.ObjectMeta{ 2917 Name: "e2e-test-clusterrole", 2918 Annotations: map[string]string{ 2919 common.AnnotationKeyAppInstance: fmt.Sprintf("%s:rbac.authorization.k8s.io/ClusterRole:%s/e2e-other-clusterrole", fixture.Name(), fixture.DeploymentNamespace()), 2920 }, 2921 Labels: map[string]string{ 2922 fixture.TestingLabel: "true", 2923 }, 2924 }, 2925 }, metav1.CreateOptions{})) 2926 }). 2927 Refresh(RefreshTypeNormal). 2928 Then(). 2929 Expect(OperationPhaseIs(OperationSucceeded)). 2930 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2931 Expect(HealthIs(health.HealthStatusHealthy)). 2932 When(). 2933 And(func() { 2934 // Add a cluster-scoped resource that is referencing itself 2935 errors.NewHandler(t).FailOnErr(fixture.KubeClientset.RbacV1().ClusterRoles().Create(t.Context(), &rbacv1.ClusterRole{ 2936 ObjectMeta: metav1.ObjectMeta{ 2937 Name: "e2e-other-clusterrole", 2938 Annotations: map[string]string{ 2939 common.AnnotationKeyAppInstance: fmt.Sprintf("%s:rbac.authorization.k8s.io/ClusterRole:%s/e2e-other-clusterrole", fixture.Name(), fixture.DeploymentNamespace()), 2940 }, 2941 Labels: map[string]string{ 2942 fixture.TestingLabel: "true", 2943 }, 2944 }, 2945 }, metav1.CreateOptions{})) 2946 }). 2947 Refresh(RefreshTypeNormal). 2948 Then(). 2949 Expect(OperationPhaseIs(OperationSucceeded)). 2950 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 2951 Expect(HealthIs(health.HealthStatusHealthy)). 2952 When(). 2953 Sync("--prune"). 2954 And(func() { 2955 // The extra configmap must be pruned now, because it's tracked and does not exist in git 2956 cr, err := fixture.KubeClientset.RbacV1().ClusterRoles().Get(t.Context(), "e2e-other-clusterrole", metav1.GetOptions{}) 2957 require.Error(t, err) 2958 require.Empty(t, cr.Name) 2959 }). 2960 Then(). 2961 Expect(OperationPhaseIs(OperationSucceeded)). 2962 Expect(SyncStatusIs(SyncStatusCodeSynced)). 2963 Expect(HealthIs(health.HealthStatusHealthy)) 2964 } 2965 2966 func TestCreateConfigMapsAndWaitForUpdate(t *testing.T) { 2967 Given(t). 2968 Path("config-map"). 2969 When(). 2970 CreateApp(). 2971 Sync(). 2972 Then(). 2973 And(func(app *Application) { 2974 _, err := fixture.RunCli("app", "set", app.Name, "--sync-policy", "automated") 2975 require.NoError(t, err) 2976 }). 2977 When(). 2978 AddFile("other-configmap.yaml", ` 2979 apiVersion: v1 2980 kind: ConfigMap 2981 metadata: 2982 name: other-map 2983 annotations: 2984 argocd.argoproj.io/sync-wave: "1" 2985 data: 2986 foo2: bar2`). 2987 AddFile("yet-another-configmap.yaml", ` 2988 apiVersion: v1 2989 kind: ConfigMap 2990 metadata: 2991 name: yet-another-map 2992 annotations: 2993 argocd.argoproj.io/sync-wave: "2" 2994 data: 2995 foo3: bar3`). 2996 PatchFile("kustomization.yaml", `[{"op": "add", "path": "/resources/-", "value": "other-configmap.yaml"}, {"op": "add", "path": "/resources/-", "value": "yet-another-configmap.yaml"}]`). 2997 Refresh(RefreshTypeNormal). 2998 Wait(). 2999 Then(). 3000 Expect(OperationPhaseIs(OperationSucceeded)). 3001 Expect(SyncStatusIs(SyncStatusCodeSynced)). 3002 Expect(HealthIs(health.HealthStatusHealthy)). 3003 Expect(ResourceHealthWithNamespaceIs("ConfigMap", "other-map", fixture.DeploymentNamespace(), health.HealthStatusHealthy)). 3004 Expect(ResourceSyncStatusWithNamespaceIs("ConfigMap", "other-map", fixture.DeploymentNamespace(), SyncStatusCodeSynced)). 3005 Expect(ResourceHealthWithNamespaceIs("ConfigMap", "yet-another-map", fixture.DeploymentNamespace(), health.HealthStatusHealthy)). 3006 Expect(ResourceSyncStatusWithNamespaceIs("ConfigMap", "yet-another-map", fixture.DeploymentNamespace(), SyncStatusCodeSynced)) 3007 } 3008 3009 func TestInstallationID(t *testing.T) { 3010 ctx := Given(t) 3011 ctx. 3012 SetTrackingMethod(string(TrackingMethodAnnotation)). 3013 And(func() { 3014 _, err := fixture.KubeClientset.CoreV1().ConfigMaps(fixture.DeploymentNamespace()).Create( 3015 t.Context(), &corev1.ConfigMap{ 3016 ObjectMeta: metav1.ObjectMeta{ 3017 Name: "test-configmap", 3018 Annotations: map[string]string{ 3019 common.AnnotationKeyAppInstance: fmt.Sprintf("%s:/ConfigMap:%s/test-configmap", ctx.AppName(), fixture.DeploymentNamespace()), 3020 }, 3021 }, 3022 }, metav1.CreateOptions{}) 3023 require.NoError(t, err) 3024 }). 3025 Path(guestbookPath). 3026 Prune(false). 3027 When().IgnoreErrors().CreateApp().Sync(). 3028 Then().Expect(OperationPhaseIs(OperationSucceeded)).Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 3029 And(func(app *Application) { 3030 var cm *ResourceStatus 3031 for i := range app.Status.Resources { 3032 if app.Status.Resources[i].Kind == "ConfigMap" && app.Status.Resources[i].Name == "test-configmap" { 3033 cm = &app.Status.Resources[i] 3034 break 3035 } 3036 } 3037 require.NotNil(t, cm) 3038 assert.Equal(t, SyncStatusCodeOutOfSync, cm.Status) 3039 }). 3040 When().SetInstallationID("test").Sync(). 3041 Then(). 3042 Expect(SyncStatusIs(SyncStatusCodeSynced)). 3043 And(func(app *Application) { 3044 require.Len(t, app.Status.Resources, 2) 3045 svc, err := fixture.KubeClientset.CoreV1().Services(fixture.DeploymentNamespace()).Get(t.Context(), "guestbook-ui", metav1.GetOptions{}) 3046 require.NoError(t, err) 3047 require.Equal(t, "test", svc.Annotations[common.AnnotationInstallationID]) 3048 3049 deploy, err := fixture.KubeClientset.AppsV1().Deployments(fixture.DeploymentNamespace()).Get(t.Context(), "guestbook-ui", metav1.GetOptions{}) 3050 require.NoError(t, err) 3051 require.Equal(t, "test", deploy.Annotations[common.AnnotationInstallationID]) 3052 }) 3053 } 3054 3055 func TestDeletionConfirmation(t *testing.T) { 3056 ctx := Given(t) 3057 ctx. 3058 And(func() { 3059 _, err := fixture.KubeClientset.CoreV1().ConfigMaps(fixture.DeploymentNamespace()).Create( 3060 t.Context(), &corev1.ConfigMap{ 3061 ObjectMeta: metav1.ObjectMeta{ 3062 Name: "test-configmap", 3063 Labels: map[string]string{ 3064 common.LabelKeyAppInstance: ctx.AppName(), 3065 }, 3066 Annotations: map[string]string{ 3067 AnnotationSyncOptions: "Prune=confirm", 3068 }, 3069 }, 3070 }, metav1.CreateOptions{}) 3071 require.NoError(t, err) 3072 }). 3073 Path(guestbookPath). 3074 Async(true). 3075 When(). 3076 PatchFile("guestbook-ui-deployment.yaml", `[{ "op": "add", "path": "/metadata/annotations", "value": { "argocd.argoproj.io/sync-options": "Delete=confirm" }}]`). 3077 CreateApp().Sync(). 3078 Then().Expect(OperationPhaseIs(OperationRunning)). 3079 When().ConfirmDeletion(). 3080 Then().Expect(OperationPhaseIs(OperationSucceeded)). 3081 When().Delete(true). 3082 Then(). 3083 And(func(app *Application) { 3084 assert.NotNil(t, app.DeletionTimestamp) 3085 }). 3086 When().ConfirmDeletion(). 3087 Then().Expect(DoesNotExist()) 3088 } 3089 3090 func TestLastTransitionTimeUnchangedError(t *testing.T) { 3091 // Ensure that, if the health status hasn't changed, the lastTransitionTime is not updated. 3092 3093 ctx := Given(t) 3094 ctx. 3095 Path(guestbookPath). 3096 When(). 3097 And(func() { 3098 // Manually create an application with an outdated reconciledAt field 3099 manifest := fmt.Sprintf(` 3100 apiVersion: argoproj.io/v1alpha1 3101 kind: Application 3102 metadata: 3103 name: %s 3104 spec: 3105 project: default 3106 source: 3107 repoURL: %s 3108 path: guestbook 3109 targetRevision: HEAD 3110 destination: 3111 server: https://non-existent-cluster 3112 namespace: default 3113 status: 3114 reconciledAt: "2023-01-01T00:00:00Z" 3115 health: 3116 status: Unknown 3117 lastTransitionTime: "2023-01-01T00:00:00Z" 3118 `, ctx.AppName(), fixture.RepoURL(fixture.RepoURLTypeFile)) 3119 _, err := fixture.RunWithStdin(manifest, "", "kubectl", "apply", "-n", fixture.ArgoCDNamespace, "-f", "-") 3120 require.NoError(t, err) 3121 }). 3122 Refresh(RefreshTypeNormal). 3123 Then(). 3124 And(func(app *Application) { 3125 // Verify the health status is still Unknown 3126 assert.Equal(t, health.HealthStatusUnknown, app.Status.Health.Status) 3127 3128 // Verify the lastTransitionTime has not been updated 3129 assert.Equal(t, "2023-01-01T00:00:00Z", app.Status.Health.LastTransitionTime.UTC().Format(time.RFC3339)) 3130 }) 3131 } 3132 3133 // TestServerSideDiffCommand tests the --server-side-diff flag for the app diff command 3134 func TestServerSideDiffCommand(t *testing.T) { 3135 Given(t). 3136 Path("two-nice-pods"). 3137 When(). 3138 CreateApp(). 3139 Sync(). 3140 Then(). 3141 Expect(OperationPhaseIs(OperationSucceeded)). 3142 Expect(SyncStatusIs(SyncStatusCodeSynced)). 3143 When(). 3144 // Create a diff by modifying a pod 3145 PatchFile("pod-1.yaml", `[{"op": "add", "path": "/metadata/annotations", "value": {"test": "server-side-diff"}}]`). 3146 AddFile("pod-3.yaml", `apiVersion: v1 3147 kind: Pod 3148 metadata: 3149 name: pod-3 3150 annotations: 3151 new: "pod" 3152 spec: 3153 containers: 3154 - name: main 3155 image: quay.io/argoprojlabs/argocd-e2e-container:0.1 3156 imagePullPolicy: IfNotPresent 3157 command: 3158 - "true" 3159 restartPolicy: Never 3160 `). 3161 Refresh(RefreshTypeHard). 3162 Then(). 3163 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 3164 And(func(app *Application) { 3165 // Test regular diff command 3166 regularOutput, err := fixture.RunCli("app", "diff", app.Name) 3167 require.Error(t, err) // diff command returns non-zero exit code when differences found 3168 assert.Contains(t, regularOutput, "===== /Pod") 3169 assert.Contains(t, regularOutput, "pod-1") 3170 assert.Contains(t, regularOutput, "pod-3") 3171 3172 // Test server-side diff command 3173 serverSideOutput, err := fixture.RunCli("app", "diff", app.Name, "--server-side-diff") 3174 require.Error(t, err) // diff command returns non-zero exit code when differences found 3175 assert.Contains(t, serverSideOutput, "===== /Pod") 3176 assert.Contains(t, serverSideOutput, "pod-1") 3177 assert.Contains(t, serverSideOutput, "pod-3") 3178 3179 // Both outputs should contain similar resource headers 3180 assert.Contains(t, regularOutput, "test: server-side-diff") 3181 assert.Contains(t, serverSideOutput, "test: server-side-diff") 3182 assert.Contains(t, regularOutput, "new: pod") 3183 assert.Contains(t, serverSideOutput, "new: pod") 3184 }) 3185 } 3186 3187 // TestServerSideDiffWithSyncedApp tests server-side diff when app is already synced (no differences) 3188 func TestServerSideDiffWithSyncedApp(t *testing.T) { 3189 Given(t). 3190 Path("guestbook"). 3191 When(). 3192 CreateApp(). 3193 Sync(). 3194 Then(). 3195 Expect(OperationPhaseIs(OperationSucceeded)). 3196 Expect(SyncStatusIs(SyncStatusCodeSynced)). 3197 And(func(app *Application) { 3198 // Test regular diff command with synced app 3199 regularOutput, err := fixture.RunCli("app", "diff", app.Name) 3200 require.NoError(t, err) // no differences, should return 0 3201 3202 // Test server-side diff command with synced app 3203 serverSideOutput, err := fixture.RunCli("app", "diff", app.Name, "--server-side-diff") 3204 require.NoError(t, err) // no differences, should return 0 3205 3206 // Both should produce similar output (minimal/no diff output) 3207 // The exact output may vary, but both should succeed without errors 3208 assert.NotContains(t, regularOutput, "===== ") 3209 assert.NotContains(t, serverSideOutput, "===== ") 3210 }) 3211 } 3212 3213 // TestServerSideDiffWithRevision tests server-side diff with a specific revision 3214 func TestServerSideDiffWithRevision(t *testing.T) { 3215 Given(t). 3216 Path("two-nice-pods"). 3217 When(). 3218 CreateApp(). 3219 Sync(). 3220 Then(). 3221 Expect(OperationPhaseIs(OperationSucceeded)). 3222 Expect(SyncStatusIs(SyncStatusCodeSynced)). 3223 When(). 3224 PatchFile("pod-1.yaml", `[{"op": "add", "path": "/metadata/labels", "value": {"version": "v1.1"}}]`). 3225 Sync(). 3226 Then(). 3227 Expect(OperationPhaseIs(OperationSucceeded)). 3228 And(func(app *Application) { 3229 // Get the current revision 3230 currentRevision := "" 3231 if len(app.Status.History) > 0 { 3232 currentRevision = app.Status.History[len(app.Status.History)-1].Revision 3233 } 3234 3235 if currentRevision != "" { 3236 // Test server-side diff with current revision (should show no differences) 3237 output, err := fixture.RunCli("app", "diff", app.Name, "--server-side-diff", "--revision", currentRevision) 3238 require.NoError(t, err) // no differences expected 3239 assert.NotContains(t, output, "===== ") 3240 } 3241 }) 3242 } 3243 3244 // TestServerSideDiffErrorHandling tests error scenarios for server-side diff 3245 func TestServerSideDiffErrorHandling(t *testing.T) { 3246 Given(t). 3247 Path("two-nice-pods"). 3248 When(). 3249 CreateApp(). 3250 Then(). 3251 And(func(_ *Application) { 3252 // Test server-side diff with non-existent app should fail gracefully 3253 _, err := fixture.RunCli("app", "diff", "non-existent-app", "--server-side-diff") 3254 require.Error(t, err) 3255 // Error occurred as expected - this verifies the command fails gracefully 3256 }) 3257 } 3258 3259 // TestServerSideDiffWithLocal tests server-side diff with --local flag 3260 func TestServerSideDiffWithLocal(t *testing.T) { 3261 Given(t). 3262 Path("guestbook"). 3263 When(). 3264 CreateApp(). 3265 Sync(). 3266 Then(). 3267 Expect(OperationPhaseIs(OperationSucceeded)). 3268 Expect(SyncStatusIs(SyncStatusCodeSynced)). 3269 And(func(_ *Application) { 3270 // Modify the live deployment in the cluster to create differences 3271 // Apply patches to the deployment 3272 _, err := fixture.KubeClientset.AppsV1().Deployments(fixture.DeploymentNamespace()).Patch(t.Context(), 3273 "guestbook-ui", types.JSONPatchType, []byte(`[ 3274 {"op": "add", "path": "/spec/template/spec/containers/0/env", "value": [{"name": "LOCAL_CHANGE", "value": "true"}]}, 3275 {"op": "replace", "path": "/spec/replicas", "value": 2} 3276 ]`), metav1.PatchOptions{}) 3277 require.NoError(t, err) 3278 3279 // Verify the patch was applied by reading back the deployment 3280 modifiedDeployment, err := fixture.KubeClientset.AppsV1().Deployments(fixture.DeploymentNamespace()).Get(t.Context(), "guestbook-ui", metav1.GetOptions{}) 3281 require.NoError(t, err) 3282 assert.Equal(t, int32(2), *modifiedDeployment.Spec.Replicas, "Replica count should be updated to 2") 3283 assert.Len(t, modifiedDeployment.Spec.Template.Spec.Containers[0].Env, 1, "Should have one environment variable") 3284 assert.Equal(t, "LOCAL_CHANGE", modifiedDeployment.Spec.Template.Spec.Containers[0].Env[0].Name) 3285 }). 3286 When(). 3287 Refresh(RefreshTypeNormal). 3288 Then(). 3289 Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). 3290 And(func(app *Application) { 3291 // Test regular diff with --local (add --server-side-generate to avoid deprecation warning) 3292 regularOutput, err := fixture.RunCli("app", "diff", app.Name, "--local", "testdata", "--server-side-generate") 3293 require.Error(t, err) // diff command returns non-zero exit code when differences found 3294 assert.Contains(t, regularOutput, "===== apps/Deployment") 3295 assert.Contains(t, regularOutput, "guestbook-ui") 3296 assert.Contains(t, regularOutput, "replicas:") 3297 3298 // Test server-side diff with --local (add --server-side-generate for consistency) 3299 serverSideOutput, err := fixture.RunCli("app", "diff", app.Name, "--server-side-diff", "--local", "testdata", "--server-side-generate") 3300 require.Error(t, err) // diff command returns non-zero exit code when differences found 3301 assert.Contains(t, serverSideOutput, "===== apps/Deployment") 3302 assert.Contains(t, serverSideOutput, "guestbook-ui") 3303 assert.Contains(t, serverSideOutput, "replicas:") 3304 3305 // Both outputs should show similar differences 3306 assert.Contains(t, regularOutput, "replicas: 2") 3307 assert.Contains(t, serverSideOutput, "replicas: 2") 3308 }) 3309 } 3310 3311 func TestServerSideDiffWithLocalValidation(t *testing.T) { 3312 Given(t). 3313 Path("guestbook"). 3314 When(). 3315 CreateApp(). 3316 Sync(). 3317 Then(). 3318 Expect(OperationPhaseIs(OperationSucceeded)). 3319 Expect(SyncStatusIs(SyncStatusCodeSynced)). 3320 And(func(app *Application) { 3321 // Test that --server-side-diff with --local without --server-side-generate fails with proper error 3322 _, err := fixture.RunCli("app", "diff", app.Name, "--server-side-diff", "--local", "testdata") 3323 require.Error(t, err) 3324 assert.Contains(t, err.Error(), "--server-side-diff with --local requires --server-side-generate") 3325 }) 3326 }