Compare commits

...

52 Commits

Author SHA1 Message Date
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
Lennart
8a64da4b0b Feature: PGP Asymmetric Encryption (#456)
* feat: asym encryption

* tests

* docs

* refactor

* logs & errs

* comment

* Update docs/reference/index.md

use correct env var in example

Co-authored-by: Frederik Ring <frederik.ring@gmail.com>

* Update cmd/backup/encrypt_archive.go

use errwarp for initial error msg

Co-authored-by: Frederik Ring <frederik.ring@gmail.com>

* rm orphaned code in encryption functions

* inline readArmoredKeys

* naming -GPG_PUBLIC_KEYS- to GPG_PUBLIC_KEY_RING

* add eror handling for closing func

* use dynamically generated keys for testing

* rm explicit gpg-agent start

* rm unnecessary private_key export

* pass PASSPHRASE correctly to the decryption command

* capture defer errors

* log & err msg

---------

Co-authored-by: Frederik Ring <frederik.ring@gmail.com>
2024-08-11 10:11:23 +02:00
J. Zebedee
f97ce11734 Add "none" compression type (#457)
* Add "none" compression type

* Add "none" compression to docs

* Use passThroughWriteCloser for "none" compression

* Add test for none compression

---------

Co-authored-by: Frederik Ring <frederik.ring@gmail.com>
2024-08-11 10:11:09 +02:00
Frederik Ring
336e12f874 Add support for Azure storage access tiers (#452) 2024-08-09 15:37:27 +02:00
dependabot[bot]
016c6c8307 Bump golang.org/x/sync from 0.7.0 to 0.8.0 (#454) 2024-08-06 03:33:32 +00:00
dependabot[bot]
e22f317fbb Bump golang.org/x/oauth2 from 0.21.0 to 0.22.0 (#453) 2024-08-06 03:15:49 +00:00
dependabot[bot]
e04bd2f066 Bump rexml from 3.2.8 to 3.3.3 in /docs (#451) 2024-08-02 06:35:44 +00:00
Frederik Ring
c4eeaad813 Bump shoutrrr to version 0.8 (#450) 2024-07-31 21:58:24 +02:00
Frederik Ring
5840f1c5dc Update docker/docker package (#449) 2024-07-31 21:54:25 +02:00
dependabot[bot]
d71b7304c2 Bump github.com/docker/cli (#448) 2024-07-30 05:14:03 +00:00
Frederik Ring
fbc7f85d9f version key in compose file is deprecated (#445) 2024-07-23 20:47:45 +02:00
dependabot[bot]
2af5bdf4d9 Bump github.com/Azure/azure-sdk-for-go/sdk/storage/azblob from 1.2.1 to 1.4.0 (#442) 2024-07-23 18:33:13 +00:00
dependabot[bot]
631ca3e07d Bump github.com/gofrs/flock from 0.12.0 to 0.12.1 (#444) 2024-07-22 21:57:20 +00:00
dependabot[bot]
3d35d7c00e Bump github.com/minio/minio-go/v7 from 7.0.73 to 7.0.74 (#443) 2024-07-22 21:39:21 +00:00
dependabot[bot]
954bde73fb Bump github.com/docker/cli (#441) 2024-07-22 21:39:11 +00:00
dependabot[bot]
ab46e96706 Bump github.com/gofrs/flock from 0.11.0 to 0.12.0 (#439) 2024-07-09 04:21:31 +00:00
dependabot[bot]
ab4ce94534 Bump github.com/minio/minio-go/v7 from 7.0.72 to 7.0.73 (#440) 2024-07-09 04:04:26 +00:00
dependabot[bot]
e4170addb6 Bump github.com/docker/cli from 26.1.4+incompatible to 27.0.3+incompatible (#437)
* Bump github.com/docker/cli

Bumps [github.com/docker/cli](https://github.com/docker/cli) from 26.1.4+incompatible to 27.0.3+incompatible.
- [Commits](https://github.com/docker/cli/compare/v26.1.4...v27.0.3)

---
updated-dependencies:
- dependency-name: github.com/docker/cli
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

* Update all Docker versions to 27

* Swap deprecated methods

---------

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-07-02 21:15:49 +02:00
dependabot[bot]
b8410bbdc5 Bump github.com/gofrs/flock from 0.8.1 to 0.11.0 (#438) 2024-07-02 05:21:06 +00:00
dependabot[bot]
24e1341589 Bump github.com/Azure/azure-sdk-for-go/sdk/azidentity (#435) 2024-06-25 03:41:41 +00:00
dependabot[bot]
3d0286472b Bump github.com/minio/minio-go/v7 from 7.0.71 to 7.0.72 (#434) 2024-06-25 03:41:21 +00:00
dependabot[bot]
bb11ae035b Bump github.com/klauspost/compress from 1.17.8 to 1.17.9 (#431) 2024-06-18 05:09:55 +00:00
dependabot[bot]
9209037ed9 Bump github.com/Azure/azure-sdk-for-go/sdk/azidentity (#430) 2024-06-11 20:45:47 +00:00
dependabot[bot]
2e73dea4f7 Bump github.com/docker/cli (#428) 2024-06-11 05:57:25 +00:00
dependabot[bot]
7dc3ae17e7 Bump github.com/minio/minio-go/v7 from 7.0.70 to 7.0.71 (#427) 2024-06-11 05:57:06 +00:00
dependabot[bot]
9d5ea718a0 Bump golang.org/x/oauth2 from 0.20.0 to 0.21.0 (#426) 2024-06-11 05:56:37 +00:00
dependabot[bot]
272495ae7d Bump alpine from 3.19 to 3.20 (#424) 2024-05-28 04:45:31 +00:00
Frederik Ring
8beb28d4f8 Documentation should mention label visibility restrictions 2024-05-26 10:28:46 +02:00
dependabot[bot]
0ec2e68076 --- (#421) 2024-05-21 04:51:09 +00:00
dependabot[bot]
b85afa6008 Bump rexml from 3.2.6 to 3.2.8 in /docs (#420) 2024-05-17 11:04:07 +00:00
guangwu
4cb47a4818 fix: close backup file (#419) 2024-05-16 09:35:28 +02:00
dependabot[bot]
9b5ba8958d Bump github.com/docker/cli (#418) 2024-05-14 04:37:36 +00:00
dependabot[bot]
0327701e2d Bump golang.org/x/oauth2 from 0.19.0 to 0.20.0 (#416) 2024-05-07 04:13:27 +00:00
dependabot[bot]
58f26ba004 Bump github.com/docker/cli (#415) 2024-05-07 04:12:59 +00:00
dependabot[bot]
f62ef6e05a Bump github.com/minio/minio-go/v7 from 7.0.69 to 7.0.70 (#414) 2024-04-30 11:08:13 +00:00
Frederik Ring
40924434e4 Use up to date version of golangci-lint (#413) 2024-04-26 17:48:00 +02:00
Frederik Ring
e613f6046f Fix issues raised by linter 2024-04-26 17:10:06 +02:00
dependabot[bot]
292d47eb19 Bump github.com/docker/cli from 24.0.9+incompatible to 26.1.0+incompatible (#411)
* Bump github.com/docker/cli

Bumps [github.com/docker/cli](https://github.com/docker/cli) from 24.0.9+incompatible to 26.1.0+incompatible.
- [Commits](https://github.com/docker/cli/compare/v24.0.9...v26.1.0)

---
updated-dependencies:
- dependency-name: github.com/docker/cli
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

* Upgrade docker/docker to matching version

* Tidy go.mod

---------

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-04-25 17:33:07 +02:00
dependabot[bot]
7637975e3f Bump golang.org/x/net from 0.22.0 to 0.23.0 (#410) 2024-04-19 13:37:08 +00:00
dependabot[bot]
c47a14c53a Bump github.com/Azure/azure-sdk-for-go/sdk/azidentity (#408) 2024-04-16 11:21:42 +00:00
dependabot[bot]
9f795761d6 Bump github.com/klauspost/compress from 1.17.7 to 1.17.8 (#409) 2024-04-16 11:21:21 +00:00
Frederik Ring
f2ef48803c Print stack trace when encountering unexpected panic (#406) 2024-04-15 15:12:10 +02:00
Frederik Ring
8b69566291 Result of query for services is used before handling possible error (#405)
* Result of query for services is used before handling possible error

* Return early when a non-replicated service is matched
2024-04-15 15:08:37 +02:00
Frederik Ring
bf79c913e0 Update test suite image to use Docker 26 (#404) 2024-04-15 13:09:43 +02:00
dependabot[bot]
2f7193aa9b Bump golang.org/x/oauth2 from 0.18.0 to 0.19.0 (#402) 2024-04-09 07:04:13 +00:00
dependabot[bot]
550c4f520f Bump golang.org/x/sync from 0.6.0 to 0.7.0 (#401) 2024-04-09 04:46:24 +00:00
Frederik Ring
1af472077c Update author reference in license statements (#393) 2024-03-15 11:42:22 +01:00
dependabot[bot]
a077f12c11 Bump google.golang.org/protobuf from 1.31.0 to 1.33.0 (#392) 2024-03-14 06:06:22 +00:00
dependabot[bot]
cb5a38a1b7 Bump github.com/minio/minio-go/v7 from 7.0.68 to 7.0.69 (#390) 2024-03-11 22:13:04 +00:00
dependabot[bot]
b8995dbc51 Bump golang.org/x/oauth2 from 0.17.0 to 0.18.0 (#389) 2024-03-11 22:11:37 +00:00
74 changed files with 787 additions and 973 deletions

View File

@@ -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.54
version: v1.57
# Optional: working directory, useful for monorepos
# working-directory: somedir

View File

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

View File

@@ -77,3 +77,8 @@ docker run --rm \
```
Alternatively, pass a `--env-file` in order to use a full config as described below.
---
Copyright &copy; 2024 <a target="_blank" href="https://www.offen.software">offen.software</a> and contributors.
Distributed under the <a href="https://github.com/offen/docker-volume-backup/tree/main/LICENSE">MPL-2.0 License</a>.

View File

@@ -1,4 +1,4 @@
// Copyright 2022 - Offen Authors <hioffen@posteo.de>
// Copyright 2022 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
// Portions of this file are taken from package `targz`, Copyright (c) 2014 Fredrik Wallgren
@@ -22,8 +22,7 @@ import (
)
func createArchive(files []string, inputFilePath, outputFilePath string, compression string, compressionConcurrency int) error {
inputFilePath = stripTrailingSlashes(inputFilePath)
inputFilePath, outputFilePath, err := makeAbsolute(inputFilePath, outputFilePath)
_, outputFilePath, err := makeAbsolute(stripTrailingSlashes(inputFilePath), outputFilePath)
if err != nil {
return errwrap.Wrap(err, "error transposing given file paths")
}
@@ -31,7 +30,7 @@ func createArchive(files []string, inputFilePath, outputFilePath string, compres
return errwrap.Wrap(err, "error creating output file path")
}
if err := compress(files, outputFilePath, filepath.Dir(inputFilePath), compression, compressionConcurrency); err != nil {
if err := compress(files, outputFilePath, compression, compressionConcurrency); err != nil {
return errwrap.Wrap(err, "error creating archive")
}
@@ -55,7 +54,7 @@ func makeAbsolute(inputFilePath, outputFilePath string) (string, string, error)
return inputFilePath, outputFilePath, err
}
func compress(paths []string, outFilePath, subPath string, algo string, concurrency int) error {
func compress(paths []string, outFilePath, algo string, concurrency int) error {
file, err := os.Create(outFilePath)
if err != nil {
return errwrap.Wrap(err, "error creating out file")
@@ -94,6 +93,8 @@ func compress(paths []string, outFilePath, subPath string, algo string, concurre
func getCompressionWriter(file *os.File, algo string, concurrency int) (io.WriteCloser, error) {
switch algo {
case "none":
return &passThroughWriteCloser{file}, nil
case "gz":
w, err := pgzip.NewWriterLevel(file, 5)
if err != nil {
@@ -166,3 +167,15 @@ func writeTarball(path string, tarWriter *tar.Writer, prefix string) error {
return nil
}
type passThroughWriteCloser struct {
target io.WriteCloser
}
func (p *passThroughWriteCloser) Write(b []byte) (int, error) {
return p.target.Write(b)
}
func (p *passThroughWriteCloser) Close() error {
return nil
}

View File

@@ -1,4 +1,4 @@
// Copyright 2024 - Offen Authors <hioffen@posteo.de>
// Copyright 2024 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package main
@@ -131,12 +131,8 @@ func (c *command) schedule(strategy configStrategy) error {
c.logger.Warn(
fmt.Sprintf("Scheduled cron expression %s will never run, is this intentional?", config.BackupCronExpression),
)
if err != nil {
return errwrap.Wrap(err, "error scheduling")
}
c.schedules = append(c.schedules, id)
}
c.schedules = append(c.schedules, id)
}
return nil

View File

@@ -1,4 +1,4 @@
// Copyright 2022 - Offen Authors <hioffen@posteo.de>
// Copyright 2022 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package main
@@ -47,6 +47,9 @@ type Config struct {
BackupExcludeRegexp RegexpDecoder `split_words:"true"`
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"`
@@ -76,6 +79,7 @@ type Config struct {
AzureStorageContainerName string `split_words:"true"`
AzureStoragePath string `split_words:"true"`
AzureStorageEndpoint string `split_words:"true" default:"https://{{ .AccountName }}.blob.core.windows.net/"`
AzureStorageAccessTier string `split_words:"true"`
DropboxEndpoint string `split_words:"true" default:"https://api.dropbox.com/"`
DropboxOAuth2Endpoint string `envconfig:"DROPBOX_OAUTH2_ENDPOINT" default:"https://api.dropbox.com/"`
DropboxRefreshToken string `split_words:"true"`
@@ -91,7 +95,7 @@ type CompressionType string
func (c *CompressionType) Decode(v string) error {
switch v {
case "gz", "zst":
case "none", "gz", "zst":
*c = CompressionType(v)
return nil
default:

View File

@@ -1,4 +1,4 @@
// Copyright 2024 - Offen Authors <hioffen@posteo.de>
// Copyright 2024 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package main

View File

@@ -1,4 +1,4 @@
// Copyright 2024 - Offen Authors <hioffen@posteo.de>
// Copyright 2024 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package main

View File

@@ -1,4 +1,4 @@
// Copyright 2024 - Offen Authors <hioffen@posteo.de>
// Copyright 2024 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package main

View File

@@ -1,64 +1,212 @@
// Copyright 2024 - Offen Authors <hioffen@posteo.de>
// Copyright 2024 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package main
import (
"bytes"
"errors"
"fmt"
"io"
"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"
)
// encryptArchive encrypts the backup file using PGP and the configured passphrase.
// In case no passphrase is given it returns early, leaving the backup file
func countTrue(b ...bool) int {
c := int(0)
for _, v := range b {
if v {
c++
}
}
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 {
if s.c.GpgPassphrase == "" {
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,
)
}
gpgFile := fmt.Sprintf("%s.gpg", s.file)
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
}
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 outFile.Close()
defer func() {
if err := outFile.Close(); err != nil {
outerr = errors.Join(outerr, errwrap.Wrap(err, "error closing out file"))
}
}()
_, name := path.Split(s.file)
dst, err := openpgp.SymmetricallyEncrypt(outFile, []byte(s.c.GpgPassphrase), &openpgp.FileHints{
FileName: name,
}, nil)
dst, err := encryptor(outFile)
if err != nil {
return errwrap.Wrap(err, "error encrypting backup file")
}
defer dst.Close()
defer func() {
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 {
outerr = errors.Join(outerr, errwrap.Wrap(err, "error closing backup file"))
}
}()
if _, err := io.Copy(dst, src); err != nil {
return errwrap.Wrap(err, "error writing ciphertext to file")
}
s.file = gpgFile
s.file = encFile
s.logger.Info(
fmt.Sprintf("Encrypted backup using given passphrase, saving as `%s`.", s.file),
fmt.Sprintf("Encrypted backup using %q, saving as %q", extension, s.file),
)
return nil
return
}

View File

@@ -1,4 +1,4 @@
// Copyright 2022 - Offen Authors <hioffen@posteo.de>
// Copyright 2022 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
// Portions of this file are taken and adapted from `moby`, Copyright 2012-2017 Docker, Inc.
@@ -16,7 +16,7 @@ import (
"strings"
"github.com/cosiner/argv"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/stdcopy"
"github.com/offen/docker-volume-backup/internal/errwrap"
@@ -28,7 +28,7 @@ func (s *script) exec(containerRef string, command string, user string) ([]byte,
commandEnv := []string{
fmt.Sprintf("COMMAND_RUNTIME_ARCHIVE_FILEPATH=%s", s.file),
}
execID, err := s.cli.ContainerExecCreate(context.Background(), containerRef, types.ExecConfig{
execID, err := s.cli.ContainerExecCreate(context.Background(), containerRef, container.ExecOptions{
Cmd: args[0],
AttachStdin: true,
AttachStderr: true,
@@ -39,7 +39,7 @@ func (s *script) exec(containerRef string, command string, user string) ([]byte,
return nil, nil, errwrap.Wrap(err, "error creating container exec")
}
resp, err := s.cli.ContainerExecAttach(context.Background(), execID.ID, types.ExecStartCheck{})
resp, err := s.cli.ContainerExecAttach(context.Background(), execID.ID, container.ExecStartOptions{})
if err != nil {
return nil, nil, errwrap.Wrap(err, "error attaching container exec")
}
@@ -96,7 +96,7 @@ func (s *script) runLabeledCommands(label string) error {
Value: fmt.Sprintf("docker-volume-backup.exec-label=%s", s.c.ExecLabel),
})
}
containersWithCommand, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{
containersWithCommand, err := s.cli.ContainerList(context.Background(), container.ListOptions{
Filters: filters.NewArgs(f...),
})
if err != nil {
@@ -109,7 +109,7 @@ func (s *script) runLabeledCommands(label string) error {
Key: "label",
Value: "docker-volume-backup.exec-pre",
}
deprecatedContainers, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{
deprecatedContainers, err := s.cli.ContainerList(context.Background(), container.ListOptions{
Filters: filters.NewArgs(f...),
})
if err != nil {
@@ -126,7 +126,7 @@ func (s *script) runLabeledCommands(label string) error {
Key: "label",
Value: "docker-volume-backup.exec-post",
}
deprecatedContainers, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{
deprecatedContainers, err := s.cli.ContainerList(context.Background(), container.ListOptions{
Filters: filters.NewArgs(f...),
})
if err != nil {

View File

@@ -1,4 +1,4 @@
// Copyright 2022 - Offen Authors <hioffen@posteo.de>
// Copyright 2022 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package main

View File

@@ -1,4 +1,4 @@
// Copyright 2022 - Offen Authors <hioffen@posteo.de>
// Copyright 2022 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package main

View File

@@ -1,4 +1,4 @@
// Copyright 2021-2022 - Offen Authors <hioffen@posteo.de>
// Copyright 2021-2022 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package main

View File

@@ -1,4 +1,4 @@
// Copyright 2022 - Offen Authors <hioffen@posteo.de>
// Copyright 2022 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package main

View File

@@ -1,4 +1,4 @@
// Copyright 2024 - Offen Authors <hioffen@posteo.de>
// Copyright 2024 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package main

View File

@@ -1,4 +1,4 @@
// Copyright 2024 - Offen Authors <hioffen@posteo.de>
// Copyright 2024 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package main

View File

@@ -1,4 +1,4 @@
// Copyright 2024 - Offen Authors <hioffen@posteo.de>
// Copyright 2024 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package main
@@ -6,6 +6,7 @@ package main
import (
"errors"
"fmt"
"runtime/debug"
"github.com/offen/docker-volume-backup/internal/errwrap"
)
@@ -17,6 +18,7 @@ import (
func runScript(c *Config) (err error) {
defer func() {
if derr := recover(); derr != nil {
fmt.Printf("%s: %s\n", derr, debug.Stack())
asErr, ok := derr.(error)
if ok {
err = errwrap.Wrap(asErr, "unexpected panic running script")

View File

@@ -1,4 +1,4 @@
// Copyright 2022 - Offen Authors <hioffen@posteo.de>
// Copyright 2022 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package main
@@ -86,7 +86,12 @@ func (s *script) init() error {
var bf bytes.Buffer
if tErr := tmplFileName.Execute(&bf, map[string]string{
"Extension": fmt.Sprintf("tar.%s", s.c.BackupCompression),
"Extension": func() string {
if s.c.BackupCompression == "none" {
return "tar"
}
return fmt.Sprintf("tar.%s", s.c.BackupCompression)
}(),
}); tErr != nil {
return errwrap.Wrap(tErr, "error executing backup file extension template")
}
@@ -194,6 +199,7 @@ func (s *script) init() error {
Endpoint: s.c.AzureStorageEndpoint,
RemotePath: s.c.AzureStoragePath,
ConnectionString: s.c.AzureStorageConnectionString,
AccessTier: s.c.AzureStorageAccessTier,
}
azureBackend, err := azure.NewStorageBackend(azureConfig, logFunc)
if err != nil {

View File

@@ -1,4 +1,4 @@
// Copyright 2022 - Offen Authors <hioffen@posteo.de>
// Copyright 2022 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package main

View File

@@ -1,4 +1,4 @@
// Copyright 2024 - Offen Authors <hioffen@posteo.de>
// Copyright 2024 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package main
@@ -14,9 +14,11 @@ import (
"github.com/docker/cli/cli/command/service/progress"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
ctr "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/client"
"github.com/offen/docker-volume-backup/internal/errwrap"
)
@@ -65,7 +67,7 @@ func awaitContainerCountForService(cli *client.Client, serviceID string, count i
),
)
case <-poll.C:
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{
containers, err := cli.ContainerList(context.Background(), container.ListOptions{
Filters: filters.NewArgs(filters.KeyValuePair{
Key: "label",
Value: fmt.Sprintf("com.docker.swarm.service.id=%s", serviceID),
@@ -82,7 +84,7 @@ func awaitContainerCountForService(cli *client.Client, serviceID string, count i
}
func isSwarm(c interface {
Info(context.Context) (types.Info, error)
Info(context.Context) (system.Info, error)
}) (bool, error) {
info, err := c.Info(context.Background())
if err != nil {
@@ -123,11 +125,11 @@ func (s *script) stopContainersAndServices() (func() error, error) {
labelValue,
)
allContainers, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{})
allContainers, err := s.cli.ContainerList(context.Background(), container.ListOptions{})
if err != nil {
return noop, errwrap.Wrap(err, "error querying for containers")
}
containersToStop, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{
containersToStop, err := s.cli.ContainerList(context.Background(), container.ListOptions{
Filters: filters.NewArgs(filters.KeyValuePair{
Key: "label",
Value: filterMatchLabel,
@@ -151,15 +153,21 @@ func (s *script) stopContainersAndServices() (func() error, error) {
}),
Status: true,
})
if err != nil {
return noop, errwrap.Wrap(err, "error querying for services to scale down")
}
for _, s := range matchingServices {
if s.Spec.Mode.Replicated == nil {
return noop, errwrap.Wrap(
nil,
fmt.Sprintf("only replicated services can be restarted, but found a label on service %s", s.Spec.Name),
)
}
servicesToScaleDown = append(servicesToScaleDown, handledSwarmService{
serviceID: s.ID,
initialReplicaCount: *s.Spec.Mode.Replicated.Replicas,
})
}
if err != nil {
return noop, errwrap.Wrap(err, "error querying for services to scale down")
}
}
if len(containersToStop) == 0 && len(servicesToScaleDown) == 0 {
@@ -303,7 +311,7 @@ func (s *script) stopContainersAndServices() (func() error, error) {
continue
}
if err := s.cli.ContainerStart(context.Background(), container.ID, types.ContainerStartOptions{}); err != nil {
if err := s.cli.ContainerStart(context.Background(), container.ID, ctr.StartOptions{}); err != nil {
restartErrors = append(restartErrors, err)
}
}

View File

@@ -5,16 +5,16 @@ import (
"errors"
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/system"
)
type mockInfoClient struct {
result types.Info
result system.Info
err error
}
func (m *mockInfoClient) Info(context.Context) (types.Info, error) {
func (m *mockInfoClient) Info(context.Context) (system.Info, error) {
return m.result, m.err
}
@@ -28,7 +28,7 @@ func TestIsSwarm(t *testing.T) {
{
"swarm",
&mockInfoClient{
result: types.Info{
result: system.Info{
Swarm: swarm.Info{
LocalNodeState: swarm.LocalNodeStateActive,
},
@@ -40,7 +40,7 @@ func TestIsSwarm(t *testing.T) {
{
"compose",
&mockInfoClient{
result: types.Info{
result: system.Info{
Swarm: swarm.Info{
LocalNodeState: swarm.LocalNodeStateInactive,
},
@@ -52,7 +52,7 @@ func TestIsSwarm(t *testing.T) {
{
"balena",
&mockInfoClient{
result: types.Info{
result: system.Info{
Swarm: swarm.Info{
LocalNodeState: "",
},

View File

@@ -1,4 +1,4 @@
// Copyright 2022 - Offen Authors <hioffen@posteo.de>
// Copyright 2022 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package main

View File

@@ -59,11 +59,13 @@ GEM
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
rexml (3.2.6)
rexml (3.3.3)
strscan
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

@@ -30,6 +30,6 @@ nav_external_links:
url: https://github.com/offen/docker-volume-backup
footer_content: >-
Copyright &copy; 2021 Offen Authors and contributors.
Copyright &copy; 2024 <a target="_blank" href="https://www.offen.software">offen.software</a> and contributors.
Distributed under the <a href="https://github.com/offen/docker-volume-backup/tree/main/LICENSE">MPL-2.0 License.</a><br>
Something missing, unclear or not working? Open <a href="https://github.com/offen/docker-volume-backup/issues">an issue</a>.

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` 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,28 @@
---
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
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

@@ -9,6 +9,11 @@ parent: How Tos
In certain scenarios it can be required to run specific commands before and after a backup is taken (e.g. dumping a database).
When mounting the Docker socket into the `docker-volume-backup` container, you can define pre- and post-commands that will be run in the context of the target container (it is also possible to run commands inside the `docker-volume-backup` container itself using this feature).
{: .important }
In a multi-node Swarm setup, commands can currently only be run on the node the `offen/docker-volume-backup` container is running on.
Labeled containers on other nodes are not visible to the backup command.
Such commands are defined by specifying the command in a `docker-volume-backup.[step]-[pre|post]` label where `step` can be any of the following phases of a backup lifecycle:
- `archive` (the tar archive is created)

View File

@@ -25,7 +25,7 @@ services:
Notification backends other than email are also supported.
Refer to the documentation of [shoutrrr][shoutrrr-docs] to find out about options and configuration.
[shoutrrr-docs]: https://containrrr.dev/shoutrrr/0.7/services/overview/
[shoutrrr-docs]: https://containrrr.dev/shoutrrr/v0.8/services/overview/
{: .note }
If you also want notifications on successful executions, set `NOTIFICATION_LEVEL` to `info`.

View File

@@ -289,7 +289,7 @@ volumes:
data:
```
## Encrypting your backups using GPG
## Encrypting your backups symmetrically using GPG
```yml
version: '3'
@@ -311,6 +311,33 @@ volumes:
data:
```
## Encrypting your backups asymmetrically using GPG
```yml
version: '3'
services:
# ... define other services using the `data` volume here
backup:
image: offen/docker-volume-backup:v2
environment:
AWS_S3_BUCKET_NAME: backup-bucket
AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
GPG_PUBLIC_KEY_RING: |
-----BEGIN PGP PUBLIC KEY BLOCK-----
D/cIHu6GH/0ghlcUVSbgMg5RRI5QKNNKh04uLAPxr75mKwUg0xPUaWgyyrAChVBi
...
-----END PGP PUBLIC KEY BLOCK-----
volumes:
- data:/backup/my-app-backup:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
volumes:
data:
```
## Using mysqldump to prepare the backup
```yml

View File

@@ -43,8 +43,8 @@ You can populate below template according to your requirements and use it as you
# BACKUP_CRON_EXPRESSION="0 2 * * *"
# The compression algorithm used in conjunction with tar.
# Valid options are: "gz" (Gzip) and "zst" (Zstd).
# Note that the selection affects the file extension.
# Valid options are: "gz" (Gzip), "zst" (Zstd) or "none" (tar only).
# Default is "gz". Note that the selection affects the file extension.
# BACKUP_COMPRESSION="gz"
@@ -60,7 +60,7 @@ You can populate below template according to your requirements and use it as you
# will result in the same filename for every backup run, which means previous
# versions will be overwritten on subsequent runs.
# Extension can be defined literally or via "{{ .Extension }}" template,
# in which case it will become either "tar.gz" or "tar.zst" (depending
# in which case it will become either "tar.gz", "tar.zst" or ".tar" (depending
# on your BACKUP_COMPRESSION setting).
# The default results in filenames like: `backup-2021-08-29T04-00-00.tar.gz`.
@@ -269,6 +269,11 @@ You can populate below template according to your requirements and use it as you
# Note: Use your app's subpath in Dropbox, if it doesn't have global access.
# Consulte the README for further information.
# The access tier when using Azure Blob Storage. Possible values are
# https://github.com/Azure/azure-sdk-for-go/blob/sdk/storage/azblob/v1.3.2/sdk/storage/azblob/internal/generated/zz_constants.go#L14-L30
# AZURE_STORAGE_ACCESS_TIER="Cold"
# DROPBOX_REMOTE_PATH="/my/directory"
# Number of concurrent chunked uploads for Dropbox.
@@ -332,10 +337,19 @@ You can populate below template according to your requirements and use it as you
########### BACKUP ENCRYPTION
# Backups can be encrypted using gpg in case a passphrase is given.
# Backups can be encrypted symmetrically using gpg in case a passphrase is given.
# GPG_PASSPHRASE="<xxx>"
# Backups can be encrypted asymmetrically using gpg in case publickeys are given.
# GPG_PUBLIC_KEY_RING= |
#-----BEGIN PGP PUBLIC KEY BLOCK-----
#
#D/cIHu6GH/0ghlcUVSbgMg5RRI5QKNNKh04uLAPxr75mKwUg0xPUaWgyyrAChVBi
#...
#-----END PGP PUBLIC KEY BLOCK-----
########### STOPPING CONTAINERS AND SERVICES DURING BACKUP
# Containers or services can be stopped by applying a
@@ -378,7 +392,7 @@ You can populate below template according to your requirements and use it as you
# Notifications (email, Slack, etc.) can be sent out when a backup run finishes.
# Configuration is provided as a comma-separated list of URLs as consumed
# by `shoutrrr`: https://containrrr.dev/shoutrrr/0.7/services/overview/
# by `shoutrrr`: https://containrrr.dev/shoutrrr/v0.8/services/overview/
# The content of such notifications can be customized. Dedicated documentation
# on how to do this can be found in the README. When providing multiple URLs or
# an URL that contains a comma, the values can be URL encoded to avoid ambiguities.

71
go.mod
View File

@@ -3,76 +3,83 @@ module github.com/offen/docker-volume-backup
go 1.22
require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.1
github.com/containrrr/shoutrrr v0.7.1
filippo.io/age v1.2.0
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
github.com/containrrr/shoutrrr v0.8.0
github.com/cosiner/argv v0.1.0
github.com/docker/cli v24.0.9+incompatible
github.com/docker/docker v24.0.7+incompatible
github.com/gofrs/flock v0.8.1
github.com/docker/cli v27.1.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.7
github.com/klauspost/compress v1.17.9
github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d
github.com/minio/minio-go/v7 v7.0.68
github.com/minio/minio-go/v7 v7.0.75
github.com/offen/envconfig v1.5.0
github.com/otiai10/copy v1.14.0
github.com/pkg/sftp v1.13.6
github.com/robfig/cron/v3 v3.0.1
github.com/studio-b12/gowebdav v0.9.0
golang.org/x/crypto v0.19.0
golang.org/x/oauth2 v0.17.0
golang.org/x/sync v0.6.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
)
require (
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
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/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
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
go.opentelemetry.io/otel/metric v1.26.0 // indirect
go.opentelemetry.io/otel/sdk v1.26.0 // indirect
go.opentelemetry.io/otel/trace v1.26.0 // indirect
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.31.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect
)
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.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/Microsoft/go-winio v0.5.2 // indirect
github.com/ProtonMail/go-crypto v1.1.0-alpha.1
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.13.0 // indirect
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/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/klauspost/pgzip v1.2.6
github.com/kr/fs v0.1.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
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/sirupsen/logrus v1.9.3 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/ini.v1 v1.67.0 // 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
gotest.tools/v3 v3.0.3 // indirect
)

894
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
// Copyright 2024 - Offen Authors <hioffen@posteo.de>
// Copyright 2024 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package errwrap

View File

@@ -1,4 +1,4 @@
// Copyright 2022 - Offen Authors <hioffen@posteo.de>
// Copyright 2022 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package azure
@@ -17,6 +17,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blockblob"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
"github.com/offen/docker-volume-backup/internal/errwrap"
"github.com/offen/docker-volume-backup/internal/storage"
@@ -24,8 +26,9 @@ import (
type azureBlobStorage struct {
*storage.StorageBackend
client *azblob.Client
containerName string
client *azblob.Client
uploadStreamOptions *blockblob.UploadStreamOptions
containerName string
}
// Config contains values that define the configuration of an Azure Blob Storage.
@@ -36,6 +39,7 @@ type Config struct {
ConnectionString string
Endpoint string
RemotePath string
AccessTier string
}
// NewStorageBackend creates and initializes a new Azure Blob Storage backend.
@@ -81,9 +85,26 @@ func NewStorageBackend(opts Config, logFunc storage.Log) (storage.Backend, error
}
}
var uploadStreamOptions *blockblob.UploadStreamOptions
if opts.AccessTier != "" {
var found bool
for _, t := range blob.PossibleAccessTierValues() {
if string(t) == opts.AccessTier {
found = true
uploadStreamOptions = &blockblob.UploadStreamOptions{
AccessTier: &t,
}
}
}
if !found {
return nil, errwrap.Wrap(nil, fmt.Sprintf("%s is not a possible access tier value", opts.AccessTier))
}
}
storage := azureBlobStorage{
client: client,
containerName: opts.ContainerName,
client: client,
uploadStreamOptions: uploadStreamOptions,
containerName: opts.ContainerName,
StorageBackend: &storage.StorageBackend{
DestinationPath: opts.RemotePath,
Log: logFunc,
@@ -103,12 +124,13 @@ func (b *azureBlobStorage) Copy(file string) error {
if err != nil {
return errwrap.Wrap(err, fmt.Sprintf("error opening file %s", file))
}
_, err = b.client.UploadStream(
context.Background(),
b.containerName,
filepath.Join(b.DestinationPath, filepath.Base(file)),
fileReader,
nil,
b.uploadStreamOptions,
)
if err != nil {
return errwrap.Wrap(err, fmt.Sprintf("error uploading file %s", file))

View File

@@ -1,4 +1,4 @@
// Copyright 2022 - Offen Authors <hioffen@posteo.de>
// Copyright 2022 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package local

View File

@@ -1,4 +1,4 @@
// Copyright 2022 - Offen Authors <hioffen@posteo.de>
// Copyright 2022 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package s3

View File

@@ -1,4 +1,4 @@
// Copyright 2022 - Offen Authors <hioffen@posteo.de>
// Copyright 2022 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package ssh

View File

@@ -1,4 +1,4 @@
// Copyright 2022 - Offen Authors <hioffen@posteo.de>
// Copyright 2022 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package storage

View File

@@ -1,4 +1,4 @@
// Copyright 2022 - Offen Authors <hioffen@posteo.de>
// Copyright 2022 - offen.software <hioffen@posteo.de>
// SPDX-License-Identifier: MPL-2.0
package webdav

View File

@@ -1,9 +1,12 @@
FROM docker:24-dind
FROM docker:27-dind
RUN apk add \
age \
coreutils \
curl \
expect \
gpg \
gpg-agent \
jq \
moreutils \
tar \

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_DAYS: ${BACKUP_RETENTION_DAYS:-7}
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_DAYS: ${BACKUP_RETENTION_DAYS:-7}
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,8 +1,6 @@
version: '3'
services:
storage:
image: mcr.microsoft.com/azure-storage/azurite:3.26.0
image: mcr.microsoft.com/azure-storage/azurite:3.31.0
volumes:
- ${DATA_DIR:-./data}:/data
command: azurite-blob --blobHost 0.0.0.0 --blobPort 10000 --location /data
@@ -36,6 +34,7 @@ services:
AZURE_STORAGE_CONTAINER_NAME: test-container
AZURE_STORAGE_ENDPOINT: http://storage:10000/{{ .AccountName }}/
AZURE_STORAGE_PATH: 'path/to/backup'
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}

View File

@@ -1,5 +1,3 @@
version: '3'
services:
minio:
hostname: minio.local

View File

@@ -1,8 +1,6 @@
# Copyright 2020-2021 - Offen Authors <hioffen@posteo.de>
# Copyright 2020-2021 - offen.software <hioffen@posteo.de>
# SPDX-License-Identifier: Unlicense
version: '3.8'
services:
backup:
image: offen/docker-volume-backup:${TEST_VERSION:-canary}

View File

@@ -1,5 +1,3 @@
version: '3.8'
services:
database:
image: mariadb:10.7

View File

@@ -1,5 +1,3 @@
version: '3'
services:
backup:
image: offen/docker-volume-backup:${TEST_VERSION:-canary}

View File

@@ -1,5 +1,3 @@
version: '3'
services:
openapi_mock:
image: muonsoft/openapi-mock:0.3.9

View File

@@ -1,5 +1,3 @@
version: '3'
services:
backup:
image: offen/docker-volume-backup:${TEST_VERSION:-canary}

View File

@@ -0,0 +1,25 @@
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.gpg
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7}
GPG_PUBLIC_KEY_RING_FILE: /keys/public_key.asc
volumes:
- ${KEY_DIR:-.}/public_key.asc:/keys/public_key.asc
- ${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:

49
test/gpg-asym/run.sh Executable file
View File

@@ -0,0 +1,49 @@
#!/bin/sh
set -e
cd "$(dirname "$0")"
. ../util.sh
current_test=$(basename $(pwd))
export LOCAL_DIR=$(mktemp -d)
export KEY_DIR=$(mktemp -d)
export PASSPHRASE="test"
gpg --batch --gen-key <<EOF
Key-Type: RSA
Key-Length: 4096
Name-Real: offen
Name-Email: docker-volume-backup@local
Expire-Date: 0
Passphrase: $PASSPHRASE
%commit
EOF
gpg --export --armor --batch --yes --pinentry-mode loopback --passphrase $PASSPHRASE --output $KEY_DIR/public_key.asc
docker compose up -d --quiet-pull
sleep 5
docker compose exec backup backup
expect_running_containers "2"
TMP_DIR=$(mktemp -d)
gpg -d --pinentry-mode loopback --yes --passphrase $PASSPHRASE "$LOCAL_DIR/test.tar.gz.gpg" > "$LOCAL_DIR/decrypted.tar.gz"
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 "$LOCAL_DIR/decrypted.tar.gz"
pass "Found relevant files in decrypted and untared local backup."
if [ ! -L "$LOCAL_DIR/test-latest.tar.gz.gpg" ]; then
fail "Could not find local symlink to latest encrypted backup."
fi

View File

@@ -1,5 +1,3 @@
version: '3'
services:
backup:
image: offen/docker-volume-backup:${TEST_VERSION:-canary}

View File

@@ -1,5 +1,3 @@
version: '3.8'
services:
backup:
image: offen/docker-volume-backup:${TEST_VERSION:-canary}

View File

@@ -1,5 +1,3 @@
version: '3'
services:
backup:
image: offen/docker-volume-backup:${TEST_VERSION:-canary}

View File

@@ -1,5 +1,3 @@
version: '3'
services:
backup:
image: offen/docker-volume-backup:${TEST_VERSION:-canary}

View File

@@ -1,5 +1,3 @@
version: '3'
services:
minio:
image: minio/minio:RELEASE.2020-08-04T23-10-51Z

View File

@@ -1,5 +1,3 @@
version: '3'
services:
backup:
image: offen/docker-volume-backup:${TEST_VERSION:-canary}

View File

@@ -1,5 +1,3 @@
version: '3'
services:
db:
image: postgres:14-alpine

View File

@@ -1,8 +1,6 @@
# Copyright 2020-2021 - Offen Authors <hioffen@posteo.de>
# Copyright 2020-2021 - offen.software <hioffen@posteo.de>
# SPDX-License-Identifier: Unlicense
version: '3.8'
services:
backup:
image: offen/docker-volume-backup:${TEST_VERSION:-canary}

View File

@@ -1,8 +1,6 @@
# Copyright 2020-2021 - Offen Authors <hioffen@posteo.de>
# Copyright 2020-2021 - offen.software <hioffen@posteo.de>
# SPDX-License-Identifier: Unlicense
version: '3.8'
services:
backup:
image: offen/docker-volume-backup:${TEST_VERSION:-canary}

View File

@@ -1,5 +1,3 @@
version: '3'
services:
minio:
image: minio/minio:RELEASE.2020-08-04T23-10-51Z

View File

@@ -1,5 +1,3 @@
version: '3'
services:
minio:
image: minio/minio:RELEASE.2020-08-04T23-10-51Z

View File

@@ -1,8 +1,6 @@
# Copyright 2020-2021 - Offen Authors <hioffen@posteo.de>
# Copyright 2020-2021 - offen.software <hioffen@posteo.de>
# SPDX-License-Identifier: Unlicense
version: '3.8'
services:
minio:
image: minio/minio:RELEASE.2020-08-04T23-10-51Z

View File

@@ -1,8 +1,6 @@
# Copyright 2020-2021 - Offen Authors <hioffen@posteo.de>
# Copyright 2020-2021 - offen.software <hioffen@posteo.de>
# SPDX-License-Identifier: Unlicense
version: '3.8'
services:
minio:
image: minio/minio:RELEASE.2020-08-04T23-10-51Z

View File

@@ -1,5 +1,3 @@
version: '3'
services:
ssh:
image: linuxserver/openssh-server:version-8.6_p1-r3

View File

@@ -1,8 +1,6 @@
# Copyright 2020-2021 - Offen Authors <hioffen@posteo.de>
# Copyright 2020-2021 - offen.software <hioffen@posteo.de>
# SPDX-License-Identifier: Unlicense
version: '3.8'
services:
minio:
image: minio/minio:RELEASE.2020-08-04T23-10-51Z

View File

@@ -0,0 +1,21 @@
services:
backup:
image: offen/docker-volume-backup:${TEST_VERSION:-canary}
restart: always
environment:
BACKUP_FILENAME: test.{{ .Extension }}
BACKUP_COMPRESSION: none
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:

25
test/tar/run.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/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 5
expect_running_containers "2"
tmp_dir=$(mktemp -d)
tar -xvf "$LOCAL_DIR/test.tar" -C $tmp_dir
if [ ! -f "$tmp_dir/backup/app_data/offen.db" ]; then
fail "Could not find expected file in untared archive."
fi
pass "Expected file was found."

View File

@@ -1,5 +1,3 @@
version: '2.4'
services:
alpine:
image: alpine:3.17.3

View File

@@ -1,5 +1,3 @@
version: '3'
services:
webdav:
image: bytemark/webdav:2.4