From 0ede781d93d4e1f4de4beb0e39e8d20f663b2cfc Mon Sep 17 00:00:00 2001 From: Zadkiel AHARONIAN Date: Wed, 7 May 2025 12:55:00 +0200 Subject: [PATCH 01/22] feat: abort ongoing operation when a new revision is available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Zadkiel AHARONIAN Signed-off-by: Oliver Gondža --- controller/appcontroller.go | 16 +++++++++ pkg/apis/application/v1alpha1/types.go | 2 ++ test/e2e/app_autosync_test.go | 45 ++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/controller/appcontroller.go b/controller/appcontroller.go index 1395c540b30ac..7b8fcab8bd326 100644 --- a/controller/appcontroller.go +++ b/controller/appcontroller.go @@ -1414,6 +1414,22 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli logCtx.Infof("Skipping retrying in-progress operation. Attempting again at: %s", retryAt.Format(time.RFC3339)) ctrl.requestAppRefresh(app.QualifiedName(), CompareWithLatest.Pointer(), &retryAfter) return + } else { + // abort ongoing operation and retry if auto-refresh is enabled and a new revision is available + if state.Phase == synccommon.OperationRunning && state.Operation.Retry.Refresh && app.Status.Sync.Revision != state.Operation.Sync.Revision { + logCtx.Infof("A new revision is available, refreshing and terminating app, was phase: %s, message: %s", state.Phase, state.Message) + ctrl.requestAppRefresh(app.QualifiedName(), CompareWithLatest.Pointer(), nil) + state.Phase = synccommon.OperationTerminating + state.Message = "Operation forced to terminate (new revision available)" + ctrl.setOperationState(app, state) + return + } + // retrying operation. remove previous failure time in app since it is used as a trigger + // that previous failed and operation should be retried + state.FinishedAt = nil + ctrl.setOperationState(app, state) + // Get rid of sync results and null out previous operation completion time + state.SyncResult = nil } // Get rid of sync results and null out previous operation completion time // This will start the retry attempt diff --git a/pkg/apis/application/v1alpha1/types.go b/pkg/apis/application/v1alpha1/types.go index c9ed8e8f9eb7f..11ac8e55adbe8 100644 --- a/pkg/apis/application/v1alpha1/types.go +++ b/pkg/apis/application/v1alpha1/types.go @@ -1467,6 +1467,8 @@ type RetryStrategy struct { Limit int64 `json:"limit,omitempty" protobuf:"bytes,1,opt,name=limit"` // Backoff controls how to backoff on subsequent retries of failed syncs Backoff *Backoff `json:"backoff,omitempty" protobuf:"bytes,2,opt,name=backoff,casttype=Backoff"` + // Refresh indicates if a new revision should trigger a new sync (default: false) + Refresh bool `json:"refresh,omitempty" protobuf:"bytes,3,opt,name=refresh"` } func parseStringToDuration(durationString string) (time.Duration, error) { diff --git a/test/e2e/app_autosync_test.go b/test/e2e/app_autosync_test.go index ef805f6bad625..ad5ac38e26a6e 100644 --- a/test/e2e/app_autosync_test.go +++ b/test/e2e/app_autosync_test.go @@ -92,3 +92,48 @@ func TestAutoSyncSelfHealEnabled(t *testing.T) { assert.Empty(t, app.Status.Conditions) }) } + +func TestAutoSyncSelfHealRetryAndRefreshEnabled(t *testing.T) { + Given(t). + Path(guestbookPath). + When(). + // app should be auto-synced once created + CreateFromFile(func(app *Application) { + app.Spec.SyncPolicy = &SyncPolicy{ + Automated: &SyncPolicyAutomated{SelfHeal: true}, + Retry: &RetryStrategy{ + Limit: -1, + Refresh: true, + }, + } + }). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + When(). + // app should be attempted to auto-synced once and marked with error after failed attempt detected + PatchFile("guestbook-ui-deployment.yaml", `[{"op": "add", "path": "/spec/selector/matchLabels/foo", "value": "bar"}, {"op": "add", "path": "/spec/template/metadata/labels/foo", "value": "bar"}]`). + Refresh(RefreshTypeNormal). + Then(). + Expect(OperationPhaseIs(OperationFailed)). + When(). + // Trigger refresh again to make sure controller notices previously failed sync attempt before expectation timeout expires + Refresh(RefreshTypeNormal). + Then(). + Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). + Expect(Condition(ApplicationConditionSyncError, "Failed sync attempt")). + When(). + // SyncError condition should be removed after successful sync + PatchFile("guestbook-ui-deployment.yaml", `[{"op": "remove", "path": "/spec/selector/matchLabels/foo"}, {"op": "remove", "path": "/spec/template/metadata/labels/foo"}]`). + Refresh(RefreshTypeNormal). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + When(). + // Trigger refresh twice to make sure controller notices successful attempt and removes condition + Refresh(RefreshTypeNormal). + Then(). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + And(func(app *Application) { + assert.Len(t, app.Status.Conditions, 0) + }) +} From cda7c15147834781dd2364dd89489a626aa3a4ed Mon Sep 17 00:00:00 2001 From: Zadkiel AHARONIAN Date: Sat, 25 May 2024 20:18:38 +0200 Subject: [PATCH 02/22] feat(cli): add --sync-retry-refresh flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Zadkiel AHARONIAN Signed-off-by: Oliver Gondža --- cmd/util/app.go | 11 +++++++++++ cmd/util/app_test.go | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/cmd/util/app.go b/cmd/util/app.go index 4f858e3851054..4e0c37219769b 100644 --- a/cmd/util/app.go +++ b/cmd/util/app.go @@ -90,6 +90,7 @@ type AppOptions struct { retryBackoffDuration time.Duration retryBackoffMaxDuration time.Duration retryBackoffFactor int64 + retryRefresh bool ref string SourceName string drySourceRepo string @@ -168,6 +169,7 @@ func AddAppFlags(command *cobra.Command, opts *AppOptions) { command.Flags().DurationVar(&opts.retryBackoffDuration, "sync-retry-backoff-duration", argoappv1.DefaultSyncRetryDuration, "Sync retry backoff base duration. Input needs to be a duration (e.g. 2m, 1h)") command.Flags().DurationVar(&opts.retryBackoffMaxDuration, "sync-retry-backoff-max-duration", argoappv1.DefaultSyncRetryMaxDuration, "Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h)") command.Flags().Int64Var(&opts.retryBackoffFactor, "sync-retry-backoff-factor", argoappv1.DefaultSyncRetryFactor, "Factor multiplies the base duration after each failed sync retry") + command.Flags().BoolVar(&opts.retryRefresh, "sync-retry-refresh", false, "Set if a new revision should trigger a new sync") command.Flags().StringVar(&opts.ref, "ref", "", "Ref is reference to another source within sources field") command.Flags().StringVar(&opts.SourceName, "source-name", "", "Name of the source from the list of sources of the app.") } @@ -261,6 +263,7 @@ func SetAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap MaxDuration: appOpts.retryBackoffMaxDuration.String(), Factor: ptr.To(appOpts.retryBackoffFactor), }, + Refresh: appOpts.retryRefresh, } case appOpts.retryLimit == 0: if spec.SyncPolicy.IsZero() { @@ -271,6 +274,14 @@ func SetAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap default: log.Fatalf("Invalid sync-retry-limit [%d]", appOpts.retryLimit) } + case "sync-retry-refresh": + if spec.SyncPolicy == nil { + spec.SyncPolicy = &argoappv1.SyncPolicy{} + } + if spec.SyncPolicy.Retry == nil { + spec.SyncPolicy.Retry = &argoappv1.RetryStrategy{} + } + spec.SyncPolicy.Retry.Refresh = appOpts.retryRefresh } }) if flags.Changed("auto-prune") { diff --git a/cmd/util/app_test.go b/cmd/util/app_test.go index 72742049bc112..7a57f31debdf3 100644 --- a/cmd/util/app_test.go +++ b/cmd/util/app_test.go @@ -274,6 +274,13 @@ func Test_setAppSpecOptions(t *testing.T) { require.NoError(t, f.SetFlag("sync-retry-limit", "0")) assert.Nil(t, f.spec.SyncPolicy.Retry) }) + t.Run("RetryRefresh", func(t *testing.T) { + assert.NoError(t, f.SetFlag("sync-retry-refresh", "true")) + assert.True(t, f.spec.SyncPolicy.Retry.Refresh) + + assert.NoError(t, f.SetFlag("sync-retry-refresh", "false")) + assert.False(t, f.spec.SyncPolicy.Retry.Refresh) + }) t.Run("Kustomize", func(t *testing.T) { require.NoError(t, f.SetFlag("kustomize-replica", "my-deployment=2")) require.NoError(t, f.SetFlag("kustomize-replica", "my-statefulset=4")) From 1c2885868def0caa4f6c843ab26cf60c730991e8 Mon Sep 17 00:00:00 2001 From: Zadkiel AHARONIAN Date: Wed, 7 May 2025 12:57:04 +0200 Subject: [PATCH 03/22] docs: add sync retry refresh feature to auto-sync docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Zadkiel AHARONIAN Signed-off-by: Oliver Gondža --- docs/user-guide/auto_sync.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/user-guide/auto_sync.md b/docs/user-guide/auto_sync.md index 7ff526da6996e..278b360dacc9e 100644 --- a/docs/user-guide/auto_sync.md +++ b/docs/user-guide/auto_sync.md @@ -94,6 +94,23 @@ spec: !!!note Disabling self-heal does not guarantee that live cluster changes in multi-source applications will persist. Although one of the resource's sources remains unchanged, changes in another can trigger `autosync`. To handle such cases, consider disabling `autosync`. +## Automatic Retry Refresh on new revisions + +This feature allows users to configure their applications to refresh on new revisions when the current sync is retrying. To enable automatic refresh during sync retries, run: + +```bash +argocd app set --sync-retry-refresh +``` + +Or by setting the `retry.refresh` option to `true` in the sync policy: + +```yaml +spec: + syncPolicy: + retry: + refresh: true +``` + ## Automated Sync Semantics * An automated sync will only be performed if the application is OutOfSync. Applications in a From 9be2b9a5ddd580f49a55d8473169a3921214181b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gond=C5=BEa?= Date: Fri, 15 Aug 2025 12:00:38 +0200 Subject: [PATCH 04/22] chore: generate files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Zadkiel AHARONIAN Signed-off-by: Oliver Gondža --- assets/swagger.json | 4 ++ .../argocd_admin_app_generate-spec.md | 1 + .../commands/argocd_app_add-source.md | 1 + docs/user-guide/commands/argocd_app_create.md | 1 + docs/user-guide/commands/argocd_app_set.md | 1 + manifests/core-install-with-hydrator.yaml | 60 +++++++++++++++++++ manifests/core-install.yaml | 60 +++++++++++++++++++ manifests/crds/application-crd.yaml | 12 ++++ manifests/crds/applicationset-crd.yaml | 48 +++++++++++++++ manifests/ha/install-with-hydrator.yaml | 60 +++++++++++++++++++ manifests/ha/install.yaml | 60 +++++++++++++++++++ manifests/install-with-hydrator.yaml | 60 +++++++++++++++++++ manifests/install.yaml | 60 +++++++++++++++++++ pkg/apis/application/v1alpha1/generated.proto | 3 + .../application/v1alpha1/openapi_generated.go | 7 +++ 15 files changed, 438 insertions(+) diff --git a/assets/swagger.json b/assets/swagger.json index f71e81bda9266..8b774d54c814e 100644 --- a/assets/swagger.json +++ b/assets/swagger.json @@ -9960,6 +9960,10 @@ "description": "Limit is the maximum number of attempts for retrying a failed sync. If set to 0, no retries will be performed.", "type": "integer", "format": "int64" + }, + "refresh": { + "type": "boolean", + "title": "Refresh indicates if a new revision should trigger a new sync (default: false)" } } }, diff --git a/docs/user-guide/commands/argocd_admin_app_generate-spec.md b/docs/user-guide/commands/argocd_admin_app_generate-spec.md index 7d8cc7e32fac4..16783b9fdaa1c 100644 --- a/docs/user-guide/commands/argocd_admin_app_generate-spec.md +++ b/docs/user-guide/commands/argocd_admin_app_generate-spec.md @@ -107,6 +107,7 @@ argocd admin app generate-spec APPNAME [flags] --sync-retry-backoff-factor int Factor multiplies the base duration after each failed sync retry (default 2) --sync-retry-backoff-max-duration duration Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h) (default 3m0s) --sync-retry-limit int Max number of allowed sync retries + --sync-retry-refresh Set if a new revision should trigger a new sync --sync-source-branch string The branch from which the app will sync --sync-source-path string The path in the repository from which the app will sync --validate Validation of repo and cluster (default true) diff --git a/docs/user-guide/commands/argocd_app_add-source.md b/docs/user-guide/commands/argocd_app_add-source.md index fe869c1f621f3..09d1cec3c39ca 100644 --- a/docs/user-guide/commands/argocd_app_add-source.md +++ b/docs/user-guide/commands/argocd_app_add-source.md @@ -84,6 +84,7 @@ argocd app add-source APPNAME [flags] --sync-retry-backoff-factor int Factor multiplies the base duration after each failed sync retry (default 2) --sync-retry-backoff-max-duration duration Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h) (default 3m0s) --sync-retry-limit int Max number of allowed sync retries + --sync-retry-refresh Set if a new revision should trigger a new sync --sync-source-branch string The branch from which the app will sync --sync-source-path string The path in the repository from which the app will sync --validate Validation of repo and cluster (default true) diff --git a/docs/user-guide/commands/argocd_app_create.md b/docs/user-guide/commands/argocd_app_create.md index 5529650f86455..73de6a512d9de 100644 --- a/docs/user-guide/commands/argocd_app_create.md +++ b/docs/user-guide/commands/argocd_app_create.md @@ -107,6 +107,7 @@ argocd app create APPNAME [flags] --sync-retry-backoff-factor int Factor multiplies the base duration after each failed sync retry (default 2) --sync-retry-backoff-max-duration duration Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h) (default 3m0s) --sync-retry-limit int Max number of allowed sync retries + --sync-retry-refresh Set if a new revision should trigger a new sync --sync-source-branch string The branch from which the app will sync --sync-source-path string The path in the repository from which the app will sync --upsert Allows to override application with the same name even if supplied application spec is different from existing spec diff --git a/docs/user-guide/commands/argocd_app_set.md b/docs/user-guide/commands/argocd_app_set.md index 9a38336762705..a49cad82369d4 100644 --- a/docs/user-guide/commands/argocd_app_set.md +++ b/docs/user-guide/commands/argocd_app_set.md @@ -97,6 +97,7 @@ argocd app set APPNAME [flags] --sync-retry-backoff-factor int Factor multiplies the base duration after each failed sync retry (default 2) --sync-retry-backoff-max-duration duration Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h) (default 3m0s) --sync-retry-limit int Max number of allowed sync retries + --sync-retry-refresh Set if a new revision should trigger a new sync --sync-source-branch string The branch from which the app will sync --sync-source-path string The path in the repository from which the app will sync --validate Validation of repo and cluster (default true) diff --git a/manifests/core-install-with-hydrator.yaml b/manifests/core-install-with-hydrator.yaml index 41aad58361bd5..179f99347bb94 100644 --- a/manifests/core-install-with-hydrator.yaml +++ b/manifests/core-install-with-hydrator.yaml @@ -111,6 +111,10 @@ spec: a failed sync. If set to 0, no retries will be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should trigger + a new sync (default: false)' + type: boolean type: object sync: description: Sync contains parameters for the operation @@ -1938,6 +1942,10 @@ spec: a failed sync. If set to 0, no retries will be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should trigger + a new sync (default: false)' + type: boolean type: object syncOptions: description: Options allow you to specify whole app sync-options @@ -2906,6 +2914,10 @@ spec: be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should + trigger a new sync (default: false)' + type: boolean type: object sync: description: Sync contains parameters for the operation @@ -6592,6 +6604,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -7274,6 +7288,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -7957,6 +7973,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -8618,6 +8636,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -9304,6 +9324,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -9986,6 +10008,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -10669,6 +10693,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -11330,6 +11356,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -11999,6 +12027,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -12895,6 +12925,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -13782,6 +13814,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -14460,6 +14494,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -15148,6 +15184,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -15830,6 +15868,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -16513,6 +16553,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -17174,6 +17216,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -17843,6 +17887,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -18739,6 +18785,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -19626,6 +19674,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -20308,6 +20358,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -20976,6 +21028,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -21872,6 +21926,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -22759,6 +22815,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -23514,6 +23572,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: diff --git a/manifests/core-install.yaml b/manifests/core-install.yaml index 78f2c1a0e07c8..703b7de55773c 100644 --- a/manifests/core-install.yaml +++ b/manifests/core-install.yaml @@ -111,6 +111,10 @@ spec: a failed sync. If set to 0, no retries will be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should trigger + a new sync (default: false)' + type: boolean type: object sync: description: Sync contains parameters for the operation @@ -1938,6 +1942,10 @@ spec: a failed sync. If set to 0, no retries will be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should trigger + a new sync (default: false)' + type: boolean type: object syncOptions: description: Options allow you to specify whole app sync-options @@ -2906,6 +2914,10 @@ spec: be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should + trigger a new sync (default: false)' + type: boolean type: object sync: description: Sync contains parameters for the operation @@ -6592,6 +6604,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -7274,6 +7288,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -7957,6 +7973,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -8618,6 +8636,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -9304,6 +9324,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -9986,6 +10008,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -10669,6 +10693,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -11330,6 +11356,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -11999,6 +12027,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -12895,6 +12925,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -13782,6 +13814,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -14460,6 +14494,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -15148,6 +15184,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -15830,6 +15868,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -16513,6 +16553,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -17174,6 +17216,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -17843,6 +17887,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -18739,6 +18785,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -19626,6 +19674,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -20308,6 +20358,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -20976,6 +21028,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -21872,6 +21926,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -22759,6 +22815,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -23514,6 +23572,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: diff --git a/manifests/crds/application-crd.yaml b/manifests/crds/application-crd.yaml index 3d644d0e5e9a4..d37c09add845e 100644 --- a/manifests/crds/application-crd.yaml +++ b/manifests/crds/application-crd.yaml @@ -110,6 +110,10 @@ spec: a failed sync. If set to 0, no retries will be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should trigger + a new sync (default: false)' + type: boolean type: object sync: description: Sync contains parameters for the operation @@ -1937,6 +1941,10 @@ spec: a failed sync. If set to 0, no retries will be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should trigger + a new sync (default: false)' + type: boolean type: object syncOptions: description: Options allow you to specify whole app sync-options @@ -2905,6 +2913,10 @@ spec: be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should + trigger a new sync (default: false)' + type: boolean type: object sync: description: Sync contains parameters for the operation diff --git a/manifests/crds/applicationset-crd.yaml b/manifests/crds/applicationset-crd.yaml index b7aeed2481837..679f64a9d8457 100644 --- a/manifests/crds/applicationset-crd.yaml +++ b/manifests/crds/applicationset-crd.yaml @@ -700,6 +700,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -1382,6 +1384,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -2065,6 +2069,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -2726,6 +2732,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -3412,6 +3420,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -4094,6 +4104,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -4777,6 +4789,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -5438,6 +5452,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -6107,6 +6123,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -7003,6 +7021,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -7890,6 +7910,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -8568,6 +8590,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -9256,6 +9280,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -9938,6 +9964,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -10621,6 +10649,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -11282,6 +11312,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -11951,6 +11983,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -12847,6 +12881,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -13734,6 +13770,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -14416,6 +14454,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -15084,6 +15124,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -15980,6 +16022,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -16867,6 +16911,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -17622,6 +17668,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: diff --git a/manifests/ha/install-with-hydrator.yaml b/manifests/ha/install-with-hydrator.yaml index a94fb3c9b68f8..eb20da1d1f434 100644 --- a/manifests/ha/install-with-hydrator.yaml +++ b/manifests/ha/install-with-hydrator.yaml @@ -111,6 +111,10 @@ spec: a failed sync. If set to 0, no retries will be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should trigger + a new sync (default: false)' + type: boolean type: object sync: description: Sync contains parameters for the operation @@ -1938,6 +1942,10 @@ spec: a failed sync. If set to 0, no retries will be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should trigger + a new sync (default: false)' + type: boolean type: object syncOptions: description: Options allow you to specify whole app sync-options @@ -2906,6 +2914,10 @@ spec: be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should + trigger a new sync (default: false)' + type: boolean type: object sync: description: Sync contains parameters for the operation @@ -6592,6 +6604,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -7274,6 +7288,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -7957,6 +7973,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -8618,6 +8636,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -9304,6 +9324,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -9986,6 +10008,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -10669,6 +10693,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -11330,6 +11356,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -11999,6 +12027,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -12895,6 +12925,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -13782,6 +13814,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -14460,6 +14494,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -15148,6 +15184,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -15830,6 +15868,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -16513,6 +16553,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -17174,6 +17216,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -17843,6 +17887,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -18739,6 +18785,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -19626,6 +19674,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -20308,6 +20358,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -20976,6 +21028,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -21872,6 +21926,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -22759,6 +22815,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -23514,6 +23572,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: diff --git a/manifests/ha/install.yaml b/manifests/ha/install.yaml index 25638fc93f22c..5e603476e7bd2 100644 --- a/manifests/ha/install.yaml +++ b/manifests/ha/install.yaml @@ -111,6 +111,10 @@ spec: a failed sync. If set to 0, no retries will be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should trigger + a new sync (default: false)' + type: boolean type: object sync: description: Sync contains parameters for the operation @@ -1938,6 +1942,10 @@ spec: a failed sync. If set to 0, no retries will be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should trigger + a new sync (default: false)' + type: boolean type: object syncOptions: description: Options allow you to specify whole app sync-options @@ -2906,6 +2914,10 @@ spec: be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should + trigger a new sync (default: false)' + type: boolean type: object sync: description: Sync contains parameters for the operation @@ -6592,6 +6604,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -7274,6 +7288,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -7957,6 +7973,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -8618,6 +8636,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -9304,6 +9324,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -9986,6 +10008,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -10669,6 +10693,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -11330,6 +11356,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -11999,6 +12027,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -12895,6 +12925,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -13782,6 +13814,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -14460,6 +14494,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -15148,6 +15184,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -15830,6 +15868,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -16513,6 +16553,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -17174,6 +17216,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -17843,6 +17887,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -18739,6 +18785,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -19626,6 +19674,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -20308,6 +20358,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -20976,6 +21028,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -21872,6 +21926,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -22759,6 +22815,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -23514,6 +23572,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: diff --git a/manifests/install-with-hydrator.yaml b/manifests/install-with-hydrator.yaml index 79dfe6af65a5e..fb60fa977bb98 100644 --- a/manifests/install-with-hydrator.yaml +++ b/manifests/install-with-hydrator.yaml @@ -111,6 +111,10 @@ spec: a failed sync. If set to 0, no retries will be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should trigger + a new sync (default: false)' + type: boolean type: object sync: description: Sync contains parameters for the operation @@ -1938,6 +1942,10 @@ spec: a failed sync. If set to 0, no retries will be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should trigger + a new sync (default: false)' + type: boolean type: object syncOptions: description: Options allow you to specify whole app sync-options @@ -2906,6 +2914,10 @@ spec: be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should + trigger a new sync (default: false)' + type: boolean type: object sync: description: Sync contains parameters for the operation @@ -6592,6 +6604,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -7274,6 +7288,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -7957,6 +7973,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -8618,6 +8636,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -9304,6 +9324,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -9986,6 +10008,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -10669,6 +10693,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -11330,6 +11356,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -11999,6 +12027,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -12895,6 +12925,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -13782,6 +13814,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -14460,6 +14494,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -15148,6 +15184,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -15830,6 +15868,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -16513,6 +16553,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -17174,6 +17216,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -17843,6 +17887,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -18739,6 +18785,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -19626,6 +19674,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -20308,6 +20358,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -20976,6 +21028,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -21872,6 +21926,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -22759,6 +22815,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -23514,6 +23572,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: diff --git a/manifests/install.yaml b/manifests/install.yaml index 9ee03bc1a9ebe..61d5802f41da4 100644 --- a/manifests/install.yaml +++ b/manifests/install.yaml @@ -111,6 +111,10 @@ spec: a failed sync. If set to 0, no retries will be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should trigger + a new sync (default: false)' + type: boolean type: object sync: description: Sync contains parameters for the operation @@ -1938,6 +1942,10 @@ spec: a failed sync. If set to 0, no retries will be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should trigger + a new sync (default: false)' + type: boolean type: object syncOptions: description: Options allow you to specify whole app sync-options @@ -2906,6 +2914,10 @@ spec: be performed. format: int64 type: integer + refresh: + description: 'Refresh indicates if a new revision should + trigger a new sync (default: false)' + type: boolean type: object sync: description: Sync contains parameters for the operation @@ -6592,6 +6604,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -7274,6 +7288,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -7957,6 +7973,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -8618,6 +8636,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -9304,6 +9324,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -9986,6 +10008,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -10669,6 +10693,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -11330,6 +11356,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -11999,6 +12027,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -12895,6 +12925,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -13782,6 +13814,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -14460,6 +14494,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -15148,6 +15184,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -15830,6 +15868,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -16513,6 +16553,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -17174,6 +17216,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -17843,6 +17887,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -18739,6 +18785,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -19626,6 +19674,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -20308,6 +20358,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -20976,6 +21028,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -21872,6 +21926,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -22759,6 +22815,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: @@ -23514,6 +23572,8 @@ spec: limit: format: int64 type: integer + refresh: + type: boolean type: object syncOptions: items: diff --git a/pkg/apis/application/v1alpha1/generated.proto b/pkg/apis/application/v1alpha1/generated.proto index be11b3ac327b5..7ca4c57d48c33 100644 --- a/pkg/apis/application/v1alpha1/generated.proto +++ b/pkg/apis/application/v1alpha1/generated.proto @@ -2225,6 +2225,9 @@ message RetryStrategy { // Backoff controls how to backoff on subsequent retries of failed syncs optional Backoff backoff = 2; + + // Refresh indicates if a new revision should trigger a new sync (default: false) + optional bool refresh = 3; } // RevisionHistory contains history information about a previous sync diff --git a/pkg/apis/application/v1alpha1/openapi_generated.go b/pkg/apis/application/v1alpha1/openapi_generated.go index 4520013d518d2..ecd4773081521 100644 --- a/pkg/apis/application/v1alpha1/openapi_generated.go +++ b/pkg/apis/application/v1alpha1/openapi_generated.go @@ -7027,6 +7027,13 @@ func schema_pkg_apis_application_v1alpha1_RetryStrategy(ref common.ReferenceCall Ref: ref("github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1.Backoff"), }, }, + "refresh": { + SchemaProps: spec.SchemaProps{ + Description: "Refresh indicates if a new revision should trigger a new sync (default: false)", + Type: []string{"boolean"}, + Format: "", + }, + }, }, }, }, From b744b3601d93f2ec6b017333e647b189c42a9043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gond=C5=BEa?= Date: Mon, 5 May 2025 13:52:24 +0200 Subject: [PATCH 05/22] feat(ui): Show retry.refresh in application details page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Oliver Gondža --- .../application-retry-view/application-retry-view.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/app/applications/components/application-retry-view/application-retry-view.tsx b/ui/src/app/applications/components/application-retry-view/application-retry-view.tsx index 0baeca32ce6ee..e3bd3e350f91b 100644 --- a/ui/src/app/applications/components/application-retry-view/application-retry-view.tsx +++ b/ui/src/app/applications/components/application-retry-view/application-retry-view.tsx @@ -17,7 +17,8 @@ const retryOptionsView: Array<(initData: models.RetryStrategy) => React.ReactNod initData => buildRetryOptionView('Limit', initData?.limit), initData => buildRetryOptionView('Duration', initData?.backoff?.duration), initData => buildRetryOptionView('Max Duration', initData?.backoff?.maxDuration), - initData => buildRetryOptionView('Factor', initData?.backoff?.factor) + initData => buildRetryOptionView('Factor', initData?.backoff?.factor), + initData => buildRetryOptionView('Refresh', initData?.refresh == true ? "True" : "False") ]; export const ApplicationRetryView = ({initValues}: {initValues?: models.RetryStrategy}) => { From 2fc1fc498c8c736ce6f1819bef4619485392cf9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gond=C5=BEa?= Date: Thu, 17 Jul 2025 08:23:28 +0200 Subject: [PATCH 06/22] fix(sync): Fix Sync Refresh handling and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Oliver Gondža --- controller/appcontroller.go | 28 ++++++++------------ controller/state.go | 2 +- test/e2e/app_autosync_test.go | 41 ++++++++++++++++------------- test/e2e/fixture/app/actions.go | 10 +++++-- test/e2e/fixture/app/expectation.go | 5 +++- test/e2e/fixture/fixture.go | 10 +++++++ 6 files changed, 56 insertions(+), 40 deletions(-) diff --git a/controller/appcontroller.go b/controller/appcontroller.go index 7b8fcab8bd326..10efa35187818 100644 --- a/controller/appcontroller.go +++ b/controller/appcontroller.go @@ -1410,27 +1410,21 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli return } retryAfter := time.Until(retryAt) - if retryAfter > 0 { + + // abort ongoing operation and retry if auto-refresh is enabled and a new revision is available + hasRevisionToRefresh := state.Operation.Retry.Refresh && state.Phase == synccommon.OperationRunning && app.Status.Sync.Revision != state.Operation.Sync.Revision + if hasRevisionToRefresh { + logCtx.Infof("A new revision is available, refreshing and terminating app, was phase: %s, message: %s", state.Phase, state.Message) + + state.Message = fmt.Sprintf("Sync revision of %s refreshed with %s", state.Operation.Sync.Revision, app.Status.Sync.Revision) + state.Operation.Sync.Revision = app.Status.Sync.Revision + } else if retryAfter > 0 { + // Do not delay retry if there are revisions to refresh logCtx.Infof("Skipping retrying in-progress operation. Attempting again at: %s", retryAt.Format(time.RFC3339)) ctrl.requestAppRefresh(app.QualifiedName(), CompareWithLatest.Pointer(), &retryAfter) return - } else { - // abort ongoing operation and retry if auto-refresh is enabled and a new revision is available - if state.Phase == synccommon.OperationRunning && state.Operation.Retry.Refresh && app.Status.Sync.Revision != state.Operation.Sync.Revision { - logCtx.Infof("A new revision is available, refreshing and terminating app, was phase: %s, message: %s", state.Phase, state.Message) - ctrl.requestAppRefresh(app.QualifiedName(), CompareWithLatest.Pointer(), nil) - state.Phase = synccommon.OperationTerminating - state.Message = "Operation forced to terminate (new revision available)" - ctrl.setOperationState(app, state) - return - } - // retrying operation. remove previous failure time in app since it is used as a trigger - // that previous failed and operation should be retried - state.FinishedAt = nil - ctrl.setOperationState(app, state) - // Get rid of sync results and null out previous operation completion time - state.SyncResult = nil } + // Get rid of sync results and null out previous operation completion time // This will start the retry attempt state.FinishedAt = nil diff --git a/controller/state.go b/controller/state.go index e0d09f1bf4504..f191f7b8be1f7 100644 --- a/controller/state.go +++ b/controller/state.go @@ -558,7 +558,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1 appLabelKey, resourceOverrides, resFilter, installationID, trackingMethod, err := m.getComparisonSettings() ts.AddCheckpoint("settings_ms") if err != nil { - // return unknown comparison result if basic comparison settings cannot be loaded + log.Infof("Basic comparison settings cannot be loaded, using unknown comparison: %s", err.Error()) return &comparisonResult{syncStatus: syncStatus, healthStatus: health.HealthStatusUnknown}, nil } diff --git a/test/e2e/app_autosync_test.go b/test/e2e/app_autosync_test.go index ad5ac38e26a6e..1902ab46b4887 100644 --- a/test/e2e/app_autosync_test.go +++ b/test/e2e/app_autosync_test.go @@ -2,6 +2,7 @@ package e2e import ( "testing" + "time" . "github.com/argoproj/gitops-engine/pkg/sync/common" "github.com/stretchr/testify/assert" @@ -97,7 +98,7 @@ func TestAutoSyncSelfHealRetryAndRefreshEnabled(t *testing.T) { Given(t). Path(guestbookPath). When(). - // app should be auto-synced once created + // Correctly configured app should be auto-synced once created CreateFromFile(func(app *Application) { app.Spec.SyncPolicy = &SyncPolicy{ Automated: &SyncPolicyAutomated{SelfHeal: true}, @@ -110,30 +111,32 @@ func TestAutoSyncSelfHealRetryAndRefreshEnabled(t *testing.T) { Then(). Expect(OperationPhaseIs(OperationSucceeded)). Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(NoConditions()). + // Broken commit should make the app stuck retrying indefinitely When(). - // app should be attempted to auto-synced once and marked with error after failed attempt detected - PatchFile("guestbook-ui-deployment.yaml", `[{"op": "add", "path": "/spec/selector/matchLabels/foo", "value": "bar"}, {"op": "add", "path": "/spec/template/metadata/labels/foo", "value": "bar"}]`). - Refresh(RefreshTypeNormal). - Then(). - Expect(OperationPhaseIs(OperationFailed)). - When(). - // Trigger refresh again to make sure controller notices previously failed sync attempt before expectation timeout expires + PatchFile("guestbook-ui-deployment.yaml", `[{"op": "replace", "path": "/spec/revisionHistoryLimit", "value": "badValue"}]`). Refresh(RefreshTypeNormal). Then(). + Expect(OperationPhaseIs(OperationRunning)). Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). - Expect(Condition(ApplicationConditionSyncError, "Failed sync attempt")). - When(). - // SyncError condition should be removed after successful sync - PatchFile("guestbook-ui-deployment.yaml", `[{"op": "remove", "path": "/spec/selector/matchLabels/foo"}, {"op": "remove", "path": "/spec/template/metadata/labels/foo"}]`). - Refresh(RefreshTypeNormal). - Then(). - Expect(OperationPhaseIs(OperationSucceeded)). + Expect(OperationMessageContains("one or more objects failed to apply")). + Expect(OperationMessageContains("Retrying attempt #1")). + // Wait to make sure the condition is consistent + And(func(_ *Application) { + time.Sleep(10 * time.Second) + }). + Expect(OperationPhaseIs(OperationRunning)). + Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). + Expect(OperationMessageContains("one or more objects failed to apply")). + Expect(OperationMessageContains("Retrying attempt #2")). + + // Push fix commit and see the app pick it up When(). - // Trigger refresh twice to make sure controller notices successful attempt and removes condition + RevertCommit(). // Fix declaration Refresh(RefreshTypeNormal). + // Wait for the sync retry to pick up new commit Then(). + Expect(NoConditions()). Expect(SyncStatusIs(SyncStatusCodeSynced)). - And(func(app *Application) { - assert.Len(t, app.Status.Conditions, 0) - }) + Expect(OperationPhaseIs(OperationSucceeded)) } diff --git a/test/e2e/fixture/app/actions.go b/test/e2e/fixture/app/actions.go index 28ff2420c4097..4212432dbc88b 100644 --- a/test/e2e/fixture/app/actions.go +++ b/test/e2e/fixture/app/actions.go @@ -40,9 +40,9 @@ func (a *Actions) DoNotIgnoreErrors() *Actions { return a } -func (a *Actions) PatchFile(file string, jsonPath string) *Actions { +func (a *Actions) PatchFile(file string, jsonPatch string) *Actions { a.context.t.Helper() - fixture.Patch(a.context.t, a.context.path+"/"+file, jsonPath) + fixture.Patch(a.context.t, a.context.path+"/"+file, jsonPatch) return a } @@ -58,6 +58,12 @@ func (a *Actions) WriteFile(fileName, fileContents string) *Actions { return a } +func (a *Actions) RevertCommit() *Actions { + a.context.t.Helper() + fixture.RevertCommit(a.context.t) + return a +} + func (a *Actions) AddFile(fileName, fileContents string) *Actions { a.context.t.Helper() fixture.AddFile(a.context.t, a.context.path+"/"+fileName, fileContents) diff --git a/test/e2e/fixture/app/expectation.go b/test/e2e/fixture/app/expectation.go index 0ca0b67b4de1c..8c47ad0c8a4f3 100644 --- a/test/e2e/fixture/app/expectation.go +++ b/test/e2e/fixture/app/expectation.go @@ -32,10 +32,13 @@ func OperationPhaseIs(expected common.OperationPhase) Expectation { return func(c *Consequences) (state, string) { operationState := c.app().Status.OperationState actual := common.OperationRunning + msg := "" if operationState != nil { actual = operationState.Phase + msg = operationState.Message } - return simple(actual == expected, fmt.Sprintf("operation phase should be %s, is %s", expected, actual)) + message := fmt.Sprintf("operation phase should be %s, is %s, message: '%s'", expected, actual, msg) + return simple(actual == expected, message) } } diff --git a/test/e2e/fixture/fixture.go b/test/e2e/fixture/fixture.go index a75b5e418ab54..b6cdf3f5d50b0 100644 --- a/test/e2e/fixture/fixture.go +++ b/test/e2e/fixture/fixture.go @@ -1128,6 +1128,16 @@ func WriteFile(t *testing.T, path, contents string) { require.NoError(t, os.WriteFile(filepath.Join(repoDirectory(), path), []byte(contents), 0o644)) } +func RevertCommit(t *testing.T) { + t.Helper() + + errors.NewHandler(t).FailOnErr(Run(repoDirectory(), "git", "revert", "HEAD")) + + if IsRemote() { + errors.NewHandler(t).FailOnErr(Run(repoDirectory(), "git", "push", "-f", "origin", "master")) + } +} + func AddFile(t *testing.T, path, contents string) { t.Helper() WriteFile(t, path, contents) From 0f3880950788b2fcee0c4218593f941f174ee3de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gond=C5=BEa?= Date: Mon, 19 May 2025 13:49:14 +0200 Subject: [PATCH 07/22] chore: Update lint warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Oliver Gondža --- cmd/util/app_test.go | 4 ++-- .../application-retry-view/application-retry-view.tsx | 2 +- ui/src/app/shared/models.ts | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd/util/app_test.go b/cmd/util/app_test.go index 7a57f31debdf3..b54c6d1cf7608 100644 --- a/cmd/util/app_test.go +++ b/cmd/util/app_test.go @@ -275,10 +275,10 @@ func Test_setAppSpecOptions(t *testing.T) { assert.Nil(t, f.spec.SyncPolicy.Retry) }) t.Run("RetryRefresh", func(t *testing.T) { - assert.NoError(t, f.SetFlag("sync-retry-refresh", "true")) + require.NoError(t, f.SetFlag("sync-retry-refresh", "true")) assert.True(t, f.spec.SyncPolicy.Retry.Refresh) - assert.NoError(t, f.SetFlag("sync-retry-refresh", "false")) + require.NoError(t, f.SetFlag("sync-retry-refresh", "false")) assert.False(t, f.spec.SyncPolicy.Retry.Refresh) }) t.Run("Kustomize", func(t *testing.T) { diff --git a/ui/src/app/applications/components/application-retry-view/application-retry-view.tsx b/ui/src/app/applications/components/application-retry-view/application-retry-view.tsx index e3bd3e350f91b..b0d622f2fcf82 100644 --- a/ui/src/app/applications/components/application-retry-view/application-retry-view.tsx +++ b/ui/src/app/applications/components/application-retry-view/application-retry-view.tsx @@ -18,7 +18,7 @@ const retryOptionsView: Array<(initData: models.RetryStrategy) => React.ReactNod initData => buildRetryOptionView('Duration', initData?.backoff?.duration), initData => buildRetryOptionView('Max Duration', initData?.backoff?.maxDuration), initData => buildRetryOptionView('Factor', initData?.backoff?.factor), - initData => buildRetryOptionView('Refresh', initData?.refresh == true ? "True" : "False") + initData => buildRetryOptionView('Refresh', initData?.refresh == true ? 'True' : 'False') ]; export const ApplicationRetryView = ({initValues}: {initValues?: models.RetryStrategy}) => { diff --git a/ui/src/app/shared/models.ts b/ui/src/app/shared/models.ts index e028509b3e64b..67caffc18c429 100644 --- a/ui/src/app/shared/models.ts +++ b/ui/src/app/shared/models.ts @@ -44,6 +44,7 @@ export interface RetryBackoff { export interface RetryStrategy { limit: number; backoff: RetryBackoff; + refresh: boolean; } export interface RollbackOperation { From 1f99b000e80f51a0c129a563fe2195d2a5bd68fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gond=C5=BEa?= Date: Wed, 16 Jul 2025 16:51:11 +0200 Subject: [PATCH 08/22] feat(auto-sync): Abort current sync and start a new one after failure with new commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Oliver Gondža --- controller/appcontroller.go | 39 ++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/controller/appcontroller.go b/controller/appcontroller.go index 10efa35187818..8fe2ffd2c06e4 100644 --- a/controller/appcontroller.go +++ b/controller/appcontroller.go @@ -1411,15 +1411,7 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli } retryAfter := time.Until(retryAt) - // abort ongoing operation and retry if auto-refresh is enabled and a new revision is available - hasRevisionToRefresh := state.Operation.Retry.Refresh && state.Phase == synccommon.OperationRunning && app.Status.Sync.Revision != state.Operation.Sync.Revision - if hasRevisionToRefresh { - logCtx.Infof("A new revision is available, refreshing and terminating app, was phase: %s, message: %s", state.Phase, state.Message) - - state.Message = fmt.Sprintf("Sync revision of %s refreshed with %s", state.Operation.Sync.Revision, app.Status.Sync.Revision) - state.Operation.Sync.Revision = app.Status.Sync.Revision - } else if retryAfter > 0 { - // Do not delay retry if there are revisions to refresh + if retryAfter > 0 { logCtx.Infof("Skipping retrying in-progress operation. Attempting again at: %s", retryAt.Format(time.RFC3339)) ctrl.requestAppRefresh(app.QualifiedName(), CompareWithLatest.Pointer(), &retryAfter) return @@ -1474,8 +1466,14 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli } } case synccommon.OperationFailed, synccommon.OperationError: - if !terminating && (state.RetryCount < state.Operation.Retry.Limit || state.Operation.Retry.Limit < 0) { - now := metav1.Now() + now := metav1.Now() + if !terminating && hasNewCommitToResync(app, state, logCtx) { + // Do not retry with old commit if there is a new one. Keep the "error" Phase. + state.FinishedAt = &now + state.Message = fmt.Sprintf("%s (retry terminated - new changes)", state.Message) + // Start from 0 with new change + state.RetryCount = 0 + } else if !terminating && (state.RetryCount < state.Operation.Retry.Limit || state.Operation.Retry.Limit < 0) { state.FinishedAt = &now if retryAt, err := state.Operation.Retry.NextRetryAt(now.Time, state.RetryCount); err != nil { state.Phase = synccommon.OperationError @@ -2080,6 +2078,24 @@ func (ctrl *ApplicationController) persistAppStatus(orig *appv1.Application, new return patchDuration } +func hasNewCommitToResync(app *appv1.Application, state *appv1.OperationState, logCtx *log.Entry) bool { + if app.Spec.SyncPolicy == nil || !app.Spec.SyncPolicy.IsAutomatedSyncEnabled() { + return false + } + + desiredRevisions := []string{app.Status.Sync.Revision} + if app.Spec.HasMultipleSources() { + desiredRevisions = app.Status.Sync.Revisions + } + + alreadyAttempted, lastRevs, lastPhase := alreadyAttemptedSync(app, desiredRevisions, true) + if !alreadyAttempted && len(lastRevs) != 0 { + logCtx.Infof("A new revision is available after %s with %s: %s", lastPhase, lastRevs, state.Message) + return true + } + return false +} + // autoSync will initiate a sync operation for an application configured with automated sync func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *appv1.SyncStatus, resources []appv1.ResourceStatus, shouldCompareRevisions bool) (*appv1.ApplicationCondition, time.Duration) { logCtx := log.WithFields(applog.GetAppLogFields(app)) @@ -2189,6 +2205,7 @@ func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus * } } ts.AddCheckpoint("already_attempted_check_ms") + logCtx.Infof("FLARE: Detected change after %s/%s, desired %s", lastAttemptedPhase, lastAttemptedRevisions, desiredRevisions) if app.Spec.SyncPolicy.Automated.Prune && !app.Spec.SyncPolicy.Automated.AllowEmpty { bAllNeedPrune := true From cd3879922f142419c79354426fdd534a46e7c551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gond=C5=BEa?= Date: Thu, 17 Jul 2025 08:04:21 +0200 Subject: [PATCH 09/22] feat(auto-sync): Pick new commits on opt-in bases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Oliver Gondža --- controller/appcontroller.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/controller/appcontroller.go b/controller/appcontroller.go index 8fe2ffd2c06e4..82b7b1c311372 100644 --- a/controller/appcontroller.go +++ b/controller/appcontroller.go @@ -1467,13 +1467,12 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli } case synccommon.OperationFailed, synccommon.OperationError: now := metav1.Now() - if !terminating && hasNewCommitToResync(app, state, logCtx) { - // Do not retry with old commit if there is a new one. Keep the "error" Phase. + switch { + case !terminating && state.Operation.Retry.Refresh && hasNewCommitToResync(app, state, logCtx): state.FinishedAt = &now - state.Message = fmt.Sprintf("%s (retry terminated - new changes)", state.Message) - // Start from 0 with new change + state.Message = state.Message + " (retry terminated - new changes)" state.RetryCount = 0 - } else if !terminating && (state.RetryCount < state.Operation.Retry.Limit || state.Operation.Retry.Limit < 0) { + case !terminating && (state.RetryCount < state.Operation.Retry.Limit || state.Operation.Retry.Limit < 0): state.FinishedAt = &now if retryAt, err := state.Operation.Retry.NextRetryAt(now.Time, state.RetryCount); err != nil { state.Phase = synccommon.OperationError @@ -1483,7 +1482,7 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli state.RetryCount++ state.Message = fmt.Sprintf("%s due to application controller sync timeout. Retrying attempt #%d at %s.", state.Message, state.RetryCount, retryAt.Format(time.Kitchen)) } - } else if state.RetryCount > 0 { + case state.RetryCount > 0: state.Message = fmt.Sprintf("%s (retried %d times).", state.Message, state.RetryCount) } } From 23d3a2546ff2ba67a760c264ce0d00c49a24805e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gond=C5=BEa?= Date: Wed, 23 Jul 2025 12:20:01 +0200 Subject: [PATCH 10/22] feat(auto-sync): Abort current sync and start a new one after failure with new commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Oliver Gondža --- controller/appcontroller.go | 14 ++++++-------- test/e2e/app_autosync_test.go | 24 +++++++++++++++++------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/controller/appcontroller.go b/controller/appcontroller.go index 82b7b1c311372..92c3b2863a413 100644 --- a/controller/appcontroller.go +++ b/controller/appcontroller.go @@ -1468,10 +1468,10 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli case synccommon.OperationFailed, synccommon.OperationError: now := metav1.Now() switch { - case !terminating && state.Operation.Retry.Refresh && hasNewCommitToResync(app, state, logCtx): - state.FinishedAt = &now - state.Message = state.Message + " (retry terminated - new changes)" - state.RetryCount = 0 + case !terminating && state.Operation.Retry.Refresh && hasNewRevisionsToResync(app): + // Stop retrying in case of failure and new revisions - will be tried in new sync attempt + state.Message = state.Message + " (retry terminated - new revisions)" + logCtx.Debug("Terminating sync retry, there are new revisions") case !terminating && (state.RetryCount < state.Operation.Retry.Limit || state.Operation.Retry.Limit < 0): state.FinishedAt = &now if retryAt, err := state.Operation.Retry.NextRetryAt(now.Time, state.RetryCount); err != nil { @@ -2077,7 +2077,7 @@ func (ctrl *ApplicationController) persistAppStatus(orig *appv1.Application, new return patchDuration } -func hasNewCommitToResync(app *appv1.Application, state *appv1.OperationState, logCtx *log.Entry) bool { +func hasNewRevisionsToResync(app *appv1.Application) bool { if app.Spec.SyncPolicy == nil || !app.Spec.SyncPolicy.IsAutomatedSyncEnabled() { return false } @@ -2087,9 +2087,8 @@ func hasNewCommitToResync(app *appv1.Application, state *appv1.OperationState, l desiredRevisions = app.Status.Sync.Revisions } - alreadyAttempted, lastRevs, lastPhase := alreadyAttemptedSync(app, desiredRevisions, true) + alreadyAttempted, lastRevs, _ := alreadyAttemptedSync(app, desiredRevisions, true) if !alreadyAttempted && len(lastRevs) != 0 { - logCtx.Infof("A new revision is available after %s with %s: %s", lastPhase, lastRevs, state.Message) return true } return false @@ -2204,7 +2203,6 @@ func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus * } } ts.AddCheckpoint("already_attempted_check_ms") - logCtx.Infof("FLARE: Detected change after %s/%s, desired %s", lastAttemptedPhase, lastAttemptedRevisions, desiredRevisions) if app.Spec.SyncPolicy.Automated.Prune && !app.Spec.SyncPolicy.Automated.AllowEmpty { bAllNeedPrune := true diff --git a/test/e2e/app_autosync_test.go b/test/e2e/app_autosync_test.go index 1902ab46b4887..3ac7e0ad65098 100644 --- a/test/e2e/app_autosync_test.go +++ b/test/e2e/app_autosync_test.go @@ -1,13 +1,12 @@ package e2e import ( - "testing" - "time" - . "github.com/argoproj/gitops-engine/pkg/sync/common" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "testing" + "time" . "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" "github.com/argoproj/argo-cd/v3/test/e2e/fixture" @@ -95,15 +94,22 @@ func TestAutoSyncSelfHealEnabled(t *testing.T) { } func TestAutoSyncSelfHealRetryAndRefreshEnabled(t *testing.T) { + autoSyncSelfHealRetryAndRefreshEnabled(t, 100) + autoSyncSelfHealRetryAndRefreshEnabled(t, -1) +} + +func autoSyncSelfHealRetryAndRefreshEnabled(t *testing.T, limit int64) { Given(t). Path(guestbookPath). When(). // Correctly configured app should be auto-synced once created CreateFromFile(func(app *Application) { app.Spec.SyncPolicy = &SyncPolicy{ - Automated: &SyncPolicyAutomated{SelfHeal: true}, + Automated: &SyncPolicyAutomated{ + SelfHeal: true, + }, Retry: &RetryStrategy{ - Limit: -1, + Limit: limit, Refresh: true, }, } @@ -132,10 +138,14 @@ func TestAutoSyncSelfHealRetryAndRefreshEnabled(t *testing.T) { // Push fix commit and see the app pick it up When(). - RevertCommit(). // Fix declaration + // Fix declaration + PatchFile("guestbook-ui-deployment.yaml", `[{"op": "replace", "path": "/spec/revisionHistoryLimit", "value": 42}]`). Refresh(RefreshTypeNormal). - // Wait for the sync retry to pick up new commit Then(). + // Wait for the sync retry to pick up new commit + And(func(_ *Application) { + time.Sleep(10 * time.Second) + }). Expect(NoConditions()). Expect(SyncStatusIs(SyncStatusCodeSynced)). Expect(OperationPhaseIs(OperationSucceeded)) From d84b5d70890646f7163ebb21a20c42bee2edbed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gond=C5=BEa?= Date: Wed, 23 Jul 2025 12:23:37 +0200 Subject: [PATCH 11/22] chore(e2e): Remove now unused RevertCommit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Oliver Gondža --- test/e2e/fixture/app/actions.go | 6 ------ test/e2e/fixture/fixture.go | 10 ---------- 2 files changed, 16 deletions(-) diff --git a/test/e2e/fixture/app/actions.go b/test/e2e/fixture/app/actions.go index 4212432dbc88b..11a268edde812 100644 --- a/test/e2e/fixture/app/actions.go +++ b/test/e2e/fixture/app/actions.go @@ -58,12 +58,6 @@ func (a *Actions) WriteFile(fileName, fileContents string) *Actions { return a } -func (a *Actions) RevertCommit() *Actions { - a.context.t.Helper() - fixture.RevertCommit(a.context.t) - return a -} - func (a *Actions) AddFile(fileName, fileContents string) *Actions { a.context.t.Helper() fixture.AddFile(a.context.t, a.context.path+"/"+fileName, fileContents) diff --git a/test/e2e/fixture/fixture.go b/test/e2e/fixture/fixture.go index b6cdf3f5d50b0..a75b5e418ab54 100644 --- a/test/e2e/fixture/fixture.go +++ b/test/e2e/fixture/fixture.go @@ -1128,16 +1128,6 @@ func WriteFile(t *testing.T, path, contents string) { require.NoError(t, os.WriteFile(filepath.Join(repoDirectory(), path), []byte(contents), 0o644)) } -func RevertCommit(t *testing.T) { - t.Helper() - - errors.NewHandler(t).FailOnErr(Run(repoDirectory(), "git", "revert", "HEAD")) - - if IsRemote() { - errors.NewHandler(t).FailOnErr(Run(repoDirectory(), "git", "push", "-f", "origin", "master")) - } -} - func AddFile(t *testing.T, path, contents string) { t.Helper() WriteFile(t, path, contents) From 6d6d352704be440589c319b7234dab9616557746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gond=C5=BEa?= Date: Wed, 23 Jul 2025 13:24:25 +0200 Subject: [PATCH 12/22] chore: Use data driven test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Oliver Gondža --- test/e2e/app_autosync_test.go | 108 +++++++++++++++++----------------- 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/test/e2e/app_autosync_test.go b/test/e2e/app_autosync_test.go index 3ac7e0ad65098..5d70bce358afa 100644 --- a/test/e2e/app_autosync_test.go +++ b/test/e2e/app_autosync_test.go @@ -94,59 +94,61 @@ func TestAutoSyncSelfHealEnabled(t *testing.T) { } func TestAutoSyncSelfHealRetryAndRefreshEnabled(t *testing.T) { - autoSyncSelfHealRetryAndRefreshEnabled(t, 100) - autoSyncSelfHealRetryAndRefreshEnabled(t, -1) -} + limits := []int64{ + 100, // Repeat enough times to see we move on to 3rd commit without reaching the limit + -1, // Repeat forever + } -func autoSyncSelfHealRetryAndRefreshEnabled(t *testing.T, limit int64) { - Given(t). - Path(guestbookPath). - When(). - // Correctly configured app should be auto-synced once created - CreateFromFile(func(app *Application) { - app.Spec.SyncPolicy = &SyncPolicy{ - Automated: &SyncPolicyAutomated{ - SelfHeal: true, - }, - Retry: &RetryStrategy{ - Limit: limit, - Refresh: true, - }, - } - }). - Then(). - Expect(OperationPhaseIs(OperationSucceeded)). - Expect(SyncStatusIs(SyncStatusCodeSynced)). - Expect(NoConditions()). - // Broken commit should make the app stuck retrying indefinitely - When(). - PatchFile("guestbook-ui-deployment.yaml", `[{"op": "replace", "path": "/spec/revisionHistoryLimit", "value": "badValue"}]`). - Refresh(RefreshTypeNormal). - Then(). - Expect(OperationPhaseIs(OperationRunning)). - Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). - Expect(OperationMessageContains("one or more objects failed to apply")). - Expect(OperationMessageContains("Retrying attempt #1")). - // Wait to make sure the condition is consistent - And(func(_ *Application) { - time.Sleep(10 * time.Second) - }). - Expect(OperationPhaseIs(OperationRunning)). - Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). - Expect(OperationMessageContains("one or more objects failed to apply")). - Expect(OperationMessageContains("Retrying attempt #2")). + for _, limit := range limits { + Given(t). + Path(guestbookPath). + When(). + // Correctly configured app should be auto-synced once created + CreateFromFile(func(app *Application) { + app.Spec.SyncPolicy = &SyncPolicy{ + Automated: &SyncPolicyAutomated{ + SelfHeal: true, + }, + Retry: &RetryStrategy{ + Limit: limit, + Refresh: true, + }, + } + }). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(NoConditions()). + // Broken commit should make the app stuck retrying indefinitely + When(). + PatchFile("guestbook-ui-deployment.yaml", `[{"op": "replace", "path": "/spec/revisionHistoryLimit", "value": "badValue"}]`). + Refresh(RefreshTypeNormal). + Then(). + Expect(OperationPhaseIs(OperationRunning)). + Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). + Expect(OperationMessageContains("one or more objects failed to apply")). + Expect(OperationMessageContains("Retrying attempt #1")). + // Wait to make sure the condition is consistent + And(func(_ *Application) { + time.Sleep(10 * time.Second) + }). + Expect(OperationPhaseIs(OperationRunning)). + Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). + Expect(OperationMessageContains("one or more objects failed to apply")). + Expect(OperationMessageContains("Retrying attempt #2")). - // Push fix commit and see the app pick it up - When(). - // Fix declaration - PatchFile("guestbook-ui-deployment.yaml", `[{"op": "replace", "path": "/spec/revisionHistoryLimit", "value": 42}]`). - Refresh(RefreshTypeNormal). - Then(). - // Wait for the sync retry to pick up new commit - And(func(_ *Application) { - time.Sleep(10 * time.Second) - }). - Expect(NoConditions()). - Expect(SyncStatusIs(SyncStatusCodeSynced)). - Expect(OperationPhaseIs(OperationSucceeded)) + // Push fix commit and see the app pick it up + When(). + // Fix declaration + PatchFile("guestbook-ui-deployment.yaml", `[{"op": "replace", "path": "/spec/revisionHistoryLimit", "value": 42}]`). + Refresh(RefreshTypeNormal). + Then(). + // Wait for the sync retry to pick up new commit + And(func(_ *Application) { + time.Sleep(10 * time.Second) + }). + Expect(NoConditions()). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(OperationPhaseIs(OperationSucceeded)) + } } From d6c97ea0c9704051c341b6d6ae653983428832e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Gond=C5=BEa?= Date: Wed, 23 Jul 2025 13:47:23 +0200 Subject: [PATCH 13/22] chore: Reorder imports to please linters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Oliver Gondža --- test/e2e/app_autosync_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/e2e/app_autosync_test.go b/test/e2e/app_autosync_test.go index 5d70bce358afa..e6572a91ab058 100644 --- a/test/e2e/app_autosync_test.go +++ b/test/e2e/app_autosync_test.go @@ -1,12 +1,13 @@ package e2e import ( + "testing" + "time" + . "github.com/argoproj/gitops-engine/pkg/sync/common" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "testing" - "time" . "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" "github.com/argoproj/argo-cd/v3/test/e2e/fixture" From a551294e3c68a437ef9fb26bd28f0963fbca46bb Mon Sep 17 00:00:00 2001 From: Alexandre Gaudreault Date: Thu, 21 Aug 2025 16:09:56 -0400 Subject: [PATCH 14/22] fix merge Signed-off-by: Alexandre Gaudreault --- controller/appcontroller.go | 20 ++++++++++++-------- server/application/application.go | 7 +++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/controller/appcontroller.go b/controller/appcontroller.go index e537b111ad422..4f2811abc7c4e 100644 --- a/controller/appcontroller.go +++ b/controller/appcontroller.go @@ -1423,14 +1423,14 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli ctrl.requestAppRefresh(app.QualifiedName(), CompareWithLatest.Pointer(), &retryAfter) return } - - // Remove the desired revisions if the sync failed and we are retrying. The latest revision from the source will be used. - extraMsg := "" - if state.Operation.Retry.Refresh { - extraMsg += " with latest revisions" - state.Operation.Revision = "" - state.Operation.Revisions = nil - } + + // Remove the desired revisions if the sync failed and we are retrying. The latest revision from the source will be used. + extraMsg := "" + if state.Operation.Retry.Refresh { + extraMsg += " with latest revisions" + state.Operation.Sync.Revision = "" + state.Operation.Sync.Revisions = nil + } // Get rid of sync results and null out previous operation completion time // This will start the retry attempt @@ -2154,16 +2154,20 @@ func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus * } } + source := ptr.To(app.Spec.GetSource()) desiredRevisions := []string{syncStatus.Revision} if app.Spec.HasMultipleSources() { + source = nil desiredRevisions = syncStatus.Revisions } op := appv1.Operation{ Sync: &appv1.SyncOperation{ + Source: source, Revision: syncStatus.Revision, Prune: app.Spec.SyncPolicy.Automated.Prune, SyncOptions: app.Spec.SyncPolicy.SyncOptions, + Sources: app.Spec.Sources, Revisions: syncStatus.Revisions, }, InitiatedBy: appv1.OperationInitiator{Automated: true}, diff --git a/server/application/application.go b/server/application/application.go index dadff6ae99a39..9ab55185fec9f 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -2060,8 +2060,15 @@ func (s *Server) Sync(ctx context.Context, syncReq *application.ApplicationSyncR } } } + + var source *v1alpha1.ApplicationSource + if !a.Spec.HasMultipleSources() { + source = ptr.To(a.Spec.GetSource()) + } + op := v1alpha1.Operation{ Sync: &v1alpha1.SyncOperation{ + Source: source, Revision: revision, Prune: syncReq.GetPrune(), DryRun: syncReq.GetDryRun(), From f447e300ef239d63ede8df135fbf24eaba2741fc Mon Sep 17 00:00:00 2001 From: Alexandre Gaudreault Date: Thu, 21 Aug 2025 16:26:21 -0400 Subject: [PATCH 15/22] cleanup Signed-off-by: Alexandre Gaudreault --- controller/appcontroller.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/controller/appcontroller.go b/controller/appcontroller.go index 4f2811abc7c4e..feaede3cd97b2 100644 --- a/controller/appcontroller.go +++ b/controller/appcontroller.go @@ -2092,23 +2092,6 @@ func (ctrl *ApplicationController) persistAppStatus(orig *appv1.Application, new return patchDuration } -func hasNewRevisionsToResync(app *appv1.Application) bool { - if app.Spec.SyncPolicy == nil || !app.Spec.SyncPolicy.IsAutomatedSyncEnabled() { - return false - } - - desiredRevisions := []string{app.Status.Sync.Revision} - if app.Spec.HasMultipleSources() { - desiredRevisions = app.Status.Sync.Revisions - } - - alreadyAttempted, lastRevs, _ := alreadyAttemptedSync(app, desiredRevisions, true) - if !alreadyAttempted && len(lastRevs) != 0 { - return true - } - return false -} - // autoSync will initiate a sync operation for an application configured with automated sync func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *appv1.SyncStatus, resources []appv1.ResourceStatus, shouldCompareRevisions bool) (*appv1.ApplicationCondition, time.Duration) { logCtx := log.WithFields(applog.GetAppLogFields(app)) From 1a67754abeab3740b21891434f6a8924434dfd4b Mon Sep 17 00:00:00 2001 From: Alexandre Gaudreault Date: Fri, 22 Aug 2025 11:06:19 -0400 Subject: [PATCH 16/22] docs Signed-off-by: Alexandre Gaudreault --- cmd/util/app.go | 2 +- pkg/apis/application/v1alpha1/types.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/util/app.go b/cmd/util/app.go index 4e0c37219769b..102a3256abe97 100644 --- a/cmd/util/app.go +++ b/cmd/util/app.go @@ -169,7 +169,7 @@ func AddAppFlags(command *cobra.Command, opts *AppOptions) { command.Flags().DurationVar(&opts.retryBackoffDuration, "sync-retry-backoff-duration", argoappv1.DefaultSyncRetryDuration, "Sync retry backoff base duration. Input needs to be a duration (e.g. 2m, 1h)") command.Flags().DurationVar(&opts.retryBackoffMaxDuration, "sync-retry-backoff-max-duration", argoappv1.DefaultSyncRetryMaxDuration, "Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h)") command.Flags().Int64Var(&opts.retryBackoffFactor, "sync-retry-backoff-factor", argoappv1.DefaultSyncRetryFactor, "Factor multiplies the base duration after each failed sync retry") - command.Flags().BoolVar(&opts.retryRefresh, "sync-retry-refresh", false, "Set if a new revision should trigger a new sync") + command.Flags().BoolVar(&opts.retryRefresh, "sync-retry-refresh", false, "Indicates if the latest revision should be used on retry instead of the initial one") command.Flags().StringVar(&opts.ref, "ref", "", "Ref is reference to another source within sources field") command.Flags().StringVar(&opts.SourceName, "source-name", "", "Name of the source from the list of sources of the app.") } diff --git a/pkg/apis/application/v1alpha1/types.go b/pkg/apis/application/v1alpha1/types.go index 11ac8e55adbe8..2acc8e02d36f7 100644 --- a/pkg/apis/application/v1alpha1/types.go +++ b/pkg/apis/application/v1alpha1/types.go @@ -1467,7 +1467,7 @@ type RetryStrategy struct { Limit int64 `json:"limit,omitempty" protobuf:"bytes,1,opt,name=limit"` // Backoff controls how to backoff on subsequent retries of failed syncs Backoff *Backoff `json:"backoff,omitempty" protobuf:"bytes,2,opt,name=backoff,casttype=Backoff"` - // Refresh indicates if a new revision should trigger a new sync (default: false) + // Refresh indicates if the latest revision should be used on retry instead of the initial one (default: false) Refresh bool `json:"refresh,omitempty" protobuf:"bytes,3,opt,name=refresh"` } From d29f69a20fa7130dcf2f0055a8328f007cf58314 Mon Sep 17 00:00:00 2001 From: Alexandre Gaudreault Date: Wed, 10 Sep 2025 11:21:01 -0400 Subject: [PATCH 17/22] codegen Signed-off-by: Alexandre Gaudreault --- assets/swagger.json | 2 +- manifests/crds/application-crd.yaml | 13 +++++++------ pkg/apis/application/v1alpha1/generated.proto | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/assets/swagger.json b/assets/swagger.json index 35978eb307c56..8740c1f157735 100644 --- a/assets/swagger.json +++ b/assets/swagger.json @@ -9963,7 +9963,7 @@ }, "refresh": { "type": "boolean", - "title": "Refresh indicates if a new revision should trigger a new sync (default: false)" + "title": "Refresh indicates if the latest revision should be used on retry instead of the initial one (default: false)" } } }, diff --git a/manifests/crds/application-crd.yaml b/manifests/crds/application-crd.yaml index d37c09add845e..2161027ee42d0 100644 --- a/manifests/crds/application-crd.yaml +++ b/manifests/crds/application-crd.yaml @@ -111,8 +111,8 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should trigger - a new sync (default: false)' + description: 'Refresh indicates if the latest revision should + be used on retry instead of the initial one (default: false)' type: boolean type: object sync: @@ -1942,8 +1942,8 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should trigger - a new sync (default: false)' + description: 'Refresh indicates if the latest revision should + be used on retry instead of the initial one (default: false)' type: boolean type: object syncOptions: @@ -2914,8 +2914,9 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should - trigger a new sync (default: false)' + description: 'Refresh indicates if the latest revision + should be used on retry instead of the initial one (default: + false)' type: boolean type: object sync: diff --git a/pkg/apis/application/v1alpha1/generated.proto b/pkg/apis/application/v1alpha1/generated.proto index f15e48ac12627..b6db18fd8c7d5 100644 --- a/pkg/apis/application/v1alpha1/generated.proto +++ b/pkg/apis/application/v1alpha1/generated.proto @@ -2226,7 +2226,7 @@ message RetryStrategy { // Backoff controls how to backoff on subsequent retries of failed syncs optional Backoff backoff = 2; - // Refresh indicates if a new revision should trigger a new sync (default: false) + // Refresh indicates if the latest revision should be used on retry instead of the initial one (default: false) optional bool refresh = 3; } From dd5fb7bd9914c44d4cdbf4a3e78091cc7b379e04 Mon Sep 17 00:00:00 2001 From: Alexandre Gaudreault Date: Wed, 10 Sep 2025 11:26:34 -0400 Subject: [PATCH 18/22] docs Signed-off-by: Alexandre Gaudreault --- .../commands/argocd_admin_app_generate-spec.md | 2 +- docs/user-guide/commands/argocd_app_add-source.md | 2 +- docs/user-guide/commands/argocd_app_create.md | 2 +- docs/user-guide/commands/argocd_app_set.md | 2 +- manifests/core-install-with-hydrator.yaml | 13 +++++++------ manifests/core-install.yaml | 13 +++++++------ manifests/ha/install-with-hydrator.yaml | 13 +++++++------ manifests/ha/install.yaml | 13 +++++++------ manifests/install-with-hydrator.yaml | 13 +++++++------ manifests/install.yaml | 13 +++++++------ 10 files changed, 46 insertions(+), 40 deletions(-) diff --git a/docs/user-guide/commands/argocd_admin_app_generate-spec.md b/docs/user-guide/commands/argocd_admin_app_generate-spec.md index 16783b9fdaa1c..569e5d16e27b8 100644 --- a/docs/user-guide/commands/argocd_admin_app_generate-spec.md +++ b/docs/user-guide/commands/argocd_admin_app_generate-spec.md @@ -107,7 +107,7 @@ argocd admin app generate-spec APPNAME [flags] --sync-retry-backoff-factor int Factor multiplies the base duration after each failed sync retry (default 2) --sync-retry-backoff-max-duration duration Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h) (default 3m0s) --sync-retry-limit int Max number of allowed sync retries - --sync-retry-refresh Set if a new revision should trigger a new sync + --sync-retry-refresh Indicates if the latest revision should be used on retry instead of the initial one --sync-source-branch string The branch from which the app will sync --sync-source-path string The path in the repository from which the app will sync --validate Validation of repo and cluster (default true) diff --git a/docs/user-guide/commands/argocd_app_add-source.md b/docs/user-guide/commands/argocd_app_add-source.md index 09d1cec3c39ca..962039c994d4a 100644 --- a/docs/user-guide/commands/argocd_app_add-source.md +++ b/docs/user-guide/commands/argocd_app_add-source.md @@ -84,7 +84,7 @@ argocd app add-source APPNAME [flags] --sync-retry-backoff-factor int Factor multiplies the base duration after each failed sync retry (default 2) --sync-retry-backoff-max-duration duration Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h) (default 3m0s) --sync-retry-limit int Max number of allowed sync retries - --sync-retry-refresh Set if a new revision should trigger a new sync + --sync-retry-refresh Indicates if the latest revision should be used on retry instead of the initial one --sync-source-branch string The branch from which the app will sync --sync-source-path string The path in the repository from which the app will sync --validate Validation of repo and cluster (default true) diff --git a/docs/user-guide/commands/argocd_app_create.md b/docs/user-guide/commands/argocd_app_create.md index 73de6a512d9de..cb5c1d0c6309f 100644 --- a/docs/user-guide/commands/argocd_app_create.md +++ b/docs/user-guide/commands/argocd_app_create.md @@ -107,7 +107,7 @@ argocd app create APPNAME [flags] --sync-retry-backoff-factor int Factor multiplies the base duration after each failed sync retry (default 2) --sync-retry-backoff-max-duration duration Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h) (default 3m0s) --sync-retry-limit int Max number of allowed sync retries - --sync-retry-refresh Set if a new revision should trigger a new sync + --sync-retry-refresh Indicates if the latest revision should be used on retry instead of the initial one --sync-source-branch string The branch from which the app will sync --sync-source-path string The path in the repository from which the app will sync --upsert Allows to override application with the same name even if supplied application spec is different from existing spec diff --git a/docs/user-guide/commands/argocd_app_set.md b/docs/user-guide/commands/argocd_app_set.md index a49cad82369d4..3390d4ac3d5c2 100644 --- a/docs/user-guide/commands/argocd_app_set.md +++ b/docs/user-guide/commands/argocd_app_set.md @@ -97,7 +97,7 @@ argocd app set APPNAME [flags] --sync-retry-backoff-factor int Factor multiplies the base duration after each failed sync retry (default 2) --sync-retry-backoff-max-duration duration Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h) (default 3m0s) --sync-retry-limit int Max number of allowed sync retries - --sync-retry-refresh Set if a new revision should trigger a new sync + --sync-retry-refresh Indicates if the latest revision should be used on retry instead of the initial one --sync-source-branch string The branch from which the app will sync --sync-source-path string The path in the repository from which the app will sync --validate Validation of repo and cluster (default true) diff --git a/manifests/core-install-with-hydrator.yaml b/manifests/core-install-with-hydrator.yaml index 179f99347bb94..a04995f511af3 100644 --- a/manifests/core-install-with-hydrator.yaml +++ b/manifests/core-install-with-hydrator.yaml @@ -112,8 +112,8 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should trigger - a new sync (default: false)' + description: 'Refresh indicates if the latest revision should + be used on retry instead of the initial one (default: false)' type: boolean type: object sync: @@ -1943,8 +1943,8 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should trigger - a new sync (default: false)' + description: 'Refresh indicates if the latest revision should + be used on retry instead of the initial one (default: false)' type: boolean type: object syncOptions: @@ -2915,8 +2915,9 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should - trigger a new sync (default: false)' + description: 'Refresh indicates if the latest revision + should be used on retry instead of the initial one (default: + false)' type: boolean type: object sync: diff --git a/manifests/core-install.yaml b/manifests/core-install.yaml index 703b7de55773c..331c732e756b7 100644 --- a/manifests/core-install.yaml +++ b/manifests/core-install.yaml @@ -112,8 +112,8 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should trigger - a new sync (default: false)' + description: 'Refresh indicates if the latest revision should + be used on retry instead of the initial one (default: false)' type: boolean type: object sync: @@ -1943,8 +1943,8 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should trigger - a new sync (default: false)' + description: 'Refresh indicates if the latest revision should + be used on retry instead of the initial one (default: false)' type: boolean type: object syncOptions: @@ -2915,8 +2915,9 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should - trigger a new sync (default: false)' + description: 'Refresh indicates if the latest revision + should be used on retry instead of the initial one (default: + false)' type: boolean type: object sync: diff --git a/manifests/ha/install-with-hydrator.yaml b/manifests/ha/install-with-hydrator.yaml index eb20da1d1f434..43632af5c9ce4 100644 --- a/manifests/ha/install-with-hydrator.yaml +++ b/manifests/ha/install-with-hydrator.yaml @@ -112,8 +112,8 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should trigger - a new sync (default: false)' + description: 'Refresh indicates if the latest revision should + be used on retry instead of the initial one (default: false)' type: boolean type: object sync: @@ -1943,8 +1943,8 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should trigger - a new sync (default: false)' + description: 'Refresh indicates if the latest revision should + be used on retry instead of the initial one (default: false)' type: boolean type: object syncOptions: @@ -2915,8 +2915,9 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should - trigger a new sync (default: false)' + description: 'Refresh indicates if the latest revision + should be used on retry instead of the initial one (default: + false)' type: boolean type: object sync: diff --git a/manifests/ha/install.yaml b/manifests/ha/install.yaml index 5e603476e7bd2..b8d231d2699e6 100644 --- a/manifests/ha/install.yaml +++ b/manifests/ha/install.yaml @@ -112,8 +112,8 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should trigger - a new sync (default: false)' + description: 'Refresh indicates if the latest revision should + be used on retry instead of the initial one (default: false)' type: boolean type: object sync: @@ -1943,8 +1943,8 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should trigger - a new sync (default: false)' + description: 'Refresh indicates if the latest revision should + be used on retry instead of the initial one (default: false)' type: boolean type: object syncOptions: @@ -2915,8 +2915,9 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should - trigger a new sync (default: false)' + description: 'Refresh indicates if the latest revision + should be used on retry instead of the initial one (default: + false)' type: boolean type: object sync: diff --git a/manifests/install-with-hydrator.yaml b/manifests/install-with-hydrator.yaml index fb60fa977bb98..5523cfb7761ba 100644 --- a/manifests/install-with-hydrator.yaml +++ b/manifests/install-with-hydrator.yaml @@ -112,8 +112,8 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should trigger - a new sync (default: false)' + description: 'Refresh indicates if the latest revision should + be used on retry instead of the initial one (default: false)' type: boolean type: object sync: @@ -1943,8 +1943,8 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should trigger - a new sync (default: false)' + description: 'Refresh indicates if the latest revision should + be used on retry instead of the initial one (default: false)' type: boolean type: object syncOptions: @@ -2915,8 +2915,9 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should - trigger a new sync (default: false)' + description: 'Refresh indicates if the latest revision + should be used on retry instead of the initial one (default: + false)' type: boolean type: object sync: diff --git a/manifests/install.yaml b/manifests/install.yaml index 61d5802f41da4..14914e16c9c8b 100644 --- a/manifests/install.yaml +++ b/manifests/install.yaml @@ -112,8 +112,8 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should trigger - a new sync (default: false)' + description: 'Refresh indicates if the latest revision should + be used on retry instead of the initial one (default: false)' type: boolean type: object sync: @@ -1943,8 +1943,8 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should trigger - a new sync (default: false)' + description: 'Refresh indicates if the latest revision should + be used on retry instead of the initial one (default: false)' type: boolean type: object syncOptions: @@ -2915,8 +2915,9 @@ spec: format: int64 type: integer refresh: - description: 'Refresh indicates if a new revision should - trigger a new sync (default: false)' + description: 'Refresh indicates if the latest revision + should be used on retry instead of the initial one (default: + false)' type: boolean type: object sync: From 37234626a437f6d055e5c09e5a20a9ee49ed5957 Mon Sep 17 00:00:00 2001 From: Alexandre Gaudreault Date: Wed, 10 Sep 2025 16:09:17 -0400 Subject: [PATCH 19/22] tests Signed-off-by: Alexandre Gaudreault --- server/application/application_test.go | 38 ++++++- test/e2e/app_autosync_test.go | 146 ++++++++++++------------- test/e2e/fixture/app/expectation.go | 18 ++- 3 files changed, 116 insertions(+), 86 deletions(-) diff --git a/server/application/application_test.go b/server/application/application_test.go index 8108c28fd0167..9fa2e58de8ec9 100644 --- a/server/application/application_test.go +++ b/server/application/application_test.go @@ -2133,6 +2133,39 @@ func TestSyncGit(t *testing.T) { func TestRollbackApp(t *testing.T) { testApp := newTestApp() + testApp.Status.History = []v1alpha1.RevisionHistory{{ + ID: 1, + Revision: "abc", + Revisions: []string{"abc"}, + Source: *testApp.Spec.Source.DeepCopy(), + Sources: []v1alpha1.ApplicationSource{*testApp.Spec.Source.DeepCopy()}, + }} + appServer := newTestAppServer(t, testApp) + + updatedApp, err := appServer.Rollback(t.Context(), &application.ApplicationRollbackRequest{ + Name: &testApp.Name, + Id: ptr.To(int64(1)), + }) + + require.NoError(t, err) + + assert.NotNil(t, updatedApp.Operation) + assert.NotNil(t, updatedApp.Operation.Sync) + assert.NotNil(t, updatedApp.Operation.Sync.Source) + assert.Equal(t, testApp.Status.History[0].Source, *updatedApp.Operation.Sync.Source) + assert.Equal(t, testApp.Status.History[0].Sources, updatedApp.Operation.Sync.Sources) + assert.Equal(t, testApp.Status.History[0].Revision, updatedApp.Operation.Sync.Revision) + assert.Equal(t, testApp.Status.History[0].Revisions, updatedApp.Operation.Sync.Revisions) +} + +func TestRollbackApp_WithRefresh(t *testing.T) { + testApp := newTestApp() + testApp.Spec.SyncPolicy = &v1alpha1.SyncPolicy{ + Retry: &v1alpha1.RetryStrategy{ + Refresh: true, + }, + } + testApp.Status.History = []v1alpha1.RevisionHistory{{ ID: 1, Revision: "abc", @@ -2148,9 +2181,8 @@ func TestRollbackApp(t *testing.T) { require.NoError(t, err) assert.NotNil(t, updatedApp.Operation) - assert.NotNil(t, updatedApp.Operation.Sync) - assert.NotNil(t, updatedApp.Operation.Sync.Source) - assert.Equal(t, "abc", updatedApp.Operation.Sync.Revision) + assert.NotNil(t, updatedApp.Operation.Retry) + assert.False(t, updatedApp.Operation.Retry.Refresh, "refresh should never be set on rollback") } func TestUpdateAppProject(t *testing.T) { diff --git a/test/e2e/app_autosync_test.go b/test/e2e/app_autosync_test.go index c2ee55824c562..b2006334f5cd1 100644 --- a/test/e2e/app_autosync_test.go +++ b/test/e2e/app_autosync_test.go @@ -1,6 +1,7 @@ package e2e import ( + "fmt" "testing" "time" @@ -8,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" . "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" "github.com/argoproj/argo-cd/v3/test/e2e/fixture" @@ -94,60 +96,8 @@ func TestAutoSyncSelfHealEnabled(t *testing.T) { }) } -// TestAutoSyncRetryAndRefreshEnabled verifies that auto-sync+refresh picks up fixed commits automatically +// TestAutoSyncRetryAndRefreshEnabled verifies that auto-sync+refresh picks up new commits automatically func TestAutoSyncRetryAndRefreshEnabled(t *testing.T) { - limits := []int64{ - 100, // Repeat enough times to see we move on to the 3rd commit without reaching the limit - -1, // Repeat forever - } - - for _, limit := range limits { - Given(t). - Path(guestbookPath). - When(). // I create an app with auto-sync and Refresh - CreateFromFile(func(app *Application) { - app.Spec.SyncPolicy = &SyncPolicy{ - Automated: &SyncPolicyAutomated{}, - Retry: &RetryStrategy{ - Limit: limit, - Refresh: true, - }, - } - }). - Then(). // It should auto-sync correctly - Expect(OperationPhaseIs(OperationSucceeded)). - Expect(SyncStatusIs(SyncStatusCodeSynced)). - Expect(NoConditions()). - When(). // Auto-sync encounters broken commit - PatchFile("guestbook-ui-deployment.yaml", `[{"op": "replace", "path": "/spec/revisionHistoryLimit", "value": "badValue"}]`). - Refresh(RefreshTypeNormal). - Then(). // It should keep on trying to sync it - Expect(OperationPhaseIs(OperationRunning)). - Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). - Expect(OperationRetriedTimes(1)). - // Wait to make sure the condition is consistent - And(func(_ *Application) { - time.Sleep(10 * time.Second) - }). - Expect(OperationPhaseIs(OperationRunning)). - Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). - Expect(OperationRetriedTimes(2)). - When(). // I push a fixed commit (while auto-sync in progress) - PatchFile("guestbook-ui-deployment.yaml", `[{"op": "replace", "path": "/spec/revisionHistoryLimit", "value": 42}]`). - Refresh(RefreshTypeNormal). - Then(). // Argo CD should pick it up and sync it successfully - // Wait for the sync retry to pick up a new commit - And(func(_ *Application) { - time.Sleep(10 * time.Second) - }). - Expect(NoConditions()). - Expect(SyncStatusIs(SyncStatusCodeSynced)). - Expect(OperationPhaseIs(OperationSucceeded)) - } -} - -// TestAutoSyncRetryAndRefreshManualSync verifies that auto-sync+refresh do not pick new commits on manual sync -func TestAutoSyncRetryAndRefreshManualSync(t *testing.T) { Given(t). Path(guestbookPath). When(). // I create an app with auto-sync and Refresh @@ -157,6 +107,11 @@ func TestAutoSyncRetryAndRefreshManualSync(t *testing.T) { Retry: &RetryStrategy{ Limit: -1, Refresh: true, + Backoff: &Backoff{ + Duration: time.Second.String(), + Factor: ptr.To(int64(1)), + MaxDuration: time.Second.String(), + }, }, } }). @@ -164,35 +119,68 @@ func TestAutoSyncRetryAndRefreshManualSync(t *testing.T) { Expect(OperationPhaseIs(OperationSucceeded)). Expect(SyncStatusIs(SyncStatusCodeSynced)). Expect(NoConditions()). - When(). // I manually sync the app on a broken commit + When(). // Auto-sync encounters broken commit PatchFile("guestbook-ui-deployment.yaml", `[{"op": "replace", "path": "/spec/revisionHistoryLimit", "value": "badValue"}]`). - Sync("--async"). - Then(). // Argo should keep on retrying - Expect(OperationPhaseIs(OperationRunning)). - Expect(OperationRetriedTimes(1)). - And(func(_ *Application) { - // Wait to make sure the condition is consistent - time.Sleep(10 * time.Second) - }). + Refresh(RefreshTypeNormal). + Then(). // It should keep on trying to sync it Expect(OperationPhaseIs(OperationRunning)). - Expect(OperationRetriedTimes(2)). - When(). // I push a fixed commit (during manual sync) + Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). + Expect(OperationRetriedMinimumTimes(1)). + When(). // I push a fixed commit (while auto-sync in progress) PatchFile("guestbook-ui-deployment.yaml", `[{"op": "replace", "path": "/spec/revisionHistoryLimit", "value": 42}]`). - Then(). // Argo CD should keep on retrying the one from the tyme of the sync start - And(func(_ *Application) { - // Wait to make sure the condition is consistent - time.Sleep(10 * time.Second) - }). - Expect(OperationRetriedTimes(3)). - When(). // I terminate the stuck sync and start a new manual one (when ref points to fixed commit) - TerminateOp(). - And(func() { - // Wait for the operation to terminate before starting new sync - time.Sleep(1 * time.Second) + Refresh(RefreshTypeNormal). + Then(). + // Argo CD should pick it up and sync it successfully + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)) +} + +// TestAutoSyncRetryAndRefreshEnabled verifies that auto-sync+refresh picks up new commits automatically on the original source +// at the time the sync was triggered +func TestAutoSyncRetryAndRefreshEnabledChangedSource(t *testing.T) { + Given(t). + Path(guestbookPath). + When(). // I create an app with auto-sync and Refresh + CreateFromFile(func(app *Application) { + app.Spec.SyncPolicy = &SyncPolicy{ + Automated: &SyncPolicyAutomated{}, + Retry: &RetryStrategy{ + Limit: -1, // Repeat forever + Refresh: true, + Backoff: &Backoff{ + Duration: time.Second.String(), + Factor: ptr.To(int64(1)), + MaxDuration: time.Second.String(), + }, + }, + } }). - Sync("--async"). - Then(). // Argo CD syncs successfully - Expect(NoConditions()). + Then(). // It should auto-sync correctly + Expect(OperationPhaseIs(OperationSucceeded)). Expect(SyncStatusIs(SyncStatusCodeSynced)). - Expect(OperationPhaseIs(OperationSucceeded)) + Expect(NoConditions()). + When(). // Auto-sync encounters broken commit + PatchFile("guestbook-ui-deployment.yaml", `[{"op": "replace", "path": "/spec/revisionHistoryLimit", "value": "badValue"}]`). + Refresh(RefreshTypeNormal). + Then(). // It should keep on trying to sync it + Expect(OperationPhaseIs(OperationRunning)). + Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). + Expect(OperationRetriedMinimumTimes(1)). + When(). + PatchApp(`[{"op": "add", "path": "/spec/source/path", "value": "failure-during-sync"}]`). + // push a fixed commit on HEAD branch + PatchFile("guestbook-ui-deployment.yaml", `[{"op": "replace", "path": "/spec/revisionHistoryLimit", "value": 42}]`). + Refresh(RefreshTypeNormal). + Then(). + Expect(Status(func(status ApplicationStatus) (bool, string) { + // Validate that the history contains the sync to the previous sources + // The history will only contain successful sync + if len(status.History) != 2 { + return false, "expected len to be 2" + } + if status.History[1].Source.Path != guestbookPath { + return false, fmt.Sprintf("expected source path to be '%s'", guestbookPath) + } + return true, "" + })) } diff --git a/test/e2e/fixture/app/expectation.go b/test/e2e/fixture/app/expectation.go index cf8987853e4c0..92f5bf44bbe51 100644 --- a/test/e2e/fixture/app/expectation.go +++ b/test/e2e/fixture/app/expectation.go @@ -53,12 +53,12 @@ func OperationMessageContains(text string) Expectation { } } -func OperationRetriedTimes(expected int64) Expectation { +func OperationRetriedMinimumTimes(minRetries int64) Expectation { return func(c *Consequences) (state, string) { operationState := c.app().Status.OperationState actual := operationState.RetryCount - message := fmt.Sprintf("operation state retry cound should be %d, is %d, message: '%s'", expected, actual, operationState.Message) - return simple(actual == expected, message) + message := fmt.Sprintf("operation state retry cound should be at least %d, is %d, message: '%s'", minRetries, actual, operationState.Message) + return simple(actual >= minRetries, message) } } @@ -126,6 +126,16 @@ func StatusExists() Expectation { } } +func Status(f func(v1alpha1.ApplicationStatus) (bool, string)) Expectation { + return func(c *Consequences) (state, string) { + ok, msg := f(c.app().Status) + if !ok { + return pending, msg + } + return succeeded, msg + } +} + func Namespace(name string, block func(app *v1alpha1.Application, ns *corev1.Namespace)) Expectation { return func(c *Consequences) (state, string) { ns, err := namespace(name) @@ -359,7 +369,7 @@ func Success(message string, matchers ...func(string, string) bool) Expectation } return func(c *Consequences) (state, string) { if c.actions.lastError != nil { - return failed, "error" + return failed, fmt.Errorf("error: %w", c.actions.lastError).Error() } if !match(c.actions.lastOutput, message) { return failed, fmt.Sprintf("output did not contain '%s'", message) From 73a7c2ef1c23a1a872a3d3fdc0a8d13f184a72cf Mon Sep 17 00:00:00 2001 From: Alexandre Gaudreault Date: Wed, 10 Sep 2025 17:03:00 -0400 Subject: [PATCH 20/22] cli Signed-off-by: Alexandre Gaudreault --- cmd/argocd/commands/app.go | 17 ++++++++++------- server/application/application_test.go | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/cmd/argocd/commands/app.go b/cmd/argocd/commands/app.go index 7c916067faf3b..e1dad479685b7 100644 --- a/cmd/argocd/commands/app.go +++ b/cmd/argocd/commands/app.go @@ -353,7 +353,7 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com command := &cobra.Command{ Use: "get APPNAME", Short: "Get application details", - Example: templates.Examples(` + Example: templates.Examples(` # Get basic details about the application "my-app" in wide format argocd app get my-app -o wide @@ -383,7 +383,7 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com # Get application details and display them in a tree format argocd app get my-app --output tree - + # Get application details and display them in a detailed tree format argocd app get my-app --output tree=detailed `), @@ -541,7 +541,7 @@ func NewApplicationLogsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co command := &cobra.Command{ Use: "logs APPNAME", Short: "Get logs of application pods", - Example: templates.Examples(` + Example: templates.Examples(` # Get logs of pods associated with the application "my-app" argocd app logs my-app @@ -855,7 +855,7 @@ func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com command := &cobra.Command{ Use: "set APPNAME", Short: "Set application parameters", - Example: templates.Examples(` + Example: templates.Examples(` # Set application parameters for the application "my-app" argocd app set my-app --parameter key1=value1 --parameter key2=value2 @@ -2085,6 +2085,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co applyOutOfSyncOnly bool async bool retryLimit int64 + retryRefresh bool retryBackoffDuration time.Duration retryBackoffMaxDuration time.Duration retryBackoffFactor int64 @@ -2356,9 +2357,10 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co default: log.Fatalf("Unknown sync strategy: '%s'", strategy) } - if retryLimit > 0 { + if retryLimit != 0 { syncReq.RetryStrategy = &argoappv1.RetryStrategy{ - Limit: retryLimit, + Limit: retryLimit, + Refresh: retryRefresh, Backoff: &argoappv1.Backoff{ Duration: retryBackoffDuration.String(), MaxDuration: retryBackoffMaxDuration.String(), @@ -2427,6 +2429,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co command.Flags().StringArrayVar(&labels, "label", []string{}, "Sync only specific resources with a label. This option may be specified repeatedly.") command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds") command.Flags().Int64Var(&retryLimit, "retry-limit", 0, "Max number of allowed sync retries") + command.Flags().BoolVar(&retryRefresh, "retry-refresh", false, "Indicates if the latest revision should be used on retry instead of the initial one") command.Flags().DurationVar(&retryBackoffDuration, "retry-backoff-duration", argoappv1.DefaultSyncRetryDuration, "Retry backoff base duration. Input needs to be a duration (e.g. 2m, 1h)") command.Flags().DurationVar(&retryBackoffMaxDuration, "retry-backoff-max-duration", argoappv1.DefaultSyncRetryMaxDuration, "Max retry backoff duration. Input needs to be a duration (e.g. 2m, 1h)") command.Flags().Int64Var(&retryBackoffFactor, "retry-backoff-factor", argoappv1.DefaultSyncRetryFactor, "Factor multiplies the base duration after each failed retry") @@ -3484,7 +3487,7 @@ func NewApplicationRemoveSourceCommand(clientOpts *argocdclient.ClientOptions) * Short: "Remove a source from multiple sources application.", Example: ` # Remove the source at position 1 from application's sources. Counting starts at 1. argocd app remove-source myapplication --source-position 1 - + # Remove the source named "test" from application's sources. argocd app remove-source myapplication --source-name test`, Run: func(c *cobra.Command, args []string) { diff --git a/server/application/application_test.go b/server/application/application_test.go index 9fa2e58de8ec9..71b70b3fa74f7 100644 --- a/server/application/application_test.go +++ b/server/application/application_test.go @@ -2053,6 +2053,7 @@ func TestSyncAndTerminate(t *testing.T) { require.NoError(t, err) assert.NotNil(t, app) assert.NotNil(t, app.Operation) + assert.Equal(t, testApp.Spec.GetSource(), *app.Operation.Sync.Source) events, err := appServer.kubeclientset.CoreV1().Events(appServer.ns).List(t.Context(), metav1.ListOptions{}) require.NoError(t, err) @@ -2131,6 +2132,28 @@ func TestSyncGit(t *testing.T) { assert.Equal(t, "Unknown user initiated sync locally", events.Items[1].Message) } +func TestSync_WithRefresh(t *testing.T) { + ctx := t.Context() + appServer := newTestAppServer(t) + testApp := newTestApp() + testApp.Spec.SyncPolicy = &v1alpha1.SyncPolicy{ + Retry: &v1alpha1.RetryStrategy{ + Refresh: true, + }, + } + testApp.Spec.Source.RepoURL = "https://github.com/argoproj/argo-cd.git" + createReq := application.ApplicationCreateRequest{ + Application: testApp, + } + app, err := appServer.Create(ctx, &createReq) + require.NoError(t, err) + app, err = appServer.Sync(ctx, &application.ApplicationSyncRequest{Name: &app.Name}) + require.NoError(t, err) + assert.NotNil(t, app) + assert.NotNil(t, app.Operation) + assert.True(t, app.Operation.Retry.Refresh) +} + func TestRollbackApp(t *testing.T) { testApp := newTestApp() testApp.Status.History = []v1alpha1.RevisionHistory{{ From f5c640d3f5810baff960de88140de3854414e74a Mon Sep 17 00:00:00 2001 From: Alexandre Gaudreault Date: Wed, 10 Sep 2025 17:19:56 -0400 Subject: [PATCH 21/22] e2e Signed-off-by: Alexandre Gaudreault --- test/e2e/app_management_test.go | 54 +++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/test/e2e/app_management_test.go b/test/e2e/app_management_test.go index df302e8641911..b06c0341b09ae 100644 --- a/test/e2e/app_management_test.go +++ b/test/e2e/app_management_test.go @@ -2112,6 +2112,60 @@ func TestSyncWithInfos(t *testing.T) { }) } +// TestSyncWithRetryAndRefreshEnabled verifies that sync+refresh picks up new commits automatically on the original source +// at the time the sync was triggered +func TestSyncWithRetryAndRefreshEnabled(t *testing.T) { + Given(t). + Timeout(2). // Quick timeout since Sync operation is expected to retry forever + Path(guestbookPath). + When(). + CreateFromFile(func(app *Application) { + app.Spec.SyncPolicy = &SyncPolicy{ + Retry: &RetryStrategy{ + Limit: -1, // Repeat forever + Refresh: true, + Backoff: &Backoff{ + Duration: time.Second.String(), + Factor: ptr.To(int64(1)), + MaxDuration: time.Second.String(), + }, + }, + } + }). + Sync(). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + When(). + PatchFile("guestbook-ui-deployment.yaml", `[{"op": "replace", "path": "/spec/revisionHistoryLimit", "value": "badValue"}]`). + IgnoreErrors(). + Sync(). + DoNotIgnoreErrors(). + Then(). + Expect(OperationPhaseIs(OperationRunning)). + Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). + Expect(OperationRetriedMinimumTimes(1)). + When(). + PatchApp(`[{"op": "add", "path": "/spec/source/path", "value": "failure-during-sync"}]`). + // push a fixed commit on HEAD branch + PatchFile("guestbook-ui-deployment.yaml", `[{"op": "replace", "path": "/spec/revisionHistoryLimit", "value": 42}]`). + IgnoreErrors(). + Sync(). + DoNotIgnoreErrors(). + Then(). + Expect(Status(func(status ApplicationStatus) (bool, string) { + // Validate that the history contains the sync to the previous sources + // The history will only contain successful sync + if len(status.History) != 2 { + return false, "expected len to be 2" + } + if status.History[1].Source.Path != guestbookPath { + return false, fmt.Sprintf("expected source path to be '%s'", guestbookPath) + } + return true, "" + })) +} + // Given: argocd app create does not provide --dest-namespace // // Manifest contains resource console which does not require namespace From 252b1607174e7e877cf0c4c524b3266aeb4977b5 Mon Sep 17 00:00:00 2001 From: Alexandre Gaudreault Date: Wed, 10 Sep 2025 17:22:55 -0400 Subject: [PATCH 22/22] codegen Signed-off-by: Alexandre Gaudreault --- docs/user-guide/commands/argocd_app_remove-source.md | 2 +- docs/user-guide/commands/argocd_app_sync.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/user-guide/commands/argocd_app_remove-source.md b/docs/user-guide/commands/argocd_app_remove-source.md index c04950b550c69..8353ddb70fc94 100644 --- a/docs/user-guide/commands/argocd_app_remove-source.md +++ b/docs/user-guide/commands/argocd_app_remove-source.md @@ -13,7 +13,7 @@ argocd app remove-source APPNAME [flags] ``` # Remove the source at position 1 from application's sources. Counting starts at 1. argocd app remove-source myapplication --source-position 1 - + # Remove the source named "test" from application's sources. argocd app remove-source myapplication --source-name test ``` diff --git a/docs/user-guide/commands/argocd_app_sync.md b/docs/user-guide/commands/argocd_app_sync.md index e9f09603e0f83..1cc09d399ed5e 100644 --- a/docs/user-guide/commands/argocd_app_sync.md +++ b/docs/user-guide/commands/argocd_app_sync.md @@ -64,6 +64,7 @@ argocd app sync [APPNAME... | -l selector | --project project-name] [flags] --retry-backoff-factor int Factor multiplies the base duration after each failed retry (default 2) --retry-backoff-max-duration duration Max retry backoff duration. Input needs to be a duration (e.g. 2m, 1h) (default 3m0s) --retry-limit int Max number of allowed sync retries + --retry-refresh Indicates if the latest revision should be used on retry instead of the initial one --revision string Sync to a specific revision. Preserves parameter overrides --revisions stringArray Show manifests at specific revisions for source position in source-positions -l, --selector string Sync apps that match this label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.