mirror of
https://github.com/offen/docker-volume-backup.git
synced 2026-04-17 14:45:35 +02:00
Add option to run pre/post commands for any container (#73)
* Add option to run pre commands on arbitrary container * Correctly handle quoted args in commands * Provide defaults for test version arg * Allow filtering of target containers * Add documentation on exec commands * Use mysqldump in exec test * Add mysqldump section to recipes * Also run commands test in swarm mode * Use name instead of id * Add syntax highlighting * Add missing license headers
This commit is contained in:
@@ -39,4 +39,6 @@ type Config struct {
|
||||
WebdavPath string `split_words:"true" default:"/"`
|
||||
WebdavUsername string `split_words:"true"`
|
||||
WebdavPassword string `split_words:"true"`
|
||||
ExecLabel string `split_words:"true"`
|
||||
ExecForwardOutput bool `split_words:"true"`
|
||||
}
|
||||
|
||||
122
cmd/backup/exec.go
Normal file
122
cmd/backup/exec.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright 2022 - Offen Authors <hioffen@posteo.de>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cosiner/argv"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
)
|
||||
|
||||
func (s *script) exec(containerRef string, command string) ([]byte, []byte, error) {
|
||||
args, _ := argv.Argv(command, nil, nil)
|
||||
execID, err := s.cli.ContainerExecCreate(context.Background(), containerRef, types.ExecConfig{
|
||||
Cmd: args[0],
|
||||
AttachStdin: true,
|
||||
AttachStderr: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("exec: error creating container exec: %w", err)
|
||||
}
|
||||
|
||||
resp, err := s.cli.ContainerExecAttach(context.Background(), execID.ID, types.ExecStartCheck{})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("exec: error attaching container exec: %w", err)
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
var outBuf, errBuf bytes.Buffer
|
||||
outputDone := make(chan error)
|
||||
|
||||
go func() {
|
||||
_, err := stdcopy.StdCopy(&outBuf, &errBuf, resp.Reader)
|
||||
outputDone <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-outputDone:
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("exec: error demultiplexing output: %w", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
stdout, err := ioutil.ReadAll(&outBuf)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("exec: error reading stdout: %w", err)
|
||||
}
|
||||
stderr, err := ioutil.ReadAll(&errBuf)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("exec: error reading stderr: %w", err)
|
||||
}
|
||||
|
||||
res, err := s.cli.ContainerExecInspect(context.Background(), execID.ID)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("exec: error inspecting container exec: %w", err)
|
||||
}
|
||||
|
||||
if res.ExitCode > 0 {
|
||||
return stdout, stderr, fmt.Errorf("exec: running command exited %d", res.ExitCode)
|
||||
}
|
||||
|
||||
return stdout, stderr, nil
|
||||
}
|
||||
|
||||
func (s *script) runLabeledCommands(label string) error {
|
||||
f := []filters.KeyValuePair{
|
||||
{Key: "label", Value: label},
|
||||
}
|
||||
if s.c.ExecLabel != "" {
|
||||
f = append(f, filters.KeyValuePair{
|
||||
Key: "label",
|
||||
Value: fmt.Sprintf("docker-volume-backup.exec-label=%s", s.c.ExecLabel),
|
||||
})
|
||||
}
|
||||
containersWithCommand, 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", err)
|
||||
}
|
||||
|
||||
if len(containersWithCommand) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(containersWithCommand))
|
||||
|
||||
var cmdErrors []error
|
||||
for _, container := range containersWithCommand {
|
||||
go func(c types.Container) {
|
||||
cmd, _ := c.Labels[label]
|
||||
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 err != nil {
|
||||
cmdErrors = append(cmdErrors, err)
|
||||
}
|
||||
if s.c.ExecForwardOutput {
|
||||
os.Stderr.Write(stderr)
|
||||
os.Stdout.Write(stdout)
|
||||
}
|
||||
wg.Done()
|
||||
}(container)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
if len(cmdErrors) != 0 {
|
||||
return join(cmdErrors...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -38,6 +38,13 @@ func main() {
|
||||
}()
|
||||
|
||||
s.must(func() error {
|
||||
runPostCommands, err := s.runCommands()
|
||||
defer func() {
|
||||
s.must(runPostCommands())
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
|
||||
@@ -212,6 +212,22 @@ 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.
|
||||
|
||||
Reference in New Issue
Block a user