refactor script initialization

This commit is contained in:
Frederik Ring 2021-08-24 11:39:27 +02:00
parent e73256ad70
commit 5334ff1a5a
2 changed files with 64 additions and 55 deletions

View File

@ -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

View File

@ -19,7 +19,7 @@ import (
"github.com/gofrs/flock" "github.com/gofrs/flock"
"github.com/kelseyhightower/envconfig" "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,12 +30,23 @@ func main() {
unlock := lock("/var/lock/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 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.")
} }
@ -71,25 +82,34 @@ type config struct {
GpgPassphrase string `split_words:"true"` GpgPassphrase string `split_words:"true"`
} }
// init creates all resources needed for the script to perform actions against // 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{
s.c = &config{} Out: os.Stdout,
if err := envconfig.Process("", s.c); err != nil { Formatter: new(logrus.TextFormatter),
return fmt.Errorf("init: failed to process configuration values: %w", err) 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
} }
@ -104,31 +124,29 @@ func (s *script) init() error {
Secure: !s.c.AwsEndpointInsecure && s.c.AwsEndpointProto == "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
} }
s.file = path.Join("/tmp", s.c.BackupFilename) return s, nil
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. In case the docker cli // stopContainers stops all Docker containers that are marked as to being
// is not configured, it will call the given thunk immediately. // stopped during the backup and returns a function that can be called to
func (s *script) stopContainersAndRun(thunk func() error) error { // 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(
@ -144,11 +162,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(
@ -168,7 +186,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
@ -218,17 +244,7 @@ func (s *script) stopContainersAndRun(thunk func() error) error {
len(stoppedContainers), len(stoppedContainers),
) )
return nil return nil
}() }, nil
if len(stopErrors) != 0 {
return fmt.Errorf(
"stopContainersAndRun: %d error(s) stopping containers: %w",
len(stopErrors),
err,
)
}
return thunk()
} }
// takeBackup creates a tar archive of the configured backup location and // takeBackup creates a tar archive of the configured backup location and
@ -249,6 +265,7 @@ func (s *script) encryptBackup() error {
if s.c.GpgPassphrase == "" { if s.c.GpgPassphrase == "" {
return nil return nil
} }
defer os.Remove(s.file)
gpgFile := fmt.Sprintf("%s.gpg", s.file) gpgFile := fmt.Sprintf("%s.gpg", s.file)
outFile, err := os.Create(gpgFile) outFile, err := os.Create(gpgFile)
@ -276,10 +293,6 @@ func (s *script) encryptBackup() error {
return fmt.Errorf("encryptBackup: error writing ciphertext to file: %w", err) return fmt.Errorf("encryptBackup: error writing ciphertext to file: %w", err)
} }
if err := os.Remove(s.file); err != nil {
return fmt.Errorf("encryptBackup: error removing unencrpyted backup: %w", err)
}
s.file = gpgFile s.file = gpgFile
s.logger.Infof("Encrypted backup using given passphrase, saving as `%s`.", s.file) s.logger.Infof("Encrypted backup using given passphrase, saving as `%s`.", s.file)
return nil return nil
@ -308,12 +321,12 @@ func (s *script) copyBackup() error {
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
} }
@ -453,11 +466,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)
} }
} }