mirror of
https://github.com/offen/docker-volume-backup.git
synced 2025-12-05 17:18:02 +01:00
Compare commits
8 Commits
v2.0.0-alp
...
v2.0.0-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6034e6a902 | ||
|
|
d0eca0a179 | ||
|
|
a0fe2cf42d | ||
|
|
5334ff1a5a | ||
|
|
e73256ad70 | ||
|
|
e0c4adc563 | ||
|
|
2469597848 | ||
|
|
b1c4bee85d |
22
NOTICE
22
NOTICE
@@ -1,22 +0,0 @@
|
|||||||
Copyright 2021 Offen Authors <hioffen@posteo.de>
|
|
||||||
|
|
||||||
This project contains software that is Copyright (c) 2018 Futurice
|
|
||||||
Licensed under the Terms of the MIT License:
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Backup Docker volumes locally or to any S3 compatible storage.
|
Backup Docker volumes locally or to any S3 compatible storage.
|
||||||
|
|
||||||
The [offen/docker-volume-backup](https://hub.docker.com/r/offen/docker-volume-backup) Docker image can be used as a lightweight (below 15MB) sidecar container to an existing Docker setup. It handles recurring backups of Docker volumes to a local directory or any S3 compatible storage (or both), and rotates away old backups if configured.
|
The [offen/docker-volume-backup](https://hub.docker.com/r/offen/docker-volume-backup) Docker image can be used as a lightweight (below 15MB) sidecar container to an existing Docker setup. It handles __recurring or one-off backups of Docker volumes__ to a __local directory__ or __any S3 compatible storage__ (or both), and __rotates away old backups__ if configured. It also supports __encrypting your backups using GPG__.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
|||||||
@@ -4,25 +4,22 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/joho/godotenv"
|
"github.com/gofrs/flock"
|
||||||
|
"github.com/kelseyhightower/envconfig"
|
||||||
"github.com/leekchan/timeutil"
|
"github.com/leekchan/timeutil"
|
||||||
minio "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/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/walle/targz"
|
"github.com/walle/targz"
|
||||||
@@ -30,15 +27,28 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
unlock := lock("/var/dockervolumebackup.lock")
|
unlock := lock("/var/lock/dockervolumebackup.lock")
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
s := &script{}
|
s, err := newScript()
|
||||||
s.must(s.init())
|
if err != nil {
|
||||||
s.must(s.stopContainersAndRun(s.takeBackup))
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.must(func() error {
|
||||||
|
restartContainers, err := s.stopContainers()
|
||||||
|
defer func() {
|
||||||
|
s.must(restartContainers())
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.takeBackup()
|
||||||
|
}())
|
||||||
|
|
||||||
s.must(s.encryptBackup())
|
s.must(s.encryptBackup())
|
||||||
s.must(s.copyBackup())
|
s.must(s.copyBackup())
|
||||||
s.must(s.cleanBackup())
|
s.must(s.removeArtifacts())
|
||||||
s.must(s.pruneOldBackups())
|
s.must(s.pruneOldBackups())
|
||||||
s.logger.Info("Finished running backup tasks.")
|
s.logger.Info("Finished running backup tasks.")
|
||||||
}
|
}
|
||||||
@@ -52,105 +62,98 @@ type script struct {
|
|||||||
logger *logrus.Logger
|
logger *logrus.Logger
|
||||||
|
|
||||||
start time.Time
|
start time.Time
|
||||||
|
file string
|
||||||
|
|
||||||
file string
|
c *config
|
||||||
bucket string
|
|
||||||
archive string
|
|
||||||
sources string
|
|
||||||
passphrase []byte
|
|
||||||
retentionDays *int
|
|
||||||
leeway *time.Duration
|
|
||||||
containerLabel string
|
|
||||||
pruningPrefix string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// init creates all resources needed for the script to perform actions against
|
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"`
|
||||||
|
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"`
|
||||||
|
AwsS3BucketName string `split_words:"true"`
|
||||||
|
AwsEndpoint string `split_words:"true" default:"s3.amazonaws.com"`
|
||||||
|
AwsEndpointProto string `split_words:"true" default:"https"`
|
||||||
|
AwsEndpointInsecure bool `split_words:"true"`
|
||||||
|
AwsAccessKeyID string `envconfig:"AWS_ACCESS_KEY_ID"`
|
||||||
|
AwsSecretAccessKey string `split_words:"true"`
|
||||||
|
GpgPassphrase string `split_words:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// newScript creates all resources needed for the script to perform actions against
|
||||||
// remote resources like the Docker engine or remote storage locations. All
|
// remote resources like the Docker engine or remote storage locations. All
|
||||||
// reading from env vars or other configuration sources is expected to happen
|
// reading from env vars or other configuration sources is expected to happen
|
||||||
// in this method.
|
// in this method.
|
||||||
func (s *script) init() error {
|
func newScript() (*script, error) {
|
||||||
s.ctx = context.Background()
|
s := &script{
|
||||||
s.logger = logrus.New()
|
c: &config{},
|
||||||
s.logger.SetOutput(os.Stdout)
|
ctx: context.Background(),
|
||||||
|
logger: &logrus.Logger{
|
||||||
if err := godotenv.Load("/etc/backup.env"); err != nil {
|
Out: os.Stdout,
|
||||||
return fmt.Errorf("init: failed to load env file: %w", err)
|
Formatter: new(logrus.TextFormatter),
|
||||||
|
Hooks: make(logrus.LevelHooks),
|
||||||
|
Level: logrus.InfoLevel,
|
||||||
|
},
|
||||||
|
start: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := envconfig.Process("", s.c); err != nil {
|
||||||
|
return nil, fmt.Errorf("newScript: failed to process configuration values: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.file = path.Join("/tmp", s.c.BackupFilename)
|
||||||
|
|
||||||
_, err := os.Stat("/var/run/docker.sock")
|
_, err := os.Stat("/var/run/docker.sock")
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("init: failed to create docker client")
|
return nil, fmt.Errorf("newScript: failed to create docker client")
|
||||||
}
|
}
|
||||||
s.cli = cli
|
s.cli = cli
|
||||||
}
|
}
|
||||||
|
|
||||||
if bucket := os.Getenv("AWS_S3_BUCKET_NAME"); bucket != "" {
|
if s.c.AwsS3BucketName != "" {
|
||||||
s.bucket = bucket
|
mc, err := minio.New(s.c.AwsEndpoint, &minio.Options{
|
||||||
mc, err := minio.New(os.Getenv("AWS_ENDPOINT"), &minio.Options{
|
|
||||||
Creds: credentials.NewStaticV4(
|
Creds: credentials.NewStaticV4(
|
||||||
os.Getenv("AWS_ACCESS_KEY_ID"),
|
s.c.AwsAccessKeyID,
|
||||||
os.Getenv("AWS_SECRET_ACCESS_KEY"),
|
s.c.AwsSecretAccessKey,
|
||||||
"",
|
"",
|
||||||
),
|
),
|
||||||
Secure: os.Getenv("AWS_ENDPOINT_INSECURE") == "" && os.Getenv("AWS_ENDPOINT_PROTO") == "https",
|
Secure: !s.c.AwsEndpointInsecure && s.c.AwsEndpointProto == "https",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("init: error setting up minio client: %w", err)
|
return nil, fmt.Errorf("newScript: error setting up minio client: %w", err)
|
||||||
}
|
}
|
||||||
s.mc = mc
|
s.mc = mc
|
||||||
}
|
}
|
||||||
|
|
||||||
file := os.Getenv("BACKUP_FILENAME")
|
return s, nil
|
||||||
if file == "" {
|
|
||||||
return errors.New("init: BACKUP_FILENAME not given")
|
|
||||||
}
|
|
||||||
s.file = path.Join("/tmp", file)
|
|
||||||
s.archive = os.Getenv("BACKUP_ARCHIVE")
|
|
||||||
s.sources = os.Getenv("BACKUP_SOURCES")
|
|
||||||
if v := os.Getenv("GPG_PASSPHRASE"); v != "" {
|
|
||||||
s.passphrase = []byte(v)
|
|
||||||
}
|
|
||||||
if v := os.Getenv("BACKUP_RETENTION_DAYS"); v != "" {
|
|
||||||
i, err := strconv.Atoi(v)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("init: error parsing BACKUP_RETENTION_DAYS as int: %w", err)
|
|
||||||
}
|
|
||||||
s.retentionDays = &i
|
|
||||||
}
|
|
||||||
if v := os.Getenv("BACKUP_PRUNING_LEEWAY"); v != "" {
|
|
||||||
d, err := time.ParseDuration(v)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("init: error parsing BACKUP_PRUNING_LEEWAY as duration: %w", err)
|
|
||||||
}
|
|
||||||
s.leeway = &d
|
|
||||||
}
|
|
||||||
s.containerLabel = os.Getenv("BACKUP_STOP_CONTAINER_LABEL")
|
|
||||||
s.pruningPrefix = os.Getenv("BACKUP_PRUNING_PREFIX")
|
|
||||||
s.start = time.Now()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// stopContainersAndRun stops all Docker containers that are marked as to being
|
var noop = func() error { return nil }
|
||||||
// stopped during the backup and runs the given thunk. After returning, it makes
|
|
||||||
// sure containers are being restarted if required.
|
// stopContainers stops all Docker containers that are marked as to being
|
||||||
func (s *script) stopContainersAndRun(thunk func() error) error {
|
// stopped during the backup and returns a function that can be called to
|
||||||
|
// restart everything that has been stopped.
|
||||||
|
func (s *script) stopContainers() (func() error, error) {
|
||||||
if s.cli == nil {
|
if s.cli == nil {
|
||||||
return thunk()
|
return noop, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
allContainers, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{
|
allContainers, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{
|
||||||
Quiet: true,
|
Quiet: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stopContainersAndRun: error querying for containers: %w", err)
|
return noop, fmt.Errorf("stopContainersAndRun: error querying for containers: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
containerLabel := fmt.Sprintf(
|
containerLabel := fmt.Sprintf(
|
||||||
"docker-volume-backup.stop-during-backup=%s",
|
"docker-volume-backup.stop-during-backup=%s",
|
||||||
s.containerLabel,
|
s.c.BackupStopContainerLabel,
|
||||||
)
|
)
|
||||||
containersToStop, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{
|
containersToStop, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{
|
||||||
Quiet: true,
|
Quiet: true,
|
||||||
@@ -161,11 +164,11 @@ func (s *script) stopContainersAndRun(thunk func() error) error {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stopContainersAndRun: error querying for containers to stop: %w", err)
|
return noop, fmt.Errorf("stopContainersAndRun: error querying for containers to stop: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(containersToStop) == 0 {
|
if len(containersToStop) == 0 {
|
||||||
return thunk()
|
return noop, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Infof(
|
s.logger.Infof(
|
||||||
@@ -185,7 +188,15 @@ func (s *script) stopContainersAndRun(thunk func() error) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() error {
|
if len(stopErrors) != 0 {
|
||||||
|
return noop, fmt.Errorf(
|
||||||
|
"stopContainersAndRun: %d error(s) stopping containers: %w",
|
||||||
|
len(stopErrors),
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() error {
|
||||||
servicesRequiringUpdate := map[string]struct{}{}
|
servicesRequiringUpdate := map[string]struct{}{}
|
||||||
|
|
||||||
var restartErrors []error
|
var restartErrors []error
|
||||||
@@ -210,7 +221,7 @@ func (s *script) stopContainersAndRun(thunk func() error) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if serviceMatch.ID == "" {
|
if serviceMatch.ID == "" {
|
||||||
return fmt.Errorf("stopContainersAndRun: Couldn't find service with name %s", serviceName)
|
return fmt.Errorf("stopContainersAndRun: couldn't find service with name %s", serviceName)
|
||||||
}
|
}
|
||||||
serviceMatch.Spec.TaskTemplate.ForceUpdate = 1
|
serviceMatch.Spec.TaskTemplate.ForceUpdate = 1
|
||||||
_, err := s.cli.ServiceUpdate(
|
_, err := s.cli.ServiceUpdate(
|
||||||
@@ -230,29 +241,22 @@ func (s *script) stopContainersAndRun(thunk func() error) error {
|
|||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
s.logger.Infof("Restarted %d container(s) and the matching service(s).", len(stoppedContainers))
|
s.logger.Infof(
|
||||||
return nil
|
"Restarted %d container(s) and the matching service(s).",
|
||||||
}()
|
len(stoppedContainers),
|
||||||
|
|
||||||
if len(stopErrors) != 0 {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"stopContainersAndRun: %d error(s) stopping containers: %w",
|
|
||||||
len(stopErrors),
|
|
||||||
err,
|
|
||||||
)
|
)
|
||||||
}
|
return nil
|
||||||
|
}, nil
|
||||||
return thunk()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// takeBackup creates a tar archive of the configured backup location and
|
// takeBackup creates a tar archive of the configured backup location and
|
||||||
// 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.sources, s.file); err != nil {
|
if err := targz.Compress(s.c.BackupSources, s.file); 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.sources, s.file)
|
s.logger.Infof("Created backup of `%s` at `%s`.", s.c.BackupSources, s.file)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,35 +264,35 @@ func (s *script) takeBackup() error {
|
|||||||
// In case no passphrase is given it returns early, leaving the backup file
|
// In case no passphrase is given it returns early, leaving the backup file
|
||||||
// untouched.
|
// untouched.
|
||||||
func (s *script) encryptBackup() error {
|
func (s *script) encryptBackup() error {
|
||||||
if s.passphrase == nil {
|
if s.c.GpgPassphrase == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
defer os.Remove(s.file)
|
||||||
|
|
||||||
|
gpgFile := fmt.Sprintf("%s.gpg", s.file)
|
||||||
|
outFile, err := os.Create(gpgFile)
|
||||||
|
defer outFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encryptBackup: error opening out file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
output := bytes.NewBuffer(nil)
|
|
||||||
_, name := path.Split(s.file)
|
_, name := path.Split(s.file)
|
||||||
|
dst, err := openpgp.SymmetricallyEncrypt(outFile, []byte(s.c.GpgPassphrase), &openpgp.FileHints{
|
||||||
pt, err := openpgp.SymmetricallyEncrypt(output, []byte(s.passphrase), &openpgp.FileHints{
|
|
||||||
IsBinary: true,
|
IsBinary: true,
|
||||||
FileName: name,
|
FileName: name,
|
||||||
}, nil)
|
}, nil)
|
||||||
|
defer dst.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("encryptBackup: error encrypting backup file: %w", err)
|
return fmt.Errorf("encryptBackup: error encrypting backup file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := ioutil.ReadFile(s.file)
|
src, err := os.Open(s.file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("encryptBackup: error opening unencrypted backup file: %w", err)
|
return fmt.Errorf("encryptBackup: error opening backup file %s: %w", s.file, err)
|
||||||
}
|
|
||||||
pt.Write(b)
|
|
||||||
pt.Close()
|
|
||||||
|
|
||||||
gpgFile := fmt.Sprintf("%s.gpg", s.file)
|
|
||||||
if err := ioutil.WriteFile(gpgFile, output.Bytes(), os.ModeAppend); err != nil {
|
|
||||||
return fmt.Errorf("encryptBackup: error writing encrypted version of backup: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.Remove(s.file); err != nil {
|
if _, err := io.Copy(dst, src); err != nil {
|
||||||
return fmt.Errorf("encryptBackup: error removing unencrpyted backup: %w", err)
|
return fmt.Errorf("encryptBackup: error writing ciphertext to file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.file = gpgFile
|
s.file = gpgFile
|
||||||
@@ -300,31 +304,31 @@ func (s *script) encryptBackup() error {
|
|||||||
// as per the given configuration.
|
// as per the given configuration.
|
||||||
func (s *script) copyBackup() error {
|
func (s *script) copyBackup() error {
|
||||||
_, name := path.Split(s.file)
|
_, name := path.Split(s.file)
|
||||||
if s.bucket != "" {
|
if s.c.AwsS3BucketName != "" {
|
||||||
_, err := s.mc.FPutObject(s.ctx, s.bucket, name, s.file, minio.PutObjectOptions{
|
_, err := s.mc.FPutObject(s.ctx, s.c.AwsS3BucketName, name, s.file, minio.PutObjectOptions{
|
||||||
ContentType: "application/tar+gzip",
|
ContentType: "application/tar+gzip",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("copyBackup: error uploading backup to remote storage: %w", err)
|
return fmt.Errorf("copyBackup: error uploading backup to remote storage: %w", err)
|
||||||
}
|
}
|
||||||
s.logger.Infof("Uploaded a copy of backup `%s` to bucket `%s`", s.file, s.bucket)
|
s.logger.Infof("Uploaded a copy of backup `%s` to bucket `%s`", s.file, s.c.AwsS3BucketName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(s.archive); !os.IsNotExist(err) {
|
if _, err := os.Stat(s.c.BackupArchive); !os.IsNotExist(err) {
|
||||||
if err := copy(s.file, path.Join(s.archive, name)); err != nil {
|
if err := copy(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.archive)
|
s.logger.Infof("Stored copy of backup `%s` in local archive `%s`", s.file, s.c.BackupArchive)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanBackup removes the backup file from disk.
|
// removeArtifacts removes the backup file from disk.
|
||||||
func (s *script) cleanBackup() error {
|
func (s *script) removeArtifacts() error {
|
||||||
if err := os.Remove(s.file); err != nil {
|
if err := os.Remove(s.file); err != nil {
|
||||||
return fmt.Errorf("cleanBackup: error removing file: %w", err)
|
return fmt.Errorf("removeArtifacts: error removing file: %w", err)
|
||||||
}
|
}
|
||||||
s.logger.Info("Cleaned up local artifacts.")
|
s.logger.Info("Removed local artifacts.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,22 +336,22 @@ func (s *script) cleanBackup() error {
|
|||||||
// the given configuration. In case the given configuration would delete all
|
// the given configuration. In case the given configuration would delete all
|
||||||
// backups, it does nothing instead.
|
// backups, it does nothing instead.
|
||||||
func (s *script) pruneOldBackups() error {
|
func (s *script) pruneOldBackups() error {
|
||||||
if s.retentionDays == nil {
|
if s.c.BackupRetentionDays < 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.leeway != nil {
|
if s.c.BackupPruningLeeway != 0 {
|
||||||
s.logger.Infof("Sleeping for %s before pruning backups.", s.leeway)
|
s.logger.Infof("Sleeping for %s before pruning backups.", s.c.BackupPruningLeeway)
|
||||||
time.Sleep(*s.leeway)
|
time.Sleep(s.c.BackupPruningLeeway)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Infof("Trying to prune backups older than %d day(s) now.", *s.retentionDays)
|
s.logger.Infof("Trying to prune backups older than %d day(s) now.", s.c.BackupRetentionDays)
|
||||||
deadline := s.start.AddDate(0, 0, -*s.retentionDays)
|
deadline := time.Now().AddDate(0, 0, -int(s.c.BackupRetentionDays))
|
||||||
|
|
||||||
if s.bucket != "" {
|
if s.c.AwsS3BucketName != "" {
|
||||||
candidates := s.mc.ListObjects(s.ctx, s.bucket, minio.ListObjectsOptions{
|
candidates := s.mc.ListObjects(s.ctx, s.c.AwsS3BucketName, minio.ListObjectsOptions{
|
||||||
WithMetadata: true,
|
WithMetadata: true,
|
||||||
Prefix: s.pruningPrefix,
|
Prefix: s.c.BackupPruningPrefix,
|
||||||
})
|
})
|
||||||
|
|
||||||
var matches []minio.ObjectInfo
|
var matches []minio.ObjectInfo
|
||||||
@@ -355,7 +359,10 @@ func (s *script) pruneOldBackups() error {
|
|||||||
for candidate := range candidates {
|
for candidate := range candidates {
|
||||||
lenCandidates++
|
lenCandidates++
|
||||||
if candidate.Err != nil {
|
if candidate.Err != nil {
|
||||||
return fmt.Errorf("pruneOldBackups: error looking up candidates from remote storage: %w", candidate.Err)
|
return fmt.Errorf(
|
||||||
|
"pruneOldBackups: error looking up candidates from remote storage: %w",
|
||||||
|
candidate.Err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if candidate.LastModified.Before(deadline) {
|
if candidate.LastModified.Before(deadline) {
|
||||||
matches = append(matches, candidate)
|
matches = append(matches, candidate)
|
||||||
@@ -370,7 +377,7 @@ func (s *script) pruneOldBackups() error {
|
|||||||
}
|
}
|
||||||
close(objectsCh)
|
close(objectsCh)
|
||||||
}()
|
}()
|
||||||
errChan := s.mc.RemoveObjects(s.ctx, s.bucket, objectsCh, minio.RemoveObjectsOptions{})
|
errChan := s.mc.RemoveObjects(s.ctx, s.c.AwsS3BucketName, objectsCh, minio.RemoveObjectsOptions{})
|
||||||
var errors []error
|
var errors []error
|
||||||
for result := range errChan {
|
for result := range errChan {
|
||||||
if result.Err != nil {
|
if result.Err != nil {
|
||||||
@@ -386,23 +393,25 @@ func (s *script) pruneOldBackups() error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
s.logger.Infof(
|
s.logger.Infof(
|
||||||
"Pruned %d out of %d remote backup(s) as their age exceeded the configured retention period.",
|
"Pruned %d out of %d remote backup(s) as their age exceeded the configured retention period of %d days.",
|
||||||
len(matches),
|
len(matches),
|
||||||
lenCandidates,
|
lenCandidates,
|
||||||
|
s.c.BackupRetentionDays,
|
||||||
)
|
)
|
||||||
} else if len(matches) != 0 && len(matches) == lenCandidates {
|
} else if len(matches) != 0 && len(matches) == lenCandidates {
|
||||||
s.logger.Warnf(
|
s.logger.Warnf(
|
||||||
"The current configuration would delete all %d remote backup copies. Refusing to do so, please check your configuration.",
|
"The current configuration would delete all %d remote backup copies.",
|
||||||
len(matches),
|
len(matches),
|
||||||
)
|
)
|
||||||
|
s.logger.Warn("Refusing to do so, please check your configuration.")
|
||||||
} else {
|
} else {
|
||||||
s.logger.Infof("None of %d remote backup(s) were pruned.", lenCandidates)
|
s.logger.Infof("None of %d remote backup(s) were pruned.", lenCandidates)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(s.archive); !os.IsNotExist(err) {
|
if _, err := os.Stat(s.c.BackupArchive); !os.IsNotExist(err) {
|
||||||
candidates, err := filepath.Glob(
|
candidates, err := filepath.Glob(
|
||||||
path.Join(s.archive, fmt.Sprintf("%s*", s.pruningPrefix)),
|
path.Join(s.c.BackupArchive, fmt.Sprintf("%s*", s.c.BackupPruningPrefix)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
@@ -441,15 +450,17 @@ func (s *script) pruneOldBackups() error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
s.logger.Infof(
|
s.logger.Infof(
|
||||||
"Pruned %d out of %d local backup(s) as their age exceeded the configured retention period.",
|
"Pruned %d out of %d local backup(s) as their age exceeded the configured retention period of %d days.",
|
||||||
len(matches),
|
len(matches),
|
||||||
len(candidates),
|
len(candidates),
|
||||||
|
s.c.BackupRetentionDays,
|
||||||
)
|
)
|
||||||
} else if len(matches) != 0 && len(matches) == len(candidates) {
|
} else if len(matches) != 0 && len(matches) == len(candidates) {
|
||||||
s.logger.Warnf(
|
s.logger.Warnf(
|
||||||
"The current configuration would delete all %d local backup copies. Refusing to do so, please check your configuration.",
|
"The current configuration would delete all %d local backup copies.",
|
||||||
len(matches),
|
len(matches),
|
||||||
)
|
)
|
||||||
|
s.logger.Warn("Refusing to do so, please check your configuration.")
|
||||||
} else {
|
} else {
|
||||||
s.logger.Infof("None of %d local backup(s) were pruned.", len(candidates))
|
s.logger.Infof("None of %d local backup(s) were pruned.", len(candidates))
|
||||||
}
|
}
|
||||||
@@ -459,11 +470,7 @@ func (s *script) pruneOldBackups() error {
|
|||||||
|
|
||||||
func (s *script) must(err error) {
|
func (s *script) must(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if s.logger == nil {
|
s.logger.Fatalf("Fatal error running backup: %s", err)
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
s.logger.Errorf("Fatal error running backup: %s", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,19 +478,15 @@ func (s *script) must(err error) {
|
|||||||
// 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.
|
||||||
func lock(lockfile string) func() error {
|
func lock(lockfile string) func() error {
|
||||||
lf, err := os.OpenFile(lockfile, os.O_CREATE|os.O_RDWR, os.ModeAppend)
|
fileLock := flock.New(lockfile)
|
||||||
|
acquired, err := fileLock.TryLock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return func() error {
|
if !acquired {
|
||||||
if err := lf.Close(); err != nil {
|
panic("unable to acquire file lock")
|
||||||
return fmt.Errorf("lock: error releasing file lock: %w", err)
|
|
||||||
}
|
|
||||||
if err := os.Remove(lockfile); err != nil {
|
|
||||||
return fmt.Errorf("lock: error removing lock file: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
return fileLock.Unlock
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy creates a copy of the file located at `dst` at `src`.
|
// copy creates a copy of the file located at `dst` at `src`.
|
||||||
|
|||||||
@@ -3,37 +3,12 @@
|
|||||||
# Copyright 2021 - Offen Authors <hioffen@posteo.de>
|
# Copyright 2021 - Offen Authors <hioffen@posteo.de>
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
# SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
# Portions of this file are taken from github.com/futurice/docker-volume-backup
|
|
||||||
# See NOTICE for information about authors and licensing.
|
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Write cronjob env to file, fill in sensible defaults, and read them back in
|
|
||||||
cat <<EOF > /etc/backup.env
|
|
||||||
BACKUP_SOURCES="${BACKUP_SOURCES:-/backup}"
|
|
||||||
BACKUP_CRON_EXPRESSION="${BACKUP_CRON_EXPRESSION:-@daily}"
|
BACKUP_CRON_EXPRESSION="${BACKUP_CRON_EXPRESSION:-@daily}"
|
||||||
BACKUP_FILENAME="${BACKUP_FILENAME:-backup-%Y-%m-%dT%H-%M-%S.tar.gz}"
|
|
||||||
BACKUP_ARCHIVE="${BACKUP_ARCHIVE:-/archive}"
|
|
||||||
|
|
||||||
BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-}"
|
|
||||||
BACKUP_PRUNING_LEEWAY="${BACKUP_PRUNING_LEEWAY:-1m}"
|
|
||||||
BACKUP_PRUNING_PREFIX="${BACKUP_PRUNING_PREFIX:-}"
|
|
||||||
BACKUP_STOP_CONTAINER_LABEL="${BACKUP_STOP_CONTAINER_LABEL:-true}"
|
|
||||||
|
|
||||||
AWS_S3_BUCKET_NAME="${AWS_S3_BUCKET_NAME:-}"
|
|
||||||
AWS_ENDPOINT="${AWS_ENDPOINT:-s3.amazonaws.com}"
|
|
||||||
AWS_ENDPOINT_PROTO="${AWS_ENDPOINT_PROTO:-https}"
|
|
||||||
AWS_ENDPOINT_INSECURE="${AWS_ENDPOINT_INSECURE:-}"
|
|
||||||
|
|
||||||
GPG_PASSPHRASE="${GPG_PASSPHRASE:-}"
|
|
||||||
EOF
|
|
||||||
chmod a+x /etc/backup.env
|
|
||||||
source /etc/backup.env
|
|
||||||
|
|
||||||
# Add our cron entry, and direct stdout & stderr to Docker commands stdout
|
|
||||||
echo "Installing cron.d entry with expression $BACKUP_CRON_EXPRESSION."
|
echo "Installing cron.d entry with expression $BACKUP_CRON_EXPRESSION."
|
||||||
echo "$BACKUP_CRON_EXPRESSION backup 2>&1" | crontab -
|
echo "$BACKUP_CRON_EXPRESSION backup 2>&1" | crontab -
|
||||||
|
|
||||||
# Let cron take the wheel
|
|
||||||
echo "Starting cron in foreground."
|
echo "Starting cron in foreground."
|
||||||
crond -f -l 8
|
crond -f -l 8
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -4,7 +4,8 @@ go 1.17
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/docker/docker v20.10.8+incompatible
|
github.com/docker/docker v20.10.8+incompatible
|
||||||
github.com/joho/godotenv v1.3.0
|
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/leekchan/timeutil v0.0.0-20150802142658-28917288c48d
|
||||||
github.com/minio/minio-go/v7 v7.0.12
|
github.com/minio/minio-go/v7 v7.0.12
|
||||||
github.com/sirupsen/logrus v1.8.1
|
github.com/sirupsen/logrus v1.8.1
|
||||||
|
|||||||
9
go.sum
9
go.sum
@@ -274,6 +274,8 @@ github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblf
|
|||||||
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
|
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
|
||||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||||
|
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||||
github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU=
|
github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU=
|
||||||
github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=
|
github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
@@ -370,8 +372,6 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
|
|||||||
github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
|
github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
|
||||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
|
||||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
@@ -382,6 +382,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
|
|||||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
@@ -397,9 +399,11 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv
|
|||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
|
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d h1:2puqoOQwi3Ai1oznMOsFIbifm6kIfJaLLyYzWD4IzTs=
|
github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d h1:2puqoOQwi3Ai1oznMOsFIbifm6kIfJaLLyYzWD4IzTs=
|
||||||
github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d/go.mod h1:hO90vCP2x3exaSH58BIAowSKvV+0OsY21TtzuFGHON4=
|
github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d/go.mod h1:hO90vCP2x3exaSH58BIAowSKvV+0OsY21TtzuFGHON4=
|
||||||
@@ -907,6 +911,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
|
|||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- app_data:/var/opt/offen
|
- app_data:/var/opt/offen
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
backup_data:
|
backup_data:
|
||||||
app_data:
|
app_data:
|
||||||
|
|||||||
Reference in New Issue
Block a user