From a1fbf24cb4d0b3d02f3687899812bf59c2ba7b39 Mon Sep 17 00:00:00 2001 From: Anisur Rahman Date: Fri, 20 Feb 2026 12:20:57 +0600 Subject: [PATCH 1/2] Retry post backup actions Signed-off-by: Anisur Rahman --- apis/stash/v1beta1/openapi_generated.go | 20 +++---- pkg/restic/backup.go | 13 +++- pkg/restic/commands.go | 4 +- pkg/restic/config.go | 6 +- pkg/restic/retry.go | 80 +++++++++++++++++++++++++ 5 files changed, 106 insertions(+), 17 deletions(-) create mode 100644 pkg/restic/retry.go diff --git a/apis/stash/v1beta1/openapi_generated.go b/apis/stash/v1beta1/openapi_generated.go index 6e58a5ab3..93d4dac52 100644 --- a/apis/stash/v1beta1/openapi_generated.go +++ b/apis/stash/v1beta1/openapi_generated.go @@ -486,7 +486,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "stash.appscode.dev/apimachinery/apis/stash/v1beta1.RestoreSessionStatus": schema_apimachinery_apis_stash_v1beta1_RestoreSessionStatus(ref), "stash.appscode.dev/apimachinery/apis/stash/v1beta1.RestoreTarget": schema_apimachinery_apis_stash_v1beta1_RestoreTarget(ref), "stash.appscode.dev/apimachinery/apis/stash/v1beta1.RestoreTargetSpec": schema_apimachinery_apis_stash_v1beta1_RestoreTargetSpec(ref), - "stash.appscode.dev/apimachinery/apis/stash/v1beta1.RetryConfig": schema_apimachinery_apis_stash_v1beta1_RetryConfig(ref), + "stash.appscode.dev/apimachinery/apis/stash/v1beta1.retryConfig": schema_apimachinery_apis_stash_v1beta1_RetryConfig(ref), "stash.appscode.dev/apimachinery/apis/stash/v1beta1.Rule": schema_apimachinery_apis_stash_v1beta1_Rule(ref), "stash.appscode.dev/apimachinery/apis/stash/v1beta1.SnapshotStats": schema_apimachinery_apis_stash_v1beta1_SnapshotStats(ref), "stash.appscode.dev/apimachinery/apis/stash/v1beta1.Summary": schema_apimachinery_apis_stash_v1beta1_Summary(ref), @@ -23496,8 +23496,8 @@ func schema_apimachinery_apis_stash_v1beta1_BackupBatchSpec(ref common.Reference }, "retryConfig": { SchemaProps: spec.SchemaProps{ - Description: "RetryConfig specify a configuration for retry a backup if it fails. By default, Stash does not retry any failed backup.", - Ref: ref("stash.appscode.dev/apimachinery/apis/stash/v1beta1.RetryConfig"), + Description: "retryConfig specify a configuration for retry a backup if it fails. By default, Stash does not retry any failed backup.", + Ref: ref("stash.appscode.dev/apimachinery/apis/stash/v1beta1.retryConfig"), }, }, }, @@ -23505,7 +23505,7 @@ func schema_apimachinery_apis_stash_v1beta1_BackupBatchSpec(ref common.Reference }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "kmodules.xyz/client-go/api/v1.ObjectReference", "kmodules.xyz/offshoot-api/api/v1.RuntimeSettings", "stash.appscode.dev/apimachinery/apis/stash/v1alpha1.RetentionPolicy", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupConfigurationTemplateSpec", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupHooks", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.RetryConfig"}, + "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "kmodules.xyz/client-go/api/v1.ObjectReference", "kmodules.xyz/offshoot-api/api/v1.RuntimeSettings", "stash.appscode.dev/apimachinery/apis/stash/v1alpha1.RetentionPolicy", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupConfigurationTemplateSpec", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupHooks", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.retryConfig"}, } } @@ -23754,8 +23754,8 @@ func schema_apimachinery_apis_stash_v1beta1_BackupBlueprintSpec(ref common.Refer }, "retryConfig": { SchemaProps: spec.SchemaProps{ - Description: "RetryConfig specify a configuration for retry a backup if it fails. By default, Stash does not retry any failed backup.", - Ref: ref("stash.appscode.dev/apimachinery/apis/stash/v1beta1.RetryConfig"), + Description: "retryConfig specify a configuration for retry a backup if it fails. By default, Stash does not retry any failed backup.", + Ref: ref("stash.appscode.dev/apimachinery/apis/stash/v1beta1.retryConfig"), }, }, }, @@ -23763,7 +23763,7 @@ func schema_apimachinery_apis_stash_v1beta1_BackupBlueprintSpec(ref common.Refer }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "kmodules.xyz/objectstore-api/api/v1.Backend", "kmodules.xyz/offshoot-api/api/v1.PersistentVolumeClaim", "kmodules.xyz/offshoot-api/api/v1.RuntimeSettings", "stash.appscode.dev/apimachinery/apis/stash/v1alpha1.RetentionPolicy", "stash.appscode.dev/apimachinery/apis/stash/v1alpha1.UsagePolicy", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupHooks", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.EmptyDirSettings", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.RetryConfig", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.TaskRef"}, + "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "kmodules.xyz/objectstore-api/api/v1.Backend", "kmodules.xyz/offshoot-api/api/v1.PersistentVolumeClaim", "kmodules.xyz/offshoot-api/api/v1.RuntimeSettings", "stash.appscode.dev/apimachinery/apis/stash/v1alpha1.RetentionPolicy", "stash.appscode.dev/apimachinery/apis/stash/v1alpha1.UsagePolicy", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupHooks", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.EmptyDirSettings", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.retryConfig", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.TaskRef"}, } } @@ -23955,8 +23955,8 @@ func schema_apimachinery_apis_stash_v1beta1_BackupConfigurationSpec(ref common.R }, "retryConfig": { SchemaProps: spec.SchemaProps{ - Description: "RetryConfig specify a configuration for retry a backup if it fails. By default, Stash does not retry any failed backup.", - Ref: ref("stash.appscode.dev/apimachinery/apis/stash/v1beta1.RetryConfig"), + Description: "retryConfig specify a configuration for retry a backup if it fails. By default, Stash does not retry any failed backup.", + Ref: ref("stash.appscode.dev/apimachinery/apis/stash/v1beta1.retryConfig"), }, }, }, @@ -23964,7 +23964,7 @@ func schema_apimachinery_apis_stash_v1beta1_BackupConfigurationSpec(ref common.R }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "kmodules.xyz/client-go/api/v1.ObjectReference", "kmodules.xyz/offshoot-api/api/v1.PersistentVolumeClaim", "kmodules.xyz/offshoot-api/api/v1.RuntimeSettings", "stash.appscode.dev/apimachinery/apis/stash/v1alpha1.RetentionPolicy", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupHooks", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupTarget", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.EmptyDirSettings", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.RetryConfig", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.TaskRef"}, + "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "kmodules.xyz/client-go/api/v1.ObjectReference", "kmodules.xyz/offshoot-api/api/v1.PersistentVolumeClaim", "kmodules.xyz/offshoot-api/api/v1.RuntimeSettings", "stash.appscode.dev/apimachinery/apis/stash/v1alpha1.RetentionPolicy", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupHooks", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupTarget", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.EmptyDirSettings", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.retryConfig", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.TaskRef"}, } } diff --git a/pkg/restic/backup.go b/pkg/restic/backup.go index ae467551e..4b9561486 100644 --- a/pkg/restic/backup.go +++ b/pkg/restic/backup.go @@ -17,6 +17,7 @@ limitations under the License. package restic import ( + "context" "sync" "time" @@ -197,7 +198,9 @@ func (w *ResticWrapper) InitializeRepository() error { func (w *ResticWrapper) ApplyRetentionPolicies(retentionPolicy api_v1alpha1.RetentionPolicy) (*RepositoryStats, error) { // Cleanup old snapshots according to retention policy - out, err := w.cleanup(retentionPolicy, "") + out, err := w.RunWithRetry(context.Background(), func() ([]byte, error) { + return w.cleanup(retentionPolicy, "") + }) if err != nil { return nil, err } @@ -211,14 +214,18 @@ func (w *ResticWrapper) ApplyRetentionPolicies(retentionPolicy api_v1alpha1.Rete func (w *ResticWrapper) VerifyRepositoryIntegrity() (*RepositoryStats, error) { // Check repository integrity - out, err := w.check() + out, err := w.RunWithRetry(context.Background(), func() ([]byte, error) { + return w.check() + }) if err != nil { return nil, err } // Extract information from output of "check" command integrity := extractCheckInfo(out) // Read repository statics after cleanup - out, err = w.stats("") + out, err = w.RunWithRetry(context.Background(), func() ([]byte, error) { + return w.stats("") + }) if err != nil { return nil, err } diff --git a/pkg/restic/commands.go b/pkg/restic/commands.go index cb317a489..0d54e5fa5 100644 --- a/pkg/restic/commands.go +++ b/pkg/restic/commands.go @@ -491,10 +491,10 @@ func (w *ResticWrapper) run(commands ...Command) ([]byte, error) { } } out, err := w.sh.Output() + klog.Infoln("sh-output:", string(out)) if err != nil { - return nil, formatError(err, errBuff.String()) + return out, formatError(err, errBuff.String()) } - klog.Infoln("sh-output:", string(out)) return out, nil } diff --git a/pkg/restic/config.go b/pkg/restic/config.go index 78fc59aae..037b29e79 100644 --- a/pkg/restic/config.go +++ b/pkg/restic/config.go @@ -38,6 +38,7 @@ const ( type ResticWrapper struct { sh *shell.Session config SetupOptions + *RetryConfig } type Command struct { @@ -112,8 +113,9 @@ type KeyOptions struct { func NewResticWrapper(options SetupOptions) (*ResticWrapper, error) { wrapper := &ResticWrapper{ - sh: shell.NewSession(), - config: options, + sh: shell.NewSession(), + config: options, + RetryConfig: NewRetryConfig(), } err := wrapper.configure() diff --git a/pkg/restic/retry.go b/pkg/restic/retry.go new file mode 100644 index 000000000..f37592047 --- /dev/null +++ b/pkg/restic/retry.go @@ -0,0 +1,80 @@ +package restic + +import ( + "context" + "fmt" + "strings" + "time" + + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/klog/v2" +) + +const ( + maxRetries = 5 + delay = 10 * time.Second +) + +var retryablePatterns = []string{ + "Connection closed by foreign host", +} + +type RetryConfig struct { + MaxRetries int + Delay time.Duration + ShouldRetry func(error, string) bool +} + +func NewRetryConfig() *RetryConfig { + return &RetryConfig{ + MaxRetries: maxRetries, + Delay: delay, + ShouldRetry: func(err error, output string) bool { + if err == nil { + return false + } + combined := strings.ToLower(err.Error() + " " + output) + klog.Info("Combined output: " + combined) + for _, pattern := range retryablePatterns { + if strings.Contains(combined, strings.ToLower(pattern)) { + return true + } + } + return false + }, + } +} + +func (rc *RetryConfig) RunWithRetry(ctx context.Context, execFunc func() ([]byte, error)) ([]byte, error) { + var output []byte + var lastErr error + attempts := 0 + + err := wait.PollUntilContextCancel( + ctx, + rc.Delay, + true, // Run immediately on first call + func(ctx context.Context) (bool, error) { + // Stop if max retries reached + if attempts >= rc.MaxRetries { + return false, fmt.Errorf("max retries reached") + } + output, lastErr = execFunc() + if !rc.ShouldRetry(lastErr, string(output)) { + return true, nil + } + klog.Info("Retrying command after error", + "attempt", attempts, + "maxRetries", rc.MaxRetries, + "error", fmt.Sprintf("%s %s", lastErr, string(output))) + attempts++ + return false, nil + }, + ) + + if err != nil { + return nil, fmt.Errorf("failed after %d attempts: %w", attempts, lastErr) + } + + return output, lastErr +} From 15535edacbf87bd4d786044c2da6f5ad3b6d3f2f Mon Sep 17 00:00:00 2001 From: Anisur Rahman Date: Fri, 20 Feb 2026 12:50:00 +0600 Subject: [PATCH 2/2] fix ci Signed-off-by: Anisur Rahman --- apis/stash/v1beta1/openapi_generated.go | 20 ++++++++++---------- pkg/restic/retry.go | 21 ++++++++++++++++++--- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/apis/stash/v1beta1/openapi_generated.go b/apis/stash/v1beta1/openapi_generated.go index 93d4dac52..6e58a5ab3 100644 --- a/apis/stash/v1beta1/openapi_generated.go +++ b/apis/stash/v1beta1/openapi_generated.go @@ -486,7 +486,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "stash.appscode.dev/apimachinery/apis/stash/v1beta1.RestoreSessionStatus": schema_apimachinery_apis_stash_v1beta1_RestoreSessionStatus(ref), "stash.appscode.dev/apimachinery/apis/stash/v1beta1.RestoreTarget": schema_apimachinery_apis_stash_v1beta1_RestoreTarget(ref), "stash.appscode.dev/apimachinery/apis/stash/v1beta1.RestoreTargetSpec": schema_apimachinery_apis_stash_v1beta1_RestoreTargetSpec(ref), - "stash.appscode.dev/apimachinery/apis/stash/v1beta1.retryConfig": schema_apimachinery_apis_stash_v1beta1_RetryConfig(ref), + "stash.appscode.dev/apimachinery/apis/stash/v1beta1.RetryConfig": schema_apimachinery_apis_stash_v1beta1_RetryConfig(ref), "stash.appscode.dev/apimachinery/apis/stash/v1beta1.Rule": schema_apimachinery_apis_stash_v1beta1_Rule(ref), "stash.appscode.dev/apimachinery/apis/stash/v1beta1.SnapshotStats": schema_apimachinery_apis_stash_v1beta1_SnapshotStats(ref), "stash.appscode.dev/apimachinery/apis/stash/v1beta1.Summary": schema_apimachinery_apis_stash_v1beta1_Summary(ref), @@ -23496,8 +23496,8 @@ func schema_apimachinery_apis_stash_v1beta1_BackupBatchSpec(ref common.Reference }, "retryConfig": { SchemaProps: spec.SchemaProps{ - Description: "retryConfig specify a configuration for retry a backup if it fails. By default, Stash does not retry any failed backup.", - Ref: ref("stash.appscode.dev/apimachinery/apis/stash/v1beta1.retryConfig"), + Description: "RetryConfig specify a configuration for retry a backup if it fails. By default, Stash does not retry any failed backup.", + Ref: ref("stash.appscode.dev/apimachinery/apis/stash/v1beta1.RetryConfig"), }, }, }, @@ -23505,7 +23505,7 @@ func schema_apimachinery_apis_stash_v1beta1_BackupBatchSpec(ref common.Reference }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "kmodules.xyz/client-go/api/v1.ObjectReference", "kmodules.xyz/offshoot-api/api/v1.RuntimeSettings", "stash.appscode.dev/apimachinery/apis/stash/v1alpha1.RetentionPolicy", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupConfigurationTemplateSpec", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupHooks", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.retryConfig"}, + "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "kmodules.xyz/client-go/api/v1.ObjectReference", "kmodules.xyz/offshoot-api/api/v1.RuntimeSettings", "stash.appscode.dev/apimachinery/apis/stash/v1alpha1.RetentionPolicy", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupConfigurationTemplateSpec", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupHooks", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.RetryConfig"}, } } @@ -23754,8 +23754,8 @@ func schema_apimachinery_apis_stash_v1beta1_BackupBlueprintSpec(ref common.Refer }, "retryConfig": { SchemaProps: spec.SchemaProps{ - Description: "retryConfig specify a configuration for retry a backup if it fails. By default, Stash does not retry any failed backup.", - Ref: ref("stash.appscode.dev/apimachinery/apis/stash/v1beta1.retryConfig"), + Description: "RetryConfig specify a configuration for retry a backup if it fails. By default, Stash does not retry any failed backup.", + Ref: ref("stash.appscode.dev/apimachinery/apis/stash/v1beta1.RetryConfig"), }, }, }, @@ -23763,7 +23763,7 @@ func schema_apimachinery_apis_stash_v1beta1_BackupBlueprintSpec(ref common.Refer }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "kmodules.xyz/objectstore-api/api/v1.Backend", "kmodules.xyz/offshoot-api/api/v1.PersistentVolumeClaim", "kmodules.xyz/offshoot-api/api/v1.RuntimeSettings", "stash.appscode.dev/apimachinery/apis/stash/v1alpha1.RetentionPolicy", "stash.appscode.dev/apimachinery/apis/stash/v1alpha1.UsagePolicy", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupHooks", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.EmptyDirSettings", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.retryConfig", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.TaskRef"}, + "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "kmodules.xyz/objectstore-api/api/v1.Backend", "kmodules.xyz/offshoot-api/api/v1.PersistentVolumeClaim", "kmodules.xyz/offshoot-api/api/v1.RuntimeSettings", "stash.appscode.dev/apimachinery/apis/stash/v1alpha1.RetentionPolicy", "stash.appscode.dev/apimachinery/apis/stash/v1alpha1.UsagePolicy", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupHooks", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.EmptyDirSettings", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.RetryConfig", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.TaskRef"}, } } @@ -23955,8 +23955,8 @@ func schema_apimachinery_apis_stash_v1beta1_BackupConfigurationSpec(ref common.R }, "retryConfig": { SchemaProps: spec.SchemaProps{ - Description: "retryConfig specify a configuration for retry a backup if it fails. By default, Stash does not retry any failed backup.", - Ref: ref("stash.appscode.dev/apimachinery/apis/stash/v1beta1.retryConfig"), + Description: "RetryConfig specify a configuration for retry a backup if it fails. By default, Stash does not retry any failed backup.", + Ref: ref("stash.appscode.dev/apimachinery/apis/stash/v1beta1.RetryConfig"), }, }, }, @@ -23964,7 +23964,7 @@ func schema_apimachinery_apis_stash_v1beta1_BackupConfigurationSpec(ref common.R }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "kmodules.xyz/client-go/api/v1.ObjectReference", "kmodules.xyz/offshoot-api/api/v1.PersistentVolumeClaim", "kmodules.xyz/offshoot-api/api/v1.RuntimeSettings", "stash.appscode.dev/apimachinery/apis/stash/v1alpha1.RetentionPolicy", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupHooks", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupTarget", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.EmptyDirSettings", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.retryConfig", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.TaskRef"}, + "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "kmodules.xyz/client-go/api/v1.ObjectReference", "kmodules.xyz/offshoot-api/api/v1.PersistentVolumeClaim", "kmodules.xyz/offshoot-api/api/v1.RuntimeSettings", "stash.appscode.dev/apimachinery/apis/stash/v1alpha1.RetentionPolicy", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupHooks", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.BackupTarget", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.EmptyDirSettings", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.RetryConfig", "stash.appscode.dev/apimachinery/apis/stash/v1beta1.TaskRef"}, } } diff --git a/pkg/restic/retry.go b/pkg/restic/retry.go index f37592047..9b9e40f85 100644 --- a/pkg/restic/retry.go +++ b/pkg/restic/retry.go @@ -1,3 +1,19 @@ +/* +Copyright AppsCode Inc. and Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package restic import ( @@ -34,7 +50,7 @@ func NewRetryConfig() *RetryConfig { return false } combined := strings.ToLower(err.Error() + " " + output) - klog.Info("Combined output: " + combined) + klog.Infoln("Combined output: " + combined) for _, pattern := range retryablePatterns { if strings.Contains(combined, strings.ToLower(pattern)) { return true @@ -63,7 +79,7 @@ func (rc *RetryConfig) RunWithRetry(ctx context.Context, execFunc func() ([]byte if !rc.ShouldRetry(lastErr, string(output)) { return true, nil } - klog.Info("Retrying command after error", + klog.Infoln("Retrying command after error", "attempt", attempts, "maxRetries", rc.MaxRetries, "error", fmt.Sprintf("%s %s", lastErr, string(output))) @@ -71,7 +87,6 @@ func (rc *RetryConfig) RunWithRetry(ctx context.Context, execFunc func() ([]byte return false, nil }, ) - if err != nil { return nil, fmt.Errorf("failed after %d attempts: %w", attempts, lastErr) }