diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 0b33ea16..e74a0026 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -41,6 +41,13 @@ jobs: - name: install redis-cli run: sudo apt-get install redis-tools + - name: Update postgres tools + run: | + sudo sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' + wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - + sudo apt-get update + sudo apt-get install -y postgresql-client + - name: Test run: go test -v ./... env: diff --git a/cmd/pgdump.go b/cmd/pgdump.go index af8bb85a..860a53ff 100644 --- a/cmd/pgdump.go +++ b/cmd/pgdump.go @@ -2,7 +2,6 @@ package cmd import ( "context" - "github.com/mittwald/brudi/pkg/source/pgdump" "github.com/spf13/cobra" diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index 1878e643..d7267730 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -2,6 +2,7 @@ package cli import ( "bufio" + "bytes" "compress/gzip" "context" "fmt" @@ -20,7 +21,7 @@ import ( const flagTag = "flag" const gzipType = "application/x-gzip" -// includeFlag returns an string slice of [, ], or [] +// includeFlag returns a string slice of [, ], or [] func includeFlag(flag, val string) []string { var cmd []string if flag != "" { @@ -107,6 +108,9 @@ func StructToCLI(optionStruct interface{}) []string { if flag == "-" { continue } + if fieldVal == nil { + continue + } switch t := fieldVal.(type) { case int: @@ -188,33 +192,86 @@ func ParseCommandLine(cmd CommandType) []string { } // RunWithTimeout executes the given binary within a max execution time -func RunWithTimeout(runContext context.Context, cmd CommandType, timeout time.Duration) ([]byte, error) { +func RunWithTimeout(runContext context.Context, cmd *CommandType, outputToPipe bool, timeout time.Duration) ([]byte, error) { ctx, cancel := context.WithTimeout(runContext, timeout) defer cancel() - return Run(ctx, cmd) + return Run(ctx, cmd, outputToPipe) } // Run executes the given binary -func Run(ctx context.Context, cmd CommandType) ([]byte, error) { - var out []byte - var err error - commandLine := ParseCommandLine(cmd) +func Run(ctx context.Context, cmd *CommandType, outputToPipe bool) ([]byte, error) { + if outputToPipe && cmd.Pipe != nil { + return nil, errors.New("output is supposed to be used but Pipe is already not nil") + } + var cmdExec *exec.Cmd + var outBuffer bytes.Buffer + var stdin io.WriteCloser + var err, pipeErr error + commandLine := ParseCommandLine(*cmd) log.WithField("command", strings.Join(commandLine, " ")).Debug("executing command") - if ctx != nil { - out, err = exec.CommandContext(ctx, commandLine[0], commandLine[1:]...).CombinedOutput() //nolint: gosec - if ctx.Err() != nil { - return out, fmt.Errorf("failed to execute command: timed out or canceled") + if ctx == nil { + ctx = context.Background() + } + cCtx, cancelFunc := context.WithCancel(ctx) + defer cancelFunc() + cmdExec = exec.CommandContext(cCtx, commandLine[0], commandLine[1:]...) //nolint: gosec + if outputToPipe { + cmd.PipeReady.L.Lock() + cmd.Pipe, err = cmdExec.StdoutPipe() + cmd.PipeReady.L.Unlock() + if err != nil { + defer cmd.PipeReady.Broadcast() + return nil, errors.Wrapf(err, "error while getting STDOUT pipe for command: %s", strings.Join(commandLine, " ")) } + cmd.ReadingDone = make(chan bool, 1) + cmd.PipeReady.Broadcast() } else { - out, err = exec.Command(commandLine[0], commandLine[1:]...).CombinedOutput() //nolint: gosec + cmdExec.Stdout = &outBuffer + } + cmdExec.Stderr = &outBuffer + if cmd.Pipe != nil { + stdin, err = cmdExec.StdinPipe() + if err != nil { + return nil, errors.Wrapf(err, "error while getting STDIN pipe for command: %s", strings.Join(commandLine, " ")) + } + } + + err = cmdExec.Start() + if err != nil { + return nil, errors.Wrapf(err, "error while getting starting restic command: %s", strings.Join(commandLine, " ")) + } + if outputToPipe { + for done := range cmd.ReadingDone { + if done { + cancelFunc() + break + } + } + } else if cmd.Pipe != nil { + _, pipeErr = io.Copy(stdin, cmd.Pipe) + cmd.ReadingDone <- true + close(cmd.ReadingDone) + if pipeErr != nil { + cancelFunc() + } + _ = stdin.Close() + } + err = cmdExec.Wait() + cancelFunc() + if ctx != nil && ctx.Err() != nil { + return outBuffer.Bytes(), fmt.Errorf("failed to execute command: timed out or canceled") + } + + if pipeErr != nil { + return outBuffer.Bytes(), fmt.Errorf("failed to pipe data into STDIN for command: %s", err) } if err != nil { - return out, fmt.Errorf("failed to execute command: %s", err) + return outBuffer.Bytes(), fmt.Errorf("failed to execute command: %s", err) } log.WithField("command", strings.Join(commandLine, " ")).Debug("successfully executed command") - return out, nil + return outBuffer.Bytes(), nil } // GzipFile compresses a file with gzip and returns the path of the created archive diff --git a/pkg/cli/types.go b/pkg/cli/types.go index 04de247c..ceeda694 100644 --- a/pkg/cli/types.go +++ b/pkg/cli/types.go @@ -1,13 +1,22 @@ package cli +import ( + "io" + "sync" +) + const GzipSuffix = ".gz" +const DoStdinBackupKey = "doPipingBackup" type CommandType struct { - Binary string - Command string - Args []string - Nice *int // https://linux.die.net/man/1/nice - IONice *int // https://linux.die.net/man/1/ionice + Binary string + Command string + Args []string + Pipe io.Reader // TODO: Remove when --stdin-command was added to restic + PipeReady *sync.Cond // TODO: Remove when --stdin-command was added to restic + ReadingDone chan bool // TODO: Remove when --stdin-command was added to restic + Nice *int // https://linux.die.net/man/1/nice + IONice *int // https://linux.die.net/man/1/ionice } type PipedCommandsPids struct { diff --git a/pkg/restic/client.go b/pkg/restic/client.go index 362c8427..70b71249 100644 --- a/pkg/restic/client.go +++ b/pkg/restic/client.go @@ -3,6 +3,8 @@ package restic import ( "context" "fmt" + "github.com/mittwald/brudi/pkg/cli" + "github.com/spf13/viper" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -36,6 +38,9 @@ func NewResticClient(logger *log.Entry, hostname string, backupPaths ...string) if err != nil { return nil, errors.WithStack(err) } + if viper.GetBool(cli.DoStdinBackupKey) { + conf.Backup.Paths = nil + } if (conf.Backup.Flags.Host) == "" { conf.Backup.Flags.Host = hostname @@ -51,7 +56,7 @@ func NewResticClient(logger *log.Entry, hostname string, backupPaths ...string) }, nil } -func (c *Client) DoResticBackup(ctx context.Context) error { +func (c *Client) DoResticBackup(ctx context.Context, backupDataCmd *cli.CommandType) error { c.Logger.Info("running 'restic backup'") _, err := initBackup(ctx, c.Config.Global) @@ -64,7 +69,7 @@ func (c *Client) DoResticBackup(ctx context.Context) error { } var out []byte - _, out, err = CreateBackup(ctx, c.Config.Global, c.Config.Backup, true) + _, out, err = CreateBackup(ctx, c.Config.Global, c.Config.Backup, true, backupDataCmd) if err != nil { return errors.WithStack(fmt.Errorf("error while running restic backup: %s - %s", err.Error(), out)) } diff --git a/pkg/restic/commands.go b/pkg/restic/commands.go index 9c82f81e..32417c65 100644 --- a/pkg/restic/commands.go +++ b/pkg/restic/commands.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "path" "strings" "time" @@ -31,7 +32,7 @@ var ( func initBackup(ctx context.Context, globalOpts *GlobalOptions) ([]byte, error) { cmd := newCommand("init", cli.StructToCLI(globalOpts)...) - out, err := cli.RunWithTimeout(ctx, cmd, cmdTimeout) + out, err := cli.RunWithTimeout(ctx, &cmd, false, cmdTimeout) if err != nil { // s3 init-check if strings.Contains(string(out), "config already initialized") { @@ -80,7 +81,7 @@ func parseSnapshotOut(jsonLog []byte) (BackupResult, error) { } // CreateBackup executes "restic backup" and returns the parent snapshot id (if available) and the snapshot id -func CreateBackup(ctx context.Context, globalOpts *GlobalOptions, backupOpts *BackupOptions, unlock bool) (BackupResult, []byte, error) { +func CreateBackup(ctx context.Context, globalOpts *GlobalOptions, backupOpts *BackupOptions, unlock bool, backupDataCmd *cli.CommandType) (BackupResult, []byte, error) { var out []byte var err error @@ -98,11 +99,29 @@ func CreateBackup(ctx context.Context, globalOpts *GlobalOptions, backupOpts *Ba var args []string args = cli.StructToCLI(globalOpts) + if backupDataCmd != nil { + backupOpts.Paths = nil + if backupOpts.Flags.StdinFilename == "" { + binName := path.Base(backupDataCmd.Binary) + if binName == "." || binName == "/" { + backupOpts.Flags.StdinFilename = "stdin-backup-file" + } else { + backupOpts.Flags.StdinFilename = fmt.Sprintf("%s-file", binName) + } + } + // TODO: Change back when --stdin-command was added to restic + // args = append(args, "--stdin-from-command", strings.Join(cli.ParseCommandLine(*backupDataCmd), " ")) + backupOpts.Flags.Stdin = true + } args = append(args, cli.StructToCLI(backupOpts)...) cmd := newCommand("backup", args...) + if backupDataCmd != nil { + cmd.Pipe = backupDataCmd.Pipe + cmd.ReadingDone = backupDataCmd.ReadingDone + } - out, err = cli.RunWithTimeout(ctx, cmd, cmdTimeout) + out, err = cli.RunWithTimeout(ctx, &cmd, false, cmdTimeout) if err != nil { return BackupResult{}, out, err } @@ -141,7 +160,7 @@ func Ls(ctx context.Context, glob *GlobalOptions, opts *LsOptions) ([]LsResult, args = append(args, cli.StructToCLI(opts)...) cmd := newCommand("ls", args...) - out, err := cli.Run(ctx, cmd) + out, err := cli.Run(ctx, &cmd, false) if err != nil { return nil, err } @@ -226,7 +245,7 @@ func GetSnapshotSize(ctx context.Context, snapshotIDs []string) (size uint64) { } cmd := newCommand("stats", cli.StructToCLI(&opts)...) - out, err := cli.Run(ctx, cmd) + out, err := cli.Run(ctx, &cmd, false) if err != nil { return } @@ -267,7 +286,7 @@ func GetSnapshotSizeByPath(ctx context.Context, snapshotID, path string) (size u func ListSnapshots(ctx context.Context, opts *SnapshotOptions) ([]Snapshot, error) { cmd := newCommand("snapshots", cli.StructToCLI(&opts)...) - out, err := cli.Run(ctx, cmd) + out, err := cli.Run(ctx, &cmd, false) if err != nil { return nil, err } @@ -282,7 +301,7 @@ func ListSnapshots(ctx context.Context, opts *SnapshotOptions) ([]Snapshot, erro // Find executes "restic find" func Find(ctx context.Context, opts *FindOptions) ([]FindResult, error) { cmd := newCommand("find", cli.StructToCLI(&opts)...) - out, err := cli.Run(ctx, cmd) + out, err := cli.Run(ctx, &cmd, false) if err != nil { return nil, err } @@ -298,7 +317,7 @@ func Find(ctx context.Context, opts *FindOptions) ([]FindResult, error) { // Check executes "restic check" func Check(ctx context.Context, flags *CheckFlags) ([]byte, error) { cmd := newCommand("check", cli.StructToCLI(flags)...) - return cli.Run(ctx, cmd) + return cli.Run(ctx, &cmd, false) } // Forget executes "restic forget" @@ -320,7 +339,7 @@ func Forget( Args: args, } - out, err := cli.Run(ctx, cmd) + out, err := cli.Run(ctx, &cmd, false) if err != nil { return nil, out, err } @@ -349,7 +368,7 @@ func Forget( func Prune(ctx context.Context, globalOpts *GlobalOptions) ([]byte, error) { cmd := newCommand("prune", cli.StructToCLI(globalOpts)...) - return cli.Run(ctx, cmd) + return cli.Run(ctx, &cmd, false) } // RebuildIndex executes "restic rebuild-index" @@ -363,7 +382,7 @@ func RebuildIndex(ctx context.Context) ([]byte, error) { Nice: &nice, IONice: &ionice, } - return cli.Run(ctx, cmd) + return cli.Run(ctx, &cmd, false) } // RestoreBackup executes "restic restore" @@ -385,7 +404,7 @@ func RestoreBackup(ctx context.Context, glob *GlobalOptions, opts *RestoreOption cmd := newCommand("restore", args...) - return cli.Run(ctx, cmd) + return cli.Run(ctx, &cmd, false) } // Unlock executes "restic unlock" @@ -395,12 +414,12 @@ func Unlock(ctx context.Context, globalOpts *GlobalOptions, unlockOpts *UnlockOp args = append(args, cli.StructToCLI(unlockOpts)...) cmd := newCommand("unlock", args...) - return cli.Run(ctx, cmd) + return cli.Run(ctx, &cmd, false) } // Tag executes "restic tag" func Tag(ctx context.Context, opts *TagOptions) ([]byte, error) { cmd := newCommand("tag", cli.StructToCLI(opts)...) - return cli.Run(ctx, cmd) + return cli.Run(ctx, &cmd, false) } diff --git a/pkg/source/backup.go b/pkg/source/backup.go index 70e20ad7..4893713d 100644 --- a/pkg/source/backup.go +++ b/pkg/source/backup.go @@ -3,6 +3,9 @@ package source import ( "context" "fmt" + "github.com/mittwald/brudi/pkg/cli" + "github.com/pkg/errors" + "github.com/spf13/viper" "github.com/mittwald/brudi/pkg/restic" @@ -35,6 +38,9 @@ func getGenericBackendForKind(kind string) (Generic, error) { } func DoBackupForKind(ctx context.Context, kind string, cleanup, useRestic, useResticForget, useResticPrune bool) error { + if viper.GetBool(cli.DoStdinBackupKey) && !useRestic { + return errors.New("doStdinBackup is enabled but restic is disabled") + } logKind := log.WithFields( log.Fields{ "kind": kind, @@ -46,29 +52,36 @@ func DoBackupForKind(ctx context.Context, kind string, cleanup, useRestic, useRe return err } - err = backend.CreateBackup(ctx) + // TODO: Re-activate when --stdin-command was added to restic + /*var backupCmd *cli.CommandType = nil + if viper.GetBool(cli.DoStdinBackupKey) { + bc := backend.GetBackupCommand() + backupCmd = &bc + } else {*/ + backupCmd, err := backend.CreateBackup(ctx) if err != nil { return err } - if cleanup { - defer func() { - cleanupLogger := logKind.WithFields( - log.Fields{ - "path": backend.GetBackupPath(), - "cmd": "cleanup", - }, - ) - if err = backend.CleanUp(); err != nil { - cleanupLogger.WithError(err).Warn("failed to cleanup backup") - } else { - cleanupLogger.Info("successfully cleaned up backup") - } - }() + if !viper.GetBool(cli.DoStdinBackupKey) { + if cleanup { + defer func() { + cleanupLogger := logKind.WithFields( + log.Fields{ + "path": backend.GetBackupPath(), + "cmd": "cleanup", + }, + ) + if err = backend.CleanUp(); err != nil { + cleanupLogger.WithError(err).Warn("failed to cleanup backup") + } else { + cleanupLogger.Info("successfully cleaned up backup") + } + }() + } + logKind.Info("finished backing up") } - logKind.Info("finished backing up") - if !useRestic { return nil } @@ -87,7 +100,7 @@ func DoBackupForKind(ctx context.Context, kind string, cleanup, useRestic, useRe resticClient.Config.Forget.Flags.Prune = false } - if doBackupErr := resticClient.DoResticBackup(ctx); doBackupErr != nil { + if doBackupErr := resticClient.DoResticBackup(ctx, backupCmd); doBackupErr != nil { return doBackupErr } diff --git a/pkg/source/mongodump/backend_config_based.go b/pkg/source/mongodump/backend_config_based.go index 1e6bff0f..78c6c3c2 100644 --- a/pkg/source/mongodump/backend_config_based.go +++ b/pkg/source/mongodump/backend_config_based.go @@ -3,7 +3,10 @@ package mongodump import ( "context" "fmt" + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" "os" + "sync" "github.com/pkg/errors" @@ -11,8 +14,11 @@ import ( "github.com/mittwald/brudi/pkg/cli" ) +//var _ source.Generic = &ConfigBasedBackend{} + type ConfigBasedBackend struct { - cfg *Config + cfg *Config + outputAsArchive bool } func NewConfigBasedBackend() (*ConfigBasedBackend, error) { @@ -27,22 +33,55 @@ func NewConfigBasedBackend() (*ConfigBasedBackend, error) { if err != nil { return nil, err } + outputAsArchive := false + if viper.GetBool(cli.DoStdinBackupKey) { + if config.Options.Flags.Archive != "" { + outputAsArchive = true + } + config.Options.Flags.Archive = "" + if config.Options.Flags.Out != "" { + config.Options.Flags.Out = "-" + } + } - return &ConfigBasedBackend{cfg: config}, nil + return &ConfigBasedBackend{cfg: config, outputAsArchive: outputAsArchive}, nil } -func (b *ConfigBasedBackend) CreateBackup(ctx context.Context) error { - cmd := cli.CommandType{ - Binary: binary, - Args: cli.StructToCLI(b.cfg.Options), +func (b *ConfigBasedBackend) CreateBackup(ctx context.Context) (*cli.CommandType, error) { + cmd := b.GetBackupCommand() + + var out []byte + var err error = nil + if viper.GetBool(cli.DoStdinBackupKey) { + cmd.PipeReady = &sync.Cond{L: &sync.Mutex{}} + if b.outputAsArchive { + cmd.Args = append(cmd.Args, "--archive") + } + go func() { + _, err = cli.Run(ctx, &cmd, true) + if err != nil { + log.Errorf("error while running backup program: %v", err) + } + }() + cmd.PipeReady.L.Lock() + cmd.PipeReady.Wait() + cmd.PipeReady.L.Unlock() + return &cmd, err + } else { + out, err = cli.Run(ctx, &cmd, false) } - - out, err := cli.Run(ctx, cmd) if err != nil { - return errors.WithStack(fmt.Errorf("%+v - %s", err, out)) + return nil, errors.WithStack(fmt.Errorf("%+v - %s", err, out)) } - return nil + return nil, nil +} + +func (b *ConfigBasedBackend) GetBackupCommand() cli.CommandType { + return cli.CommandType{ + Binary: binary, + Args: cli.StructToCLI(b.cfg.Options), + } } func (b *ConfigBasedBackend) GetBackupPath() string { diff --git a/pkg/source/mongorestore/backend_config_based.go b/pkg/source/mongorestore/backend_config_based.go index 8cee5229..47bf3a34 100644 --- a/pkg/source/mongorestore/backend_config_based.go +++ b/pkg/source/mongorestore/backend_config_based.go @@ -11,6 +11,8 @@ import ( "github.com/mittwald/brudi/pkg/cli" ) +//var _ source.GenericRestore = &ConfigBasedBackend{} + type ConfigBasedBackend struct { cfg *Config } @@ -36,7 +38,7 @@ func (b *ConfigBasedBackend) RestoreBackup(ctx context.Context) error { Binary: binary, Args: cli.StructToCLI(b.cfg.Options), } - out, err := cli.Run(ctx, cmd) + out, err := cli.Run(ctx, &cmd, false) if err != nil { return errors.WithStack(fmt.Errorf("%+v - %s", err, out)) } diff --git a/pkg/source/mysqldump/backend_config_based.go b/pkg/source/mysqldump/backend_config_based.go index 8edb2147..9418d400 100644 --- a/pkg/source/mysqldump/backend_config_based.go +++ b/pkg/source/mysqldump/backend_config_based.go @@ -3,14 +3,19 @@ package mysqldump import ( "context" "fmt" + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" "os" "strings" + "sync" "github.com/pkg/errors" "github.com/mittwald/brudi/pkg/cli" ) +//var _ source.Generic = &ConfigBasedBackend{} + type ConfigBasedBackend struct { cfg *Config } @@ -27,36 +32,59 @@ func NewConfigBasedBackend() (*ConfigBasedBackend, error) { if err != nil { return nil, err } + if viper.GetBool(cli.DoStdinBackupKey) { + config.Options.Flags.ResultFile = "" + } return &ConfigBasedBackend{cfg: config}, nil } -func (b *ConfigBasedBackend) CreateBackup(ctx context.Context) error { +func (b *ConfigBasedBackend) CreateBackup(ctx context.Context) (*cli.CommandType, error) { gzip := false // create temporary, unzipped backup first, thus trim '.gz' extension if strings.HasSuffix(b.cfg.Options.Flags.ResultFile, cli.GzipSuffix) { b.cfg.Options.Flags.ResultFile = strings.TrimSuffix(b.cfg.Options.Flags.ResultFile, cli.GzipSuffix) gzip = true } + cmd := b.GetBackupCommand() - cmd := cli.CommandType{ - Binary: binary, - Args: cli.StructToCLI(b.cfg.Options), + var out []byte + var err error = nil + if viper.GetBool(cli.DoStdinBackupKey) { + cmd.PipeReady = &sync.Cond{L: &sync.Mutex{}} + go func() { + _, err = cli.Run(ctx, &cmd, true) + if err != nil { + log.Errorf("error while running backup program: %v", err) + } + }() + cmd.PipeReady.L.Lock() + cmd.PipeReady.Wait() + cmd.PipeReady.L.Unlock() + return &cmd, err + } else { + out, err = cli.Run(ctx, &cmd, false) } - out, err := cli.Run(ctx, cmd) if err != nil { - return errors.WithStack(fmt.Errorf("%+v - %s", err, out)) + return nil, errors.WithStack(fmt.Errorf("%+v - %s", err, out)) } // zip backup, update flag with the name returned by GzipFile for correct handover to restic if gzip { b.cfg.Options.Flags.ResultFile, err = cli.GzipFile(b.cfg.Options.Flags.ResultFile) if err != nil { - return err + return nil, err } } - return nil + return nil, nil +} + +func (b *ConfigBasedBackend) GetBackupCommand() cli.CommandType { + return cli.CommandType{ + Binary: binary, + Args: cli.StructToCLI(b.cfg.Options), + } } func (b *ConfigBasedBackend) GetBackupPath() string { diff --git a/pkg/source/mysqlrestore/backend_config_based.go b/pkg/source/mysqlrestore/backend_config_based.go index 16befdee..936889ab 100644 --- a/pkg/source/mysqlrestore/backend_config_based.go +++ b/pkg/source/mysqlrestore/backend_config_based.go @@ -10,6 +10,8 @@ import ( "github.com/mittwald/brudi/pkg/cli" ) +//var _ source.GenericRestore = &ConfigBasedBackend{} + type ConfigBasedBackend struct { cfg *Config } @@ -44,7 +46,7 @@ func (b *ConfigBasedBackend) RestoreBackup(ctx context.Context) error { Args: args, } var out []byte - out, err = cli.Run(ctx, cmd) + out, err = cli.Run(ctx, &cmd, false) if err != nil { return errors.WithStack(fmt.Errorf("%+v - %s", err, out)) } diff --git a/pkg/source/pgdump/backend_config_based.go b/pkg/source/pgdump/backend_config_based.go index f5c4d022..bb152c68 100644 --- a/pkg/source/pgdump/backend_config_based.go +++ b/pkg/source/pgdump/backend_config_based.go @@ -3,14 +3,19 @@ package pgdump import ( "context" "fmt" + "github.com/spf13/viper" "os" "strings" + "sync" "github.com/pkg/errors" + log "github.com/sirupsen/logrus" "github.com/mittwald/brudi/pkg/cli" ) +//var _ source.Generic = &ConfigBasedBackend{} + type ConfigBasedBackend struct { cfg *Config } @@ -27,36 +32,63 @@ func NewConfigBasedBackend() (*ConfigBasedBackend, error) { if err != nil { return nil, err } + if viper.GetBool(cli.DoStdinBackupKey) { + if strings.TrimSpace(strings.ToLower(config.Options.Flags.Format)) == "directory" { + return nil, errors.New("the output format of pgdump is set to directory but doPipingBackup " + + "is also active. Use 'plain', 'custom' or 'tar' instead") + } + config.Options.Flags.File = "" + } return &ConfigBasedBackend{cfg: config}, nil } -func (b *ConfigBasedBackend) CreateBackup(ctx context.Context) error { +func (b *ConfigBasedBackend) CreateBackup(ctx context.Context) (*cli.CommandType, error) { gzip := false // create temporary, unzipped backup first, thus trim '.gz' extension if strings.HasSuffix(b.cfg.Options.Flags.File, cli.GzipSuffix) { b.cfg.Options.Flags.File = strings.TrimSuffix(b.cfg.Options.Flags.File, cli.GzipSuffix) gzip = true } - cmd := cli.CommandType{ - Binary: binary, - Args: cli.StructToCLI(b.cfg.Options), - } + cmd := b.GetBackupCommand() - out, err := cli.Run(ctx, cmd) + var out []byte + var err error = nil + if viper.GetBool(cli.DoStdinBackupKey) { + cmd.PipeReady = &sync.Cond{L: &sync.Mutex{}} + go func() { + _, err = cli.Run(ctx, &cmd, true) + if err != nil { + log.Errorf("error while running backup program: %v", err) + } + }() + cmd.PipeReady.L.Lock() + cmd.PipeReady.Wait() + cmd.PipeReady.L.Unlock() + return &cmd, err + } else { + out, err = cli.Run(ctx, &cmd, false) + } if err != nil { - return errors.WithStack(fmt.Errorf("%+v - %s", err, out)) + return nil, errors.WithStack(fmt.Errorf("%+v - %s", err, out)) } // zip backup, update flag with the name returned by GzipFile for correct handover to restic if gzip { b.cfg.Options.Flags.File, err = cli.GzipFile(b.cfg.Options.Flags.File) if err != nil { - return err + return nil, err } } - return nil + return nil, nil +} + +func (b *ConfigBasedBackend) GetBackupCommand() cli.CommandType { + return cli.CommandType{ + Binary: binary, + Args: cli.StructToCLI(b.cfg.Options), + } } func (b *ConfigBasedBackend) GetBackupPath() string { diff --git a/pkg/source/pgrestore/backend_config_based.go b/pkg/source/pgrestore/backend_config_based.go index 96445b43..183b5f44 100644 --- a/pkg/source/pgrestore/backend_config_based.go +++ b/pkg/source/pgrestore/backend_config_based.go @@ -10,6 +10,8 @@ import ( "github.com/mittwald/brudi/pkg/cli" ) +//var _ source.GenericRestore = &ConfigBasedBackend{} + type ConfigBasedBackend struct { cfg *Config } @@ -44,7 +46,7 @@ func (b *ConfigBasedBackend) RestoreBackup(ctx context.Context) error { Args: args, } var out []byte - out, err = cli.Run(ctx, cmd) + out, err = cli.Run(ctx, &cmd, false) if err != nil { return errors.WithStack(fmt.Errorf("%+v - %s", err, out)) } diff --git a/pkg/source/psql/backend_config_based.go b/pkg/source/psql/backend_config_based.go index 5a7e9af3..4b7493f9 100644 --- a/pkg/source/psql/backend_config_based.go +++ b/pkg/source/psql/backend_config_based.go @@ -3,6 +3,7 @@ package psql import ( "context" "fmt" + "github.com/spf13/viper" "os" "github.com/mittwald/brudi/pkg/cli" @@ -10,6 +11,8 @@ import ( "github.com/pkg/errors" ) +//var _ source.GenericRestore = &ConfigBasedBackend{} + type ConfigBasedBackend struct { cfg *Config } @@ -27,6 +30,9 @@ func NewConfigBasedBackend() (*ConfigBasedBackend, error) { if err != nil { return nil, err } + if viper.GetBool(cli.DoStdinBackupKey) { + config.Options.Flags.Output = "" + } return &ConfigBasedBackend{cfg: config}, nil } @@ -45,7 +51,7 @@ func (b *ConfigBasedBackend) RestoreBackup(ctx context.Context) error { Args: args, } var out []byte - out, err = cli.Run(ctx, cmd) + out, err = cli.Run(ctx, &cmd, false) if err != nil { return errors.WithStack(fmt.Errorf("%+v - %s", err, out)) } diff --git a/pkg/source/redisdump/backend_config_based.go b/pkg/source/redisdump/backend_config_based.go index dfcf0402..ae6ef361 100644 --- a/pkg/source/redisdump/backend_config_based.go +++ b/pkg/source/redisdump/backend_config_based.go @@ -3,6 +3,7 @@ package redisdump import ( "context" "fmt" + "github.com/spf13/viper" "os" "strings" @@ -11,11 +12,18 @@ import ( "github.com/mittwald/brudi/pkg/cli" ) +//var _ source.Generic = &ConfigBasedBackend{} + type ConfigBasedBackend struct { cfg *Config } func NewConfigBasedBackend() (*ConfigBasedBackend, error) { + if viper.GetBool(cli.DoStdinBackupKey) { + //config.Options.Flags.Pipe = true + return nil, errors.Errorf("can't do a backup to STDOUT with redisdump but %s is set", cli.DoStdinBackupKey) + } + config := &Config{ &Options{ Flags: &Flags{}, @@ -33,32 +41,36 @@ func NewConfigBasedBackend() (*ConfigBasedBackend, error) { } // Do a bgsave of the given redis instance -func (b *ConfigBasedBackend) CreateBackup(ctx context.Context) error { +func (b *ConfigBasedBackend) CreateBackup(ctx context.Context) (*cli.CommandType, error) { gzip := false // create temporary, unzipped backup first, thus trim '.gz' extension if strings.HasSuffix(b.cfg.Options.Flags.Rdb, cli.GzipSuffix) { b.cfg.Options.Flags.Rdb = strings.TrimSuffix(b.cfg.Options.Flags.Rdb, cli.GzipSuffix) gzip = true } - cmd := cli.CommandType{ - Binary: binary, - Args: cli.StructToCLI(b.cfg.Options), - } + cmd := b.GetBackupCommand() - out, err := cli.Run(ctx, cmd) + out, err := cli.Run(ctx, &cmd, false) if err != nil { - return errors.WithStack(fmt.Errorf("%+v - %s", err, out)) + return nil, errors.WithStack(fmt.Errorf("%+v - %s", err, out)) } // zip backup, update flag with the name returned by GzipFile for correct handover to restic if gzip { b.cfg.Options.Flags.Rdb, err = cli.GzipFile(b.cfg.Options.Flags.Rdb) if err != nil { - return err + return nil, err } } - return nil + return nil, nil +} + +func (b *ConfigBasedBackend) GetBackupCommand() cli.CommandType { + return cli.CommandType{ + Binary: binary, + Args: cli.StructToCLI(b.cfg.Options), + } } func (b *ConfigBasedBackend) GetBackupPath() string { diff --git a/pkg/source/tar/backend_config_based.go b/pkg/source/tar/backend_config_based.go index 62f740c9..2826ef4e 100644 --- a/pkg/source/tar/backend_config_based.go +++ b/pkg/source/tar/backend_config_based.go @@ -3,13 +3,18 @@ package tar import ( "context" "fmt" + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" "os" + "sync" "github.com/pkg/errors" "github.com/mittwald/brudi/pkg/cli" ) +//var _ source.Generic = &ConfigBasedBackend{} + type ConfigBasedBackend struct { cfg *Config } @@ -27,21 +32,45 @@ func NewConfigBasedBackend() (*ConfigBasedBackend, error) { if err != nil { return nil, err } + if viper.GetBool(cli.DoStdinBackupKey) { + config.Options.Flags.File = "-" + } return &ConfigBasedBackend{cfg: config}, nil } -func (b *ConfigBasedBackend) CreateBackup(ctx context.Context) error { - cmd := cli.CommandType{ - Binary: binary, - Args: cli.StructToCLI(b.cfg.Options), +func (b *ConfigBasedBackend) CreateBackup(ctx context.Context) (*cli.CommandType, error) { + cmd := b.GetBackupCommand() + + var out []byte + var err error = nil + if viper.GetBool(cli.DoStdinBackupKey) { + cmd.PipeReady = &sync.Cond{L: &sync.Mutex{}} + go func() { + _, err = cli.Run(ctx, &cmd, true) + if err != nil { + log.Errorf("error while running backup program: %v", err) + } + }() + cmd.PipeReady.L.Lock() + cmd.PipeReady.Wait() + cmd.PipeReady.L.Unlock() + return &cmd, err + } else { + out, err = cli.Run(ctx, &cmd, false) } - out, err := cli.Run(ctx, cmd) if err != nil { - return errors.WithStack(fmt.Errorf("%+v - %s", err, out)) + return nil, errors.WithStack(fmt.Errorf("%+v - %s", err, out)) } - return nil + return nil, nil +} + +func (b *ConfigBasedBackend) GetBackupCommand() cli.CommandType { + return cli.CommandType{ + Binary: binary, + Args: cli.StructToCLI(b.cfg.Options), + } } func (b *ConfigBasedBackend) GetBackupPath() string { diff --git a/pkg/source/tarrestore/backend_config_based.go b/pkg/source/tarrestore/backend_config_based.go index 3aa5d02c..aa3e5bae 100644 --- a/pkg/source/tarrestore/backend_config_based.go +++ b/pkg/source/tarrestore/backend_config_based.go @@ -10,6 +10,8 @@ import ( "github.com/mittwald/brudi/pkg/cli" ) +//var _ source.GenericRestore = &ConfigBasedBackend{} + type ConfigBasedBackend struct { cfg *Config } @@ -36,7 +38,7 @@ func (b *ConfigBasedBackend) RestoreBackup(ctx context.Context) error { Binary: binary, Args: cli.StructToCLI(b.cfg.Options), } - out, err := cli.Run(ctx, cmd) + out, err := cli.Run(ctx, &cmd, false) if err != nil { return errors.WithStack(fmt.Errorf("%+v - %s", err, out)) } diff --git a/pkg/source/types.go b/pkg/source/types.go index c8d23772..1b61176a 100644 --- a/pkg/source/types.go +++ b/pkg/source/types.go @@ -2,10 +2,12 @@ package source import ( "context" + "github.com/mittwald/brudi/pkg/cli" ) type Generic interface { - CreateBackup(ctx context.Context) error + CreateBackup(ctx context.Context) (*cli.CommandType, error) // TODO: Remove *cli.CommandType when --stdin-command was added to restic + GetBackupCommand() cli.CommandType GetBackupPath() string GetHostname() string CleanUp() error diff --git a/test/pkg/cli/cli_test.go b/test/pkg/cli/cli_test.go index 80bb5e88..7bb448e3 100644 --- a/test/pkg/cli/cli_test.go +++ b/test/pkg/cli/cli_test.go @@ -93,7 +93,7 @@ func (cliTestSuite *CliTestSuite) TestGzipFile() { Binary: binary, Args: []string{"-t", fileName}, } - _, err = cli.Run(context.TODO(), cmd) + _, err = cli.Run(context.TODO(), &cmd, false) cliTestSuite.Require().NoError(err) } diff --git a/test/pkg/source/internal/testcommons.go b/test/pkg/source/internal/testcommons.go index f13edc0e..760178d9 100644 --- a/test/pkg/source/internal/testcommons.go +++ b/test/pkg/source/internal/testcommons.go @@ -6,6 +6,8 @@ import ( "os" "os/exec" "strings" + "testing" + "time" "github.com/pkg/errors" "github.com/spf13/viper" @@ -68,6 +70,68 @@ func TestSetup() { os.Setenv("RESTIC_PASSWORD", ResticPassword) } +// CheckProgramsAndRestic determines the given programs versions (see GetProgramsVersions) and evaluates restics existence. +// Returns the determined program versions and a bool that describes if restic exists. +// Lets the test fail if the program versions check wasn't successful. +func CheckProgramsAndRestic(t *testing.T, programsAndVersions ...string) (versions []string, resticExists bool) { + var err error + versions, err = GetProgramsVersions(programsAndVersions...) + if err != nil { + t.Error(err) + t.FailNow() + } + _, err = GetProgramVersion("restic", "version") + resticExists = err == nil + if !resticExists { + t.Logf("can't determine restics version: %v", err) + } + return +} + +// GetProgramVersion tries to run the given program with the given version argument to determine its version. +// Leave the version string empty to use "--version". +func GetProgramVersion(program, versionArg string) (string, error) { + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*10) + defer cancelFunc() + if versionArg == "" { + versionArg = "--version" + } + cmd := exec.CommandContext(ctx, program, versionArg) + version, err := cmd.Output() + if err != nil { + return "", errors.Wrapf(err, "error running '%s %s'", program, versionArg) + } + return string(version), nil +} + +// GetProgramsVersions does the same as GetProgramVersion but for multiple programs. Give the programs and their versions +// like this: "[program]", "[version]", "[program]", "[version]"... - Leave the version string empty to use "--version". +// Returns after all programs have been tested. +func GetProgramsVersions(programsAndVersions ...string) (versions []string, err error) { + versions = make([]string, 0, len(programsAndVersions)) + if len(programsAndVersions) == 0 { + return versions, nil + } + if len(programsAndVersions)%2 != 0 { + return versions, errors.New("the count of the given programs and their version arguments aren't a multiple of 2") + } + + errs := make([]string, 0, len(programsAndVersions)) + for i := 0; i < len(programsAndVersions); i += 2 { + v, err := GetProgramVersion(programsAndVersions[i], programsAndVersions[i+1]) + versions = append(versions, v) + if err != nil { + errs = append(errs, err.Error()) + } + } + err = nil + if len(errs) > 0 { + err = errors.Errorf("got error(s) while determining the versions of required programsAndVersions for testing:\n\t%s", + strings.Join(errs, "\n\t")) + } + return +} + // DoResticRestore pulls the given backup from the given restic repo func DoResticRestore(ctx context.Context, resticContainer TestContainerSetup, dataDir string) error { cmd := exec.CommandContext(ctx, "restic", "restore", "-r", // nolint: gosec diff --git a/test/pkg/source/mongodbtest/mongodb_test.go b/test/pkg/source/mongodbtest/mongodb_test.go index 41326cca..f0249ab6 100644 --- a/test/pkg/source/mongodbtest/mongodb_test.go +++ b/test/pkg/source/mongodbtest/mongodb_test.go @@ -34,6 +34,7 @@ const logString = "Waiting for connections" type MongoDumpAndRestoreTestSuite struct { suite.Suite + resticExists bool } func (mongoDumpAndRestoreTestSuite *MongoDumpAndRestoreTestSuite) SetupTest() { @@ -62,7 +63,7 @@ func (mongoDumpAndRestoreTestSuite *MongoDumpAndRestoreTestSuite) TestBasicMongo ctx, false, commons.TestContainerSetup{ Port: "", Address: "", - }, + }, false, ) mongoDumpAndRestoreTestSuite.Require().NoError(err) @@ -82,6 +83,19 @@ func (mongoDumpAndRestoreTestSuite *MongoDumpAndRestoreTestSuite) TestBasicMongo // TestBasicMongoDBDumpRestic performs an integration test for the `mongodump` command with restic support func (mongoDumpAndRestoreTestSuite *MongoDumpAndRestoreTestSuite) TestBasicMongoDBDumpAndRestoreRestic() { + mongoDumpAndRestoreTestSuite.basicMongoDBDumpAndRestoreRestic(backupPath, false) +} + +// TestBasicMongoDBDumpResticStdin performs an integration test for the `mongodump` command with restic support using STDIN +func (mongoDumpAndRestoreTestSuite *MongoDumpAndRestoreTestSuite) TestBasicMongoDBDumpAndRestoreResticStdin() { + mongoDumpAndRestoreTestSuite.basicMongoDBDumpAndRestoreRestic(backupPath, true) +} + +func (mongoDumpAndRestoreTestSuite *MongoDumpAndRestoreTestSuite) basicMongoDBDumpAndRestoreRestic(backupPath string, useStdin bool) { + mongoDumpAndRestoreTestSuite.True(mongoDumpAndRestoreTestSuite.resticExists, "can't use restic on this machine") + if !mongoDumpAndRestoreTestSuite.resticExists { + return + } ctx := context.Background() // remove files after test is done @@ -93,7 +107,7 @@ func (mongoDumpAndRestoreTestSuite *MongoDumpAndRestoreTestSuite) TestBasicMongo }() // create a container running the restic rest-server resticContainer, err := commons.NewTestContainerSetup(ctx, &commons.ResticReq, commons.ResticPort) - mongoDumpAndRestoreTestSuite.Require().NoError(err) + mongoDumpAndRestoreTestSuite.Require().NoErrorf(err, "backupPath: '%s', useStdin: '%t'", backupPath, useStdin) defer func() { resticErr := resticContainer.Container.Terminate(ctx) if resticErr != nil { @@ -103,24 +117,29 @@ func (mongoDumpAndRestoreTestSuite *MongoDumpAndRestoreTestSuite) TestBasicMongo // backup database and retain test data for verification var testData []interface{} - testData, err = mongoDoBackup(ctx, true, resticContainer) - mongoDumpAndRestoreTestSuite.Require().NoError(err) + testData, err = mongoDoBackup(ctx, true, resticContainer, useStdin) + mongoDumpAndRestoreTestSuite.Require().NoErrorf(err, "backupPath: '%s', useStdin: '%t'", backupPath, useStdin) // restore database from backup and pull test data for verification var results []interface{} results, err = mongoDoRestore(ctx, true, resticContainer) + mongoDumpAndRestoreTestSuite.Require().NoErrorf(err, "backupPath: '%s', useStdin: '%t'", backupPath, useStdin) assert.DeepEqual(mongoDumpAndRestoreTestSuite.T(), testData, results) } func TestMongoDumpAndRestoreTestSuite(t *testing.T) { - suite.Run(t, new(MongoDumpAndRestoreTestSuite)) + _, resticExists := commons.CheckProgramsAndRestic(t, "mongodump", "", "mongorestore", "") + testSuite := &MongoDumpAndRestoreTestSuite{ + resticExists: resticExists, + } + suite.Run(t, testSuite) } // mongoDoBackup performs a mongodump and returns the test data that was used for verification purposes func mongoDoBackup( ctx context.Context, useRestic bool, - resticContainer commons.TestContainerSetup, + resticContainer commons.TestContainerSetup, doStdinBackup bool, ) ([]interface{}, error) { // create a mongodb-container to test backup function mongoBackupTarget, err := commons.NewTestContainerSetup(ctx, &mongoRequest, mongoPort) @@ -155,7 +174,7 @@ func mongoDoBackup( } // create brudi config for backup - backupMongoConfig := createMongoConfig(mongoBackupTarget, useRestic, resticContainer.Address, resticContainer.Port, dumpKind) + backupMongoConfig := createMongoConfig(mongoBackupTarget, useRestic, resticContainer.Address, resticContainer.Port, dumpKind, doStdinBackup) err = viper.ReadConfig(bytes.NewBuffer(backupMongoConfig)) if err != nil { return []interface{}{}, err @@ -188,7 +207,7 @@ func mongoDoRestore( }() // create brudi config for restoration - restoreMongoConfig := createMongoConfig(mongoRestoreTarget, useRestic, resticContainer.Address, resticContainer.Port, restoreKind) + restoreMongoConfig := createMongoConfig(mongoRestoreTarget, useRestic, resticContainer.Address, resticContainer.Port, restoreKind, false) err = viper.ReadConfig(bytes.NewBuffer(restoreMongoConfig)) if err != nil { return []interface{}{}, err @@ -277,7 +296,7 @@ func newMongoClient(target *commons.TestContainerSetup) (mongo.Client, error) { } // createMongoConfig creates a brudi config for the brudi command specified via kind -func createMongoConfig(container commons.TestContainerSetup, useRestic bool, resticIP, resticPort, kind string) []byte { +func createMongoConfig(container commons.TestContainerSetup, useRestic bool, resticIP, resticPort, kind string, doStdinBackup bool) []byte { if !useRestic { return []byte(fmt.Sprintf( ` @@ -294,8 +313,17 @@ func createMongoConfig(container commons.TestContainerSetup, useRestic bool, res `, kind, container.Address, container.Port, mongoUser, mongoPW, backupPath, )) } + //restoreTarget := "/" + stdinFilename := "" + if doStdinBackup { + stdinFilename = fmt.Sprintf(" backup:\n flags:\n stdinFilename: %s\n", + backupPath) + //restoreTarget = path.Join(restoreTarget, backupPath) + + } return []byte(fmt.Sprintf( ` + doPipingBackup: %t %s: options: flags: @@ -310,7 +338,7 @@ func createMongoConfig(container commons.TestContainerSetup, useRestic bool, res global: flags: repo: rest:http://%s:%s/ - forget: +%s forget: flags: keepLast: 1 keepHourly: 0 @@ -322,7 +350,7 @@ func createMongoConfig(container commons.TestContainerSetup, useRestic bool, res flags: target: "/" id: "latest" -`, kind, container.Address, container.Port, mongoUser, mongoPW, backupPath, resticIP, resticPort, +`, doStdinBackup, kind, container.Address, container.Port, mongoUser, mongoPW, backupPath, resticIP, resticPort, stdinFilename, )) } diff --git a/test/pkg/source/mysqltest/mysql_test.go b/test/pkg/source/mysqltest/mysql_test.go index 2d8a0a3e..4c16de06 100644 --- a/test/pkg/source/mysqltest/mysql_test.go +++ b/test/pkg/source/mysqltest/mysql_test.go @@ -5,6 +5,7 @@ import ( "context" "database/sql" "fmt" + "github.com/pkg/errors" "os" "testing" "time" @@ -41,6 +42,7 @@ const mysqlImage = "docker.io/bitnami/mysql:latest" type MySQLDumpAndRestoreTestSuite struct { suite.Suite + resticExists bool } // struct for test data @@ -76,7 +78,7 @@ func (mySQLDumpAndRestoreTestSuite *MySQLDumpAndRestoreTestSuite) TestBasicMySQL ctx, false, commons.TestContainerSetup{ Port: "", Address: "", - }, backupPath, + }, backupPath, false, ) mySQLDumpAndRestoreTestSuite.Require().NoError(err) @@ -110,7 +112,7 @@ func (mySQLDumpAndRestoreTestSuite *MySQLDumpAndRestoreTestSuite) TestBasicMySQL ctx, false, commons.TestContainerSetup{ Port: "", Address: "", - }, backupPathZip, + }, backupPathZip, false, ) mySQLDumpAndRestoreTestSuite.Require().NoError(err) @@ -127,8 +129,11 @@ func (mySQLDumpAndRestoreTestSuite *MySQLDumpAndRestoreTestSuite) TestBasicMySQL assert.DeepEqual(mySQLDumpAndRestoreTestSuite.T(), testData, restoreResult) } -// TestMySQLDumpRestic performs an integration test for mysqldump with restic -func (mySQLDumpAndRestoreTestSuite *MySQLDumpAndRestoreTestSuite) TestMySQLDumpAndRestoreRestic() { +func (mySQLDumpAndRestoreTestSuite *MySQLDumpAndRestoreTestSuite) mySQLDumpAndRestoreRestic(backupPath string, useStdin bool) { + mySQLDumpAndRestoreTestSuite.True(mySQLDumpAndRestoreTestSuite.resticExists, "can't use restic on this machine") + if !mySQLDumpAndRestoreTestSuite.resticExists { + return + } ctx := context.Background() defer func() { @@ -140,7 +145,7 @@ func (mySQLDumpAndRestoreTestSuite *MySQLDumpAndRestoreTestSuite) TestMySQLDumpA // setup a container running the restic rest-server resticContainer, err := commons.NewTestContainerSetup(ctx, &commons.ResticReq, commons.ResticPort) - mySQLDumpAndRestoreTestSuite.Require().NoError(err) + mySQLDumpAndRestoreTestSuite.Require().NoErrorf(err, "backupPath: '%s', useStdin: '%t'", backupPath, useStdin) defer func() { resticErr := resticContainer.Container.Terminate(ctx) if resticErr != nil { @@ -150,59 +155,44 @@ func (mySQLDumpAndRestoreTestSuite *MySQLDumpAndRestoreTestSuite) TestMySQLDumpA // backup test data with brudi and retain test data for verification var testData []TestStruct - testData, err = mySQLDoBackup(ctx, true, resticContainer, backupPath) - mySQLDumpAndRestoreTestSuite.Require().NoError(err) + testData, err = mySQLDoBackup(ctx, true, resticContainer, backupPath, useStdin) + mySQLDumpAndRestoreTestSuite.Require().NoErrorf(err, "backupPath: '%s', useStdin: '%t'", backupPath, useStdin) // restore database from backup and pull test data from it for verification var restoreResult []TestStruct restoreResult, err = mySQLDoRestore(ctx, true, resticContainer, backupPath) - mySQLDumpAndRestoreTestSuite.Require().NoError(err) + mySQLDumpAndRestoreTestSuite.Require().NoErrorf(err, "backupPath: '%s', useStdin: '%t'", backupPath, useStdin) assert.DeepEqual(mySQLDumpAndRestoreTestSuite.T(), testData, restoreResult) } +// TestMySQLDumpRestic performs an integration test for mysqldump with restic +func (mySQLDumpAndRestoreTestSuite *MySQLDumpAndRestoreTestSuite) TestMySQLDumpAndRestoreRestic() { + mySQLDumpAndRestoreTestSuite.mySQLDumpAndRestoreRestic(backupPath, false) +} + // TestMySQLDumpResticGzip performs an integration test for mysqldump with restic and gzip func (mySQLDumpAndRestoreTestSuite *MySQLDumpAndRestoreTestSuite) TestMySQLDumpAndRestoreResticGzip() { - ctx := context.Background() - - defer func() { - removeErr := os.Remove(backupPathZip) - if removeErr != nil { - log.WithError(removeErr).Error("failed to clean up mysql backup files") - } - }() - - // setup a container running the restic rest-server - resticContainer, err := commons.NewTestContainerSetup(ctx, &commons.ResticReq, commons.ResticPort) - mySQLDumpAndRestoreTestSuite.Require().NoError(err) - defer func() { - resticErr := resticContainer.Container.Terminate(ctx) - if resticErr != nil { - log.WithError(resticErr).Error("failed to terminate mysql restic container") - } - }() - - // backup test data with brudi and retain test data for verification - var testData []TestStruct - testData, err = mySQLDoBackup(ctx, true, resticContainer, backupPathZip) - mySQLDumpAndRestoreTestSuite.Require().NoError(err) - - // restore database from backup and pull test data from it for verification - var restoreResult []TestStruct - restoreResult, err = mySQLDoRestore(ctx, true, resticContainer, backupPathZip) - mySQLDumpAndRestoreTestSuite.Require().NoError(err) + mySQLDumpAndRestoreTestSuite.mySQLDumpAndRestoreRestic(backupPathZip, false) +} - assert.DeepEqual(mySQLDumpAndRestoreTestSuite.T(), testData, restoreResult) +// TestMySQLDumpResticStdin performs an integration test for mysqldump with restic using STDIN +func (mySQLDumpAndRestoreTestSuite *MySQLDumpAndRestoreTestSuite) TestMySQLDumpAndRestoreResticStdin() { + mySQLDumpAndRestoreTestSuite.mySQLDumpAndRestoreRestic(backupPath, true) } func TestMySQLDumpAndRestoreTestSuite(t *testing.T) { - suite.Run(t, new(MySQLDumpAndRestoreTestSuite)) + _, resticExists := commons.CheckProgramsAndRestic(t, "mysqldump", "", "mysql", "") + testSuite := &MySQLDumpAndRestoreTestSuite{ + resticExists: resticExists, + } + suite.Run(t, testSuite) } // mySQLDoBackup inserts test data into the given database and then executes brudi's `mysqldump` func mySQLDoBackup( ctx context.Context, useRestic bool, - resticContainer commons.TestContainerSetup, path string, + resticContainer commons.TestContainerSetup, path string, useStdinBackup bool, ) ([]TestStruct, error) { // setup a mysql container to backup from mySQLBackupTarget, err := commons.NewTestContainerSetup(ctx, &mySQLRequest, sqlPort) @@ -232,8 +222,10 @@ func mySQLDoBackup( log.WithError(dbErr).Error("failed to close connection to mysql backup database") } }() - // sleep to give mysql server time to get ready - time.Sleep(1 * time.Second) + err = waitForDb(db) + if err != nil { + return []TestStruct{}, err + } // create table for test data _, err = db.Exec(fmt.Sprintf("CREATE TABLE %s(id INT NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, PRIMARY KEY ( id ));", tableName)) @@ -248,7 +240,7 @@ func mySQLDoBackup( } // create brudi config for mysqldump - MySQLBackupConfig := createMySQLConfig(mySQLBackupTarget, useRestic, resticContainer.Address, resticContainer.Port, path) + MySQLBackupConfig := createMySQLConfig(mySQLBackupTarget, useRestic, resticContainer.Address, resticContainer.Port, path, useStdinBackup) err = viper.ReadConfig(bytes.NewBuffer(MySQLBackupConfig)) if err != nil { return []TestStruct{}, err @@ -280,22 +272,13 @@ func mySQLDoRestore( }() // create a brudi config for mysql restore - MySQLRestoreConfig := createMySQLConfig(mySQLRestoreTarget, useRestic, resticContainer.Address, resticContainer.Port, path) + MySQLRestoreConfig := createMySQLConfig(mySQLRestoreTarget, useRestic, resticContainer.Address, resticContainer.Port, path, false) err = viper.ReadConfig(bytes.NewBuffer(MySQLRestoreConfig)) if err != nil { return []TestStruct{}, err } - // sleep to give mysql time to get ready - time.Sleep(1 * time.Second) - - // restore server from mysqldump - err = source.DoBackupForKind(ctx, dumpKind, false, useRestic, false, false) - if err != nil { - return []TestStruct{}, err - } - - // establish connection for retrieving restored data + // establish connection to be able to wait for the DB and retrieving restored data restoreConnectionString := fmt.Sprintf( "%s:%s@tcp(%s:%s)/%s?tls=skip-verify", mySQLRoot, mySQLRootPW, mySQLRestoreTarget.Address, mySQLRestoreTarget.Port, mySQLDatabase, @@ -308,9 +291,19 @@ func mySQLDoRestore( defer func() { dbErr := dbRestore.Close() if dbErr != nil { - log.WithError(dbErr).Error("failed to close connection to mysql restore database") + log.WithError(dbErr).Error("failed to close connection to mysql backup database") } }() + err = waitForDb(dbRestore) + if err != nil { + return []TestStruct{}, err + } + + // restore server from mysqldump + err = source.DoRestoreForKind(ctx, restoreKind, false, useRestic) + if err != nil { + return []TestStruct{}, err + } // attempt to retrieve test data from database var result *sql.Rows @@ -343,16 +336,22 @@ func mySQLDoRestore( } // createMySQLConfig creates a brudi config for mysqldump and mysqlrestore command. -func createMySQLConfig(container commons.TestContainerSetup, useRestic bool, resticIP, resticPort, path string) []byte { +func createMySQLConfig(container commons.TestContainerSetup, useRestic bool, resticIP, resticPort, filepath string, doStdinBackup bool) []byte { var resticConfig string if useRestic { + //restoreTarget := "/" + stdinFilename := "" + if doStdinBackup { + stdinFilename = fmt.Sprintf(" backup:\n flags:\n stdinFilename: %s\n", filepath) + //restoreTarget = path.Join(restoreTarget, filepath) + } resticConfig = fmt.Sprintf( - ` + `doPipingBackup: %t restic: global: flags: repo: rest:http://%s:%s/ - forget: +%s forget: flags: keepLast: 1 keepHourly: 0 @@ -364,7 +363,7 @@ restic: flags: target: "/" id: "latest" -`, resticIP, resticPort, +`, doStdinBackup, resticIP, resticPort, stdinFilename, ) } @@ -391,14 +390,33 @@ mysqlrestore: user: %s Database: %s additionalArgs: [] - sourceFile: %s%s -`, hostName, container.Port, mySQLRootPW, mySQLRoot, path, - hostName, container.Port, mySQLRootPW, mySQLRoot, mySQLDatabase, path, + sourceFile: %s +%s +`, hostName, container.Port, mySQLRootPW, mySQLRoot, filepath, + hostName, container.Port, mySQLRootPW, mySQLRoot, mySQLDatabase, filepath, resticConfig, )) return result } +func waitForDb(db *sql.DB) error { + var err error + // sleep to give mysql server time to get ready + time.Sleep(10 * time.Second) + // Ping until ready or 30 seconds are over + for i := 0; i < 20; i++ { + err = db.Ping() + if err == nil { + break + } + time.Sleep(time.Second) + } + if err != nil { + return errors.Wrap(err, "can't ping database after 30 seconds") + } + return nil +} + // prepareTestData creates test data and inserts it into the given database func prepareTestData(database *sql.DB) ([]TestStruct, error) { var err error diff --git a/test/pkg/source/postgrestest/postgres_test.go b/test/pkg/source/postgrestest/postgres_test.go index c98d7f2b..dac26e88 100644 --- a/test/pkg/source/postgrestest/postgres_test.go +++ b/test/pkg/source/postgrestest/postgres_test.go @@ -42,6 +42,7 @@ const plainKind = "plain" type PGDumpAndRestoreTestSuite struct { suite.Suite + resticExists bool } func (pgDumpAndRestoreTestSuite *PGDumpAndRestoreTestSuite) SetupTest() { @@ -80,7 +81,7 @@ func (pgDumpAndRestoreTestSuite *PGDumpAndRestoreTestSuite) TestBasicPGDumpAndRe Port: "", Address: "", }, - "tar", backupPath, + "tar", backupPath, false, ) pgDumpAndRestoreTestSuite.Require().NoError(err) @@ -91,7 +92,7 @@ func (pgDumpAndRestoreTestSuite *PGDumpAndRestoreTestSuite) TestBasicPGDumpAndRe Port: "", Address: "", }, - "tar", backupPath, + "tar", backupPath, false, ) pgDumpAndRestoreTestSuite.Require().NoError(err) @@ -104,7 +105,7 @@ func (pgDumpAndRestoreTestSuite *PGDumpAndRestoreTestSuite) TestBasicPGDumpAndRe Port: "", Address: "", }, - "plain", backupPathPlain, + "plain", backupPathPlain, false, ) pgDumpAndRestoreTestSuite.Require().NoError(err) @@ -115,7 +116,7 @@ func (pgDumpAndRestoreTestSuite *PGDumpAndRestoreTestSuite) TestBasicPGDumpAndRe Port: "", Address: "", }, - "plain", backupPathPlain, + "plain", backupPathPlain, false, ) pgDumpAndRestoreTestSuite.Require().NoError(err) @@ -149,7 +150,7 @@ func (pgDumpAndRestoreTestSuite *PGDumpAndRestoreTestSuite) TestBasicPGDumpAndRe Port: "", Address: "", }, - "tar", backupPathZip, + "tar", backupPathZip, false, ) pgDumpAndRestoreTestSuite.Require().NoError(err) @@ -160,7 +161,7 @@ func (pgDumpAndRestoreTestSuite *PGDumpAndRestoreTestSuite) TestBasicPGDumpAndRe Port: "", Address: "", }, - "tar", backupPathZip, + "tar", backupPathZip, false, ) pgDumpAndRestoreTestSuite.Require().NoError(err) @@ -173,7 +174,7 @@ func (pgDumpAndRestoreTestSuite *PGDumpAndRestoreTestSuite) TestBasicPGDumpAndRe Port: "", Address: "", }, - "plain", backupPathPlainZip, + "plain", backupPathPlainZip, false, ) pgDumpAndRestoreTestSuite.Require().NoError(err) @@ -184,7 +185,7 @@ func (pgDumpAndRestoreTestSuite *PGDumpAndRestoreTestSuite) TestBasicPGDumpAndRe Port: "", Address: "", }, - "plain", backupPathPlainZip, + "plain", backupPathPlainZip, false, ) pgDumpAndRestoreTestSuite.Require().NoError(err) @@ -193,54 +194,30 @@ func (pgDumpAndRestoreTestSuite *PGDumpAndRestoreTestSuite) TestBasicPGDumpAndRe // TestPGDumpRestic performs an integration test for brudi pgdump with restic func (pgDumpAndRestoreTestSuite *PGDumpAndRestoreTestSuite) TestPGDumpAndRestoreRestic() { - ctx := context.Background() - - // remove backup files after test - defer func() { - // delete folder with backup file - removeErr := os.RemoveAll(backupPath) - if removeErr != nil { - log.WithError(removeErr).Error("failed to remove pgdump backup files") - } - }() - - // setup a container running the restic rest-server - resticContainer, err := commons.NewTestContainerSetup(ctx, &commons.ResticReq, commons.ResticPort) - pgDumpAndRestoreTestSuite.Require().NoError(err) - defer func() { - resticErr := resticContainer.Container.Terminate(ctx) - if resticErr != nil { - log.WithError(resticErr).Error("failed to terminate pgdump restic container") - } - }() - - // backup test data with brudi and retain test data for verification - var testData []testStruct - testData, err = pgDoBackup( - ctx, true, resticContainer, - "tar", backupPath, - ) - pgDumpAndRestoreTestSuite.Require().NoError(err) - - // restore test data with brudi and retrieve it from the db for verification - var restoreResult []testStruct - restoreResult, err = pgDoRestore( - ctx, true, resticContainer, - "tar", backupPath, - ) - pgDumpAndRestoreTestSuite.Require().NoError(err) - - assert.DeepEqual(pgDumpAndRestoreTestSuite.T(), testData, restoreResult) + pgDumpAndRestoreTestSuite.pgDumpAndRestoreRestic(backupPath, false) } // TestPGDumpResticGzip performs an integration test for brudi pgdump with restic and gzip func (pgDumpAndRestoreTestSuite *PGDumpAndRestoreTestSuite) TestPGDumpAndRestoreResticGzip() { + pgDumpAndRestoreTestSuite.pgDumpAndRestoreRestic(backupPathZip, false) +} + +// TestPGDumpResticStdin performs an integration test for brudi pgdump with restic over STDIN +func (pgDumpAndRestoreTestSuite *PGDumpAndRestoreTestSuite) TestPGDumpAndRestoreResticStdin() { + pgDumpAndRestoreTestSuite.pgDumpAndRestoreRestic(backupPath, true) +} + +func (pgDumpAndRestoreTestSuite *PGDumpAndRestoreTestSuite) pgDumpAndRestoreRestic(backupPath string, useStdin bool) { + pgDumpAndRestoreTestSuite.True(pgDumpAndRestoreTestSuite.resticExists, "can't use restic on this machine") + if !pgDumpAndRestoreTestSuite.resticExists { + return + } ctx := context.Background() // remove backup files after test defer func() { // delete folder with backup file - removeErr := os.RemoveAll(backupPathZip) + removeErr := os.RemoveAll(backupPath) if removeErr != nil { log.WithError(removeErr).Error("failed to remove pgdump backup files") } @@ -248,7 +225,7 @@ func (pgDumpAndRestoreTestSuite *PGDumpAndRestoreTestSuite) TestPGDumpAndRestore // setup a container running the restic rest-server resticContainer, err := commons.NewTestContainerSetup(ctx, &commons.ResticReq, commons.ResticPort) - pgDumpAndRestoreTestSuite.Require().NoError(err) + pgDumpAndRestoreTestSuite.Require().NoErrorf(err, "backupPath: '%s', useStdin: '%t'", backupPath, useStdin) defer func() { resticErr := resticContainer.Container.Terminate(ctx) if resticErr != nil { @@ -260,29 +237,33 @@ func (pgDumpAndRestoreTestSuite *PGDumpAndRestoreTestSuite) TestPGDumpAndRestore var testData []testStruct testData, err = pgDoBackup( ctx, true, resticContainer, - "tar", backupPathZip, + "tar", backupPath, useStdin, ) - pgDumpAndRestoreTestSuite.Require().NoError(err) + pgDumpAndRestoreTestSuite.Require().NoErrorf(err, "backupPath: '%s', useStdin: '%t'", backupPath, useStdin) // restore test data with brudi and retrieve it from the db for verification var restoreResult []testStruct restoreResult, err = pgDoRestore( ctx, true, resticContainer, - "tar", backupPathZip, + "tar", backupPath, useStdin, ) - pgDumpAndRestoreTestSuite.Require().NoError(err) + pgDumpAndRestoreTestSuite.Require().NoErrorf(err, "backupPath: '%s', useStdin: '%t'", backupPath, useStdin) assert.DeepEqual(pgDumpAndRestoreTestSuite.T(), testData, restoreResult) } func TestPGDumpAndRestoreTestSuite(t *testing.T) { - suite.Run(t, new(PGDumpAndRestoreTestSuite)) + _, resticExists := commons.CheckProgramsAndRestic(t, "pg_dump", "", "pg_restore", "", "psql", "") + testSuite := &PGDumpAndRestoreTestSuite{ + resticExists: resticExists, + } + suite.Run(t, testSuite) } // pgDoBackup populates a database with data and performs a backup, optionally with restic func pgDoBackup( ctx context.Context, useRestic bool, - resticContainer commons.TestContainerSetup, format, path string, + resticContainer commons.TestContainerSetup, format, path string, doStdinBackup bool, ) ([]testStruct, error) { // create a postgres container to test backup function pgBackupTarget, err := commons.NewTestContainerSetup(ctx, &pgRequest, pgPort) @@ -328,7 +309,7 @@ func pgDoBackup( } // create a brudi config for pgdump - testPGConfig := createPGConfig(pgBackupTarget, useRestic, resticContainer.Address, resticContainer.Port, format, path) + testPGConfig := createPGConfig(pgBackupTarget, useRestic, resticContainer.Address, resticContainer.Port, format, path, doStdinBackup) err = viper.ReadConfig(bytes.NewBuffer(testPGConfig)) if err != nil { return []testStruct{}, err @@ -346,7 +327,7 @@ func pgDoBackup( // pgDoRestore restores data from backup and retrieves it for verification, optionally using restic func pgDoRestore( ctx context.Context, useRestic bool, resticContainer commons.TestContainerSetup, - format, path string, + format, path string, backuppedWithStdin bool, ) ([]testStruct, error) { // setup second postgres container to test if correct data is restored @@ -362,7 +343,7 @@ func pgDoRestore( }() // create a brudi configuration for pgrestore, depending on backup format - restorePGConfig := createPGConfig(pgRestoreTarget, useRestic, resticContainer.Address, resticContainer.Port, format, path) + restorePGConfig := createPGConfig(pgRestoreTarget, useRestic, resticContainer.Address, resticContainer.Port, format, path, backuppedWithStdin) err = viper.ReadConfig(bytes.NewBuffer(restorePGConfig)) if err != nil { return []testStruct{}, err @@ -427,7 +408,7 @@ func pgDoRestore( } // createPGConfig creates a brudi config for the pgdump and the correct restoration command based on format -func createPGConfig(container commons.TestContainerSetup, useRestic bool, resticIP, resticPort, format, path string) []byte { +func createPGConfig(container commons.TestContainerSetup, useRestic bool, resticIP, resticPort, format, filepath string, doStdinBackup bool) []byte { var restoreConfig string if format != plainKind { restoreConfig = fmt.Sprintf( @@ -441,7 +422,7 @@ func createPGConfig(container commons.TestContainerSetup, useRestic bool, restic dbname: %s additionalArgs: [] sourcefile: %s -`, hostName, container.Port, postgresPW, postgresUser, postgresDB, path, +`, hostName, container.Port, postgresPW, postgresUser, postgresDB, filepath, ) } else { restoreConfig = fmt.Sprintf( @@ -455,18 +436,25 @@ func createPGConfig(container commons.TestContainerSetup, useRestic bool, restic dbname: %s additionalArgs: [] sourcefile: %s -`, hostName, container.Port, postgresUser, postgresPW, postgresDB, path, +`, hostName, container.Port, postgresUser, postgresPW, postgresDB, filepath, ) } var resticConfig string if useRestic { + stdinFilename := "" + //restoreTarget := "/" + if doStdinBackup { + stdinFilename = fmt.Sprintf(" backup:\n flags:\n stdinFilename: %s\n", filepath) + //restoreTarget = path.Join(restoreTarget, filepath) + } resticConfig = fmt.Sprintf( - `restic: + `doPipingBackup: %t +restic: global: flags: repo: rest:http://%s:%s/ - forget: +%s forget: flags: keepLast: 1 keepHourly: 0 @@ -478,10 +466,15 @@ func createPGConfig(container commons.TestContainerSetup, useRestic bool, restic flags: target: "/" id: "latest" -`, resticIP, resticPort, +`, doStdinBackup, resticIP, resticPort, stdinFilename, ) } + filename := "" + if !doStdinBackup { + filename = fmt.Sprintf(" file: %s\n", filepath) + } + result := []byte(fmt.Sprintf( ` pgdump: @@ -492,13 +485,12 @@ pgdump: password: %s username: %s dbName: %s - file: %s - format: %s +%s format: %s additionalArgs: [] %s %s -`, hostName, container.Port, postgresPW, postgresUser, postgresDB, path, format, restoreConfig, resticConfig, +`, hostName, container.Port, postgresPW, postgresUser, postgresDB, filename, format, restoreConfig, resticConfig, )) return result } diff --git a/test/pkg/source/redisdump/redisdump_test.go b/test/pkg/source/redisdump/redisdump_test.go index be9b3b67..45fd3a94 100644 --- a/test/pkg/source/redisdump/redisdump_test.go +++ b/test/pkg/source/redisdump/redisdump_test.go @@ -37,6 +37,7 @@ const redisImage = "docker.io/bitnami/redis:latest" type RedisDumpTestSuite struct { suite.Suite + resticExists bool } func (redisDumpTestSuite *RedisDumpTestSuite) SetupTest() { @@ -118,6 +119,10 @@ func (redisDumpTestSuite *RedisDumpTestSuite) TestBasicRedisDumpGzip() { // TestBasicRedisDumpRestic performs an integration test for brudi's `redisdump` command with restic func (redisDumpTestSuite *RedisDumpTestSuite) TestRedisDumpRestic() { + redisDumpTestSuite.True(redisDumpTestSuite.resticExists, "can't use restic on this machine") + if !redisDumpTestSuite.resticExists { + return + } ctx := context.Background() // remove backup files after test @@ -154,6 +159,10 @@ func (redisDumpTestSuite *RedisDumpTestSuite) TestRedisDumpRestic() { // TestBasicRedisDumpRestic performs an integration test for brudi's `redisdump` command with restic and gzip func (redisDumpTestSuite *RedisDumpTestSuite) TestRedisDumpResticGzip() { + redisDumpTestSuite.True(redisDumpTestSuite.resticExists, "can't use restic on this machine") + if !redisDumpTestSuite.resticExists { + return + } ctx := context.Background() // remove backup files after test @@ -189,7 +198,11 @@ func (redisDumpTestSuite *RedisDumpTestSuite) TestRedisDumpResticGzip() { } func TestRedisDumpTestSuite(t *testing.T) { - suite.Run(t, new(RedisDumpTestSuite)) + _, resticExists := commons.CheckProgramsAndRestic(t, "redis-cli", "--version", "tar", "--version") + testSuite := &RedisDumpTestSuite{ + resticExists: resticExists, + } + suite.Run(t, testSuite) } // redisDoBackup populates a database with test data and performs a backup @@ -285,7 +298,11 @@ func redisDoRestore( // pull data from restic repository if needed if useRestic { - err = commons.DoResticRestore(ctx, resticContainer, backupPath) + err = os.Remove(path) + if err != nil { + return testStruct{}, errors.Wrapf(err, "error while removing redis dump before restoring it") + } + err = commons.DoResticRestore(ctx, resticContainer, path) if err != nil { return testStruct{}, errors.WithStack(err) } diff --git a/test/pkg/source/tar/tar_test.go b/test/pkg/source/tar/tar_test.go index 60ec6d49..f2177f9c 100644 --- a/test/pkg/source/tar/tar_test.go +++ b/test/pkg/source/tar/tar_test.go @@ -25,6 +25,7 @@ const extractedPath = "/tmp/testdata/tarTestFile.yaml" type TarTestSuite struct { suite.Suite + resticExists bool } func (tarTestSuite *TarTestSuite) SetupTest() { @@ -70,7 +71,11 @@ func (tarTestSuite *TarTestSuite) TestBasicTarDump() { } func TestTarTestSuite(t *testing.T) { - suite.Run(t, new(TarTestSuite)) + _, resticExists := commons.CheckProgramsAndRestic(t, "tar", "--version") + testSuite := &TarTestSuite{ + resticExists: resticExists, + } + suite.Run(t, testSuite) } // tarDoBackup uses brudi to compress a test file into a tar.gz archive and returns the uncompressed files md5 hash @@ -121,7 +126,15 @@ func hashFile(filename string) (string, error) { // createTarConfig creates a brudi config for the tar commands func createTarConfig() []byte { + //restoreTarget := "/tmp" + /*stdinFilename := "" + if doStdinBackup { + stdinFilename = fmt.Sprintf(" backup:\n flags:\n stdinFilename: %s\n", + backupPath) + //restoreTarget = path.Join(restoreTarget, targetPath) + }*/ return []byte(fmt.Sprintf( + //doPipingBackup: %t ` tar: options: