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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -4,25 +4,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/gofrs/flock"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"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/sirupsen/logrus"
|
||||
"github.com/walle/targz"
|
||||
@@ -30,15 +27,28 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
unlock := lock("/var/dockervolumebackup.lock")
|
||||
unlock := lock("/var/lock/dockervolumebackup.lock")
|
||||
defer unlock()
|
||||
|
||||
s := &script{}
|
||||
s.must(s.init())
|
||||
s.must(s.stopContainersAndRun(s.takeBackup))
|
||||
s, err := newScript()
|
||||
if err != nil {
|
||||
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.copyBackup())
|
||||
s.must(s.cleanBackup())
|
||||
s.must(s.removeArtifacts())
|
||||
s.must(s.pruneOldBackups())
|
||||
s.logger.Info("Finished running backup tasks.")
|
||||
}
|
||||
@@ -52,105 +62,98 @@ type script struct {
|
||||
logger *logrus.Logger
|
||||
|
||||
start time.Time
|
||||
|
||||
file string
|
||||
bucket string
|
||||
archive string
|
||||
sources string
|
||||
passphrase []byte
|
||||
retentionDays *int
|
||||
leeway *time.Duration
|
||||
containerLabel string
|
||||
pruningPrefix string
|
||||
|
||||
c *config
|
||||
}
|
||||
|
||||
// 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
|
||||
// reading from env vars or other configuration sources is expected to happen
|
||||
// in this method.
|
||||
func (s *script) init() error {
|
||||
s.ctx = context.Background()
|
||||
s.logger = logrus.New()
|
||||
s.logger.SetOutput(os.Stdout)
|
||||
|
||||
if err := godotenv.Load("/etc/backup.env"); err != nil {
|
||||
return fmt.Errorf("init: failed to load env file: %w", err)
|
||||
func newScript() (*script, error) {
|
||||
s := &script{
|
||||
c: &config{},
|
||||
ctx: context.Background(),
|
||||
logger: &logrus.Logger{
|
||||
Out: os.Stdout,
|
||||
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")
|
||||
if !os.IsNotExist(err) {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
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
|
||||
}
|
||||
|
||||
if bucket := os.Getenv("AWS_S3_BUCKET_NAME"); bucket != "" {
|
||||
s.bucket = bucket
|
||||
mc, err := minio.New(os.Getenv("AWS_ENDPOINT"), &minio.Options{
|
||||
if s.c.AwsS3BucketName != "" {
|
||||
mc, err := minio.New(s.c.AwsEndpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(
|
||||
os.Getenv("AWS_ACCESS_KEY_ID"),
|
||||
os.Getenv("AWS_SECRET_ACCESS_KEY"),
|
||||
s.c.AwsAccessKeyID,
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
file := os.Getenv("BACKUP_FILENAME")
|
||||
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
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// stopContainersAndRun stops all Docker containers that are marked as to being
|
||||
// stopped during the backup and runs the given thunk. After returning, it makes
|
||||
// sure containers are being restarted if required.
|
||||
func (s *script) stopContainersAndRun(thunk func() error) error {
|
||||
var noop = func() error { return nil }
|
||||
|
||||
// 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.
|
||||
func (s *script) stopContainers() (func() error, error) {
|
||||
if s.cli == nil {
|
||||
return thunk()
|
||||
return noop, nil
|
||||
}
|
||||
|
||||
allContainers, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{
|
||||
Quiet: true,
|
||||
})
|
||||
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(
|
||||
"docker-volume-backup.stop-during-backup=%s",
|
||||
s.containerLabel,
|
||||
s.c.BackupStopContainerLabel,
|
||||
)
|
||||
containersToStop, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{
|
||||
Quiet: true,
|
||||
@@ -161,11 +164,11 @@ func (s *script) stopContainersAndRun(thunk func() error) error {
|
||||
})
|
||||
|
||||
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 {
|
||||
return thunk()
|
||||
return noop, nil
|
||||
}
|
||||
|
||||
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{}{}
|
||||
|
||||
var restartErrors []error
|
||||
@@ -210,7 +221,7 @@ func (s *script) stopContainersAndRun(thunk func() error) error {
|
||||
}
|
||||
}
|
||||
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
|
||||
_, err := s.cli.ServiceUpdate(
|
||||
@@ -230,29 +241,22 @@ func (s *script) stopContainersAndRun(thunk func() error) error {
|
||||
err,
|
||||
)
|
||||
}
|
||||
s.logger.Infof("Restarted %d container(s) and the matching service(s).", len(stoppedContainers))
|
||||
return nil
|
||||
}()
|
||||
|
||||
if len(stopErrors) != 0 {
|
||||
return fmt.Errorf(
|
||||
"stopContainersAndRun: %d error(s) stopping containers: %w",
|
||||
len(stopErrors),
|
||||
err,
|
||||
s.logger.Infof(
|
||||
"Restarted %d container(s) and the matching service(s).",
|
||||
len(stoppedContainers),
|
||||
)
|
||||
}
|
||||
|
||||
return thunk()
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// takeBackup creates a tar archive of the configured backup location and
|
||||
// saves it to disk.
|
||||
func (s *script) takeBackup() error {
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -260,35 +264,35 @@ func (s *script) takeBackup() error {
|
||||
// In case no passphrase is given it returns early, leaving the backup file
|
||||
// untouched.
|
||||
func (s *script) encryptBackup() error {
|
||||
if s.passphrase == nil {
|
||||
if s.c.GpgPassphrase == "" {
|
||||
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)
|
||||
|
||||
pt, err := openpgp.SymmetricallyEncrypt(output, []byte(s.passphrase), &openpgp.FileHints{
|
||||
dst, err := openpgp.SymmetricallyEncrypt(outFile, []byte(s.c.GpgPassphrase), &openpgp.FileHints{
|
||||
IsBinary: true,
|
||||
FileName: name,
|
||||
}, nil)
|
||||
defer dst.Close()
|
||||
if err != nil {
|
||||
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 {
|
||||
return fmt.Errorf("encryptBackup: error opening unencrypted backup file: %w", 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)
|
||||
return fmt.Errorf("encryptBackup: error opening backup file %s: %w", s.file, err)
|
||||
}
|
||||
|
||||
if err := os.Remove(s.file); err != nil {
|
||||
return fmt.Errorf("encryptBackup: error removing unencrpyted backup: %w", err)
|
||||
if _, err := io.Copy(dst, src); err != nil {
|
||||
return fmt.Errorf("encryptBackup: error writing ciphertext to file: %w", err)
|
||||
}
|
||||
|
||||
s.file = gpgFile
|
||||
@@ -300,31 +304,31 @@ func (s *script) encryptBackup() error {
|
||||
// as per the given configuration.
|
||||
func (s *script) copyBackup() error {
|
||||
_, name := path.Split(s.file)
|
||||
if s.bucket != "" {
|
||||
_, err := s.mc.FPutObject(s.ctx, s.bucket, name, s.file, minio.PutObjectOptions{
|
||||
if s.c.AwsS3BucketName != "" {
|
||||
_, err := s.mc.FPutObject(s.ctx, s.c.AwsS3BucketName, name, s.file, minio.PutObjectOptions{
|
||||
ContentType: "application/tar+gzip",
|
||||
})
|
||||
if err != nil {
|
||||
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 := copy(s.file, path.Join(s.archive, name)); err != nil {
|
||||
if _, err := os.Stat(s.c.BackupArchive); !os.IsNotExist(err) {
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// cleanBackup removes the backup file from disk.
|
||||
func (s *script) cleanBackup() error {
|
||||
// removeArtifacts removes the backup file from disk.
|
||||
func (s *script) removeArtifacts() error {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -332,22 +336,22 @@ func (s *script) cleanBackup() error {
|
||||
// the given configuration. In case the given configuration would delete all
|
||||
// backups, it does nothing instead.
|
||||
func (s *script) pruneOldBackups() error {
|
||||
if s.retentionDays == nil {
|
||||
if s.c.BackupRetentionDays < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.leeway != nil {
|
||||
s.logger.Infof("Sleeping for %s before pruning backups.", s.leeway)
|
||||
time.Sleep(*s.leeway)
|
||||
if s.c.BackupPruningLeeway != 0 {
|
||||
s.logger.Infof("Sleeping for %s before pruning backups.", s.c.BackupPruningLeeway)
|
||||
time.Sleep(s.c.BackupPruningLeeway)
|
||||
}
|
||||
|
||||
s.logger.Infof("Trying to prune backups older than %d day(s) now.", *s.retentionDays)
|
||||
deadline := s.start.AddDate(0, 0, -*s.retentionDays)
|
||||
s.logger.Infof("Trying to prune backups older than %d day(s) now.", s.c.BackupRetentionDays)
|
||||
deadline := time.Now().AddDate(0, 0, -int(s.c.BackupRetentionDays))
|
||||
|
||||
if s.bucket != "" {
|
||||
candidates := s.mc.ListObjects(s.ctx, s.bucket, minio.ListObjectsOptions{
|
||||
if s.c.AwsS3BucketName != "" {
|
||||
candidates := s.mc.ListObjects(s.ctx, s.c.AwsS3BucketName, minio.ListObjectsOptions{
|
||||
WithMetadata: true,
|
||||
Prefix: s.pruningPrefix,
|
||||
Prefix: s.c.BackupPruningPrefix,
|
||||
})
|
||||
|
||||
var matches []minio.ObjectInfo
|
||||
@@ -355,7 +359,10 @@ func (s *script) pruneOldBackups() error {
|
||||
for candidate := range candidates {
|
||||
lenCandidates++
|
||||
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) {
|
||||
matches = append(matches, candidate)
|
||||
@@ -370,7 +377,7 @@ func (s *script) pruneOldBackups() error {
|
||||
}
|
||||
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
|
||||
for result := range errChan {
|
||||
if result.Err != nil {
|
||||
@@ -386,23 +393,25 @@ func (s *script) pruneOldBackups() error {
|
||||
)
|
||||
}
|
||||
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),
|
||||
lenCandidates,
|
||||
s.c.BackupRetentionDays,
|
||||
)
|
||||
} else if len(matches) != 0 && len(matches) == lenCandidates {
|
||||
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),
|
||||
)
|
||||
s.logger.Warn("Refusing to do so, please check your configuration.")
|
||||
} else {
|
||||
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(
|
||||
path.Join(s.archive, fmt.Sprintf("%s*", s.pruningPrefix)),
|
||||
path.Join(s.c.BackupArchive, fmt.Sprintf("%s*", s.c.BackupPruningPrefix)),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
@@ -441,15 +450,17 @@ func (s *script) pruneOldBackups() error {
|
||||
)
|
||||
}
|
||||
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(candidates),
|
||||
s.c.BackupRetentionDays,
|
||||
)
|
||||
} else if len(matches) != 0 && len(matches) == len(candidates) {
|
||||
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),
|
||||
)
|
||||
s.logger.Warn("Refusing to do so, please check your configuration.")
|
||||
} else {
|
||||
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) {
|
||||
if err != nil {
|
||||
if s.logger == nil {
|
||||
panic(err)
|
||||
}
|
||||
s.logger.Errorf("Fatal error running backup: %s", err)
|
||||
os.Exit(1)
|
||||
s.logger.Fatalf("Fatal error running backup: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,19 +478,15 @@ func (s *script) must(err error) {
|
||||
// caller invokes the returned release func. When invoked while the file is
|
||||
// still locked the function panics.
|
||||
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 {
|
||||
panic(err)
|
||||
}
|
||||
return func() error {
|
||||
if err := lf.Close(); err != nil {
|
||||
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
|
||||
if !acquired {
|
||||
panic("unable to acquire file lock")
|
||||
}
|
||||
return fileLock.Unlock
|
||||
}
|
||||
|
||||
// copy creates a copy of the file located at `dst` at `src`.
|
||||
|
||||
@@ -3,37 +3,12 @@
|
||||
# Copyright 2021 - Offen Authors <hioffen@posteo.de>
|
||||
# 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
|
||||
|
||||
# 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_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 "$BACKUP_CRON_EXPRESSION backup 2>&1" | crontab -
|
||||
|
||||
# Let cron take the wheel
|
||||
echo "Starting cron in foreground."
|
||||
crond -f -l 8
|
||||
|
||||
3
go.mod
3
go.mod
@@ -4,7 +4,8 @@ go 1.17
|
||||
|
||||
require (
|
||||
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/minio/minio-go/v7 v7.0.12
|
||||
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/v5 v5.0.3/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.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=
|
||||
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/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/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/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
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/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
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.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
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/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.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
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.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/leekchan/timeutil v0.0.0-20150802142658-28917288c48d h1:2puqoOQwi3Ai1oznMOsFIbifm6kIfJaLLyYzWD4IzTs=
|
||||
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 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-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
|
||||
@@ -41,7 +41,6 @@ services:
|
||||
volumes:
|
||||
- app_data:/var/opt/offen
|
||||
|
||||
|
||||
volumes:
|
||||
backup_data:
|
||||
app_data:
|
||||
|
||||
Reference in New Issue
Block a user