mirror of
https://github.com/offen/docker-volume-backup.git
synced 2024-11-22 05:10:28 +01:00
Fine grained labels (#115)
* Refactor label command mechanism to be more flexible * Run all steps wrapped in labeled commands * Rename methods to be in line with lifecycle * Deprecate exec-pre and exec-post labels * Add documentation * Use type alias for lifecycle phases * Fix bad imports * Fix command lookup for deprecated labels * Use more generic naming for lifecycle phase * Fail on erroneous post command * Update documentation
This commit is contained in:
parent
82f66565da
commit
b441cf3e2b
45
README.md
45
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.
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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)())
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user