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" "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" "github.com/docker/docker/pkg/archive" "github.com/sirupsen/logrus" "github.com/urfave/cli" ) type restoreConfig struct { preHookCmd string postHookCmd string } var appRestoreCommand = cli.Command{ Name: "restore", Aliases: []string{"rs"}, Usage: "Run app restore", ArgsUsage: " ", Flags: []cli.Flag{ internal.DebugFlag, }, Before: internal.SubCommandBefore, BashComplete: autocomplete.AppNameComplete, Description: ` This command runs an app restore. Pre/post hook commands are defined in the recipe configuration. Abra reads this config and run the comands in the context of the service before restoring the backup. Example: abra app restore example.com app mybackup.tar.gz /var/lib/content `, Action: func(c *cli.Context) error { app := internal.ValidateApp(c) serviceName := c.Args().Get(1) if serviceName == "" { internal.ShowSubcommandHelpAndError(c, errors.New("missing ?")) } backupPath := c.Args().Get(2) if backupPath == "" { internal.ShowSubcommandHelpAndError(c, errors.New("missing ?")) } restorePath := c.Args().Get(3) if restorePath == "" { internal.ShowSubcommandHelpAndError(c, errors.New("missing ?")) } if _, err := os.Stat(backupPath); err != nil { if os.IsNotExist(err) { logrus.Fatalf("%s doesn't exist?", backupPath) } } recipe, err := recipe.Get(app.Recipe) if err != nil { logrus.Fatal(err) } 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{} } if err := runRestore(app, backupPath, restorePath, serviceName, rsConfig); err != nil { logrus.Fatal(err) } return nil }, } // runRestore does the actual restore logic. func runRestore(app config.App, backupPath, restorePath, serviceName string, rsConfig restoreConfig) error { cl, err := client.New(app.Server) if err != nil { return err } // 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 } 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:%s", backupPath, fullServiceName, restorePath) 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 }