2022-03-30 23:53:11 +00:00
|
|
|
package app
|
|
|
|
|
|
|
|
import (
|
2022-04-19 10:52:30 +00:00
|
|
|
"archive/tar"
|
2022-03-30 23:53:11 +00:00
|
|
|
"context"
|
|
|
|
"fmt"
|
2022-04-19 10:52:30 +00:00
|
|
|
"io"
|
|
|
|
"os"
|
2022-03-30 23:53:11 +00:00
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"coopcloud.tech/abra/cli/internal"
|
|
|
|
"coopcloud.tech/abra/pkg/autocomplete"
|
|
|
|
"coopcloud.tech/abra/pkg/client"
|
|
|
|
"coopcloud.tech/abra/pkg/config"
|
|
|
|
containerPkg "coopcloud.tech/abra/pkg/container"
|
|
|
|
"coopcloud.tech/abra/pkg/recipe"
|
|
|
|
"coopcloud.tech/abra/pkg/upstream/container"
|
|
|
|
"github.com/docker/cli/cli/command"
|
|
|
|
"github.com/docker/docker/api/types"
|
|
|
|
"github.com/docker/docker/api/types/filters"
|
2023-01-31 15:09:09 +00:00
|
|
|
dockerClient "github.com/docker/docker/client"
|
2022-04-19 10:52:30 +00:00
|
|
|
"github.com/docker/docker/pkg/archive"
|
|
|
|
"github.com/docker/docker/pkg/system"
|
|
|
|
"github.com/klauspost/pgzip"
|
2022-03-30 23:53:11 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/urfave/cli"
|
|
|
|
)
|
|
|
|
|
|
|
|
type backupConfig struct {
|
|
|
|
preHookCmd string
|
|
|
|
postHookCmd string
|
|
|
|
backupPaths []string
|
|
|
|
}
|
|
|
|
|
|
|
|
var appBackupCommand = cli.Command{
|
|
|
|
Name: "backup",
|
|
|
|
Aliases: []string{"bk"},
|
|
|
|
Usage: "Run app backup",
|
|
|
|
ArgsUsage: "<domain> [<service>]",
|
|
|
|
Flags: []cli.Flag{
|
|
|
|
internal.DebugFlag,
|
|
|
|
},
|
|
|
|
Before: internal.SubCommandBefore,
|
|
|
|
BashComplete: autocomplete.AppNameComplete,
|
|
|
|
Description: `
|
2022-05-13 14:44:49 +00:00
|
|
|
Run an app backup.
|
2022-03-30 23:53:11 +00:00
|
|
|
|
|
|
|
A backup command and pre/post hook commands are defined in the recipe
|
2022-04-19 11:01:04 +00:00
|
|
|
configuration. Abra reads this configuration and run the comands in the context
|
|
|
|
of the deployed services. Pass <service> if you only want to back up a single
|
|
|
|
service. All backups are placed in the ~/.abra/backups directory.
|
2022-03-30 23:53:11 +00:00
|
|
|
|
2022-04-19 11:01:04 +00:00
|
|
|
A single backup file is produced for all backup paths specified for a service.
|
|
|
|
If we have the following backup configuration:
|
2022-03-30 23:53:11 +00:00
|
|
|
|
2022-04-19 11:01:04 +00:00
|
|
|
- "backupbot.backup.path=/var/lib/foo,/var/lib/bar"
|
|
|
|
|
|
|
|
And we run "abra app backup example.com app", Abra will produce a file that
|
|
|
|
looks like:
|
|
|
|
|
|
|
|
~/.abra/backups/example_com_app_609341138.tar.gz
|
|
|
|
|
|
|
|
This file is a compressed archive which contains all backup paths. To see paths, run:
|
|
|
|
|
|
|
|
tar -tf ~/.abra/backups/example_com_app_609341138.tar.gz
|
|
|
|
|
|
|
|
(Make sure to change the name of the backup file)
|
|
|
|
|
|
|
|
This single file can be used to restore your app. See "abra app restore" for more.
|
2022-03-30 23:53:11 +00:00
|
|
|
`,
|
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
app := internal.ValidateApp(c)
|
|
|
|
|
2023-01-31 15:09:09 +00:00
|
|
|
cl, err := client.New(app.Server)
|
|
|
|
if err != nil {
|
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2022-03-30 23:53:11 +00:00
|
|
|
recipe, err := recipe.Get(app.Recipe)
|
|
|
|
if err != nil {
|
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
backupConfigs := make(map[string]backupConfig)
|
|
|
|
for _, service := range recipe.Config.Services {
|
|
|
|
if backupsEnabled, ok := service.Deploy.Labels["backupbot.backup"]; ok {
|
|
|
|
if backupsEnabled == "true" {
|
|
|
|
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), service.Name)
|
|
|
|
bkConfig := backupConfig{}
|
|
|
|
|
|
|
|
logrus.Debugf("backup config detected for %s", fullServiceName)
|
|
|
|
|
|
|
|
if paths, ok := service.Deploy.Labels["backupbot.backup.path"]; ok {
|
|
|
|
logrus.Debugf("detected backup paths for %s: %s", fullServiceName, paths)
|
|
|
|
bkConfig.backupPaths = strings.Split(paths, ",")
|
|
|
|
}
|
|
|
|
|
|
|
|
if preHookCmd, ok := service.Deploy.Labels["backupbot.backup.pre-hook"]; ok {
|
|
|
|
logrus.Debugf("detected pre-hook command for %s: %s", fullServiceName, preHookCmd)
|
|
|
|
bkConfig.preHookCmd = preHookCmd
|
|
|
|
}
|
|
|
|
|
|
|
|
if postHookCmd, ok := service.Deploy.Labels["backupbot.backup.post-hook"]; ok {
|
|
|
|
logrus.Debugf("detected post-hook command for %s: %s", fullServiceName, postHookCmd)
|
|
|
|
bkConfig.postHookCmd = postHookCmd
|
|
|
|
}
|
|
|
|
|
|
|
|
backupConfigs[service.Name] = bkConfig
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
serviceName := c.Args().Get(1)
|
|
|
|
if serviceName != "" {
|
|
|
|
backupConfig, ok := backupConfigs[serviceName]
|
|
|
|
if !ok {
|
|
|
|
logrus.Fatalf("no backup config for %s? does %s exist?", serviceName, serviceName)
|
|
|
|
}
|
2022-04-19 11:50:13 +00:00
|
|
|
|
|
|
|
logrus.Infof("running backup for the %s service", serviceName)
|
|
|
|
|
2023-01-31 15:09:09 +00:00
|
|
|
if err := runBackup(cl, app, serviceName, backupConfig); err != nil {
|
2022-03-30 23:53:11 +00:00
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
|
|
|
} else {
|
2023-01-22 17:50:45 +00:00
|
|
|
if len(backupConfigs) == 0 {
|
|
|
|
logrus.Fatalf("no backup configs discovered for %s?", app.Name)
|
|
|
|
}
|
|
|
|
|
2022-03-30 23:53:11 +00:00
|
|
|
for serviceName, backupConfig := range backupConfigs {
|
2022-04-19 11:50:13 +00:00
|
|
|
logrus.Infof("running backup for the %s service", serviceName)
|
|
|
|
|
2023-01-31 15:09:09 +00:00
|
|
|
if err := runBackup(cl, app, serviceName, backupConfig); err != nil {
|
2022-03-30 23:53:11 +00:00
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2023-01-22 17:50:27 +00:00
|
|
|
// TimeStamp generates a file name friendly timestamp.
|
|
|
|
func TimeStamp() string {
|
|
|
|
ts := time.Now().UTC().Format(time.RFC3339)
|
|
|
|
return strings.Replace(ts, ":", "-", -1)
|
|
|
|
}
|
|
|
|
|
2022-03-30 23:53:11 +00:00
|
|
|
// runBackup does the actual backup logic.
|
2023-01-31 15:09:09 +00:00
|
|
|
func runBackup(cl *dockerClient.Client, app config.App, serviceName string, bkConfig backupConfig) error {
|
2022-03-30 23:53:11 +00:00
|
|
|
if len(bkConfig.backupPaths) == 0 {
|
|
|
|
return fmt.Errorf("backup paths are empty for %s?", serviceName)
|
|
|
|
}
|
|
|
|
|
|
|
|
// FIXME: avoid instantiating a new CLI
|
|
|
|
dcli, err := command.NewDockerCli()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
filters := filters.NewArgs()
|
|
|
|
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
|
|
|
|
|
|
|
|
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, true)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
|
|
|
|
if bkConfig.preHookCmd != "" {
|
|
|
|
splitCmd := internal.SafeSplit(bkConfig.preHookCmd)
|
|
|
|
|
|
|
|
logrus.Debugf("split pre-hook command for %s into %s", fullServiceName, splitCmd)
|
|
|
|
|
|
|
|
preHookExecOpts := types.ExecConfig{
|
|
|
|
AttachStderr: true,
|
|
|
|
AttachStdin: true,
|
|
|
|
AttachStdout: true,
|
|
|
|
Cmd: splitCmd,
|
|
|
|
Detach: false,
|
|
|
|
Tty: true,
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := container.RunExec(dcli, cl, targetContainer.ID, &preHookExecOpts); err != nil {
|
2022-04-19 10:52:30 +00:00
|
|
|
return fmt.Errorf("failed to run %s on %s: %s", bkConfig.preHookCmd, targetContainer.ID, err.Error())
|
2022-03-30 23:53:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
logrus.Infof("succesfully ran %s pre-hook command: %s", fullServiceName, bkConfig.preHookCmd)
|
|
|
|
}
|
|
|
|
|
2022-04-19 10:52:30 +00:00
|
|
|
var tempBackupPaths []string
|
2022-03-30 23:53:11 +00:00
|
|
|
for _, remoteBackupPath := range bkConfig.backupPaths {
|
|
|
|
sanitisedPath := strings.ReplaceAll(remoteBackupPath, "/", "_")
|
2023-01-22 17:50:27 +00:00
|
|
|
localBackupPath := filepath.Join(config.BACKUP_DIR, fmt.Sprintf("%s%s_%s.tar.gz", fullServiceName, sanitisedPath, TimeStamp()))
|
2022-04-19 10:52:30 +00:00
|
|
|
logrus.Debugf("temporarily backing up %s:%s to %s", fullServiceName, remoteBackupPath, localBackupPath)
|
|
|
|
|
|
|
|
logrus.Infof("backing up %s:%s", fullServiceName, remoteBackupPath)
|
2022-03-30 23:53:11 +00:00
|
|
|
|
|
|
|
content, _, err := cl.CopyFromContainer(context.Background(), targetContainer.ID, remoteBackupPath)
|
|
|
|
if err != nil {
|
2022-04-19 10:52:30 +00:00
|
|
|
logrus.Debugf("failed to copy %s from container: %s", remoteBackupPath, err.Error())
|
|
|
|
if err := cleanupTempArchives(tempBackupPaths); err != nil {
|
|
|
|
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
|
|
|
|
}
|
|
|
|
return fmt.Errorf("failed to copy %s from container: %s", remoteBackupPath, err.Error())
|
2022-03-30 23:53:11 +00:00
|
|
|
}
|
|
|
|
defer content.Close()
|
|
|
|
|
2022-04-19 10:52:30 +00:00
|
|
|
_, srcBase := archive.SplitPathDirEntry(remoteBackupPath)
|
|
|
|
preArchive := archive.RebaseArchiveEntries(content, srcBase, remoteBackupPath)
|
|
|
|
if err := copyToFile(localBackupPath, preArchive); err != nil {
|
|
|
|
logrus.Debugf("failed to create tar archive (%s): %s", localBackupPath, err.Error())
|
|
|
|
if err := cleanupTempArchives(tempBackupPaths); err != nil {
|
|
|
|
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
|
|
|
|
}
|
|
|
|
return fmt.Errorf("failed to create tar archive (%s): %s", localBackupPath, err.Error())
|
2022-03-30 23:53:11 +00:00
|
|
|
}
|
|
|
|
|
2022-04-19 10:52:30 +00:00
|
|
|
tempBackupPaths = append(tempBackupPaths, localBackupPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
logrus.Infof("compressing and merging archives...")
|
|
|
|
|
|
|
|
if err := mergeArchives(tempBackupPaths, fullServiceName); err != nil {
|
|
|
|
logrus.Debugf("failed to merge archive files: %s", err.Error())
|
|
|
|
if err := cleanupTempArchives(tempBackupPaths); err != nil {
|
|
|
|
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
|
2022-03-30 23:53:11 +00:00
|
|
|
}
|
2022-04-19 10:52:30 +00:00
|
|
|
return fmt.Errorf("failed to merge archive files: %s", err.Error())
|
|
|
|
}
|
2022-03-30 23:53:11 +00:00
|
|
|
|
2022-04-19 10:52:30 +00:00
|
|
|
if err := cleanupTempArchives(tempBackupPaths); err != nil {
|
|
|
|
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
|
2022-03-30 23:53:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if bkConfig.postHookCmd != "" {
|
|
|
|
splitCmd := internal.SafeSplit(bkConfig.postHookCmd)
|
|
|
|
|
|
|
|
logrus.Debugf("split post-hook command for %s into %s", fullServiceName, splitCmd)
|
|
|
|
|
|
|
|
postHookExecOpts := types.ExecConfig{
|
|
|
|
AttachStderr: true,
|
|
|
|
AttachStdin: true,
|
|
|
|
AttachStdout: true,
|
|
|
|
Cmd: splitCmd,
|
|
|
|
Detach: false,
|
|
|
|
Tty: true,
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := container.RunExec(dcli, cl, targetContainer.ID, &postHookExecOpts); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
logrus.Infof("succesfully ran %s post-hook command: %s", fullServiceName, bkConfig.postHookCmd)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2022-04-19 10:52:30 +00:00
|
|
|
|
|
|
|
func copyToFile(outfile string, r io.Reader) error {
|
|
|
|
tmpFile, err := system.TempFileSequential(filepath.Dir(outfile), ".tar_temp")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
tmpPath := tmpFile.Name()
|
|
|
|
|
|
|
|
_, err = io.Copy(tmpFile, r)
|
|
|
|
tmpFile.Close()
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
os.Remove(tmpPath)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = os.Rename(tmpPath, outfile); err != nil {
|
|
|
|
os.Remove(tmpPath)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func cleanupTempArchives(tarPaths []string) error {
|
|
|
|
for _, tarPath := range tarPaths {
|
|
|
|
if err := os.RemoveAll(tarPath); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
logrus.Debugf("remove temporary archive file %s", tarPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func mergeArchives(tarPaths []string, serviceName string) error {
|
|
|
|
var out io.Writer
|
|
|
|
var cout *pgzip.Writer
|
|
|
|
|
2023-01-22 17:50:27 +00:00
|
|
|
localBackupPath := filepath.Join(config.BACKUP_DIR, fmt.Sprintf("%s_%s.tar.gz", serviceName, TimeStamp()))
|
2022-04-19 10:52:30 +00:00
|
|
|
|
|
|
|
fout, err := os.Create(localBackupPath)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Failed to open %s: %s", localBackupPath, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer fout.Close()
|
|
|
|
out = fout
|
|
|
|
|
|
|
|
cout = pgzip.NewWriter(out)
|
|
|
|
out = cout
|
|
|
|
|
|
|
|
tw := tar.NewWriter(out)
|
|
|
|
|
|
|
|
for _, tarPath := range tarPaths {
|
|
|
|
if err := addTar(tw, tarPath); err != nil {
|
|
|
|
return fmt.Errorf("failed to merge %s: %v", tarPath, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := tw.Close(); err != nil {
|
|
|
|
return fmt.Errorf("failed to close tar writer %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if cout != nil {
|
|
|
|
if err := cout.Flush(); err != nil {
|
|
|
|
return fmt.Errorf("failed to flush: %s", err)
|
|
|
|
} else if err = cout.Close(); err != nil {
|
|
|
|
return fmt.Errorf("failed to close compressed writer: %s", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
logrus.Infof("backed up %s to %s", serviceName, localBackupPath)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func addTar(tw *tar.Writer, pth string) (err error) {
|
|
|
|
var tr *tar.Reader
|
|
|
|
var rc io.ReadCloser
|
|
|
|
var hdr *tar.Header
|
|
|
|
|
|
|
|
if tr, rc, err = openTarFile(pth); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for {
|
|
|
|
if hdr, err = tr.Next(); err != nil {
|
|
|
|
if err == io.EOF {
|
|
|
|
err = nil
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if err = tw.WriteHeader(hdr); err != nil {
|
|
|
|
break
|
|
|
|
} else if _, err = io.Copy(tw, tr); err != nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err == nil {
|
|
|
|
err = rc.Close()
|
|
|
|
} else {
|
|
|
|
rc.Close()
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func openTarFile(pth string) (tr *tar.Reader, rc io.ReadCloser, err error) {
|
|
|
|
var fin *os.File
|
|
|
|
var n int
|
|
|
|
buff := make([]byte, 1024)
|
|
|
|
|
|
|
|
if fin, err = os.Open(pth); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if n, err = fin.Read(buff); err != nil {
|
|
|
|
fin.Close()
|
|
|
|
return
|
|
|
|
} else if n == 0 {
|
|
|
|
fin.Close()
|
|
|
|
err = fmt.Errorf("%s is empty", pth)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err = fin.Seek(0, 0); err != nil {
|
|
|
|
fin.Close()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
rc = fin
|
|
|
|
tr = tar.NewReader(rc)
|
|
|
|
|
|
|
|
return tr, rc, nil
|
|
|
|
}
|