mirror of
https://github.com/offen/docker-volume-backup.git
synced 2024-12-23 09:20:22 +01:00
223 lines
6.4 KiB
Go
223 lines
6.4 KiB
Go
// Copyright 2022 - offen.software <hioffen@posteo.de>
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
// Portions of this file are taken and adapted from `moby`, Copyright 2012-2017 Docker, Inc.
|
|
// Licensed under the Apache 2.0 License: https://github.com/moby/moby/blob/8e610b2b55bfd1bfa9436ab110d311f5e8a74dcb/LICENSE
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/cosiner/argv"
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/api/types/filters"
|
|
"github.com/docker/docker/pkg/stdcopy"
|
|
"github.com/offen/docker-volume-backup/internal/errwrap"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
func (s *script) exec(containerRef string, command string, user string) ([]byte, []byte, error) {
|
|
args, err := argv.Argv(command, nil, nil)
|
|
if err != nil {
|
|
return nil, nil, errwrap.Wrap(err, fmt.Sprintf("error parsing argv from '%s'", command))
|
|
}
|
|
if len(args) == 0 {
|
|
return nil, nil, errwrap.Wrap(nil, "received unexpected empty command")
|
|
}
|
|
|
|
commandEnv := []string{
|
|
fmt.Sprintf("COMMAND_RUNTIME_ARCHIVE_FILEPATH=%s", s.file),
|
|
}
|
|
|
|
execID, err := s.cli.ContainerExecCreate(context.Background(), containerRef, container.ExecOptions{
|
|
Cmd: args[0],
|
|
AttachStdin: true,
|
|
AttachStderr: true,
|
|
Env: commandEnv,
|
|
User: user,
|
|
})
|
|
if err != nil {
|
|
return nil, nil, errwrap.Wrap(err, "error creating container exec")
|
|
}
|
|
|
|
resp, err := s.cli.ContainerExecAttach(context.Background(), execID.ID, container.ExecStartOptions{})
|
|
if err != nil {
|
|
return nil, nil, errwrap.Wrap(err, "error attaching container exec")
|
|
}
|
|
defer resp.Close()
|
|
|
|
var outBuf, errBuf, fullRespBuf bytes.Buffer
|
|
outputDone := make(chan error)
|
|
|
|
tee := io.TeeReader(resp.Reader, &fullRespBuf)
|
|
|
|
go func() {
|
|
_, err := stdcopy.StdCopy(&outBuf, &errBuf, tee)
|
|
outputDone <- err
|
|
}()
|
|
|
|
if err := <-outputDone; err != nil {
|
|
if body, bErr := io.ReadAll(&fullRespBuf); bErr == nil {
|
|
// if possible, try to append the exec output to the error
|
|
// as it's likely to be more relevant for users than the error from
|
|
// calling stdcopy.Copy
|
|
err = errwrap.Wrap(errors.New(string(body)), err.Error())
|
|
}
|
|
return nil, nil, errwrap.Wrap(err, "error demultiplexing output")
|
|
}
|
|
|
|
stdout, err := io.ReadAll(&outBuf)
|
|
if err != nil {
|
|
return nil, nil, errwrap.Wrap(err, "error reading stdout")
|
|
}
|
|
stderr, err := io.ReadAll(&errBuf)
|
|
if err != nil {
|
|
return nil, nil, errwrap.Wrap(err, "error reading stderr")
|
|
}
|
|
|
|
res, err := s.cli.ContainerExecInspect(context.Background(), execID.ID)
|
|
if err != nil {
|
|
return nil, nil, errwrap.Wrap(err, "error inspecting container exec")
|
|
}
|
|
|
|
if res.ExitCode > 0 {
|
|
return stdout, stderr, errwrap.Wrap(nil, fmt.Sprintf("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(), container.ListOptions{
|
|
Filters: filters.NewArgs(f...),
|
|
})
|
|
if err != nil {
|
|
return errwrap.Wrap(err, "error querying for containers")
|
|
}
|
|
|
|
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(), container.ListOptions{
|
|
Filters: filters.NewArgs(f...),
|
|
})
|
|
if err != nil {
|
|
return errwrap.Wrap(err, "error querying for containers")
|
|
}
|
|
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(), container.ListOptions{
|
|
Filters: filters.NewArgs(f...),
|
|
})
|
|
if err != nil {
|
|
return errwrap.Wrap(err, "error querying for containers")
|
|
}
|
|
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, 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"]
|
|
}
|
|
|
|
userLabelName := fmt.Sprintf("%s.user", label)
|
|
user := c.Labels[userLabelName]
|
|
|
|
s.logger.Info(fmt.Sprintf("Running %s command %s for container %s", label, cmd, strings.TrimPrefix(c.Names[0], "/")))
|
|
stdout, stderr, err := s.exec(c.ID, cmd, user)
|
|
if s.c.ExecForwardOutput {
|
|
os.Stderr.Write(stderr)
|
|
os.Stdout.Write(stdout)
|
|
}
|
|
if err != nil {
|
|
return errwrap.Wrap(err, "error executing command")
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
if err := g.Wait(); err != nil {
|
|
return errwrap.Wrap(err, "error from errgroup")
|
|
}
|
|
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() (err error) {
|
|
if err = s.runLabeledCommands(fmt.Sprintf("docker-volume-backup.%s-pre", step)); err != nil {
|
|
err = errwrap.Wrap(err, fmt.Sprintf("error running %s-pre commands", step))
|
|
return
|
|
}
|
|
defer func() {
|
|
if derr := s.runLabeledCommands(fmt.Sprintf("docker-volume-backup.%s-post", step)); derr != nil {
|
|
err = errors.Join(err, errwrap.Wrap(derr, fmt.Sprintf("error running %s-post commands", step)))
|
|
}
|
|
}()
|
|
err = cb()
|
|
return
|
|
}
|
|
}
|