forked from toolshed/abra
Revert "Revert "feat: backup revolution""
This reverts commit 2c515ce70a
.
This commit is contained in:
@ -1,223 +1,82 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"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"
|
||||
recipePkg "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"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
type restoreConfig struct {
|
||||
preHookCmd string
|
||||
postHookCmd string
|
||||
var targetPath string
|
||||
var targetPathFlag = &cli.StringFlag{
|
||||
Name: "target, t",
|
||||
Usage: "Target path",
|
||||
Destination: &targetPath,
|
||||
}
|
||||
|
||||
var appRestoreCommand = cli.Command{
|
||||
Name: "restore",
|
||||
Aliases: []string{"rs"},
|
||||
Usage: "Run app restore",
|
||||
ArgsUsage: "<domain> <service> <file>",
|
||||
Usage: "Restore an app backup",
|
||||
ArgsUsage: "<domain> <service>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.OfflineFlag,
|
||||
internal.ChaosFlag,
|
||||
targetPathFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
BashComplete: autocomplete.AppNameComplete,
|
||||
Description: `
|
||||
Run an app restore.
|
||||
|
||||
Pre/post hook commands are defined in the recipe configuration. Abra reads this
|
||||
configuration and run the comands in the context of the service before
|
||||
restoring the backup.
|
||||
|
||||
Unlike "abra app backup", restore must be run on a per-service basis. You can
|
||||
not restore all services in one go. Backup files produced by Abra are
|
||||
compressed archives which use absolute paths. This allows Abra to restore
|
||||
according to standard tar command logic, i.e. the backup will be restored to
|
||||
the path it was originally backed up from.
|
||||
|
||||
Example:
|
||||
|
||||
abra app restore example.com app ~/.abra/backups/example_com_app_609341138.tar.gz
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
|
||||
recipe, err := recipe.Get(app.Recipe, internal.Offline)
|
||||
if err != nil {
|
||||
if err := recipe.EnsureExists(app.Recipe); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if !internal.Chaos {
|
||||
if err := recipePkg.EnsureIsClean(app.Recipe); err != nil {
|
||||
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if !internal.Offline {
|
||||
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil {
|
||||
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := recipePkg.EnsureLatest(app.Recipe); err != nil {
|
||||
if err := recipe.EnsureLatest(app.Recipe); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
serviceName := c.Args().Get(1)
|
||||
if serviceName == "" {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("missing <service>?"))
|
||||
}
|
||||
|
||||
backupPath := c.Args().Get(2)
|
||||
if backupPath == "" {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("missing <file>?"))
|
||||
}
|
||||
|
||||
if _, err := os.Stat(backupPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
logrus.Fatalf("%s doesn't exist?", backupPath)
|
||||
}
|
||||
}
|
||||
|
||||
restoreConfigs := make(map[string]restoreConfig)
|
||||
for _, service := range recipe.Config.Services {
|
||||
if restoreEnabled, ok := service.Deploy.Labels["backupbot.restore"]; ok {
|
||||
if restoreEnabled == "true" {
|
||||
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), service.Name)
|
||||
rsConfig := restoreConfig{}
|
||||
|
||||
logrus.Debugf("restore config detected for %s", fullServiceName)
|
||||
|
||||
if preHookCmd, ok := service.Deploy.Labels["backupbot.restore.pre-hook"]; ok {
|
||||
logrus.Debugf("detected pre-hook command for %s: %s", fullServiceName, preHookCmd)
|
||||
rsConfig.preHookCmd = preHookCmd
|
||||
}
|
||||
|
||||
if postHookCmd, ok := service.Deploy.Labels["backupbot.restore.post-hook"]; ok {
|
||||
logrus.Debugf("detected post-hook command for %s: %s", fullServiceName, postHookCmd)
|
||||
rsConfig.postHookCmd = postHookCmd
|
||||
}
|
||||
|
||||
restoreConfigs[service.Name] = rsConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rsConfig, ok := restoreConfigs[serviceName]
|
||||
if !ok {
|
||||
rsConfig = restoreConfig{}
|
||||
}
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := runRestore(cl, app, backupPath, serviceName, rsConfig); err != nil {
|
||||
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
|
||||
if snapshot != "" {
|
||||
logrus.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
|
||||
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
|
||||
}
|
||||
if targetPath != "" {
|
||||
logrus.Debugf("including TARGET=%s in backupbot exec invocation", targetPath)
|
||||
execEnv = append(execEnv, fmt.Sprintf("TARGET=%s", targetPath))
|
||||
}
|
||||
|
||||
if err := internal.RunBackupCmdRemote(cl, "restore", targetContainer.ID, execEnv); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// runRestore does the actual restore logic.
|
||||
func runRestore(cl *dockerClient.Client, app config.App, backupPath, serviceName string, rsConfig restoreConfig) error {
|
||||
// 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 rsConfig.preHookCmd != "" {
|
||||
splitCmd := internal.SafeSplit(rsConfig.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 {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("succesfully ran %s pre-hook command: %s", fullServiceName, rsConfig.preHookCmd)
|
||||
}
|
||||
|
||||
backupReader, err := os.Open(backupPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content, err := archive.DecompressStream(backupReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// NOTE(d1): we use absolute paths so tar knows what to do. it will restore
|
||||
// files according to the paths set in the compressed archive
|
||||
restorePath := "/"
|
||||
|
||||
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
|
||||
if err := cl.CopyToContainer(context.Background(), targetContainer.ID, restorePath, content, copyOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("restored %s to %s", backupPath, fullServiceName)
|
||||
|
||||
if rsConfig.postHookCmd != "" {
|
||||
splitCmd := internal.SafeSplit(rsConfig.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, rsConfig.postHookCmd)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user