forked from toolshed/abra
		
	Compare commits
	
		
			49 Commits
		
	
	
		
			renovate/m
			...
			add-waitin
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 412e366200 | |||
| f5cadcc5f0 | |||
| 
						
						
							
						
						5ae73f700e
	
				 | 
					
					
						|||
| 
						
						
							
						
						63d419caae
	
				 | 
					
					
						|||
| 
						
						
							
						
						179b66d65c
	
				 | 
					
					
						|||
| 
						
						
							
						
						c9144d90f3
	
				 | 
					
					
						|||
| 
						
						
							
						
						ebf5d82c56
	
				 | 
					
					
						|||
| 
						
						
							
						
						8bb98ed0ed
	
				 | 
					
					
						|||
| 
						
						
							
						
						23f5745cb8
	
				 | 
					
					
						|||
| 
						
						
							
						
						2cd453ae8d
	
				 | 
					
					
						|||
| 
						
						
							
						
						e42cc0f91d
	
				 | 
					
					
						|||
| ae1a6c45f9 | |||
| 65fdaf43cc | |||
| 2d135329bb | |||
| 3e95319969 | |||
| 1de45a6508 | |||
| 1208438cba | |||
| 
						
						
							
						
						55c7aca3c0
	
				 | 
					
					
						|||
| 
						
						
							
						
						8fa9419c99
	
				 | 
					
					
						|||
| 
						
						
							
						
						73ad0a802e
	
				 | 
					
					
						|||
| 
						
						
							
						
						798fd2336c
	
				 | 
					
					
						|||
| 70e65d6667 | |||
| 
						
						
							
						
						efc9602808
	
				 | 
					
					
						|||
| 
						
						
							
						
						1e110f1375
	
				 | 
					
					
						|||
| 
						
						
							
						
						473cae0146
	
				 | 
					
					
						|||
| 
						
						
							
						
						2da859896a
	
				 | 
					
					
						|||
| 
						
						
							
						
						ab00578ee1
	
				 | 
					
					
						|||
| 
						
						
							
						
						3dc5662821
	
				 | 
					
					
						|||
| 
						
						
							
						
						ab64eb2e8d
	
				 | 
					
					
						|||
| 
						
						
							
						
						4f22228aab
	
				 | 
					
					
						|||
| 
						
						
							
						
						a7f1af7476
	
				 | 
					
					
						|||
| 949510d4c3 | |||
| 
						
						
							
						
						9f478dac1d
	
				 | 
					
					
						|||
| 
						
						
							
						
						69f38ea445
	
				 | 
					
					
						|||
| 
						
						
							
						
						0582147874
	
				 | 
					
					
						|||
| 
						
						
							
						
						bdeeb75973
	
				 | 
					
					
						|||
| 
						
						
							
						
						2518e65e3e
	
				 | 
					
					
						|||
| 
						
						
							
						
						8354c92654
	
				 | 
					
					
						|||
| 173e81b885 | |||
| d91731518b | |||
| 2bfee5058d | |||
| a7ce71d6cf | |||
| 10f60fee1d | |||
| 6025ab443f | |||
| 43ecf35449 | |||
| 4d2a1065d2 | |||
| 0b67500cab | |||
| 
						
						
							
						
						e0c3a06182
	
				 | 
					
					
						|||
| a86ba4e97b | 
@ -33,7 +33,7 @@ steps:
 | 
			
		||||
      event: tag
 | 
			
		||||
 | 
			
		||||
  - name: release
 | 
			
		||||
    image: golang:1.20
 | 
			
		||||
    image: goreleaser/goreleaser:v1.18.2
 | 
			
		||||
    environment:
 | 
			
		||||
      GITEA_TOKEN:
 | 
			
		||||
        from_secret: goreleaser_gitea_token
 | 
			
		||||
@ -41,7 +41,7 @@ steps:
 | 
			
		||||
      - name: deps
 | 
			
		||||
        path: /go
 | 
			
		||||
    commands:
 | 
			
		||||
      - curl -sL https://git.io/goreleaser | bash
 | 
			
		||||
      - goreleaser release
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - fetch
 | 
			
		||||
    when:
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,7 @@
 | 
			
		||||
- kawaiipunk
 | 
			
		||||
- knoflook
 | 
			
		||||
- moritz
 | 
			
		||||
- rix
 | 
			
		||||
- roxxers
 | 
			
		||||
- vera
 | 
			
		||||
