Files

285 lines
6.8 KiB
Go

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
}