mirror of
https://github.com/offen/docker-volume-backup.git
synced 2025-12-06 17:38:01 +01:00
Compare commits
7 Commits
v2.40.0
...
validate-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8aa6db3f5 | ||
|
|
8a64da4b0b | ||
|
|
f97ce11734 | ||
|
|
336e12f874 | ||
|
|
016c6c8307 | ||
|
|
e22f317fbb | ||
|
|
e04bd2f066 |
@@ -93,6 +93,8 @@ func compress(paths []string, outFilePath, algo string, concurrency int) error {
|
|||||||
|
|
||||||
func getCompressionWriter(file *os.File, algo string, concurrency int) (io.WriteCloser, error) {
|
func getCompressionWriter(file *os.File, algo string, concurrency int) (io.WriteCloser, error) {
|
||||||
switch algo {
|
switch algo {
|
||||||
|
case "none":
|
||||||
|
return &passThroughWriteCloser{file}, nil
|
||||||
case "gz":
|
case "gz":
|
||||||
w, err := pgzip.NewWriterLevel(file, 5)
|
w, err := pgzip.NewWriterLevel(file, 5)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -165,3 +167,15 @@ func writeTarball(path string, tarWriter *tar.Writer, prefix string) error {
|
|||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ func (c *command) runAsCommand() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, config := range configurations {
|
for _, config := range configurations {
|
||||||
|
if err := config.validate(); err != nil {
|
||||||
|
return errwrap.Wrap(err, "error validating config")
|
||||||
|
}
|
||||||
if err := runScript(config); err != nil {
|
if err := runScript(config); err != nil {
|
||||||
return errwrap.Wrap(err, "error running script")
|
return errwrap.Wrap(err, "error running script")
|
||||||
}
|
}
|
||||||
@@ -101,6 +104,12 @@ func (c *command) schedule(strategy configStrategy) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, cfg := range configurations {
|
for _, cfg := range configurations {
|
||||||
|
if err := cfg.validate(); err != nil {
|
||||||
|
return errwrap.Wrap(
|
||||||
|
err,
|
||||||
|
fmt.Sprintf("error validating config for schedule %s", cfg.BackupCronExpression),
|
||||||
|
)
|
||||||
|
}
|
||||||
config := cfg
|
config := cfg
|
||||||
id, err := c.cr.AddFunc(config.BackupCronExpression, func() {
|
id, err := c.cr.AddFunc(config.BackupCronExpression, func() {
|
||||||
c.logger.Info(
|
c.logger.Info(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
|
||||||
"github.com/offen/docker-volume-backup/internal/errwrap"
|
"github.com/offen/docker-volume-backup/internal/errwrap"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ type Config struct {
|
|||||||
BackupExcludeRegexp RegexpDecoder `split_words:"true"`
|
BackupExcludeRegexp RegexpDecoder `split_words:"true"`
|
||||||
BackupSkipBackendsFromPrune []string `split_words:"true"`
|
BackupSkipBackendsFromPrune []string `split_words:"true"`
|
||||||
GpgPassphrase string `split_words:"true"`
|
GpgPassphrase string `split_words:"true"`
|
||||||
|
GpgPublicKeyRing string `split_words:"true"`
|
||||||
NotificationURLs []string `envconfig:"NOTIFICATION_URLS"`
|
NotificationURLs []string `envconfig:"NOTIFICATION_URLS"`
|
||||||
NotificationLevel string `split_words:"true" default:"error"`
|
NotificationLevel string `split_words:"true" default:"error"`
|
||||||
EmailNotificationRecipient string `split_words:"true"`
|
EmailNotificationRecipient string `split_words:"true"`
|
||||||
@@ -76,6 +78,7 @@ type Config struct {
|
|||||||
AzureStorageContainerName string `split_words:"true"`
|
AzureStorageContainerName string `split_words:"true"`
|
||||||
AzureStoragePath string `split_words:"true"`
|
AzureStoragePath string `split_words:"true"`
|
||||||
AzureStorageEndpoint string `split_words:"true" default:"https://{{ .AccountName }}.blob.core.windows.net/"`
|
AzureStorageEndpoint string `split_words:"true" default:"https://{{ .AccountName }}.blob.core.windows.net/"`
|
||||||
|
AzureStorageAccessTier AzureStorageAccessTier `split_words:"true"`
|
||||||
DropboxEndpoint string `split_words:"true" default:"https://api.dropbox.com/"`
|
DropboxEndpoint string `split_words:"true" default:"https://api.dropbox.com/"`
|
||||||
DropboxOAuth2Endpoint string `envconfig:"DROPBOX_OAUTH2_ENDPOINT" default:"https://api.dropbox.com/"`
|
DropboxOAuth2Endpoint string `envconfig:"DROPBOX_OAUTH2_ENDPOINT" default:"https://api.dropbox.com/"`
|
||||||
DropboxRefreshToken string `split_words:"true"`
|
DropboxRefreshToken string `split_words:"true"`
|
||||||
@@ -87,11 +90,18 @@ type Config struct {
|
|||||||
additionalEnvVars map[string]string
|
additionalEnvVars map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Config) validate() error {
|
||||||
|
if c.AzureStoragePrimaryAccountKey != "" && c.AzureStorageConnectionString != "" {
|
||||||
|
return errwrap.Wrap(nil, "using azure primary account key and connection string are mutually exclusive")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type CompressionType string
|
type CompressionType string
|
||||||
|
|
||||||
func (c *CompressionType) Decode(v string) error {
|
func (c *CompressionType) Decode(v string) error {
|
||||||
switch v {
|
switch v {
|
||||||
case "gz", "zst":
|
case "none", "gz", "zst":
|
||||||
*c = CompressionType(v)
|
*c = CompressionType(v)
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
@@ -178,6 +188,30 @@ func (n *WholeNumber) Int() int {
|
|||||||
return int(*n)
|
return int(*n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AzureStorageAccessTier string
|
||||||
|
|
||||||
|
func (t *AzureStorageAccessTier) Decode(v string) error {
|
||||||
|
if v == "" {
|
||||||
|
*t = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, a := range blob.PossibleAccessTierValues() {
|
||||||
|
if string(a) == v {
|
||||||
|
*t = AzureStorageAccessTier(v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errwrap.Wrap(nil, fmt.Sprintf("%s is not a possible access tier value", v))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *AzureStorageAccessTier) AccessTier() *blob.AccessTier {
|
||||||
|
if *t == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
a := blob.AccessTier(*t)
|
||||||
|
return &a
|
||||||
|
}
|
||||||
|
|
||||||
type envVarLookup struct {
|
type envVarLookup struct {
|
||||||
ok bool
|
ok bool
|
||||||
key string
|
key string
|
||||||
|
|||||||
@@ -4,20 +4,75 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||||
openpgp "github.com/ProtonMail/go-crypto/openpgp/v2"
|
openpgp "github.com/ProtonMail/go-crypto/openpgp/v2"
|
||||||
"github.com/offen/docker-volume-backup/internal/errwrap"
|
"github.com/offen/docker-volume-backup/internal/errwrap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// encryptArchive encrypts the backup file using PGP and the configured passphrase.
|
func (s *script) encryptAsymmetrically(outFile *os.File) (io.WriteCloser, func() error, error) {
|
||||||
// In case no passphrase is given it returns early, leaving the backup file
|
|
||||||
|
entityList, err := openpgp.ReadArmoredKeyRing(bytes.NewReader([]byte(s.c.GpgPublicKeyRing)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errwrap.Wrap(err, "error parsing armored keyring")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
// untouched.
|
||||||
func (s *script) encryptArchive() error {
|
func (s *script) encryptArchive() error {
|
||||||
if s.c.GpgPassphrase == "" {
|
|
||||||
|
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
|
||||||
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,22 +91,31 @@ func (s *script) encryptArchive() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errwrap.Wrap(err, "error opening out file")
|
return errwrap.Wrap(err, "error opening out file")
|
||||||
}
|
}
|
||||||
defer outFile.Close()
|
defer func() {
|
||||||
|
if err := outFile.Close(); err != nil {
|
||||||
|
cleanUpErr = errors.Join(cleanUpErr, errwrap.Wrap(err, "error closing out file"))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
_, name := path.Split(s.file)
|
dst, dstCloseCallback, err := encrypt(outFile)
|
||||||
dst, err := openpgp.SymmetricallyEncrypt(outFile, []byte(s.c.GpgPassphrase), &openpgp.FileHints{
|
|
||||||
FileName: name,
|
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errwrap.Wrap(err, "error encrypting backup file")
|
return errwrap.Wrap(err, "error encrypting backup file")
|
||||||
}
|
}
|
||||||
defer dst.Close()
|
defer func() {
|
||||||
|
if err := dstCloseCallback(); err != nil {
|
||||||
|
cleanUpErr = errors.Join(cleanUpErr, errwrap.Wrap(err, "error closing encrypted backup file"))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
src, err := os.Open(s.file)
|
src, err := os.Open(s.file)
|
||||||
if err != nil {
|
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 `%s`", s.file))
|
||||||
}
|
}
|
||||||
defer src.Close()
|
defer func() {
|
||||||
|
if err := src.Close(); err != nil {
|
||||||
|
cleanUpErr = errors.Join(cleanUpErr, errwrap.Wrap(err, "error closing backup file"))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if _, err := io.Copy(dst, src); err != nil {
|
if _, err := io.Copy(dst, src); err != nil {
|
||||||
return errwrap.Wrap(err, "error writing ciphertext to file")
|
return errwrap.Wrap(err, "error writing ciphertext to file")
|
||||||
@@ -59,7 +123,7 @@ func (s *script) encryptArchive() error {
|
|||||||
|
|
||||||
s.file = gpgFile
|
s.file = gpgFile
|
||||||
s.logger.Info(
|
s.logger.Info(
|
||||||
fmt.Sprintf("Encrypted backup using given passphrase, saving as `%s`.", s.file),
|
fmt.Sprintf("Encrypted backup using gpg, saving as `%s`.", s.file),
|
||||||
)
|
)
|
||||||
return nil
|
return cleanUpErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,12 @@ func (s *script) init() error {
|
|||||||
|
|
||||||
var bf bytes.Buffer
|
var bf bytes.Buffer
|
||||||
if tErr := tmplFileName.Execute(&bf, map[string]string{
|
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 {
|
}); tErr != nil {
|
||||||
return errwrap.Wrap(tErr, "error executing backup file extension template")
|
return errwrap.Wrap(tErr, "error executing backup file extension template")
|
||||||
}
|
}
|
||||||
@@ -194,6 +199,7 @@ func (s *script) init() error {
|
|||||||
Endpoint: s.c.AzureStorageEndpoint,
|
Endpoint: s.c.AzureStorageEndpoint,
|
||||||
RemotePath: s.c.AzureStoragePath,
|
RemotePath: s.c.AzureStoragePath,
|
||||||
ConnectionString: s.c.AzureStorageConnectionString,
|
ConnectionString: s.c.AzureStorageConnectionString,
|
||||||
|
AccessTier: s.c.AzureStorageAccessTier.AccessTier(),
|
||||||
}
|
}
|
||||||
azureBackend, err := azure.NewStorageBackend(azureConfig, logFunc)
|
azureBackend, err := azure.NewStorageBackend(azureConfig, logFunc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ GEM
|
|||||||
rb-fsevent (0.11.2)
|
rb-fsevent (0.11.2)
|
||||||
rb-inotify (0.10.1)
|
rb-inotify (0.10.1)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
rexml (3.2.8)
|
rexml (3.3.3)
|
||||||
strscan (>= 3.0.9)
|
strscan
|
||||||
rouge (3.30.0)
|
rouge (3.30.0)
|
||||||
safe_yaml (1.0.5)
|
safe_yaml (1.0.5)
|
||||||
sassc (2.4.0)
|
sassc (2.4.0)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ nav_order: 7
|
|||||||
# Encrypt backups using GPG
|
# Encrypt backups using GPG
|
||||||
|
|
||||||
The image supports encrypting backups using GPG out of the box.
|
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.
|
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):
|
Assuming you have `gpg` installed, you can decrypt such a backup using (your OS will prompt for the passphrase before decryption can happen):
|
||||||
|
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ volumes:
|
|||||||
data:
|
data:
|
||||||
```
|
```
|
||||||
|
|
||||||
## Encrypting your backups using GPG
|
## Encrypting your backups symmetrically using GPG
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
version: '3'
|
version: '3'
|
||||||
@@ -311,6 +311,33 @@ volumes:
|
|||||||
data:
|
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
|
## Using mysqldump to prepare the backup
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ You can populate below template according to your requirements and use it as you
|
|||||||
# BACKUP_CRON_EXPRESSION="0 2 * * *"
|
# BACKUP_CRON_EXPRESSION="0 2 * * *"
|
||||||
|
|
||||||
# The compression algorithm used in conjunction with tar.
|
# The compression algorithm used in conjunction with tar.
|
||||||
# Valid options are: "gz" (Gzip) and "zst" (Zstd).
|
# Valid options are: "gz" (Gzip), "zst" (Zstd) or "none" (tar only).
|
||||||
# Note that the selection affects the file extension.
|
# Default is "gz". Note that the selection affects the file extension.
|
||||||
|
|
||||||
# BACKUP_COMPRESSION="gz"
|
# 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
|
# will result in the same filename for every backup run, which means previous
|
||||||
# versions will be overwritten on subsequent runs.
|
# versions will be overwritten on subsequent runs.
|
||||||
# Extension can be defined literally or via "{{ .Extension }}" template,
|
# 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).
|
# on your BACKUP_COMPRESSION setting).
|
||||||
# The default results in filenames like: `backup-2021-08-29T04-00-00.tar.gz`.
|
# 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.
|
# Note: Use your app's subpath in Dropbox, if it doesn't have global access.
|
||||||
# Consulte the README for further information.
|
# 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"
|
# DROPBOX_REMOTE_PATH="/my/directory"
|
||||||
|
|
||||||
# Number of concurrent chunked uploads for Dropbox.
|
# 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
|
########### 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>"
|
# 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
|
########### STOPPING CONTAINERS AND SERVICES DURING BACKUP
|
||||||
|
|
||||||
# Containers or services can be stopped by applying a
|
# Containers or services can be stopped by applying a
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -20,8 +20,8 @@ require (
|
|||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/studio-b12/gowebdav v0.9.0
|
github.com/studio-b12/gowebdav v0.9.0
|
||||||
golang.org/x/crypto v0.25.0
|
golang.org/x/crypto v0.25.0
|
||||||
golang.org/x/oauth2 v0.21.0
|
golang.org/x/oauth2 v0.22.0
|
||||||
golang.org/x/sync v0.7.0
|
golang.org/x/sync v0.8.0
|
||||||
mvdan.cc/sh/v3 v3.8.0
|
mvdan.cc/sh/v3 v3.8.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
8
go.sum
8
go.sum
@@ -368,8 +368,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr
|
|||||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/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-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.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
|
||||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/oauth2 v0.22.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-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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -380,8 +380,8 @@ 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-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-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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.8.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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import (
|
|||||||
|
|
||||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
"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"
|
||||||
|
"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/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
|
||||||
"github.com/offen/docker-volume-backup/internal/errwrap"
|
"github.com/offen/docker-volume-backup/internal/errwrap"
|
||||||
"github.com/offen/docker-volume-backup/internal/storage"
|
"github.com/offen/docker-volume-backup/internal/storage"
|
||||||
@@ -25,6 +27,7 @@ import (
|
|||||||
type azureBlobStorage struct {
|
type azureBlobStorage struct {
|
||||||
*storage.StorageBackend
|
*storage.StorageBackend
|
||||||
client *azblob.Client
|
client *azblob.Client
|
||||||
|
uploadStreamOptions *blockblob.UploadStreamOptions
|
||||||
containerName string
|
containerName string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,14 +39,11 @@ type Config struct {
|
|||||||
ConnectionString string
|
ConnectionString string
|
||||||
Endpoint string
|
Endpoint string
|
||||||
RemotePath string
|
RemotePath string
|
||||||
|
AccessTier *blob.AccessTier
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStorageBackend creates and initializes a new Azure Blob Storage backend.
|
// NewStorageBackend creates and initializes a new Azure Blob Storage backend.
|
||||||
func NewStorageBackend(opts Config, logFunc storage.Log) (storage.Backend, error) {
|
func NewStorageBackend(opts Config, logFunc storage.Log) (storage.Backend, error) {
|
||||||
if opts.PrimaryAccountKey != "" && opts.ConnectionString != "" {
|
|
||||||
return nil, errwrap.Wrap(nil, "using primary account key and connection string are mutually exclusive")
|
|
||||||
}
|
|
||||||
|
|
||||||
endpointTemplate, err := template.New("endpoint").Parse(opts.Endpoint)
|
endpointTemplate, err := template.New("endpoint").Parse(opts.Endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errwrap.Wrap(err, "error parsing endpoint template")
|
return nil, errwrap.Wrap(err, "error parsing endpoint template")
|
||||||
@@ -83,6 +83,9 @@ func NewStorageBackend(opts Config, logFunc storage.Log) (storage.Backend, error
|
|||||||
|
|
||||||
storage := azureBlobStorage{
|
storage := azureBlobStorage{
|
||||||
client: client,
|
client: client,
|
||||||
|
uploadStreamOptions: &blockblob.UploadStreamOptions{
|
||||||
|
AccessTier: opts.AccessTier,
|
||||||
|
},
|
||||||
containerName: opts.ContainerName,
|
containerName: opts.ContainerName,
|
||||||
StorageBackend: &storage.StorageBackend{
|
StorageBackend: &storage.StorageBackend{
|
||||||
DestinationPath: opts.RemotePath,
|
DestinationPath: opts.RemotePath,
|
||||||
@@ -103,12 +106,13 @@ func (b *azureBlobStorage) Copy(file string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errwrap.Wrap(err, fmt.Sprintf("error opening file %s", file))
|
return errwrap.Wrap(err, fmt.Sprintf("error opening file %s", file))
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = b.client.UploadStream(
|
_, err = b.client.UploadStream(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
b.containerName,
|
b.containerName,
|
||||||
filepath.Join(b.DestinationPath, filepath.Base(file)),
|
filepath.Join(b.DestinationPath, filepath.Base(file)),
|
||||||
fileReader,
|
fileReader,
|
||||||
nil,
|
b.uploadStreamOptions,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errwrap.Wrap(err, fmt.Sprintf("error uploading file %s", file))
|
return errwrap.Wrap(err, fmt.Sprintf("error uploading file %s", file))
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ RUN apk add \
|
|||||||
coreutils \
|
coreutils \
|
||||||
curl \
|
curl \
|
||||||
gpg \
|
gpg \
|
||||||
|
gpg-agent \
|
||||||
jq \
|
jq \
|
||||||
moreutils \
|
moreutils \
|
||||||
tar \
|
tar \
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ services:
|
|||||||
AZURE_STORAGE_CONTAINER_NAME: test-container
|
AZURE_STORAGE_CONTAINER_NAME: test-container
|
||||||
AZURE_STORAGE_ENDPOINT: http://storage:10000/{{ .AccountName }}/
|
AZURE_STORAGE_ENDPOINT: http://storage:10000/{{ .AccountName }}/
|
||||||
AZURE_STORAGE_PATH: 'path/to/backup'
|
AZURE_STORAGE_PATH: 'path/to/backup'
|
||||||
|
AZURE_STORAGE_ACCESS_TIER: Hot
|
||||||
BACKUP_FILENAME: test.tar.gz
|
BACKUP_FILENAME: test.tar.gz
|
||||||
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
|
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
|
||||||
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7}
|
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7}
|
||||||
|
|||||||
25
test/gpg-asym/docker-compose.yml
Normal file
25
test/gpg-asym/docker-compose.yml
Normal 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
49
test/gpg-asym/run.sh
Executable 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
|
||||||
21
test/tar/docker-compose.yml
Normal file
21
test/tar/docker-compose.yml
Normal 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
25
test/tar/run.sh
Executable 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."
|
||||||
Reference in New Issue
Block a user