Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/backup/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ type Config struct {
SSHIdentityPassphrase string `split_words:"true"`
SSHRemotePath string `split_words:"true"`
SSHInsecureIgnoreHostKey bool `split_words:"true"`
SSHUploadRetries WholeNumber `split_words:"true" default:"0"`
SSHUploadRetryDelay time.Duration `split_words:"true" default:"5s"`
ExecLabel string `split_words:"true"`
ExecForwardOutput bool `split_words:"true"`
LockTimeout time.Duration `split_words:"true" default:"60m"`
Expand Down
48 changes: 48 additions & 0 deletions cmd/backup/config_ssh_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2026 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0

package main

import (
"testing"
"time"

"github.com/offen/envconfig"
)

func TestSSHUploadRetryConfiguration(t *testing.T) {
originalLookup := envconfig.Lookup
t.Cleanup(func() { envconfig.Lookup = originalLookup })
for _, test := range []struct {
name string
values map[string]string
retries int
delay time.Duration
invalid bool
}{
{name: "defaults", retries: 0, delay: 5 * time.Second},
{name: "disabled", values: map[string]string{"SSH_UPLOAD_RETRIES": "0"}, delay: 5 * time.Second},
{name: "custom", values: map[string]string{"SSH_UPLOAD_RETRIES": "4", "SSH_UPLOAD_RETRY_DELAY": "500ms"}, retries: 4, delay: 500 * time.Millisecond},
{name: "negative retries", values: map[string]string{"SSH_UPLOAD_RETRIES": "-1"}, invalid: true},
{name: "invalid delay", values: map[string]string{"SSH_UPLOAD_RETRY_DELAY": "invalid"}, invalid: true},
} {
t.Run(test.name, func(t *testing.T) {
config, err := loadConfig(func(key string) (string, bool) {
value, ok := test.values[key]
return value, ok
})
if test.invalid {
if err == nil {
t.Fatal("Expected invalid retry configuration to be rejected")
}
return
}
if err != nil {
t.Fatal(err)
}
if int(config.SSHUploadRetries) != test.retries || config.SSHUploadRetryDelay != test.delay {
t.Errorf("Got retries=%d, delay=%s; want %d, %s", config.SSHUploadRetries, config.SSHUploadRetryDelay, test.retries, test.delay)
}
})
}
}
2 changes: 2 additions & 0 deletions cmd/backup/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ func (s *script) init() error {
InsecureIgnoreHostKey: s.c.SSHInsecureIgnoreHostKey,
HostPublicKey: s.c.SSHHostPublicKey,
KnownHostsFile: s.c.SSHKnownHostsFile,
UploadRetries: int(s.c.SSHUploadRetries),
UploadRetryDelay: s.c.SSHUploadRetryDelay,
}

sshBackend, closeSSHConnection, err := ssh.NewStorageBackend(sshConfig, logFunc)
Expand Down
19 changes: 19 additions & 0 deletions docs/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,25 @@ If you need to confirm what the container actually loaded, see [Show loaded conf

# SSH_IGNORE_HOST_KEY="false"

# ---

# The number of additional upload attempts after an SSH/SFTP connection error.
# Each retry opens a new connection and transfers the existing archive from the
# beginning; containers are not stopped again and the archive is not recreated.
# Permission errors and other non-connection errors are not retried.
# Retries are disabled by default. Set to a positive number to enable them.
# Initial connection failures during backup setup and pruning are not retried.
# Exhausted retries still fail the backup run; this setting does not retain
# temporary archives after a failed run.

# SSH_UPLOAD_RETRIES="0"

# ---

# The delay before each upload retry, as a non-negative duration.

# SSH_UPLOAD_RETRY_DELAY="5s"

########### AZURE BLOB STORAGE

# The credential's account name when using Azure Blob Storage. This has to be
Expand Down
Loading
Loading