mirror of
https://github.com/offen/docker-volume-backup.git
synced 2025-12-05 17:18:02 +01:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
210c7d4540 | ||
|
|
3c06bf8102 | ||
|
|
411c39ee72 | ||
|
|
0c666d0c88 | ||
|
|
a0402b407d | ||
|
|
3193e88fc0 | ||
|
|
c391230be6 | ||
|
|
f946f36fb0 | ||
|
|
5245b5882f | ||
|
|
7f0f173115 | ||
|
|
ad7ec58322 | ||
|
|
b7ab2fbacc | ||
|
|
789fc656e8 | ||
|
|
c59b40f2df | ||
|
|
cff418e735 |
44
README.md
44
README.md
@@ -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)
|
||||
- [Encrypting your backup using GPG](#encrypting-your-backup-using-gpg)
|
||||
- [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)
|
||||
- [Manually triggering a backup](#manually-triggering-a-backup)
|
||||
- [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"
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
# 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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
### 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
|
||||
|
||||
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
|
||||
backup:
|
||||
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:
|
||||
- data:/backup/my-app-backup:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
@@ -574,7 +612,7 @@ volumes:
|
||||
|
||||
## 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.
|
||||
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.
|
||||
- `arm64` and `arm/v7` architectures are supported.
|
||||
- Docker in Swarm mode is supported.
|
||||
- Notifications on failed backups are supported
|
||||
- IAM authentication through instance profiles is supported
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/m90/targz"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/otiai10/copy"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/openpgp"
|
||||
)
|
||||
@@ -40,16 +41,31 @@ func main() {
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
if e, ok := err.(error); ok && strings.Contains(e.Error(), msgBackupFailed) {
|
||||
if pArg := recover(); pArg != nil {
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
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() {
|
||||
s.must(restartContainers())
|
||||
}()
|
||||
@@ -59,16 +75,9 @@ func main() {
|
||||
return s.takeBackup()
|
||||
}())
|
||||
|
||||
s.must(func() error {
|
||||
defer func() {
|
||||
s.must(s.removeArtifacts())
|
||||
}()
|
||||
s.must(s.encryptBackup())
|
||||
return s.copyBackup()
|
||||
}())
|
||||
|
||||
s.must(s.encryptBackup())
|
||||
s.must(s.copyBackup())
|
||||
s.must(s.pruneOldBackups())
|
||||
s.logger.Info("Finished running backup tasks.")
|
||||
}
|
||||
|
||||
// script holds all the stateful information required to orchestrate a
|
||||
@@ -89,11 +98,13 @@ type script struct {
|
||||
type config struct {
|
||||
BackupSources string `split_words:"true" default:"/backup"`
|
||||
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"`
|
||||
BackupRetentionDays int32 `split_words:"true" default:"-1"`
|
||||
BackupPruningLeeway time.Duration `split_words:"true" default:"1m"`
|
||||
BackupPruningPrefix string `split_words:"true"`
|
||||
BackupStopContainerLabel string `split_words:"true" default:"true"`
|
||||
BackupFromSnapshot bool `split_words:"true"`
|
||||
AwsS3BucketName string `split_words:"true"`
|
||||
AwsEndpoint string `split_words:"true" default:"s3.amazonaws.com"`
|
||||
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")
|
||||
}
|
||||
|
||||
mc, err := minio.New(s.c.AwsEndpoint, &minio.Options{
|
||||
options := minio.Options{
|
||||
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 {
|
||||
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 }
|
||||
|
||||
// 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
|
||||
// stopped during the backup and returns a function that can be called to
|
||||
// restart everything that has been stopped.
|
||||
@@ -314,10 +345,37 @@ func (s *script) stopContainers() (func() error, error) {
|
||||
// saves it to disk.
|
||||
func (s *script) takeBackup() error {
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -328,9 +386,16 @@ func (s *script) encryptBackup() error {
|
||||
if s.c.GpgPassphrase == "" {
|
||||
return nil
|
||||
}
|
||||
defer os.Remove(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)
|
||||
defer outFile.Close()
|
||||
if err != nil {
|
||||
@@ -349,7 +414,7 @@ func (s *script) encryptBackup() error {
|
||||
|
||||
src, err := os.Open(s.file)
|
||||
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 {
|
||||
@@ -376,27 +441,21 @@ func (s *script) copyBackup() error {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
s.logger.Infof("Stored copy of backup `%s` in local archive `%s`.", s.file, s.c.BackupArchive)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeArtifacts removes the backup file from disk.
|
||||
func (s *script) removeArtifacts() error {
|
||||
_, err := os.Stat(s.file)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
if s.c.BackupLatestSymlink != "" {
|
||||
symlink := path.Join(s.c.BackupArchive, s.c.BackupLatestSymlink)
|
||||
if _, err := os.Lstat(symlink); err == nil {
|
||||
os.Remove(symlink)
|
||||
}
|
||||
if err := os.Symlink(name, symlink); err != nil {
|
||||
return fmt.Errorf("copyBackup: error creating latest symlink: %w", err)
|
||||
}
|
||||
s.logger.Infof("Created/Updated symlink `%s` for latest backup.", s.c.BackupLatestSymlink)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -477,15 +536,35 @@ func (s *script) pruneOldBackups() error {
|
||||
}
|
||||
|
||||
if _, err := os.Stat(s.c.BackupArchive); !os.IsNotExist(err) {
|
||||
candidates, err := filepath.Glob(
|
||||
path.Join(s.c.BackupArchive, fmt.Sprintf("%s*", s.c.BackupPruningPrefix)),
|
||||
globPattern := path.Join(
|
||||
s.c.BackupArchive,
|
||||
fmt.Sprintf("%s*", s.c.BackupPruningPrefix),
|
||||
)
|
||||
globMatches, err := filepath.Glob(globPattern)
|
||||
if err != nil {
|
||||
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
|
||||
for _, candidate := range candidates {
|
||||
fi, err := os.Stat(candidate)
|
||||
@@ -496,7 +575,6 @@ func (s *script) pruneOldBackups() error {
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
if fi.ModTime().Before(deadline) {
|
||||
matches = append(matches, candidate)
|
||||
}
|
||||
@@ -504,8 +582,8 @@ func (s *script) pruneOldBackups() error {
|
||||
|
||||
if len(matches) != 0 && len(matches) != len(candidates) {
|
||||
var removeErrors []error
|
||||
for _, candidate := range matches {
|
||||
if err := os.Remove(candidate); err != nil {
|
||||
for _, match := range matches {
|
||||
if err := os.Remove(match); err != nil {
|
||||
removeErrors = append(removeErrors, err)
|
||||
}
|
||||
}
|
||||
@@ -536,16 +614,18 @@ func (s *script) pruneOldBackups() error {
|
||||
}
|
||||
|
||||
// runHooks runs all hooks that have been registered using the
|
||||
// given level. In case executing a hook returns an error, the following
|
||||
// hooks will still be run before the function returns an error.
|
||||
func (s *script) runHooks(err error, targetLevel string) error {
|
||||
// given levels in the defined ordering. In case executing a hook returns an
|
||||
// error, the following hooks will still be run before the function returns.
|
||||
func (s *script) runHooks(err error, levels ...hookLevel) error {
|
||||
var actionErrors []error
|
||||
for _, hook := range s.hooks {
|
||||
if hook.level != targetLevel {
|
||||
continue
|
||||
}
|
||||
if err := hook.action(err, s.start, s.output.String()); err != nil {
|
||||
actionErrors = append(actionErrors, err)
|
||||
for _, level := range levels {
|
||||
for _, hook := range s.hooks {
|
||||
if hook.level != level {
|
||||
continue
|
||||
}
|
||||
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 {
|
||||
@@ -555,18 +635,34 @@ func (s *script) runHooks(err error, targetLevel string) 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
|
||||
// will be called, passing the failure and previous log output.
|
||||
// is non-nil.
|
||||
func (s *script) must(err error) {
|
||||
if err != nil {
|
||||
s.logger.Errorf("Fatal error running backup: %s", err)
|
||||
if hookErr := s.runHooks(err, hookLevelFailure); hookErr != nil {
|
||||
s.logger.Errorf("An error occurred calling the registered failure hooks: %s", hookErr)
|
||||
}
|
||||
panic(errors.New(msgBackupFailed))
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// caller invokes the returned release func. When invoked while the file is
|
||||
// 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`.
|
||||
func copy(src, dst string) error {
|
||||
func copyFile(src, dst string) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
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
|
||||
// reaches a certain point (e.g. unsuccessful backup)
|
||||
type hook struct {
|
||||
level string
|
||||
level hookLevel
|
||||
action func(err error, start time.Time, logOutput string) error
|
||||
}
|
||||
|
||||
type hookLevel int
|
||||
|
||||
const (
|
||||
hookLevelFailure = "failure"
|
||||
hookLevelFailure hookLevel = iota
|
||||
hookLevelCleanup
|
||||
)
|
||||
|
||||
4
go.mod
4
go.mod
@@ -4,11 +4,13 @@ go 1.17
|
||||
|
||||
require (
|
||||
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/kelseyhightower/envconfig v1.4.0
|
||||
github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d
|
||||
github.com/m90/targz v0.0.0-20210904082215-2e9a4529a615
|
||||
github.com/minio/minio-go/v7 v7.0.12
|
||||
github.com/otiai10/copy v1.6.0
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
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-units v0.4.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/golang/protobuf v1.5.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/protobuf v1.26.0 // 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
|
||||
)
|
||||
|
||||
9
go.sum
9
go.sum
@@ -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.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo=
|
||||
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.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
||||
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/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/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/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
|
||||
@@ -23,7 +23,6 @@ docker exec minio mkdir -p /data/backup
|
||||
docker run -d \
|
||||
--name offen \
|
||||
--network test_network \
|
||||
--label "docker-volume-backup.stop-during-backup=true" \
|
||||
-v app_data:/var/opt/offen/ \
|
||||
offen/offen:latest
|
||||
|
||||
@@ -39,6 +38,7 @@ docker run --rm \
|
||||
--env AWS_ENDPOINT_PROTO=http \
|
||||
--env AWS_S3_BUCKET_NAME=backup \
|
||||
--env BACKUP_FILENAME=test.tar.gz \
|
||||
--env "BACKUP_FROM_SNAPSHOT=true" \
|
||||
--entrypoint backup \
|
||||
offen/docker-volume-backup:$TEST_VERSION
|
||||
|
||||
@@ -56,6 +56,6 @@ fi
|
||||
|
||||
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 network rm test_network
|
||||
|
||||
@@ -24,6 +24,7 @@ services:
|
||||
AWS_ENDPOINT_PROTO: http
|
||||
AWS_S3_BUCKET_NAME: backup
|
||||
BACKUP_FILENAME: test.tar.gz
|
||||
BACKUP_LATEST_SYMLINK: test.latest.tar.gz.gpg
|
||||
BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ?
|
||||
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7}
|
||||
BACKUP_PRUNING_LEEWAY: 5s
|
||||
|
||||
@@ -18,6 +18,7 @@ docker run --rm -it \
|
||||
|
||||
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
|
||||
tar -xf ./local/decrypted.tar.gz -C /tmp && test -f /tmp/backup/app_data/offen.db
|
||||
rm ./local/decrypted.tar.gz
|
||||
|
||||
Reference in New Issue
Block a user