diff --git a/README.md b/README.md index 53b482a7..8d53b98e 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ It handles __recurring or one-off backups of Docker volumes__ to a __local direc - [Automatically pruning old backups](#automatically-pruning-old-backups) - [Send email notifications on failed backup runs](#send-email-notifications-on-failed-backup-runs) - [Customize notifications](#customize-notifications) - - [Run custom commands before / after backup](#run-custom-commands-before--after-backup) + - [Run custom commands during the backup lifecycle](#run-custom-commands-during-the-backup-lifecycle) - [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) @@ -28,6 +28,7 @@ It handles __recurring or one-off backups of Docker volumes__ to a __local direc - [Manually triggering a backup](#manually-triggering-a-backup) - [Update deprecated email configuration](#update-deprecated-email-configuration) - [Replace deprecated `BACKUP_FROM_SNAPSHOT` usage](#replace-deprecated-backup_from_snapshot-usage) + - [Replace deprecated `exec-pre` and `exec-post` labels](#replace-deprecated-exec-pre-and-exec-post-labels) - [Using a custom Docker host](#using-a-custom-docker-host) - [Run multiple backup schedules in the same container](#run-multiple-backup-schedules-in-the-same-container) - [Define different retention schedules](#define-different-retention-schedules) @@ -351,7 +352,7 @@ You can populate below template according to your requirements and use it as you # It is possible to define commands to be run in any container before and after # a backup is conducted. The commands themselves are defined in labels like -# `docker-volume-backup.exec-pre=/bin/sh -c 'mysqldump [options] > dump.sql'. +# `docker-volume-backup.archive-pre=/bin/sh -c 'mysqldump [options] > dump.sql'. # Several options exist for controlling this feature: # By default, any output of such a command is suppressed. If this value @@ -543,11 +544,16 @@ Overridable template names are: `title_success`, `body_success`, `title_failure` For a full list of available variables and functions, see [this page](https://github.com/offen/docker-volume-backup/blob/master/docs/NOTIFICATION-TEMPLATES.md). -### Run custom commands before / after backup +### Run custom commands during the backup lifecycle In certain scenarios it can be required to run specific commands before and after a backup is taken (e.g. dumping a database). -When mounting the Docker socket into the `docker-volume-backup` container, you can define pre- and post-commands that will be run in the context of the target container. -Such commands are defined by specifying the command in a `docker-volume-backup.exec-[pre|post]` label. +When mounting the Docker socket into the `docker-volume-backup` container, you can define pre- and post-commands that will be run in the context of the target container (it is also possible to run commands inside the `docker-volume-backup` container itself using this feature). +Such commands are defined by specifying the command in a `docker-volume-backup.[step]-[pre|post]` label where `step` can be any of the following phases of a backup lifecyle: + +- `archive` (the tar archive is created) +- `process` (the tar archive is processed, e.g. encrypted - optional) +- `copy` (the tar archive is copied to all configured storages) +- `prune` (existing backups are pruned based on the defined ruleset - optional) Taking a database dump using `mysqldump` would look like this: @@ -561,7 +567,7 @@ services: volumes: - backup_data:/tmp/backups labels: - - docker-volume-backup.exec-pre=/bin/sh -c 'mysqldump --all-databases > /backups/dump.sql' + - docker-volume-backup.archive-pre=/bin/sh -c 'mysqldump --all-databases > /backups/dump.sql' volumes: backup_data: @@ -581,7 +587,7 @@ services: volumes: - backup_data:/tmp/backups labels: - - docker-volume-backup.exec-pre=/bin/sh -c 'mysqldump --all-databases > /tmp/volume/dump.sql' + - docker-volume-backup.archive-pre=/bin/sh -c 'mysqldump --all-databases > /tmp/volume/dump.sql' - docker-volume-backup.exec-label=database backup: @@ -597,7 +603,7 @@ volumes: ``` -The backup procedure is guaranteed to wait for all `pre` commands to finish. +The backup procedure is guaranteed to wait for all `pre` or `post` commands to finish before proceeding. However there are no guarantees about the order in which they are run, which could also happen concurrently. ### Encrypting your backup using GPG @@ -723,7 +729,7 @@ NOTIFICATION_URLS=smtp://me:secret@posteo.de:587/?fromAddress=no-reply@example.c ### Replace deprecated `BACKUP_FROM_SNAPSHOT` usage Starting with version 2.15.0, the `BACKUP_FROM_SNAPSHOT` feature has been deprecated. -If you need to prepare your sources before the backup is taken, use `exec-pre`, `exec-post` and an intermediate volume: +If you need to prepare your sources before the backup is taken, use `archive-pre`, `archive-post` and an intermediate volume: ```yml version: '3' @@ -735,8 +741,8 @@ services: - data:/var/my_app - backup:/tmp/backup labels: - - docker-volume-backup.exec-pre=cp -r /var/my_app /tmp/backup/my-app - - docker-volume-backup.exec-post=rm -rf /tmp/backup/my-app + - docker-volume-backup.archive-pre=cp -r /var/my_app /tmp/backup/my-app + - docker-volume-backup.archive-post=rm -rf /tmp/backup/my-app backup: image: offen/docker-volume-backup:latest @@ -751,6 +757,23 @@ volumes: backup: ``` +### Replace deprecated `exec-pre` and `exec-post` labels + +Version 2.19.0 introduced the option to run labeled commands at multiple points in time during the backup lifecycle. +In order to be able to use more obvious terminology in the new labels, the existing `exec-pre` and `exec-post` labels have been deprecated. +If you want to emulate the existing behavior, all you need to do is change `exec-pre` to `archive-pre` and `exec-post` to `archive-post`: + +```diff + labels: +- - docker-volume-backup.exec-pre=cp -r /var/my_app /tmp/backup/my-app ++ - docker-volume-backup.archive-pre=cp -r /var/my_app /tmp/backup/my-app +- - docker-volume-backup.exec-post=rm -rf /tmp/backup/my-app ++ - docker-volume-backup.archive-post=rm -rf /tmp/backup/my-app +``` + +The `EXEC_LABEL` setting and the `docker-volume-backup.exec-label` label stay as is. +Check the additional documentation on running commands during the backup lifecycle to find out about further possibilities. + ### Using a custom Docker host If you are interfacing with Docker via TCP, set `DOCKER_HOST` to the correct URL. diff --git a/cmd/backup/exec.go b/cmd/backup/exec.go index 9499396c..04eee8f2 100644 --- a/cmd/backup/exec.go +++ b/cmd/backup/exec.go @@ -93,16 +93,68 @@ func (s *script) runLabeledCommands(label string) error { return fmt.Errorf("runLabeledCommands: error querying for containers: %w", err) } + var hasDeprecatedContainers bool + if label == "docker-volume-backup.archive-pre" { + f[0] = filters.KeyValuePair{ + Key: "label", + Value: "docker-volume-backup.exec-pre", + } + deprecatedContainers, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{ + Quiet: true, + Filters: filters.NewArgs(f...), + }) + if err != nil { + return fmt.Errorf("runLabeledCommands: error querying for containers: %w", err) + } + if len(deprecatedContainers) != 0 { + hasDeprecatedContainers = true + containersWithCommand = append(containersWithCommand, deprecatedContainers...) + } + } + + if label == "docker-volume-backup.archive-post" { + f[0] = filters.KeyValuePair{ + Key: "label", + Value: "docker-volume-backup.exec-post", + } + deprecatedContainers, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{ + Quiet: true, + Filters: filters.NewArgs(f...), + }) + if err != nil { + return fmt.Errorf("runLabeledCommands: error querying for containers: %w", err) + } + if len(deprecatedContainers) != 0 { + hasDeprecatedContainers = true + containersWithCommand = append(containersWithCommand, deprecatedContainers...) + } + } + if len(containersWithCommand) == 0 { return nil } + if hasDeprecatedContainers { + s.logger.Warn( + "Using `docker-volume-backup.exec-pre` and `docker-volume-backup.exec-post` labels has been deprecated and will be removed in the next major version.", + ) + s.logger.Warn( + "Please use other `-pre` and `-post` labels instead. Refer to the README for an upgrade guide.", + ) + } + g := new(errgroup.Group) for _, container := range containersWithCommand { c := container g.Go(func() error { - cmd, _ := c.Labels[label] + cmd, ok := c.Labels[label] + if !ok && label == "docker-volume-backup.archive-pre" { + cmd, _ = c.Labels["docker-volume-backup.exec-pre"] + } else if !ok && label == "docker-volume-backup.archive-post" { + cmd, _ = c.Labels["docker-volume-backup.exec-post"] + } + s.logger.Infof("Running %s command %s for container %s", label, cmd, strings.TrimPrefix(c.Names[0], "/")) stdout, stderr, err := s.exec(c.ID, cmd) if s.c.ExecForwardOutput { @@ -121,3 +173,27 @@ func (s *script) runLabeledCommands(label string) error { } return nil } + +type lifecyclePhase string + +const ( + lifecyclePhaseArchive lifecyclePhase = "archive" + lifecyclePhaseProcess lifecyclePhase = "process" + lifecyclePhaseCopy lifecyclePhase = "copy" + lifecyclePhasePrune lifecyclePhase = "prune" +) + +func (s *script) withLabeledCommands(step lifecyclePhase, cb func() error) func() error { + if s.cli == nil { + return cb + } + return func() error { + if err := s.runLabeledCommands(fmt.Sprintf("docker-volume-backup.%s-pre", step)); err != nil { + return fmt.Errorf("withLabeledCommands: %s: error running pre commands: %w", step, err) + } + defer func() { + s.must(s.runLabeledCommands(fmt.Sprintf("docker-volume-backup.%s-post", step))) + }() + return cb() + } +} diff --git a/cmd/backup/main.go b/cmd/backup/main.go index 9da34109..1a8fe98b 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -38,14 +38,7 @@ func main() { s.logger.Info("Finished running backup tasks.") }() - s.must(func() error { - runPostCommands, err := s.runCommands() - defer func() { - s.must(runPostCommands()) - }() - if err != nil { - return err - } + s.must(s.withLabeledCommands(lifecyclePhaseArchive, 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 @@ -56,10 +49,10 @@ func main() { if err != nil { return err } - return s.takeBackup() - }()) + return s.createArchive() + })()) - s.must(s.encryptBackup()) - s.must(s.copyBackup()) - s.must(s.pruneBackups()) + s.must(s.withLabeledCommands(lifecyclePhaseProcess, s.encryptArchive)()) + s.must(s.withLabeledCommands(lifecyclePhaseCopy, s.copyArchive)()) + s.must(s.withLabeledCommands(lifecyclePhasePrune, s.pruneBackups)()) } diff --git a/cmd/backup/script.go b/cmd/backup/script.go index 99657be7..41e7adec 100644 --- a/cmd/backup/script.go +++ b/cmd/backup/script.go @@ -18,9 +18,6 @@ import ( "text/template" "time" - "github.com/pkg/sftp" - "golang.org/x/crypto/ssh" - "github.com/containrrr/shoutrrr" "github.com/containrrr/shoutrrr/pkg/router" "github.com/docker/docker/api/types" @@ -32,9 +29,11 @@ import ( "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "github.com/otiai10/copy" + "github.com/pkg/sftp" "github.com/sirupsen/logrus" "github.com/studio-b12/gowebdav" "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/ssh" ) // script holds all the stateful information required to orchestrate a @@ -282,22 +281,6 @@ func newScript() (*script, error) { return s, nil } -func (s *script) runCommands() (func() error, error) { - if s.cli == nil { - return noop, nil - } - - if err := s.runLabeledCommands("docker-volume-backup.exec-pre"); err != nil { - return noop, fmt.Errorf("runCommands: error running pre commands: %w", err) - } - return func() error { - if err := s.runLabeledCommands("docker-volume-backup.exec-post"); err != nil { - return fmt.Errorf("runCommands: error running post commands: %w", err) - } - return nil - }, 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. @@ -417,9 +400,9 @@ func (s *script) stopContainers() (func() error, error) { }, stopError } -// takeBackup creates a tar archive of the configured backup location and +// createArchive creates a tar archive of the configured backup location and // saves it to disk. -func (s *script) takeBackup() error { +func (s *script) createArchive() error { backupSources := s.c.BackupSources if s.c.BackupFromSnapshot { @@ -427,7 +410,7 @@ func (s *script) takeBackup() error { "Using BACKUP_FROM_SNAPSHOT has been deprecated and will be removed in the next major version.", ) s.logger.Warn( - "Please use `exec-pre` and `exec-post` commands to prepare your backup sources. Refer to the README for an upgrade guide.", + "Please use `archive-pre` and `archive-post` commands to prepare your backup sources. Refer to the README for an upgrade guide.", ) backupSources = filepath.Join("/tmp", s.c.BackupSources) // copy before compressing guard against a situation where backup folder's content are still growing. @@ -484,10 +467,10 @@ func (s *script) takeBackup() error { return nil } -// encryptBackup encrypts the backup file using PGP and the configured passphrase. +// encryptArchive encrypts the backup file using PGP and the configured passphrase. // In case no passphrase is given it returns early, leaving the backup file // untouched. -func (s *script) encryptBackup() error { +func (s *script) encryptArchive() error { if s.c.GpgPassphrase == "" { return nil } @@ -531,9 +514,9 @@ func (s *script) encryptBackup() error { return nil } -// copyBackup makes sure the backup file is copied to both local and remote locations +// copyArchive makes sure the backup file is copied to both local and remote locations // as per the given configuration. -func (s *script) copyBackup() error { +func (s *script) copyArchive() error { _, name := path.Split(s.file) if stat, err := os.Stat(s.file); err != nil { return fmt.Errorf("copyBackup: unable to stat backup file: %w", err) diff --git a/test/commands/docker-compose.yml b/test/commands/docker-compose.yml index 0119fcc9..fe0d57fd 100644 --- a/test/commands/docker-compose.yml +++ b/test/commands/docker-compose.yml @@ -10,8 +10,9 @@ services: MARIADB_ROOT_PASSWORD: test MARIADB_DATABASE: backup labels: + # this is testing the deprecated label on purpose - docker-volume-backup.exec-pre=/bin/sh -c 'mysqldump -ptest --all-databases > /tmp/volume/dump.sql' - - docker-volume-backup.exec-post=/bin/sh -c 'echo "post" > /tmp/volume/post.txt' + - docker-volume-backup.copy-post=/bin/sh -c 'echo "post" > /tmp/volume/post.txt' - docker-volume-backup.exec-label=test volumes: - app_data:/tmp/volume