2024-03-15 11:42:22 +01:00
// Copyright 2022 - offen.software <hioffen@posteo.de>
2022-02-22 07:53:33 +01:00
// SPDX-License-Identifier: MPL-2.0
2022-03-04 16:40:34 +01:00
// 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
2022-02-22 07:53:33 +01:00
package main
import (
"bytes"
"context"
2024-02-13 19:21:57 +01:00
"errors"
2022-02-22 07:53:33 +01:00
"fmt"
2023-08-27 18:14:55 +02:00
"io"
2022-02-22 07:53:33 +01:00
"os"
"strings"
"github.com/cosiner/argv"
2024-04-25 17:33:07 +02:00
"github.com/docker/docker/api/types/container"
2022-02-22 07:53:33 +01:00
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/stdcopy"
2024-02-16 15:35:42 +01:00
"github.com/offen/docker-volume-backup/internal/errwrap"
2022-03-10 11:09:39 +01:00
"golang.org/x/sync/errgroup"
2022-02-22 07:53:33 +01:00
)
2023-04-02 11:12:10 +02:00
func ( s * script ) exec ( containerRef string , command string , user string ) ( [ ] byte , [ ] byte , error ) {
2024-12-13 13:13:42 +01:00
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" )
}
2022-12-30 16:07:34 +01:00
commandEnv := [ ] string {
fmt . Sprintf ( "COMMAND_RUNTIME_ARCHIVE_FILEPATH=%s" , s . file ) ,
}
2024-12-13 13:13:42 +01:00
2024-07-02 21:15:49 +02:00
execID , err := s . cli . ContainerExecCreate ( context . Background ( ) , containerRef , container . ExecOptions {
2022-02-22 07:53:33 +01:00
Cmd : args [ 0 ] ,
AttachStdin : true ,
AttachStderr : true ,
2022-12-30 16:07:34 +01:00
Env : commandEnv ,
2023-04-02 11:12:10 +02:00
User : user ,
2022-02-22 07:53:33 +01:00
} )
if err != nil {
2024-02-16 15:35:42 +01:00
return nil , nil , errwrap . Wrap ( err , "error creating container exec" )
2022-02-22 07:53:33 +01:00
}
2024-07-02 21:15:49 +02:00
resp , err := s . cli . ContainerExecAttach ( context . Background ( ) , execID . ID , container . ExecStartOptions { } )
2022-02-22 07:53:33 +01:00
if err != nil {
2024-02-16 15:35:42 +01:00
return nil , nil , errwrap . Wrap ( err , "error attaching container exec" )
2022-02-22 07:53:33 +01:00
}
defer resp . Close ( )
2024-03-01 09:18:39 +01:00
var outBuf , errBuf , fullRespBuf bytes . Buffer
2022-02-22 07:53:33 +01:00
outputDone := make ( chan error )
2024-03-01 09:18:39 +01:00
tee := io . TeeReader ( resp . Reader , & fullRespBuf )
2022-02-22 07:53:33 +01:00
go func ( ) {
2024-03-01 09:18:39 +01:00
_ , err := stdcopy . StdCopy ( & outBuf , & errBuf , tee )
2022-02-22 07:53:33 +01:00
outputDone <- err
} ( )
2023-10-29 15:43:34 +01:00
if err := <- outputDone ; err != nil {
2024-03-01 09:18:39 +01:00
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 ( ) )
}
2024-02-16 15:35:42 +01:00
return nil , nil , errwrap . Wrap ( err , "error demultiplexing output" )
2022-02-22 07:53:33 +01:00
}
2023-08-27 18:14:55 +02:00
stdout , err := io . ReadAll ( & outBuf )
2022-02-22 07:53:33 +01:00
if err != nil {
2024-02-16 15:35:42 +01:00
return nil , nil , errwrap . Wrap ( err , "error reading stdout" )
2022-02-22 07:53:33 +01:00
}
2023-08-27 18:14:55 +02:00
stderr , err := io . ReadAll ( & errBuf )
2022-02-22 07:53:33 +01:00
if err != nil {
2024-02-16 15:35:42 +01:00
return nil , nil , errwrap . Wrap ( err , "error reading stderr" )
2022-02-22 07:53:33 +01:00
}
res , err := s . cli . ContainerExecInspect ( context . Background ( ) , execID . ID )
if err != nil {
2024-02-16 15:35:42 +01:00
return nil , nil , errwrap . Wrap ( err , "error inspecting container exec" )
2022-02-22 07:53:33 +01:00
}
if res . ExitCode > 0 {
2024-02-16 15:35:42 +01:00
return stdout , stderr , errwrap . Wrap ( nil , fmt . Sprintf ( "running command exited %d" , res . ExitCode ) )
2022-02-22 07:53:33 +01:00
}
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 ) ,
} )
}
2024-04-25 17:33:07 +02:00
containersWithCommand , err := s . cli . ContainerList ( context . Background ( ) , container . ListOptions {
2022-02-22 07:53:33 +01:00
Filters : filters . NewArgs ( f ... ) ,
} )
if err != nil {
2024-02-16 15:35:42 +01:00
return errwrap . Wrap ( err , "error querying for containers" )
2022-02-22 07:53:33 +01:00
}
2022-07-10 10:36:56 +02:00
var hasDeprecatedContainers bool
if label == "docker-volume-backup.archive-pre" {
f [ 0 ] = filters . KeyValuePair {
Key : "label" ,
Value : "docker-volume-backup.exec-pre" ,
}
2024-04-25 17:33:07 +02:00
deprecatedContainers , err := s . cli . ContainerList ( context . Background ( ) , container . ListOptions {
2022-07-10 10:36:56 +02:00
Filters : filters . NewArgs ( f ... ) ,
} )
if err != nil {
2024-02-16 15:35:42 +01:00
return errwrap . Wrap ( err , "error querying for containers" )
2022-07-10 10:36:56 +02:00
}
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" ,
}
2024-04-25 17:33:07 +02:00
deprecatedContainers , err := s . cli . ContainerList ( context . Background ( ) , container . ListOptions {
2022-07-10 10:36:56 +02:00
Filters : filters . NewArgs ( f ... ) ,
} )
if err != nil {
2024-02-16 15:35:42 +01:00
return errwrap . Wrap ( err , "error querying for containers" )
2022-07-10 10:36:56 +02:00
}
if len ( deprecatedContainers ) != 0 {
hasDeprecatedContainers = true
containersWithCommand = append ( containersWithCommand , deprecatedContainers ... )
}
}
2022-02-22 07:53:33 +01:00
if len ( containersWithCommand ) == 0 {
return nil
}
2022-07-10 10:36:56 +02:00
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." ,
)
}
2022-03-10 11:09:39 +01:00
g := new ( errgroup . Group )
2022-02-22 07:53:33 +01:00
for _ , container := range containersWithCommand {
2022-03-10 11:09:39 +01:00
c := container
g . Go ( func ( ) error {
2022-07-10 10:36:56 +02:00
cmd , ok := c . Labels [ label ]
if ! ok && label == "docker-volume-backup.archive-pre" {
2023-08-27 18:14:55 +02:00
cmd = c . Labels [ "docker-volume-backup.exec-pre" ]
2022-07-10 10:36:56 +02:00
} else if ! ok && label == "docker-volume-backup.archive-post" {
2023-08-27 18:14:55 +02:00
cmd = c . Labels [ "docker-volume-backup.exec-post" ]
2022-07-10 10:36:56 +02:00
}
2023-04-02 11:12:10 +02:00
userLabelName := fmt . Sprintf ( "%s.user" , label )
user := c . Labels [ userLabelName ]
2023-08-10 19:41:03 +02:00
s . logger . Info ( fmt . Sprintf ( "Running %s command %s for container %s" , label , cmd , strings . TrimPrefix ( c . Names [ 0 ] , "/" ) ) )
2023-04-02 11:12:10 +02:00
stdout , stderr , err := s . exec ( c . ID , cmd , user )
2022-02-22 07:53:33 +01:00
if s . c . ExecForwardOutput {
os . Stderr . Write ( stderr )
os . Stdout . Write ( stdout )
}
2022-03-10 11:09:39 +01:00
if err != nil {
2024-02-16 15:35:42 +01:00
return errwrap . Wrap ( err , "error executing command" )
2022-03-10 11:09:39 +01:00
}
return nil
} )
2022-02-22 07:53:33 +01:00
}
2022-03-10 11:09:39 +01:00
if err := g . Wait ( ) ; err != nil {
2024-02-16 15:35:42 +01:00
return errwrap . Wrap ( err , "error from errgroup" )
2022-02-22 07:53:33 +01:00
}
return nil
}
2022-07-10 10:36:56 +02:00
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
}
2024-02-09 10:24:28 +01:00
return func ( ) ( err error ) {
if err = s . runLabeledCommands ( fmt . Sprintf ( "docker-volume-backup.%s-pre" , step ) ) ; err != nil {
2024-02-16 15:35:42 +01:00
err = errwrap . Wrap ( err , fmt . Sprintf ( "error running %s-pre commands" , step ) )
2024-02-09 10:24:28 +01:00
return
2022-07-10 10:36:56 +02:00
}
defer func ( ) {
2024-02-13 19:21:57 +01:00
if derr := s . runLabeledCommands ( fmt . Sprintf ( "docker-volume-backup.%s-post" , step ) ) ; derr != nil {
2024-02-16 15:35:42 +01:00
err = errors . Join ( err , errwrap . Wrap ( derr , fmt . Sprintf ( "error running %s-post commands" , step ) ) )
2024-02-09 10:24:28 +01:00
}
2022-07-10 10:36:56 +02:00
} ( )
2024-02-09 10:24:28 +01:00
err = cb ( )
return
2022-07-10 10:36:56 +02:00
}
}