Compare commits

...

50 Commits

Author SHA1 Message Date
Frederik Ring
8a853a5b03 Update existing tests and test actual pruning 2025-02-06 17:40:45 +01:00
Frederik Ring
378217e517 Passing a full duration as retention period is more flexible 2025-02-06 15:33:08 +01:00
Frederik Ring
23756074f9 Publishing actions have been deprecated 2025-02-04 16:37:25 +01:00
Frederik Ring
40b12b9d07 Configuration reference on age encryption is lacking 2025-02-04 16:34:10 +01:00
dependabot[bot]
e628f09122 Bump github.com/docker/cli (#526) 2025-01-28 06:21:38 +00:00
dependabot[bot]
7340e00dab Bump github.com/Azure/azure-sdk-for-go/sdk/storage/azblob (#527) 2025-01-28 06:02:39 +00:00
Frederik Ring
958585336a Funding info for GitHub is redundant 2025-01-23 13:53:26 +01:00
dependabot[bot]
68b7e4d678 Bump github.com/minio/minio-go/v7 from 7.0.83 to 7.0.84 (#524) 2025-01-21 06:01:34 +00:00
dependabot[bot]
857e4fc605 Bump github.com/Azure/azure-sdk-for-go/sdk/azidentity (#523) 2025-01-21 06:01:11 +00:00
dependabot[bot]
8d26194809 Bump github.com/docker/cli (#522) 2025-01-14 12:09:03 +00:00
Frederik Ring
3063288d1e Main README should refer to configuration reference instead 2025-01-09 18:17:53 +01:00
dependabot[bot]
02fdfb363c Bump alpine from 3.20 to 3.21 (#502) 2025-01-07 15:00:54 +00:00
dependabot[bot]
2ee23a9384 Bump golang.org/x/oauth2 from 0.24.0 to 0.25.0 (#517) 2025-01-07 06:06:22 +00:00
dependabot[bot]
16be0c0217 Bump github.com/otiai10/copy from 1.14.0 to 1.14.1 (#518) 2025-01-07 05:47:03 +00:00
Frederik Ring
4799795f0a MariaDB image stopped providing mysqldump shim, use plain name 2025-01-06 20:07:35 +01:00
Frederik Ring
49b8d2f8d8 Error cases when parsing commands into argv are not handled sufficiently (#506) 2025-01-06 19:55:15 +01:00
Frederik Ring
e4beef200a Update minio client to v7.0.83 (#516) 2025-01-06 19:54:54 +01:00
dependabot[bot]
e75ab8bdd8 Bump github.com/studio-b12/gowebdav from 0.9.0 to 0.10.0 (#512) 2024-12-24 06:31:56 +00:00
dependabot[bot]
a4145352f9 Bump github.com/docker/cli (#511) 2024-12-24 06:27:44 +00:00
dependabot[bot]
615256cda9 Bump filippo.io/age from 1.2.0 to 1.2.1 (#510) 2024-12-18 18:43:47 +00:00
dependabot[bot]
40c4f11d70 Bump github.com/minio/minio-go/v7 from 7.0.81 to 7.0.82 (#508) 2024-12-16 21:21:07 +00:00
dependabot[bot]
2685571c58 Bump golang.org/x/sync from 0.9.0 to 0.10.0 (#500) 2024-12-09 21:35:36 +00:00
dependabot[bot]
04ad0777e0 Bump github.com/docker/cli (#501) 2024-12-09 21:35:31 +00:00
dependabot[bot]
50e41eac02 Bump github.com/minio/minio-go/v7 from 7.0.80 to 7.0.81 (#499) 2024-12-03 06:20:34 +00:00
dependabot[bot]
94e59a102e Bump github.com/Azure/azure-sdk-for-go/sdk/storage/azblob from 1.4.1 to 1.5.0 (#495)
* Bump github.com/Azure/azure-sdk-for-go/sdk/storage/azblob

Bumps [github.com/Azure/azure-sdk-for-go/sdk/storage/azblob](https://github.com/Azure/azure-sdk-for-go) from 1.4.1 to 1.5.0.
- [Release notes](https://github.com/Azure/azure-sdk-for-go/releases)
- [Changelog](https://github.com/Azure/azure-sdk-for-go/blob/main/documentation/release.md)
- [Commits](https://github.com/Azure/azure-sdk-for-go/compare/sdk/storage/azblob/v1.4.1...sdk/azcore/v1.5.0)

---
updated-dependencies:
- dependency-name: github.com/Azure/azure-sdk-for-go/sdk/storage/azblob
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update azurite image to match updated client capabilities

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Frederik Ring <frederik.ring@gmail.com>
2024-11-19 13:27:36 +01:00
dependabot[bot]
964a5e0342 Bump golang.org/x/oauth2 from 0.23.0 to 0.24.0 (#494) 2024-11-12 06:20:46 +00:00
dependabot[bot]
2363c3c9cb Bump golang.org/x/sync from 0.8.0 to 0.9.0 (#493) 2024-11-12 06:02:14 +00:00
dependabot[bot]
6bc66db833 Bump github.com/minio/minio-go/v7 from 7.0.79 to 7.0.80 (#491) 2024-11-04 21:39:31 +00:00
dependabot[bot]
de40eae4de Bump github.com/minio/minio-go/v7 from 7.0.78 to 7.0.79 (#488) 2024-10-28 21:53:31 +00:00
dependabot[bot]
731421e359 Bump github.com/pkg/sftp from 1.13.6 to 1.13.7 (#489) 2024-10-28 21:33:27 +00:00
dependabot[bot]
d46918b13a Bump rexml from 3.3.6 to 3.3.9 in /docs (#487) 2024-10-28 20:41:16 +00:00
dependabot[bot]
2fb63059b3 Bump mvdan.cc/sh/v3 from 3.9.0 to 3.10.0 (#485) 2024-10-22 04:23:31 +00:00
dependabot[bot]
e0fcbd27e5 Bump github.com/Azure/azure-sdk-for-go/sdk/azidentity (#483) 2024-10-15 04:45:35 +00:00
dependabot[bot]
f4884bf190 Bump github.com/minio/minio-go/v7 from 7.0.77 to 7.0.78 (#482) 2024-10-15 04:26:31 +00:00
dependabot[bot]
52787a1e42 Bump github.com/minio/minio-go/v7 from 7.0.76 to 7.0.77 (#479) 2024-10-01 04:43:46 +00:00
dependabot[bot]
6e08ae7c39 Bump github.com/Azure/azure-sdk-for-go/sdk/storage/azblob (#477) 2024-09-24 05:40:41 +00:00
dependabot[bot]
0183db831b Bump github.com/klauspost/compress from 1.17.9 to 1.17.10 (#476) 2024-09-24 05:22:26 +00:00
dependabot[bot]
f481fda848 Bump github.com/docker/cli (#475) 2024-09-24 05:22:06 +00:00
shalafi99
f4cf4173e6 Documentation update for DROPBOX_REMOTE_PATH environment variable (#473)
* Update set-up-dropbox.md

Updated text for explanation of DROPBOX_REMOTE_PATH environment variable usage accordingly to how the Dropbox App was scoped during creation.

* Update set-up-dropbox.md

Minor style edit

* Update index.md

Replaced /Apps/my-test-app/some_subdir with /somedir in example value for DROPBOX_REMOTE_PATH environment variable under "Backing up to Dropbox" section, to align with updates done to related "how-to" text
2024-09-18 20:43:54 +02:00
dependabot[bot]
681983608f Bump github.com/docker/cli (#470) 2024-09-10 05:35:58 +00:00
dependabot[bot]
45335ffb67 Bump golang.org/x/oauth2 from 0.22.0 to 0.23.0 (#469) 2024-09-10 05:35:34 +00:00
dependabot[bot]
01a595607d Bump github.com/docker/cli (#467) 2024-09-03 05:00:46 +00:00
dependabot[bot]
119391e8df Bump github.com/minio/minio-go/v7 from 7.0.75 to 7.0.76 (#464) 2024-08-27 05:27:41 +00:00
dependabot[bot]
dd5f7f5b66 Bump rexml from 3.3.3 to 3.3.6 in /docs (#463) 2024-08-23 05:23:04 +00:00
dependabot[bot]
c54a5bef5f Update to use Go 1.23 (#462)
* Bump golang from 1.22-alpine to 1.23-alpine

Bumps golang from 1.22-alpine to 1.23-alpine.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* Use Go 1.23 throughout

* Update golangci-lint

* Fix complaint raised by linter

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Frederik Ring <frederik.ring@gmail.com>
2024-08-20 11:17:49 +02:00
dependabot[bot]
8fac9608ff Bump github.com/docker/cli (#461) 2024-08-20 04:53:30 +00:00
dependabot[bot]
3ee40b6422 Bump mvdan.cc/sh/v3 from 3.8.0 to 3.9.0 (#460) 2024-08-20 04:53:06 +00:00
Frederik Ring
8b5c9a494f Exclude legacy docs page from navigation 2024-08-19 22:50:26 +02:00
nick comer
44ad3bbda2 feat: allow backups to be encrypted with age (#432)
GPG is known to have usability issues and is generally cumbersome to
use. age [0] is a modern alternative to GPG that is designed by a
cryptographer that has worked and continues to work on Golang's crypto
packages for years.

Allowing age to be used to encrypt backups dramatically simplifies the
backup process.

[0]: https://age-encryption.org/
2024-08-19 22:49:49 +02:00
dependabot[bot]
74e065cbb9 Bump github.com/minio/minio-go/v7 from 7.0.74 to 7.0.75 (#458) 2024-08-13 04:05:47 +00:00
51 changed files with 564 additions and 207 deletions

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
github: offen
patreon: offen

View File

@@ -39,7 +39,7 @@ jobs:
env:
JEKYLL_ENV: production
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
uses: actions/upload-pages-artifact@v3
with:
path: 'docs/_site/'
@@ -52,4 +52,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
uses: actions/deploy-pages@v4

View File

@@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
go-version: '1.23'
cache: false
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
@@ -26,7 +26,7 @@ jobs:
# Require: The version of golangci-lint to use.
# When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version.
# When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit.
version: v1.57
version: v1.60
# Optional: working directory, useful for monorepos
# working-directory: somedir

View File

@@ -14,7 +14,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.22.x'
go-version: '1.23.x'
- name: Install dependencies
run: go mod download
- name: Test with the Go CLI

View File

@@ -5,4 +5,5 @@ linters:
- staticcheck
- govet
output:
format: github-actions
formats:
- format: colored-line-number

View File

@@ -1,7 +1,7 @@
# Copyright 2022 - offen.software <hioffen@posteo.de>
# SPDX-License-Identifier: MPL-2.0
FROM golang:1.22-alpine as builder
FROM golang:1.23-alpine as builder
WORKDIR /app
COPY . .
@@ -9,7 +9,7 @@ RUN go mod download
WORKDIR /app/cmd/backup
RUN go build -o backup .
FROM alpine:3.20
FROM alpine:3.21
WORKDIR /root

View File

@@ -76,7 +76,7 @@ docker run --rm \
offen/docker-volume-backup:v2
```
Alternatively, pass a `--env-file` in order to use a full config as described below.
Alternatively, pass a `--env-file` in order to use a full config as described [in the docs](https://offen.github.io/docker-volume-backup/reference/).
---

View File

@@ -38,6 +38,7 @@ type Config struct {
BackupArchive string `split_words:"true" default:"/archive"`
BackupCronExpression string `split_words:"true" default:"@daily"`
BackupRetentionDays int32 `split_words:"true" default:"-1"`
BackupRetentionPeriod time.Duration `split_words:"true"`
BackupPruningLeeway time.Duration `split_words:"true" default:"1m"`
BackupPruningPrefix string `split_words:"true"`
BackupStopContainerLabel string `split_words:"true"`
@@ -48,6 +49,8 @@ type Config struct {
BackupSkipBackendsFromPrune []string `split_words:"true"`
GpgPassphrase string `split_words:"true"`
GpgPublicKeyRing string `split_words:"true"`
AgePassphrase string `split_words:"true"`
AgePublicKeys []string `split_words:"true"`
NotificationURLs []string `envconfig:"NOTIFICATION_URLS"`
NotificationLevel string `split_words:"true" default:"error"`
EmailNotificationRecipient string `split_words:"true"`

View File

@@ -11,109 +11,191 @@ import (
"os"
"path"
"filippo.io/age"
"github.com/ProtonMail/go-crypto/openpgp/armor"
openpgp "github.com/ProtonMail/go-crypto/openpgp/v2"
"github.com/offen/docker-volume-backup/internal/errwrap"
)
func (s *script) encryptAsymmetrically(outFile *os.File) (io.WriteCloser, func() error, error) {
entityList, err := openpgp.ReadArmoredKeyRing(bytes.NewReader([]byte(s.c.GpgPublicKeyRing)))
if err != nil {
return nil, nil, errwrap.Wrap(err, "error parsing armored keyring")
func countTrue(b ...bool) int {
c := int(0)
for _, v := range b {
if v {
c++
}
armoredWriter, err := armor.Encode(outFile, "PGP MESSAGE", nil)
if err != nil {
return nil, nil, errwrap.Wrap(err, "error preparing encryption")
}
_, name := path.Split(s.file)
dst, err := openpgp.Encrypt(armoredWriter, entityList, nil, nil, &openpgp.FileHints{
FileName: name,
}, nil)
if err != nil {
return nil, nil, err
}
return dst, func() error {
if err := dst.Close(); err != nil {
return err
}
return armoredWriter.Close()
}, err
}
func (s *script) encryptSymmetrically(outFile *os.File) (io.WriteCloser, func() error, error) {
_, name := path.Split(s.file)
dst, err := openpgp.SymmetricallyEncrypt(outFile, []byte(s.c.GpgPassphrase), &openpgp.FileHints{
FileName: name,
}, nil)
if err != nil {
return nil, nil, err
}
return dst, dst.Close, nil
return c
}
// encryptArchive encrypts the backup file using PGP and the configured passphrase or publickey(s).
// In case no passphrase or publickey is given it returns early, leaving the backup file
// untouched.
func (s *script) encryptArchive() error {
var encrypt func(outFile *os.File) (io.WriteCloser, func() error, error)
var cleanUpErr error
switch {
case s.c.GpgPassphrase != "" && s.c.GpgPublicKeyRing != "":
return errwrap.Wrap(nil, "error in selecting asymmetric and symmetric encryption methods: conflicting env vars are set")
case s.c.GpgPassphrase != "":
encrypt = s.encryptSymmetrically
case s.c.GpgPublicKeyRing != "":
encrypt = s.encryptAsymmetrically
useGPGSymmetric := s.c.GpgPassphrase != ""
useGPGAsymmetric := s.c.GpgPublicKeyRing != ""
useAgeSymmetric := s.c.AgePassphrase != ""
useAgeAsymmetric := len(s.c.AgePublicKeys) > 0
switch nconfigured := countTrue(
useGPGSymmetric,
useGPGAsymmetric,
useAgeSymmetric,
useAgeAsymmetric,
); nconfigured {
case 0:
return nil
case 1:
// ok!
default:
return fmt.Errorf(
"error in selecting archive encryption method: expected 0 or 1 to be configured, %d methods are configured",
nconfigured,
)
}
if useGPGSymmetric {
return s.encryptWithGPGSymmetric()
} else if useGPGAsymmetric {
return s.encryptWithGPGAsymmetric()
} else if useAgeSymmetric || useAgeAsymmetric {
ar, err := s.getConfiguredAgeRecipients()
if err != nil {
return errwrap.Wrap(err, "failed to get configured age recipients")
}
return s.encryptWithAge(ar)
}
return nil
}
gpgFile := fmt.Sprintf("%s.gpg", s.file)
func (s *script) getConfiguredAgeRecipients() ([]age.Recipient, error) {
if s.c.AgePassphrase == "" && len(s.c.AgePublicKeys) == 0 {
return nil, fmt.Errorf("no age recipients configured")
}
recipients := []age.Recipient{}
if len(s.c.AgePublicKeys) > 0 {
for _, pk := range s.c.AgePublicKeys {
pkr, err := age.ParseX25519Recipient(pk)
if err != nil {
return nil, errwrap.Wrap(err, "failed to parse age public key")
}
recipients = append(recipients, pkr)
}
}
if s.c.AgePassphrase != "" {
if len(recipients) != 0 {
return nil, fmt.Errorf("age encryption must only be enabled via passphrase or public key, not both")
}
r, err := age.NewScryptRecipient(s.c.AgePassphrase)
if err != nil {
return nil, errwrap.Wrap(err, "failed to create scrypt identity from age passphrase")
}
recipients = append(recipients, r)
}
return recipients, nil
}
func (s *script) encryptWithAge(rec []age.Recipient) error {
return s.doEncrypt("age", func(ciphertextWriter io.Writer) (io.WriteCloser, error) {
return age.Encrypt(ciphertextWriter, rec...)
})
}
func (s *script) encryptWithGPGSymmetric() error {
return s.doEncrypt("gpg", func(ciphertextWriter io.Writer) (io.WriteCloser, error) {
_, name := path.Split(s.file)
return openpgp.SymmetricallyEncrypt(ciphertextWriter, []byte(s.c.GpgPassphrase), &openpgp.FileHints{
FileName: name,
}, nil)
})
}
type closeAllWriter struct {
io.Writer
closers []io.Closer
}
func (c *closeAllWriter) Close() (err error) {
for _, cl := range c.closers {
err = errors.Join(err, cl.Close())
}
return
}
var _ io.WriteCloser = (*closeAllWriter)(nil)
func (s *script) encryptWithGPGAsymmetric() error {
return s.doEncrypt("gpg", func(ciphertextWriter io.Writer) (_ io.WriteCloser, outerr error) {
entityList, err := openpgp.ReadArmoredKeyRing(bytes.NewReader([]byte(s.c.GpgPublicKeyRing)))
if err != nil {
return nil, errwrap.Wrap(err, "error parsing armored keyring")
}
armoredWriter, err := armor.Encode(ciphertextWriter, "PGP MESSAGE", nil)
if err != nil {
return nil, errwrap.Wrap(err, "error preparing encryption")
}
defer func() {
if outerr != nil {
_ = armoredWriter.Close()
}
}()
_, name := path.Split(s.file)
encWriter, err := openpgp.Encrypt(armoredWriter, entityList, nil, nil, &openpgp.FileHints{
FileName: name,
}, nil)
if err != nil {
return nil, err
}
return &closeAllWriter{
Writer: encWriter,
closers: []io.Closer{encWriter, armoredWriter},
}, nil
})
}
func (s *script) doEncrypt(
extension string,
encryptor func(ciphertextWriter io.Writer) (io.WriteCloser, error),
) (outerr error) {
encFile := fmt.Sprintf("%s.%s", s.file, extension)
s.registerHook(hookLevelPlumbing, func(error) error {
if err := remove(gpgFile); err != nil {
return errwrap.Wrap(err, "error removing gpg file")
if err := remove(encFile); err != nil {
return errwrap.Wrap(err, "error removing encrypted file")
}
s.logger.Info(
fmt.Sprintf("Removed GPG file `%s`.", gpgFile),
fmt.Sprintf("Removed encrypted file `%s`.", encFile),
)
return nil
})
outFile, err := os.Create(gpgFile)
outFile, err := os.Create(encFile)
if err != nil {
return errwrap.Wrap(err, "error opening out file")
}
defer func() {
if err := outFile.Close(); err != nil {
cleanUpErr = errors.Join(cleanUpErr, errwrap.Wrap(err, "error closing out file"))
outerr = errors.Join(outerr, errwrap.Wrap(err, "error closing out file"))
}
}()
dst, dstCloseCallback, err := encrypt(outFile)
dst, err := encryptor(outFile)
if err != nil {
return errwrap.Wrap(err, "error encrypting backup file")
}
defer func() {
if err := dstCloseCallback(); err != nil {
cleanUpErr = errors.Join(cleanUpErr, errwrap.Wrap(err, "error closing encrypted backup file"))
if err := dst.Close(); err != nil {
outerr = errors.Join(outerr, errwrap.Wrap(err, "error closing encrypted backup file"))
}
}()
src, err := os.Open(s.file)
if err != nil {
return errwrap.Wrap(err, fmt.Sprintf("error opening backup file `%s`", s.file))
return errwrap.Wrap(err, fmt.Sprintf("error opening backup file %q", s.file))
}
defer func() {
if err := src.Close(); err != nil {
cleanUpErr = errors.Join(cleanUpErr, errwrap.Wrap(err, "error closing backup file"))
outerr = errors.Join(outerr, errwrap.Wrap(err, "error closing backup file"))
}
}()
@@ -121,9 +203,10 @@ func (s *script) encryptArchive() error {
return errwrap.Wrap(err, "error writing ciphertext to file")
}
s.file = gpgFile
s.file = encFile
s.logger.Info(
fmt.Sprintf("Encrypted backup using gpg, saving as `%s`.", s.file),
fmt.Sprintf("Encrypted backup using %q, saving as %q", extension, s.file),
)
return cleanUpErr
return
}

View File

@@ -24,10 +24,18 @@ import (
)
func (s *script) exec(containerRef string, command string, user string) ([]byte, []byte, error) {
args, _ := argv.Argv(command, nil, nil)
args, err := argv.Argv(command, nil, nil)
if err != nil {
return nil, nil, errwrap.Wrap(err, fmt.Sprintf("error parsing argv from '%s'", command))
}
if len(args) == 0 {
return nil, nil, errwrap.Wrap(nil, "received unexpected empty command")
}
commandEnv := []string{
fmt.Sprintf("COMMAND_RUNTIME_ARCHIVE_FILEPATH=%s", s.file),
}
execID, err := s.cli.ContainerExecCreate(context.Background(), containerRef, container.ExecOptions{
Cmd: args[0],
AttachStdin: true,

View File

@@ -17,11 +17,18 @@ import (
// the given configuration. In case the given configuration would delete all
// backups, it does nothing instead and logs a warning.
func (s *script) pruneBackups() error {
if s.c.BackupRetentionDays < 0 {
if s.c.BackupRetentionDays < 0 && s.c.BackupRetentionPeriod == 0 {
return nil
}
deadline := time.Now().AddDate(0, 0, -int(s.c.BackupRetentionDays)).Add(s.c.BackupPruningLeeway)
var deadline time.Time
if s.c.BackupRetentionPeriod != 0 {
deadline = time.Now().Add(-s.c.BackupRetentionPeriod)
} else {
s.logger.Warn("Using BACKUP_RETENTION_DAYS has been deprecated and will be removed in the next major version. Please use BACKUP_RETENTION_PERIOD instead.")
deadline = time.Now().AddDate(0, 0, -int(s.c.BackupRetentionDays))
}
deadline = deadline.Add(s.c.BackupPruningLeeway)
eg := errgroup.Group{}
for _, backend := range s.storages {

View File

@@ -225,6 +225,10 @@ func (s *script) init() error {
s.storages = append(s.storages, dropboxBackend)
}
if s.c.BackupRetentionDays > -1 && s.c.BackupRetentionPeriod > 0 {
return errwrap.Wrap(nil, "both BACKUP_RETENTION_DAYS and BACKUP_RETENTION_PERIOD were configured, which are mutually exclusive")
}
if s.c.EmailNotificationRecipient != "" {
emailURL := fmt.Sprintf(
"smtp://%s:%s@%s:%d/?from=%s&to=%s",

View File

@@ -59,13 +59,11 @@ GEM
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
rexml (3.3.3)
strscan
rexml (3.3.9)
rouge (3.30.0)
safe_yaml (1.0.5)
sassc (2.4.0)
ffi (~> 1.9)
strscan (3.1.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
unicode-display_width (2.4.2)

View File

@@ -7,7 +7,8 @@ nav_order: 3
# Automatically prune old backups
When `BACKUP_RETENTION_DAYS` is configured, the command will check if there are any archives in the remote storage backend(s) or local archive that are older than the given retention value and rotate these backups away.
When `BACKUP_RETENTION_PERIOD` is configured, the command will check if there are any archives in the remote storage backend(s) or local archive that are older than the given retention value and rotate these backups away.
The value is a duration as per Go's [`time.ParseDuration`][duration].
{: .note }
Be aware that this mechanism looks at __all files in the target bucket or archive__, which means that other files that are older than the given deadline are deleted as well.
@@ -23,7 +24,7 @@ services:
environment:
BACKUP_FILENAME: backup-%Y-%m-%dT%H-%M-%S.tar.gz
BACKUP_PRUNING_PREFIX: backup-
BACKUP_RETENTION_DAYS: '7'
BACKUP_RETENTION_PERIOD: '168h'
volumes:
- ${HOME}/backups:/archive
- data:/backup/my-app-backup:ro
@@ -32,3 +33,5 @@ services:
volumes:
data:
```
[duration]: https://pkg.go.dev/time#ParseDuration

View File

@@ -3,15 +3,7 @@ title: Encrypt backups using GPG
layout: default
parent: How Tos
nav_order: 7
nav_exclude: true
---
# Encrypt backups using GPG
The image supports encrypting backups using GPG out of the box.
In case a `GPG_PASSPHRASE` or `GPG_PUBLIC_KEY_RING` environment variable is set, the backup archive will be encrypted using the given key and saved as a `.gpg` file instead.
Assuming you have `gpg` installed, you can decrypt such a backup using (your OS will prompt for the passphrase before decryption can happen):
```console
gpg -o backup.tar.gz -d backup.tar.gz.gpg
```
See: [Encrypt Backups](encrypt-backups)

View File

@@ -0,0 +1,32 @@
---
title: Encrypting backups
layout: default
parent: How Tos
nav_order: 7
---
# Encrypting backups
The image supports encrypting backups using one of two available methods: **GPG** or **[age](https://age-encryption.org/)**
## Using GPG encryption
In case a `GPG_PASSPHRASE` or `GPG_PUBLIC_KEY_RING` environment variable is set, the backup archive will be encrypted using the given key and saved as a `.gpg` file instead.
Assuming you have `gpg` installed, you can decrypt such a backup using (your OS will prompt for the passphrase before decryption can happen):
```console
gpg -o backup.tar.gz -d backup.tar.gz.gpg
```
## Using age encryption
{: .note }
Even though the `age` CLI tools supports encryption using SSH keys, this is not supported by this tool.
`AGE_PUBLIC_KEYS` currently expects `age` keys to be given.
age allows backups to be encrypted with either a symmetric key (password) or a public key. One of those options are available for use.
Given `AGE_PASSPHRASE` being provided, the backup archive will be encrypted with the passphrase and saved as a `.age` file instead. Refer to age documentation for how to properly decrypt.
Given `AGE_PUBLIC_KEYS` being provided (allowing multiple by separating each public key with `,`), the backup archive will be encrypted with the provided public keys. It will also result in the archive being saved as a `.age` file.

View File

@@ -33,5 +33,7 @@ Note: Using the "Generated access token" in the app console is not supported, as
## Other parameters
Important: If you chose `App folder` access during the creation of your Dropbox app in step 1 above, you can only write in the app's directory!
This means, that `DROPBOX_REMOTE_PATH` must start with e.g. `/Apps/YOUR_APP_NAME` or `/Apps/YOUR_APP_NAME/some_sub_dir`
Important: If you chose `App folder` access during the creation of your Dropbox app in step 1 above, `DROPBOX_REMOTE_PATH` will be a relative path under the App folder!
(_For example, DROPBOX_REMOTE_PATH=/somedir means the backup file will be uploaded to /Apps/myapp/somedir_)
On the other hand if you chose `Full Dropbox` access, the value for `DROPBOX_REMOTE_PATH` will represent an absolute path inside your Dropbox storage area.
(_Still considering the same example above, the backup file will be uploaded to /somedir in your Dropbox root_)

View File

@@ -190,7 +190,7 @@ services:
DROPBOX_REFRESH_TOKEN: REFRESH_KEY # replace
DROPBOX_APP_KEY: APP_KEY # replace
DROPBOX_APP_SECRET: APP_SECRET # replace
DROPBOX_REMOTE_PATH: /Apps/my-test-app/some_subdir # replace
DROPBOX_REMOTE_PATH: /somedir # replace
volumes:
- data:/backup/my-app-backup:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
@@ -280,7 +280,7 @@ services:
AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
BACKUP_FILENAME: backup-%Y-%m-%dT%H-%M-%S.tar.gz
BACKUP_PRUNING_PREFIX: backup-
BACKUP_RETENTION_DAYS: 7
BACKUP_RETENTION_PERIOD: 168h
volumes:
- data:/backup/my-app-backup:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
@@ -338,7 +338,7 @@ volumes:
data:
```
## Using mysqldump to prepare the backup
## Using mariadb-dump/mysqldump to prepare the backup
```yml
version: '3'
@@ -347,7 +347,7 @@ services:
database:
image: mariadb:latest
labels:
- docker-volume-backup.archive-pre=/bin/sh -c 'mysqldump -psecret --all-databases > /tmp/dumps/dump.sql'
- docker-volume-backup.archive-pre=/bin/sh -c 'mariadb-dump -psecret --all-databases > /tmp/dumps/dump.sql'
volumes:
- data:/tmp/dumps
backup:

View File

@@ -312,9 +312,10 @@ You can populate below template according to your requirements and use it as you
# removal to certain files.
# Define this value to enable automatic rotation of old backups. The value
# declares the number of days for which a backup is kept.
# declares the duration for which a backup is kept. It is formatted as per
# https://pkg.go.dev/time#ParseDuration, e.g. 1 day turns into `24h`
# BACKUP_RETENTION_DAYS="7"
# BACKUP_RETENTION_PERIOD="168h"
# In case the duration a backup takes fluctuates noticeably in your setup
# you can adjust this setting to make sure there are no race conditions
@@ -337,6 +338,9 @@ You can populate below template according to your requirements and use it as you
########### BACKUP ENCRYPTION
# All of the encryption options are mutually exclusive. Provide a single option
# for the encryption scheme of your choice.
# Backups can be encrypted symmetrically using gpg in case a passphrase is given.
# GPG_PASSPHRASE="<xxx>"
@@ -350,6 +354,16 @@ You can populate below template according to your requirements and use it as you
#...
#-----END PGP PUBLIC KEY BLOCK-----
# Backups can be encrypted symmetrically using age in case a passphrase is given.
# AGE_PASSPHRASE="<xxx>"
# Backups can be encrypted asymmetrically using age in case publickeys are given.
# Multiple keys need to be provided as a comma separated list. Right now, this only
# support passing age keys, with no support for ssh keys.
# AGE_PUBLIC_KEYS="<xxx>"
########### STOPPING CONTAINERS AND SERVICES DURING BACKUP
# Containers or services can be stopped by applying a

44
go.mod
View File

@@ -1,28 +1,29 @@
module github.com/offen/docker-volume-backup
go 1.22
go 1.23
require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.0
filippo.io/age v1.2.1
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0
github.com/containrrr/shoutrrr v0.8.0
github.com/cosiner/argv v0.1.0
github.com/docker/cli v27.1.1+incompatible
github.com/docker/cli v27.5.1+incompatible
github.com/docker/docker v27.1.1+incompatible
github.com/gofrs/flock v0.12.1
github.com/joho/godotenv v1.5.1
github.com/klauspost/compress v1.17.9
github.com/klauspost/compress v1.17.11
github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d
github.com/minio/minio-go/v7 v7.0.74
github.com/minio/minio-go/v7 v7.0.84
github.com/offen/envconfig v1.5.0
github.com/otiai10/copy v1.14.0
github.com/pkg/sftp v1.13.6
github.com/otiai10/copy v1.14.1
github.com/pkg/sftp v1.13.7
github.com/robfig/cron/v3 v3.0.1
github.com/studio-b12/gowebdav v0.9.0
golang.org/x/crypto v0.25.0
golang.org/x/oauth2 v0.22.0
golang.org/x/sync v0.8.0
mvdan.cc/sh/v3 v3.8.0
github.com/studio-b12/gowebdav v0.10.0
golang.org/x/crypto v0.32.0
golang.org/x/oauth2 v0.25.0
golang.org/x/sync v0.10.0
mvdan.cc/sh/v3 v3.10.0
)
require (
@@ -34,10 +35,11 @@ require (
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/goccy/go-json v0.10.4 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/otiai10/mint v1.6.3 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect
go.opentelemetry.io/otel v1.26.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 // indirect
@@ -50,9 +52,9 @@ require (
)
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/ProtonMail/go-crypto v1.1.0-alpha.1
github.com/docker/go-connections v0.4.0 // indirect
@@ -62,7 +64,7 @@ require (
github.com/fatih/color v1.17.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/klauspost/pgzip v1.2.6
github.com/kr/fs v0.1.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
@@ -75,10 +77,10 @@ require (
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
gotest.tools/v3 v3.0.3 // indirect
)

138
go.sum
View File

@@ -1,3 +1,5 @@
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0=
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -31,20 +33,26 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0 h1:GJHeeA2N7xrG3q30L2UXDyuWRzDM900/65j70wcM4Ww=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o=
filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 h1:1mvYtZfWQAnwNah/C+Z+Jb9rQH95LPE2vlmMuWAHJk8=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1/go.mod h1:75I/mXtme1JyWFtz8GocPHVFyH421IBoZErnO16dd0k=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1 h1:Bk5uOhSAenHyR5P61D/NzeQCv+4fEVV8mOkJ82NqpWw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1/go.mod h1:QZ4pw3or1WPmRBxf0cHd1tknzrT54WPBOQoGutCPvSU=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.0 h1:Be6KInmFEKV81c0pOAEbRYehLMwmmGI1exuFj248AMk=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.0/go.mod h1:WCPBHsOXfBVnivScjs2ypRfimjEW0qPVLGgJkZlrIOA=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
@@ -54,6 +62,8 @@ github.com/ProtonMail/go-crypto v1.1.0-alpha.1/go.mod h1:rA3QumHc/FZ8pAHreoekgiA
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -70,10 +80,12 @@ github.com/cosiner/argv v0.1.0/go.mod h1:EusR6TucWKX+zFgtdUsKT2Cvg45K5rtpCcWz4hK
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE=
github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v27.5.1+incompatible h1:JB9cieUT9YNiMITtIsguaN55PLOHhBSz3LKVc6cqWaY=
github.com/docker/cli v27.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
@@ -92,8 +104,6 @@ github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -104,10 +114,12 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -178,13 +190,15 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -208,8 +222,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.74 h1:fTo/XlPBTSpo3BAMshlwKL5RspXRv9us5UeHEGYCFe0=
github.com/minio/minio-go/v7 v7.0.74/go.mod h1:qydcVzV8Hqtj1VtEocfxbmVFa2siu6HGa+LDEPogjD8=
github.com/minio/minio-go/v7 v7.0.84 h1:D1HVmAF8JF8Bpi6IU4V9vIEj+8pc+xU88EWMs2yed0E=
github.com/minio/minio-go/v7 v7.0.84/go.mod h1:57YXpvc5l3rjPdhqNrDsvVlY0qPI6UTk1bflAe+9doY=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd h1:aY7OQNf2XqY/JQ6qREWamhI/81os/agb2BAGpcx5yWI=
@@ -226,27 +240,29 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec=
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/pkg/sftp v1.13.7 h1:uv+I3nNJvlKZIQGSr8JVQLNHFU9YhhNpvC14Y6KgmSM=
github.com/pkg/sftp v1.13.7/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
@@ -260,10 +276,10 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU=
github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/studio-b12/gowebdav v0.10.0 h1:Yewz8FFiadcGEu4hxS/AAJQlHelndqln1bns3hcJIYc=
github.com/studio-b12/gowebdav v0.10.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -296,9 +312,9 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -330,6 +346,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -359,17 +376,18 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -380,8 +398,9 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -420,22 +439,28 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -485,8 +510,9 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -591,8 +617,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
mvdan.cc/sh/v3 v3.8.0 h1:ZxuJipLZwr/HLbASonmXtcvvC9HXY9d2lXZHnKGjFc8=
mvdan.cc/sh/v3 v3.8.0/go.mod h1:w04623xkgBVo7/IUK89E0g8hBykgEpN0vgOj3RJr6MY=
mvdan.cc/sh/v3 v3.10.0 h1:v9z7N1DLZ7owyLM/SXZQkBSXcwr2IGMm2LY2pmhVXj4=
mvdan.cc/sh/v3 v3.10.0/go.mod h1:z/mSSVyLFGZzqb3ZIKojjyqIx/xbmz/UHdCSv9HmqXY=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@@ -21,7 +21,7 @@ func Wrap(err error, msg string) error {
chunks := strings.Split(frame.Function, "/")
withCaller := fmt.Sprintf("%s: %s", chunks[len(chunks)-1], msg)
if err == nil {
return fmt.Errorf(withCaller)
return errors.New(withCaller)
}
return fmt.Errorf("%s: %w", withCaller, err)
}

View File

@@ -1,8 +1,10 @@
FROM docker:27-dind
RUN apk add \
age \
coreutils \
curl \
expect \
gpg \
gpg-agent \
jq \

View File

@@ -0,0 +1,24 @@
services:
backup:
image: offen/docker-volume-backup:${TEST_VERSION:-canary}
restart: always
environment:
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
BACKUP_FILENAME: test.tar.gz
BACKUP_LATEST_SYMLINK: test-latest.tar.gz.age
BACKUP_RETENTION_PERIOD: ${BACKUP_RETENTION_PERIOD:-168h}
AGE_PASSPHRASE: "Dance.0Tonight.Go.Typical"
volumes:
- ${LOCAL_DIR:-./local}:/archive
- app_data:/backup/app_data:ro
- /var/run/docker.sock:/var/run/docker.sock
offen:
image: offen/offen:latest
labels:
- docker-volume-backup.stop-during-backup=true
volumes:
- app_data:/var/opt/offen
volumes:
app_data:

39
test/age-passphrase/run.sh Executable file
View File

@@ -0,0 +1,39 @@
#!/bin/sh
set -e
cd "$(dirname "$0")"
. ../util.sh
current_test=$(basename "$(pwd)")
export LOCAL_DIR="$(mktemp -d)"
docker compose up -d --quiet-pull
sleep 5
docker compose exec backup backup
expect_running_containers "2"
TMP_DIR=$(mktemp -d)
# complex usage of expect(1) due to age not have a way to programmatically
# provide the passphrase
expect -i <<EOL
spawn age --decrypt -o "$LOCAL_DIR/decrypted.tar.gz" "$LOCAL_DIR/test.tar.gz.age"
expect -exact "Enter passphrase: "
send -- "Dance.0Tonight.Go.Typical\r"
sleep 1
EOL
tar -xf "$LOCAL_DIR/decrypted.tar.gz" -C "$TMP_DIR"
if [ ! -f "$TMP_DIR/backup/app_data/offen.db" ]; then
fail "Could not find expected file in untared archive."
fi
rm -vf "$LOCAL_DIR/decrypted.tar.gz"
pass "Found relevant files in decrypted and untared local backup."
if [ ! -L "$LOCAL_DIR/test-latest.tar.gz.age" ]; then
fail "Could not find local symlink to latest encrypted backup."
fi

1
test/age-publickey/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
pk-*.txt

View File

@@ -0,0 +1,24 @@
services:
backup:
image: offen/docker-volume-backup:${TEST_VERSION:-canary}
restart: always
environment:
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
BACKUP_FILENAME: test.tar.gz
BACKUP_LATEST_SYMLINK: test-latest.tar.gz.age
BACKUP_RETENTION_PERIOD: ${BACKUP_RETENTION_PERIOD:-168h}
AGE_PUBLIC_KEYS: "${BACKUP_AGE_PUBLIC_KEYS}"
volumes:
- ${LOCAL_DIR:-./local}:/archive
- app_data:/backup/app_data:ro
- /var/run/docker.sock:/var/run/docker.sock
offen:
image: offen/offen:latest
labels:
- docker-volume-backup.stop-during-backup=true
volumes:
- app_data:/var/opt/offen
volumes:
app_data:

43
test/age-publickey/run.sh Executable file
View File

@@ -0,0 +1,43 @@
#!/bin/sh
set -e
cd "$(dirname "$0")"
. ../util.sh
current_test=$(basename "$(pwd)")
export LOCAL_DIR="$(mktemp -d)"
age-keygen >"$LOCAL_DIR/pk-a.txt"
PK_A="$(grep -E 'public key' <"$LOCAL_DIR/pk-a.txt" | cut -d: -f2 | xargs)"
age-keygen >"$LOCAL_DIR/pk-b.txt"
PK_B="$(grep -E 'public key' <"$LOCAL_DIR/pk-b.txt" | cut -d: -f2 | xargs)"
export BACKUP_AGE_PUBLIC_KEYS="$PK_A,$PK_B"
docker compose up -d --quiet-pull
sleep 5
docker compose exec backup backup
expect_running_containers "2"
do_decrypt() {
TMP_DIR=$(mktemp -d)
age --decrypt -i "$1" -o "$LOCAL_DIR/decrypted.tar.gz" "$LOCAL_DIR/test.tar.gz.age"
tar -xf "$LOCAL_DIR/decrypted.tar.gz" -C "$TMP_DIR"
if [ ! -f "$TMP_DIR/backup/app_data/offen.db" ]; then
fail "Could not find expected file in untared archive."
fi
rm -vf "$LOCAL_DIR/decrypted.tar.gz"
pass "Found relevant files in decrypted and untared local backup."
if [ ! -L "$LOCAL_DIR/test-latest.tar.gz.age" ]; then
fail "Could not find local symlink to latest encrypted backup."
fi
}
do_decrypt "$LOCAL_DIR/pk-a.txt"
do_decrypt "$LOCAL_DIR/pk-b.txt"

View File

@@ -1,6 +1,6 @@
services:
storage:
image: mcr.microsoft.com/azure-storage/azurite:3.31.0
image: mcr.microsoft.com/azure-storage/azurite:3.33.0
volumes:
- ${DATA_DIR:-./data}:/data
command: azurite-blob --blobHost 0.0.0.0 --blobPort 10000 --location /data
@@ -37,7 +37,7 @@ services:
AZURE_STORAGE_ACCESS_TIER: Hot
BACKUP_FILENAME: test.tar.gz
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7}
BACKUP_RETENTION_PERIOD: ${BACKUP_RETENTION_PERIOD:-168h}
BACKUP_PRUNING_LEEWAY: 5s
BACKUP_PRUNING_PREFIX: test
volumes:

View File

@@ -38,7 +38,7 @@ rm "$LOCAL_DIR/test.tar.gz"
# The second part of this test checks if backups get deleted when the retention
# is set to 0 days (which it should not as it would mean all backups get deleted)
BACKUP_RETENTION_DAYS="0" docker compose up -d
BACKUP_RETENTION_PERIOD="1s" docker compose up -d
sleep 5
docker compose exec backup backup
@@ -52,7 +52,7 @@ pass "Remote backups have not been deleted."
# The third part of this test checks if old backups get deleted when the retention
# is set to 7 days (which it should)
BACKUP_RETENTION_DAYS="7" docker compose up -d
BACKUP_RETENTION_PERIOD="168h" docker compose up -d
sleep 5
info "Create first backup with no prune"

View File

@@ -26,7 +26,7 @@ services:
AWS_ENDPOINT_CA_CERT: /root/minio-rootCA.crt
AWS_S3_BUCKET_NAME: backup
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7}
BACKUP_RETENTION_PERIOD: ${BACKUP_RETENTION_PERIOD:-168h}
BACKUP_PRUNING_LEEWAY: 5s
volumes:
- app_data:/backup/app_data:ro

View File

@@ -30,7 +30,7 @@ services:
BACKUP_FILENAME_EXPAND: 'true'
BACKUP_FILENAME: test-$$HOSTNAME.tar.gz
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7}
BACKUP_RETENTION_PERIOD: ${BACKUP_RETENTION_PERIOD:-168h}
BACKUP_PRUNING_LEEWAY: 5s
BACKUP_PRUNING_PREFIX: test
DROPBOX_ENDPOINT: http://openapi_mock:8080

View File

@@ -29,7 +29,7 @@ fi
# The second part of this test checks if backups get deleted when the retention
# is set to 0 days (which it should not as it would mean all backups get deleted)
BACKUP_RETENTION_DAYS="0" docker compose up -d
BACKUP_RETENTION_PERIOD="1s" docker compose up -d
sleep 5
logs=$(docker compose exec -T backup backup)
@@ -43,7 +43,7 @@ fi
# The third part of this test checks if old backups get deleted when the retention
# is set to 7 days (which it should)
BACKUP_RETENTION_DAYS="7" docker compose up -d
BACKUP_RETENTION_PERIOD="168h" docker compose up -d
sleep 5
info "Create second backup and prune"

View File

@@ -6,7 +6,7 @@ services:
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
BACKUP_FILENAME: test.tar.gz
BACKUP_LATEST_SYMLINK: test-latest.tar.gz.gpg
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7}
BACKUP_RETENTION_PERIOD: ${BACKUP_RETENTION_PERIOD:-168h}
GPG_PUBLIC_KEY_RING_FILE: /keys/public_key.asc
volumes:
- ${KEY_DIR:-.}/public_key.asc:/keys/public_key.asc

View File

@@ -6,7 +6,7 @@ services:
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
BACKUP_FILENAME: test.tar.gz
BACKUP_LATEST_SYMLINK: test-latest.tar.gz.gpg
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7}
BACKUP_RETENTION_PERIOD: ${BACKUP_RETENTION_PERIOD:-168h}
GPG_PASSPHRASE: 1234#$$ecret
volumes:
- ${LOCAL_DIR:-./local}:/archive

View File

@@ -8,7 +8,7 @@ services:
BACKUP_FILENAME: test-$$HOSTNAME.tar.gz
BACKUP_LATEST_SYMLINK: test-$$HOSTNAME.latest.tar.gz.gpg
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7}
BACKUP_RETENTION_PERIOD: ${BACKUP_RETENTION_PERIOD:-168h}
BACKUP_PRUNING_LEEWAY: 5s
BACKUP_PRUNING_PREFIX: test
volumes:

View File

@@ -41,7 +41,7 @@ pass "Found symlink to latest version in local backup."
# The second part of this test checks if backups get deleted when the retention
# is set to 0 days (which it should not as it would mean all backups get deleted)
BACKUP_RETENTION_DAYS="0" docker compose up -d
BACKUP_RETENTION_PERIOD="1s" docker compose up -d
sleep 5
docker compose exec backup backup
@@ -54,7 +54,7 @@ pass "Local backups have not been deleted."
# The third part of this test checks if old backups get deleted when the retention
# is set to 7 days (which it should)
BACKUP_RETENTION_DAYS="7" docker compose up -d
BACKUP_RETENTION_PERIOD="168h" docker compose up -d
sleep 5
info "Create first backup with no prune"

View File

@@ -4,7 +4,7 @@ services:
restart: always
environment:
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
BACKUP_RETENTION_DAYS: '7'
BACKUP_RETENTION_PERIOD: 168h
volumes:
- app_data:/backup/app_data:ro
- /var/run/docker.sock:/var/run/docker.sock

View File

@@ -13,7 +13,7 @@ sleep 5
ec=0
docker compose exec -e BACKUP_RETENTION_DAYS=7 -e BACKUP_FILENAME=test.tar.gz backup backup & \
docker compose exec -e BACKUP_RETENTION_PERIOD=168h -e BACKUP_FILENAME=test.tar.gz backup backup & \
{ set +e; sleep 0.1; docker compose exec -e BACKUP_FILENAME=test2.tar.gz -e LOCK_TIMEOUT=1s backup backup; ec=$?;}
if [ "$ec" = "0" ]; then

View File

@@ -25,7 +25,7 @@ services:
BACKUP_FILENAME_EXPAND: 'true'
BACKUP_FILENAME: test-$$HOSTNAME.tar.gz
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
BACKUP_RETENTION_DAYS: 7
BACKUP_RETENTION_PERIOD: 168h
BACKUP_PRUNING_LEEWAY: 5s
BACKUP_PRUNING_PREFIX: test
BACKUP_LATEST_SYMLINK: test-$$HOSTNAME.latest.tar.gz

View File

@@ -0,0 +1,22 @@
services:
backup:
image: offen/docker-volume-backup:${TEST_VERSION:-canary}
restart: always
environment:
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
BACKUP_RETENTION_PERIOD: 15s
BACKUP_PRUNING_LEEWAY: 1s
volumes:
- app_data:/backup/app_data:ro
- /var/run/docker.sock:/var/run/docker.sock
- ${LOCAL_DIR:-./local}:/archive
offen:
image: offen/offen:latest
labels:
- docker-volume-backup.stop-during-backup=true
volumes:
- app_data:/var/opt/offen
volumes:
app_data:

28
test/retention/run.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/bin/sh
set -e
cd "$(dirname "$0")"
. ../util.sh
current_test=$(basename $(pwd))
export LOCAL_DIR=$(mktemp -d)
docker compose up -d --quiet-pull
sleep 5
docker compose exec backup backup
sleep 20
if [ $(ls -1 $LOCAL_DIR | wc -l) != "1" ]; then
fail "Unexpected number of backups after initial run"
fi
pass "Found 1 backup files."
docker compose exec backup backup
if [ $(ls -1 $LOCAL_DIR | wc -l) != "1" ]; then
fail "Unexpected number of backups after initial run"
fi
pass "Found 1 backup files."

View File

@@ -25,7 +25,7 @@ services:
BACKUP_FILENAME_EXPAND: 'true'
BACKUP_FILENAME: test-$$HOSTNAME.tar.gz
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7}
BACKUP_RETENTION_PERIOD: ${BACKUP_RETENTION_PERIOD:-168h}
BACKUP_PRUNING_LEEWAY: 5s
BACKUP_PRUNING_PREFIX: test
volumes:

View File

@@ -22,9 +22,11 @@ docker run --rm \
pass "Found relevant files in untared remote backups."
sleep 5
# The second part of this test checks if backups get deleted when the retention
# is set to 0 days (which it should not as it would mean all backups get deleted)
BACKUP_RETENTION_DAYS="0" docker compose up -d
BACKUP_RETENTION_PERIOD="5s" docker compose up -d
sleep 5
docker compose exec backup backup
@@ -39,7 +41,7 @@ pass "Remote backups have not been deleted."
# The third part of this test checks if old backups get deleted when the retention
# is set to 7 days (which it should)
BACKUP_RETENTION_DAYS="7" docker compose up -d
BACKUP_RETENTION_PERIOD="168h" docker compose up -d
sleep 5
info "Create first backup with no prune"

View File

@@ -31,7 +31,7 @@ services:
AWS_S3_BUCKET_NAME: backup
BACKUP_FILENAME: test.tar.gz
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
BACKUP_RETENTION_DAYS: 7
BACKUP_RETENTION_PERIOD: 168h
BACKUP_PRUNING_LEEWAY: 5s
volumes:
- pg_data:/backup/pg_data:ro

View File

@@ -25,7 +25,7 @@ services:
AWS_S3_BUCKET_NAME: backup
BACKUP_FILENAME: test.tar.gz
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
BACKUP_RETENTION_DAYS: 7
BACKUP_RETENTION_PERIOD: 168h
BACKUP_PRUNING_LEEWAY: 5s
volumes:
- pg_data:/backup/pg_data:ro

View File

@@ -19,7 +19,7 @@ services:
BACKUP_FILENAME_EXPAND: 'true'
BACKUP_FILENAME: test-$$HOSTNAME.tar.gz
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7}
BACKUP_RETENTION_PERIOD: ${BACKUP_RETENTION_PERIOD:-168h}
BACKUP_PRUNING_LEEWAY: 5s
BACKUP_PRUNING_PREFIX: test
SSH_HOST_NAME: ssh

View File

@@ -28,7 +28,7 @@ pass "Found relevant files in decrypted and untared remote backups."
# The second part of this test checks if backups get deleted when the retention
# is set to 0 days (which it should not as it would mean all backups get deleted)
BACKUP_RETENTION_DAYS="0" docker compose up -d
BACKUP_RETENTION_PERIOD="1s" docker compose up -d
sleep 5
docker compose exec backup backup
@@ -43,7 +43,7 @@ pass "Remote backups have not been deleted."
# The third part of this test checks if old backups get deleted when the retention
# is set to 7 days (which it should)
BACKUP_RETENTION_DAYS="7" docker compose up -d
BACKUP_RETENTION_PERIOD="168h" docker compose up -d
sleep 5
info "Create first backup with no prune"

View File

@@ -31,7 +31,7 @@ services:
AWS_S3_BUCKET_NAME: backup
BACKUP_FILENAME: test.tar.gz
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
BACKUP_RETENTION_DAYS: 7
BACKUP_RETENTION_PERIOD: 168h
BACKUP_PRUNING_LEEWAY: 5s
volumes:
- pg_data:/backup/pg_data:ro

View File

@@ -18,7 +18,7 @@ services:
BACKUP_FILENAME_EXPAND: 'true'
BACKUP_FILENAME: test-$$HOSTNAME.tar.gz
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7}
BACKUP_RETENTION_PERIOD: ${BACKUP_RETENTION_PERIOD:-168h}
BACKUP_PRUNING_LEEWAY: 5s
BACKUP_PRUNING_PREFIX: test
WEBDAV_URL: http://webdav/

View File

@@ -24,7 +24,7 @@ pass "Found relevant files in untared remote backup."
# The second part of this test checks if backups get deleted when the retention
# is set to 0 days (which it should not as it would mean all backups get deleted)
BACKUP_RETENTION_DAYS="0" docker compose up -d
BACKUP_RETENTION_PERIOD="1s" docker compose up -d
sleep 5
docker compose exec backup backup
@@ -39,7 +39,7 @@ pass "Remote backups have not been deleted."
# The third part of this test checks if old backups get deleted when the retention
# is set to 7 days (which it should)
BACKUP_RETENTION_DAYS="7" docker compose up -d
BACKUP_RETENTION_PERIOD="168h" docker compose up -d
sleep 5
info "Create first backup with no prune"