package cli import ( "archive/tar" "bytes" "context" "encoding/json" "errors" "fmt" "io" "log" "net/http" "path/filepath" "strconv" "strings" // used for replacing abra code "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/config" containerPkg "coopcloud.tech/abra/pkg/container" "coopcloud.tech/abra/pkg/i18n" "coopcloud.tech/abra/pkg/service" "github.com/docker/docker/api/types" containertypes "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" dockerClient "github.com/docker/docker/client" "github.com/docker/docker/api/types/container" "github.com/docker/docker/pkg/stdcopy" ) func (h *abraHandler) pingBackupBot(w http.ResponseWriter, r *http.Request, server string) { cl, err := client.New(server) if err != nil { InternalServerError(w, err) } _, err = RetrieveBackupBotContainer(cl) if err != nil { InternalServerError(w, err) } w.WriteHeader(http.StatusOK) } func (h *abraHandler) handleBackupCreateApp(w http.ResponseWriter, r *http.Request, appName string) { log.Printf("Handling App Backup!") args := []string{} _, err := handleBackupCommand(appName, args, "create") if err != nil { InternalServerError(w, err) } log.Printf("Finishing app backup!") w.WriteHeader(http.StatusOK) } func (h *abraHandler) handleBackupRestoreApp(w http.ResponseWriter, r *http.Request, appName string) { log.Printf("Handling Backup Restore!") args := []string{} d := json.NewDecoder(r.Body) d.DisallowUnknownFields() body := struct { Snapshot *string `json:"snapshot"` }{} err := d.Decode(&body) if err != nil { log.Printf("error: %v", err) BadRequest(w, err) return } if body.Snapshot != nil { args = append(args, fmt.Sprintf("SNAPSHOT=%s", *body.Snapshot)) } args = append(args, fmt.Sprintf("NONINTERACTIVE=%v", true)) _, err = handleBackupCommand(appName, args, "restore") if err != nil { InternalServerError(w, err) } log.Printf("Finishing app restore!") w.WriteHeader(http.StatusOK) } func (h *abraHandler) handleBackupListApp(w http.ResponseWriter, r *http.Request, appName string) { log.Printf("Handling App List Backup!") args := []string{} output, err := handleBackupCommand(appName, args, "snapshots") if err != nil { InternalServerError(w, err) } snapshots := strings.Split(output, "\n") resp := []AppSnapshot{} for _, snapshot := range snapshots { items := strings.Split(snapshot, " ") if len(items) < 3 { break } resp = append(resp, AppSnapshot{ CreatedAt: items[0], Snapshot: items[1], Version: items[2], }) } jsonBytes, err := json.Marshal(resp) if err != nil { InternalServerError(w, err) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write(jsonBytes) } func (h *abraHandler) handleBackupDownloadSnapshot(w http.ResponseWriter, r *http.Request, appName string) { log.Printf("Handling Snapshot Download!") app, err := app.Get(appName) if err != nil { InternalServerError(w, err) } cl, err := client.New(app.Server) if err != nil { InternalServerError(w, err) } targetContainer, err := RetrieveBackupBotContainer(cl) if err != nil { InternalServerError(w, err) } execEnv := []string{ fmt.Sprintf("SERVICE=%s", app.Domain), "MACHINE_LOGS=true", } // TODO: add args _, err = RunBackupCmdRemote(cl, "download", targetContainer.ID, execEnv) if err != nil { InternalServerError(w, err) } remoteBackupDir := "/tmp/backup.tar.gz" content, size, err := CopyFromContainer(cl, targetContainer.ID, remoteBackupDir) if err != nil { InternalServerError(w, fmt.Errorf("error copying from container: %v", err)) } defer content.Close() w.Header().Set("Content-type", "application/x-tar") tr := tar.NewReader(content) for { header, err := tr.Next() if err == io.EOF { break } if err != nil { InternalServerError(w, err) } if filepath.Base(header.Name) == "backup.tar.gz" { w.Header().Set( "Content-Disposition", `attachment; filename="backup.tar.gz"`, ) w.Header().Set("Content-Type", "application/gzip") w.Header().Set( "Content-Length", strconv.FormatInt(size, 10), ) if _, err := io.Copy(w, tr); err != nil { InternalServerError(w, err) } return } } } func handleBackupCommand(appName string, args []string, action string) (string, error) { app, err := app.Get(appName) if err != nil { return "", err } cl, err := client.New(app.Server) if err != nil { return "", err } targetContainer, err := RetrieveBackupBotContainer(cl) if err != nil { return "", err } execEnv := []string{ fmt.Sprintf("SERVICE=%s", app.Domain), "MACHINE_LOGS=true", } for _, arg := range args { execEnv = append(execEnv, arg) } output, err := RunBackupCmdRemote(cl, action, targetContainer.ID, execEnv) if err != nil { return "", err } return output, nil } // copied from https://git.coopcloud.tech/toolshed/abra/src/branch/main/cli/internal/backup.go // tty is set to false func RunBackupCmdRemote( cl *dockerClient.Client, backupCmd string, containerID string, execEnv []string) (string, error) { execBackupListOpts := containertypes.ExecOptions{ AttachStderr: true, AttachStdout: true, Cmd: []string{"/usr/bin/backup", "--", backupCmd}, Env: execEnv, Tty: false, } stdout, err := Exec(context.Background(), cl, containerID, &execBackupListOpts) log.Printf("stdout: %v | err: %v", stdout, err) if err != nil { return "", err } return stdout, nil } func RetrieveBackupBotContainer(cl *dockerClient.Client) (types.Container, error) { ctx := context.Background() chosenService, err := service.GetServiceByLabel(ctx, cl, config.BackupbotLabel, true) if err != nil { return types.Container{}, errors.New(i18n.G("no backupbot discovered, is it deployed?")) } filters := filters.NewArgs() filters.Add("name", chosenService.Spec.Name) targetContainer, err := containerPkg.GetContainer( ctx, cl, filters, true, ) if err != nil { return types.Container{}, err } return targetContainer, nil } func Exec(ctx context.Context, cli *dockerClient.Client, containerID string, execOptions *container.ExecOptions) (string, error) { execResp, err := cli.ContainerExecCreate(ctx, containerID, *execOptions) if err != nil { return "", err } resp, err := cli.ContainerExecAttach(ctx, execResp.ID, container.ExecStartOptions{ Tty: false, }) if err != nil { return "", err } defer resp.Close() var stdout, stderr bytes.Buffer _, err = stdcopy.StdCopy(&stdout, &stderr, resp.Reader) if err != nil { return "", err } inspect, err := cli.ContainerExecInspect(ctx, execResp.ID) if err != nil { return "", err } if inspect.ExitCode != 0 { return stdout.String(), fmt.Errorf("command exited with status %d and error %s", inspect.ExitCode, stderr.String()) } return stdout.String(), nil }