From ca91abbed9d0297fc49d9dad8012749217e269d5 Mon Sep 17 00:00:00 2001 From: p4u1 Date: Fri, 22 Dec 2023 12:08:12 +0000 Subject: [PATCH 1/7] fix: correct append service name logic in Filters function (!396) This fixes a regression introduced by #395 Reviewed-on: https://git.coopcloud.tech/coop-cloud/abra/pulls/396 Co-authored-by: p4u1 Co-committed-by: p4u1 --- go.mod | 2 +- pkg/config/app.go | 4 +- pkg/config/app_test.go | 89 ++++++++++++++++++++++ pkg/config/testdir/filtertest.env | 2 + pkg/config/testdir/test-recipe/compose.yml | 6 ++ 5 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 pkg/config/testdir/filtertest.env create mode 100644 pkg/config/testdir/test-recipe/compose.yml diff --git a/go.mod b/go.mod index 5287ec3d..950841f8 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/docker/docker v24.0.7+incompatible github.com/docker/go-units v0.5.0 github.com/go-git/go-git/v5 v5.10.0 + github.com/google/go-cmp v0.5.9 github.com/moby/sys/signal v0.7.0 github.com/moby/term v0.5.0 github.com/olekukonko/tablewriter v0.0.5 @@ -47,7 +48,6 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/go-cmp v0.5.9 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect diff --git a/pkg/config/app.go b/pkg/config/app.go index a0d3e869..38b9ddc6 100644 --- a/pkg/config/app.go +++ b/pkg/config/app.go @@ -95,7 +95,9 @@ func (a App) Filters(appendServiceNames, exactMatch bool, services ...string) (f return filters, nil } - if appendServiceNames { + // When not appending the service name, just add one filter for the whole + // stack. + if !appendServiceNames { f := fmt.Sprintf("%s", a.StackName()) if exactMatch { f = fmt.Sprintf("^%s", f) diff --git a/pkg/config/app_test.go b/pkg/config/app_test.go index 94398a0c..0823f4e2 100644 --- a/pkg/config/app_test.go +++ b/pkg/config/app_test.go @@ -1,12 +1,15 @@ package config_test import ( + "encoding/json" "fmt" "reflect" "testing" "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/recipe" + "github.com/docker/docker/api/types/filters" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" ) @@ -106,3 +109,89 @@ func TestGetComposeFilesError(t *testing.T) { } } } + +func TestFilters(t *testing.T) { + oldDir := config.RECIPES_DIR + config.RECIPES_DIR = "./testdir" + defer func() { + config.RECIPES_DIR = oldDir + }() + + app, err := config.NewApp(config.AppEnv{ + "DOMAIN": "test.example.com", + "RECIPE": "test-recipe", + }, "test_example_com", config.AppFile{ + Path: "./testdir/filtertest.end", + Server: "local", + }) + if err != nil { + t.Fatal(err) + } + + f, err := app.Filters(false, false) + if err != nil { + t.Error(err) + } + compareFilter(t, f, map[string]map[string]bool{ + "name": { + "test_example_com": true, + }, + }) + + f2, err := app.Filters(false, true) + if err != nil { + t.Error(err) + } + compareFilter(t, f2, map[string]map[string]bool{ + "name": { + "^test_example_com": true, + }, + }) + + f3, err := app.Filters(true, false) + if err != nil { + t.Error(err) + } + compareFilter(t, f3, map[string]map[string]bool{ + "name": { + "test_example_com_bar": true, + "test_example_com_foo": true, + }, + }) + + f4, err := app.Filters(true, true) + if err != nil { + t.Error(err) + } + compareFilter(t, f4, map[string]map[string]bool{ + "name": { + "^test_example_com_bar": true, + "^test_example_com_foo": true, + }, + }) + + f5, err := app.Filters(false, false, "foo") + if err != nil { + t.Error(err) + } + compareFilter(t, f5, map[string]map[string]bool{ + "name": { + "test_example_com_foo": true, + }, + }) +} + +func compareFilter(t *testing.T, f1 filters.Args, f2 map[string]map[string]bool) { + t.Helper() + j1, err := f1.MarshalJSON() + if err != nil { + t.Error(err) + } + j2, err := json.Marshal(f2) + if err != nil { + t.Error(err) + } + if diff := cmp.Diff(string(j2), string(j1)); diff != "" { + t.Errorf("filters mismatch (-want +got):\n%s", diff) + } +} diff --git a/pkg/config/testdir/filtertest.env b/pkg/config/testdir/filtertest.env new file mode 100644 index 00000000..9250f3b4 --- /dev/null +++ b/pkg/config/testdir/filtertest.env @@ -0,0 +1,2 @@ +RECIPE=test-recipe +DOMAIN=test.example.com diff --git a/pkg/config/testdir/test-recipe/compose.yml b/pkg/config/testdir/test-recipe/compose.yml new file mode 100644 index 00000000..8232eca0 --- /dev/null +++ b/pkg/config/testdir/test-recipe/compose.yml @@ -0,0 +1,6 @@ +version: "3.8" +services: + foo: + image: debian + bar: + image: debian From c5687dfbd7413f87ad202a4a275da495edae4730 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Sun, 1 Oct 2023 08:02:30 +0200 Subject: [PATCH 2/7] feat: backup revolution See https://git.coopcloud.tech/coop-cloud/organising/issues/485 --- cli/app/backup.go | 564 +++++++++++++-------------------- cli/app/cp.go | 16 +- cli/app/restore.go | 197 ++---------- cli/app/run.go | 2 +- cli/internal/backup.go | 86 +++-- cli/internal/command.go | 4 +- pkg/config/env.go | 2 + pkg/container/container.go | 2 +- pkg/service/service.go | 64 ++++ pkg/upstream/container/exec.go | 23 +- 10 files changed, 401 insertions(+), 559 deletions(-) diff --git a/cli/app/backup.go b/cli/app/backup.go index f9a239b1..f6c8d2ae 100644 --- a/cli/app/backup.go +++ b/cli/app/backup.go @@ -1,414 +1,296 @@ package app import ( - "archive/tar" - "context" "fmt" - "io" - "os" - "path/filepath" - "strings" - "time" "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" - 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/klauspost/pgzip" + "coopcloud.tech/abra/pkg/recipe" "github.com/sirupsen/logrus" "github.com/urfave/cli" ) -type backupConfig struct { - preHookCmd string - postHookCmd string - backupPaths []string +var snapshot string +var snapshotFlag = &cli.StringFlag{ + Name: "snapshot, s", + Usage: "Lists specific snapshot", + Destination: &snapshot, } -var appBackupCommand = cli.Command{ - Name: "backup", - Aliases: []string{"bk"}, - Usage: "Run app backup", - ArgsUsage: " []", +var includePath string +var includePathFlag = &cli.StringFlag{ + Name: "path, p", + Usage: "Include path", + Destination: &includePath, +} + +var resticRepo string +var resticRepoFlag = &cli.StringFlag{ + Name: "repo, r", + Usage: "Restic repository", + Destination: &resticRepo, +} + +var appBackupListCommand = cli.Command{ + Name: "list", + Aliases: []string{"ls"}, Flags: []cli.Flag{ internal.DebugFlag, internal.OfflineFlag, - internal.ChaosFlag, + snapshotFlag, + includePathFlag, }, Before: internal.SubCommandBefore, + Usage: "List all backups", BashComplete: autocomplete.AppNameComplete, - Description: ` -Run an app backup. - -A backup command and pre/post hook commands are defined in the recipe -configuration. Abra reads this configuration and run the comands in the context -of the deployed services. Pass if you only want to back up a single -service. All backups are placed in the ~/.abra/backups directory. - -A single backup file is produced for all backup paths specified for a service. -If we have the following backup configuration: - - - "backupbot.backup.path=/var/lib/foo,/var/lib/bar" - -And we run "abra app backup example.com app", Abra will produce a file that -looks like: - - ~/.abra/backups/example_com_app_609341138.tar.gz - -This file is a compressed archive which contains all backup paths. To see paths, run: - - tar -tf ~/.abra/backups/example_com_app_609341138.tar.gz - -(Make sure to change the name of the backup file) - -This single file can be used to restore your app. See "abra app restore" for more. -`, Action: func(c *cli.Context) error { app := internal.ValidateApp(c) - recipe, err := recipePkg.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) } } - backupConfigs := make(map[string]backupConfig) - for _, service := range recipe.Config.Services { - if backupsEnabled, ok := service.Deploy.Labels["backupbot.backup"]; ok { - if backupsEnabled == "true" { - fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), service.Name) - bkConfig := backupConfig{} - - logrus.Debugf("backup config detected for %s", fullServiceName) - - if paths, ok := service.Deploy.Labels["backupbot.backup.path"]; ok { - logrus.Debugf("detected backup paths for %s: %s", fullServiceName, paths) - bkConfig.backupPaths = strings.Split(paths, ",") - } - - if preHookCmd, ok := service.Deploy.Labels["backupbot.backup.pre-hook"]; ok { - logrus.Debugf("detected pre-hook command for %s: %s", fullServiceName, preHookCmd) - bkConfig.preHookCmd = preHookCmd - } - - if postHookCmd, ok := service.Deploy.Labels["backupbot.backup.post-hook"]; ok { - logrus.Debugf("detected post-hook command for %s: %s", fullServiceName, postHookCmd) - bkConfig.postHookCmd = postHookCmd - } - - backupConfigs[service.Name] = bkConfig - } - } - } - cl, err := client.New(app.Server) if err != nil { logrus.Fatal(err) } - serviceName := c.Args().Get(1) - if serviceName != "" { - backupConfig, ok := backupConfigs[serviceName] - if !ok { - logrus.Fatalf("no backup config for %s? does %s exist?", serviceName, serviceName) - } + targetContainer, err := internal.RetrieveBackupBotContainer(cl) + if err != nil { + logrus.Fatal(err) + } - logrus.Infof("running backup for the %s service", serviceName) + 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 includePath != "" { + logrus.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath) + execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath)) + } - if err := runBackup(cl, app, serviceName, backupConfig); err != nil { - logrus.Fatal(err) - } - } else { - if len(backupConfigs) == 0 { - logrus.Fatalf("no backup configs discovered for %s?", app.Name) - } - - for serviceName, backupConfig := range backupConfigs { - logrus.Infof("running backup for the %s service", serviceName) - - if err := runBackup(cl, app, serviceName, backupConfig); err != nil { - logrus.Fatal(err) - } - } + if err := internal.RunBackupCmdRemote(cl, "ls", targetContainer.ID, execEnv); err != nil { + logrus.Fatal(err) } return nil }, } -// TimeStamp generates a file name friendly timestamp. -func TimeStamp() string { - ts := time.Now().UTC().Format(time.RFC3339) - return strings.Replace(ts, ":", "-", -1) -} +var appBackupDownloadCommand = cli.Command{ + Name: "download", + Aliases: []string{"d"}, + Flags: []cli.Flag{ + internal.DebugFlag, + internal.OfflineFlag, + snapshotFlag, + includePathFlag, + }, + Before: internal.SubCommandBefore, + Usage: "Download a backup", + BashComplete: autocomplete.AppNameComplete, + Action: func(c *cli.Context) error { + app := internal.ValidateApp(c) -// runBackup does the actual backup logic. -func runBackup(cl *dockerClient.Client, app config.App, serviceName string, bkConfig backupConfig) error { - if len(bkConfig.backupPaths) == 0 { - return fmt.Errorf("backup paths are empty for %s?", serviceName) - } - - // 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 bkConfig.preHookCmd != "" { - splitCmd := internal.SafeSplit(bkConfig.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 := recipe.EnsureExists(app.Recipe); err != nil { + logrus.Fatal(err) } - if err := container.RunExec(dcli, cl, targetContainer.ID, &preHookExecOpts); err != nil { - return fmt.Errorf("failed to run %s on %s: %s", bkConfig.preHookCmd, targetContainer.ID, err.Error()) + if !internal.Chaos { + if err := recipe.EnsureIsClean(app.Recipe); err != nil { + logrus.Fatal(err) + } + + if !internal.Offline { + if err := recipe.EnsureUpToDate(app.Recipe); err != nil { + logrus.Fatal(err) + } + } + + if err := recipe.EnsureLatest(app.Recipe); err != nil { + logrus.Fatal(err) + } } - logrus.Infof("succesfully ran %s pre-hook command: %s", fullServiceName, bkConfig.preHookCmd) - } - - var tempBackupPaths []string - for _, remoteBackupPath := range bkConfig.backupPaths { - sanitisedPath := strings.ReplaceAll(remoteBackupPath, "/", "_") - localBackupPath := filepath.Join(config.BACKUP_DIR, fmt.Sprintf("%s%s_%s.tar.gz", fullServiceName, sanitisedPath, TimeStamp())) - logrus.Debugf("temporarily backing up %s:%s to %s", fullServiceName, remoteBackupPath, localBackupPath) - - logrus.Infof("backing up %s:%s", fullServiceName, remoteBackupPath) - - content, _, err := cl.CopyFromContainer(context.Background(), targetContainer.ID, remoteBackupPath) + cl, err := client.New(app.Server) if err != nil { - logrus.Debugf("failed to copy %s from container: %s", remoteBackupPath, err.Error()) - if err := cleanupTempArchives(tempBackupPaths); err != nil { - return fmt.Errorf("failed to clean up temporary archives: %s", err.Error()) + logrus.Fatal(err) + } + + 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 includePath != "" { + logrus.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath) + execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath)) + } + + if err := internal.RunBackupCmdRemote(cl, "download", targetContainer.ID, execEnv); err != nil { + logrus.Fatal(err) + } + + remoteBackupDir := "/tmp/backup.tar.gz" + currentWorkingDir := "." + if err = CopyFromContainer(cl, targetContainer.ID, remoteBackupDir, currentWorkingDir); err != nil { + logrus.Fatal(err) + } + + fmt.Println("backup successfully downloaded to current working directory") + + return nil + }, +} + +var appBackupCreateCommand = cli.Command{ + Name: "create", + Aliases: []string{"c"}, + Flags: []cli.Flag{ + internal.DebugFlag, + internal.OfflineFlag, + resticRepoFlag, + }, + Before: internal.SubCommandBefore, + Usage: "Create a new backup", + BashComplete: autocomplete.AppNameComplete, + Action: func(c *cli.Context) error { + app := internal.ValidateApp(c) + + if err := recipe.EnsureExists(app.Recipe); err != nil { + logrus.Fatal(err) + } + + if !internal.Chaos { + if err := recipe.EnsureIsClean(app.Recipe); err != nil { + logrus.Fatal(err) } - return fmt.Errorf("failed to copy %s from container: %s", remoteBackupPath, err.Error()) - } - defer content.Close() - _, srcBase := archive.SplitPathDirEntry(remoteBackupPath) - preArchive := archive.RebaseArchiveEntries(content, srcBase, remoteBackupPath) - if err := copyToFile(localBackupPath, preArchive); err != nil { - logrus.Debugf("failed to create tar archive (%s): %s", localBackupPath, err.Error()) - if err := cleanupTempArchives(tempBackupPaths); err != nil { - return fmt.Errorf("failed to clean up temporary archives: %s", err.Error()) + if !internal.Offline { + if err := recipe.EnsureUpToDate(app.Recipe); err != nil { + logrus.Fatal(err) + } } - return fmt.Errorf("failed to create tar archive (%s): %s", localBackupPath, err.Error()) - } - tempBackupPaths = append(tempBackupPaths, localBackupPath) - } - - logrus.Infof("compressing and merging archives...") - - if err := mergeArchives(tempBackupPaths, fullServiceName); err != nil { - logrus.Debugf("failed to merge archive files: %s", err.Error()) - if err := cleanupTempArchives(tempBackupPaths); err != nil { - return fmt.Errorf("failed to clean up temporary archives: %s", err.Error()) - } - return fmt.Errorf("failed to merge archive files: %s", err.Error()) - } - - if err := cleanupTempArchives(tempBackupPaths); err != nil { - return fmt.Errorf("failed to clean up temporary archives: %s", err.Error()) - } - - if bkConfig.postHookCmd != "" { - splitCmd := internal.SafeSplit(bkConfig.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, bkConfig.postHookCmd) - } - - return nil -} - -func copyToFile(outfile string, r io.Reader) error { - tmpFile, err := os.CreateTemp(filepath.Dir(outfile), ".tar_temp") - if err != nil { - return err - } - - tmpPath := tmpFile.Name() - - _, err = io.Copy(tmpFile, r) - tmpFile.Close() - - if err != nil { - os.Remove(tmpPath) - return err - } - - if err = os.Rename(tmpPath, outfile); err != nil { - os.Remove(tmpPath) - return err - } - - return nil -} - -func cleanupTempArchives(tarPaths []string) error { - for _, tarPath := range tarPaths { - if err := os.RemoveAll(tarPath); err != nil { - return err - } - - logrus.Debugf("remove temporary archive file %s", tarPath) - } - - return nil -} - -func mergeArchives(tarPaths []string, serviceName string) error { - var out io.Writer - var cout *pgzip.Writer - - localBackupPath := filepath.Join(config.BACKUP_DIR, fmt.Sprintf("%s_%s.tar.gz", serviceName, TimeStamp())) - - fout, err := os.Create(localBackupPath) - if err != nil { - return fmt.Errorf("Failed to open %s: %s", localBackupPath, err) - } - - defer fout.Close() - out = fout - - cout = pgzip.NewWriter(out) - out = cout - - tw := tar.NewWriter(out) - - for _, tarPath := range tarPaths { - if err := addTar(tw, tarPath); err != nil { - return fmt.Errorf("failed to merge %s: %v", tarPath, err) - } - } - - if err := tw.Close(); err != nil { - return fmt.Errorf("failed to close tar writer %v", err) - } - - if cout != nil { - if err := cout.Flush(); err != nil { - return fmt.Errorf("failed to flush: %s", err) - } else if err = cout.Close(); err != nil { - return fmt.Errorf("failed to close compressed writer: %s", err) - } - } - - logrus.Infof("backed up %s to %s", serviceName, localBackupPath) - - return nil -} - -func addTar(tw *tar.Writer, pth string) (err error) { - var tr *tar.Reader - var rc io.ReadCloser - var hdr *tar.Header - - if tr, rc, err = openTarFile(pth); err != nil { - return - } - - for { - if hdr, err = tr.Next(); err != nil { - if err == io.EOF { - err = nil + if err := recipe.EnsureLatest(app.Recipe); err != nil { + logrus.Fatal(err) } - break } - if err = tw.WriteHeader(hdr); err != nil { - break - } else if _, err = io.Copy(tw, tr); err != nil { - break + + cl, err := client.New(app.Server) + if err != nil { + logrus.Fatal(err) } - } - if err == nil { - err = rc.Close() - } else { - rc.Close() - } - return + + targetContainer, err := internal.RetrieveBackupBotContainer(cl) + if err != nil { + logrus.Fatal(err) + } + + execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)} + if resticRepo != "" { + logrus.Debugf("including RESTIC_REPO=%s in backupbot exec invocation", resticRepo) + execEnv = append(execEnv, fmt.Sprintf("RESTIC_REPO=%s", resticRepo)) + } + + if err := internal.RunBackupCmdRemote(cl, "create", targetContainer.ID, execEnv); err != nil { + logrus.Fatal(err) + } + + return nil + }, } -func openTarFile(pth string) (tr *tar.Reader, rc io.ReadCloser, err error) { - var fin *os.File - var n int - buff := make([]byte, 1024) +var appBackupSnapshotsCommand = cli.Command{ + Name: "snapshots", + Aliases: []string{"s"}, + Flags: []cli.Flag{ + internal.DebugFlag, + internal.OfflineFlag, + snapshotFlag, + }, + Before: internal.SubCommandBefore, + Usage: "List backup snapshots", + BashComplete: autocomplete.AppNameComplete, + Action: func(c *cli.Context) error { + app := internal.ValidateApp(c) - if fin, err = os.Open(pth); err != nil { - return - } + if err := recipe.EnsureExists(app.Recipe); err != nil { + logrus.Fatal(err) + } - if n, err = fin.Read(buff); err != nil { - fin.Close() - return - } else if n == 0 { - fin.Close() - err = fmt.Errorf("%s is empty", pth) - return - } + if !internal.Chaos { + if err := recipe.EnsureIsClean(app.Recipe); err != nil { + logrus.Fatal(err) + } - if _, err = fin.Seek(0, 0); err != nil { - fin.Close() - return - } + if !internal.Offline { + if err := recipe.EnsureUpToDate(app.Recipe); err != nil { + logrus.Fatal(err) + } + } - rc = fin - tr = tar.NewReader(rc) + if err := recipe.EnsureLatest(app.Recipe); err != nil { + logrus.Fatal(err) + } + } - return tr, rc, nil + cl, err := client.New(app.Server) + if err != nil { + logrus.Fatal(err) + } + + 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 err := internal.RunBackupCmdRemote(cl, "snapshots", targetContainer.ID, execEnv); err != nil { + logrus.Fatal(err) + } + + return nil + }, +} + +var appBackupCommand = cli.Command{ + Name: "backup", + Aliases: []string{"b"}, + Usage: "Manage app backups", + ArgsUsage: "", + Subcommands: []cli.Command{ + appBackupListCommand, + appBackupSnapshotsCommand, + appBackupDownloadCommand, + appBackupCreateCommand, + }, } diff --git a/cli/app/cp.go b/cli/app/cp.go index bfc2c789..f27b42ce 100644 --- a/cli/app/cp.go +++ b/cli/app/cp.go @@ -76,9 +76,9 @@ And if you want to copy that file back to your current working directory locally logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server) if toContainer { - err = copyToContainer(cl, container.ID, srcPath, dstPath) + err = CopyToContainer(cl, container.ID, srcPath, dstPath) } else { - err = copyFromContainer(cl, container.ID, srcPath, dstPath) + err = CopyFromContainer(cl, container.ID, srcPath, dstPath) } if err != nil { logrus.Fatal(err) @@ -106,9 +106,9 @@ func parseSrcAndDst(src, dst string) (srcPath string, dstPath string, service st return "", "", "", false, errServiceMissing } -// copyToContainer copies a file or directory from the local file system to the container. +// CopyToContainer copies a file or directory from the local file system to the container. // See the possible copy modes and their documentation. -func copyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath string) error { +func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath string) error { srcStat, err := os.Stat(srcPath) if err != nil { return fmt.Errorf("local %s ", err) @@ -140,7 +140,7 @@ func copyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri if err != nil { return err } - if err := container.RunExec(dcli, cl, containerID, &types.ExecConfig{ + if _, err := container.RunExec(dcli, cl, containerID, &types.ExecConfig{ AttachStderr: true, AttachStdin: true, AttachStdout: true, @@ -179,7 +179,7 @@ func copyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri if err != nil { return err } - if err := container.RunExec(dcli, cl, containerID, &types.ExecConfig{ + if _, err := container.RunExec(dcli, cl, containerID, &types.ExecConfig{ AttachStderr: true, AttachStdin: true, AttachStdout: true, @@ -194,9 +194,9 @@ func copyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri return nil } -// copyFromContainer copies a file or directory from the given container to the local file system. +// CopyFromContainer copies a file or directory from the given container to the local file system. // See the possible copy modes and their documentation. -func copyFromContainer(cl *dockerClient.Client, containerID, srcPath, dstPath string) error { +func CopyFromContainer(cl *dockerClient.Client, containerID, srcPath, dstPath string) error { srcStat, err := cl.ContainerStatPath(context.Background(), containerID, srcPath) if err != nil { if errdefs.IsNotFound(err) { diff --git a/cli/app/restore.go b/cli/app/restore.go index 1bf9c840..c80347f5 100644 --- a/cli/app/restore.go +++ b/cli/app/restore.go @@ -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: " ", + Usage: "Restore an app backup", + ArgsUsage: " ", 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 ?")) - } - - 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) - } - } - - 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 -} diff --git a/cli/app/run.go b/cli/app/run.go index 4ae68c1b..b5e0a9ce 100644 --- a/cli/app/run.go +++ b/cli/app/run.go @@ -91,7 +91,7 @@ var appRunCommand = cli.Command{ logrus.Fatal(err) } - if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { + if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { logrus.Fatal(err) } diff --git a/cli/internal/backup.go b/cli/internal/backup.go index 79951810..530735c9 100644 --- a/cli/internal/backup.go +++ b/cli/internal/backup.go @@ -1,35 +1,67 @@ package internal import ( - "strings" + "context" + + "coopcloud.tech/abra/pkg/config" + containerPkg "coopcloud.tech/abra/pkg/container" + "coopcloud.tech/abra/pkg/service" + "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/sirupsen/logrus" ) -// SafeSplit splits up a string into a list of commands safely. -func SafeSplit(s string) []string { - split := strings.Split(s, " ") - - var result []string - var inquote string - var block string - for _, i := range split { - if inquote == "" { - if strings.HasPrefix(i, "'") || strings.HasPrefix(i, "\"") { - inquote = string(i[0]) - block = strings.TrimPrefix(i, inquote) + " " - } else { - result = append(result, i) - } - } else { - if !strings.HasSuffix(i, inquote) { - block += i + " " - } else { - block += strings.TrimSuffix(i, inquote) - inquote = "" - result = append(result, block) - block = "" - } - } +// RetrieveBackupBotContainer gets the deployed backupbot container. +func RetrieveBackupBotContainer(cl *dockerClient.Client) (types.Container, error) { + ctx := context.Background() + chosenService, err := service.GetServiceByLabel(ctx, cl, config.BackupbotLabel, NoInput) + if err != nil { + return types.Container{}, err } - return result + logrus.Debugf("retrieved %s as backup enabled service", chosenService.Spec.Name) + + filters := filters.NewArgs() + filters.Add("name", chosenService.Spec.Name) + targetContainer, err := containerPkg.GetContainer( + ctx, + cl, + filters, + NoInput, + ) + if err != nil { + return types.Container{}, err + } + + return targetContainer, nil +} + +// RunBackupCmdRemote runs a backup related command on a remote backupbot container. +func RunBackupCmdRemote(cl *dockerClient.Client, backupCmd string, containerID string, execEnv []string) error { + execBackupListOpts := types.ExecConfig{ + AttachStderr: true, + AttachStdin: true, + AttachStdout: true, + Cmd: []string{"/usr/bin/backup", "--", backupCmd}, + Detach: false, + Env: execEnv, + Tty: true, + } + + logrus.Debugf("running backup %s on %s with exec config %v", backupCmd, containerID, execBackupListOpts) + + // FIXME: avoid instantiating a new CLI + dcli, err := command.NewDockerCli() + if err != nil { + return err + } + + if _, err := container.RunExec(dcli, cl, containerID, &execBackupListOpts); err != nil { + return err + } + + return nil } diff --git a/cli/internal/command.go b/cli/internal/command.go index 6f02ae1c..13c007be 100644 --- a/cli/internal/command.go +++ b/cli/internal/command.go @@ -60,7 +60,7 @@ func RunCmdRemote(cl *dockerClient.Client, app config.App, abraSh, serviceName, Tty: false, } - if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { + if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { logrus.Infof("%s does not exist for %s, use /bin/sh as fallback", shell, app.Name) shell = "/bin/sh" } @@ -85,7 +85,7 @@ func RunCmdRemote(cl *dockerClient.Client, app config.App, abraSh, serviceName, execCreateOpts.Tty = false } - if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { + if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { return err } diff --git a/pkg/config/env.go b/pkg/config/env.go index 62f6a71d..adedc6f4 100644 --- a/pkg/config/env.go +++ b/pkg/config/env.go @@ -36,6 +36,8 @@ var REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud" var CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json" var SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git" +var BackupbotLabel = "coop-cloud.backupbot.enabled" + // envVarModifiers is a list of env var modifier strings. These are added to // env vars as comments and modify their processing by Abra, e.g. determining // how long secrets should be. diff --git a/pkg/container/container.go b/pkg/container/container.go index 09d5703b..1354b0dd 100644 --- a/pkg/container/container.go +++ b/pkg/container/container.go @@ -28,7 +28,7 @@ func GetContainer(c context.Context, cl *client.Client, filters filters.Args, no return types.Container{}, fmt.Errorf("no containers matching the %v filter found?", filter) } - if len(containers) != 1 { + if len(containers) > 1 { var containersRaw []string for _, container := range containers { containerName := strings.Join(container.Names, " ") diff --git a/pkg/service/service.go b/pkg/service/service.go index 3d92d821..48cdce75 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -14,6 +14,70 @@ import ( "github.com/sirupsen/logrus" ) +// GetService retrieves a service container based on a label. If prompt is true +// and the retrievd count of service containers does not match 1, then a prompt +// is presented to let the user choose. An error is returned when no service is +// found. +func GetServiceByLabel(c context.Context, cl *client.Client, label string, prompt bool) (swarm.Service, error) { + services, err := cl.ServiceList(c, types.ServiceListOptions{}) + if err != nil { + return swarm.Service{}, err + } + + if len(services) == 0 { + return swarm.Service{}, fmt.Errorf("no services deployed?") + } + + var matchingServices []swarm.Service + for _, service := range services { + if enabled, exists := service.Spec.Labels[label]; exists && enabled == "true" { + matchingServices = append(matchingServices, service) + } + } + + if len(matchingServices) == 0 { + return swarm.Service{}, fmt.Errorf("no services deployed matching label '%s'?", label) + } + + if len(matchingServices) > 1 { + var servicesRaw []string + for _, service := range matchingServices { + serviceName := service.Spec.Name + created := formatter.HumanDuration(service.CreatedAt.Unix()) + servicesRaw = append(servicesRaw, fmt.Sprintf("%s (created %v)", serviceName, created)) + } + + if !prompt { + err := fmt.Errorf("expected 1 service but found %v: %s", len(matchingServices), strings.Join(servicesRaw, " ")) + return swarm.Service{}, err + } + + logrus.Warnf("ambiguous service list received, prompting for input") + + var response string + prompt := &survey.Select{ + Message: "which service are you looking for?", + Options: servicesRaw, + } + + if err := survey.AskOne(prompt, &response); err != nil { + return swarm.Service{}, err + } + + chosenService := strings.TrimSpace(strings.Split(response, " ")[0]) + for _, service := range matchingServices { + serviceName := strings.ToLower(service.Spec.Name) + if serviceName == chosenService { + return service, nil + } + } + + logrus.Panic("failed to match chosen service") + } + + return matchingServices[0], nil +} + // GetService retrieves a service container. If prompt is true and the retrievd // count of service containers does not match 1, then a prompt is presented to // let the user choose. A count of 0 is handled gracefully. diff --git a/pkg/upstream/container/exec.go b/pkg/upstream/container/exec.go index e811481a..82a2c570 100644 --- a/pkg/upstream/container/exec.go +++ b/pkg/upstream/container/exec.go @@ -13,7 +13,10 @@ import ( "github.com/sirupsen/logrus" ) -func RunExec(dockerCli command.Cli, client *apiclient.Client, containerID string, execConfig *types.ExecConfig) error { +// RunExec runs a command on a remote container. io.Writer corresponds to the +// command output. +func RunExec(dockerCli command.Cli, client *apiclient.Client, containerID string, + execConfig *types.ExecConfig) (io.Writer, error) { ctx := context.Background() // We need to check the tty _before_ we do the ContainerExecCreate, because @@ -21,22 +24,22 @@ func RunExec(dockerCli command.Cli, client *apiclient.Client, containerID string // there's no easy way to clean those up). But also in order to make "not // exist" errors take precedence we do a dummy inspect first. if _, err := client.ContainerInspect(ctx, containerID); err != nil { - return err + return nil, err } if !execConfig.Detach { if err := dockerCli.In().CheckTty(execConfig.AttachStdin, execConfig.Tty); err != nil { - return err + return nil, err } } response, err := client.ContainerExecCreate(ctx, containerID, *execConfig) if err != nil { - return err + return nil, err } execID := response.ID if execID == "" { - return errors.New("exec ID empty") + return nil, errors.New("exec ID empty") } if execConfig.Detach { @@ -44,13 +47,13 @@ func RunExec(dockerCli command.Cli, client *apiclient.Client, containerID string Detach: execConfig.Detach, Tty: execConfig.Tty, } - return client.ContainerExecStart(ctx, execID, execStartCheck) + return nil, client.ContainerExecStart(ctx, execID, execStartCheck) } return interactiveExec(ctx, dockerCli, client, execConfig, execID) } func interactiveExec(ctx context.Context, dockerCli command.Cli, client *apiclient.Client, - execConfig *types.ExecConfig, execID string) error { + execConfig *types.ExecConfig, execID string) (io.Writer, error) { // Interactive exec requested. var ( out, stderr io.Writer @@ -76,7 +79,7 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, client *apiclie } resp, err := client.ContainerExecAttach(ctx, execID, execStartCheck) if err != nil { - return err + return out, err } defer resp.Close() @@ -107,10 +110,10 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, client *apiclie if err := <-errCh; err != nil { logrus.Debugf("Error hijack: %s", err) - return err + return out, err } - return getExecExitStatus(ctx, client, execID) + return out, getExecExitStatus(ctx, client, execID) } func getExecExitStatus(ctx context.Context, client apiclient.ContainerAPIClient, execID string) error { From 0a3624c15bc46607d2b2b7da33c91e26df4768cf Mon Sep 17 00:00:00 2001 From: p4u1 Date: Fri, 19 Jan 2024 15:08:41 +0000 Subject: [PATCH 3/7] feat: add version input to abra app new (!400) Closes https://git.coopcloud.tech/coop-cloud/organising/issues/519 Reviewed-on: https://git.coopcloud.tech/coop-cloud/abra/pulls/400 Reviewed-by: decentral1se Co-authored-by: p4u1 Co-committed-by: p4u1 --- cli/app/new.go | 24 ++++++++++++--- pkg/autocomplete/autocomplete.go | 14 +++++++++ tests/integration/app_new.bats | 53 +++++++++++++++++++++++++------- 3 files changed, 75 insertions(+), 16 deletions(-) diff --git a/cli/app/new.go b/cli/app/new.go index 22f51fd2..1a430dd3 100644 --- a/cli/app/new.go +++ b/cli/app/new.go @@ -54,9 +54,17 @@ var appNewCommand = cli.Command{ internal.OfflineFlag, internal.ChaosFlag, }, - Before: internal.SubCommandBefore, - ArgsUsage: "[]", - BashComplete: autocomplete.RecipeNameComplete, + Before: internal.SubCommandBefore, + ArgsUsage: "[] []", + BashComplete: func(ctx *cli.Context) { + args := ctx.Args() + switch len(args) { + case 0: + autocomplete.RecipeNameComplete(ctx) + case 1: + autocomplete.RecipeVersionComplete(ctx.Args().Get(0)) + } + }, Action: func(c *cli.Context) error { recipe := internal.ValidateRecipe(c) @@ -69,8 +77,14 @@ var appNewCommand = cli.Command{ logrus.Fatal(err) } } - if err := recipePkg.EnsureLatest(recipe.Name); err != nil { - logrus.Fatal(err) + if c.Args().Get(1) == "" { + if err := recipePkg.EnsureLatest(recipe.Name); err != nil { + logrus.Fatal(err) + } + } else { + if err := recipePkg.EnsureVersion(recipe.Name, c.Args().Get(1)); err != nil { + logrus.Fatal(err) + } } } diff --git a/pkg/autocomplete/autocomplete.go b/pkg/autocomplete/autocomplete.go index 9d5075d6..7b28f134 100644 --- a/pkg/autocomplete/autocomplete.go +++ b/pkg/autocomplete/autocomplete.go @@ -51,6 +51,20 @@ func RecipeNameComplete(c *cli.Context) { } } +// RecipeVersionComplete completes versions for the recipe. +func RecipeVersionComplete(recipeName string) { + catl, err := recipe.ReadRecipeCatalogue(false) + if err != nil { + logrus.Warn(err) + } + + for _, v := range catl[recipeName].Versions { + for v2 := range v { + fmt.Println(v2) + } + } +} + // ServerNameComplete completes server names. func ServerNameComplete(c *cli.Context) { files, err := config.LoadAppFiles("") diff --git a/tests/integration/app_new.bats b/tests/integration/app_new.bats index ac8c94fd..885e02cd 100644 --- a/tests/integration/app_new.bats +++ b/tests/integration/app_new.bats @@ -18,9 +18,24 @@ setup(){ } teardown(){ + load "$PWD/tests/integration/helpers/common" _rm_app } +@test "autocomplete" { + run $ABRA app new --generate-bash-completion + assert_success + assert_output --partial "traefik" + assert_output --partial "abra-test-recipe" + + # Note: this test needs to be updated when a new version of the test recipe is published. + run $ABRA app new abra-test-recipe --generate-bash-completion + assert_success + assert_output "0.1.0+1.20.0 +0.1.1+1.20.2 +0.2.0+1.21.0" +} + @test "create new app" { run $ABRA app new "$TEST_RECIPE" \ --no-input \ @@ -28,10 +43,29 @@ teardown(){ --domain "$TEST_APP_DOMAIN" assert_success assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" + + run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status + assert_output --partial "Your branch is up to date with 'origin/main'." +} + +@test "create new app with version" { + run $ABRA app new "$TEST_RECIPE" 0.1.1+1.20.2 \ + --no-input \ + --server "$TEST_SERVER" \ + --domain "$TEST_APP_DOMAIN" + assert_success + assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" + + run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" log -1 + assert_output --partial "453db7121c0a56a7a8f15378f18fe3bf21ccfdef" } @test "does not overwrite existing env files" { - _new_app + run $ABRA app new "$TEST_RECIPE" \ + --no-input \ + --server "$TEST_SERVER" \ + --domain "$TEST_APP_DOMAIN" + assert_success run $ABRA app new "$TEST_RECIPE" \ --no-input \ @@ -74,8 +108,7 @@ teardown(){ --no-input \ --chaos \ --server "$TEST_SERVER" \ - --domain "$TEST_APP_DOMAIN" \ - --secrets + --domain "$TEST_APP_DOMAIN" assert_success assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" @@ -88,18 +121,17 @@ teardown(){ assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --partial "Your branch is behind 'origin/main' by 3 commits, and can be fast-forwarded." run $ABRA app new "$TEST_RECIPE" \ --no-input \ --server "$TEST_SERVER" \ - --domain "$TEST_APP_DOMAIN" \ - --secrets + --domain "$TEST_APP_DOMAIN" assert_success assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - refute_output --partial 'behind 3' + assert_output --partial "Your branch is up to date with 'origin/main'." _reset_recipe } @@ -109,7 +141,7 @@ teardown(){ assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --partial "Your branch is behind 'origin/main' by 3 commits, and can be fast-forwarded." # NOTE(d1): need to use --chaos to force same commit run $ABRA app new "$TEST_RECIPE" \ @@ -117,13 +149,12 @@ teardown(){ --offline \ --chaos \ --server "$TEST_SERVER" \ - --domain "$TEST_APP_DOMAIN" \ - --secrets + --domain "$TEST_APP_DOMAIN" assert_success assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --partial "Your branch is behind 'origin/main' by 3 commits, and can be fast-forwarded." _reset_recipe } From 4920dfedb38d312e966d8cfab2fdaa6e19c57b71 Mon Sep 17 00:00:00 2001 From: p4u1 Date: Fri, 19 Jan 2024 15:09:00 +0000 Subject: [PATCH 4/7] fix: retry docker volume remove (!399) Closes https://git.coopcloud.tech/coop-cloud/organising/issues/509 Reviewed-on: https://git.coopcloud.tech/coop-cloud/abra/pulls/399 Reviewed-by: decentral1se Co-authored-by: p4u1 Co-committed-by: p4u1 --- cli/app/remove.go | 26 ++++++++++++++++++++++++-- cli/app/remove_test.go | 26 ++++++++++++++++++++++++++ tests/integration/app_remove.bats | 8 +------- 3 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 cli/app/remove_test.go diff --git a/cli/app/remove.go b/cli/app/remove.go index ea4efedf..82499fd1 100644 --- a/cli/app/remove.go +++ b/cli/app/remove.go @@ -3,7 +3,9 @@ package app import ( "context" "fmt" + "log" "os" + "time" "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" @@ -124,9 +126,11 @@ flag. if len(vols) > 0 { for _, vol := range vols { - err := cl.VolumeRemove(context.Background(), vol, internal.Force) // last argument is for force removing + err = retryFunc(5, func() error { + return cl.VolumeRemove(context.Background(), vol, internal.Force) // last argument is for force removing + }) if err != nil { - logrus.Fatal(err) + log.Fatalf("removing volumes failed: %s", err) } logrus.Info(fmt.Sprintf("volume %s removed", vol)) } @@ -143,3 +147,21 @@ flag. return nil }, } + +// retryFunc retries the given function for the given retries. After the nth +// retry it waits (n + 1)^2 seconds before the next retry (starting with n=0). +// It returns an error if the function still failed after the last retry. +func retryFunc(retries int, fn func() error) error { + for i := 0; i < retries; i++ { + err := fn() + if err == nil { + return nil + } + if i+1 < retries { + sleep := time.Duration(i+1) * time.Duration(i+1) + logrus.Infof("%s: waiting %d seconds before next retry", err, sleep) + time.Sleep(sleep * time.Second) + } + } + return fmt.Errorf("%d retries failed", retries) +} diff --git a/cli/app/remove_test.go b/cli/app/remove_test.go new file mode 100644 index 00000000..c3c9f8a0 --- /dev/null +++ b/cli/app/remove_test.go @@ -0,0 +1,26 @@ +package app + +import ( + "fmt" + "testing" +) + +func TestRetryFunc(t *testing.T) { + err := retryFunc(1, func() error { return nil }) + if err != nil { + t.Errorf("should not return an error: %s", err) + } + + i := 0 + fn := func() error { + i++ + return fmt.Errorf("oh no, something went wrong!") + } + err = retryFunc(2, fn) + if err == nil { + t.Error("should return an error") + } + if i != 2 { + t.Errorf("The function should have been called 1 times, got %d", i) + } +} diff --git a/tests/integration/app_remove.bats b/tests/integration/app_remove.bats index 8b53984c..8800e70a 100644 --- a/tests/integration/app_remove.bats +++ b/tests/integration/app_remove.bats @@ -104,10 +104,7 @@ teardown(){ _undeploy_app - # NOTE(d1): to let the stack come down before nuking volumes - sleep 5 - - run $ABRA app volume rm "$TEST_APP_DOMAIN" --force + run $ABRA app volume rm "$TEST_APP_DOMAIN" assert_success run $ABRA app volume ls "$TEST_APP_DOMAIN" @@ -132,9 +129,6 @@ teardown(){ _undeploy_app - # NOTE(d1): to let the stack come down before nuking volumes - sleep 5 - run $ABRA app rm "$TEST_APP_DOMAIN" --no-input assert_success assert_output --partial 'test-volume' From e9b99fe921891eb8ef115f9e6cd6e4ac3050e1f9 Mon Sep 17 00:00:00 2001 From: basebuilder Date: Tue, 16 Jan 2024 17:38:05 +0100 Subject: [PATCH 5/7] make installer save abra-download to /tmp/ directory the current location of download is ~/.local/bin/ but this conflicts with some security tools --- scripts/installer/installer | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/installer/installer b/scripts/installer/installer index 2a73b442..01605942 100755 --- a/scripts/installer/installer +++ b/scripts/installer/installer @@ -65,17 +65,19 @@ function install_abra_release { checksums=$(wget -q -O- $checksums_url) checksum=$(echo "$checksums" | grep "$FILENAME" - | sed -En 's/([0-9a-f]{64})\s+'"$FILENAME"'.*/\1/p') + abra_download="/tmp/abra-download" echo "downloading $ABRA_VERSION $PLATFORM binary release for abra..." - wget -q "$release_url" -O "$HOME/.local/bin/.abra-download" - localsum=$(sha256sum $HOME/.local/bin/.abra-download | sed -En 's/([0-9a-f]{64})\s+.*/\1/p') + + wget -q "$release_url" -O $abra_download + localsum=$(sha256sum $abra_download | sed -En 's/([0-9a-f]{64})\s+.*/\1/p') echo "checking if checksums match..." if [[ "$localsum" != "$checksum" ]]; then print_checksum_error exit 1 fi echo "$(tput setaf 2)check successful!$(tput sgr0)" - mv "$HOME/.local/bin/.abra-download" "$HOME/.local/bin/abra" + mv "$abra_download" "$HOME/.local/bin/abra" chmod +x "$HOME/.local/bin/abra" x=$(echo $PATH | grep $HOME/.local/bin) From 0643df6d73ae5464fa1c29e52c2c5fbb5ae97e30 Mon Sep 17 00:00:00 2001 From: p4u1 Date: Wed, 24 Jan 2024 15:01:33 +0000 Subject: [PATCH 6/7] feat: fetch all recipes when no recipe is specified (!401) Closes https://git.coopcloud.tech/coop-cloud/organising/issues/530 Reviewed-on: https://git.coopcloud.tech/coop-cloud/abra/pulls/401 Reviewed-by: decentral1se Co-authored-by: p4u1 Co-committed-by: p4u1 --- cli/recipe/fetch.go | 22 ++++++++++++++-------- pkg/recipe/recipe.go | 14 ++++++++++++++ tests/integration/recipe_fetch.bats | 12 +++++++++++- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/cli/recipe/fetch.go b/cli/recipe/fetch.go index acfc1708..de81cfec 100644 --- a/cli/recipe/fetch.go +++ b/cli/recipe/fetch.go @@ -3,6 +3,7 @@ package recipe import ( "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" + "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/recipe" "github.com/sirupsen/logrus" "github.com/urfave/cli" @@ -17,26 +18,31 @@ var recipeFetchCommand = cli.Command{ Flags: []cli.Flag{ internal.DebugFlag, internal.NoInputFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, BashComplete: autocomplete.RecipeNameComplete, Action: func(c *cli.Context) error { recipeName := c.Args().First() - if recipeName != "" { internal.ValidateRecipe(c) + if err := recipe.Ensure(recipeName); err != nil { + logrus.Fatal(err) + } + return nil } - if err := recipe.EnsureExists(recipeName); err != nil { + catalogue, err := recipe.ReadRecipeCatalogue(internal.Offline) + if err != nil { logrus.Fatal(err) } - if err := recipe.EnsureUpToDate(recipeName); err != nil { - logrus.Fatal(err) - } - - if err := recipe.EnsureLatest(recipeName); err != nil { - logrus.Fatal(err) + catlBar := formatter.CreateProgressbar(len(catalogue), "fetching latest recipes...") + for recipeName := range catalogue { + if err := recipe.Ensure(recipeName); err != nil { + logrus.Error(err) + } + catlBar.Add(1) } return nil diff --git a/pkg/recipe/recipe.go b/pkg/recipe/recipe.go index dbd4a520..a5d6d2eb 100644 --- a/pkg/recipe/recipe.go +++ b/pkg/recipe/recipe.go @@ -264,6 +264,20 @@ func (r Recipe) SampleEnv() (map[string]string, error) { return sampleEnv, nil } +// Ensure makes sure the recipe exists, is up to date and has the latest version checked out. +func Ensure(recipeName string) error { + if err := EnsureExists(recipeName); err != nil { + return err + } + if err := EnsureUpToDate(recipeName); err != nil { + return err + } + if err := EnsureLatest(recipeName); err != nil { + return err + } + return nil +} + // EnsureExists ensures that a recipe is locally cloned func EnsureExists(recipeName string) error { recipeDir := path.Join(config.RECIPES_DIR, recipeName) diff --git a/tests/integration/recipe_fetch.bats b/tests/integration/recipe_fetch.bats index a64306ab..1cc61e7c 100644 --- a/tests/integration/recipe_fetch.bats +++ b/tests/integration/recipe_fetch.bats @@ -5,7 +5,17 @@ setup() { _common_setup } -@test "recipe fetch" { +@test "recipe fetch all" { + run rm -rf "$ABRA_DIR/recipes/matrix-synapse" + assert_success + assert_not_exists "$ABRA_DIR/recipes/matrix-synapse" + + run $ABRA recipe fetch + assert_success + assert_exists "$ABRA_DIR/recipes/matrix-synapse" +} + +@test "recipe fetch single recipe" { run rm -rf "$ABRA_DIR/recipes/matrix-synapse" assert_success assert_not_exists "$ABRA_DIR/recipes/matrix-synapse" From 40c0fb4bac3dc288aa99bba3dbedb00cb76dab7f Mon Sep 17 00:00:00 2001 From: p4u1 Date: Mon, 11 Mar 2024 13:27:21 +0000 Subject: [PATCH 7/7] fix-integration-tests (!403) In preparation for the new abra release, let's fix all integration tests After merging, this needs to be cherry-picked into the release-0-9 branch. - [x] app_backup.bats (skip this one) - [x] app_check.bats (fixed by https://git.coopcloud.tech/coop-cloud/abra/commit/bd21014fedf6f8d4fb8f1baf44fb8b0df99fc326) - [x] app_cmd.bats (partially fixed in https://git.coopcloud.tech/coop-cloud/abra/commit/08232b74f6d512ab2ca7c08f67bb7c227b82a2ca), has known regression https://git.coopcloud.tech/coop-cloud/organising/issues/581 - [x] app_config.bats (no changes needed) - [x] app_cp.bats (no changes needed) - [x] app_deploy.bats - [x] app_errors.bats (no changes needed) - [x] app_list.bats (no changes needed) - [x] app_logs.bats (no changes needed) - [x] app_new.bats (no changes needed) - [x] app_ps.bats (no changes needed) - [x] app_remove.bats (fixed by [2f29fbeb2e](https://git.coopcloud.tech/coop-cloud/abra/pulls/403/commits/2f29fbeb2e018656413fa25f8615b7a98cdcb083)) - [x] app_restart.bats (no changes needed - [x] app_restore.bats (fixed by [f2dd5afc38](https://git.coopcloud.tech/coop-cloud/abra/pulls/403/commits/f2dd5afc38a25a8316899fa0c6d59499445868d7)) - [x] app_rollback.bats (partially fixed by https://git.coopcloud.tech/coop-cloud/abra/commit/6e99b74c24e92a293a5f71d7aacd552441dc8613) - [x] app_run.bats (no changes needed) - [x] app_secret.bats (fixed by https://git.coopcloud.tech/coop-cloud/abra/commit/bd069d32f6d58f12cf8b0d2a1fef0e4cdc50d94d) - [x] app_services.bats (no changes needed) - [x] app_undeploy.bats (no changes needed) - [x] app_upgrade.bats (no changes needed) - [x] app_version.bats (partially fixed by https://git.coopcloud.tech/coop-cloud/abra/commit/ad323ad2bd6184c69baf909f891a8bf8ffc168a4) - [x] app_volume.bats (fixed by [03c3823770](https://git.coopcloud.tech/coop-cloud/abra/pulls/403/commits/03c38237707ae795b723180eb07a7edc84a8de35)) - [x] autocomplete.bats (no changes needed) - [x] catalogue.bats (no changes needed) - [x] dirs.bats (no changes needed) - [x] install.bats (failes, but is expected) - [x] recipe_diff.bats (no changes needed) - [x] recipe_fetch.bats (no changes needed) - [x] recipe_lint.bats (fixed by [b6b0808066](https://git.coopcloud.tech/coop-cloud/abra/pulls/403/commits/b6b0808066a11e4bcd77517ec39600d500bcb944)) - [x] recipe_list.bats (no changes needed) - [x] recipe_new.bats (fixed by [0aac464ded](https://git.coopcloud.tech/coop-cloud/abra/pulls/403/commits/0aac464ded6b43afb3ec37ade2f64d6191b9838f)) - [x] recipe_release.bats (no changes needed) - [x] recipe_reset.bats (no changes needed) - [x] recipe_sync.bats (no changes needed) - [x] recipe_upgrade.bats (fixed by [ab86904cf4](https://git.coopcloud.tech/coop-cloud/abra/pulls/403/commits/ab86904cf45db89c7c189ca1fd9971909bd446dd)) - [x] recipe_version.bats (fixed by https://git.coopcloud.tech/coop-cloud/abra/commit/81897bf4daf6d37f3997fdea0deafb1d8f241c6c) - [x] server_add.bats - [x] server_list.bats - [x] server_prune.bats (no changes needed) - [x] server_remove.bats - [x] upgrade.bats - [x] version.bats (no changes needed) Co-authored-by: decentral1se Reviewed-on: https://git.coopcloud.tech/coop-cloud/abra/pulls/403 Co-authored-by: p4u1 Co-committed-by: p4u1 --- scripts/installer/installer | 2 +- tests/integration/app_check.bats | 8 +++---- tests/integration/app_cmd.bats | 32 +++++++++++++-------------- tests/integration/app_deploy.bats | 18 ++++++++++----- tests/integration/app_new.bats | 10 ++++----- tests/integration/app_remove.bats | 5 ++++- tests/integration/app_restore.bats | 14 ++++++------ tests/integration/app_rollback.bats | 10 ++++----- tests/integration/app_secret.bats | 9 +------- tests/integration/app_version.bats | 4 +++- tests/integration/app_volume.bats | 4 ++-- tests/integration/helpers/app.bash | 2 +- tests/integration/helpers/server.bash | 6 ++++- tests/integration/recipe_lint.bats | 8 +++---- tests/integration/recipe_upgrade.bats | 4 ++-- tests/integration/recipe_version.bats | 2 ++ 16 files changed, 75 insertions(+), 63 deletions(-) diff --git a/scripts/installer/installer b/scripts/installer/installer index 01605942..8b8ee5ec 100755 --- a/scripts/installer/installer +++ b/scripts/installer/installer @@ -2,7 +2,7 @@ ABRA_VERSION="0.8.1-beta" ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION" -RC_VERSION="0.8.1-beta" +RC_VERSION="0.8.0-rc1-beta" RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION" for arg in "$@"; do diff --git a/tests/integration/app_check.bats b/tests/integration/app_check.bats index 2ea628c9..e296e3cb 100644 --- a/tests/integration/app_check.bats +++ b/tests/integration/app_check.bats @@ -70,13 +70,13 @@ setup(){ assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' run $ABRA app check "$TEST_APP_DOMAIN" assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - refute_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' _reset_recipe } @@ -86,7 +86,7 @@ setup(){ assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 1' + assert_output --partial "Your branch is behind 'origin/main' by 1 commit" # NOTE(d1): we can't quite tell if this will fail or not in the future, so, # since it isn't an important part of what we're testing here, we don't check @@ -94,7 +94,7 @@ setup(){ run $ABRA app check "$TEST_APP_DOMAIN" --offline run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 1' + assert_output --partial "Your branch is behind 'origin/main' by 1 commit" _reset_recipe } diff --git a/tests/integration/app_cmd.bats b/tests/integration/app_cmd.bats index db2808ba..fd5e6695 100644 --- a/tests/integration/app_cmd.bats +++ b/tests/integration/app_cmd.bats @@ -58,7 +58,7 @@ test_cmd_export" assert_success assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE" - run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local + run $ABRA app cmd --local "$TEST_APP_DOMAIN" test_cmd assert_success assert_output --partial 'baz' @@ -70,7 +70,7 @@ test_cmd_export" assert_success assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo" - run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local + run $ABRA app cmd --local "$TEST_APP_DOMAIN" test_cmd assert_failure assert_output --partial 'locally unstaged changes' @@ -83,7 +83,7 @@ test_cmd_export" assert_success assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo" - run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local --chaos + run $ABRA app cmd --local --chaos "$TEST_APP_DOMAIN" test_cmd assert_success assert_output --partial 'baz' @@ -96,14 +96,14 @@ test_cmd_export" assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' - run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local + run $ABRA app cmd --local "$TEST_APP_DOMAIN" test_cmd assert_success assert_output --partial 'baz' run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - refute_output --partial 'behind 3' + assert_output --partial "up to date" _reset_recipe "$TEST_RECIPE" } @@ -113,14 +113,14 @@ test_cmd_export" assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' - run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local --offline + run $ABRA app cmd --local --offline "$TEST_APP_DOMAIN" test_cmd assert_success assert_output --partial 'baz' run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' _reset_recipe "$TEST_RECIPE" } @@ -132,13 +132,13 @@ test_cmd_export" } @test "error if missing arguments when passing --local" { - run $ABRA app cmd "$TEST_APP_DOMAIN" --local + run $ABRA app cmd --local "$TEST_APP_DOMAIN" assert_failure assert_output --partial 'missing arguments' } @test "cannot use --local and --user at same time" { - run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local --user root + run $ABRA app cmd --local --user root "$TEST_APP_DOMAIN" test_cmd assert_failure assert_output --partial 'cannot use --local & --user together' } @@ -147,7 +147,7 @@ test_cmd_export" run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/abra.sh" assert_success - run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local --chaos + run $ABRA app cmd --local --chaos "$TEST_APP_DOMAIN" test_cmd assert_failure assert_output --partial "$ABRA_DIR/recipes/$TEST_RECIPE/abra.sh does not exist" @@ -155,25 +155,25 @@ test_cmd_export" } @test "error if missing command" { - run $ABRA app cmd "$TEST_APP_DOMAIN" doesnt_exist --local + run $ABRA app cmd --local "$TEST_APP_DOMAIN" doesnt_exist assert_failure assert_output --partial "doesn't have a doesnt_exist function" } @test "run --local command" { - run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local + run $ABRA app cmd --local "$TEST_APP_DOMAIN" test_cmd assert_success assert_output --partial 'baz' } @test "run command with single arg" { - run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd_arg --local -- bing + run $ABRA app cmd --local "$TEST_APP_DOMAIN" test_cmd_arg -- bing assert_success assert_output --partial 'bing' } @test "run command with several args" { - run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd_args --local -- bong bang + run $ABRA app cmd --local "$TEST_APP_DOMAIN" test_cmd_args -- bong bang assert_success assert_output --partial 'bong bang' } diff --git a/tests/integration/app_deploy.bats b/tests/integration/app_deploy.bats index 5747ce6e..0df25675 100644 --- a/tests/integration/app_deploy.bats +++ b/tests/integration/app_deploy.bats @@ -16,6 +16,7 @@ teardown_file(){ setup(){ load "$PWD/tests/integration/helpers/common" _common_setup + _reset_recipe } teardown(){ @@ -82,13 +83,13 @@ teardown(){ assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - refute_output --partial 'behind 3' + refute_output --regexp 'behind .* 3 commits' _reset_recipe _undeploy_app @@ -100,7 +101,7 @@ teardown(){ assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' # NOTE(d1): need to use --chaos to force same commit run $ABRA app deploy "$TEST_APP_DOMAIN" \ @@ -108,7 +109,7 @@ teardown(){ assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' _undeploy_app _reset_recipe @@ -116,6 +117,9 @@ teardown(){ # bats test_tags=slow @test "deploy latest commit if no published versions and no --chaos" { + # TODO(d1): fix with a new test recipe which has no published versions? + skip "known issue, abra-test-recipe has published versions now" + latestCommit="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)" _remove_tags @@ -140,7 +144,7 @@ teardown(){ assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' threeCommitsBack="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)" @@ -273,6 +277,10 @@ teardown(){ } @test "ensure domain is checked" { + if [[ "$TEST_SERVER" == "default" ]]; then + skip "domain checks are disabled for local server" + fi + appDomain="custom-html.DOESNTEXIST" run $ABRA app new custom-html \ diff --git a/tests/integration/app_new.bats b/tests/integration/app_new.bats index 885e02cd..457ee152 100644 --- a/tests/integration/app_new.bats +++ b/tests/integration/app_new.bats @@ -45,7 +45,7 @@ teardown(){ assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial "Your branch is up to date with 'origin/main'." + assert_output --partial "up to date" } @test "create new app with version" { @@ -121,7 +121,7 @@ teardown(){ assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial "Your branch is behind 'origin/main' by 3 commits, and can be fast-forwarded." + assert_output --regexp 'behind .* 3 commits' run $ABRA app new "$TEST_RECIPE" \ --no-input \ @@ -131,7 +131,7 @@ teardown(){ assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial "Your branch is up to date with 'origin/main'." + assert_output --partial "up to date" _reset_recipe } @@ -141,7 +141,7 @@ teardown(){ assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial "Your branch is behind 'origin/main' by 3 commits, and can be fast-forwarded." + assert_output --regexp 'behind .* 3 commits' # NOTE(d1): need to use --chaos to force same commit run $ABRA app new "$TEST_RECIPE" \ @@ -154,7 +154,7 @@ teardown(){ assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial "Your branch is behind 'origin/main' by 3 commits, and can be fast-forwarded." + assert_output --regexp 'behind .* 3 commits' _reset_recipe } diff --git a/tests/integration/app_remove.bats b/tests/integration/app_remove.bats index 8800e70a..4cf6f07a 100644 --- a/tests/integration/app_remove.bats +++ b/tests/integration/app_remove.bats @@ -104,7 +104,10 @@ teardown(){ _undeploy_app - run $ABRA app volume rm "$TEST_APP_DOMAIN" + # TODO: should wait as long as volume is no longer in use + sleep 10 + + run $ABRA app volume rm "$TEST_APP_DOMAIN" --no-input assert_success run $ABRA app volume ls "$TEST_APP_DOMAIN" diff --git a/tests/integration/app_restore.bats b/tests/integration/app_restore.bats index 77a63f63..f2cf52e8 100644 --- a/tests/integration/app_restore.bats +++ b/tests/integration/app_restore.bats @@ -109,13 +109,13 @@ teardown(){ assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' - run $ABRA app restore "$TEST_APP_DOMAIN" app DOESNTEXIST + run $ABRA app restore "$TEST_APP_DOMAIN" app assert_failure run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - refute_output --partial 'behind 3' + assert_output --partial "up to date" } @test "ensure recipe not up to date if --offline" { @@ -126,19 +126,19 @@ teardown(){ assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' - run $ABRA app restore "$TEST_APP_DOMAIN" app DOESNTEXIST --offline + run $ABRA app restore "$TEST_APP_DOMAIN" app --offline assert_failure run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$latestCommit" assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - refute_output --partial 'behind 3' + assert_output --partial "HEAD detached at $latestCommit" } @test "error if missing service" { diff --git a/tests/integration/app_rollback.bats b/tests/integration/app_rollback.bats index 0439b94f..b8200bbb 100644 --- a/tests/integration/app_rollback.bats +++ b/tests/integration/app_rollback.bats @@ -50,13 +50,13 @@ teardown(){ assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' run $ABRA app rollback "$TEST_APP_DOMAIN" --no-input --no-converge-checks assert_failure run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - refute_output --partial 'behind 3' + assert_output --partial "up to date" } @test "ensure recipe not up to date if --offline" { @@ -67,14 +67,14 @@ teardown(){ assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' run $ABRA app rollback "$TEST_APP_DOMAIN" \ --no-input --no-converge-checks --offline assert_failure run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$latestCommit" assert_success @@ -131,7 +131,7 @@ teardown(){ latestCommit="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)" run $ABRA app deploy "$TEST_APP_DOMAIN" \ - --no-input --no-converge-checks --chaos + --no-input --chaos assert_success assert_output --partial "$latestCommit" assert_output --partial 'chaos' diff --git a/tests/integration/app_secret.bats b/tests/integration/app_secret.bats index 3d15e959..b745a939 100644 --- a/tests/integration/app_secret.bats +++ b/tests/integration/app_secret.bats @@ -8,7 +8,7 @@ setup_file(){ run $ABRA app new "$TEST_RECIPE" \ --no-input \ --server "$TEST_SERVER" \ - --domain "$TEST_APP_DOMAIN" \ + --domain "$TEST_APP_DOMAIN" assert_success assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" } @@ -19,13 +19,6 @@ teardown_file(){ _reset_recipe } -teardown(){ - # https://github.com/bats-core/bats-core/issues/383#issuecomment-738628888 - if [[ -z "${BATS_TEST_COMPLETED}" ]]; then - _undeploy_app - fi -} - setup(){ load "$PWD/tests/integration/helpers/common" _common_setup diff --git a/tests/integration/app_version.bats b/tests/integration/app_version.bats index 2a6d6319..b11f58ef 100644 --- a/tests/integration/app_version.bats +++ b/tests/integration/app_version.bats @@ -59,6 +59,8 @@ teardown(){ # bats test_tags=slow @test "error if not in catalogue" { + skip "known issue, see https://git.coopcloud.tech/coop-cloud/recipes-catalogue-json/issues/6" + _deploy_app run $ABRA app version "$TEST_APP_DOMAIN" @@ -92,7 +94,7 @@ teardown(){ assert_success # NOTE(d1): to let the stack come down before nuking volumes - sleep 3 + sleep 5 run $ABRA app volume remove "$appDomain" --no-input assert_success diff --git a/tests/integration/app_volume.bats b/tests/integration/app_volume.bats index 5b4ff600..99acc6c9 100644 --- a/tests/integration/app_volume.bats +++ b/tests/integration/app_volume.bats @@ -79,7 +79,7 @@ teardown(){ _undeploy_app # NOTE(d1): to let the stack come down before nuking volumes - sleep 5 + sleep 10 run $ABRA app volume rm "$TEST_APP_DOMAIN" --force assert_success @@ -93,7 +93,7 @@ teardown(){ _undeploy_app # NOTE(d1): to let the stack come down before nuking volumes - sleep 5 + sleep 10 run $ABRA app volume rm "$TEST_APP_DOMAIN" --force assert_success diff --git a/tests/integration/helpers/app.bash b/tests/integration/helpers/app.bash index ee3e30f6..32208b84 100644 --- a/tests/integration/helpers/app.bash +++ b/tests/integration/helpers/app.bash @@ -49,7 +49,7 @@ _reset_app(){ run $ABRA app new "$TEST_RECIPE" \ --no-input \ --server "$TEST_SERVER" \ - --domain "$TEST_APP_DOMAIN" \ + --domain "$TEST_APP_DOMAIN" assert_success assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" } diff --git a/tests/integration/helpers/server.bash b/tests/integration/helpers/server.bash index 3c6c74d7..e3bcb320 100644 --- a/tests/integration/helpers/server.bash +++ b/tests/integration/helpers/server.bash @@ -11,7 +11,11 @@ _add_server() { } _rm_server() { - run $ABRA server remove --no-input "$TEST_SERVER" + if [[ "$TEST_SERVER" == "default" ]]; then + run rm -rf "$ABRA_DIR/servers/default" + else + run $ABRA server remove --no-input "$TEST_SERVER" + fi assert_success assert_not_exists "$ABRA_DIR/servers/$TEST_SERVER" } diff --git a/tests/integration/recipe_lint.bats b/tests/integration/recipe_lint.bats index 0b2617b4..401ab8fd 100644 --- a/tests/integration/recipe_lint.bats +++ b/tests/integration/recipe_lint.bats @@ -66,13 +66,13 @@ setup() { assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' run $ABRA recipe lint "$TEST_RECIPE" assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - refute_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' _reset_recipe } @@ -82,13 +82,13 @@ setup() { assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' run $ABRA recipe lint "$TEST_RECIPE" --offline assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' _reset_recipe } diff --git a/tests/integration/recipe_upgrade.bats b/tests/integration/recipe_upgrade.bats index 183d1f22..a36efe26 100644 --- a/tests/integration/recipe_upgrade.bats +++ b/tests/integration/recipe_upgrade.bats @@ -61,14 +61,14 @@ setup(){ assert_success run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - assert_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' run $ABRA recipe upgrade "$TEST_RECIPE" --no-input assert_success assert_output --partial 'can upgrade service: app' run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status - refute_output --partial 'behind 3' + assert_output --regexp 'behind .* 3 commits' _reset_recipe } diff --git a/tests/integration/recipe_version.bats b/tests/integration/recipe_version.bats index 42d2e4d0..ebc5bad9 100644 --- a/tests/integration/recipe_version.bats +++ b/tests/integration/recipe_version.bats @@ -12,6 +12,8 @@ setup() { } @test "error if not present in catalogue" { + skip "known issue, see https://git.coopcloud.tech/coop-cloud/recipes-catalogue-json/issues/6" + run $ABRA recipe versions "$TEST_RECIPE" assert_failure assert_output --partial "is not published on the catalogue"