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

View File

@ -19,7 +19,7 @@ import (
"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,12 +30,23 @@ func main() {
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 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.")
}
@ -71,25 +82,34 @@ type config struct {
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
// 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)
s.c = &config{}
if err := envconfig.Process("", s.c); err != nil {
return fmt.Errorf("init: failed to process configuration values: %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
}
@ -104,31 +124,29 @@ func (s *script) init() error {
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
}
s.file = path.Join("/tmp", s.c.BackupFilename)
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. In case the docker cli
// is not configured, it will call the given thunk immediately.
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(
@ -144,11 +162,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(
@ -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{}{}
var restartErrors []error
@ -218,17 +244,7 @@ func (s *script) stopContainersAndRun(thunk func() error) error {
len(stoppedContainers),
)
return nil
}()
if len(stopErrors) != 0 {
return fmt.Errorf(
"stopContainersAndRun: %d error(s) stopping containers: %w",
len(stopErrors),
err,
)
}
return thunk()
}, nil
}
// takeBackup creates a tar archive of the configured backup location and
@ -249,6 +265,7 @@ func (s *script) encryptBackup() error {
if s.c.GpgPassphrase == "" {
return nil
}
defer os.Remove(s.file)
gpgFile := fmt.Sprintf("%s.gpg", s.file)
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)
}
if err := os.Remove(s.file); err != nil {
return fmt.Errorf("encryptBackup: error removing unencrpyted backup: %w", err)
}
s.file = gpgFile
s.logger.Infof("Encrypted backup using given passphrase, saving as `%s`.", s.file)
return nil
@ -308,12 +321,12 @@ func (s *script) copyBackup() error {
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
}
@ -453,11 +466,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)
}
}