Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fine grained labels #115

Merged
merged 11 commits into from
Jul 10, 2022
45 changes: 34 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ 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)
- [Using with Docker Swarm](#using-with-docker-swarm)
- [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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:

Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -723,7 +729,7 @@ NOTIFICATION_URLS=smtp://me:[email protected]:587/[email protected]
### 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'
Expand All @@ -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
Expand All @@ -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.
Expand Down
78 changes: 77 additions & 1 deletion cmd/backup/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()
}
}
19 changes: 6 additions & 13 deletions cmd/backup/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)())
}
35 changes: 9 additions & 26 deletions cmd/backup/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -417,17 +400,17 @@ 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 {
s.logger.Warn(
"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.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion test/commands/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down