- yksflip
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,6 @@ import (
 | 
			
		||||
	"github.com/docker/docker/api/types/filters"
 | 
			
		||||
	dockerClient "github.com/docker/docker/client"
 | 
			
		||||
	"github.com/docker/docker/pkg/archive"
 | 
			
		||||
	"github.com/docker/docker/pkg/system"
 | 
			
		||||
	"github.com/klauspost/pgzip"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
@ -42,6 +41,7 @@ var appBackupCommand = cli.Command{
 | 
			
		||||
	ArgsUsage: "<domain> [<service>]",
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	BashComplete: autocomplete.AppNameComplete,
 | 
			
		||||
@ -72,8 +72,8 @@ This file is a compressed archive which contains all backup paths. To see paths,
 | 
			
		||||
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)
 | 
			
		||||
		conf := runtime.New()
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New(app.Server)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@ -262,7 +262,7 @@ func runBackup(cl *dockerClient.Client, app config.App, serviceName string, bkCo
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func copyToFile(outfile string, r io.Reader) error {
 | 
			
		||||
	tmpFile, err := system.TempFileSequential(filepath.Dir(outfile), ".tar_temp")
 | 
			
		||||
	tmpFile, err := os.CreateTemp(filepath.Dir(outfile), ".tar_temp")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import (
 | 
			
		||||
	"coopcloud.tech/abra/cli/internal"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/autocomplete"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/config"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
)
 | 
			
		||||
@ -19,10 +20,12 @@ var appCheckCommand = cli.Command{
 | 
			
		||||
	ArgsUsage: "<domain>",
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
 | 
			
		||||
		envSamplePath := path.Join(config.RECIPES_DIR, app.Recipe, ".env.sample")
 | 
			
		||||
		if _, err := os.Stat(envSamplePath); err != nil {
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ import (
 | 
			
		||||
	"coopcloud.tech/abra/pkg/autocomplete"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/client"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/config"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
)
 | 
			
		||||
@ -38,11 +39,13 @@ Example:
 | 
			
		||||
		internal.LocalCmdFlag,
 | 
			
		||||
		internal.RemoteUserFlag,
 | 
			
		||||
		internal.TtyFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	BashComplete: autocomplete.AppNameComplete,
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New(app.Server)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@ var appConfigCommand = cli.Command{
 | 
			
		||||
	ArgsUsage: "<domain>",
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ import (
 | 
			
		||||
	"coopcloud.tech/abra/pkg/config"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/container"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/formatter"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
	"github.com/docker/docker/api/types"
 | 
			
		||||
	"github.com/docker/docker/api/types/filters"
 | 
			
		||||
	dockerClient "github.com/docker/docker/client"
 | 
			
		||||
@ -27,6 +28,7 @@ var appCpCommand = cli.Command{
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.NoInputFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Usage:  "Copy files to/from a running app service",
 | 
			
		||||
@ -42,7 +44,8 @@ And if you want to copy that file back to your current working directory locally
 | 
			
		||||
		abra app cp <domain> app:/myfile.txt .
 | 
			
		||||
`,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New(app.Server)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,28 @@
 | 
			
		||||
package app
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"coopcloud.tech/abra/cli/internal"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/autocomplete"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
 | 
			
		||||
	"coopcloud.tech/abra/pkg/client"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/config"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/dns"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/formatter"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/git"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/lint"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/recipe"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/upstream/stack"
 | 
			
		||||
	"github.com/AlecAivazis/survey/v2"
 | 
			
		||||
	dockerClient "github.com/docker/docker/client"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -18,6 +38,7 @@ var appDeployCommand = cli.Command{
 | 
			
		||||
		internal.ChaosFlag,
 | 
			
		||||
		internal.NoDomainChecksFlag,
 | 
			
		||||
		internal.DontWaitConvergeFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Description: `
 | 
			
		||||
@ -31,6 +52,328 @@ Chas mode ("--chaos") will deploy your local checkout of a recipe as-is,
 | 
			
		||||
including unstaged changes and can be useful for live hacking and testing new
 | 
			
		||||
recipes.
 | 
			
		||||
`,
 | 
			
		||||
	Action:       internal.DeployAction,
 | 
			
		||||
	BashComplete: autocomplete.AppNameComplete,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
		stackName := app.StackName()
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New(app.Server)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !internal.Chaos {
 | 
			
		||||
			if err := recipe.EnsureUpToDate(app.Recipe, conf); err != nil {
 | 
			
		||||
				logrus.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		r, err := recipe.Get(app.Recipe, conf)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := lint.LintForErrors(r); err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		logrus.Debugf("checking whether %s is already deployed", stackName)
 | 
			
		||||
 | 
			
		||||
		isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if isDeployed {
 | 
			
		||||
			if internal.Force || internal.Chaos {
 | 
			
		||||
				logrus.Warnf("%s is already deployed but continuing (--force/--chaos)", app.Name)
 | 
			
		||||
			} else {
 | 
			
		||||
				logrus.Fatalf("%s is already deployed", app.Name)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		isLatestHash := false
 | 
			
		||||
		version := deployedVersion
 | 
			
		||||
		if version == "unknown" && !internal.Chaos {
 | 
			
		||||
			catl, err := recipe.ReadRecipeCatalogue(conf)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logrus.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
			versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logrus.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
			if len(versions) > 0 {
 | 
			
		||||
				version = versions[len(versions)-1]
 | 
			
		||||
				logrus.Debugf("choosing %s as version to deploy", version)
 | 
			
		||||
				if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
 | 
			
		||||
					logrus.Fatal(err)
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				head, err := git.GetRecipeHead(app.Recipe)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					logrus.Fatal(err)
 | 
			
		||||
				}
 | 
			
		||||
				isLatestHash = true
 | 
			
		||||
				version = formatter.SmallSHA(head.String())
 | 
			
		||||
				logrus.Warn("no versions detected, using latest commit")
 | 
			
		||||
				if err := recipe.EnsureLatest(app.Recipe, conf); err != nil {
 | 
			
		||||
					logrus.Fatal(err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if version == "unknown" && !internal.Chaos {
 | 
			
		||||
			logrus.Debugf("choosing %s as version to deploy", version)
 | 
			
		||||
			if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
 | 
			
		||||
				logrus.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if version != "unknown" && !internal.Chaos && !isLatestHash {
 | 
			
		||||
			if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
 | 
			
		||||
				logrus.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if internal.Chaos {
 | 
			
		||||
			logrus.Warnf("chaos mode engaged")
 | 
			
		||||
			var err error
 | 
			
		||||
			version, err = recipe.ChaosVersion(app.Recipe)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logrus.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
 | 
			
		||||
		abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		for k, v := range abraShEnv {
 | 
			
		||||
			app.Env[k] = v
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		composeFiles, err := config.GetAppComposeFiles(app.Recipe, app.Env)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		deployOpts := stack.Deploy{
 | 
			
		||||
			Composefiles: composeFiles,
 | 
			
		||||
			Namespace:    stackName,
 | 
			
		||||
			Prune:        false,
 | 
			
		||||
			ResolveImage: stack.ResolveImageAlways,
 | 
			
		||||
		}
 | 
			
		||||
		compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		config.ExposeAllEnv(stackName, compose, app.Env)
 | 
			
		||||
		config.SetRecipeLabel(compose, stackName, app.Recipe)
 | 
			
		||||
		config.SetChaosLabel(compose, stackName, internal.Chaos)
 | 
			
		||||
		config.SetChaosVersionLabel(compose, stackName, version)
 | 
			
		||||
		config.SetUpdateLabel(compose, stackName, app.Env)
 | 
			
		||||
 | 
			
		||||
		if err := DeployOverview(app, version, "continue with deployment?"); err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !internal.NoDomainChecks {
 | 
			
		||||
			domainName, ok := app.Env["DOMAIN"]
 | 
			
		||||
			if ok {
 | 
			
		||||
				if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
 | 
			
		||||
					logrus.Fatal(err)
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				logrus.Warn("skipping domain checks as no DOMAIN=... configured for app")
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			logrus.Warn("skipping domain checks as requested")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		stack.WaitTimeout, err = config.GetTimeoutFromLabel(compose, stackName)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		logrus.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
 | 
			
		||||
 | 
			
		||||
		if err := stack.RunDeploy(cl, deployOpts, compose, app.Name, internal.DontWaitConverge); err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"]
 | 
			
		||||
		if ok && !internal.DontWaitConverge {
 | 
			
		||||
			logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds)
 | 
			
		||||
			if err := PostCmds(cl, app, postDeployCmds); err != nil {
 | 
			
		||||
				logrus.Fatalf("attempting to run post deploy commands, saw: %s", err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PostCmds parses a string of commands and executes them inside of the respective services
 | 
			
		||||
// the commands string must have the following format:
 | 
			
		||||
// "<service> <command> <arguments>|<service> <command> <arguments>|... "
 | 
			
		||||
func PostCmds(cl *dockerClient.Client, app config.App, commands string) error {
 | 
			
		||||
	abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
 | 
			
		||||
	if _, err := os.Stat(abraSh); err != nil {
 | 
			
		||||
		if os.IsNotExist(err) {
 | 
			
		||||
			return fmt.Errorf(fmt.Sprintf("%s does not exist for %s?", abraSh, app.Name))
 | 
			
		||||
		}
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, command := range strings.Split(commands, "|") {
 | 
			
		||||
		commandParts := strings.Split(command, " ")
 | 
			
		||||
		if len(commandParts) < 2 {
 | 
			
		||||
			return fmt.Errorf(fmt.Sprintf("not enough arguments: %s", command))
 | 
			
		||||
		}
 | 
			
		||||
		targetServiceName := commandParts[0]
 | 
			
		||||
		cmdName := commandParts[1]
 | 
			
		||||
		parsedCmdArgs := ""
 | 
			
		||||
		if len(commandParts) > 2 {
 | 
			
		||||
			parsedCmdArgs = fmt.Sprintf("%s ", strings.Join(commandParts[2:], " "))
 | 
			
		||||
		}
 | 
			
		||||
		logrus.Infof("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName)
 | 
			
		||||
 | 
			
		||||
		if err := internal.EnsureCommand(abraSh, app.Recipe, cmdName); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		serviceNames, err := config.GetAppServiceNames(app.Name)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		matchingServiceName := false
 | 
			
		||||
		for _, serviceName := range serviceNames {
 | 
			
		||||
			if serviceName == targetServiceName {
 | 
			
		||||
				matchingServiceName = true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !matchingServiceName {
 | 
			
		||||
			return fmt.Errorf(fmt.Sprintf("no service %s for %s?", targetServiceName, app.Name))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		logrus.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName)
 | 
			
		||||
 | 
			
		||||
		internal.Tty = true
 | 
			
		||||
		if err := internal.RunCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeployOverview shows a deployment overview
 | 
			
		||||
func DeployOverview(app config.App, version, message string) error {
 | 
			
		||||
	tableCol := []string{"server", "recipe", "config", "domain", "version"}
 | 
			
		||||
	table := formatter.CreateTable(tableCol)
 | 
			
		||||
 | 
			
		||||
	deployConfig := "compose.yml"
 | 
			
		||||
	if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
 | 
			
		||||
		deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	server := app.Server
 | 
			
		||||
	if app.Server == "default" {
 | 
			
		||||
		server = "local"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	table.Append([]string{server, app.Recipe, deployConfig, app.Domain, version})
 | 
			
		||||
	table.Render()
 | 
			
		||||
 | 
			
		||||
	if internal.NoInput {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	response := false
 | 
			
		||||
	prompt := &survey.Confirm{
 | 
			
		||||
		Message: message,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := survey.AskOne(prompt, &response); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !response {
 | 
			
		||||
		logrus.Fatal("exiting as requested")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewVersionOverview shows an upgrade or downgrade overview
 | 
			
		||||
func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes string) error {
 | 
			
		||||
	tableCol := []string{"server", "recipe", "config", "domain", "current version", "to be deployed"}
 | 
			
		||||
	table := formatter.CreateTable(tableCol)
 | 
			
		||||
 | 
			
		||||
	deployConfig := "compose.yml"
 | 
			
		||||
	if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
 | 
			
		||||
		deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	server := app.Server
 | 
			
		||||
	if app.Server == "default" {
 | 
			
		||||
		server = "local"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	table.Append([]string{server, app.Recipe, deployConfig, app.Domain, currentVersion, newVersion})
 | 
			
		||||
	table.Render()
 | 
			
		||||
 | 
			
		||||
	if releaseNotes == "" {
 | 
			
		||||
		var err error
 | 
			
		||||
		releaseNotes, err = GetReleaseNotes(app.Recipe, newVersion)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if releaseNotes != "" && newVersion != "" {
 | 
			
		||||
		fmt.Println()
 | 
			
		||||
		fmt.Println(fmt.Sprintf("%s release notes:\n\n%s", newVersion, releaseNotes))
 | 
			
		||||
	} else {
 | 
			
		||||
		logrus.Warnf("no release notes available for %s", newVersion)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if internal.NoInput {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	response := false
 | 
			
		||||
	prompt := &survey.Confirm{
 | 
			
		||||
		Message: "continue with deployment?",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := survey.AskOne(prompt, &response); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !response {
 | 
			
		||||
		logrus.Fatal("exiting as requested")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetReleaseNotes prints release notes for a recipe version
 | 
			
		||||
func GetReleaseNotes(recipeName, version string) (string, error) {
 | 
			
		||||
	if version == "" {
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fpath := path.Join(config.RECIPES_DIR, recipeName, "release", version)
 | 
			
		||||
 | 
			
		||||
	if _, err := os.Stat(fpath); !os.IsNotExist(err) {
 | 
			
		||||
		releaseNotes, err := ioutil.ReadFile(fpath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return string(releaseNotes), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return "", nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -51,12 +51,13 @@ the logs.
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.WatchFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	BashComplete: autocomplete.AppNameComplete,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
		conf := runtime.New()
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New(app.Server)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,7 @@ import (
 | 
			
		||||
	"coopcloud.tech/abra/pkg/config"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/formatter"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/recipe"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
	"coopcloud.tech/tagcmp"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
@ -79,9 +80,12 @@ can take some time.
 | 
			
		||||
		statusFlag,
 | 
			
		||||
		listAppServerFlag,
 | 
			
		||||
		recipeFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
 | 
			
		||||
		appFiles, err := config.LoadAppFiles(listAppServer)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
@ -109,7 +113,7 @@ can take some time.
 | 
			
		||||
				logrus.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			catl, err = recipe.ReadRecipeCatalogue()
 | 
			
		||||
			catl, err = recipe.ReadRecipeCatalogue(conf)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logrus.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@ -79,11 +79,17 @@ var appLogsCommand = cli.Command{
 | 
			
		||||
		internal.StdErrOnlyFlag,
 | 
			
		||||
		internal.SinceLogsFlag,
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	BashComplete: autocomplete.AppNameComplete,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c, runtime.WithEnsureRecipeExists(false))
 | 
			
		||||
		conf := runtime.New(
 | 
			
		||||
			runtime.WithOffline(internal.Offline),
 | 
			
		||||
			runtime.WithEnsureRecipeExists(false),
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New(app.Server)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										215
									
								
								cli/app/new.go
									
									
									
									
									
								
							
							
						
						
									
										215
									
								
								cli/app/new.go
									
									
									
									
									
								
							@ -1,8 +1,23 @@
 | 
			
		||||
package app
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"coopcloud.tech/abra/cli/internal"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/app"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/autocomplete"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/client"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/config"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/formatter"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/jsontable"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/recipe"
 | 
			
		||||
	recipePkg "coopcloud.tech/abra/pkg/recipe"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/secret"
 | 
			
		||||
	"github.com/AlecAivazis/survey/v2"
 | 
			
		||||
	dockerClient "github.com/docker/docker/client"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -38,9 +53,203 @@ var appNewCommand = cli.Command{
 | 
			
		||||
		internal.DomainFlag,
 | 
			
		||||
		internal.PassFlag,
 | 
			
		||||
		internal.SecretsFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:    internal.SubCommandBefore,
 | 
			
		||||
	ArgsUsage: "[<recipe>]",
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		conf := runtime.New(
 | 
			
		||||
			runtime.WithOffline(internal.Offline),
 | 
			
		||||
			runtime.WithEnsureRecipeUpToDate(false),
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		recipe := internal.ValidateRecipeWithPrompt(c, conf)
 | 
			
		||||
 | 
			
		||||
		if err := recipePkg.EnsureUpToDate(recipe.Name, conf); err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := ensureServerFlag(); err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := ensureDomainFlag(recipe, internal.NewAppServer); err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		sanitisedAppName := config.SanitiseAppName(internal.Domain)
 | 
			
		||||
		logrus.Debugf("%s sanitised as %s for new app", internal.Domain, sanitisedAppName)
 | 
			
		||||
 | 
			
		||||
		if err := config.TemplateAppEnvSample(
 | 
			
		||||
			recipe.Name,
 | 
			
		||||
			internal.Domain,
 | 
			
		||||
			internal.NewAppServer,
 | 
			
		||||
			internal.Domain,
 | 
			
		||||
		); err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := promptForSecrets(internal.Domain); err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New(internal.NewAppServer)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var secrets AppSecrets
 | 
			
		||||
		var secretTable *jsontable.JSONTable
 | 
			
		||||
		if internal.Secrets {
 | 
			
		||||
			secrets, err := createSecrets(cl, sanitisedAppName)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logrus.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			secretCols := []string{"Name", "Value"}
 | 
			
		||||
			secretTable = formatter.CreateTable(secretCols)
 | 
			
		||||
			for secret := range secrets {
 | 
			
		||||
				secretTable.Append([]string{secret, secrets[secret]})
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if internal.NewAppServer == "default" {
 | 
			
		||||
			internal.NewAppServer = "local"
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tableCol := []string{"server", "recipe", "domain"}
 | 
			
		||||
		table := formatter.CreateTable(tableCol)
 | 
			
		||||
		table.Append([]string{internal.NewAppServer, recipe.Name, internal.Domain})
 | 
			
		||||
 | 
			
		||||
		fmt.Println("")
 | 
			
		||||
		fmt.Println(fmt.Sprintf("A new %s app has been created! Here is an overview:", recipe.Name))
 | 
			
		||||
		fmt.Println("")
 | 
			
		||||
		table.Render()
 | 
			
		||||
		fmt.Println("")
 | 
			
		||||
		fmt.Println("You can configure this app by running the following:")
 | 
			
		||||
		fmt.Println(fmt.Sprintf("\n    abra app config %s", internal.Domain))
 | 
			
		||||
		fmt.Println("")
 | 
			
		||||
		fmt.Println("You can deploy this app by running the following:")
 | 
			
		||||
		fmt.Println(fmt.Sprintf("\n    abra app deploy %s", internal.Domain))
 | 
			
		||||
		fmt.Println("")
 | 
			
		||||
 | 
			
		||||
		if len(secrets) > 0 {
 | 
			
		||||
			fmt.Println("Here are your generated secrets:")
 | 
			
		||||
			fmt.Println("")
 | 
			
		||||
			secretTable.Render()
 | 
			
		||||
			fmt.Println("")
 | 
			
		||||
			logrus.Warn("generated secrets are not shown again, please take note of them *now*")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	},
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	ArgsUsage:    "[<recipe>]",
 | 
			
		||||
	Action:       internal.NewAction,
 | 
			
		||||
	BashComplete: autocomplete.RecipeNameComplete,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AppSecrets represents all app secrest
 | 
			
		||||
type AppSecrets map[string]string
 | 
			
		||||
 | 
			
		||||
// createSecrets creates all secrets for a new app.
 | 
			
		||||
func createSecrets(cl *dockerClient.Client, sanitisedAppName string) (AppSecrets, error) {
 | 
			
		||||
	appEnvPath := path.Join(
 | 
			
		||||
		config.ABRA_DIR,
 | 
			
		||||
		"servers",
 | 
			
		||||
		internal.NewAppServer,
 | 
			
		||||
		fmt.Sprintf("%s.env", internal.Domain),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	appEnv, err := config.ReadEnv(appEnvPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	secretEnvVars := secret.ReadSecretEnvVars(appEnv)
 | 
			
		||||
	secrets, err := secret.GenerateSecrets(cl, secretEnvVars, sanitisedAppName, internal.NewAppServer)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if internal.Pass {
 | 
			
		||||
		for secretName := range secrets {
 | 
			
		||||
			secretValue := secrets[secretName]
 | 
			
		||||
			if err := secret.PassInsertSecret(
 | 
			
		||||
				secretValue,
 | 
			
		||||
				secretName,
 | 
			
		||||
				internal.Domain,
 | 
			
		||||
				internal.NewAppServer,
 | 
			
		||||
			); err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return secrets, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
 | 
			
		||||
func ensureDomainFlag(recipe recipe.Recipe, server string) error {
 | 
			
		||||
	if internal.Domain == "" && !internal.NoInput {
 | 
			
		||||
		prompt := &survey.Input{
 | 
			
		||||
			Message: "Specify app domain",
 | 
			
		||||
			Default: fmt.Sprintf("%s.%s", recipe.Name, server),
 | 
			
		||||
		}
 | 
			
		||||
		if err := survey.AskOne(prompt, &internal.Domain); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if internal.Domain == "" {
 | 
			
		||||
		return fmt.Errorf("no domain provided")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// promptForSecrets asks if we should generate secrets for a new app.
 | 
			
		||||
func promptForSecrets(appName string) error {
 | 
			
		||||
	app, err := app.Get(appName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	secretEnvVars := secret.ReadSecretEnvVars(app.Env)
 | 
			
		||||
	if len(secretEnvVars) == 0 {
 | 
			
		||||
		logrus.Debugf("%s has no secrets to generate, skipping...", app.Recipe)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !internal.Secrets && !internal.NoInput {
 | 
			
		||||
		prompt := &survey.Confirm{
 | 
			
		||||
			Message: "Generate app secrets?",
 | 
			
		||||
		}
 | 
			
		||||
		if err := survey.AskOne(prompt, &internal.Secrets); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ensureServerFlag checks if the server flag was used. if not, asks the user for it.
 | 
			
		||||
func ensureServerFlag() error {
 | 
			
		||||
	servers, err := config.GetServers()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if internal.NewAppServer == "" && !internal.NoInput {
 | 
			
		||||
		prompt := &survey.Select{
 | 
			
		||||
			Message: "Select app server:",
 | 
			
		||||
			Options: servers,
 | 
			
		||||
		}
 | 
			
		||||
		if err := survey.AskOne(prompt, &internal.NewAppServer); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if internal.NewAppServer == "" {
 | 
			
		||||
		return fmt.Errorf("no server provided")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ import (
 | 
			
		||||
	"coopcloud.tech/abra/pkg/client"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/config"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/formatter"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/service"
 | 
			
		||||
	stack "coopcloud.tech/abra/pkg/upstream/stack"
 | 
			
		||||
	"github.com/buger/goterm"
 | 
			
		||||
@ -29,11 +30,13 @@ var appPsCommand = cli.Command{
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		internal.WatchFlag,
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	BashComplete: autocomplete.AppNameComplete,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New(app.Server)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
 | 
			
		||||
@ -8,9 +8,11 @@ import (
 | 
			
		||||
	"coopcloud.tech/abra/cli/internal"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/autocomplete"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/client"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
	stack "coopcloud.tech/abra/pkg/upstream/stack"
 | 
			
		||||
	"github.com/AlecAivazis/survey/v2"
 | 
			
		||||
	"github.com/docker/docker/api/types"
 | 
			
		||||
	"github.com/docker/docker/api/types/volume"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
)
 | 
			
		||||
@ -42,11 +44,13 @@ flag.
 | 
			
		||||
		internal.ForceFlag,
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.NoInputFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	BashComplete: autocomplete.AppNameComplete,
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
 | 
			
		||||
		if !internal.Force && !internal.NoInput {
 | 
			
		||||
			response := false
 | 
			
		||||
@ -108,7 +112,8 @@ flag.
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		volumeListOKBody, err := cl.VolumeList(context.Background(), fs)
 | 
			
		||||
		volumeListOptions := volume.ListOptions{fs}
 | 
			
		||||
		volumeListOKBody, err := cl.VolumeList(context.Background(), volumeListOptions)
 | 
			
		||||
		volumeList := volumeListOKBody.Volumes
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import (
 | 
			
		||||
	"coopcloud.tech/abra/cli/internal"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/autocomplete"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/client"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
	upstream "coopcloud.tech/abra/pkg/upstream/service"
 | 
			
		||||
	stack "coopcloud.tech/abra/pkg/upstream/stack"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
@ -21,12 +22,14 @@ var appRestartCommand = cli.Command{
 | 
			
		||||
	ArgsUsage: "<domain>",
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	Description:  `This command restarts a service within a deployed app.`,
 | 
			
		||||
	BashComplete: autocomplete.AppNameComplete,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
 | 
			
		||||
		serviceNameShort := c.Args().Get(1)
 | 
			
		||||
		if serviceNameShort == "" {
 | 
			
		||||
 | 
			
		||||
@ -35,6 +35,7 @@ var appRestoreCommand = cli.Command{
 | 
			
		||||
	ArgsUsage: "<domain> <service> <file>",
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	BashComplete: autocomplete.AppNameComplete,
 | 
			
		||||
@ -55,8 +56,8 @@ 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)
 | 
			
		||||
		conf := runtime.New()
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New(app.Server)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
 | 
			
		||||
@ -31,6 +31,7 @@ var appRollbackCommand = cli.Command{
 | 
			
		||||
		internal.ChaosFlag,
 | 
			
		||||
		internal.NoDomainChecksFlag,
 | 
			
		||||
		internal.DontWaitConvergeFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Description: `
 | 
			
		||||
@ -48,9 +49,9 @@ recipes.
 | 
			
		||||
`,
 | 
			
		||||
	BashComplete: autocomplete.AppNameComplete,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
		stackName := app.StackName()
 | 
			
		||||
		conf := runtime.New()
 | 
			
		||||
 | 
			
		||||
		if !internal.Chaos {
 | 
			
		||||
			if err := recipe.EnsureUpToDate(app.Recipe, conf); err != nil {
 | 
			
		||||
@ -83,7 +84,7 @@ recipes.
 | 
			
		||||
			logrus.Fatalf("%s is not deployed?", app.Name)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		catl, err := recipe.ReadRecipeCatalogue()
 | 
			
		||||
		catl, err := recipe.ReadRecipeCatalogue(conf)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
@ -124,17 +125,15 @@ recipes.
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		availableDowngrades = internal.ReverseStringList(availableDowngrades)
 | 
			
		||||
 | 
			
		||||
		var chosenDowngrade string
 | 
			
		||||
		if !internal.Chaos {
 | 
			
		||||
		if len(availableDowngrades) > 0 && !internal.Chaos {
 | 
			
		||||
			if internal.Force || internal.NoInput {
 | 
			
		||||
				chosenDowngrade = availableDowngrades[0]
 | 
			
		||||
				chosenDowngrade = availableDowngrades[len(availableDowngrades)-1]
 | 
			
		||||
				logrus.Debugf("choosing %s as version to downgrade to (--force)", chosenDowngrade)
 | 
			
		||||
			} else {
 | 
			
		||||
				prompt := &survey.Select{
 | 
			
		||||
					Message: fmt.Sprintf("Please select a downgrade (current version: %s):", deployedVersion),
 | 
			
		||||
					Options: availableDowngrades,
 | 
			
		||||
					Options: internal.ReverseStringList(availableDowngrades),
 | 
			
		||||
				}
 | 
			
		||||
				if err := survey.AskOne(prompt, &chosenDowngrade); err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
@ -186,10 +185,8 @@ recipes.
 | 
			
		||||
		config.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
 | 
			
		||||
		config.SetUpdateLabel(compose, stackName, app.Env)
 | 
			
		||||
 | 
			
		||||
		if !internal.Force {
 | 
			
		||||
			if err := internal.NewVersionOverview(app, deployedVersion, chosenDowngrade, ""); err != nil {
 | 
			
		||||
				logrus.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
		if err := NewVersionOverview(app, deployedVersion, chosenDowngrade, ""); err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil {
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ import (
 | 
			
		||||
	"coopcloud.tech/abra/pkg/autocomplete"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/client"
 | 
			
		||||
	containerPkg "coopcloud.tech/abra/pkg/container"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/upstream/container"
 | 
			
		||||
	"github.com/docker/cli/cli/command"
 | 
			
		||||
	"github.com/docker/docker/api/types"
 | 
			
		||||
@ -37,13 +38,15 @@ var appRunCommand = cli.Command{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		noTTYFlag,
 | 
			
		||||
		userFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	ArgsUsage:    "<domain> <service> <args>...",
 | 
			
		||||
	Usage:        "Run a command in a service container",
 | 
			
		||||
	BashComplete: autocomplete.AppNameComplete,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
 | 
			
		||||
		if len(c.Args()) < 2 {
 | 
			
		||||
			internal.ShowSubcommandHelpAndError(c, errors.New("no <service> provided?"))
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ import (
 | 
			
		||||
	"coopcloud.tech/abra/pkg/client"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/config"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/formatter"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/secret"
 | 
			
		||||
	"github.com/docker/docker/api/types"
 | 
			
		||||
	dockerClient "github.com/docker/docker/client"
 | 
			
		||||
@ -42,11 +43,13 @@ var appSecretGenerateCommand = cli.Command{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		allSecretsFlag,
 | 
			
		||||
		internal.PassFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	BashComplete: autocomplete.AppNameComplete,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New(app.Server)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@ -121,6 +124,7 @@ var appSecretInsertCommand = cli.Command{
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.PassFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	ArgsUsage:    "<domain> <secret-name> <version> <data>",
 | 
			
		||||
@ -138,7 +142,8 @@ Example:
 | 
			
		||||
 | 
			
		||||
`,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New(app.Server)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@ -198,6 +203,7 @@ var appSecretRmCommand = cli.Command{
 | 
			
		||||
		internal.NoInputFlag,
 | 
			
		||||
		rmAllSecretsFlag,
 | 
			
		||||
		internal.PassRemoveFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	ArgsUsage:    "<domain> [<secret-name>]",
 | 
			
		||||
@ -210,7 +216,8 @@ Example:
 | 
			
		||||
    abra app secret remove myapp db_pass
 | 
			
		||||
`,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
		secrets := secret.ReadSecretEnvVars(app.Env)
 | 
			
		||||
 | 
			
		||||
		if c.Args().Get(1) != "" && rmAllSecrets {
 | 
			
		||||
@ -288,11 +295,13 @@ var appSecretLsCommand = cli.Command{
 | 
			
		||||
	Aliases: []string{"ls"},
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Usage:  "List all secrets",
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
		secrets := secret.ReadSecretEnvVars(app.Env)
 | 
			
		||||
 | 
			
		||||
		tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"}
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ import (
 | 
			
		||||
	"coopcloud.tech/abra/pkg/autocomplete"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/client"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/formatter"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/service"
 | 
			
		||||
	stack "coopcloud.tech/abra/pkg/upstream/stack"
 | 
			
		||||
	"github.com/docker/docker/api/types"
 | 
			
		||||
@ -23,11 +24,13 @@ var appServicesCommand = cli.Command{
 | 
			
		||||
	ArgsUsage: "<domain>",
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	BashComplete: autocomplete.AppNameComplete,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New(app.Server)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ import (
 | 
			
		||||
	"coopcloud.tech/abra/pkg/client"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/config"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/formatter"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
	stack "coopcloud.tech/abra/pkg/upstream/stack"
 | 
			
		||||
	"github.com/docker/docker/api/types/filters"
 | 
			
		||||
	dockerClient "github.com/docker/docker/client"
 | 
			
		||||
@ -86,6 +87,7 @@ var appUndeployCommand = cli.Command{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.NoInputFlag,
 | 
			
		||||
		pruneFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	Usage:        "Undeploy an app",
 | 
			
		||||
@ -99,8 +101,10 @@ any previously attached volumes as eligible for pruning once undeployed.
 | 
			
		||||
Passing "-p/--prune" does not remove those volumes.
 | 
			
		||||
`,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
		stackName := app.StackName()
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New(app.Server)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
@ -117,7 +121,7 @@ Passing "-p/--prune" does not remove those volumes.
 | 
			
		||||
			logrus.Fatalf("%s is not deployed?", app.Name)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := internal.DeployOverview(app, deployedVersion, "continue with undeploy?"); err != nil {
 | 
			
		||||
		if err := DeployOverview(app, deployedVersion, "continue with undeploy?"); err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -30,6 +30,7 @@ var appUpgradeCommand = cli.Command{
 | 
			
		||||
		internal.ChaosFlag,
 | 
			
		||||
		internal.NoDomainChecksFlag,
 | 
			
		||||
		internal.DontWaitConvergeFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Description: `
 | 
			
		||||
@ -51,9 +52,9 @@ including unstaged changes and can be useful for live hacking and testing new
 | 
			
		||||
recipes.
 | 
			
		||||
`,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
		stackName := app.StackName()
 | 
			
		||||
		conf := runtime.New()
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New(app.Server)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@ -86,7 +87,7 @@ recipes.
 | 
			
		||||
			logrus.Fatalf("%s is not deployed?", app.Name)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		catl, err := recipe.ReadRecipeCatalogue()
 | 
			
		||||
		catl, err := recipe.ReadRecipeCatalogue(conf)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
@ -127,8 +128,6 @@ recipes.
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		availableUpgrades = internal.ReverseStringList(availableUpgrades)
 | 
			
		||||
 | 
			
		||||
		var chosenUpgrade string
 | 
			
		||||
		if len(availableUpgrades) > 0 && !internal.Chaos {
 | 
			
		||||
			if internal.Force || internal.NoInput {
 | 
			
		||||
@ -137,7 +136,7 @@ recipes.
 | 
			
		||||
			} else {
 | 
			
		||||
				prompt := &survey.Select{
 | 
			
		||||
					Message: fmt.Sprintf("Please select an upgrade (current version: %s):", deployedVersion),
 | 
			
		||||
					Options: availableUpgrades,
 | 
			
		||||
					Options: internal.ReverseStringList(availableUpgrades),
 | 
			
		||||
				}
 | 
			
		||||
				if err := survey.AskOne(prompt, &chosenUpgrade); err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
@ -145,10 +144,15 @@ recipes.
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if internal.Force && chosenUpgrade == "" {
 | 
			
		||||
			logrus.Warnf("%s is already upgraded to latest but continuing (--force/--chaos)", app.Name)
 | 
			
		||||
			chosenUpgrade = deployedVersion
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// if release notes written after git tag published, read them before we
 | 
			
		||||
		// check out the tag and then they'll appear to be missing. this covers
 | 
			
		||||
		// when we obviously will forget to write release notes before publishing
 | 
			
		||||
		releaseNotes, err := internal.GetReleaseNotes(app.Recipe, chosenUpgrade)
 | 
			
		||||
		releaseNotes, err := GetReleaseNotes(app.Recipe, chosenUpgrade)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
@ -197,7 +201,7 @@ recipes.
 | 
			
		||||
		config.SetChaosVersionLabel(compose, stackName, chosenUpgrade)
 | 
			
		||||
		config.SetUpdateLabel(compose, stackName, app.Env)
 | 
			
		||||
 | 
			
		||||
		if err := internal.NewVersionOverview(app, deployedVersion, chosenUpgrade, releaseNotes); err != nil {
 | 
			
		||||
		if err := NewVersionOverview(app, deployedVersion, chosenUpgrade, releaseNotes); err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -214,7 +218,7 @@ recipes.
 | 
			
		||||
		postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"]
 | 
			
		||||
		if ok && !internal.DontWaitConverge {
 | 
			
		||||
			logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds)
 | 
			
		||||
			if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
 | 
			
		||||
			if err := PostCmds(cl, app, postDeployCmds); err != nil {
 | 
			
		||||
				logrus.Fatalf("attempting to run post deploy commands, saw: %s", err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -38,6 +38,7 @@ var appVersionCommand = cli.Command{
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.NoInputFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Usage:  "Show app versions",
 | 
			
		||||
@ -47,9 +48,9 @@ the individual image names, tags and digests. But also the Co-op Cloud recipe
 | 
			
		||||
version.
 | 
			
		||||
`,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
		stackName := app.StackName()
 | 
			
		||||
		conf := runtime.New()
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New(app.Server)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ import (
 | 
			
		||||
	"coopcloud.tech/abra/pkg/autocomplete"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/client"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/formatter"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
	"github.com/AlecAivazis/survey/v2"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
@ -19,12 +20,14 @@ var appVolumeListCommand = cli.Command{
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.NoInputFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	Usage:        "List volumes associated with an app",
 | 
			
		||||
	BashComplete: autocomplete.AppNameComplete,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New(app.Server)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@ -80,10 +83,12 @@ Passing "--force/-f" will select all volumes for removal. Be careful.
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.NoInputFlag,
 | 
			
		||||
		internal.ForceFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		app := internal.ValidateApp(c, conf)
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New(app.Server)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,7 @@ var catalogueGenerateCommand = cli.Command{
 | 
			
		||||
		internal.PublishFlag,
 | 
			
		||||
		internal.DryFlag,
 | 
			
		||||
		internal.SkipUpdatesFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Description: `
 | 
			
		||||
@ -54,18 +55,18 @@ keys configured on your account.
 | 
			
		||||
`,
 | 
			
		||||
	ArgsUsage: "[<recipe>]",
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		recipeName := c.Args().First()
 | 
			
		||||
		conf := runtime.New()
 | 
			
		||||
 | 
			
		||||
		if recipeName != "" {
 | 
			
		||||
			internal.ValidateRecipe(c)
 | 
			
		||||
			internal.ValidateRecipe(c, conf)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := catalogue.EnsureUpToDate(); err != nil {
 | 
			
		||||
		if err := catalogue.EnsureUpToDate(conf); err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		repos, err := recipe.ReadReposMetadata()
 | 
			
		||||
		repos, err := recipe.ReadReposMetadata(conf)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
@ -136,7 +137,7 @@ keys configured on your account.
 | 
			
		||||
				logrus.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			catlFS, err := recipe.ReadRecipeCatalogue()
 | 
			
		||||
			catlFS, err := recipe.ReadRecipeCatalogue(conf)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logrus.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@ -253,6 +253,16 @@ var DebugFlag = &cli.BoolFlag{
 | 
			
		||||
	Usage:       "Show DEBUG messages",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Offline stores the variable from OfflineFlag.
 | 
			
		||||
var Offline bool
 | 
			
		||||
 | 
			
		||||
// DebugFlag turns on/off offline mode.
 | 
			
		||||
var OfflineFlag = &cli.BoolFlag{
 | 
			
		||||
	Name:        "offline, o",
 | 
			
		||||
	Destination: &Offline,
 | 
			
		||||
	Usage:       "Prefer offline & filesystem access when possible",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MachineReadable stores the variable from MachineReadableFlag
 | 
			
		||||
var MachineReadable bool
 | 
			
		||||
 | 
			
		||||
@ -270,7 +280,7 @@ var RC bool
 | 
			
		||||
var RCFlag = &cli.BoolFlag{
 | 
			
		||||
	Name:        "rc, r",
 | 
			
		||||
	Destination: &RC,
 | 
			
		||||
	Usage:       "Insatll the latest release candidate",
 | 
			
		||||
	Usage:       "Install the latest release candidate",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var Major bool
 | 
			
		||||
 | 
			
		||||
@ -1,348 +0,0 @@
 | 
			
		||||
package internal
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"coopcloud.tech/abra/pkg/client"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/config"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/dns"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/formatter"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/git"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/lint"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/recipe"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/upstream/stack"
 | 
			
		||||
	"github.com/AlecAivazis/survey/v2"
 | 
			
		||||
	dockerClient "github.com/docker/docker/client"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// DeployAction is the main command-line action for this package
 | 
			
		||||
func DeployAction(c *cli.Context) error {
 | 
			
		||||
	app := ValidateApp(c)
 | 
			
		||||
	stackName := app.StackName()
 | 
			
		||||
	conf := runtime.New()
 | 
			
		||||
 | 
			
		||||
	cl, err := client.New(app.Server)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logrus.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !Chaos {
 | 
			
		||||
		if err := recipe.EnsureUpToDate(app.Recipe, conf); err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r, err := recipe.Get(app.Recipe, conf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logrus.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := lint.LintForErrors(r); err != nil {
 | 
			
		||||
		logrus.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logrus.Debugf("checking whether %s is already deployed", stackName)
 | 
			
		||||
 | 
			
		||||
	isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logrus.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if isDeployed {
 | 
			
		||||
		if Force || Chaos {
 | 
			
		||||
			logrus.Warnf("%s is already deployed but continuing (--force/--chaos)", app.Name)
 | 
			
		||||
		} else {
 | 
			
		||||
			logrus.Fatalf("%s is already deployed", app.Name)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	version := deployedVersion
 | 
			
		||||
	if version == "unknown" && !Chaos {
 | 
			
		||||
		catl, err := recipe.ReadRecipeCatalogue()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		if len(versions) > 0 {
 | 
			
		||||
			version = versions[len(versions)-1]
 | 
			
		||||
			logrus.Debugf("choosing %s as version to deploy", version)
 | 
			
		||||
			if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
 | 
			
		||||
				logrus.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			head, err := git.GetRecipeHead(app.Recipe)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logrus.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
			version = formatter.SmallSHA(head.String())
 | 
			
		||||
			logrus.Warn("no versions detected, using latest commit")
 | 
			
		||||
			if err := recipe.EnsureLatest(app.Recipe, conf); err != nil {
 | 
			
		||||
				logrus.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if version == "unknown" && !Chaos {
 | 
			
		||||
		logrus.Debugf("choosing %s as version to deploy", version)
 | 
			
		||||
		if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if version != "unknown" && !Chaos {
 | 
			
		||||
		if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if Chaos {
 | 
			
		||||
		logrus.Warnf("chaos mode engaged")
 | 
			
		||||
		var err error
 | 
			
		||||
		version, err = recipe.ChaosVersion(app.Recipe)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
 | 
			
		||||
	abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logrus.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	for k, v := range abraShEnv {
 | 
			
		||||
		app.Env[k] = v
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	composeFiles, err := config.GetAppComposeFiles(app.Recipe, app.Env)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logrus.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	deployOpts := stack.Deploy{
 | 
			
		||||
		Composefiles: composeFiles,
 | 
			
		||||
		Namespace:    stackName,
 | 
			
		||||
		Prune:        false,
 | 
			
		||||
		ResolveImage: stack.ResolveImageAlways,
 | 
			
		||||
	}
 | 
			
		||||
	compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logrus.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	config.ExposeAllEnv(stackName, compose, app.Env)
 | 
			
		||||
	config.SetRecipeLabel(compose, stackName, app.Recipe)
 | 
			
		||||
	config.SetChaosLabel(compose, stackName, Chaos)
 | 
			
		||||
	config.SetChaosVersionLabel(compose, stackName, version)
 | 
			
		||||
	config.SetUpdateLabel(compose, stackName, app.Env)
 | 
			
		||||
 | 
			
		||||
	if err := DeployOverview(app, version, "continue with deployment?"); err != nil {
 | 
			
		||||
		logrus.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !NoDomainChecks {
 | 
			
		||||
		domainName, ok := app.Env["DOMAIN"]
 | 
			
		||||
		if ok {
 | 
			
		||||
			if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
 | 
			
		||||
				logrus.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			logrus.Warn("skipping domain checks as no DOMAIN=... configured for app")
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		logrus.Warn("skipping domain checks as requested")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stack.WaitTimeout, err = config.GetTimeoutFromLabel(compose, stackName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logrus.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	logrus.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
 | 
			
		||||
 | 
			
		||||
	if err := stack.RunDeploy(cl, deployOpts, compose, app.Name, DontWaitConverge); err != nil {
 | 
			
		||||
		logrus.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"]
 | 
			
		||||
	if ok && !DontWaitConverge {
 | 
			
		||||
		logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds)
 | 
			
		||||
		if err := PostCmds(cl, app, postDeployCmds); err != nil {
 | 
			
		||||
			logrus.Fatalf("attempting to run post deploy commands, saw: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PostCmds parses a string of commands and executes them inside of the respective services
 | 
			
		||||
// the commands string must have the following format:
 | 
			
		||||
// "<service> <command> <arguments>|<service> <command> <arguments>|... "
 | 
			
		||||
func PostCmds(cl *dockerClient.Client, app config.App, commands string) error {
 | 
			
		||||
	abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
 | 
			
		||||
	if _, err := os.Stat(abraSh); err != nil {
 | 
			
		||||
		if os.IsNotExist(err) {
 | 
			
		||||
			return fmt.Errorf(fmt.Sprintf("%s does not exist for %s?", abraSh, app.Name))
 | 
			
		||||
		}
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, command := range strings.Split(commands, "|") {
 | 
			
		||||
		commandParts := strings.Split(command, " ")
 | 
			
		||||
		if len(commandParts) < 2 {
 | 
			
		||||
			return fmt.Errorf(fmt.Sprintf("not enough arguments: %s", command))
 | 
			
		||||
		}
 | 
			
		||||
		targetServiceName := commandParts[0]
 | 
			
		||||
		cmdName := commandParts[1]
 | 
			
		||||
		parsedCmdArgs := ""
 | 
			
		||||
		if len(commandParts) > 2 {
 | 
			
		||||
			parsedCmdArgs = fmt.Sprintf("%s ", strings.Join(commandParts[2:], " "))
 | 
			
		||||
		}
 | 
			
		||||
		logrus.Infof("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName)
 | 
			
		||||
 | 
			
		||||
		if err := EnsureCommand(abraSh, app.Recipe, cmdName); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		serviceNames, err := config.GetAppServiceNames(app.Name)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		matchingServiceName := false
 | 
			
		||||
		for _, serviceName := range serviceNames {
 | 
			
		||||
			if serviceName == targetServiceName {
 | 
			
		||||
				matchingServiceName = true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !matchingServiceName {
 | 
			
		||||
			return fmt.Errorf(fmt.Sprintf("no service %s for %s?", targetServiceName, app.Name))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		logrus.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName)
 | 
			
		||||
 | 
			
		||||
		Tty = true
 | 
			
		||||
		if err := RunCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeployOverview shows a deployment overview
 | 
			
		||||
func DeployOverview(app config.App, version, message string) error {
 | 
			
		||||
	tableCol := []string{"server", "recipe", "config", "domain", "version"}
 | 
			
		||||
	table := formatter.CreateTable(tableCol)
 | 
			
		||||
 | 
			
		||||
	deployConfig := "compose.yml"
 | 
			
		||||
	if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
 | 
			
		||||
		deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	server := app.Server
 | 
			
		||||
	if app.Server == "default" {
 | 
			
		||||
		server = "local"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	table.Append([]string{server, app.Recipe, deployConfig, app.Domain, version})
 | 
			
		||||
	table.Render()
 | 
			
		||||
 | 
			
		||||
	if NoInput {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	response := false
 | 
			
		||||
	prompt := &survey.Confirm{
 | 
			
		||||
		Message: message,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := survey.AskOne(prompt, &response); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !response {
 | 
			
		||||
		logrus.Fatal("exiting as requested")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewVersionOverview shows an upgrade or downgrade overview
 | 
			
		||||
func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes string) error {
 | 
			
		||||
	tableCol := []string{"server", "recipe", "config", "domain", "current version", "to be deployed"}
 | 
			
		||||
	table := formatter.CreateTable(tableCol)
 | 
			
		||||
 | 
			
		||||
	deployConfig := "compose.yml"
 | 
			
		||||
	if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
 | 
			
		||||
		deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	server := app.Server
 | 
			
		||||
	if app.Server == "default" {
 | 
			
		||||
		server = "local"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	table.Append([]string{server, app.Recipe, deployConfig, app.Domain, currentVersion, newVersion})
 | 
			
		||||
	table.Render()
 | 
			
		||||
 | 
			
		||||
	if releaseNotes == "" {
 | 
			
		||||
		var err error
 | 
			
		||||
		releaseNotes, err = GetReleaseNotes(app.Recipe, newVersion)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if releaseNotes != "" && newVersion != "" {
 | 
			
		||||
		fmt.Println()
 | 
			
		||||
		fmt.Println(fmt.Sprintf("%s release notes:\n\n%s", newVersion, releaseNotes))
 | 
			
		||||
	} else {
 | 
			
		||||
		logrus.Warnf("no release notes available for %s", newVersion)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if NoInput {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	response := false
 | 
			
		||||
	prompt := &survey.Confirm{
 | 
			
		||||
		Message: "continue with deployment?",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := survey.AskOne(prompt, &response); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !response {
 | 
			
		||||
		logrus.Fatal("exiting as requested")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetReleaseNotes prints release notes for a recipe version
 | 
			
		||||
func GetReleaseNotes(recipeName, version string) (string, error) {
 | 
			
		||||
	if version == "" {
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fpath := path.Join(config.RECIPES_DIR, recipeName, "release", version)
 | 
			
		||||
 | 
			
		||||
	if _, err := os.Stat(fpath); !os.IsNotExist(err) {
 | 
			
		||||
		releaseNotes, err := ioutil.ReadFile(fpath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return string(releaseNotes), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return "", nil
 | 
			
		||||
}
 | 
			
		||||
@ -1,199 +0,0 @@
 | 
			
		||||
package internal
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"path"
 | 
			
		||||
 | 
			
		||||
	"coopcloud.tech/abra/pkg/app"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/client"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/config"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/formatter"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/jsontable"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/recipe"
 | 
			
		||||
	recipePkg "coopcloud.tech/abra/pkg/recipe"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/secret"
 | 
			
		||||
	"github.com/AlecAivazis/survey/v2"
 | 
			
		||||
	dockerClient "github.com/docker/docker/client"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// AppSecrets represents all app secrest
 | 
			
		||||
type AppSecrets map[string]string
 | 
			
		||||
 | 
			
		||||
// RecipeName is used for configuring recipe name programmatically
 | 
			
		||||
var RecipeName string
 | 
			
		||||
 | 
			
		||||
// createSecrets creates all secrets for a new app.
 | 
			
		||||
func createSecrets(cl *dockerClient.Client, sanitisedAppName string) (AppSecrets, error) {
 | 
			
		||||
	appEnvPath := path.Join(config.ABRA_DIR, "servers", NewAppServer, fmt.Sprintf("%s.env", Domain))
 | 
			
		||||
	appEnv, err := config.ReadEnv(appEnvPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	secretEnvVars := secret.ReadSecretEnvVars(appEnv)
 | 
			
		||||
	secrets, err := secret.GenerateSecrets(cl, secretEnvVars, sanitisedAppName, NewAppServer)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if Pass {
 | 
			
		||||
		for secretName := range secrets {
 | 
			
		||||
			secretValue := secrets[secretName]
 | 
			
		||||
			if err := secret.PassInsertSecret(secretValue, secretName, Domain, NewAppServer); err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return secrets, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
 | 
			
		||||
func ensureDomainFlag(recipe recipe.Recipe, server string) error {
 | 
			
		||||
	if Domain == "" && !NoInput {
 | 
			
		||||
		prompt := &survey.Input{
 | 
			
		||||
			Message: "Specify app domain",
 | 
			
		||||
			Default: fmt.Sprintf("%s.%s", recipe.Name, server),
 | 
			
		||||
		}
 | 
			
		||||
		if err := survey.AskOne(prompt, &Domain); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if Domain == "" {
 | 
			
		||||
		return fmt.Errorf("no domain provided")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// promptForSecrets asks if we should generate secrets for a new app.
 | 
			
		||||
func promptForSecrets(appName string) error {
 | 
			
		||||
	app, err := app.Get(appName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	secretEnvVars := secret.ReadSecretEnvVars(app.Env)
 | 
			
		||||
	if len(secretEnvVars) == 0 {
 | 
			
		||||
		logrus.Debugf("%s has no secrets to generate, skipping...", app.Recipe)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !Secrets && !NoInput {
 | 
			
		||||
		prompt := &survey.Confirm{
 | 
			
		||||
			Message: "Generate app secrets?",
 | 
			
		||||
		}
 | 
			
		||||
		if err := survey.AskOne(prompt, &Secrets); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ensureServerFlag checks if the server flag was used. if not, asks the user for it.
 | 
			
		||||
func ensureServerFlag() error {
 | 
			
		||||
	servers, err := config.GetServers()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if NewAppServer == "" && !NoInput {
 | 
			
		||||
		prompt := &survey.Select{
 | 
			
		||||
			Message: "Select app server:",
 | 
			
		||||
			Options: servers,
 | 
			
		||||
		}
 | 
			
		||||
		if err := survey.AskOne(prompt, &NewAppServer); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if NewAppServer == "" {
 | 
			
		||||
		return fmt.Errorf("no server provided")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewAction is the new app creation logic
 | 
			
		||||
func NewAction(c *cli.Context) error {
 | 
			
		||||
	recipe := ValidateRecipeWithPrompt(c, runtime.WithEnsureRecipeLatest(false))
 | 
			
		||||
 | 
			
		||||
	conf := runtime.New(runtime.WithEnsureRecipeLatest(false))
 | 
			
		||||
	if err := recipePkg.EnsureUpToDate(recipe.Name, conf); err != nil {
 | 
			
		||||
		logrus.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := ensureServerFlag(); err != nil {
 | 
			
		||||
		logrus.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := ensureDomainFlag(recipe, NewAppServer); err != nil {
 | 
			
		||||
		logrus.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sanitisedAppName := config.SanitiseAppName(Domain)
 | 
			
		||||
	logrus.Debugf("%s sanitised as %s for new app", Domain, sanitisedAppName)
 | 
			
		||||
 | 
			
		||||
	if err := config.TemplateAppEnvSample(recipe.Name, Domain, NewAppServer, Domain); err != nil {
 | 
			
		||||
		logrus.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := promptForSecrets(Domain); err != nil {
 | 
			
		||||
		logrus.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cl, err := client.New(NewAppServer)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logrus.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var secrets AppSecrets
 | 
			
		||||
	var secretTable *jsontable.JSONTable
 | 
			
		||||
	if Secrets {
 | 
			
		||||
		secrets, err := createSecrets(cl, sanitisedAppName)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		secretCols := []string{"Name", "Value"}
 | 
			
		||||
		secretTable = formatter.CreateTable(secretCols)
 | 
			
		||||
		for secret := range secrets {
 | 
			
		||||
			secretTable.Append([]string{secret, secrets[secret]})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if NewAppServer == "default" {
 | 
			
		||||
		NewAppServer = "local"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tableCol := []string{"server", "recipe", "domain"}
 | 
			
		||||
	table := formatter.CreateTable(tableCol)
 | 
			
		||||
	table.Append([]string{NewAppServer, recipe.Name, Domain})
 | 
			
		||||
 | 
			
		||||
	fmt.Println("")
 | 
			
		||||
	fmt.Println(fmt.Sprintf("A new %s app has been created! Here is an overview:", recipe.Name))
 | 
			
		||||
	fmt.Println("")
 | 
			
		||||
	table.Render()
 | 
			
		||||
	fmt.Println("")
 | 
			
		||||
	fmt.Println("You can configure this app by running the following:")
 | 
			
		||||
	fmt.Println(fmt.Sprintf("\n    abra app config %s", Domain))
 | 
			
		||||
	fmt.Println("")
 | 
			
		||||
	fmt.Println("You can deploy this app by running the following:")
 | 
			
		||||
	fmt.Println(fmt.Sprintf("\n    abra app deploy %s", Domain))
 | 
			
		||||
	fmt.Println("")
 | 
			
		||||
 | 
			
		||||
	if len(secrets) > 0 {
 | 
			
		||||
		fmt.Println("Here are your generated secrets:")
 | 
			
		||||
		fmt.Println("")
 | 
			
		||||
		secretTable.Render()
 | 
			
		||||
		fmt.Println("")
 | 
			
		||||
		logrus.Warn("generated secrets are not shown again, please take note of them *now*")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@ -15,13 +15,9 @@ import (
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// AppName is used for configuring app name programmatically
 | 
			
		||||
var AppName string
 | 
			
		||||
 | 
			
		||||
// ValidateRecipe ensures the recipe arg is valid.
 | 
			
		||||
func ValidateRecipe(c *cli.Context, opts ...runtime.Option) recipe.Recipe {
 | 
			
		||||
func ValidateRecipe(c *cli.Context, conf *runtime.Config) recipe.Recipe {
 | 
			
		||||
	recipeName := c.Args().First()
 | 
			
		||||
	conf := runtime.New(opts...)
 | 
			
		||||
 | 
			
		||||
	if recipeName == "" {
 | 
			
		||||
		ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
 | 
			
		||||
@ -53,14 +49,13 @@ func ValidateRecipe(c *cli.Context, opts ...runtime.Option) recipe.Recipe {
 | 
			
		||||
 | 
			
		||||
// ValidateRecipeWithPrompt ensures a recipe argument is present before
 | 
			
		||||
// validating, asking for input if required.
 | 
			
		||||
func ValidateRecipeWithPrompt(c *cli.Context, opts ...runtime.Option) recipe.Recipe {
 | 
			
		||||
func ValidateRecipeWithPrompt(c *cli.Context, conf *runtime.Config) recipe.Recipe {
 | 
			
		||||
	recipeName := c.Args().First()
 | 
			
		||||
	conf := runtime.New(opts...)
 | 
			
		||||
 | 
			
		||||
	if recipeName == "" && !NoInput {
 | 
			
		||||
		var recipes []string
 | 
			
		||||
 | 
			
		||||
		catl, err := recipe.ReadRecipeCatalogue()
 | 
			
		||||
		catl, err := recipe.ReadRecipeCatalogue(conf)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
@ -94,11 +89,6 @@ func ValidateRecipeWithPrompt(c *cli.Context, opts ...runtime.Option) recipe.Rec
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if RecipeName != "" {
 | 
			
		||||
		recipeName = RecipeName
 | 
			
		||||
		logrus.Debugf("programmatically setting recipe name to %s", recipeName)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if recipeName == "" {
 | 
			
		||||
		ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
 | 
			
		||||
	}
 | 
			
		||||
@ -118,14 +108,8 @@ func ValidateRecipeWithPrompt(c *cli.Context, opts ...runtime.Option) recipe.Rec
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ValidateApp ensures the app name arg is valid.
 | 
			
		||||
func ValidateApp(c *cli.Context, opts ...runtime.Option) config.App {
 | 
			
		||||
func ValidateApp(c *cli.Context, conf *runtime.Config) config.App {
 | 
			
		||||
	appName := c.Args().First()
 | 
			
		||||
	conf := runtime.New(opts...)
 | 
			
		||||
 | 
			
		||||
	if AppName != "" {
 | 
			
		||||
		appName = AppName
 | 
			
		||||
		logrus.Debugf("programmatically setting app name to %s", appName)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if appName == "" {
 | 
			
		||||
		ShowSubcommandHelpAndError(c, errors.New("no app provided"))
 | 
			
		||||
 | 
			
		||||
@ -17,19 +17,20 @@ var recipeFetchCommand = cli.Command{
 | 
			
		||||
	Description: "Fetchs all recipes without arguments.",
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	BashComplete: autocomplete.RecipeNameComplete,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		recipeName := c.Args().First()
 | 
			
		||||
		conf := runtime.New()
 | 
			
		||||
 | 
			
		||||
		if recipeName != "" {
 | 
			
		||||
			internal.ValidateRecipe(c)
 | 
			
		||||
			internal.ValidateRecipe(c, conf)
 | 
			
		||||
			return nil // ValidateRecipe ensures latest checkout
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		repos, err := recipe.ReadReposMetadata()
 | 
			
		||||
		repos, err := recipe.ReadReposMetadata(conf)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -21,12 +21,13 @@ var recipeLintCommand = cli.Command{
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.OnlyErrorFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	BashComplete: autocomplete.RecipeNameComplete,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		recipe := internal.ValidateRecipe(c)
 | 
			
		||||
		conf := runtime.New()
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		recipe := internal.ValidateRecipe(c, conf)
 | 
			
		||||
 | 
			
		||||
		if err := recipePkg.EnsureUpToDate(recipe.Name, conf); err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ import (
 | 
			
		||||
	"coopcloud.tech/abra/cli/internal"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/formatter"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/recipe"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
)
 | 
			
		||||
@ -29,10 +30,13 @@ var recipeListCommand = cli.Command{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.MachineReadableFlag,
 | 
			
		||||
		patternFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		catl, err := recipe.ReadRecipeCatalogue()
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
 | 
			
		||||
		catl, err := recipe.ReadRecipeCatalogue(conf)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err.Error())
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -36,6 +36,7 @@ var recipeNewCommand = cli.Command{
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.NoInputFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:    internal.SubCommandBefore,
 | 
			
		||||
	Usage:     "Create a new recipe",
 | 
			
		||||
 | 
			
		||||
@ -55,11 +55,17 @@ your SSH keys configured on your account.
 | 
			
		||||
		internal.MinorFlag,
 | 
			
		||||
		internal.PatchFlag,
 | 
			
		||||
		internal.PublishFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	BashComplete: autocomplete.RecipeNameComplete,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		recipe := internal.ValidateRecipeWithPrompt(c, runtime.WithEnsureRecipeLatest(false))
 | 
			
		||||
		conf := runtime.New(
 | 
			
		||||
			runtime.WithOffline(internal.Offline),
 | 
			
		||||
			runtime.WithEnsureRecipeUpToDate(false),
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		recipe := internal.ValidateRecipeWithPrompt(c, conf)
 | 
			
		||||
 | 
			
		||||
		imagesTmp, err := getImageVersions(recipe)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,7 @@ var recipeSyncCommand = cli.Command{
 | 
			
		||||
		internal.MajorFlag,
 | 
			
		||||
		internal.MinorFlag,
 | 
			
		||||
		internal.PatchFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Description: `
 | 
			
		||||
@ -42,7 +43,12 @@ auto-generate it for you. The <recipe> configuration will be updated on the
 | 
			
		||||
local file system.
 | 
			
		||||
`,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		recipe := internal.ValidateRecipeWithPrompt(c, runtime.WithEnsureRecipeLatest(false))
 | 
			
		||||
		conf := runtime.New(
 | 
			
		||||
			runtime.WithOffline(internal.Offline),
 | 
			
		||||
			runtime.WithEnsureRecipeUpToDate(false),
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		recipe := internal.ValidateRecipeWithPrompt(c, conf)
 | 
			
		||||
 | 
			
		||||
		mainApp, err := internal.GetMainAppImage(recipe)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
 | 
			
		||||
@ -68,11 +68,12 @@ You may invoke this command in "wizard" mode and be prompted for input:
 | 
			
		||||
		internal.MajorFlag,
 | 
			
		||||
		internal.MachineReadableFlag,
 | 
			
		||||
		internal.AllTagsFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		recipe := internal.ValidateRecipeWithPrompt(c)
 | 
			
		||||
		conf := runtime.New()
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
		recipe := internal.ValidateRecipeWithPrompt(c, conf)
 | 
			
		||||
 | 
			
		||||
		if err := recipePkg.EnsureUpToDate(recipe.Name, conf); err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
@ -184,7 +185,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
 | 
			
		||||
				continue // skip on to the next tag and don't update any compose files
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name)
 | 
			
		||||
			catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name, conf)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logrus.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@ -17,13 +17,19 @@ var recipeVersionCommand = cli.Command{
 | 
			
		||||
	ArgsUsage: "<recipe>",
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	BashComplete: autocomplete.RecipeNameComplete,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		recipe := internal.ValidateRecipe(c, runtime.WithEnsureRecipeLatest(false))
 | 
			
		||||
		conf := runtime.New(
 | 
			
		||||
			runtime.WithOffline(internal.Offline),
 | 
			
		||||
			runtime.WithEnsureRecipeUpToDate(false),
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		catalogue, err := recipePkg.ReadRecipeCatalogue()
 | 
			
		||||
		recipe := internal.ValidateRecipe(c, conf)
 | 
			
		||||
 | 
			
		||||
		catalogue, err := recipePkg.ReadRecipeCatalogue(conf)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,7 @@ var RecordListCommand = cli.Command{
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.DNSProviderFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Description: `
 | 
			
		||||
 | 
			
		||||
@ -30,6 +30,7 @@ var RecordNewCommand = cli.Command{
 | 
			
		||||
		internal.DNSValueFlag,
 | 
			
		||||
		internal.DNSTTLFlag,
 | 
			
		||||
		internal.DNSPriorityFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Description: `
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@ var RecordRemoveCommand = cli.Command{
 | 
			
		||||
		internal.DNSProviderFlag,
 | 
			
		||||
		internal.DNSTypeFlag,
 | 
			
		||||
		internal.DNSNameFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Description: `
 | 
			
		||||
 | 
			
		||||
@ -118,6 +118,7 @@ developer machine.
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.NoInputFlag,
 | 
			
		||||
		localFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:    internal.SubCommandBefore,
 | 
			
		||||
	ArgsUsage: "<domain>",
 | 
			
		||||
 | 
			
		||||
@ -28,6 +28,7 @@ var serverListCommand = cli.Command{
 | 
			
		||||
		problemsFilterFlag,
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.MachineReadableFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
 | 
			
		||||
@ -222,6 +222,7 @@ API tokens are read from the environment if specified, e.g.
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.NoInputFlag,
 | 
			
		||||
		internal.ServerProviderFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
 | 
			
		||||
		// Capsul
 | 
			
		||||
		internal.CapsulInstanceURLFlag,
 | 
			
		||||
 | 
			
		||||
@ -43,6 +43,7 @@ also be removed. This can result in unwanted data loss if not used carefully.
 | 
			
		||||
		allFilterFlag,
 | 
			
		||||
		volumesFilterFlag,
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	BashComplete: autocomplete.ServerNameComplete,
 | 
			
		||||
 | 
			
		||||
@ -120,6 +120,7 @@ like tears in rain.
 | 
			
		||||
		internal.NoInputFlag,
 | 
			
		||||
		rmServerFlag,
 | 
			
		||||
		internal.ServerProviderFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
 | 
			
		||||
		// Hetzner
 | 
			
		||||
		internal.HetznerCloudNameFlag,
 | 
			
		||||
 | 
			
		||||
@ -49,6 +49,7 @@ var Notify = cli.Command{
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		internal.DebugFlag,
 | 
			
		||||
		majorFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Description: `
 | 
			
		||||
@ -57,6 +58,8 @@ catalogue. If a new patch/minor version is available, a notification is
 | 
			
		||||
printed. To include major versions use the --major flag.
 | 
			
		||||
`,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New("default")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
@ -75,7 +78,7 @@ printed. To include major versions use the --major flag.
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if recipeName != "" {
 | 
			
		||||
				_, err = getLatestUpgrade(cl, stackName, recipeName)
 | 
			
		||||
				_, err = getLatestUpgrade(cl, stackName, recipeName, conf)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					logrus.Fatal(err)
 | 
			
		||||
				}
 | 
			
		||||
@ -97,6 +100,7 @@ var UpgradeApp = cli.Command{
 | 
			
		||||
		internal.ChaosFlag,
 | 
			
		||||
		majorFlag,
 | 
			
		||||
		allFlag,
 | 
			
		||||
		internal.OfflineFlag,
 | 
			
		||||
	},
 | 
			
		||||
	Before: internal.SubCommandBefore,
 | 
			
		||||
	Description: `
 | 
			
		||||
@ -109,13 +113,13 @@ break things. Only apps that are not deployed with "--chaos" are upgraded, to
 | 
			
		||||
update chaos deployments use the "--chaos" flag. Use it with care.
 | 
			
		||||
`,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		conf := runtime.New(runtime.WithOffline(internal.Offline))
 | 
			
		||||
 | 
			
		||||
		cl, err := client.New("default")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logrus.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		conf := runtime.New(runtime.WithEnsureRecipeLatest(true))
 | 
			
		||||
 | 
			
		||||
		if !updateAll {
 | 
			
		||||
			stackName := c.Args().Get(0)
 | 
			
		||||
			recipeName := c.Args().Get(1)
 | 
			
		||||
@ -223,13 +227,14 @@ func getEnv(cl *dockerclient.Client, stackName string) (config.AppEnv, error) {
 | 
			
		||||
 | 
			
		||||
// getLatestUpgrade returns the latest available version for an app respecting
 | 
			
		||||
// the "--major" flag if it is newer than the currently deployed version.
 | 
			
		||||
func getLatestUpgrade(cl *dockerclient.Client, stackName string, recipeName string) (string, error) {
 | 
			
		||||
func getLatestUpgrade(cl *dockerclient.Client, stackName string,
 | 
			
		||||
	recipeName string, conf *runtime.Config) (string, error) {
 | 
			
		||||
	deployedVersion, err := getDeployedVersion(cl, stackName, recipeName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	availableUpgrades, err := getAvailableUpgrades(cl, stackName, recipeName, deployedVersion)
 | 
			
		||||
	availableUpgrades, err := getAvailableUpgrades(cl, stackName, recipeName, deployedVersion, conf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
@ -272,8 +277,8 @@ func getDeployedVersion(cl *dockerclient.Client, stackName string, recipeName st
 | 
			
		||||
// than the deployed version. It only includes major upgrades if the "--major"
 | 
			
		||||
// flag is set.
 | 
			
		||||
func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName string,
 | 
			
		||||
	deployedVersion string) ([]string, error) {
 | 
			
		||||
	catl, err := recipe.ReadRecipeCatalogue()
 | 
			
		||||
	deployedVersion string, conf *runtime.Config) ([]string, error) {
 | 
			
		||||
	catl, err := recipe.ReadRecipeCatalogue(conf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@ -389,7 +394,7 @@ func createDeployConfig(recipeName string, stackName string, env config.AppEnv)
 | 
			
		||||
// tryUpgrade performs the upgrade if all the requirements are fulfilled.
 | 
			
		||||
func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string, conf *runtime.Config) error {
 | 
			
		||||
	if recipeName == "" {
 | 
			
		||||
		logrus.Debugf("Don't update %s due to missing recipe name", stackName)
 | 
			
		||||
		logrus.Debugf("don't update %s due to missing recipe name", stackName)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -399,7 +404,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string, conf *run
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if chaos && !internal.Chaos {
 | 
			
		||||
		logrus.Debugf("Don't update %s due to chaos deployment.", stackName)
 | 
			
		||||
		logrus.Debugf("don't update %s due to chaos deployment", stackName)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -409,17 +414,17 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string, conf *run
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !updatesEnabled {
 | 
			
		||||
		logrus.Debugf("Don't update %s due to disabled auto updates or missing ENABLE_AUTO_UPDATE env.", stackName)
 | 
			
		||||
		logrus.Debugf("don't update %s due to disabled auto updates or missing ENABLE_AUTO_UPDATE env", stackName)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	upgradeVersion, err := getLatestUpgrade(cl, stackName, recipeName)
 | 
			
		||||
	upgradeVersion, err := getLatestUpgrade(cl, stackName, recipeName, conf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if upgradeVersion == "" {
 | 
			
		||||
		logrus.Debugf("Don't update %s due to no new version.", stackName)
 | 
			
		||||
		logrus.Debugf("don't update %s due to no new version", stackName)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -429,7 +434,8 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string, conf *run
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// upgrade performs all necessary steps to upgrade an app.
 | 
			
		||||
func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion string, conf *runtime.Config) error {
 | 
			
		||||
func upgrade(cl *dockerclient.Client, stackName, recipeName,
 | 
			
		||||
	upgradeVersion string, conf *runtime.Config) error {
 | 
			
		||||
	env, err := getEnv(cl, stackName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@ -455,7 +461,7 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion stri
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logrus.Infof("Upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion)
 | 
			
		||||
	logrus.Infof("upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion)
 | 
			
		||||
 | 
			
		||||
	err = stack.RunDeploy(cl, deployOpts, compose, stackName, true)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										13
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								go.mod
									
									
									
									
									
								
							@ -7,12 +7,11 @@ require (
 | 
			
		||||
	github.com/AlecAivazis/survey/v2 v2.3.7
 | 
			
		||||
	github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731094149-b031ea1211e7
 | 
			
		||||
	github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
 | 
			
		||||
	github.com/docker/cli v20.10.25+incompatible
 | 
			
		||||
	github.com/docker/cli v24.0.5+incompatible
 | 
			
		||||
	github.com/docker/distribution v2.8.2+incompatible
 | 
			
		||||
	github.com/docker/docker v20.10.25+incompatible
 | 
			
		||||
	github.com/docker/docker v24.0.5+incompatible
 | 
			
		||||
	github.com/docker/go-units v0.5.0
 | 
			
		||||
	github.com/go-git/go-git/v5 v5.7.0
 | 
			
		||||
	github.com/hetznercloud/hcloud-go v1.48.0
 | 
			
		||||
	github.com/go-git/go-git/v5 v5.8.1
 | 
			
		||||
	github.com/moby/sys/signal v0.7.0
 | 
			
		||||
	github.com/moby/term v0.5.0
 | 
			
		||||
	github.com/olekukonko/tablewriter v0.0.5
 | 
			
		||||
@ -24,6 +23,7 @@ require (
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	coopcloud.tech/libcapsul v0.0.0-20230605070824-878af473f07b
 | 
			
		||||
	github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect
 | 
			
		||||
	github.com/buger/goterm v1.0.4
 | 
			
		||||
	github.com/containerd/containerd v1.5.9 // indirect
 | 
			
		||||
	github.com/containers/image v3.0.2+incompatible
 | 
			
		||||
@ -35,11 +35,14 @@ require (
 | 
			
		||||
	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 | 
			
		||||
	github.com/gorilla/mux v1.8.0 // indirect
 | 
			
		||||
	github.com/hashicorp/go-retryablehttp v0.7.4
 | 
			
		||||
	github.com/hetznercloud/hcloud-go v1.48.0
 | 
			
		||||
	github.com/klauspost/pgzip v1.2.6
 | 
			
		||||
	github.com/libdns/gandi v1.0.2
 | 
			
		||||
	github.com/libdns/libdns v0.2.1
 | 
			
		||||
	github.com/moby/sys/mount v0.2.0 // indirect
 | 
			
		||||
	github.com/moby/patternmatcher v0.5.0 // indirect
 | 
			
		||||
	github.com/moby/sys/sequential v0.5.0 // indirect
 | 
			
		||||
	github.com/opencontainers/image-spec v1.0.3-0.20211202193544-a5463b7f9c84 // indirect
 | 
			
		||||
	github.com/rogpeppe/go-internal v1.11.0 // indirect
 | 
			
		||||
	github.com/sergi/go-diff v1.2.0 // indirect
 | 
			
		||||
	github.com/spf13/cobra v1.3.0 // indirect
 | 
			
		||||
	github.com/theupdateframework/notary v0.7.0 // indirect
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										72
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										72
									
								
								go.sum
									
									
									
									
									
								
							@ -51,7 +51,11 @@ coopcloud.tech/libcapsul v0.0.0-20230605070824-878af473f07b h1:ORxAmzrd6SSlSGm/R
 | 
			
		||||
coopcloud.tech/libcapsul v0.0.0-20230605070824-878af473f07b/go.mod h1:6u7ekg+v+yL07QtU7E+K+WqK9LKDDqTF4s+PrIXZ+QM=
 | 
			
		||||
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52 h1:cyFFOl0tKe+dVHt8saejG8xoff33eQiHxFCVzRpPUjM=
 | 
			
		||||
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
 | 
			
		||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
 | 
			
		||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
 | 
			
		||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 | 
			
		||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 h1:EKPd1INOIyr5hWOWhvpmQpY6tKjeG0hT1s3AMC/9fic=
 | 
			
		||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1/go.mod h1:VzwV+t+dZ9j/H867F1M2ziD+yLHtB46oM35FxxMJ4d0=
 | 
			
		||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
 | 
			
		||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
 | 
			
		||||
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731094149-b031ea1211e7 h1:asQtdXYbxEYWcwAQqJTVYC/RltB4eqoWKvqWg/LFPOg=
 | 
			
		||||
@ -86,8 +90,9 @@ github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JP
 | 
			
		||||
github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
 | 
			
		||||
github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
 | 
			
		||||
github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
 | 
			
		||||
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
 | 
			
		||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
 | 
			
		||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
 | 
			
		||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
 | 
			
		||||
github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
 | 
			
		||||
github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
 | 
			
		||||
github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ=
 | 
			
		||||
@ -105,8 +110,8 @@ github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb0
 | 
			
		||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
 | 
			
		||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
 | 
			
		||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 | 
			
		||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek=
 | 
			
		||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
 | 
			
		||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs=
 | 
			
		||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
 | 
			
		||||
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
 | 
			
		||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
 | 
			
		||||
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 | 
			
		||||
@ -161,7 +166,6 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembj
 | 
			
		||||
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
 | 
			
		||||
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=
 | 
			
		||||
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
 | 
			
		||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
 | 
			
		||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
 | 
			
		||||
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
 | 
			
		||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 | 
			
		||||
@ -189,7 +193,6 @@ github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp
 | 
			
		||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 | 
			
		||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ=
 | 
			
		||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
 | 
			
		||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
 | 
			
		||||
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
 | 
			
		||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
 | 
			
		||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 | 
			
		||||
@ -216,7 +219,6 @@ github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1
 | 
			
		||||
github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo=
 | 
			
		||||
github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo=
 | 
			
		||||
github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=
 | 
			
		||||
github.com/containerd/cgroups v1.0.1 h1:iJnMvco9XGvKUvNQkv88bE4uJXxRQH18efbKo9w5vHQ=
 | 
			
		||||
github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU=
 | 
			
		||||
github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
 | 
			
		||||
github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
 | 
			
		||||
@ -247,7 +249,6 @@ github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL
 | 
			
		||||
github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo=
 | 
			
		||||
github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y=
 | 
			
		||||
github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ=
 | 
			
		||||
github.com/containerd/continuity v0.1.0 h1:UFRRY5JemiAhPZrr/uE0n8fMTLcZsUvySPr1+D7pgr8=
 | 
			
		||||
github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM=
 | 
			
		||||
github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
 | 
			
		||||
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
 | 
			
		||||
@ -323,6 +324,7 @@ github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr
 | 
			
		||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
 | 
			
		||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
 | 
			
		||||
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
 | 
			
		||||
github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=
 | 
			
		||||
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
 | 
			
		||||
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
 | 
			
		||||
github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
 | 
			
		||||
@ -341,16 +343,16 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
 | 
			
		||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 | 
			
		||||
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
 | 
			
		||||
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 | 
			
		||||
github.com/docker/cli v20.10.25+incompatible h1:ZdAJcooG2FK5PojbK8ALV6pf8Tut+1hgv39AxnAq9lo=
 | 
			
		||||
github.com/docker/cli v20.10.25+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 | 
			
		||||
github.com/docker/cli v24.0.5+incompatible h1:WeBimjvS0eKdH4Ygx+ihVq1Q++xg36M/rMi4aXAvodc=
 | 
			
		||||
github.com/docker/cli v24.0.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 | 
			
		||||
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
 | 
			
		||||
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 | 
			
		||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 | 
			
		||||
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
 | 
			
		||||
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 | 
			
		||||
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 | 
			
		||||
github.com/docker/docker v20.10.25+incompatible h1:URiHXOEOlhi6FS5U+YUE8YnsnZjIV3R+TFezL2ngdW0=
 | 
			
		||||
github.com/docker/docker v20.10.25+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 | 
			
		||||
github.com/docker/docker v24.0.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY=
 | 
			
		||||
github.com/docker/docker v24.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 | 
			
		||||
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
 | 
			
		||||
github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o=
 | 
			
		||||
github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c=
 | 
			
		||||
@ -423,8 +425,8 @@ github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8
 | 
			
		||||
github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
 | 
			
		||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
 | 
			
		||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
 | 
			
		||||
github.com/go-git/go-git/v5 v5.7.0 h1:t9AudWVLmqzlo+4bqdf7GY+46SUuRsx59SboFxkq2aE=
 | 
			
		||||
github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8=
 | 
			
		||||
github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A=
 | 
			
		||||
github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo=
 | 
			
		||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 | 
			
		||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 | 
			
		||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 | 
			
		||||
@ -619,8 +621,8 @@ github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn
 | 
			
		||||
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
 | 
			
		||||
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
 | 
			
		||||
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
 | 
			
		||||
github.com/hetznercloud/hcloud-go v1.47.0 h1:WMZDwLPtMZwOLWIgERHrrrTzRFdHx0hTygYVQ4VWHW4=
 | 
			
		||||
github.com/hetznercloud/hcloud-go v1.47.0/go.mod h1:zSpmBnxIdb5oMdbpVg1Q977Cq2qiwERkjj3jqRbHH5U=
 | 
			
		||||
github.com/hetznercloud/hcloud-go v1.48.0 h1:b6x0ABNYJr8zVX9sE1kCNMU/sndzI8vmZjxEWEr+Gn0=
 | 
			
		||||
github.com/hetznercloud/hcloud-go v1.48.0/go.mod h1:VzDWThl47lOnZXY0q5/LPFD+M62pfe/52TV+mOrpp9Q=
 | 
			
		||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
 | 
			
		||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
 | 
			
		||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 | 
			
		||||
@ -631,9 +633,8 @@ github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ
 | 
			
		||||
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 | 
			
		||||
github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 | 
			
		||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 | 
			
		||||
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
 | 
			
		||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 | 
			
		||||
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
 | 
			
		||||
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
 | 
			
		||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 | 
			
		||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 | 
			
		||||
github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
 | 
			
		||||
@ -763,12 +764,14 @@ github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
 | 
			
		||||
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
 | 
			
		||||
github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM=
 | 
			
		||||
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
 | 
			
		||||
github.com/moby/sys/mount v0.2.0 h1:WhCW5B355jtxndN5ovugJlMFJawbUODuW8fSnEH6SSM=
 | 
			
		||||
github.com/moby/sys/mount v0.2.0/go.mod h1:aAivFE2LB3W4bACsUXChRHQ0qKWsetY4Y9V7sxOougM=
 | 
			
		||||
github.com/moby/patternmatcher v0.5.0 h1:YCZgJOeULcxLw1Q+sVR636pmS7sPEn1Qo2iAN6M7DBo=
 | 
			
		||||
github.com/moby/patternmatcher v0.5.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
 | 
			
		||||
github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
 | 
			
		||||
github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
 | 
			
		||||
github.com/moby/sys/mountinfo v0.5.0 h1:2Ks8/r6lopsxWi9m58nlwjaeSzUX9iiL1vj5qB/9ObI=
 | 
			
		||||
github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=
 | 
			
		||||
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
 | 
			
		||||
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
 | 
			
		||||
github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI=
 | 
			
		||||
github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg=
 | 
			
		||||
github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ=
 | 
			
		||||
@ -919,8 +922,9 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So
 | 
			
		||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 | 
			
		||||
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
 | 
			
		||||
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
 | 
			
		||||
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
 | 
			
		||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 | 
			
		||||
@ -948,10 +952,11 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
 | 
			
		||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
 | 
			
		||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 | 
			
		||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 | 
			
		||||
github.com/skeema/knownhosts v1.1.1 h1:MTk78x9FPgDFVFkDLTrsnnfCJl7g1C/nnKvePgrIngE=
 | 
			
		||||
github.com/skeema/knownhosts v1.1.1/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo=
 | 
			
		||||
github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM=
 | 
			
		||||
github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo=
 | 
			
		||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 | 
			
		||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 | 
			
		||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
 | 
			
		||||
@ -1072,7 +1077,6 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 | 
			
		||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 | 
			
		||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 | 
			
		||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 | 
			
		||||
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
 | 
			
		||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 | 
			
		||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 | 
			
		||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 | 
			
		||||
@ -1110,9 +1114,8 @@ golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw
 | 
			
		||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 | 
			
		||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
 | 
			
		||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
 | 
			
		||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
 | 
			
		||||
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
 | 
			
		||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
 | 
			
		||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
 | 
			
		||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
 | 
			
		||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 | 
			
		||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 | 
			
		||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 | 
			
		||||
@ -1152,6 +1155,8 @@ golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
 | 
			
		||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 | 
			
		||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
 | 
			
		||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 | 
			
		||||
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
 | 
			
		||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 | 
			
		||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
@ -1217,8 +1222,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 | 
			
		||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 | 
			
		||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 | 
			
		||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 | 
			
		||||
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
 | 
			
		||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
 | 
			
		||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
 | 
			
		||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
			
		||||
@ -1252,6 +1257,7 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
 | 
			
		||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
 | 
			
		||||
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
@ -1374,7 +1380,6 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
 | 
			
		||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 | 
			
		||||
@ -1387,8 +1392,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 | 
			
		||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
 | 
			
		||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
 | 
			
		||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 | 
			
		||||
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
 | 
			
		||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
 | 
			
		||||
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
 | 
			
		||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
 | 
			
		||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
@ -1402,8 +1407,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 | 
			
		||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 | 
			
		||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 | 
			
		||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 | 
			
		||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
 | 
			
		||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 | 
			
		||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
 | 
			
		||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 | 
			
		||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
			
		||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
			
		||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
			
		||||
@ -1477,6 +1482,7 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 | 
			
		||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 | 
			
		||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 | 
			
		||||
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
 | 
			
		||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
 | 
			
		||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@ import (
 | 
			
		||||
 | 
			
		||||
	"coopcloud.tech/abra/pkg/config"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/recipe"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
)
 | 
			
		||||
@ -27,7 +28,12 @@ func AppNameComplete(c *cli.Context) {
 | 
			
		||||
 | 
			
		||||
// RecipeNameComplete completes recipe names.
 | 
			
		||||
func RecipeNameComplete(c *cli.Context) {
 | 
			
		||||
	catl, err := recipe.ReadRecipeCatalogue()
 | 
			
		||||
	// defaults since we can't take arguments here... this means auto-completion
 | 
			
		||||
	// of recipe names always access the network if e.g. the catalogue needs
 | 
			
		||||
	// cloning / updating
 | 
			
		||||
	conf := runtime.New()
 | 
			
		||||
 | 
			
		||||
	catl, err := recipe.ReadRecipeCatalogue(conf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logrus.Warn(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import (
 | 
			
		||||
 | 
			
		||||
	"coopcloud.tech/abra/pkg/config"
 | 
			
		||||
	gitPkg "coopcloud.tech/abra/pkg/git"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
	"github.com/go-git/go-git/v5"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
)
 | 
			
		||||
@ -52,9 +53,13 @@ var CatalogueSkipList = map[string]bool{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// EnsureCatalogue ensures that the catalogue is cloned locally & present.
 | 
			
		||||
func EnsureCatalogue() error {
 | 
			
		||||
func EnsureCatalogue(conf *runtime.Config) error {
 | 
			
		||||
	catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
 | 
			
		||||
	if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) {
 | 
			
		||||
		if conf.Offline {
 | 
			
		||||
			return fmt.Errorf("no local copy of the catalogue available, network access required")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME)
 | 
			
		||||
		if err := gitPkg.Clone(catalogueDir, url); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
@ -68,7 +73,7 @@ func EnsureCatalogue() error {
 | 
			
		||||
 | 
			
		||||
// EnsureUpToDate ensures that the local catalogue has no unstaged changes as
 | 
			
		||||
// is up to date. This is useful to run before doing catalogue generation.
 | 
			
		||||
func EnsureUpToDate() error {
 | 
			
		||||
func EnsureUpToDate(conf *runtime.Config) error {
 | 
			
		||||
	isClean, err := gitPkg.IsClean(config.CATALOGUE_DIR)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@ -79,6 +84,11 @@ func EnsureUpToDate() error {
 | 
			
		||||
		return fmt.Errorf(msg, config.CATALOGUE_DIR)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if conf.Offline {
 | 
			
		||||
		logrus.Debug("attempting to use local catalogue without access network (\"--offline\")")
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	repo, err := git.PlainOpen(config.CATALOGUE_DIR)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
 | 
			
		||||
@ -3,13 +3,14 @@ package client
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
 | 
			
		||||
	"github.com/docker/docker/api/types"
 | 
			
		||||
	"github.com/docker/docker/api/types/filters"
 | 
			
		||||
	"github.com/docker/docker/api/types/volume"
 | 
			
		||||
	"github.com/docker/docker/client"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GetVolumes(cl *client.Client, ctx context.Context, server string, fs filters.Args) ([]*types.Volume, error) {
 | 
			
		||||
	volumeListOKBody, err := cl.VolumeList(ctx, fs)
 | 
			
		||||
func GetVolumes(cl *client.Client, ctx context.Context, server string, fs filters.Args) ([]*volume.Volume, error) {
 | 
			
		||||
	volumeListOptions := volume.ListOptions{fs}
 | 
			
		||||
	volumeListOKBody, err := cl.VolumeList(ctx, volumeListOptions)
 | 
			
		||||
	volumeList := volumeListOKBody.Volumes
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return volumeList, err
 | 
			
		||||
@ -18,7 +19,7 @@ func GetVolumes(cl *client.Client, ctx context.Context, server string, fs filter
 | 
			
		||||
	return volumeList, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetVolumeNames(volumes []*types.Volume) []string {
 | 
			
		||||
func GetVolumeNames(volumes []*volume.Volume) []string {
 | 
			
		||||
	var volumeNames []string
 | 
			
		||||
 | 
			
		||||
	for _, vol := range volumes {
 | 
			
		||||
 | 
			
		||||
@ -8,22 +8,19 @@ import (
 | 
			
		||||
	"github.com/docker/cli/cli/context"
 | 
			
		||||
	contextStore "github.com/docker/cli/cli/context/store"
 | 
			
		||||
	cliflags "github.com/docker/cli/cli/flags"
 | 
			
		||||
	"github.com/moby/term"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func NewDefaultDockerContextStore() *command.ContextStoreWithDefault {
 | 
			
		||||
	_, _, stderr := term.StdStreams()
 | 
			
		||||
	dockerConfig := dConfig.LoadDefaultConfigFile(stderr)
 | 
			
		||||
	contextDir := dConfig.ContextStoreDir()
 | 
			
		||||
	storeConfig := command.DefaultContextStoreConfig()
 | 
			
		||||
	store := newContextStore(contextDir, storeConfig)
 | 
			
		||||
 | 
			
		||||
	opts := &cliflags.CommonOptions{Context: "default"}
 | 
			
		||||
	opts := &cliflags.ClientOptions{Context: "default"}
 | 
			
		||||
 | 
			
		||||
	dockerContextStore := &command.ContextStoreWithDefault{
 | 
			
		||||
		Store: store,
 | 
			
		||||
		Resolver: func() (*command.DefaultContext, error) {
 | 
			
		||||
			return command.ResolveDefaultContext(opts, dockerConfig, storeConfig, stderr)
 | 
			
		||||
			return command.ResolveDefaultContext(opts, storeConfig)
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -9,8 +9,11 @@ import (
 | 
			
		||||
	"coopcloud.tech/abra/pkg/config"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/recipe"
 | 
			
		||||
	recipePkg "coopcloud.tech/abra/pkg/recipe"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/runtime"
 | 
			
		||||
	"coopcloud.tech/tagcmp"
 | 
			
		||||
	"github.com/docker/distribution/reference"
 | 
			
		||||
	"github.com/go-git/go-git/v5"
 | 
			
		||||
	"github.com/go-git/go-git/v5/plumbing"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -151,6 +154,13 @@ var LintRules = map[string][]LintRule{
 | 
			
		||||
			HowToResolve: "vendor config versions in an abra.sh",
 | 
			
		||||
			Function:     LintAbraShVendors,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Ref:          "R014",
 | 
			
		||||
			Level:        "error",
 | 
			
		||||
			Description:  "only annotated tags used for recipe version",
 | 
			
		||||
			HowToResolve: "replace lightweight tag with annotated tag",
 | 
			
		||||
			Function:     LintValidTags,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -324,7 +334,11 @@ func LintImagePresent(recipe recipe.Recipe) (bool, error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func LintHasPublishedVersion(recipe recipe.Recipe) (bool, error) {
 | 
			
		||||
	catl, err := recipePkg.ReadRecipeCatalogue()
 | 
			
		||||
	// defaults since we can't take arguments here... this means this lint rule
 | 
			
		||||
	// always access the network if e.g. the catalogue needs cloning / updating
 | 
			
		||||
	conf := runtime.New()
 | 
			
		||||
 | 
			
		||||
	catl, err := recipePkg.ReadRecipeCatalogue(conf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logrus.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
@ -391,3 +405,34 @@ func LintHasRecipeRepo(recipe recipe.Recipe) (bool, error) {
 | 
			
		||||
 | 
			
		||||
	return true, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func LintValidTags(recipe recipe.Recipe) (bool, error) {
 | 
			
		||||
	recipeDir := path.Join(config.RECIPES_DIR, recipe.Name)
 | 
			
		||||
 | 
			
		||||
	repo, err := git.PlainOpen(recipeDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, fmt.Errorf("unable to open %s: %s", recipeDir, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	iter, err := repo.Tags()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logrus.Fatalf("unable to list local tags for %s", recipe.Name)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := iter.ForEach(func(ref *plumbing.Reference) error {
 | 
			
		||||
		_, err := repo.TagObject(ref.Hash())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			switch err {
 | 
			
		||||
			case plumbing.ErrObjectNotFound:
 | 
			
		||||
				return fmt.Errorf("invalid lightweight tag detected")
 | 
			
		||||
			default:
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		return false, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return true, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,6 @@ package recipe
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
@ -11,7 +10,6 @@ import (
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"coopcloud.tech/abra/pkg/catalogue"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/compose"
 | 
			
		||||
@ -238,9 +236,10 @@ func Get(recipeName string, conf *runtime.Config) (Recipe, error) {
 | 
			
		||||
 | 
			
		||||
	meta, err := GetRecipeMeta(recipeName, conf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if strings.Contains(err.Error(), "does not exist") {
 | 
			
		||||
		switch err.(type) {
 | 
			
		||||
		case RecipeMissingFromCatalogue:
 | 
			
		||||
			meta = RecipeMeta{}
 | 
			
		||||
		} else {
 | 
			
		||||
		default:
 | 
			
		||||
			return Recipe{}, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@ -254,13 +253,18 @@ func Get(recipeName string, conf *runtime.Config) (Recipe, error) {
 | 
			
		||||
 | 
			
		||||
// EnsureExists ensures that a recipe is locally cloned
 | 
			
		||||
func EnsureExists(recipeName string, conf *runtime.Config) error {
 | 
			
		||||
	if !conf.EnsureRecipeExists {
 | 
			
		||||
	if !conf.RecipeExists {
 | 
			
		||||
		logrus.Debug("skipping ensuring recipe locally exists")
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	recipeDir := path.Join(config.RECIPES_DIR, recipeName)
 | 
			
		||||
 | 
			
		||||
	if _, err := os.Stat(recipeDir); os.IsNotExist(err) {
 | 
			
		||||
		if conf.Offline {
 | 
			
		||||
			return fmt.Errorf("no local copy of %s available, network access required", recipeName)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		logrus.Debugf("%s does not exist, attemmpting to clone", recipeDir)
 | 
			
		||||
		url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, recipeName)
 | 
			
		||||
		if err := gitPkg.Clone(recipeDir, url); err != nil {
 | 
			
		||||
@ -317,7 +321,7 @@ func EnsureVersion(recipeName, version string) error {
 | 
			
		||||
	logrus.Debugf("read %s as tags for recipe %s", strings.Join(parsedTags, ", "), recipeName)
 | 
			
		||||
 | 
			
		||||
	if tagRef.String() == "" {
 | 
			
		||||
		return fmt.Errorf("no published release discovered for %s", recipeName)
 | 
			
		||||
		return fmt.Errorf("the local copy of %s doesn't seem to have version %s available?", recipeName, version)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	worktree, err := repo.Worktree()
 | 
			
		||||
@ -341,7 +345,8 @@ func EnsureVersion(recipeName, version string) error {
 | 
			
		||||
 | 
			
		||||
// EnsureLatest makes sure the latest commit is checked out for a local recipe repository
 | 
			
		||||
func EnsureLatest(recipeName string, conf *runtime.Config) error {
 | 
			
		||||
	if !conf.EnsureRecipeLatest {
 | 
			
		||||
	if !conf.RecipeLatest {
 | 
			
		||||
		logrus.Debug("skipping ensuring recipe is synced with remote")
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -374,7 +379,12 @@ func EnsureLatest(recipeName string, conf *runtime.Config) error {
 | 
			
		||||
 | 
			
		||||
	meta, err := GetRecipeMeta(recipeName, conf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
		switch err.(type) {
 | 
			
		||||
		case RecipeMissingFromCatalogue:
 | 
			
		||||
			meta = RecipeMeta{}
 | 
			
		||||
		default:
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var branch plumbing.ReferenceName
 | 
			
		||||
@ -590,7 +600,8 @@ func GetStringInBetween(recipeName, str, start, end string) (result string, err
 | 
			
		||||
 | 
			
		||||
// EnsureUpToDate ensures that the local repo is synced to the remote
 | 
			
		||||
func EnsureUpToDate(recipeName string, conf *runtime.Config) error {
 | 
			
		||||
	if !conf.EnsureRecipeLatest {
 | 
			
		||||
	if !conf.RecipeLatest {
 | 
			
		||||
		logrus.Debug("skipping ensuring recipe is synced with remote")
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -606,6 +617,11 @@ func EnsureUpToDate(recipeName string, conf *runtime.Config) error {
 | 
			
		||||
		return fmt.Errorf(msg, recipeName, recipeDir)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if conf.Offline {
 | 
			
		||||
		logrus.Debug("attempting to use local recipe without access network (\"--offline\")")
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	repo, err := git.PlainOpen(recipeDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("unable to open %s: %s", recipeDir, err)
 | 
			
		||||
@ -660,76 +676,16 @@ func EnsureUpToDate(recipeName string, conf *runtime.Config) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CatalogueOfflineError struct {
 | 
			
		||||
	msg string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *CatalogueOfflineError) Error() string {
 | 
			
		||||
	return fmt.Sprintf("catalogue offline: %s", e.msg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// recipeCatalogueFSIsLatest checks whether the recipe catalogue stored locally
 | 
			
		||||
// is up to date.
 | 
			
		||||
func recipeCatalogueFSIsLatest() (bool, error) {
 | 
			
		||||
	httpClient := web.NewHTTPRetryClient()
 | 
			
		||||
	res, err := httpClient.Head(RecipeCatalogueURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, &CatalogueOfflineError{err.Error()}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lastModified := res.Header["Last-Modified"][0]
 | 
			
		||||
	parsed, err := time.Parse(time.RFC1123, lastModified)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	info, err := os.Stat(config.RECIPES_JSON)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if os.IsNotExist(err) {
 | 
			
		||||
			logrus.Debugf("no recipe catalogue found in file system cache")
 | 
			
		||||
			return false, nil
 | 
			
		||||
		}
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	localModifiedTime := info.ModTime().Unix()
 | 
			
		||||
	remoteModifiedTime := parsed.Unix()
 | 
			
		||||
 | 
			
		||||
	if localModifiedTime < remoteModifiedTime {
 | 
			
		||||
		logrus.Debug("file system cached recipe catalogue is out-of-date")
 | 
			
		||||
		return false, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logrus.Debug("file system cached recipe catalogue is up-to-date")
 | 
			
		||||
 | 
			
		||||
	return true, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ReadRecipeCatalogue reads the recipe catalogue.
 | 
			
		||||
func ReadRecipeCatalogue() (RecipeCatalogue, error) {
 | 
			
		||||
func ReadRecipeCatalogue(conf *runtime.Config) (RecipeCatalogue, error) {
 | 
			
		||||
	recipes := make(RecipeCatalogue)
 | 
			
		||||
 | 
			
		||||
	if err := catalogue.EnsureCatalogue(); err != nil {
 | 
			
		||||
	if err := catalogue.EnsureCatalogue(conf); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	recipeFSIsLatest, err := recipeCatalogueFSIsLatest()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		var offlineErr *CatalogueOfflineError
 | 
			
		||||
		if errors.As(err, &offlineErr) {
 | 
			
		||||
			logrus.Error(err)
 | 
			
		||||
			logrus.Error("unable to retrieve catalogue from internet, using local copy.")
 | 
			
		||||
			recipeFSIsLatest = true
 | 
			
		||||
		} else {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !recipeFSIsLatest {
 | 
			
		||||
		if err := readRecipeCatalogueWeb(&recipes); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		return recipes, nil
 | 
			
		||||
	if err := catalogue.EnsureUpToDate(conf); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := readRecipeCatalogueFS(&recipes); err != nil {
 | 
			
		||||
@ -755,31 +711,11 @@ func readRecipeCatalogueFS(target interface{}) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// readRecipeCatalogueWeb reads the catalogue from the web.
 | 
			
		||||
func readRecipeCatalogueWeb(target interface{}) error {
 | 
			
		||||
	if err := web.ReadJSON(RecipeCatalogueURL, &target); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	recipesJSON, err := json.MarshalIndent(target, "", "    ")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := ioutil.WriteFile(config.RECIPES_JSON, recipesJSON, 0764); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logrus.Debugf("read recipe catalogue from web at %s", RecipeCatalogueURL)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// VersionsOfService lists the version of a service.
 | 
			
		||||
func VersionsOfService(recipe, serviceName string) ([]string, error) {
 | 
			
		||||
func VersionsOfService(recipe, serviceName string, conf *runtime.Config) ([]string, error) {
 | 
			
		||||
	var versions []string
 | 
			
		||||
 | 
			
		||||
	catalogue, err := ReadRecipeCatalogue()
 | 
			
		||||
	catalogue, err := ReadRecipeCatalogue(conf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@ -804,16 +740,26 @@ func VersionsOfService(recipe, serviceName string) ([]string, error) {
 | 
			
		||||
	return versions, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RecipeMissingFromCatalogue signifies a recipe is not present in the catalogue.
 | 
			
		||||
type RecipeMissingFromCatalogue struct{ err string }
 | 
			
		||||
 | 
			
		||||
// Error outputs the error message.
 | 
			
		||||
func (r RecipeMissingFromCatalogue) Error() string {
 | 
			
		||||
	return r.err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetRecipeMeta retrieves the recipe metadata from the recipe catalogue.
 | 
			
		||||
func GetRecipeMeta(recipeName string, conf *runtime.Config) (RecipeMeta, error) {
 | 
			
		||||
	catl, err := ReadRecipeCatalogue()
 | 
			
		||||
	catl, err := ReadRecipeCatalogue(conf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return RecipeMeta{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	recipeMeta, ok := catl[recipeName]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return RecipeMeta{}, fmt.Errorf("recipe %s does not exist?", recipeName)
 | 
			
		||||
		return RecipeMeta{}, RecipeMissingFromCatalogue{
 | 
			
		||||
			err: fmt.Sprintf("recipe %s does not exist?", recipeName),
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := EnsureExists(recipeName, conf); err != nil {
 | 
			
		||||
@ -903,7 +849,11 @@ type InternalTracker struct {
 | 
			
		||||
type RepoCatalogue map[string]RepoMeta
 | 
			
		||||
 | 
			
		||||
// ReadReposMetadata retrieves coop-cloud/... repo metadata from Gitea.
 | 
			
		||||
func ReadReposMetadata() (RepoCatalogue, error) {
 | 
			
		||||
func ReadReposMetadata(conf *runtime.Config) (RepoCatalogue, error) {
 | 
			
		||||
	if conf.Offline {
 | 
			
		||||
		return nil, fmt.Errorf("network access required to query recipes metadata")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	reposMeta := make(RepoCatalogue)
 | 
			
		||||
 | 
			
		||||
	pageIdx := 1
 | 
			
		||||
@ -1076,12 +1026,18 @@ func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]stri
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sortVersionStrings(versions)
 | 
			
		||||
 | 
			
		||||
	return versions, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateRepositories clones and updates all recipe repositories locally.
 | 
			
		||||
func UpdateRepositories(repos RepoCatalogue, recipeName string, conf *runtime.Config) error {
 | 
			
		||||
	if conf.Offline {
 | 
			
		||||
		return fmt.Errorf("network access required to update recipes")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var barLength int
 | 
			
		||||
	if recipeName != "" {
 | 
			
		||||
		barLength = 1
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										59
									
								
								pkg/runtime/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								pkg/runtime/config.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,59 @@
 | 
			
		||||
package runtime
 | 
			
		||||
 | 
			
		||||
import "github.com/sirupsen/logrus"
 | 
			
		||||
 | 
			
		||||
// Config is a runtime behaviour modifier.
 | 
			
		||||
type Config struct {
 | 
			
		||||
	Offline      bool // Whether or not Abra should prefer local / offline access
 | 
			
		||||
	RecipeExists bool // Whether or not Abra should ensure the recipe is locally cloned
 | 
			
		||||
	RecipeLatest bool // Whether or not Abra should ensure the recipe has the latest commit
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Option is a runtime configuration option.
 | 
			
		||||
type Option func(c *Config)
 | 
			
		||||
 | 
			
		||||
// New creates a new runtime configuration.
 | 
			
		||||
func New(opts ...Option) *Config {
 | 
			
		||||
	conf := &Config{
 | 
			
		||||
		Offline:      false,
 | 
			
		||||
		RecipeExists: true,
 | 
			
		||||
		RecipeLatest: true,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, optFunc := range opts {
 | 
			
		||||
		optFunc(conf)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return conf
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WithOffline ensures Abra attempts to prefer local file system and offline
 | 
			
		||||
// access.
 | 
			
		||||
func WithOffline(offline bool) Option {
 | 
			
		||||
	return func(c *Config) {
 | 
			
		||||
		if offline {
 | 
			
		||||
			logrus.Debugf("runtime config: attempting to run in offline mode")
 | 
			
		||||
		}
 | 
			
		||||
		c.Offline = offline
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WithEnsureRecipeExists ensures recipe exists locally.
 | 
			
		||||
func WithEnsureRecipeExists(exists bool) Option {
 | 
			
		||||
	return func(c *Config) {
 | 
			
		||||
		if exists {
 | 
			
		||||
			logrus.Debugf("runtime config: ensuring recipe exists")
 | 
			
		||||
		}
 | 
			
		||||
		c.RecipeExists = exists
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WithEnsureRecipeUpToDate ensures recipe is synced with the remote.
 | 
			
		||||
func WithEnsureRecipeUpToDate(upToDate bool) Option {
 | 
			
		||||
	return func(c *Config) {
 | 
			
		||||
		if upToDate {
 | 
			
		||||
			logrus.Debugf("runtime config: ensuring recipe is synced with remote")
 | 
			
		||||
		}
 | 
			
		||||
		c.RecipeLatest = upToDate
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,61 +0,0 @@
 | 
			
		||||
package runtime
 | 
			
		||||
 | 
			
		||||
import "github.com/sirupsen/logrus"
 | 
			
		||||
 | 
			
		||||
// Config is an internal configuration modifier. It can be instantiated on a
 | 
			
		||||
// command call and can be changed on the fly to help make decisions further
 | 
			
		||||
// down in the internals, e.g. whether or not to clone the recipe locally or
 | 
			
		||||
// not.
 | 
			
		||||
type Config struct {
 | 
			
		||||
	EnsureRecipeExists bool // ensure that the recipe is cloned locally
 | 
			
		||||
	EnsureRecipeLatest bool // ensure the local recipe has latest changes
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Option modifies a Config. The convention for passing an Option to a function
 | 
			
		||||
// is so far, only used in internal/validate.go. A Config is then constructed
 | 
			
		||||
// and passed down further into the code. This may change in the future but
 | 
			
		||||
// this is at least the abstraction so far.
 | 
			
		||||
type Option func(c *Config)
 | 
			
		||||
 | 
			
		||||
// New instantiates a Config.
 | 
			
		||||
func New(opts ...Option) *Config {
 | 
			
		||||
	conf := &Config{
 | 
			
		||||
		EnsureRecipeExists: true,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, optFunc := range opts {
 | 
			
		||||
		optFunc(conf)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return conf
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WithEnsureRecipeExists determines whether or not we should be cloning the
 | 
			
		||||
// local recipe or not. This can be useful for being more adaptable to offline
 | 
			
		||||
// scenarios.
 | 
			
		||||
func WithEnsureRecipeExists(ensureRecipeExists bool) Option {
 | 
			
		||||
	return func(c *Config) {
 | 
			
		||||
		if ensureRecipeExists {
 | 
			
		||||
			logrus.Debugf("runtime config: EnsureRecipeExists = %v, ensuring recipes are cloned", ensureRecipeExists)
 | 
			
		||||
		} else {
 | 
			
		||||
			logrus.Debugf("runtime config: EnsureRecipeExists = %v, not cloning recipes", ensureRecipeExists)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.EnsureRecipeExists = ensureRecipeExists
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WithEnsureRecipeLatest determines whether we should update the local recipes
 | 
			
		||||
// remotely via Git. This can be useful when e.g. ensuring we have the latest
 | 
			
		||||
// changes before making new ones.
 | 
			
		||||
func WithEnsureRecipeLatest(ensureRecipeLatest bool) Option {
 | 
			
		||||
	return func(c *Config) {
 | 
			
		||||
		if ensureRecipeLatest {
 | 
			
		||||
			logrus.Debugf("runtime config: EnsureRecipeLatest = %v, ensuring recipes have latest changes", ensureRecipeLatest)
 | 
			
		||||
		} else {
 | 
			
		||||
			logrus.Debugf("runtime config: EnsureRecipeLatest = %v, leaving recipes alone", ensureRecipeLatest)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.EnsureRecipeLatest = ensureRecipeLatest
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -5,7 +5,8 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"coopcloud.tech/abra/pkg/upstream/convert"
 | 
			
		||||
@ -414,7 +415,7 @@ func deployServices(
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logrus.Infof("waiting for services to converge: %s", strings.Join(serviceNames, ", "))
 | 
			
		||||
	logrus.Infof("Starting to poll for deployment status for: %s", appName)
 | 
			
		||||
	ch := make(chan error, len(serviceIDs))
 | 
			
		||||
	for serviceID, serviceName := range serviceIDs {
 | 
			
		||||
		logrus.Debugf("waiting on %s to converge", serviceName)
 | 
			
		||||
@ -431,7 +432,7 @@ func deployServices(
 | 
			
		||||
		logrus.Debugf("assuming %s converged successfully", serviceID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logrus.Info("services converged 👌")
 | 
			
		||||
	logrus.Infof("Successfully deployed %s", appName)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@ -454,6 +455,10 @@ func WaitOnService(ctx context.Context, cl *dockerClient.Client, serviceID, appN
 | 
			
		||||
	errChan := make(chan error, 1)
 | 
			
		||||
	pipeReader, pipeWriter := io.Pipe()
 | 
			
		||||
 | 
			
		||||
	sigintChannel := make(chan os.Signal, 1)
 | 
			
		||||
	signal.Notify(sigintChannel, os.Interrupt)
 | 
			
		||||
	defer signal.Stop(sigintChannel)
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
		errChan <- progress.ServiceProgress(ctx, cl, serviceID, pipeWriter)
 | 
			
		||||
	}()
 | 
			
		||||
@ -465,6 +470,14 @@ func WaitOnService(ctx context.Context, cl *dockerClient.Client, serviceID, appN
 | 
			
		||||
	select {
 | 
			
		||||
	case err := <-errChan:
 | 
			
		||||
		return err
 | 
			
		||||
	case <-sigintChannel:
 | 
			
		||||
		return fmt.Errorf(fmt.Sprintf(`
 | 
			
		||||
Cancelling polling for %s, deployment is still continuing.  
 | 
			
		||||
 | 
			
		||||
If you want to stop the deployment try:
 | 
			
		||||
	abra app undeploy %s
 | 
			
		||||
 | 
			
		||||
`, appName, appName))
 | 
			
		||||
	case <-time.After(timeout):
 | 
			
		||||
		return fmt.Errorf(fmt.Sprintf(`
 | 
			
		||||
%s has not converged (%s second timeout reached).
 | 
			
		||||
@ -481,7 +494,7 @@ And inspect the logs with:
 | 
			
		||||
 | 
			
		||||
    abra app logs %s
 | 
			
		||||
 | 
			
		||||
If a service is failing to even start, try smoke out the error with:
 | 
			
		||||
If a service is failing to even start, try to smoke out the error with:
 | 
			
		||||
 | 
			
		||||
    abra app errors --watch %s
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@
 | 
			
		||||
    "gomodTidy"
 | 
			
		||||
  ],
 | 
			
		||||
  "ignoreDeps": [
 | 
			
		||||
    "github.com/urfave/cli"
 | 
			
		||||
    "github.com/urfave/cli",
 | 
			
		||||
    "goreleaser/goreleaser"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
ABRA_VERSION="0.7.0-beta"
 | 
			
		||||
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION"
 | 
			
		||||
RC_VERSION="0.7.0-beta"
 | 
			
		||||
RC_VERSION="0.8.0-rc2-beta"
 | 
			
		||||
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION"
 | 
			
		||||
 | 
			
		||||
for arg in "$@"; do
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user