Compare commits

..

15 Commits

Author SHA1 Message Date
Frederik Ring
210c7d4540 Reuse hook mechanism for scheduling clean up tasks (#33)
* reuse hook mechanism for scheduling clean up tasks

* register hooks before creating files or dirs

* fix logging order

* use typed hook levels
2021-11-08 19:10:10 +01:00
Frederik Ring
3c06bf8102 run cli test using BACKUP_FROM_SNAPSHOT 2021-11-08 08:44:59 +01:00
schwannden
411c39ee72 create a snapshot before creating tar archive (#32)
* create a snapshot before creating tar archive

* safeguard snapshot removal and make snapshot optional

* fix typo, make sure remove snapshot failure triggers failure hook

Co-authored-by: Schwannden Kuo <schwannden@mobagel.com>
2021-11-08 08:39:18 +01:00
Frederik Ring
0c666d0c88 use lstat when checking whether file is a symlink 2021-11-03 18:07:55 +01:00
Frederik Ring
a0402b407d fix fileinfo mode comparison when checking for symlinks 2021-11-03 18:03:44 +01:00
Frederik Ring
3193e88fc0 os.FileInfo cannot be used for deleting files as it does not contain a full path 2021-11-02 06:40:37 +01:00
Frederik Ring
c391230be6 Merge pull request #31 from offen/exclude-symlink-candidates
Exclude symlinks from candidates when pruning local files
2021-10-31 20:07:51 +01:00
Frederik Ring
f946f36fb0 exclude symlinks from candidates when pruning local files
Previously, symlinks would be included in the set of candidates, but would
be skipped when pruning. This could lead to a wrong number of candidates
being printed in the log messages.
2021-10-29 09:00:37 +02:00
Frederik Ring
5245b5882f update README, save some indentation 2021-10-28 19:55:39 +02:00
schwannden
7f0f173115 adding option to skip tls verification error (#30)
* adding option to skip tls verification error

* merge options

* removed merged option from README

Co-authored-by: Schwannden Kuo <schwannden@mobagel.com>
2021-10-28 19:51:35 +02:00
Frederik Ring
ad7ec58322 add syntax highlighting 2021-10-23 17:45:57 +02:00
Frederik Ring
b7ab2fbacc add section about container timezones to the README 2021-10-23 17:44:30 +02:00
Frederik Ring
789fc656e8 Merge pull request #27 from offen/latest-symlink
Automatically create symlink to latest local backup if configured
2021-10-01 18:47:16 +02:00
Frederik Ring
c59b40f2df automatically create symlink to latest local backup if configured 2021-10-01 18:19:24 +02:00
Frederik Ring
cff418e735 fix README grammar 2021-10-01 08:48:20 +02:00
7 changed files with 216 additions and 64 deletions

View File

@@ -17,6 +17,7 @@ It handles __recurring or one-off backups of Docker volumes__ to a __local direc
- [Send email notifications on failed backup runs](#send-email-notifications-on-failed-backup-runs) - [Send email notifications on failed backup runs](#send-email-notifications-on-failed-backup-runs)
- [Encrypting your backup using GPG](#encrypting-your-backup-using-gpg) - [Encrypting your backup using GPG](#encrypting-your-backup-using-gpg)
- [Restoring a volume from a backup](#restoring-a-volume-from-a-backup) - [Restoring a volume from a backup](#restoring-a-volume-from-a-backup)
- [Set the timezone the container runs in](#set-the-timezone-the-container-runs-in)
- [Using with Docker Swarm](#using-with-docker-swarm) - [Using with Docker Swarm](#using-with-docker-swarm)
- [Manually triggering a backup](#manually-triggering-a-backup) - [Manually triggering a backup](#manually-triggering-a-backup)
- [Recipes](#recipes) - [Recipes](#recipes)
@@ -120,6 +121,18 @@ You can populate below template according to your requirements and use it as you
# BACKUP_FILENAME="backup-%Y-%m-%dT%H-%M-%S.tar.gz" # BACKUP_FILENAME="backup-%Y-%m-%dT%H-%M-%S.tar.gz"
# When storing local backups, a symlink to the latest backup can be created
# in case a value is given for this key. This has no effect on remote backups.
# BACKUP_LATEST_SYMLINK="backup.latest.tar.gz"
# Whether to copy the content of backup folder before creating the tar archive.
# In the rare scenario where the content of the source backup volume is continously
# updating, but we do not wish to stop the container while performing the backup,
# this setting can be used to ensure the integrity of the tar.gz file.
# BACKUP_FROM_SNAPSHOT="false"
########### BACKUP STORAGE ########### BACKUP STORAGE
# The name of the remote bucket that should be used for storing backups. If # The name of the remote bucket that should be used for storing backups. If
@@ -156,7 +169,8 @@ You can populate below template according to your requirements and use it as you
# Setting this variable to `true` will disable verification of # Setting this variable to `true` will disable verification of
# SSL certificates. You shouldn't use this unless you use self-signed # SSL certificates. You shouldn't use this unless you use self-signed
# certificates for your remote storage backend. # certificates for your remote storage backend. This can only be used
# when AWS_ENDPOINT_PROTO is set to `https`.
# AWS_ENDPOINT_INSECURE="true" # AWS_ENDPOINT_INSECURE="true"
@@ -353,6 +367,27 @@ In case you need to restore a volume from a backup, the most straight forward pr
Depending on your setup and the application(s) you are running, this might involve other steps to be taken still. Depending on your setup and the application(s) you are running, this might involve other steps to be taken still.
### Set the timezone the container runs in
By default a container based on this image will run in the UTC timezone.
As the image is designed to be as small as possible, additional timezone data is not included.
In case you want to run your cron rules in your local timezone (respecting DST and similar), you can mount your Docker host's `/etc/timezone` and `/etc/localtime` in read-only mode:
```yml
version: '3'
services:
backup:
image: offen/docker-volume-backup:latest
volumes:
- data:/backup/my-app-backup:ro
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
volumes:
data:
```
### Using with Docker Swarm ### Using with Docker Swarm
By default, Docker Swarm will restart stopped containers automatically, even when manually stopped. By default, Docker Swarm will restart stopped containers automatically, even when manually stopped.
@@ -437,6 +472,9 @@ services:
# ... define other services using the `data` volume here # ... define other services using the `data` volume here
backup: backup:
image: offen/docker-volume-backup:latest image: offen/docker-volume-backup:latest
environment:
BACKUP_FILENAME: backup-%Y-%m-%dT%H-%M-%S.tar.gz
BACKUP_LATEST_SYMLINK: backup-latest.tar.gz
volumes: volumes:
- data:/backup/my-app-backup:ro - data:/backup/my-app-backup:ro
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
@@ -574,7 +612,7 @@ volumes:
## Differences to `futurice/docker-volume-backup` ## Differences to `futurice/docker-volume-backup`
This image is heavily inspired by the `futurice/docker-volume-backup`. We decided to publish this image as a simpler and more lightweight alternative because of the following requirements: This image is heavily inspired by `futurice/docker-volume-backup`. We decided to publish this image as a simpler and more lightweight alternative because of the following requirements:
- The original image is based on `ubuntu` and requires additional tools, making it heavy. - The original image is based on `ubuntu` and requires additional tools, making it heavy.
This version is roughly 1/25 in compressed size (it's ~12MB). This version is roughly 1/25 in compressed size (it's ~12MB).
@@ -585,3 +623,5 @@ Local copies of backups can also be pruned once they reach a certain age.
- InfluxDB specific functionality from the original image was removed. - InfluxDB specific functionality from the original image was removed.
- `arm64` and `arm/v7` architectures are supported. - `arm64` and `arm/v7` architectures are supported.
- Docker in Swarm mode is supported. - Docker in Swarm mode is supported.
- Notifications on failed backups are supported
- IAM authentication through instance profiles is supported

View File

@@ -26,6 +26,7 @@ import (
"github.com/m90/targz" "github.com/m90/targz"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio-go/v7/pkg/credentials"
"github.com/otiai10/copy"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp"
) )
@@ -40,16 +41,31 @@ func main() {
} }
defer func() { defer func() {
if err := recover(); err != nil { if pArg := recover(); pArg != nil {
if e, ok := err.(error); ok && strings.Contains(e.Error(), msgBackupFailed) { if err, ok := pArg.(error); ok {
if hookErr := s.runHooks(err, hookLevelCleanup, hookLevelFailure); hookErr != nil {
s.logger.Errorf("An error occurred calling the registered hooks: %s", hookErr)
}
os.Exit(1) os.Exit(1)
} }
panic(err) panic(pArg)
} }
if err := s.runHooks(nil, hookLevelCleanup); err != nil {
s.logger.Errorf(
"Backup procedure ran successfully, but an error ocurred calling the registered hooks: %v",
err,
)
os.Exit(1)
}
s.logger.Info("Finished running backup tasks.")
}() }()
s.must(func() error { s.must(func() error {
restartContainers, err := s.stopContainers() restartContainers, err := s.stopContainers()
// The mechanism for restarting containers is not using hooks as it
// should happen as soon as possible (i.e. before uploading backups or
// similar).
defer func() { defer func() {
s.must(restartContainers()) s.must(restartContainers())
}() }()
@@ -59,16 +75,9 @@ func main() {
return s.takeBackup() return s.takeBackup()
}()) }())
s.must(func() error { s.must(s.encryptBackup())
defer func() { s.must(s.copyBackup())
s.must(s.removeArtifacts())
}()
s.must(s.encryptBackup())
return s.copyBackup()
}())
s.must(s.pruneOldBackups()) s.must(s.pruneOldBackups())
s.logger.Info("Finished running backup tasks.")
} }
// script holds all the stateful information required to orchestrate a // script holds all the stateful information required to orchestrate a
@@ -89,11 +98,13 @@ type script struct {
type config struct { type config struct {
BackupSources string `split_words:"true" default:"/backup"` BackupSources string `split_words:"true" default:"/backup"`
BackupFilename string `split_words:"true" default:"backup-%Y-%m-%dT%H-%M-%S.tar.gz"` BackupFilename string `split_words:"true" default:"backup-%Y-%m-%dT%H-%M-%S.tar.gz"`
BackupLatestSymlink string `split_words:"true"`
BackupArchive string `split_words:"true" default:"/archive"` BackupArchive string `split_words:"true" default:"/archive"`
BackupRetentionDays int32 `split_words:"true" default:"-1"` BackupRetentionDays int32 `split_words:"true" default:"-1"`
BackupPruningLeeway time.Duration `split_words:"true" default:"1m"` BackupPruningLeeway time.Duration `split_words:"true" default:"1m"`
BackupPruningPrefix string `split_words:"true"` BackupPruningPrefix string `split_words:"true"`
BackupStopContainerLabel string `split_words:"true" default:"true"` BackupStopContainerLabel string `split_words:"true" default:"true"`
BackupFromSnapshot bool `split_words:"true"`
AwsS3BucketName string `split_words:"true"` AwsS3BucketName string `split_words:"true"`
AwsEndpoint string `split_words:"true" default:"s3.amazonaws.com"` AwsEndpoint string `split_words:"true" default:"s3.amazonaws.com"`
AwsEndpointProto string `split_words:"true" default:"https"` AwsEndpointProto string `split_words:"true" default:"https"`
@@ -159,10 +170,25 @@ func newScript() (*script, error) {
return nil, errors.New("newScript: AWS_S3_BUCKET_NAME is defined, but no credentials were provided") return nil, errors.New("newScript: AWS_S3_BUCKET_NAME is defined, but no credentials were provided")
} }
mc, err := minio.New(s.c.AwsEndpoint, &minio.Options{ options := minio.Options{
Creds: creds, Creds: creds,
Secure: !s.c.AwsEndpointInsecure && s.c.AwsEndpointProto == "https", Secure: s.c.AwsEndpointProto == "https",
}) }
if s.c.AwsEndpointInsecure {
if !options.Secure {
return nil, errors.New("newScript: AWS_ENDPOINT_INSECURE = true is only meaningful for https")
}
transport, err := minio.DefaultTransport(true)
if err != nil {
return nil, fmt.Errorf("newScript: failed to create default minio transport")
}
transport.TLSClientConfig.InsecureSkipVerify = true
options.Transport = transport
}
mc, err := minio.New(s.c.AwsEndpoint, &options)
if err != nil { if err != nil {
return nil, fmt.Errorf("newScript: error setting up minio client: %w", err) return nil, fmt.Errorf("newScript: error setting up minio client: %w", err)
} }
@@ -196,6 +222,11 @@ func newScript() (*script, error) {
var noop = func() error { return nil } var noop = func() error { return nil }
// registerHook adds the given action at the given level.
func (s *script) registerHook(level hookLevel, action func(err error, start time.Time, logOutput string) error) {
s.hooks = append(s.hooks, hook{level, action})
}
// stopContainers stops all Docker containers that are marked as to being // stopContainers stops all Docker containers that are marked as to being
// stopped during the backup and returns a function that can be called to // stopped during the backup and returns a function that can be called to
// restart everything that has been stopped. // restart everything that has been stopped.
@@ -314,10 +345,37 @@ func (s *script) stopContainers() (func() error, error) {
// saves it to disk. // saves it to disk.
func (s *script) takeBackup() error { func (s *script) takeBackup() error {
s.file = timeutil.Strftime(&s.start, s.file) s.file = timeutil.Strftime(&s.start, s.file)
if err := targz.Compress(s.c.BackupSources, s.file); err != nil { backupSources := s.c.BackupSources
if s.c.BackupFromSnapshot {
backupSources = filepath.Join("/tmp", s.c.BackupSources)
// copy before compressing guard against a situation where backup folder's content are still growing.
s.registerHook(hookLevelCleanup, func(error, time.Time, string) error {
if err := remove(backupSources); err != nil {
return fmt.Errorf("takeBackup: error removing snapshot: %w", err)
}
s.logger.Infof("Removed snapshot `%s`.", backupSources)
return nil
})
if err := copy.Copy(s.c.BackupSources, backupSources, copy.Options{PreserveTimes: true}); err != nil {
return fmt.Errorf("takeBackup: error creating snapshot: %w", err)
}
s.logger.Infof("Created snapshot of `%s` at `%s`.", s.c.BackupSources, backupSources)
}
tarFile := s.file
s.registerHook(hookLevelCleanup, func(error, time.Time, string) error {
if err := remove(tarFile); err != nil {
return fmt.Errorf("takeBackup: error removing tar file: %w", err)
}
s.logger.Infof("Removed tar file `%s`.", tarFile)
return nil
})
if err := targz.Compress(backupSources, tarFile); err != nil {
return fmt.Errorf("takeBackup: error compressing backup folder: %w", err) return fmt.Errorf("takeBackup: error compressing backup folder: %w", err)
} }
s.logger.Infof("Created backup of `%s` at `%s`.", s.c.BackupSources, s.file)
s.logger.Infof("Created backup of `%s` at `%s`.", backupSources, tarFile)
return nil return nil
} }
@@ -328,9 +386,16 @@ func (s *script) encryptBackup() error {
if s.c.GpgPassphrase == "" { if s.c.GpgPassphrase == "" {
return nil return nil
} }
defer os.Remove(s.file)
gpgFile := fmt.Sprintf("%s.gpg", s.file) gpgFile := fmt.Sprintf("%s.gpg", s.file)
s.registerHook(hookLevelCleanup, func(error, time.Time, string) error {
if err := remove(gpgFile); err != nil {
return fmt.Errorf("encryptBackup: error removing gpg file: %w", err)
}
s.logger.Infof("Removed GPG file `%s`.", gpgFile)
return nil
})
outFile, err := os.Create(gpgFile) outFile, err := os.Create(gpgFile)
defer outFile.Close() defer outFile.Close()
if err != nil { if err != nil {
@@ -349,7 +414,7 @@ func (s *script) encryptBackup() error {
src, err := os.Open(s.file) src, err := os.Open(s.file)
if err != nil { if err != nil {
return fmt.Errorf("encryptBackup: error opening backup file %s: %w", s.file, err) return fmt.Errorf("encryptBackup: error opening backup file `%s`: %w", s.file, err)
} }
if _, err := io.Copy(dst, src); err != nil { if _, err := io.Copy(dst, src); err != nil {
@@ -376,27 +441,21 @@ func (s *script) copyBackup() error {
} }
if _, err := os.Stat(s.c.BackupArchive); !os.IsNotExist(err) { if _, err := os.Stat(s.c.BackupArchive); !os.IsNotExist(err) {
if err := copy(s.file, path.Join(s.c.BackupArchive, name)); err != nil { if err := copyFile(s.file, path.Join(s.c.BackupArchive, name)); err != nil {
return fmt.Errorf("copyBackup: error copying file to local archive: %w", err) return fmt.Errorf("copyBackup: error copying file to local archive: %w", err)
} }
s.logger.Infof("Stored copy of backup `%s` in local archive `%s`.", s.file, s.c.BackupArchive) s.logger.Infof("Stored copy of backup `%s` in local archive `%s`.", s.file, s.c.BackupArchive)
} if s.c.BackupLatestSymlink != "" {
return nil symlink := path.Join(s.c.BackupArchive, s.c.BackupLatestSymlink)
} if _, err := os.Lstat(symlink); err == nil {
os.Remove(symlink)
// removeArtifacts removes the backup file from disk. }
func (s *script) removeArtifacts() error { if err := os.Symlink(name, symlink); err != nil {
_, err := os.Stat(s.file) return fmt.Errorf("copyBackup: error creating latest symlink: %w", err)
if err != nil { }
if os.IsNotExist(err) { s.logger.Infof("Created/Updated symlink `%s` for latest backup.", s.c.BackupLatestSymlink)
return nil
} }
return fmt.Errorf("removeArtifacts: error calling stat on file %s: %w", s.file, err)
} }
if err := os.Remove(s.file); err != nil {
return fmt.Errorf("removeArtifacts: error removing file %s: %w", s.file, err)
}
s.logger.Infof("Removed local artifacts %s.", s.file)
return nil return nil
} }
@@ -477,15 +536,35 @@ func (s *script) pruneOldBackups() error {
} }
if _, err := os.Stat(s.c.BackupArchive); !os.IsNotExist(err) { if _, err := os.Stat(s.c.BackupArchive); !os.IsNotExist(err) {
candidates, err := filepath.Glob( globPattern := path.Join(
path.Join(s.c.BackupArchive, fmt.Sprintf("%s*", s.c.BackupPruningPrefix)), s.c.BackupArchive,
fmt.Sprintf("%s*", s.c.BackupPruningPrefix),
) )
globMatches, err := filepath.Glob(globPattern)
if err != nil { if err != nil {
return fmt.Errorf( return fmt.Errorf(
"pruneOldBackups: error looking up matching files, starting with: %w", err, "pruneOldBackups: error looking up matching files using pattern %s: %w",
globPattern,
err,
) )
} }
var candidates []string
for _, candidate := range globMatches {
fi, err := os.Lstat(candidate)
if err != nil {
return fmt.Errorf(
"pruneOldBackups: error calling Lstat on file %s: %w",
candidate,
err,
)
}
if fi.Mode()&os.ModeSymlink != os.ModeSymlink {
candidates = append(candidates, candidate)
}
}
var matches []string var matches []string
for _, candidate := range candidates { for _, candidate := range candidates {
fi, err := os.Stat(candidate) fi, err := os.Stat(candidate)
@@ -496,7 +575,6 @@ func (s *script) pruneOldBackups() error {
err, err,
) )
} }
if fi.ModTime().Before(deadline) { if fi.ModTime().Before(deadline) {
matches = append(matches, candidate) matches = append(matches, candidate)
} }
@@ -504,8 +582,8 @@ func (s *script) pruneOldBackups() error {
if len(matches) != 0 && len(matches) != len(candidates) { if len(matches) != 0 && len(matches) != len(candidates) {
var removeErrors []error var removeErrors []error
for _, candidate := range matches { for _, match := range matches {
if err := os.Remove(candidate); err != nil { if err := os.Remove(match); err != nil {
removeErrors = append(removeErrors, err) removeErrors = append(removeErrors, err)
} }
} }
@@ -536,16 +614,18 @@ func (s *script) pruneOldBackups() error {
} }
// runHooks runs all hooks that have been registered using the // runHooks runs all hooks that have been registered using the
// given level. In case executing a hook returns an error, the following // given levels in the defined ordering. In case executing a hook returns an
// hooks will still be run before the function returns an error. // error, the following hooks will still be run before the function returns.
func (s *script) runHooks(err error, targetLevel string) error { func (s *script) runHooks(err error, levels ...hookLevel) error {
var actionErrors []error var actionErrors []error
for _, hook := range s.hooks { for _, level := range levels {
if hook.level != targetLevel { for _, hook := range s.hooks {
continue if hook.level != level {
} continue
if err := hook.action(err, s.start, s.output.String()); err != nil { }
actionErrors = append(actionErrors, err) if actionErr := hook.action(err, s.start, s.output.String()); actionErr != nil {
actionErrors = append(actionErrors, fmt.Errorf("runHooks: error running hook: %w", actionErr))
}
} }
} }
if len(actionErrors) != 0 { if len(actionErrors) != 0 {
@@ -555,18 +635,34 @@ func (s *script) runHooks(err error, targetLevel string) error {
} }
// must exits the script run prematurely in case the given error // must exits the script run prematurely in case the given error
// is non-nil. If failure hooks have been registered on the script object, they // is non-nil.
// will be called, passing the failure and previous log output.
func (s *script) must(err error) { func (s *script) must(err error) {
if err != nil { if err != nil {
s.logger.Errorf("Fatal error running backup: %s", err) s.logger.Errorf("Fatal error running backup: %s", err)
if hookErr := s.runHooks(err, hookLevelFailure); hookErr != nil { panic(err)
s.logger.Errorf("An error occurred calling the registered failure hooks: %s", hookErr)
}
panic(errors.New(msgBackupFailed))
} }
} }
// remove removes the given file or directory from disk.
func remove(location string) error {
fi, err := os.Lstat(location)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("remove: error checking for existence of `%s`: %w", location, err)
}
if fi.IsDir() {
err = os.RemoveAll(location)
} else {
err = os.Remove(location)
}
if err != nil {
return fmt.Errorf("remove: error removing `%s`: %w", location, err)
}
return nil
}
// lock opens a lockfile at the given location, keeping it locked until the // lock opens a lockfile at the given location, keeping it locked until the
// caller invokes the returned release func. When invoked while the file is // caller invokes the returned release func. When invoked while the file is
// still locked the function panics. // still locked the function panics.
@@ -583,7 +679,7 @@ func lock(lockfile string) func() error {
} }
// copy creates a copy of the file located at `dst` at `src`. // copy creates a copy of the file located at `dst` at `src`.
func copy(src, dst string) error { func copyFile(src, dst string) error {
in, err := os.Open(src) in, err := os.Open(src)
if err != nil { if err != nil {
return err return err
@@ -640,10 +736,13 @@ func (b *bufferingWriter) Write(p []byte) (n int, err error) {
// hook contains a queued action that can be trigger them when the script // hook contains a queued action that can be trigger them when the script
// reaches a certain point (e.g. unsuccessful backup) // reaches a certain point (e.g. unsuccessful backup)
type hook struct { type hook struct {
level string level hookLevel
action func(err error, start time.Time, logOutput string) error action func(err error, start time.Time, logOutput string) error
} }
type hookLevel int
const ( const (
hookLevelFailure = "failure" hookLevelFailure hookLevel = iota
hookLevelCleanup
) )

4
go.mod
View File

@@ -4,11 +4,13 @@ go 1.17
require ( require (
github.com/docker/docker v20.10.8+incompatible github.com/docker/docker v20.10.8+incompatible
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
github.com/gofrs/flock v0.8.1 github.com/gofrs/flock v0.8.1
github.com/kelseyhightower/envconfig v1.4.0 github.com/kelseyhightower/envconfig v1.4.0
github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d
github.com/m90/targz v0.0.0-20210904082215-2e9a4529a615 github.com/m90/targz v0.0.0-20210904082215-2e9a4529a615
github.com/minio/minio-go/v7 v7.0.12 github.com/minio/minio-go/v7 v7.0.12
github.com/otiai10/copy v1.6.0
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
) )
@@ -20,7 +22,6 @@ require (
github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.0 // indirect github.com/golang/protobuf v1.5.0 // indirect
github.com/google/uuid v1.2.0 // indirect github.com/google/uuid v1.2.0 // indirect
@@ -43,5 +44,6 @@ require (
google.golang.org/grpc v1.33.2 // indirect google.golang.org/grpc v1.33.2 // indirect
google.golang.org/protobuf v1.26.0 // indirect google.golang.org/protobuf v1.26.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
gopkg.in/ini.v1 v1.57.0 // indirect gopkg.in/ini.v1 v1.57.0 // indirect
) )

9
go.sum
View File

@@ -494,6 +494,13 @@ github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mo
github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE=
github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo=
github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8=
github.com/otiai10/copy v1.6.0 h1:IinKAryFFuPONZ7cm6T6E2QX/vcJwSnlaA5lfoaXIiQ=
github.com/otiai10/copy v1.6.0/go.mod h1:XWfuS3CrI0R6IE0FbgHsEazaXO8G0LpMp9o8tos0x4E=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.2 h1:VYWnrP5fXmz1MXvjuUvcBrXSjGE6xjON+axB/UrpO3E=
github.com/otiai10/mint v1.3.2/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
@@ -921,6 +928,8 @@ gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qS
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=

View File

@@ -23,7 +23,6 @@ docker exec minio mkdir -p /data/backup
docker run -d \ docker run -d \
--name offen \ --name offen \
--network test_network \ --network test_network \
--label "docker-volume-backup.stop-during-backup=true" \
-v app_data:/var/opt/offen/ \ -v app_data:/var/opt/offen/ \
offen/offen:latest offen/offen:latest
@@ -39,6 +38,7 @@ docker run --rm \
--env AWS_ENDPOINT_PROTO=http \ --env AWS_ENDPOINT_PROTO=http \
--env AWS_S3_BUCKET_NAME=backup \ --env AWS_S3_BUCKET_NAME=backup \
--env BACKUP_FILENAME=test.tar.gz \ --env BACKUP_FILENAME=test.tar.gz \
--env "BACKUP_FROM_SNAPSHOT=true" \
--entrypoint backup \ --entrypoint backup \
offen/docker-volume-backup:$TEST_VERSION offen/docker-volume-backup:$TEST_VERSION
@@ -56,6 +56,6 @@ fi
echo "[TEST:PASS] All containers running post backup." echo "[TEST:PASS] All containers running post backup."
docker rm $(docker stop minio offen backup) docker rm $(docker stop minio offen)
docker volume rm backup_data app_data docker volume rm backup_data app_data
docker network rm test_network docker network rm test_network

View File

@@ -24,6 +24,7 @@ services:
AWS_ENDPOINT_PROTO: http AWS_ENDPOINT_PROTO: http
AWS_S3_BUCKET_NAME: backup AWS_S3_BUCKET_NAME: backup
BACKUP_FILENAME: test.tar.gz BACKUP_FILENAME: test.tar.gz
BACKUP_LATEST_SYMLINK: test.latest.tar.gz.gpg
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}
BACKUP_PRUNING_LEEWAY: 5s BACKUP_PRUNING_LEEWAY: 5s

View File

@@ -18,6 +18,7 @@ docker run --rm -it \
echo "[TEST:PASS] Found relevant files in untared remote backup." echo "[TEST:PASS] Found relevant files in untared remote backup."
test -L ./local/test.latest.tar.gz.gpg
echo 1234secret | gpg -d --yes --passphrase-fd 0 ./local/test.tar.gz.gpg > ./local/decrypted.tar.gz echo 1234secret | gpg -d --yes --passphrase-fd 0 ./local/test.tar.gz.gpg > ./local/decrypted.tar.gz
tar -xf ./local/decrypted.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db tar -xf ./local/decrypted.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db
rm ./local/decrypted.tar.gz rm ./local/decrypted.tar.gz