Skip to content

Commit b441cf3

Browse files
authored
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
1 parent 82f6656 commit b441cf3

File tree

5 files changed

+128
-52
lines changed

5 files changed

+128
-52
lines changed

README.md

+34-11
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@ It handles __recurring or one-off backups of Docker volumes__ to a __local direc
2020
- [Automatically pruning old backups](#automatically-pruning-old-backups)
2121
- [Send email notifications on failed backup runs](#send-email-notifications-on-failed-backup-runs)
2222
- [Customize notifications](#customize-notifications)
23-
- [Run custom commands before / after backup](#run-custom-commands-before--after-backup)
23+
- [Run custom commands during the backup lifecycle](#run-custom-commands-during-the-backup-lifecycle)
2424
- [Encrypting your backup using GPG](#encrypting-your-backup-using-gpg)
2525
- [Restoring a volume from a backup](#restoring-a-volume-from-a-backup)
2626
- [Set the timezone the container runs in](#set-the-timezone-the-container-runs-in)
2727
- [Using with Docker Swarm](#using-with-docker-swarm)
2828
- [Manually triggering a backup](#manually-triggering-a-backup)
2929
- [Update deprecated email configuration](#update-deprecated-email-configuration)
3030
- [Replace deprecated `BACKUP_FROM_SNAPSHOT` usage](#replace-deprecated-backup_from_snapshot-usage)
31+
- [Replace deprecated `exec-pre` and `exec-post` labels](#replace-deprecated-exec-pre-and-exec-post-labels)
3132
- [Using a custom Docker host](#using-a-custom-docker-host)
3233
- [Run multiple backup schedules in the same container](#run-multiple-backup-schedules-in-the-same-container)
3334
- [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
351352

352353
# It is possible to define commands to be run in any container before and after
353354
# a backup is conducted. The commands themselves are defined in labels like
354-
# `docker-volume-backup.exec-pre=/bin/sh -c 'mysqldump [options] > dump.sql'.
355+
# `docker-volume-backup.archive-pre=/bin/sh -c 'mysqldump [options] > dump.sql'.
355356
# Several options exist for controlling this feature:
356357

357358
# 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`
543544

544545
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).
545546

546-
### Run custom commands before / after backup
547+
### Run custom commands during the backup lifecycle
547548

548549
In certain scenarios it can be required to run specific commands before and after a backup is taken (e.g. dumping a database).
549-
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.
550-
Such commands are defined by specifying the command in a `docker-volume-backup.exec-[pre|post]` label.
550+
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).
551+
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:
552+
553+
- `archive` (the tar archive is created)
554+
- `process` (the tar archive is processed, e.g. encrypted - optional)
555+
- `copy` (the tar archive is copied to all configured storages)
556+
- `prune` (existing backups are pruned based on the defined ruleset - optional)
551557

552558
Taking a database dump using `mysqldump` would look like this:
553559

@@ -561,7 +567,7 @@ services:
561567
volumes:
562568
- backup_data:/tmp/backups
563569
labels:
564-
- docker-volume-backup.exec-pre=/bin/sh -c 'mysqldump --all-databases > /backups/dump.sql'
570+
- docker-volume-backup.archive-pre=/bin/sh -c 'mysqldump --all-databases > /backups/dump.sql'
565571

566572
volumes:
567573
backup_data:
@@ -581,7 +587,7 @@ services:
581587
volumes:
582588
- backup_data:/tmp/backups
583589
labels:
584-
- docker-volume-backup.exec-pre=/bin/sh -c 'mysqldump --all-databases > /tmp/volume/dump.sql'
590+
- docker-volume-backup.archive-pre=/bin/sh -c 'mysqldump --all-databases > /tmp/volume/dump.sql'
585591
- docker-volume-backup.exec-label=database
586592
587593
backup:
@@ -597,7 +603,7 @@ volumes:
597603
```
598604

599605

600-
The backup procedure is guaranteed to wait for all `pre` commands to finish.
606+
The backup procedure is guaranteed to wait for all `pre` or `post` commands to finish before proceeding.
601607
However there are no guarantees about the order in which they are run, which could also happen concurrently.
602608

603609
### Encrypting your backup using GPG
@@ -723,7 +729,7 @@ NOTIFICATION_URLS=smtp://me:[email protected]:587/[email protected]
723729
### Replace deprecated `BACKUP_FROM_SNAPSHOT` usage
724730

725731
Starting with version 2.15.0, the `BACKUP_FROM_SNAPSHOT` feature has been deprecated.
726-
If you need to prepare your sources before the backup is taken, use `exec-pre`, `exec-post` and an intermediate volume:
732+
If you need to prepare your sources before the backup is taken, use `archive-pre`, `archive-post` and an intermediate volume:
727733

728734
```yml
729735
version: '3'
@@ -735,8 +741,8 @@ services:
735741
- data:/var/my_app
736742
- backup:/tmp/backup
737743
labels:
738-
- docker-volume-backup.exec-pre=cp -r /var/my_app /tmp/backup/my-app
739-
- docker-volume-backup.exec-post=rm -rf /tmp/backup/my-app
744+
- docker-volume-backup.archive-pre=cp -r /var/my_app /tmp/backup/my-app
745+
- docker-volume-backup.archive-post=rm -rf /tmp/backup/my-app
740746
741747
backup:
742748
image: offen/docker-volume-backup:latest
@@ -751,6 +757,23 @@ volumes:
751757
backup:
752758
```
753759

760+
### Replace deprecated `exec-pre` and `exec-post` labels
761+
762+
Version 2.19.0 introduced the option to run labeled commands at multiple points in time during the backup lifecycle.
763+
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.
764+
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`:
765+
766+
```diff
767+
labels:
768+
- - docker-volume-backup.exec-pre=cp -r /var/my_app /tmp/backup/my-app
769+
+ - docker-volume-backup.archive-pre=cp -r /var/my_app /tmp/backup/my-app
770+
- - docker-volume-backup.exec-post=rm -rf /tmp/backup/my-app
771+
+ - docker-volume-backup.archive-post=rm -rf /tmp/backup/my-app
772+
```
773+
774+
The `EXEC_LABEL` setting and the `docker-volume-backup.exec-label` label stay as is.
775+
Check the additional documentation on running commands during the backup lifecycle to find out about further possibilities.
776+
754777
### Using a custom Docker host
755778

756779
If you are interfacing with Docker via TCP, set `DOCKER_HOST` to the correct URL.

cmd/backup/exec.go

+77-1
Original file line numberDiff line numberDiff line change
@@ -93,16 +93,68 @@ func (s *script) runLabeledCommands(label string) error {
9393
return fmt.Errorf("runLabeledCommands: error querying for containers: %w", err)
9494
}
9595

96+
var hasDeprecatedContainers bool
97+
if label == "docker-volume-backup.archive-pre" {
98+
f[0] = filters.KeyValuePair{
99+
Key: "label",
100+
Value: "docker-volume-backup.exec-pre",
101+
}
102+
deprecatedContainers, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{
103+
Quiet: true,
104+
Filters: filters.NewArgs(f...),
105+
})
106+
if err != nil {
107+
return fmt.Errorf("runLabeledCommands: error querying for containers: %w", err)
108+
}
109+
if len(deprecatedContainers) != 0 {
110+
hasDeprecatedContainers = true
111+
containersWithCommand = append(containersWithCommand, deprecatedContainers...)
112+
}
113+
}
114+
115+
if label == "docker-volume-backup.archive-post" {
116+
f[0] = filters.KeyValuePair{
117+
Key: "label",
118+
Value: "docker-volume-backup.exec-post",
119+
}
120+
deprecatedContainers, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{
121+
Quiet: true,
122+
Filters: filters.NewArgs(f...),
123+
})
124+
if err != nil {
125+
return fmt.Errorf("runLabeledCommands: error querying for containers: %w", err)
126+
}
127+
if len(deprecatedContainers) != 0 {
128+
hasDeprecatedContainers = true
129+
containersWithCommand = append(containersWithCommand, deprecatedContainers...)
130+
}
131+
}
132+
96133
if len(containersWithCommand) == 0 {
97134
return nil
98135
}
99136

137+
if hasDeprecatedContainers {
138+
s.logger.Warn(
139+
"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.",
140+
)
141+
s.logger.Warn(
142+
"Please use other `-pre` and `-post` labels instead. Refer to the README for an upgrade guide.",
143+
)
144+
}
145+
100146
g := new(errgroup.Group)
101147

102148
for _, container := range containersWithCommand {
103149
c := container
104150
g.Go(func() error {
105-
cmd, _ := c.Labels[label]
151+
cmd, ok := c.Labels[label]
152+
if !ok && label == "docker-volume-backup.archive-pre" {
153+
cmd, _ = c.Labels["docker-volume-backup.exec-pre"]
154+
} else if !ok && label == "docker-volume-backup.archive-post" {
155+
cmd, _ = c.Labels["docker-volume-backup.exec-post"]
156+
}
157+
106158
s.logger.Infof("Running %s command %s for container %s", label, cmd, strings.TrimPrefix(c.Names[0], "/"))
107159
stdout, stderr, err := s.exec(c.ID, cmd)
108160
if s.c.ExecForwardOutput {
@@ -121,3 +173,27 @@ func (s *script) runLabeledCommands(label string) error {
121173
}
122174
return nil
123175
}
176+
177+
type lifecyclePhase string
178+
179+
const (
180+
lifecyclePhaseArchive lifecyclePhase = "archive"
181+
lifecyclePhaseProcess lifecyclePhase = "process"
182+
lifecyclePhaseCopy lifecyclePhase = "copy"
183+
lifecyclePhasePrune lifecyclePhase = "prune"
184+
)
185+
186+
func (s *script) withLabeledCommands(step lifecyclePhase, cb func() error) func() error {
187+
if s.cli == nil {
188+
return cb
189+
}
190+
return func() error {
191+
if err := s.runLabeledCommands(fmt.Sprintf("docker-volume-backup.%s-pre", step)); err != nil {
192+
return fmt.Errorf("withLabeledCommands: %s: error running pre commands: %w", step, err)
193+
}
194+
defer func() {
195+
s.must(s.runLabeledCommands(fmt.Sprintf("docker-volume-backup.%s-post", step)))
196+
}()
197+
return cb()
198+
}
199+
}

cmd/backup/main.go

+6-13
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,7 @@ func main() {
3838
s.logger.Info("Finished running backup tasks.")
3939
}()
4040

41-
s.must(func() error {
42-
runPostCommands, err := s.runCommands()
43-
defer func() {
44-
s.must(runPostCommands())
45-
}()
46-
if err != nil {
47-
return err
48-
}
41+
s.must(s.withLabeledCommands(lifecyclePhaseArchive, func() error {
4942
restartContainers, err := s.stopContainers()
5043
// The mechanism for restarting containers is not using hooks as it
5144
// should happen as soon as possible (i.e. before uploading backups or
@@ -56,10 +49,10 @@ func main() {
5649
if err != nil {
5750
return err
5851
}
59-
return s.takeBackup()
60-
}())
52+
return s.createArchive()
53+
})())
6154

62-
s.must(s.encryptBackup())
63-
s.must(s.copyBackup())
64-
s.must(s.pruneBackups())
55+
s.must(s.withLabeledCommands(lifecyclePhaseProcess, s.encryptArchive)())
56+
s.must(s.withLabeledCommands(lifecyclePhaseCopy, s.copyArchive)())
57+
s.must(s.withLabeledCommands(lifecyclePhasePrune, s.pruneBackups)())
6558
}

cmd/backup/script.go

+9-26
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ import (
1818
"text/template"
1919
"time"
2020

21-
"github.com/pkg/sftp"
22-
"golang.org/x/crypto/ssh"
23-
2421
"github.com/containrrr/shoutrrr"
2522
"github.com/containrrr/shoutrrr/pkg/router"
2623
"github.com/docker/docker/api/types"
@@ -32,9 +29,11 @@ import (
3229
"github.com/minio/minio-go/v7"
3330
"github.com/minio/minio-go/v7/pkg/credentials"
3431
"github.com/otiai10/copy"
32+
"github.com/pkg/sftp"
3533
"github.com/sirupsen/logrus"
3634
"github.com/studio-b12/gowebdav"
3735
"golang.org/x/crypto/openpgp"
36+
"golang.org/x/crypto/ssh"
3837
)
3938

4039
// script holds all the stateful information required to orchestrate a
@@ -282,22 +281,6 @@ func newScript() (*script, error) {
282281
return s, nil
283282
}
284283

285-
func (s *script) runCommands() (func() error, error) {
286-
if s.cli == nil {
287-
return noop, nil
288-
}
289-
290-
if err := s.runLabeledCommands("docker-volume-backup.exec-pre"); err != nil {
291-
return noop, fmt.Errorf("runCommands: error running pre commands: %w", err)
292-
}
293-
return func() error {
294-
if err := s.runLabeledCommands("docker-volume-backup.exec-post"); err != nil {
295-
return fmt.Errorf("runCommands: error running post commands: %w", err)
296-
}
297-
return nil
298-
}, nil
299-
}
300-
301284
// stopContainers stops all Docker containers that are marked as to being
302285
// stopped during the backup and returns a function that can be called to
303286
// restart everything that has been stopped.
@@ -417,17 +400,17 @@ func (s *script) stopContainers() (func() error, error) {
417400
}, stopError
418401
}
419402

420-
// takeBackup creates a tar archive of the configured backup location and
403+
// createArchive creates a tar archive of the configured backup location and
421404
// saves it to disk.
422-
func (s *script) takeBackup() error {
405+
func (s *script) createArchive() error {
423406
backupSources := s.c.BackupSources
424407

425408
if s.c.BackupFromSnapshot {
426409
s.logger.Warn(
427410
"Using BACKUP_FROM_SNAPSHOT has been deprecated and will be removed in the next major version.",
428411
)
429412
s.logger.Warn(
430-
"Please use `exec-pre` and `exec-post` commands to prepare your backup sources. Refer to the README for an upgrade guide.",
413+
"Please use `archive-pre` and `archive-post` commands to prepare your backup sources. Refer to the README for an upgrade guide.",
431414
)
432415
backupSources = filepath.Join("/tmp", s.c.BackupSources)
433416
// copy before compressing guard against a situation where backup folder's content are still growing.
@@ -484,10 +467,10 @@ func (s *script) takeBackup() error {
484467
return nil
485468
}
486469

487-
// encryptBackup encrypts the backup file using PGP and the configured passphrase.
470+
// encryptArchive encrypts the backup file using PGP and the configured passphrase.
488471
// In case no passphrase is given it returns early, leaving the backup file
489472
// untouched.
490-
func (s *script) encryptBackup() error {
473+
func (s *script) encryptArchive() error {
491474
if s.c.GpgPassphrase == "" {
492475
return nil
493476
}
@@ -531,9 +514,9 @@ func (s *script) encryptBackup() error {
531514
return nil
532515
}
533516

534-
// copyBackup makes sure the backup file is copied to both local and remote locations
517+
// copyArchive makes sure the backup file is copied to both local and remote locations
535518
// as per the given configuration.
536-
func (s *script) copyBackup() error {
519+
func (s *script) copyArchive() error {
537520
_, name := path.Split(s.file)
538521
if stat, err := os.Stat(s.file); err != nil {
539522
return fmt.Errorf("copyBackup: unable to stat backup file: %w", err)

test/commands/docker-compose.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ services:
1010
MARIADB_ROOT_PASSWORD: test
1111
MARIADB_DATABASE: backup
1212
labels:
13+
# this is testing the deprecated label on purpose
1314
- docker-volume-backup.exec-pre=/bin/sh -c 'mysqldump -ptest --all-databases > /tmp/volume/dump.sql'
14-
- docker-volume-backup.exec-post=/bin/sh -c 'echo "post" > /tmp/volume/post.txt'
15+
- docker-volume-backup.copy-post=/bin/sh -c 'echo "post" > /tmp/volume/post.txt'
1516
- docker-volume-backup.exec-label=test
1617
volumes:
1718
- app_data:/tmp/volume

0 commit comments

Comments
 (0)