Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions cmd/backup/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type Config struct {
BackupStopContainerLabel string `split_words:"true"`
BackupStopDuringBackupLabel string `split_words:"true" default:"true"`
BackupStopDuringBackupNoRestartLabel string `split_words:"true" default:"true"`
BackupLabelMatchBehavior MatchBehavior `split_words:"true" default:"match"`
BackupStopServiceTimeout time.Duration `split_words:"true" default:"5m"`
BackupFromSnapshot bool `split_words:"true"`
BackupExcludeRegexp RegexpDecoder `split_words:"true"`
Expand Down Expand Up @@ -120,6 +121,27 @@ func (c *CompressionType) String() string {
return string(*c)
}

// MatchBehavior controls how the value of a container's stop-during-backup
// label is matched against the value configured on this instance. With "match"
// the values have to be equal. With "one-of" the container's label value is
// split on commas so a single container can be targeted by multiple instances,
// each configured with a different value.
type MatchBehavior string

func (l *MatchBehavior) Decode(v string) error {
switch v {
case "match", "one-of":
*l = MatchBehavior(v)
return nil
default:
return errwrap.Wrap(nil, fmt.Sprintf("error decoding label match behavior %s, expected one of \"match\" or \"one-of\"", v))
}
}

func (l *MatchBehavior) String() string {
return string(*l)
}

type CertDecoder struct {
Cert *x509.Certificate
}
Expand Down
26 changes: 19 additions & 7 deletions cmd/backup/stop_restart.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"io"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -85,14 +86,25 @@ func isSwarm(c interface {
return result.Info.Swarm.LocalNodeState != "" && result.Info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive && result.Info.Swarm.ControlAvailable, nil
}

func hasLabel(labels map[string]string, key, value string) bool {
func hasLabel(labels map[string]string, key, value string, matchBehavior MatchBehavior) bool {
val, ok := labels[key]
return ok && val == value
if !ok {
return false
}
if matchBehavior == "one-of" {
for _, candidate := range strings.Split(val, ",") {
if strings.TrimSpace(candidate) == value {
return true
}
}
return false
}
return val == value
}

func checkStopLabels(labels map[string]string, stopDuringBackupLabelValue string, stopDuringBackupNoRestartLabelValue string) (bool, bool, error) {
hasStopDuringBackupLabel := hasLabel(labels, "docker-volume-backup.stop-during-backup", stopDuringBackupLabelValue)
hasStopDuringBackupNoRestartLabel := hasLabel(labels, "docker-volume-backup.stop-during-backup-no-restart", stopDuringBackupNoRestartLabelValue)
func checkStopLabels(labels map[string]string, stopDuringBackupLabelValue string, stopDuringBackupNoRestartLabelValue string, matchBehavior MatchBehavior) (bool, bool, error) {
hasStopDuringBackupLabel := hasLabel(labels, "docker-volume-backup.stop-during-backup", stopDuringBackupLabelValue, matchBehavior)
hasStopDuringBackupNoRestartLabel := hasLabel(labels, "docker-volume-backup.stop-during-backup-no-restart", stopDuringBackupNoRestartLabelValue, matchBehavior)
if hasStopDuringBackupLabel && hasStopDuringBackupNoRestartLabel {
return hasStopDuringBackupLabel, hasStopDuringBackupNoRestartLabel, errwrap.Wrap(nil, "both docker-volume-backup.stop-during-backup and docker-volume-backup.stop-during-backup-no-restart have been set, cannot continue")
}
Expand Down Expand Up @@ -129,7 +141,7 @@ func (s *script) stopContainersAndServices() (func() error, error) {

var containersToStop []handledContainer
for _, c := range allContainers.Items {
hasStopDuringBackupLabel, hasStopDuringBackupNoRestartLabel, err := checkStopLabels(c.Labels, s.c.BackupStopDuringBackupLabel, s.c.BackupStopDuringBackupNoRestartLabel)
hasStopDuringBackupLabel, hasStopDuringBackupNoRestartLabel, err := checkStopLabels(c.Labels, s.c.BackupStopDuringBackupLabel, s.c.BackupStopDuringBackupNoRestartLabel, s.c.BackupLabelMatchBehavior)
if err != nil {
return noop, errwrap.Wrap(err, "error querying for containers to stop")
}
Expand All @@ -154,7 +166,7 @@ func (s *script) stopContainersAndServices() (func() error, error) {
}

for _, service := range allServices {
hasStopDuringBackupLabel, hasStopDuringBackupNoRestartLabel, err := checkStopLabels(service.Spec.Labels, s.c.BackupStopDuringBackupLabel, s.c.BackupStopDuringBackupNoRestartLabel)
hasStopDuringBackupLabel, hasStopDuringBackupNoRestartLabel, err := checkStopLabels(service.Spec.Labels, s.c.BackupStopDuringBackupLabel, s.c.BackupStopDuringBackupNoRestartLabel, s.c.BackupLabelMatchBehavior)
if err != nil {
return noop, errwrap.Wrap(err, "error querying for services to scale down")
}
Expand Down
85 changes: 85 additions & 0 deletions cmd/backup/stop_restart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,91 @@ func (m *mockInfoClient) Info(context.Context, client.InfoOptions) (client.Syste
return m.result, m.err
}

func TestHasLabel(t *testing.T) {
tests := []struct {
name string
labels map[string]string
key string
value string
matchBehavior MatchBehavior
expected bool
}{
{
"match exact",
map[string]string{"docker-volume-backup.stop-during-backup": "service1"},
"docker-volume-backup.stop-during-backup",
"service1",
"match",
true,
},
{
"match mismatch",
map[string]string{"docker-volume-backup.stop-during-backup": "service2"},
"docker-volume-backup.stop-during-backup",
"service1",
"match",
false,
},
{
"match does not split",
map[string]string{"docker-volume-backup.stop-during-backup": "service1,service2"},
"docker-volume-backup.stop-during-backup",
"service1",
"match",
false,
},
{
"one-of first",
map[string]string{"docker-volume-backup.stop-during-backup": "service1,service2"},
"docker-volume-backup.stop-during-backup",
"service1",
"one-of",
true,
},
{
"one-of last with spaces",
map[string]string{"docker-volume-backup.stop-during-backup": "service1, service2"},
"docker-volume-backup.stop-during-backup",
"service2",
"one-of",
true,
},
{
"one-of no member",
map[string]string{"docker-volume-backup.stop-during-backup": "service1,service2"},
"docker-volume-backup.stop-during-backup",
"service3",
"one-of",
false,
},
{
"one-of single value",
map[string]string{"docker-volume-backup.stop-during-backup": "true"},
"docker-volume-backup.stop-during-backup",
"true",
"one-of",
true,
},
{
"label absent",
map[string]string{},
"docker-volume-backup.stop-during-backup",
"true",
"one-of",
false,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := hasLabel(test.labels, test.key, test.value, test.matchBehavior)
if result != test.expected {
t.Errorf("Expected %v, got %v", test.expected, result)
}
})
}
}

func TestIsSwarm(t *testing.T) {
tests := []struct {
name string
Expand Down
41 changes: 41 additions & 0 deletions docs/how-tos/stop-containers-during-backup.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,47 @@ volumes:
data:
```

## Target a single container from multiple backup instances

A Docker label can only hold a single value per key, so a container can carry
only one `docker-volume-backup.stop-during-backup` value. If you run multiple
instances of this image with different `BACKUP_STOP_DURING_BACKUP_LABEL` values
and want the same container to be stopped by more than one of them, set
`BACKUP_LABEL_MATCH_BEHAVIOR` to `one-of`. The container's label value is then split on
commas and matches if any of the entries equals the configured value.

```yml
services:
app:
# definition for app ...
labels:
- docker-volume-backup.stop-during-backup=service1,service2

backup-one:
image: offen/docker-volume-backup:v2
environment:
BACKUP_STOP_DURING_BACKUP_LABEL: service1
BACKUP_LABEL_MATCH_BEHAVIOR: one-of
volumes:
- data:/backup/my-app-backup:ro
- /var/run/docker.sock:/var/run/docker.sock:ro

backup-two:
image: offen/docker-volume-backup:v2
environment:
BACKUP_STOP_DURING_BACKUP_LABEL: service2
BACKUP_LABEL_MATCH_BEHAVIOR: one-of
volumes:
- data:/backup/my-app-backup:ro
- /var/run/docker.sock:/var/run/docker.sock:ro

volumes:
data:
```

The default value `match` keeps the previous behavior and requires the label
value to be equal to the configured value.

## Stop containers during backup without restarting

Sometimes you might want to stop containers for the backup but not have them start again automatically, for example if they are normally started by an external process or scheduler.
Expand Down
8 changes: 8 additions & 0 deletions docs/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,14 @@ If you need to confirm what the container actually loaded, see [Show loaded conf
# skips restarting the container or service once the backup has finished.
# BACKUP_STOP_DURING_BACKUP_NO_RESTART_LABEL="true"

# Controls how a container's stop-during-backup label value is matched against
# the value configured above. The default "match" requires the values to be
# equal. Setting this to "one-of" splits the container's label value on commas,
# so a single container labeled `stop-during-backup=service1,service2` can be
# stopped by multiple instances of this image, each configured with a different
# value.
# BACKUP_LABEL_MATCH_BEHAVIOR="match"

# When trying to scale down Docker Swarm services, give up after
# the specified amount of time in case the service has not converged yet.
# In case you need to adjust this timeout, supply a duration
Expand Down
Loading