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: ` 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. 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) 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 ?")) } 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, serviceName, rsConfig); err != nil { logrus.Fatal(err) } return nil }, } // runRestore does the actual restore logic. func runRestore(app config.App, backupPath, 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 } // we use absolute paths so tar knows what to do. it will restore files // according to the paths set in the compresed 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 }