forked from toolshed/abra
		
	Compare commits
	
		
			107 Commits
		
	
	
		
			0.1.3-alph
			...
			0.2.0-alph
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 75fb9a2774 | |||
| 0d500b636d | |||
| 5dd97cace0 | |||
| ae32b1eed2 | |||
| 113bdf9e86 | |||
| d4d4da19b7 | |||
| 454ee696d6 | |||
| ca16c002ba | |||
| 91cc8b00b3 | |||
| d0828c4d8d | |||
| b69aed3bcf | |||
| 875255fd8c | |||
| 2dca602c0b | |||
| 1dca8a1067 | |||
| 37022bf0c8 | |||
| 
						
						
							
						
						eb5b35d47f
	
				 | 
					
					
						|||
| 
						
						
							
						
						ece1130797
	
				 | 
					
					
						|||
| 
						
						
							
						
						c266316f7e
	
				 | 
					
					
						|||
| d804276cf2 | |||
| 4235e06943 | |||
| a9af0b3627 | |||
| a0b4886eba | |||
| 84489495dc | |||
| a8683dc38a | |||
| e2128ea5b6 | |||
| ca3c5fef0f | |||
| 4a01e411be | |||
| 777d49ac1d | |||
| deb7d21158 | |||
| 6db1fdcfba | |||
| 44dc0edf7b | |||
| 
						
						
							
						
						36ff50312c
	
				 | 
					
					
						|||
| ff4b978876 | |||
| b68547b2c2 | |||
| 0140f96ca1 | |||
| 1cb45113db | |||
| c764243f3a | |||
| dde8afcd43 | |||
| 98ffc210e1 | |||
| 7c0d883135 | |||
| e78ced41fb | |||
| e9113500d8 | |||
| 7368cabc49 | |||
| f75e264811 | |||
| 8bfd76fd04 | |||
| 
						
						
							
						
						1cb5e3509d
	
				 | 
					
					
						|||
| 3cd2399cca | |||
| 11c4651a3b | |||
| 49f90674f2 | |||
| 74a70edb03 | |||
| 6fc5c31347 | |||
| c616907b71 | |||
| a58cea3e0a | |||
| 700f89425a | |||
| 8cc0a350e6 | |||
| 46e67fa420 | |||
| cacbb5a0f1 | |||
| e7046a15aa | |||
| c1fd97c427 | |||
| 2f218bd99f | |||
| 48290aa316 | |||
| db5cbfa992 | |||
| 4c11e813e8 | |||
| 
						
						
							
						
						6ae75e013a
	
				 | 
					
					
						|||
| 09f49cdc76 | |||
| 22118b88e4 | |||
| e6db064149 | |||
| 3688ea9d69 | |||
| 7c4cdc530c | |||
| 49781c7e3f | |||
| 10b15d65b4 | |||
| 1c5d6d6357 | |||
| 75bdd59585 | |||
| 
						
						
							
						
						96bb145981
	
				 | 
					
					
						|||
| 
						
						
							
						
						c4c76f4848
	
				 | 
					
					
						|||
| 2076c566bb | |||
| 62f6327b66 | |||
| 6f9120b59c | |||
| 8c617a9f12 | |||
| 
						
						
							
						
						857d12d23c
	
				 | 
					
					
						|||
| 
						
						
							
						
						22c4d0d864
	
				 | 
					
					
						|||
| 
						
						
							
						
						e700e44363
	
				 | 
					
					
						|||
| 
						
						
							
						
						9faefd2592
	
				 | 
					
					
						|||
| 
						
						
							
						
						cd179175f5
	
				 | 
					
					
						|||
| 
						
						
							
						
						c0f92ca13d
	
				 | 
					
					
						|||
| 
						
						
							
						
						48d28c8dd1
	
				 | 
					
					
						|||
| e840328e44 | |||
| 6f43778691 | |||
| 9783563fa6 | |||
| 1392afc015 | |||
| 886009975d | |||
| b1147cd136 | |||
| 95a9013658 | |||
| bd1bf3b0d6 | |||
| 7b349732ac | |||
| a8ce64a9db | |||
| 96aa74a977 | |||
| 700f022790 | |||
| d188327b17 | |||
| fdd46a4d98 | |||
| e00920643e | |||
| 754fe81e01 | |||
| bece2e8351 | |||
| 
						
						
							
						
						e47d7029d7
	
				 | 
					
					
						|||
| 
						
						
							
						
						31edbbd32e
	
				 | 
					
					
						|||
| 
						
						
							
						
						0a1c73bf00
	
				 | 
					
					
						|||
| a74a8bc21b | 
@ -14,12 +14,12 @@ builds:
 | 
				
			|||||||
    dir: cmd/abra
 | 
					    dir: cmd/abra
 | 
				
			||||||
    goos:
 | 
					    goos:
 | 
				
			||||||
      - linux
 | 
					      - linux
 | 
				
			||||||
 | 
					      - darwin
 | 
				
			||||||
    ldflags:
 | 
					    ldflags:
 | 
				
			||||||
      - "-X 'main.Commit={{ .Commit }}'"
 | 
					      - "-X 'main.Commit={{ .Commit }}'"
 | 
				
			||||||
      - "-X 'main.Version={{ .Version }}'"
 | 
					      - "-X 'main.Version={{ .Version }}'"
 | 
				
			||||||
archives:
 | 
					archives:
 | 
				
			||||||
  - replacements:
 | 
					  - replacements:
 | 
				
			||||||
      linux: Linux
 | 
					 | 
				
			||||||
      386: i386
 | 
					      386: i386
 | 
				
			||||||
      amd64: x86_64
 | 
					      amd64: x86_64
 | 
				
			||||||
    format: binary
 | 
					    format: binary
 | 
				
			||||||
@ -28,8 +28,13 @@ checksum:
 | 
				
			|||||||
snapshot:
 | 
					snapshot:
 | 
				
			||||||
  name_template: "{{ incpatch .Version }}-next"
 | 
					  name_template: "{{ incpatch .Version }}-next"
 | 
				
			||||||
changelog:
 | 
					changelog:
 | 
				
			||||||
  sort: asc
 | 
					  sort: desc
 | 
				
			||||||
  filters:
 | 
					  filters:
 | 
				
			||||||
    exclude:
 | 
					    exclude:
 | 
				
			||||||
 | 
					      - "^WIP:"
 | 
				
			||||||
 | 
					      - "^chore:"
 | 
				
			||||||
      - "^docs:"
 | 
					      - "^docs:"
 | 
				
			||||||
 | 
					      - "^refactor:"
 | 
				
			||||||
 | 
					      - "^style:"
 | 
				
			||||||
      - "^test:"
 | 
					      - "^test:"
 | 
				
			||||||
 | 
					      - "^tests:"
 | 
				
			||||||
 | 
				
			|||||||
@ -89,6 +89,7 @@ For developers, while using this `-alpha` format, the `y` part is the "major" ve
 | 
				
			|||||||
- Make a new tag (e.g. `git tag 0.y.z-alpha`)
 | 
					- Make a new tag (e.g. `git tag 0.y.z-alpha`)
 | 
				
			||||||
- Push the new tag (e.g. `git push && git push --tags`)
 | 
					- Push the new tag (e.g. `git push && git push --tags`)
 | 
				
			||||||
- Wait until the build finishes on [build.coopcloud.tech](https://build.coopcloud.tech/coop-cloud/abra)
 | 
					- Wait until the build finishes on [build.coopcloud.tech](https://build.coopcloud.tech/coop-cloud/abra)
 | 
				
			||||||
 | 
					- Deploy the new installer script (e.g. `cd ./scripts/installer && make`)
 | 
				
			||||||
- Check the release worked, (e.g. `abra upgrade; abra version`)
 | 
					- Check the release worked, (e.g. `abra upgrade; abra version`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Fork maintenance
 | 
					## Fork maintenance
 | 
				
			||||||
 | 
				
			|||||||
@ -19,6 +19,7 @@ to scaling apps up and spinning them down.
 | 
				
			|||||||
		appNewCommand,
 | 
							appNewCommand,
 | 
				
			||||||
		appConfigCommand,
 | 
							appConfigCommand,
 | 
				
			||||||
		appDeployCommand,
 | 
							appDeployCommand,
 | 
				
			||||||
 | 
							appUpgradeCommand,
 | 
				
			||||||
		appUndeployCommand,
 | 
							appUndeployCommand,
 | 
				
			||||||
		appBackupCommand,
 | 
							appBackupCommand,
 | 
				
			||||||
		appRestoreCommand,
 | 
							appRestoreCommand,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
package app
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
@ -75,7 +74,6 @@ var appCpCommand = &cli.Command{
 | 
				
			|||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx := context.Background()
 | 
					 | 
				
			||||||
		cl, err := client.New(app.Server)
 | 
							cl, err := client.New(app.Server)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
@ -83,7 +81,7 @@ var appCpCommand = &cli.Command{
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		filters := filters.NewArgs()
 | 
							filters := filters.NewArgs()
 | 
				
			||||||
		filters.Add("name", fmt.Sprintf("%s_%s", appEnv.StackName(), service))
 | 
							filters.Add("name", fmt.Sprintf("%s_%s", appEnv.StackName(), service))
 | 
				
			||||||
		containers, err := cl.ContainerList(ctx, types.ContainerListOptions{Filters: filters})
 | 
							containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -107,11 +105,11 @@ var appCpCommand = &cli.Command{
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
 | 
								copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
 | 
				
			||||||
			if err := cl.CopyToContainer(ctx, container.ID, dstPath, content, copyOpts); err != nil {
 | 
								if err := cl.CopyToContainer(c.Context, container.ID, dstPath, content, copyOpts); err != nil {
 | 
				
			||||||
				logrus.Fatal(err)
 | 
									logrus.Fatal(err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			content, _, err := cl.CopyFromContainer(ctx, container.ID, srcPath)
 | 
								content, _, err := cl.CopyFromContainer(c.Context, container.ID, srcPath)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				logrus.Fatal(err)
 | 
									logrus.Fatal(err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
				
			|||||||
@ -2,11 +2,16 @@ package app
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						abraFormatter "coopcloud.tech/abra/cli/formatter"
 | 
				
			||||||
	"coopcloud.tech/abra/cli/internal"
 | 
						"coopcloud.tech/abra/cli/internal"
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/pkg/catalogue"
 | 
				
			||||||
	"coopcloud.tech/abra/pkg/client"
 | 
						"coopcloud.tech/abra/pkg/client"
 | 
				
			||||||
	stack "coopcloud.tech/abra/pkg/client/stack"
 | 
						stack "coopcloud.tech/abra/pkg/client/stack"
 | 
				
			||||||
	"coopcloud.tech/abra/pkg/config"
 | 
						"coopcloud.tech/abra/pkg/config"
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/pkg/recipe"
 | 
				
			||||||
 | 
						"github.com/AlecAivazis/survey/v2"
 | 
				
			||||||
	"github.com/sirupsen/logrus"
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
	"github.com/urfave/cli/v2"
 | 
						"github.com/urfave/cli/v2"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@ -15,14 +20,63 @@ var appDeployCommand = &cli.Command{
 | 
				
			|||||||
	Name:    "deploy",
 | 
						Name:    "deploy",
 | 
				
			||||||
	Aliases: []string{"d"},
 | 
						Aliases: []string{"d"},
 | 
				
			||||||
	Usage:   "Deploy an app",
 | 
						Usage:   "Deploy an app",
 | 
				
			||||||
 | 
						Flags: []cli.Flag{
 | 
				
			||||||
 | 
							internal.ForceFlag,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						Description: `
 | 
				
			||||||
 | 
					This command deploys a new instance of an app. It does not support changing the
 | 
				
			||||||
 | 
					version of an existing deployed app, for this you need to look at the "abra app
 | 
				
			||||||
 | 
					upgrade <app>" command.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may pass "--force" to re-deploy the same version again. This can be useful
 | 
				
			||||||
 | 
					if the container runtime has gotten into a weird state or your doing some live
 | 
				
			||||||
 | 
					hacking.
 | 
				
			||||||
 | 
					`,
 | 
				
			||||||
	Action: func(c *cli.Context) error {
 | 
						Action: func(c *cli.Context) error {
 | 
				
			||||||
		app := internal.ValidateApp(c)
 | 
							app := internal.ValidateApp(c)
 | 
				
			||||||
 | 
							stackName := app.StackName()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		cl, err := client.New(app.Server)
 | 
							cl, err := client.New(app.Server)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							logrus.Debugf("checking whether '%s' is already deployed", stackName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logrus.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if isDeployed {
 | 
				
			||||||
 | 
								if internal.Force {
 | 
				
			||||||
 | 
									logrus.Infof("continuing with deployment due to '--force/-f' being set")
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									logrus.Fatalf("'%s' is already deployed", stackName)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							version := deployedVersion
 | 
				
			||||||
 | 
							if version == "" {
 | 
				
			||||||
 | 
								versions, err := catalogue.GetRecipeCatalogueVersions(app.Type)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									logrus.Fatal(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if len(versions) > 0 {
 | 
				
			||||||
 | 
									version = versions[len(versions)-1]
 | 
				
			||||||
 | 
									logrus.Infof("choosing '%s' as version to deploy", version)
 | 
				
			||||||
 | 
									if err := recipe.EnsureVersion(app.Type, version); err != nil {
 | 
				
			||||||
 | 
										logrus.Fatal(err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									version = "latest commit"
 | 
				
			||||||
 | 
									logrus.Warning("no versions detected, using latest commit")
 | 
				
			||||||
 | 
									if err := recipe.EnsureLatest(app.Type); err != nil {
 | 
				
			||||||
 | 
										logrus.Fatal(err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh")
 | 
							abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh")
 | 
				
			||||||
		abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
 | 
							abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
@ -31,7 +85,6 @@ var appDeployCommand = &cli.Command{
 | 
				
			|||||||
		for k, v := range abraShEnv {
 | 
							for k, v := range abraShEnv {
 | 
				
			||||||
			app.Env[k] = v
 | 
								app.Env[k] = v
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		app.Env["STACK_NAME"] = app.StackName()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
 | 
							composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
@ -39,7 +92,7 @@ var appDeployCommand = &cli.Command{
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		deployOpts := stack.Deploy{
 | 
							deployOpts := stack.Deploy{
 | 
				
			||||||
			Composefiles: composeFiles,
 | 
								Composefiles: composeFiles,
 | 
				
			||||||
			Namespace:    app.StackName(),
 | 
								Namespace:    stackName,
 | 
				
			||||||
			Prune:        false,
 | 
								Prune:        false,
 | 
				
			||||||
			ResolveImage: stack.ResolveImageAlways,
 | 
								ResolveImage: stack.ResolveImageAlways,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -48,6 +101,10 @@ var appDeployCommand = &cli.Command{
 | 
				
			|||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := DeployOverview(app, version); err != nil {
 | 
				
			||||||
 | 
								logrus.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
 | 
							if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -67,3 +124,61 @@ var appDeployCommand = &cli.Command{
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeployOverview shows a deployment overview
 | 
				
			||||||
 | 
					func DeployOverview(app config.App, version string) error {
 | 
				
			||||||
 | 
						tableCol := []string{"server", "compose", "domain", "stack", "version"}
 | 
				
			||||||
 | 
						table := abraFormatter.CreateTable(tableCol)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						deployConfig := "compose.yml"
 | 
				
			||||||
 | 
						if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
 | 
				
			||||||
 | 
							deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						table.Append([]string{app.Server, deployConfig, app.Domain, app.StackName(), version})
 | 
				
			||||||
 | 
						table.Render()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewVersionOverview shows an upgrade or downgrade overview
 | 
				
			||||||
 | 
					func NewVersionOverview(app config.App, currentVersion, newVersion string) error {
 | 
				
			||||||
 | 
						tableCol := []string{"server", "compose", "domain", "stack", "current version", "to be deployed"}
 | 
				
			||||||
 | 
						table := abraFormatter.CreateTable(tableCol)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						deployConfig := "compose.yml"
 | 
				
			||||||
 | 
						if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
 | 
				
			||||||
 | 
							deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						table.Append([]string{app.Server, deployConfig, app.Domain, app.StackName(), currentVersion, newVersion})
 | 
				
			||||||
 | 
						table.Render()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,14 @@
 | 
				
			|||||||
package app
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"sort"
 | 
						"sort"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	abraFormatter "coopcloud.tech/abra/cli/formatter"
 | 
						abraFormatter "coopcloud.tech/abra/cli/formatter"
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/pkg/catalogue"
 | 
				
			||||||
	"coopcloud.tech/abra/pkg/config"
 | 
						"coopcloud.tech/abra/pkg/config"
 | 
				
			||||||
 | 
						"coopcloud.tech/tagcmp"
 | 
				
			||||||
	"github.com/sirupsen/logrus"
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
	"github.com/urfave/cli/v2"
 | 
						"github.com/urfave/cli/v2"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@ -65,10 +69,10 @@ can take some time.
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		sort.Sort(config.ByServerAndType(apps))
 | 
							sort.Sort(config.ByServerAndType(apps))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		statuses := map[string]string{}
 | 
							statuses := make(map[string]map[string]string)
 | 
				
			||||||
		tableCol := []string{"Server", "Type", "Domain"}
 | 
							tableCol := []string{"Server", "Type", "Domain"}
 | 
				
			||||||
		if status {
 | 
							if status {
 | 
				
			||||||
			tableCol = append(tableCol, "Status")
 | 
								tableCol = append(tableCol, "Status", "Version", "Updates")
 | 
				
			||||||
			statuses, err = config.GetAppStatuses(appFiles)
 | 
								statuses, err = config.GetAppStatuses(appFiles)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				logrus.Fatal(err)
 | 
									logrus.Fatal(err)
 | 
				
			||||||
@ -78,22 +82,90 @@ can take some time.
 | 
				
			|||||||
		table := abraFormatter.CreateTable(tableCol)
 | 
							table := abraFormatter.CreateTable(tableCol)
 | 
				
			||||||
		table.SetAutoMergeCellsByColumnIndex([]int{0})
 | 
							table.SetAutoMergeCellsByColumnIndex([]int{0})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var (
 | 
				
			||||||
 | 
								versionedAppsCount   int
 | 
				
			||||||
 | 
								unversionedAppsCount int
 | 
				
			||||||
 | 
								onLatestCount        int
 | 
				
			||||||
 | 
								canUpgradeCount      int
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		for _, app := range apps {
 | 
							for _, app := range apps {
 | 
				
			||||||
			var tableRow []string
 | 
								var tableRow []string
 | 
				
			||||||
			if app.Type == appType || appType == "" {
 | 
								if app.Type == appType || appType == "" {
 | 
				
			||||||
				// If type flag is set, check for it, if not, Type == ""
 | 
									// If type flag is set, check for it, if not, Type == ""
 | 
				
			||||||
				tableRow = []string{app.Server, app.Type, app.Domain}
 | 
									tableRow = []string{app.Server, app.Type, app.Domain}
 | 
				
			||||||
				if status {
 | 
									if status {
 | 
				
			||||||
					if status, ok := statuses[app.StackName()]; ok {
 | 
										stackName := app.StackName()
 | 
				
			||||||
						tableRow = append(tableRow, status)
 | 
										status := "unknown"
 | 
				
			||||||
 | 
										version := "unknown"
 | 
				
			||||||
 | 
										if statusMeta, ok := statuses[stackName]; ok {
 | 
				
			||||||
 | 
											if currentVersion, exists := statusMeta["version"]; exists {
 | 
				
			||||||
 | 
												version = currentVersion
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											if statusMeta["status"] != "" {
 | 
				
			||||||
 | 
												status = statusMeta["status"]
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											tableRow = append(tableRow, status, version)
 | 
				
			||||||
 | 
											versionedAppsCount++
 | 
				
			||||||
					} else {
 | 
										} else {
 | 
				
			||||||
						tableRow = append(tableRow, "unknown")
 | 
											tableRow = append(tableRow, status, version)
 | 
				
			||||||
 | 
											unversionedAppsCount++
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										var newUpdates []string
 | 
				
			||||||
 | 
										if version != "unknown" {
 | 
				
			||||||
 | 
											updates, err := catalogue.GetRecipeCatalogueVersions(app.Type)
 | 
				
			||||||
 | 
											if err != nil {
 | 
				
			||||||
 | 
												logrus.Fatal(err)
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											parsedVersion, err := tagcmp.Parse(version)
 | 
				
			||||||
 | 
											if err != nil {
 | 
				
			||||||
 | 
												logrus.Fatal(err)
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											for _, update := range updates {
 | 
				
			||||||
 | 
												parsedUpdate, err := tagcmp.Parse(update)
 | 
				
			||||||
 | 
												if err != nil {
 | 
				
			||||||
 | 
													logrus.Fatal(err)
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
												if update != version && parsedUpdate.IsGreaterThan(parsedVersion) {
 | 
				
			||||||
 | 
													newUpdates = append(newUpdates, update)
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										if len(newUpdates) == 0 {
 | 
				
			||||||
 | 
											if version == "unknown" {
 | 
				
			||||||
 | 
												tableRow = append(tableRow, "unknown")
 | 
				
			||||||
 | 
											} else {
 | 
				
			||||||
 | 
												tableRow = append(tableRow, "on latest")
 | 
				
			||||||
 | 
												onLatestCount++
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										} else {
 | 
				
			||||||
 | 
											// FIXME: jeezus golang why do you not have a list reverse function
 | 
				
			||||||
 | 
											for i, j := 0, len(newUpdates)-1; i < j; i, j = i+1, j-1 {
 | 
				
			||||||
 | 
												newUpdates[i], newUpdates[j] = newUpdates[j], newUpdates[i]
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											tableRow = append(tableRow, strings.Join(newUpdates, "\n"))
 | 
				
			||||||
 | 
											canUpgradeCount++
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			table.Append(tableRow)
 | 
								table.Append(tableRow)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							stats := fmt.Sprintf(
 | 
				
			||||||
 | 
								"Total apps: %v | Versioned: %v | Unversioned: %v | On latest: %v | Can upgrade: %v",
 | 
				
			||||||
 | 
								len(apps),
 | 
				
			||||||
 | 
								versionedAppsCount,
 | 
				
			||||||
 | 
								unversionedAppsCount,
 | 
				
			||||||
 | 
								onLatestCount,
 | 
				
			||||||
 | 
								canUpgradeCount,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							table.SetCaption(true, stats)
 | 
				
			||||||
		table.Render()
 | 
							table.Render()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
package app
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
@ -18,12 +17,11 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// stackLogs lists logs for all stack services
 | 
					// stackLogs lists logs for all stack services
 | 
				
			||||||
func stackLogs(stackName string, client *dockerClient.Client) {
 | 
					func stackLogs(c *cli.Context, stackName string, client *dockerClient.Client) {
 | 
				
			||||||
	ctx := context.Background()
 | 
					 | 
				
			||||||
	filters := filters.NewArgs()
 | 
						filters := filters.NewArgs()
 | 
				
			||||||
	filters.Add("name", stackName)
 | 
						filters.Add("name", stackName)
 | 
				
			||||||
	serviceOpts := types.ServiceListOptions{Filters: filters}
 | 
						serviceOpts := types.ServiceListOptions{Filters: filters}
 | 
				
			||||||
	services, err := client.ServiceList(ctx, serviceOpts)
 | 
						services, err := client.ServiceList(c.Context, serviceOpts)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		logrus.Fatal(err)
 | 
							logrus.Fatal(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -40,7 +38,7 @@ func stackLogs(stackName string, client *dockerClient.Client) {
 | 
				
			|||||||
				Tail:       "20",
 | 
									Tail:       "20",
 | 
				
			||||||
				Timestamps: true,
 | 
									Timestamps: true,
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			logs, err := client.ServiceLogs(ctx, s, logOpts)
 | 
								logs, err := client.ServiceLogs(c.Context, s, logOpts)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				logrus.Fatal(err)
 | 
									logrus.Fatal(err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@ -65,7 +63,6 @@ var appLogsCommand = &cli.Command{
 | 
				
			|||||||
	Action: func(c *cli.Context) error {
 | 
						Action: func(c *cli.Context) error {
 | 
				
			||||||
		app := internal.ValidateApp(c)
 | 
							app := internal.ValidateApp(c)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx := context.Background()
 | 
					 | 
				
			||||||
		cl, err := client.New(app.Server)
 | 
							cl, err := client.New(app.Server)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
@ -74,7 +71,7 @@ var appLogsCommand = &cli.Command{
 | 
				
			|||||||
		serviceName := c.Args().Get(1)
 | 
							serviceName := c.Args().Get(1)
 | 
				
			||||||
		if serviceName == "" {
 | 
							if serviceName == "" {
 | 
				
			||||||
			logrus.Debug("tailing logs for all app services")
 | 
								logrus.Debug("tailing logs for all app services")
 | 
				
			||||||
			stackLogs(app.StackName(), cl)
 | 
								stackLogs(c, app.StackName(), cl)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		logrus.Debugf("tailing logs for '%s'", serviceName)
 | 
							logrus.Debugf("tailing logs for '%s'", serviceName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -82,7 +79,7 @@ var appLogsCommand = &cli.Command{
 | 
				
			|||||||
		filters := filters.NewArgs()
 | 
							filters := filters.NewArgs()
 | 
				
			||||||
		filters.Add("name", service)
 | 
							filters.Add("name", service)
 | 
				
			||||||
		serviceOpts := types.ServiceListOptions{Filters: filters}
 | 
							serviceOpts := types.ServiceListOptions{Filters: filters}
 | 
				
			||||||
		services, err := cl.ServiceList(ctx, serviceOpts)
 | 
							services, err := cl.ServiceList(c.Context, serviceOpts)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -98,7 +95,7 @@ var appLogsCommand = &cli.Command{
 | 
				
			|||||||
			Tail:       "20",
 | 
								Tail:       "20",
 | 
				
			||||||
			Timestamps: true,
 | 
								Timestamps: true,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		logs, err := cl.ServiceLogs(ctx, services[0].ID, logOpts)
 | 
							logs, err := cl.ServiceLogs(c.Context, services[0].ID, logOpts)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,6 @@ import (
 | 
				
			|||||||
	"coopcloud.tech/abra/cli/internal"
 | 
						"coopcloud.tech/abra/cli/internal"
 | 
				
			||||||
	"coopcloud.tech/abra/pkg/catalogue"
 | 
						"coopcloud.tech/abra/pkg/catalogue"
 | 
				
			||||||
	"coopcloud.tech/abra/pkg/config"
 | 
						"coopcloud.tech/abra/pkg/config"
 | 
				
			||||||
	recipePkg "coopcloud.tech/abra/pkg/recipe"
 | 
					 | 
				
			||||||
	"coopcloud.tech/abra/pkg/secret"
 | 
						"coopcloud.tech/abra/pkg/secret"
 | 
				
			||||||
	"github.com/AlecAivazis/survey/v2"
 | 
						"github.com/AlecAivazis/survey/v2"
 | 
				
			||||||
	"github.com/sirupsen/logrus"
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
@ -86,7 +85,7 @@ var appNewCommand = &cli.Command{
 | 
				
			|||||||
		if c.NArg() > 0 {
 | 
							if c.NArg() > 0 {
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		for name, _ := range catl {
 | 
							for name := range catl {
 | 
				
			||||||
			fmt.Println(name)
 | 
								fmt.Println(name)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
@ -172,16 +171,6 @@ func action(c *cli.Context) error {
 | 
				
			|||||||
		logrus.Fatal(err)
 | 
							logrus.Fatal(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	recipeMeta, err := catalogue.GetRecipeMeta(recipe.Name)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		logrus.Fatal(err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	latestVersion := recipeMeta.LatestVersion()
 | 
					 | 
				
			||||||
	if err := recipePkg.EnsureVersion(recipe.Name, latestVersion); err != nil {
 | 
					 | 
				
			||||||
		logrus.Fatal(err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err := ensureServerFlag(); err != nil {
 | 
						if err := ensureServerFlag(); err != nil {
 | 
				
			||||||
		logrus.Fatal(err)
 | 
							logrus.Fatal(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -200,7 +189,7 @@ func action(c *cli.Context) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	logrus.Debugf("'%s' sanitised as '%s' for new app", newAppName, sanitisedAppName)
 | 
						logrus.Debugf("'%s' sanitised as '%s' for new app", newAppName, sanitisedAppName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := config.CopyAppEnvSample(recipe.Name, newAppName, newAppServer); err != nil {
 | 
						if err := config.TemplateAppEnvSample(recipe.Name, newAppName, newAppServer, domain, recipe.Name); err != nil {
 | 
				
			||||||
		logrus.Fatal(err)
 | 
							logrus.Fatal(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -224,7 +213,18 @@ func action(c *cli.Context) error {
 | 
				
			|||||||
	tableCol := []string{"Name", "Domain", "Type", "Server"}
 | 
						tableCol := []string{"Name", "Domain", "Type", "Server"}
 | 
				
			||||||
	table := abraFormatter.CreateTable(tableCol)
 | 
						table := abraFormatter.CreateTable(tableCol)
 | 
				
			||||||
	table.Append([]string{sanitisedAppName, domain, recipe.Name, newAppServer})
 | 
						table.Append([]string{sanitisedAppName, domain, recipe.Name, newAppServer})
 | 
				
			||||||
	defer table.Render()
 | 
					
 | 
				
			||||||
 | 
						fmt.Println("")
 | 
				
			||||||
 | 
						fmt.Println(fmt.Sprintf("New '%s' created! Here is your new app 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", sanitisedAppName))
 | 
				
			||||||
 | 
						fmt.Println("")
 | 
				
			||||||
 | 
						fmt.Println("You can deploy this app by running the following:")
 | 
				
			||||||
 | 
						fmt.Println(fmt.Sprintf("\n    abra app deploy %s", sanitisedAppName))
 | 
				
			||||||
 | 
						fmt.Println("")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,9 @@
 | 
				
			|||||||
package app
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	abraFormatter "coopcloud.tech/abra/cli/formatter"
 | 
						abraFormatter "coopcloud.tech/abra/cli/formatter"
 | 
				
			||||||
	"coopcloud.tech/abra/cli/internal"
 | 
						"coopcloud.tech/abra/cli/internal"
 | 
				
			||||||
@ -16,45 +16,33 @@ import (
 | 
				
			|||||||
	"github.com/urfave/cli/v2"
 | 
						"github.com/urfave/cli/v2"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var watch bool
 | 
				
			||||||
 | 
					var watchFlag = &cli.BoolFlag{
 | 
				
			||||||
 | 
						Name:        "watch",
 | 
				
			||||||
 | 
						Aliases:     []string{"w"},
 | 
				
			||||||
 | 
						Value:       false,
 | 
				
			||||||
 | 
						Usage:       "Watch status by polling repeatedly",
 | 
				
			||||||
 | 
						Destination: &watch,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var appPsCommand = &cli.Command{
 | 
					var appPsCommand = &cli.Command{
 | 
				
			||||||
	Name:    "ps",
 | 
						Name:    "ps",
 | 
				
			||||||
	Usage:   "Check app status",
 | 
						Usage:   "Check app status",
 | 
				
			||||||
	Aliases: []string{"p"},
 | 
						Aliases: []string{"p"},
 | 
				
			||||||
 | 
						Flags: []cli.Flag{
 | 
				
			||||||
 | 
							watchFlag,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
	Action: func(c *cli.Context) error {
 | 
						Action: func(c *cli.Context) error {
 | 
				
			||||||
		app := internal.ValidateApp(c)
 | 
							if !watch {
 | 
				
			||||||
 | 
								showPSOutput(c)
 | 
				
			||||||
		ctx := context.Background()
 | 
								return nil
 | 
				
			||||||
		cl, err := client.New(app.Server)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			logrus.Fatal(err)
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		filters := filters.NewArgs()
 | 
							// TODO: how do we make this update in-place in an x-platform way?
 | 
				
			||||||
		filters.Add("name", app.StackName())
 | 
							for {
 | 
				
			||||||
 | 
								showPSOutput(c)
 | 
				
			||||||
		containers, err := cl.ContainerList(ctx, types.ContainerListOptions{Filters: filters})
 | 
								time.Sleep(2 * time.Second)
 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			logrus.Fatal(err)
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					 | 
				
			||||||
		tableCol := []string{"id", "image", "command", "created", "status", "ports", "names"}
 | 
					 | 
				
			||||||
		table := abraFormatter.CreateTable(tableCol)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		for _, container := range containers {
 | 
					 | 
				
			||||||
			tableRow := []string{
 | 
					 | 
				
			||||||
				abraFormatter.ShortenID(container.ID),
 | 
					 | 
				
			||||||
				abraFormatter.RemoveSha(container.Image),
 | 
					 | 
				
			||||||
				abraFormatter.Truncate(container.Command),
 | 
					 | 
				
			||||||
				abraFormatter.HumanDuration(container.Created),
 | 
					 | 
				
			||||||
				container.Status,
 | 
					 | 
				
			||||||
				formatter.DisplayablePorts(container.Ports),
 | 
					 | 
				
			||||||
				strings.Join(container.Names, ", "),
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			table.Append(tableRow)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		table.Render()
 | 
					 | 
				
			||||||
		return nil
 | 
					 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	BashComplete: func(c *cli.Context) {
 | 
						BashComplete: func(c *cli.Context) {
 | 
				
			||||||
		appNames, err := config.GetAppNames()
 | 
							appNames, err := config.GetAppNames()
 | 
				
			||||||
@ -69,3 +57,43 @@ var appPsCommand = &cli.Command{
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// showPSOutput renders ps output.
 | 
				
			||||||
 | 
					func showPSOutput(c *cli.Context) {
 | 
				
			||||||
 | 
						app := internal.ValidateApp(c)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cl, err := client.New(app.Server)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logrus.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						filters := filters.NewArgs()
 | 
				
			||||||
 | 
						filters.Add("name", app.StackName())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logrus.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tableCol := []string{"image", "created", "status", "ports", "names"}
 | 
				
			||||||
 | 
						table := abraFormatter.CreateTable(tableCol)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, container := range containers {
 | 
				
			||||||
 | 
							var containerNames []string
 | 
				
			||||||
 | 
							for _, containerName := range container.Names {
 | 
				
			||||||
 | 
								trimmed := strings.TrimPrefix(containerName, "/")
 | 
				
			||||||
 | 
								containerNames = append(containerNames, trimmed)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							tableRow := []string{
 | 
				
			||||||
 | 
								abraFormatter.RemoveSha(container.Image),
 | 
				
			||||||
 | 
								abraFormatter.HumanDuration(container.Created),
 | 
				
			||||||
 | 
								container.Status,
 | 
				
			||||||
 | 
								formatter.DisplayablePorts(container.Ports),
 | 
				
			||||||
 | 
								strings.Join(containerNames, "\n"),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							table.Append(tableRow)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						table.Render()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
package app
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -54,7 +53,6 @@ var appRemoveCommand = &cli.Command{
 | 
				
			|||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx := context.Background()
 | 
					 | 
				
			||||||
		cl, err := client.New(app.Server)
 | 
							cl, err := client.New(app.Server)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
@ -65,14 +63,14 @@ var appRemoveCommand = &cli.Command{
 | 
				
			|||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				logrus.Fatal(err)
 | 
									logrus.Fatal(err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if statuses[app.Name] == "deployed" {
 | 
								if statuses[app.Name]["status"] == "deployed" {
 | 
				
			||||||
				logrus.Fatalf("'%s' is still deployed. Run \"abra app %s undeploy\" or pass --force", app.Name, app.Name)
 | 
									logrus.Fatalf("'%s' is still deployed. Run \"abra app %s undeploy\" or pass --force", app.Name, app.Name)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		fs := filters.NewArgs()
 | 
							fs := filters.NewArgs()
 | 
				
			||||||
		fs.Add("name", app.Name)
 | 
							fs.Add("name", app.Name)
 | 
				
			||||||
		secretList, err := cl.SecretList(ctx, types.SecretListOptions{Filters: fs})
 | 
							secretList, err := cl.SecretList(c.Context, types.SecretListOptions{Filters: fs})
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -99,7 +97,7 @@ var appRemoveCommand = &cli.Command{
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			for _, name := range secretNamesToRemove {
 | 
								for _, name := range secretNamesToRemove {
 | 
				
			||||||
				err := cl.SecretRemove(ctx, secrets[name])
 | 
									err := cl.SecretRemove(c.Context, secrets[name])
 | 
				
			||||||
				if err != nil {
 | 
									if err != nil {
 | 
				
			||||||
					logrus.Fatal(err)
 | 
										logrus.Fatal(err)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
@ -109,7 +107,7 @@ var appRemoveCommand = &cli.Command{
 | 
				
			|||||||
			logrus.Info("no secrets to remove")
 | 
								logrus.Info("no secrets to remove")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		volumeListOKBody, err := cl.VolumeList(ctx, fs)
 | 
							volumeListOKBody, err := cl.VolumeList(c.Context, fs)
 | 
				
			||||||
		volumeList := volumeListOKBody.Volumes
 | 
							volumeList := volumeListOKBody.Volumes
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
@ -134,7 +132,7 @@ var appRemoveCommand = &cli.Command{
 | 
				
			|||||||
					}
 | 
										}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				for _, vol := range removeVols {
 | 
									for _, vol := range removeVols {
 | 
				
			||||||
					err := cl.VolumeRemove(ctx, vol, internal.Force) // last argument is for force removing
 | 
										err := cl.VolumeRemove(c.Context, vol, internal.Force) // last argument is for force removing
 | 
				
			||||||
					if err != nil {
 | 
										if err != nil {
 | 
				
			||||||
						logrus.Fatal(err)
 | 
											logrus.Fatal(err)
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
				
			|||||||
@ -3,14 +3,15 @@ package app
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"context"
 | 
						"coopcloud.tech/abra/pkg/catalogue"
 | 
				
			||||||
 | 
						stack "coopcloud.tech/abra/pkg/client/stack"
 | 
				
			||||||
	"coopcloud.tech/abra/pkg/config"
 | 
						"coopcloud.tech/abra/pkg/config"
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/pkg/recipe"
 | 
				
			||||||
 | 
						"coopcloud.tech/tagcmp"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"coopcloud.tech/abra/cli/internal"
 | 
						"coopcloud.tech/abra/cli/internal"
 | 
				
			||||||
	appPkg "coopcloud.tech/abra/pkg/app"
 | 
					 | 
				
			||||||
	"coopcloud.tech/abra/pkg/catalogue"
 | 
					 | 
				
			||||||
	"coopcloud.tech/abra/pkg/client"
 | 
						"coopcloud.tech/abra/pkg/client"
 | 
				
			||||||
 | 
						"github.com/AlecAivazis/survey/v2"
 | 
				
			||||||
	"github.com/sirupsen/logrus"
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
	"github.com/urfave/cli/v2"
 | 
						"github.com/urfave/cli/v2"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@ -18,8 +19,21 @@ import (
 | 
				
			|||||||
var appRollbackCommand = &cli.Command{
 | 
					var appRollbackCommand = &cli.Command{
 | 
				
			||||||
	Name:      "rollback",
 | 
						Name:      "rollback",
 | 
				
			||||||
	Usage:     "Roll an app back to a previous version",
 | 
						Usage:     "Roll an app back to a previous version",
 | 
				
			||||||
	Aliases:   []string{"r"},
 | 
						Aliases:   []string{"r", "downgrade"},
 | 
				
			||||||
	ArgsUsage: "[<version>]",
 | 
						ArgsUsage: "<app>",
 | 
				
			||||||
 | 
						Flags: []cli.Flag{
 | 
				
			||||||
 | 
							internal.ForceFlag,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						Description: `
 | 
				
			||||||
 | 
					This command rolls an app back to a previous version if one exists.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may pass "--force/-f" to downgrade to the same version again. This can be
 | 
				
			||||||
 | 
					useful if the container runtime has gotten into a weird state or your doing
 | 
				
			||||||
 | 
					some live hacking.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This action could be destructive, please ensure you have a copy of your app
 | 
				
			||||||
 | 
					data beforehand - see "abra app backup <app>" for more.
 | 
				
			||||||
 | 
					`,
 | 
				
			||||||
	BashComplete: func(c *cli.Context) {
 | 
						BashComplete: func(c *cli.Context) {
 | 
				
			||||||
		appNames, err := config.GetAppNames()
 | 
							appNames, err := config.GetAppNames()
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
@ -34,48 +48,110 @@ var appRollbackCommand = &cli.Command{
 | 
				
			|||||||
	},
 | 
						},
 | 
				
			||||||
	Action: func(c *cli.Context) error {
 | 
						Action: func(c *cli.Context) error {
 | 
				
			||||||
		app := internal.ValidateApp(c)
 | 
							app := internal.ValidateApp(c)
 | 
				
			||||||
 | 
							stackName := app.StackName()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx := context.Background()
 | 
					 | 
				
			||||||
		cl, err := client.New(app.Server)
 | 
							cl, err := client.New(app.Server)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		recipeMeta, err := catalogue.GetRecipeMeta(app.Type)
 | 
							logrus.Debugf("checking whether '%s' is already deployed", stackName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if len(recipeMeta.Versions) == 0 {
 | 
					 | 
				
			||||||
			logrus.Fatalf("no catalogue versions available for '%s'", app.Type)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		deployedVersions, isDeployed, err := appPkg.DeployedVersions(ctx, cl, app)
 | 
							if deployedVersion == "" {
 | 
				
			||||||
		if err != nil {
 | 
								logrus.Fatalf("failed to determine version of deployed '%s'", app.Name)
 | 
				
			||||||
			logrus.Fatal(err)
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if !isDeployed {
 | 
							if !isDeployed {
 | 
				
			||||||
			logrus.Fatalf("'%s' is not deployed?", app.Name)
 | 
								logrus.Fatalf("'%s' is not deployed?", app.Name)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if _, exists := deployedVersions["app"]; !exists {
 | 
					
 | 
				
			||||||
			logrus.Fatalf("no versioned 'app' service for '%s', cannot determine version", app.Name)
 | 
							versions, err := catalogue.GetRecipeCatalogueVersions(app.Type)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		version := c.Args().Get(1)
 | 
							var availableDowngrades []string
 | 
				
			||||||
		if version == "" {
 | 
							for _, version := range versions {
 | 
				
			||||||
			// TODO:
 | 
								parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
 | 
				
			||||||
			// using deployedVersions["app"], get index+1 version from catalogue
 | 
								if err != nil {
 | 
				
			||||||
			// otherwise bail out saying there is nothing to rollback to
 | 
									logrus.Fatal(err)
 | 
				
			||||||
		} else {
 | 
								}
 | 
				
			||||||
			// TODO
 | 
								parsedVersion, err := tagcmp.Parse(version)
 | 
				
			||||||
			// ensure this version is listed in the catalogue
 | 
								if err != nil {
 | 
				
			||||||
			// ensure this version is "older" (lower down in the list)
 | 
									logrus.Fatal(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if parsedVersion != parsedDeployedVersion && parsedVersion.IsLessThan(parsedDeployedVersion) {
 | 
				
			||||||
 | 
									availableDowngrades = append(availableDowngrades, version)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// TODO
 | 
							if len(availableDowngrades) == 0 {
 | 
				
			||||||
		// display table of existing state and expected state and prompt
 | 
								logrus.Fatal("no available downgrades, you're on latest")
 | 
				
			||||||
		// run the deployment with this target version!
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		logrus.Fatal("command not implemented yet, coming soon TM")
 | 
							// FIXME: jeezus golang why do you not have a list reverse function
 | 
				
			||||||
 | 
							for i, j := 0, len(availableDowngrades)-1; i < j; i, j = i+1, j-1 {
 | 
				
			||||||
 | 
								availableDowngrades[i], availableDowngrades[j] = availableDowngrades[j], availableDowngrades[i]
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var chosenDowngrade string
 | 
				
			||||||
 | 
							if !internal.Force {
 | 
				
			||||||
 | 
								prompt := &survey.Select{
 | 
				
			||||||
 | 
									Message: fmt.Sprintf("Please select a downgrade (current version: '%s'):", deployedVersion),
 | 
				
			||||||
 | 
									Options: availableDowngrades,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err := survey.AskOne(prompt, &chosenDowngrade); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if internal.Force {
 | 
				
			||||||
 | 
								chosenDowngrade = availableDowngrades[0]
 | 
				
			||||||
 | 
								logrus.Debugf("choosing '%s' as version to downgrade to", chosenDowngrade)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := recipe.EnsureVersion(app.Type, chosenDowngrade); err != nil {
 | 
				
			||||||
 | 
								logrus.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "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.Type, 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)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !internal.Force {
 | 
				
			||||||
 | 
								if err := NewVersionOverview(app, deployedVersion, chosenDowngrade); err != nil {
 | 
				
			||||||
 | 
									logrus.Fatal(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
 | 
				
			||||||
 | 
								logrus.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
package app
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -50,7 +49,6 @@ var appRunCommand = &cli.Command{
 | 
				
			|||||||
			internal.ShowSubcommandHelpAndError(c, errors.New("no <args> provided?"))
 | 
								internal.ShowSubcommandHelpAndError(c, errors.New("no <args> provided?"))
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx := context.Background()
 | 
					 | 
				
			||||||
		cl, err := client.New(app.Server)
 | 
							cl, err := client.New(app.Server)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
@ -60,7 +58,7 @@ var appRunCommand = &cli.Command{
 | 
				
			|||||||
		filters := filters.NewArgs()
 | 
							filters := filters.NewArgs()
 | 
				
			||||||
		filters.Add("name", fmt.Sprintf("%s_%s", app.StackName(), serviceName))
 | 
							filters.Add("name", fmt.Sprintf("%s_%s", app.StackName(), serviceName))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		containers, err := cl.ContainerList(ctx, types.ContainerListOptions{Filters: filters})
 | 
							containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
package app
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
@ -144,7 +143,6 @@ var appSecretRmCommand = &cli.Command{
 | 
				
			|||||||
			internal.ShowSubcommandHelpAndError(c, errors.New("no secret(s) specified?"))
 | 
								internal.ShowSubcommandHelpAndError(c, errors.New("no secret(s) specified?"))
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx := context.Background()
 | 
					 | 
				
			||||||
		cl, err := client.New(app.Server)
 | 
							cl, err := client.New(app.Server)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
@ -152,7 +150,7 @@ var appSecretRmCommand = &cli.Command{
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		filters := filters.NewArgs()
 | 
							filters := filters.NewArgs()
 | 
				
			||||||
		filters.Add("name", app.StackName())
 | 
							filters.Add("name", app.StackName())
 | 
				
			||||||
		secretList, err := cl.SecretList(ctx, types.SecretListOptions{Filters: filters})
 | 
							secretList, err := cl.SecretList(c.Context, types.SecretListOptions{Filters: filters})
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -162,7 +160,7 @@ var appSecretRmCommand = &cli.Command{
 | 
				
			|||||||
			secretName := cont.Spec.Annotations.Name
 | 
								secretName := cont.Spec.Annotations.Name
 | 
				
			||||||
			parsed := secret.ParseGeneratedSecretName(secretName, app)
 | 
								parsed := secret.ParseGeneratedSecretName(secretName, app)
 | 
				
			||||||
			if allSecrets {
 | 
								if allSecrets {
 | 
				
			||||||
				if err := cl.SecretRemove(ctx, secretName); err != nil {
 | 
									if err := cl.SecretRemove(c.Context, secretName); err != nil {
 | 
				
			||||||
					logrus.Fatal(err)
 | 
										logrus.Fatal(err)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				if internal.Pass {
 | 
									if internal.Pass {
 | 
				
			||||||
@ -172,7 +170,7 @@ var appSecretRmCommand = &cli.Command{
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				if parsed == secretToRm {
 | 
									if parsed == secretToRm {
 | 
				
			||||||
					if err := cl.SecretRemove(ctx, secretName); err != nil {
 | 
										if err := cl.SecretRemove(c.Context, secretName); err != nil {
 | 
				
			||||||
						logrus.Fatal(err)
 | 
											logrus.Fatal(err)
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					if internal.Pass {
 | 
										if internal.Pass {
 | 
				
			||||||
@ -199,7 +197,6 @@ var appSecretLsCommand = &cli.Command{
 | 
				
			|||||||
		tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"}
 | 
							tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"}
 | 
				
			||||||
		table := abraFormatter.CreateTable(tableCol)
 | 
							table := abraFormatter.CreateTable(tableCol)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx := context.Background()
 | 
					 | 
				
			||||||
		cl, err := client.New(app.Server)
 | 
							cl, err := client.New(app.Server)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
@ -207,7 +204,7 @@ var appSecretLsCommand = &cli.Command{
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		filters := filters.NewArgs()
 | 
							filters := filters.NewArgs()
 | 
				
			||||||
		filters.Add("name", app.StackName())
 | 
							filters.Add("name", app.StackName())
 | 
				
			||||||
		secretList, err := cl.SecretList(ctx, types.SecretListOptions{Filters: filters})
 | 
							secretList, err := cl.SecretList(c.Context, types.SecretListOptions{Filters: filters})
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
package app
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"coopcloud.tech/abra/cli/internal"
 | 
						"coopcloud.tech/abra/cli/internal"
 | 
				
			||||||
@ -24,14 +23,13 @@ volumes as eligiblef or pruning once undeployed.
 | 
				
			|||||||
	Action: func(c *cli.Context) error {
 | 
						Action: func(c *cli.Context) error {
 | 
				
			||||||
		app := internal.ValidateApp(c)
 | 
							app := internal.ValidateApp(c)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx := context.Background()
 | 
					 | 
				
			||||||
		cl, err := client.New(app.Server)
 | 
							cl, err := client.New(app.Server)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		rmOpts := stack.Remove{Namespaces: []string{app.StackName()}}
 | 
							rmOpts := stack.Remove{Namespaces: []string{app.StackName()}}
 | 
				
			||||||
		if err := stack.RunRemove(ctx, cl, rmOpts); err != nil {
 | 
							if err := stack.RunRemove(c.Context, cl, rmOpts); err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										157
									
								
								cli/app/upgrade.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								cli/app/upgrade.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,157 @@
 | 
				
			|||||||
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/cli/internal"
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/pkg/catalogue"
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/pkg/client"
 | 
				
			||||||
 | 
						stack "coopcloud.tech/abra/pkg/client/stack"
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/pkg/config"
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/pkg/recipe"
 | 
				
			||||||
 | 
						"coopcloud.tech/tagcmp"
 | 
				
			||||||
 | 
						"github.com/AlecAivazis/survey/v2"
 | 
				
			||||||
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
 | 
						"github.com/urfave/cli/v2"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var appUpgradeCommand = &cli.Command{
 | 
				
			||||||
 | 
						Name:      "upgrade",
 | 
				
			||||||
 | 
						Aliases:   []string{"u"},
 | 
				
			||||||
 | 
						Usage:     "Upgrade an app",
 | 
				
			||||||
 | 
						ArgsUsage: "<app>",
 | 
				
			||||||
 | 
						Flags: []cli.Flag{
 | 
				
			||||||
 | 
							internal.ForceFlag,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						Description: `
 | 
				
			||||||
 | 
					This command supports upgrading an app. You can use it to choose and roll out a
 | 
				
			||||||
 | 
					new upgrade to an existing app.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This command specifically supports changing the version of running apps, as
 | 
				
			||||||
 | 
					opposed to "abra app deploy <app>" which will not change the version of a
 | 
				
			||||||
 | 
					deployed app.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may pass "--force/-f" to upgrade to the same version again. This can be
 | 
				
			||||||
 | 
					useful if the container runtime has gotten into a weird state or your doing
 | 
				
			||||||
 | 
					some live hacking.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This action could be destructive, please ensure you have a copy of your app
 | 
				
			||||||
 | 
					data beforehand - see "abra app backup <app>" for more.
 | 
				
			||||||
 | 
					`,
 | 
				
			||||||
 | 
						Action: func(c *cli.Context) error {
 | 
				
			||||||
 | 
							app := internal.ValidateApp(c)
 | 
				
			||||||
 | 
							stackName := app.StackName()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							cl, err := client.New(app.Server)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logrus.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							logrus.Debugf("checking whether '%s' is already deployed", stackName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logrus.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if deployedVersion == "" {
 | 
				
			||||||
 | 
								logrus.Fatalf("failed to determine version of deployed '%s'", app.Name)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !isDeployed {
 | 
				
			||||||
 | 
								logrus.Fatalf("'%s' is not deployed?", app.Name)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							versions, err := catalogue.GetRecipeCatalogueVersions(app.Type)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logrus.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var availableUpgrades []string
 | 
				
			||||||
 | 
							for _, version := range versions {
 | 
				
			||||||
 | 
								parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									logrus.Fatal(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								parsedVersion, err := tagcmp.Parse(version)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									logrus.Fatal(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if parsedVersion.IsGreaterThan(parsedDeployedVersion) {
 | 
				
			||||||
 | 
									availableUpgrades = append(availableUpgrades, version)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if len(availableUpgrades) == 0 {
 | 
				
			||||||
 | 
								logrus.Fatal("no available upgrades, you're on latest")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var chosenUpgrade string
 | 
				
			||||||
 | 
							if !internal.Force {
 | 
				
			||||||
 | 
								prompt := &survey.Select{
 | 
				
			||||||
 | 
									Message: fmt.Sprintf("Please select an upgrade (current version: '%s'):", deployedVersion),
 | 
				
			||||||
 | 
									Options: availableUpgrades,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err := survey.AskOne(prompt, &chosenUpgrade); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if internal.Force {
 | 
				
			||||||
 | 
								chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
 | 
				
			||||||
 | 
								logrus.Debugf("choosing '%s' as version to upgrade to", chosenUpgrade)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := recipe.EnsureVersion(app.Type, chosenUpgrade); err != nil {
 | 
				
			||||||
 | 
								logrus.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "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.Type, 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)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !internal.Force {
 | 
				
			||||||
 | 
								if err := NewVersionOverview(app, deployedVersion, chosenUpgrade); err != nil {
 | 
				
			||||||
 | 
									logrus.Fatal(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
 | 
				
			||||||
 | 
								logrus.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						BashComplete: func(c *cli.Context) {
 | 
				
			||||||
 | 
							appNames, err := config.GetAppNames()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logrus.Warn(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if c.NArg() > 0 {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							for _, a := range appNames {
 | 
				
			||||||
 | 
								fmt.Println(a)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -2,12 +2,12 @@ package app
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"sort"
 | 
					 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	abraFormatter "coopcloud.tech/abra/cli/formatter"
 | 
						abraFormatter "coopcloud.tech/abra/cli/formatter"
 | 
				
			||||||
	"coopcloud.tech/abra/cli/internal"
 | 
						"coopcloud.tech/abra/cli/internal"
 | 
				
			||||||
	appPkg "coopcloud.tech/abra/pkg/app"
 | 
						"coopcloud.tech/abra/pkg/catalogue"
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/pkg/client"
 | 
				
			||||||
	"coopcloud.tech/abra/pkg/client/stack"
 | 
						"coopcloud.tech/abra/pkg/client/stack"
 | 
				
			||||||
	"coopcloud.tech/abra/pkg/config"
 | 
						"coopcloud.tech/abra/pkg/config"
 | 
				
			||||||
	"github.com/docker/distribution/reference"
 | 
						"github.com/docker/distribution/reference"
 | 
				
			||||||
@ -32,65 +32,61 @@ func getImagePath(image string) (string, error) {
 | 
				
			|||||||
var appVersionCommand = &cli.Command{
 | 
					var appVersionCommand = &cli.Command{
 | 
				
			||||||
	Name:    "version",
 | 
						Name:    "version",
 | 
				
			||||||
	Aliases: []string{"v"},
 | 
						Aliases: []string{"v"},
 | 
				
			||||||
	Usage:   "Show version of all services in app",
 | 
						Usage:   "Show app versions",
 | 
				
			||||||
 | 
						Description: `
 | 
				
			||||||
 | 
					This command shows all information about versioning related to a deployed app.
 | 
				
			||||||
 | 
					This includes the individual image names, tags and digests. But also the Co-op
 | 
				
			||||||
 | 
					Cloud recipe version.
 | 
				
			||||||
 | 
					`,
 | 
				
			||||||
	Action: func(c *cli.Context) error {
 | 
						Action: func(c *cli.Context) error {
 | 
				
			||||||
		app := internal.ValidateApp(c)
 | 
							app := internal.ValidateApp(c)
 | 
				
			||||||
 | 
							stackName := app.StackName()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
 | 
							cl, err := client.New(app.Server)
 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			logrus.Fatal(err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		opts := stack.Deploy{Composefiles: composeFiles}
 | 
					 | 
				
			||||||
		compose, err := config.GetAppComposeConfig(app.Type, opts, app.Env)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ch := make(chan stack.StackStatus, len(compose.Services))
 | 
							logrus.Debugf("checking whether '%s' is already deployed", stackName)
 | 
				
			||||||
		for _, service := range compose.Services {
 | 
					
 | 
				
			||||||
			label := fmt.Sprintf("coop-cloud.%s.%s.version", app.StackName(), service.Name)
 | 
							isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
 | 
				
			||||||
			go func(s string, l string) {
 | 
							if err != nil {
 | 
				
			||||||
				ch <- stack.GetDeployedServicesByLabel(s, l)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
			}(app.Server, label)
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		tableCol := []string{"name", "image", "version", "digest"}
 | 
							if deployedVersion == "" {
 | 
				
			||||||
 | 
								logrus.Fatalf("failed to determine version of deployed '%s'", app.Name)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !isDeployed {
 | 
				
			||||||
 | 
								logrus.Fatalf("'%s' is not deployed?", app.Name)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							recipeMeta, err := catalogue.GetRecipeMeta(app.Type)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logrus.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							versionsMeta := make(map[string]catalogue.ServiceMeta)
 | 
				
			||||||
 | 
							for _, recipeVersion := range recipeMeta.Versions {
 | 
				
			||||||
 | 
								if currentVersion, exists := recipeVersion[deployedVersion]; exists {
 | 
				
			||||||
 | 
									versionsMeta = currentVersion
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if len(versionsMeta) == 0 {
 | 
				
			||||||
 | 
								logrus.Fatalf("PANIC: could not retrieve deployed version ('%s') from recipe catalogue?", deployedVersion)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							tableCol := []string{"name", "image", "version", "tag", "digest"}
 | 
				
			||||||
		table := abraFormatter.CreateTable(tableCol)
 | 
							table := abraFormatter.CreateTable(tableCol)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		statuses := make(map[string]stack.StackStatus)
 | 
							for serviceName, versionMeta := range versionsMeta {
 | 
				
			||||||
		for range compose.Services {
 | 
								table.Append([]string{serviceName, versionMeta.Image, deployedVersion, versionMeta.Tag, versionMeta.Digest})
 | 
				
			||||||
			status := <-ch
 | 
					 | 
				
			||||||
			if len(status.Services) > 0 {
 | 
					 | 
				
			||||||
				serviceName := appPkg.ParseServiceName(status.Services[0].Spec.Name)
 | 
					 | 
				
			||||||
				statuses[serviceName] = status
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		sort.SliceStable(compose.Services, func(i, j int) bool {
 | 
					 | 
				
			||||||
			return compose.Services[i].Name < compose.Services[j].Name
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		for _, service := range compose.Services {
 | 
					 | 
				
			||||||
			if status, ok := statuses[service.Name]; ok {
 | 
					 | 
				
			||||||
				statusService := status.Services[0]
 | 
					 | 
				
			||||||
				label := fmt.Sprintf("coop-cloud.%s.%s.version", app.StackName(), service.Name)
 | 
					 | 
				
			||||||
				version, digest := appPkg.ParseVersionLabel(statusService.Spec.Labels[label])
 | 
					 | 
				
			||||||
				image, err := getImagePath(statusService.Spec.Labels["com.docker.stack.image"])
 | 
					 | 
				
			||||||
				if err != nil {
 | 
					 | 
				
			||||||
					logrus.Fatal(err)
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				table.Append([]string{service.Name, image, version, digest})
 | 
					 | 
				
			||||||
				continue
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			image, err := getImagePath(service.Image)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				logrus.Fatal(err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			table.Append([]string{service.Name, image, "?", "?"})
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		table.Render()
 | 
							table.Render()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	BashComplete: func(c *cli.Context) {
 | 
						BashComplete: func(c *cli.Context) {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
package app
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	abraFormatter "coopcloud.tech/abra/cli/formatter"
 | 
						abraFormatter "coopcloud.tech/abra/cli/formatter"
 | 
				
			||||||
@ -20,8 +19,7 @@ var appVolumeListCommand = &cli.Command{
 | 
				
			|||||||
	Action: func(c *cli.Context) error {
 | 
						Action: func(c *cli.Context) error {
 | 
				
			||||||
		app := internal.ValidateApp(c)
 | 
							app := internal.ValidateApp(c)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx := context.Background()
 | 
							volumeList, err := client.GetVolumes(c.Context, app.Server, app.Name)
 | 
				
			||||||
		volumeList, err := client.GetVolumes(ctx, app.Server, app.Name)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -53,8 +51,7 @@ var appVolumeRemoveCommand = &cli.Command{
 | 
				
			|||||||
	Action: func(c *cli.Context) error {
 | 
						Action: func(c *cli.Context) error {
 | 
				
			||||||
		app := internal.ValidateApp(c)
 | 
							app := internal.ValidateApp(c)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx := context.Background()
 | 
							volumeList, err := client.GetVolumes(c.Context, app.Server, app.Name)
 | 
				
			||||||
		volumeList, err := client.GetVolumes(ctx, app.Server, app.Name)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -74,7 +71,7 @@ var appVolumeRemoveCommand = &cli.Command{
 | 
				
			|||||||
			volumesToRemove = volumeNames
 | 
								volumesToRemove = volumeNames
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		err = client.RemoveVolumes(ctx, app.Server, volumesToRemove, internal.Force)
 | 
							err = client.RemoveVolumes(c.Context, app.Server, volumesToRemove, internal.Force)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -99,7 +96,7 @@ var appVolumeRemoveCommand = &cli.Command{
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
var appVolumeCommand = &cli.Command{
 | 
					var appVolumeCommand = &cli.Command{
 | 
				
			||||||
	Name:      "volume",
 | 
						Name:      "volume",
 | 
				
			||||||
	Aliases:   []string{"v"},
 | 
						Aliases:   []string{"vl"},
 | 
				
			||||||
	Usage:     "Manage app volumes",
 | 
						Usage:     "Manage app volumes",
 | 
				
			||||||
	ArgsUsage: "<command>",
 | 
						ArgsUsage: "<command>",
 | 
				
			||||||
	Subcommands: []*cli.Command{
 | 
						Subcommands: []*cli.Command{
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,13 @@
 | 
				
			|||||||
package catalogue
 | 
					package catalogue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/cli/formatter"
 | 
				
			||||||
	"coopcloud.tech/abra/pkg/catalogue"
 | 
						"coopcloud.tech/abra/pkg/catalogue"
 | 
				
			||||||
	"coopcloud.tech/abra/pkg/config"
 | 
						"coopcloud.tech/abra/pkg/config"
 | 
				
			||||||
	"coopcloud.tech/abra/pkg/git"
 | 
						"coopcloud.tech/abra/pkg/git"
 | 
				
			||||||
@ -11,30 +15,158 @@ import (
 | 
				
			|||||||
	"github.com/urfave/cli/v2"
 | 
						"github.com/urfave/cli/v2"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CatalogueSkipList is all the repos that are not recipes.
 | 
				
			||||||
 | 
					var CatalogueSkipList = map[string]bool{
 | 
				
			||||||
 | 
						"abra":                  true,
 | 
				
			||||||
 | 
						"abra-bash":             true,
 | 
				
			||||||
 | 
						"abra-apps":             true,
 | 
				
			||||||
 | 
						"abra-aur":              true,
 | 
				
			||||||
 | 
						"abra-capsul":           true,
 | 
				
			||||||
 | 
						"abra-gandi":            true,
 | 
				
			||||||
 | 
						"abra-hetzner":          true,
 | 
				
			||||||
 | 
						"apps":                  true,
 | 
				
			||||||
 | 
						"aur-abra-git":          true,
 | 
				
			||||||
 | 
						"auto-apps-json":        true,
 | 
				
			||||||
 | 
						"auto-mirror":           true,
 | 
				
			||||||
 | 
						"backup-bot":            true,
 | 
				
			||||||
 | 
						"coopcloud.tech":        true,
 | 
				
			||||||
 | 
						"coturn":                true,
 | 
				
			||||||
 | 
						"docker-cp-deploy":      true,
 | 
				
			||||||
 | 
						"docker-dind-bats-kcov": true,
 | 
				
			||||||
 | 
						"docs.coopcloud.tech":   true,
 | 
				
			||||||
 | 
						"example":               true,
 | 
				
			||||||
 | 
						"gardening":             true,
 | 
				
			||||||
 | 
						"go-abra":               true,
 | 
				
			||||||
 | 
						"organising":            true,
 | 
				
			||||||
 | 
						"pyabra":                true,
 | 
				
			||||||
 | 
						"radicle-seed-node":     true,
 | 
				
			||||||
 | 
						"stack-ssh-deploy":      true,
 | 
				
			||||||
 | 
						"swarm-cronjob":         true,
 | 
				
			||||||
 | 
						"tagcmp":                true,
 | 
				
			||||||
 | 
						"tyop":                  true,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var catalogueGenerateCommand = &cli.Command{
 | 
					var catalogueGenerateCommand = &cli.Command{
 | 
				
			||||||
	Name:         "generate",
 | 
						Name:      "generate",
 | 
				
			||||||
	Aliases:      []string{"g"},
 | 
						Aliases:   []string{"g"},
 | 
				
			||||||
	Usage:        "Generate a new copy of the catalogue",
 | 
						Usage:     "Generate a new copy of the catalogue",
 | 
				
			||||||
	BashComplete: func(c *cli.Context) {},
 | 
						ArgsUsage: "[<recipe>]",
 | 
				
			||||||
	Action: func(c *cli.Context) error {
 | 
						BashComplete: func(c *cli.Context) {
 | 
				
			||||||
		catl, err := catalogue.ReadRecipeCatalogue()
 | 
							catl, err := catalogue.ReadRecipeCatalogue()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logrus.Warn(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if c.NArg() > 0 {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							for name := range catl {
 | 
				
			||||||
 | 
								fmt.Println(name)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						Action: func(c *cli.Context) error {
 | 
				
			||||||
 | 
							recipeName := c.Args().First()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							repos, err := catalogue.ReadReposMetadata()
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		for recipeName, recipeMeta := range catl {
 | 
							logrus.Debugf("ensuring '%v' recipe(s) are locally present and up-to-date", len(repos))
 | 
				
			||||||
			recipeDir := path.Join(config.ABRA_DIR, "apps", strings.ToLower(recipeName))
 | 
					
 | 
				
			||||||
			if err := git.Clone(recipeDir, recipeMeta.Repository); err != nil {
 | 
							retrieveBar := formatter.CreateProgressbar(len(repos), "retrieving recipes...")
 | 
				
			||||||
 | 
							ch := make(chan string, len(repos))
 | 
				
			||||||
 | 
							for _, repoMeta := range repos {
 | 
				
			||||||
 | 
								go func(rm catalogue.RepoMeta) {
 | 
				
			||||||
 | 
									if recipeName != "" && recipeName != rm.Name {
 | 
				
			||||||
 | 
										ch <- rm.Name
 | 
				
			||||||
 | 
										retrieveBar.Add(1)
 | 
				
			||||||
 | 
										return
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if _, exists := CatalogueSkipList[rm.Name]; exists {
 | 
				
			||||||
 | 
										ch <- rm.Name
 | 
				
			||||||
 | 
										retrieveBar.Add(1)
 | 
				
			||||||
 | 
										return
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									recipeDir := path.Join(config.ABRA_DIR, "apps", rm.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if err := git.Clone(recipeDir, rm.SSHURL); err != nil {
 | 
				
			||||||
 | 
										logrus.Fatal(err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if err := git.EnsureUpToDate(recipeDir); err != nil {
 | 
				
			||||||
 | 
										logrus.Fatal(err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									ch <- rm.Name
 | 
				
			||||||
 | 
									retrieveBar.Add(1)
 | 
				
			||||||
 | 
								}(repoMeta)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for range repos {
 | 
				
			||||||
 | 
								<-ch // wait for everything
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							catl := make(catalogue.RecipeCatalogue)
 | 
				
			||||||
 | 
							catlBar := formatter.CreateProgressbar(len(repos), "generating catalogue...")
 | 
				
			||||||
 | 
							for _, recipeMeta := range repos {
 | 
				
			||||||
 | 
								if recipeName != "" && recipeName != recipeMeta.Name {
 | 
				
			||||||
 | 
									catlBar.Add(1)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if _, exists := CatalogueSkipList[recipeMeta.Name]; exists {
 | 
				
			||||||
 | 
									catlBar.Add(1)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								versions, err := catalogue.GetRecipeVersions(recipeMeta.Name)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
				logrus.Fatal(err)
 | 
									logrus.Fatal(err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if err := git.EnsureUpToDate(recipeDir); err != nil {
 | 
								catl[recipeMeta.Name] = catalogue.RecipeMeta{
 | 
				
			||||||
 | 
									Name:          recipeMeta.Name,
 | 
				
			||||||
 | 
									Repository:    recipeMeta.CloneURL,
 | 
				
			||||||
 | 
									Icon:          recipeMeta.AvatarURL,
 | 
				
			||||||
 | 
									DefaultBranch: recipeMeta.DefaultBranch,
 | 
				
			||||||
 | 
									Description:   recipeMeta.Description,
 | 
				
			||||||
 | 
									Website:       recipeMeta.Website,
 | 
				
			||||||
 | 
									Versions:      versions,
 | 
				
			||||||
 | 
									// Category:      ..., // FIXME: once we sort out the machine-readable catalogue interface
 | 
				
			||||||
 | 
									// Features:      ..., // FIXME: once we figure out the machine-readable catalogue interface
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								catlBar.Add(1)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							recipesJSON, err := json.MarshalIndent(catl, "", "    ")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logrus.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if _, err := os.Stat(config.APPS_JSON); err != nil && os.IsNotExist(err) {
 | 
				
			||||||
 | 
								if err := ioutil.WriteFile(config.APPS_JSON, recipesJSON, 0644); err != nil {
 | 
				
			||||||
				logrus.Fatal(err)
 | 
									logrus.Fatal(err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								if recipeName != "" {
 | 
				
			||||||
 | 
									catlFS, err := catalogue.ReadRecipeCatalogue()
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										logrus.Fatal(err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									catlFS[recipeName] = catl[recipeName]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									updatedRecipesJSON, err := json.MarshalIndent(catlFS, "", "    ")
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										logrus.Fatal(err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if err := ioutil.WriteFile(config.APPS_JSON, updatedRecipesJSON, 0644); err != nil {
 | 
				
			||||||
 | 
										logrus.Fatal(err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// for reach app, build the recipemeta from parsing
 | 
							logrus.Infof("generated new recipe catalogue in '%s'", config.APPS_JSON)
 | 
				
			||||||
		// spit out a JSON file
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										31
									
								
								cli/cli.go
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								cli/cli.go
									
									
									
									
									
								
							@ -4,11 +4,14 @@ package cli
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"coopcloud.tech/abra/cli/app"
 | 
						"coopcloud.tech/abra/cli/app"
 | 
				
			||||||
	"coopcloud.tech/abra/cli/catalogue"
 | 
						"coopcloud.tech/abra/cli/catalogue"
 | 
				
			||||||
	"coopcloud.tech/abra/cli/recipe"
 | 
						"coopcloud.tech/abra/cli/recipe"
 | 
				
			||||||
	"coopcloud.tech/abra/cli/server"
 | 
						"coopcloud.tech/abra/cli/server"
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/pkg/config"
 | 
				
			||||||
 | 
						logrusStack "github.com/Gurpartap/logrus-stack"
 | 
				
			||||||
	"github.com/sirupsen/logrus"
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
	"github.com/urfave/cli/v2"
 | 
						"github.com/urfave/cli/v2"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@ -64,7 +67,7 @@ func RunApp(version, commit string) {
 | 
				
			|||||||
			DebugFlag,
 | 
								DebugFlag,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		Authors: []*cli.Author{
 | 
							Authors: []*cli.Author{
 | 
				
			||||||
			&cli.Author{
 | 
								{
 | 
				
			||||||
				Name:  "Autonomic Co-op",
 | 
									Name:  "Autonomic Co-op",
 | 
				
			||||||
				Email: "helo@autonomic.zone",
 | 
									Email: "helo@autonomic.zone",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
@ -76,12 +79,34 @@ func RunApp(version, commit string) {
 | 
				
			|||||||
	app.Before = func(c *cli.Context) error {
 | 
						app.Before = func(c *cli.Context) error {
 | 
				
			||||||
		if Debug {
 | 
							if Debug {
 | 
				
			||||||
			logrus.SetLevel(logrus.DebugLevel)
 | 
								logrus.SetLevel(logrus.DebugLevel)
 | 
				
			||||||
 | 
								logrus.SetFormatter(&logrus.TextFormatter{})
 | 
				
			||||||
 | 
								logrus.SetOutput(os.Stderr)
 | 
				
			||||||
 | 
								logrus.AddHook(logrusStack.StandardHook())
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							paths := []string{
 | 
				
			||||||
 | 
								config.ABRA_DIR,
 | 
				
			||||||
 | 
								path.Join(config.ABRA_DIR, "servers"),
 | 
				
			||||||
 | 
								path.Join(config.ABRA_DIR, "apps"),
 | 
				
			||||||
 | 
								path.Join(config.ABRA_DIR, "vendor"),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, path := range paths {
 | 
				
			||||||
 | 
								if err := os.Mkdir(path, 0755); err != nil {
 | 
				
			||||||
 | 
									if !os.IsExist(err) {
 | 
				
			||||||
 | 
										logrus.Fatal(err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									logrus.Debugf("'%s' already created, moving on...", path)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								logrus.Debugf("'%s' is missing, creating...", path)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							logrus.Debugf("abra version '%s', commit '%s'", version, commit)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	logrus.Debugf("Flying abra version '%s', commit '%s', enjoy the ride", version, commit)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err := app.Run(os.Args); err != nil {
 | 
						if err := app.Run(os.Args); err != nil {
 | 
				
			||||||
		logrus.Fatal(err)
 | 
							logrus.Fatal(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -9,6 +9,7 @@ import (
 | 
				
			|||||||
	"github.com/docker/cli/cli/command/formatter"
 | 
						"github.com/docker/cli/cli/command/formatter"
 | 
				
			||||||
	"github.com/docker/go-units"
 | 
						"github.com/docker/go-units"
 | 
				
			||||||
	"github.com/olekukonko/tablewriter"
 | 
						"github.com/olekukonko/tablewriter"
 | 
				
			||||||
 | 
						"github.com/schollz/progressbar/v3"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func ShortenID(str string) string {
 | 
					func ShortenID(str string) string {
 | 
				
			||||||
@ -37,3 +38,15 @@ func CreateTable(columns []string) *tablewriter.Table {
 | 
				
			|||||||
	table.SetHeader(columns)
 | 
						table.SetHeader(columns)
 | 
				
			||||||
	return table
 | 
						return table
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateProgressbar generates a progress bar
 | 
				
			||||||
 | 
					func CreateProgressbar(length int, title string) *progressbar.ProgressBar {
 | 
				
			||||||
 | 
						return progressbar.NewOptions(
 | 
				
			||||||
 | 
							length,
 | 
				
			||||||
 | 
							progressbar.OptionClearOnFinish(),
 | 
				
			||||||
 | 
							progressbar.OptionSetPredictTime(false),
 | 
				
			||||||
 | 
							progressbar.OptionShowCount(),
 | 
				
			||||||
 | 
							progressbar.OptionFullWidth(),
 | 
				
			||||||
 | 
							progressbar.OptionSetDescription(title),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -41,6 +41,10 @@ func ValidateApp(c *cli.Context) config.App {
 | 
				
			|||||||
		logrus.Fatal(err)
 | 
							logrus.Fatal(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := recipe.EnsureExists(app.Type); err != nil {
 | 
				
			||||||
 | 
							logrus.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	logrus.Debugf("validated '%s' as app argument", appName)
 | 
						logrus.Debugf("validated '%s' as app argument", appName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return app
 | 
						return app
 | 
				
			||||||
 | 
				
			|||||||
@ -99,7 +99,7 @@ var recipeLintCommand = &cli.Command{
 | 
				
			|||||||
		if c.NArg() > 0 {
 | 
							if c.NArg() > 0 {
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		for name, _ := range catl {
 | 
							for name := range catl {
 | 
				
			||||||
			fmt.Println(name)
 | 
								fmt.Println(name)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,30 @@ import (
 | 
				
			|||||||
	"github.com/urfave/cli/v2"
 | 
						"github.com/urfave/cli/v2"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var Major bool
 | 
				
			||||||
 | 
					var MajorFlag = &cli.BoolFlag{
 | 
				
			||||||
 | 
						Name:        "major",
 | 
				
			||||||
 | 
						Value:       false,
 | 
				
			||||||
 | 
						Aliases:     []string{"ma", "x"},
 | 
				
			||||||
 | 
						Destination: &Major,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var Minor bool
 | 
				
			||||||
 | 
					var MinorFlag = &cli.BoolFlag{
 | 
				
			||||||
 | 
						Name:        "minor",
 | 
				
			||||||
 | 
						Value:       false,
 | 
				
			||||||
 | 
						Aliases:     []string{"mi", "y"},
 | 
				
			||||||
 | 
						Destination: &Minor,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var Patch bool
 | 
				
			||||||
 | 
					var PatchFlag = &cli.BoolFlag{
 | 
				
			||||||
 | 
						Name:        "patch",
 | 
				
			||||||
 | 
						Value:       false,
 | 
				
			||||||
 | 
						Aliases:     []string{"p", "z"},
 | 
				
			||||||
 | 
						Destination: &Patch,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// RecipeCommand defines all recipe related sub-commands.
 | 
					// RecipeCommand defines all recipe related sub-commands.
 | 
				
			||||||
var RecipeCommand = &cli.Command{
 | 
					var RecipeCommand = &cli.Command{
 | 
				
			||||||
	Name:      "recipe",
 | 
						Name:      "recipe",
 | 
				
			||||||
@ -18,6 +42,7 @@ Cloud community and you can use Abra to read them and create apps for you.
 | 
				
			|||||||
	Subcommands: []*cli.Command{
 | 
						Subcommands: []*cli.Command{
 | 
				
			||||||
		recipeListCommand,
 | 
							recipeListCommand,
 | 
				
			||||||
		recipeVersionCommand,
 | 
							recipeVersionCommand,
 | 
				
			||||||
 | 
							recipeReleaseCommand,
 | 
				
			||||||
		recipeNewCommand,
 | 
							recipeNewCommand,
 | 
				
			||||||
		recipeUpgradeCommand,
 | 
							recipeUpgradeCommand,
 | 
				
			||||||
		recipeSyncCommand,
 | 
							recipeSyncCommand,
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										312
									
								
								cli/recipe/release.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								cli/recipe/release.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,312 @@
 | 
				
			|||||||
 | 
					package recipe
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/cli/internal"
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/pkg/config"
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/pkg/recipe"
 | 
				
			||||||
 | 
						recipePkg "coopcloud.tech/abra/pkg/recipe"
 | 
				
			||||||
 | 
						"coopcloud.tech/tagcmp"
 | 
				
			||||||
 | 
						"github.com/AlecAivazis/survey/v2"
 | 
				
			||||||
 | 
						"github.com/go-git/go-git/v5"
 | 
				
			||||||
 | 
						"github.com/go-git/go-git/v5/plumbing"
 | 
				
			||||||
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
 | 
						"github.com/urfave/cli/v2"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var Push bool
 | 
				
			||||||
 | 
					var PushFlag = &cli.BoolFlag{
 | 
				
			||||||
 | 
						Name:        "push",
 | 
				
			||||||
 | 
						Value:       false,
 | 
				
			||||||
 | 
						Destination: &Push,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var Dry bool
 | 
				
			||||||
 | 
					var DryFlag = &cli.BoolFlag{
 | 
				
			||||||
 | 
						Name:        "dry-run",
 | 
				
			||||||
 | 
						Value:       false,
 | 
				
			||||||
 | 
						Aliases:     []string{"d"},
 | 
				
			||||||
 | 
						Destination: &Dry,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var CommitMessage string
 | 
				
			||||||
 | 
					var CommitMessageFlag = &cli.StringFlag{
 | 
				
			||||||
 | 
						Name:        "commit-message",
 | 
				
			||||||
 | 
						Usage:       "commit message. Implies --commit",
 | 
				
			||||||
 | 
						Aliases:     []string{"cm"},
 | 
				
			||||||
 | 
						Destination: &CommitMessage,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var Commit bool
 | 
				
			||||||
 | 
					var CommitFlag = &cli.BoolFlag{
 | 
				
			||||||
 | 
						Name:        "commit",
 | 
				
			||||||
 | 
						Usage:       "add compose.yml to staging area and commit changes",
 | 
				
			||||||
 | 
						Value:       false,
 | 
				
			||||||
 | 
						Aliases:     []string{"c"},
 | 
				
			||||||
 | 
						Destination: &Commit,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var TagMessage string
 | 
				
			||||||
 | 
					var TagMessageFlag = &cli.StringFlag{
 | 
				
			||||||
 | 
						Name:        "tag-comment",
 | 
				
			||||||
 | 
						Usage:       "tag comment. If not given, user will be asked for it",
 | 
				
			||||||
 | 
						Aliases:     []string{"t", "tm"},
 | 
				
			||||||
 | 
						Destination: &TagMessage,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var recipeReleaseCommand = &cli.Command{
 | 
				
			||||||
 | 
						Name:      "release",
 | 
				
			||||||
 | 
						Usage:     "tag a recipe",
 | 
				
			||||||
 | 
						Aliases:   []string{"rl"},
 | 
				
			||||||
 | 
						ArgsUsage: "<recipe> [<tag>]",
 | 
				
			||||||
 | 
						Description: `
 | 
				
			||||||
 | 
					This command is used to specify a new tag for a recipe. These tags are used to
 | 
				
			||||||
 | 
					identify different versions of the recipe and are published on the Co-op Cloud
 | 
				
			||||||
 | 
					recipe catalogue.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					These tags take the following form:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    a.b.c+x.y.z
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Where the "a.b.c" part is maintained as a semantic version of the recipe by the
 | 
				
			||||||
 | 
					recipe maintainer. And the "x.y.z" part is the image tag of the recipe "app"
 | 
				
			||||||
 | 
					service (the main container which contains the software to be used).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					We maintain a semantic versioning scheme ("a.b.c") alongside the libre app
 | 
				
			||||||
 | 
					versioning scheme in order to maximise the chances that the nature of recipe
 | 
				
			||||||
 | 
					updates are properly communicated.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Abra does its best to read the "a.b.c" version scheme and communicate what
 | 
				
			||||||
 | 
					action needs to be taken when performing different operations such as an update
 | 
				
			||||||
 | 
					or a rollback of an app.
 | 
				
			||||||
 | 
					`,
 | 
				
			||||||
 | 
						Flags: []cli.Flag{
 | 
				
			||||||
 | 
							DryFlag,
 | 
				
			||||||
 | 
							PatchFlag,
 | 
				
			||||||
 | 
							MinorFlag,
 | 
				
			||||||
 | 
							MajorFlag,
 | 
				
			||||||
 | 
							PushFlag,
 | 
				
			||||||
 | 
							CommitFlag,
 | 
				
			||||||
 | 
							CommitMessageFlag,
 | 
				
			||||||
 | 
							TagMessageFlag,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						Action: func(c *cli.Context) error {
 | 
				
			||||||
 | 
							recipe := internal.ValidateRecipe(c)
 | 
				
			||||||
 | 
							directory := path.Join(config.APPS_DIR, recipe.Name)
 | 
				
			||||||
 | 
							tagstring := c.Args().Get(1)
 | 
				
			||||||
 | 
							imagesTmp := getImageVersions(recipe)
 | 
				
			||||||
 | 
							mainApp := getMainApp(recipe)
 | 
				
			||||||
 | 
							mainAppVersion := imagesTmp[mainApp]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := recipePkg.EnsureExists(recipe.Name); err != nil {
 | 
				
			||||||
 | 
								logrus.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if mainAppVersion == "" {
 | 
				
			||||||
 | 
								logrus.Fatal("main app version is empty?")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if tagstring != "" {
 | 
				
			||||||
 | 
								if _, err := tagcmp.Parse(tagstring); err != nil {
 | 
				
			||||||
 | 
									logrus.Fatal("invalid tag specified")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if TagMessage == "" {
 | 
				
			||||||
 | 
								prompt := &survey.Input{
 | 
				
			||||||
 | 
									Message: "tag message",
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								survey.AskOne(prompt, &TagMessage)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var createTagOptions git.CreateTagOptions
 | 
				
			||||||
 | 
							createTagOptions.Message = TagMessage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if Commit || (CommitMessage != "") {
 | 
				
			||||||
 | 
								commitRepo, err := git.PlainOpen(directory)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									logrus.Fatal(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								commitWorktree, err := commitRepo.Worktree()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									logrus.Fatal(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if CommitMessage == "" {
 | 
				
			||||||
 | 
									prompt := &survey.Input{
 | 
				
			||||||
 | 
										Message: "commit message",
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									survey.AskOne(prompt, &CommitMessage)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								err = commitWorktree.AddGlob("compose.**yml")
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									logrus.Fatal(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								logrus.Debug("staged compose.**yml for commit")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								_, err = commitWorktree.Commit(CommitMessage, &git.CommitOptions{})
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									logrus.Fatal(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								logrus.Info("changes commited")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							repo, err := git.PlainOpen(directory)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logrus.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							head, err := repo.Head()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logrus.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// bumpType is used to decide what part of the tag should be incremented
 | 
				
			||||||
 | 
							bumpType := btoi(Major)*4 + btoi(Minor)*2 + btoi(Patch)
 | 
				
			||||||
 | 
							if bumpType != 0 {
 | 
				
			||||||
 | 
								// a bitwise check if the number is a power of 2
 | 
				
			||||||
 | 
								if (bumpType & (bumpType - 1)) != 0 {
 | 
				
			||||||
 | 
									logrus.Fatal("you can only use one of: --major, --minor, --patch.")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if tagstring != "" {
 | 
				
			||||||
 | 
								if bumpType > 0 {
 | 
				
			||||||
 | 
									logrus.Warn("user specified a version number and --major/--minor/--patch at the same time! using version number...")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								tag, err := tagcmp.Parse(tagstring)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									logrus.Fatal(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if tag.MissingMinor {
 | 
				
			||||||
 | 
									tag.Minor = "0"
 | 
				
			||||||
 | 
									tag.MissingMinor = false
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if tag.MissingPatch {
 | 
				
			||||||
 | 
									tag.Patch = "0"
 | 
				
			||||||
 | 
									tag.MissingPatch = false
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								tagstring = fmt.Sprintf("%s+%s", tag.String(), mainAppVersion)
 | 
				
			||||||
 | 
								if Dry {
 | 
				
			||||||
 | 
									logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", tagstring, head.Hash()))
 | 
				
			||||||
 | 
									return nil
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								repo.CreateTag(tagstring, head.Hash(), &createTagOptions)
 | 
				
			||||||
 | 
								logrus.Info(fmt.Sprintf("created tag %s at %s", tagstring, head.Hash()))
 | 
				
			||||||
 | 
								if Push {
 | 
				
			||||||
 | 
									if err := repo.Push(&git.PushOptions{}); err != nil {
 | 
				
			||||||
 | 
										logrus.Fatal(err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									logrus.Info(fmt.Sprintf("pushed tag %s to remote", tagstring))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// get the latest tag with its hash, name etc
 | 
				
			||||||
 | 
							var lastGitTag tagcmp.Tag
 | 
				
			||||||
 | 
							iter, err := repo.Tags()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logrus.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := iter.ForEach(func(ref *plumbing.Reference) error {
 | 
				
			||||||
 | 
								obj, err := repo.TagObject(ref.Hash())
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								tagcmpTag, err := tagcmp.Parse(obj.Name)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (lastGitTag == tagcmp.Tag{}) {
 | 
				
			||||||
 | 
									lastGitTag = tagcmpTag
 | 
				
			||||||
 | 
								} else if tagcmpTag.IsGreaterThan(lastGitTag) {
 | 
				
			||||||
 | 
									lastGitTag = tagcmpTag
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}); err != nil {
 | 
				
			||||||
 | 
								logrus.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							fmt.Println(lastGitTag)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							newTag := lastGitTag
 | 
				
			||||||
 | 
							var newTagString string
 | 
				
			||||||
 | 
							if bumpType > 0 {
 | 
				
			||||||
 | 
								if Patch {
 | 
				
			||||||
 | 
									now, err := strconv.Atoi(newTag.Patch)
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										logrus.Fatal(err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									newTag.Patch = strconv.Itoa(now + 1)
 | 
				
			||||||
 | 
								} else if Minor {
 | 
				
			||||||
 | 
									now, err := strconv.Atoi(newTag.Minor)
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										logrus.Fatal(err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									newTag.Patch = "0"
 | 
				
			||||||
 | 
									newTag.Minor = strconv.Itoa(now + 1)
 | 
				
			||||||
 | 
								} else if Major {
 | 
				
			||||||
 | 
									now, err := strconv.Atoi(newTag.Major)
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										logrus.Fatal(err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									newTag.Patch = "0"
 | 
				
			||||||
 | 
									newTag.Minor = "0"
 | 
				
			||||||
 | 
									newTag.Major = strconv.Itoa(now + 1)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								logrus.Fatal("we don't support automatic tag generation yet - specify a version or use one of: --major --minor --patch")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							newTag.Metadata = mainAppVersion
 | 
				
			||||||
 | 
							newTagString = newTag.String()
 | 
				
			||||||
 | 
							if Dry {
 | 
				
			||||||
 | 
								logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", newTagString, head.Hash()))
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							repo.CreateTag(newTagString, head.Hash(), &createTagOptions)
 | 
				
			||||||
 | 
							logrus.Info(fmt.Sprintf("created tag %s at %s", newTagString, head.Hash()))
 | 
				
			||||||
 | 
							if Push {
 | 
				
			||||||
 | 
								if err := repo.Push(&git.PushOptions{}); err != nil {
 | 
				
			||||||
 | 
									logrus.Fatal(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								logrus.Info(fmt.Sprintf("pushed tag %s to remote", newTagString))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getImageVersions(recipe recipe.Recipe) map[string]string {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var services = make(map[string]string)
 | 
				
			||||||
 | 
						for _, service := range recipe.Config.Services {
 | 
				
			||||||
 | 
							srv := strings.Split(service.Image, ":")
 | 
				
			||||||
 | 
							services[srv[0]] = srv[1]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return services
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getMainApp(recipe recipe.Recipe) string {
 | 
				
			||||||
 | 
						for _, service := range recipe.Config.Services {
 | 
				
			||||||
 | 
							name := service.Name
 | 
				
			||||||
 | 
							if name == "app" {
 | 
				
			||||||
 | 
								return strings.Split(service.Image, ":")[0]
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func btoi(b bool) int {
 | 
				
			||||||
 | 
						if b {
 | 
				
			||||||
 | 
							return 1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,65 +1,88 @@
 | 
				
			|||||||
package recipe
 | 
					package recipe
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"coopcloud.tech/abra/cli/internal"
 | 
						"coopcloud.tech/abra/cli/internal"
 | 
				
			||||||
	"coopcloud.tech/abra/pkg/client"
 | 
						"coopcloud.tech/abra/pkg/catalogue"
 | 
				
			||||||
	"github.com/docker/distribution/reference"
 | 
						"github.com/AlecAivazis/survey/v2"
 | 
				
			||||||
	"github.com/sirupsen/logrus"
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
	"github.com/urfave/cli/v2"
 | 
						"github.com/urfave/cli/v2"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var recipeSyncCommand = &cli.Command{
 | 
					var recipeSyncCommand = &cli.Command{
 | 
				
			||||||
	Name:    "sync",
 | 
						Name:      "sync",
 | 
				
			||||||
	Usage:   "Generate new recipe labels",
 | 
						Usage:     "Ensure recipe version labels are up-to-date",
 | 
				
			||||||
	Aliases: []string{"s"},
 | 
						Aliases:   []string{"s"},
 | 
				
			||||||
 | 
						ArgsUsage: "<recipe> <version>",
 | 
				
			||||||
	Description: `
 | 
						Description: `
 | 
				
			||||||
This command will generate labels for each service which correspond to the
 | 
					This command will generate labels for the main recipe service (i.e. by
 | 
				
			||||||
 | 
					convention, typically the service named "app") which corresponds to the
 | 
				
			||||||
following format:
 | 
					following format:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    coop-cloud.${STACK_NAME}.${SERVICE_NAME}.version=${IMAGE_TAG}-${IMAGE_DIGEST}
 | 
					    coop-cloud.${STACK_NAME}.version=<version>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The <recipe> configuration will be updated on the local file system. These
 | 
					The <version> is determined by the recipe maintainer and is specified on the
 | 
				
			||||||
labels are consumed by abra in other command invocations and used to determine
 | 
					command-line. The <recipe> configuration will be updated on the local file
 | 
				
			||||||
the versioning metadata of up-and-running containers are.
 | 
					system.
 | 
				
			||||||
`,
 | 
					`,
 | 
				
			||||||
	ArgsUsage: "<recipe>",
 | 
						BashComplete: func(c *cli.Context) {
 | 
				
			||||||
 | 
							catl, err := catalogue.ReadRecipeCatalogue()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logrus.Warn(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if c.NArg() > 0 {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							for name := range catl {
 | 
				
			||||||
 | 
								fmt.Println(name)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
	Action: func(c *cli.Context) error {
 | 
						Action: func(c *cli.Context) error {
 | 
				
			||||||
 | 
							if c.Args().Len() != 2 {
 | 
				
			||||||
 | 
								internal.ShowSubcommandHelpAndError(c, errors.New("missing <recipe>/<version> arguments?"))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		recipe := internal.ValidateRecipe(c)
 | 
							recipe := internal.ValidateRecipe(c)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// TODO: validate with tagcmp when new commits come in
 | 
				
			||||||
 | 
							// See https://git.coopcloud.tech/coop-cloud/abra/pulls/109
 | 
				
			||||||
 | 
							nextTag := c.Args().Get(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							mainService := "app"
 | 
				
			||||||
 | 
							var services []string
 | 
				
			||||||
		hasAppService := false
 | 
							hasAppService := false
 | 
				
			||||||
		for _, service := range recipe.Config.Services {
 | 
							for _, service := range recipe.Config.Services {
 | 
				
			||||||
 | 
								services = append(services, service.Name)
 | 
				
			||||||
			if service.Name == "app" {
 | 
								if service.Name == "app" {
 | 
				
			||||||
				hasAppService = true
 | 
									hasAppService = true
 | 
				
			||||||
 | 
									logrus.Debugf("detected app service in '%s'", recipe.Name)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if !hasAppService {
 | 
							if !hasAppService {
 | 
				
			||||||
			logrus.Fatal(fmt.Sprintf("no 'app' service defined in '%s'", recipe.Name))
 | 
								logrus.Warnf("no 'app' service defined in '%s'", recipe.Name)
 | 
				
			||||||
 | 
								var chosenService string
 | 
				
			||||||
 | 
								prompt := &survey.Select{
 | 
				
			||||||
 | 
									Message: fmt.Sprintf("what is the main service name for '%s'?", recipe.Name),
 | 
				
			||||||
 | 
									Options: services,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err := survey.AskOne(prompt, &chosenService); err != nil {
 | 
				
			||||||
 | 
									logrus.Fatal(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								mainService = chosenService
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		for _, service := range recipe.Config.Services {
 | 
							logrus.Debugf("selecting '%s' as the service to sync version labels", mainService)
 | 
				
			||||||
			img, err := reference.ParseNormalizedNamed(service.Image)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				logrus.Fatal(err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			logrus.Debugf("detected image '%s' for service '%s'", img, service.Name)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			digest, err := client.GetTagDigest(img)
 | 
							label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag)
 | 
				
			||||||
			if err != nil {
 | 
							if err := recipe.UpdateLabel(mainService, label); err != nil {
 | 
				
			||||||
				logrus.Fatal(err)
 | 
								logrus.Fatal(err)
 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			logrus.Debugf("retrieved digest '%s' for '%s'", digest, img)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			tag := img.(reference.NamedTagged).Tag()
 | 
					 | 
				
			||||||
			label := fmt.Sprintf("coop-cloud.${STACK_NAME}.%s.version=%s-%s", service.Name, tag, digest)
 | 
					 | 
				
			||||||
			if err := recipe.UpdateLabel(service.Name, label); err != nil {
 | 
					 | 
				
			||||||
				logrus.Fatal(err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			logrus.Debugf("added label '%s' to service '%s'", label, service.Name)
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							logrus.Infof("synced label '%s' to service '%s'", label, mainService)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -27,20 +27,29 @@ update the relevant compose file tags on the local file system.
 | 
				
			|||||||
Some image tags cannot be parsed because they do not follow some sort of
 | 
					Some image tags cannot be parsed because they do not follow some sort of
 | 
				
			||||||
semver-like convention. In this case, all possible tags will be listed and it
 | 
					semver-like convention. In this case, all possible tags will be listed and it
 | 
				
			||||||
is up to the end-user to decide.
 | 
					is up to the end-user to decide.
 | 
				
			||||||
 | 
					 | 
				
			||||||
This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync
 | 
					 | 
				
			||||||
<recipe>".
 | 
					 | 
				
			||||||
`,
 | 
					`,
 | 
				
			||||||
	ArgsUsage: "<recipe>",
 | 
						ArgsUsage: "<recipe>",
 | 
				
			||||||
 | 
						Flags: []cli.Flag{
 | 
				
			||||||
 | 
							PatchFlag,
 | 
				
			||||||
 | 
							MinorFlag,
 | 
				
			||||||
 | 
							MajorFlag,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
	Action: func(c *cli.Context) error {
 | 
						Action: func(c *cli.Context) error {
 | 
				
			||||||
		recipe := internal.ValidateRecipe(c)
 | 
							recipe := internal.ValidateRecipe(c)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							bumpType := btoi(Major)*4 + btoi(Minor)*2 + btoi(Patch)
 | 
				
			||||||
 | 
							if bumpType != 0 {
 | 
				
			||||||
 | 
								// a bitwise check if the number is a power of 2
 | 
				
			||||||
 | 
								if (bumpType & (bumpType - 1)) != 0 {
 | 
				
			||||||
 | 
									logrus.Fatal("you can only use one of: --major, --minor, --patch.")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		for _, service := range recipe.Config.Services {
 | 
							for _, service := range recipe.Config.Services {
 | 
				
			||||||
			catlVersions, err := catalogue.VersionsOfService(recipe.Name, service.Name)
 | 
								catlVersions, err := catalogue.VersionsOfService(recipe.Name, service.Name)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				logrus.Fatal(err)
 | 
									logrus.Fatal(err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			logrus.Debugf("read '%s' from the recipe catalogue for '%s'", catlVersions, service.Name)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			img, err := reference.ParseNormalizedNamed(service.Image)
 | 
								img, err := reference.ParseNormalizedNamed(service.Image)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
@ -108,31 +117,48 @@ This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			logrus.Debugf("detected compatible upgradable tags '%s' for '%s'", compatibleStrings, service.Name)
 | 
								logrus.Debugf("detected compatible upgradable tags '%s' for '%s'", compatibleStrings, service.Name)
 | 
				
			||||||
 | 
					 | 
				
			||||||
			msg := fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
 | 
					 | 
				
			||||||
			if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
 | 
					 | 
				
			||||||
				tag := img.(reference.NamedTagged).Tag()
 | 
					 | 
				
			||||||
				logrus.Warning(fmt.Sprintf("unable to determine versioning semantics of '%s', listing all tags", tag))
 | 
					 | 
				
			||||||
				msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
 | 
					 | 
				
			||||||
				compatibleStrings = []string{}
 | 
					 | 
				
			||||||
				for _, regVersion := range regVersions {
 | 
					 | 
				
			||||||
					compatibleStrings = append(compatibleStrings, regVersion.Name)
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			var upgradeTag string
 | 
								var upgradeTag string
 | 
				
			||||||
			prompt := &survey.Select{
 | 
								if bumpType != 0 {
 | 
				
			||||||
				Message: msg,
 | 
									for _, upTag := range compatible {
 | 
				
			||||||
				Options: compatibleStrings,
 | 
										upElement, err := tag.UpgradeDelta(upTag)
 | 
				
			||||||
			}
 | 
										if err != nil {
 | 
				
			||||||
			if err := survey.AskOne(prompt, &upgradeTag); err != nil {
 | 
											return err
 | 
				
			||||||
				logrus.Fatal(err)
 | 
										}
 | 
				
			||||||
 | 
										delta := upElement.UpgradeType()
 | 
				
			||||||
 | 
										if delta <= bumpType {
 | 
				
			||||||
 | 
											upgradeTag = upTag.String()
 | 
				
			||||||
 | 
											break
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if upgradeTag == "" {
 | 
				
			||||||
 | 
										logrus.Warnf("not upgrading from '%s' to '%s' for '%s', because the upgrade type is more serious than what user wants.", tag.String(), compatible[0].String(), image)
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									msg := fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
 | 
				
			||||||
 | 
									if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
 | 
				
			||||||
 | 
										tag := img.(reference.NamedTagged).Tag()
 | 
				
			||||||
 | 
										logrus.Warning(fmt.Sprintf("unable to determine versioning semantics of '%s', listing all tags", tag))
 | 
				
			||||||
 | 
										msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
 | 
				
			||||||
 | 
										compatibleStrings = []string{}
 | 
				
			||||||
 | 
										for _, regVersion := range regVersions {
 | 
				
			||||||
 | 
											compatibleStrings = append(compatibleStrings, regVersion.Name)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									prompt := &survey.Select{
 | 
				
			||||||
 | 
										Message: msg,
 | 
				
			||||||
 | 
										Options: compatibleStrings,
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if err := survey.AskOne(prompt, &upgradeTag); err != nil {
 | 
				
			||||||
 | 
										logrus.Fatal(err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if err := recipe.UpdateTag(image, upgradeTag); err != nil {
 | 
								if err := recipe.UpdateTag(image, upgradeTag); err != nil {
 | 
				
			||||||
				logrus.Fatal(err)
 | 
									logrus.Fatal(err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			logrus.Debugf("tag updated from '%s' to '%s' for '%s'", image, upgradeTag, recipe.Name)
 | 
								logrus.Infof("tag upgraded from '%s' to '%s' for '%s'", tag.String(), upgradeTag, image)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
 | 
				
			|||||||
@ -2,40 +2,78 @@ package server
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"os/user"
 | 
						"os/user"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"coopcloud.tech/abra/cli/internal"
 | 
						"coopcloud.tech/abra/cli/internal"
 | 
				
			||||||
	"coopcloud.tech/abra/pkg/client"
 | 
						"coopcloud.tech/abra/pkg/client"
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/pkg/server"
 | 
				
			||||||
	"github.com/sirupsen/logrus"
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
	"github.com/urfave/cli/v2"
 | 
						"github.com/urfave/cli/v2"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var local bool
 | 
				
			||||||
 | 
					var localFlag = &cli.BoolFlag{
 | 
				
			||||||
 | 
						Name:        "local",
 | 
				
			||||||
 | 
						Aliases:     []string{"L"},
 | 
				
			||||||
 | 
						Value:       false,
 | 
				
			||||||
 | 
						Usage:       "Set up the local server",
 | 
				
			||||||
 | 
						Destination: &local,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var serverAddCommand = &cli.Command{
 | 
					var serverAddCommand = &cli.Command{
 | 
				
			||||||
	Name:  "add",
 | 
						Name:  "add",
 | 
				
			||||||
	Usage: "Add a new server",
 | 
						Usage: "Add a new server",
 | 
				
			||||||
	Description: `
 | 
						Description: `
 | 
				
			||||||
This command adds a new server that abra will communicate with, to deploy apps.
 | 
					This command adds a new server that abra will communicate with, to deploy apps.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The <domain> argument must be a publicy accessible domain name which points to
 | 
					If "--local" is passed, then Abra assumes that the current local server is
 | 
				
			||||||
your server. You should have SSH access to this server, Abra will assume port
 | 
					intended as the target server.
 | 
				
			||||||
22 and will use your current system username to make an initial connection. You
 | 
					
 | 
				
			||||||
can use the <user> and <port> arguments to adjust this.
 | 
					Otherwise, you may specify a remote server. The <domain> argument must be a
 | 
				
			||||||
 | 
					publicy accessible domain name which points to your server. You should have SSH
 | 
				
			||||||
 | 
					access to this server, Abra will assume port 22 and will use your current
 | 
				
			||||||
 | 
					system username to make an initial connection. You can use the <user> and
 | 
				
			||||||
 | 
					<port> arguments to adjust this.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
For example:
 | 
					For example:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    abra server add varia.zone 12345 glodemodem
 | 
					    abra server add varia.zone glodemodem 12345
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Abra will construct the following SSH connection string then:
 | 
					Abra will construct the following SSH connection string then:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ssh://globemodem@varia.zone:12345
 | 
					    ssh://globemodem@varia.zone:12345
 | 
				
			||||||
 | 
					
 | 
				
			||||||
All communication between Abra and the server will use this SSH connection.
 | 
					All communication between Abra and the server will use this SSH connection.
 | 
				
			||||||
 | 
					 | 
				
			||||||
`,
 | 
					`,
 | 
				
			||||||
	Aliases:   []string{"a"},
 | 
						Aliases: []string{"a"},
 | 
				
			||||||
 | 
						Flags: []cli.Flag{
 | 
				
			||||||
 | 
							localFlag,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
	ArgsUsage: "<domain> [<user>] [<port>]",
 | 
						ArgsUsage: "<domain> [<user>] [<port>]",
 | 
				
			||||||
	Action: func(c *cli.Context) error {
 | 
						Action: func(c *cli.Context) error {
 | 
				
			||||||
		domainName := internal.ValidateDomain(c)
 | 
							if c.Args().Len() == 1 && !local {
 | 
				
			||||||
 | 
								err := errors.New("missing arguments <domain> or '--local'")
 | 
				
			||||||
 | 
								internal.ShowSubcommandHelpAndError(c, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if c.Args().Get(1) != "" && local {
 | 
				
			||||||
 | 
								err := errors.New("cannot use '<domain>' and '--local' together")
 | 
				
			||||||
 | 
								internal.ShowSubcommandHelpAndError(c, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							domainName := "default"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if local {
 | 
				
			||||||
 | 
								if err := server.CreateServerDir(domainName); err != nil {
 | 
				
			||||||
 | 
									logrus.Fatal(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								logrus.Info("local server has been added")
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							domainName = internal.ValidateDomain(c)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		var username string
 | 
							var username string
 | 
				
			||||||
		var port string
 | 
							var port string
 | 
				
			||||||
@ -79,11 +117,20 @@ All communication between Abra and the server will use this SSH connection.
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if _, err := cl.Info(ctx); err != nil {
 | 
							if _, err := cl.Info(ctx); err != nil {
 | 
				
			||||||
			logrus.Fatalf("unable to make a connection to '%s'?", domainName)
 | 
								if strings.Contains(err.Error(), "command not found") {
 | 
				
			||||||
 | 
									logrus.Fatalf("docker is not installed on '%s'?", domainName)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									logrus.Fatalf("unable to make a connection to '%s'?", domainName)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
			logrus.Debug(err)
 | 
								logrus.Debug(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		logrus.Debugf("remote connection to '%s' is definitely up", domainName)
 | 
							logrus.Debugf("remote connection to '%s' is definitely up", domainName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := server.CreateServerDir(domainName); err != nil {
 | 
				
			||||||
 | 
								logrus.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		logrus.Infof("server at '%s' has been added", domainName)
 | 
							logrus.Infof("server at '%s' has been added", domainName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
 | 
				
			|||||||
@ -35,6 +35,9 @@ later for more advanced use cases.
 | 
				
			|||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// https://www.privacy-handbuch.de/handbuch_93d.htm
 | 
				
			||||||
 | 
							freifunkDNS := "5.1.66.255:53"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		resolver := &net.Resolver{
 | 
							resolver := &net.Resolver{
 | 
				
			||||||
			PreferGo: false,
 | 
								PreferGo: false,
 | 
				
			||||||
			Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
 | 
								Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
 | 
				
			||||||
@ -42,10 +45,10 @@ later for more advanced use cases.
 | 
				
			|||||||
					Timeout: time.Millisecond * time.Duration(10000),
 | 
										Timeout: time.Millisecond * time.Duration(10000),
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				// comrade librehosters DNS resolver https://snopyta.org/service/dns/
 | 
									// comrade librehosters DNS resolver https://snopyta.org/service/dns/
 | 
				
			||||||
				return d.DialContext(ctx, "udp", "95.216.24.230:53")
 | 
									return d.DialContext(ctx, "udp", freifunkDNS)
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		logrus.Debugf("created DNS resolver via 95.216.24.230")
 | 
							logrus.Debugf("created DNS resolver via '%s'", freifunkDNS)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx := context.Background()
 | 
							ctx := context.Background()
 | 
				
			||||||
		ips, err := resolver.LookupIPAddr(ctx, domainName)
 | 
							ips, err := resolver.LookupIPAddr(ctx, domainName)
 | 
				
			||||||
 | 
				
			|||||||
@ -45,7 +45,11 @@ var serverListCommand = &cli.Command{
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if len(row) == 0 {
 | 
								if len(row) == 0 {
 | 
				
			||||||
				row = []string{serverName, "UNKNOWN"}
 | 
									if serverName == "default" {
 | 
				
			||||||
 | 
										row = []string{serverName, "local"}
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										row = []string{serverName, "unknown"}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			table.Append(row)
 | 
								table.Append(row)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										64
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										64
									
								
								go.mod
									
									
									
									
									
								
							@ -1,11 +1,12 @@
 | 
				
			|||||||
module coopcloud.tech/abra
 | 
					module coopcloud.tech/abra
 | 
				
			||||||
 | 
					
 | 
				
			||||||
go 1.17
 | 
					go 1.16
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require (
 | 
					require (
 | 
				
			||||||
	coopcloud.tech/tagcmp v0.0.0-20210906102006-2a8edd82d75d
 | 
						coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb
 | 
				
			||||||
	github.com/AlecAivazis/survey/v2 v2.3.1
 | 
						github.com/AlecAivazis/survey/v2 v2.3.1
 | 
				
			||||||
	github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4
 | 
						github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4
 | 
				
			||||||
 | 
						github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
 | 
				
			||||||
	github.com/docker/cli v20.10.8+incompatible
 | 
						github.com/docker/cli v20.10.8+incompatible
 | 
				
			||||||
	github.com/docker/distribution v2.7.1+incompatible
 | 
						github.com/docker/distribution v2.7.1+incompatible
 | 
				
			||||||
	github.com/docker/docker v20.10.8+incompatible
 | 
						github.com/docker/docker v20.10.8+incompatible
 | 
				
			||||||
@ -16,6 +17,7 @@ require (
 | 
				
			|||||||
	github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
 | 
						github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
 | 
				
			||||||
	github.com/olekukonko/tablewriter v0.0.5
 | 
						github.com/olekukonko/tablewriter v0.0.5
 | 
				
			||||||
	github.com/pkg/errors v0.9.1
 | 
						github.com/pkg/errors v0.9.1
 | 
				
			||||||
 | 
						github.com/schollz/progressbar/v3 v3.8.3
 | 
				
			||||||
	github.com/schultz-is/passgen v1.0.1
 | 
						github.com/schultz-is/passgen v1.0.1
 | 
				
			||||||
	github.com/sirupsen/logrus v1.8.1
 | 
						github.com/sirupsen/logrus v1.8.1
 | 
				
			||||||
	github.com/urfave/cli/v2 v2.3.0
 | 
						github.com/urfave/cli/v2 v2.3.0
 | 
				
			||||||
@ -23,72 +25,16 @@ require (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require (
 | 
					require (
 | 
				
			||||||
	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
 | 
					 | 
				
			||||||
	github.com/Microsoft/go-winio v0.4.17 // indirect
 | 
					 | 
				
			||||||
	github.com/Microsoft/hcsshim v0.8.21 // indirect
 | 
						github.com/Microsoft/hcsshim v0.8.21 // indirect
 | 
				
			||||||
	github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect
 | 
					 | 
				
			||||||
	github.com/acomagu/bufpipe v1.0.3 // indirect
 | 
					 | 
				
			||||||
	github.com/beorn7/perks v1.0.1 // indirect
 | 
					 | 
				
			||||||
	github.com/cespare/xxhash/v2 v2.1.1 // indirect
 | 
					 | 
				
			||||||
	github.com/containerd/cgroups v1.0.1 // indirect
 | 
					 | 
				
			||||||
	github.com/containerd/containerd v1.5.5 // indirect
 | 
						github.com/containerd/containerd v1.5.5 // indirect
 | 
				
			||||||
	github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
 | 
					 | 
				
			||||||
	github.com/docker/docker-credential-helpers v0.6.4 // indirect
 | 
						github.com/docker/docker-credential-helpers v0.6.4 // indirect
 | 
				
			||||||
	github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
 | 
						github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
 | 
				
			||||||
	github.com/docker/go-connections v0.4.0 // indirect
 | 
					 | 
				
			||||||
	github.com/docker/go-metrics v0.0.1 // indirect
 | 
					 | 
				
			||||||
	github.com/emirpasic/gods v1.12.0 // indirect
 | 
					 | 
				
			||||||
	github.com/fvbommel/sortorder v1.0.2 // indirect
 | 
						github.com/fvbommel/sortorder v1.0.2 // indirect
 | 
				
			||||||
	github.com/go-git/gcfg v1.5.0 // indirect
 | 
					 | 
				
			||||||
	github.com/go-git/go-billy/v5 v5.3.1 // indirect
 | 
					 | 
				
			||||||
	github.com/gogo/protobuf v1.3.2 // indirect
 | 
					 | 
				
			||||||
	github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
 | 
					 | 
				
			||||||
	github.com/golang/protobuf v1.5.0 // indirect
 | 
					 | 
				
			||||||
	github.com/google/go-cmp v0.5.5 // indirect
 | 
					 | 
				
			||||||
	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 | 
						github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 | 
				
			||||||
	github.com/gorilla/mux v1.8.0 // indirect
 | 
						github.com/gorilla/mux v1.8.0 // indirect
 | 
				
			||||||
	github.com/imdario/mergo v0.3.12 // indirect
 | 
					 | 
				
			||||||
	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 | 
					 | 
				
			||||||
	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
 | 
					 | 
				
			||||||
	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
 | 
					 | 
				
			||||||
	github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
 | 
					 | 
				
			||||||
	github.com/mattn/go-colorable v0.1.2 // indirect
 | 
					 | 
				
			||||||
	github.com/mattn/go-isatty v0.0.8 // indirect
 | 
					 | 
				
			||||||
	github.com/mattn/go-runewidth v0.0.9 // indirect
 | 
					 | 
				
			||||||
	github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
 | 
					 | 
				
			||||||
	github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
 | 
					 | 
				
			||||||
	github.com/miekg/pkcs11 v1.0.3 // indirect
 | 
					 | 
				
			||||||
	github.com/mitchellh/go-homedir v1.1.0 // indirect
 | 
					 | 
				
			||||||
	github.com/mitchellh/mapstructure v1.1.2 // indirect
 | 
					 | 
				
			||||||
	github.com/moby/sys/mount v0.2.0 // indirect
 | 
						github.com/moby/sys/mount v0.2.0 // indirect
 | 
				
			||||||
	github.com/moby/sys/mountinfo v0.4.1 // indirect
 | 
					 | 
				
			||||||
	github.com/morikuni/aec v1.0.0 // indirect
 | 
						github.com/morikuni/aec v1.0.0 // indirect
 | 
				
			||||||
	github.com/opencontainers/go-digest v1.0.0 // indirect
 | 
					 | 
				
			||||||
	github.com/opencontainers/image-spec v1.0.1 // indirect
 | 
					 | 
				
			||||||
	github.com/opencontainers/runc v1.0.2 // indirect
 | 
						github.com/opencontainers/runc v1.0.2 // indirect
 | 
				
			||||||
	github.com/prometheus/client_golang v1.11.0 // indirect
 | 
					 | 
				
			||||||
	github.com/prometheus/client_model v0.2.0 // indirect
 | 
					 | 
				
			||||||
	github.com/prometheus/common v0.26.0 // indirect
 | 
					 | 
				
			||||||
	github.com/prometheus/procfs v0.6.0 // indirect
 | 
					 | 
				
			||||||
	github.com/russross/blackfriday/v2 v2.0.1 // indirect
 | 
					 | 
				
			||||||
	github.com/sergi/go-diff v1.1.0 // indirect
 | 
					 | 
				
			||||||
	github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
 | 
					 | 
				
			||||||
	github.com/spf13/cobra v1.0.0 // indirect
 | 
					 | 
				
			||||||
	github.com/spf13/pflag v1.0.5 // indirect
 | 
					 | 
				
			||||||
	github.com/theupdateframework/notary v0.7.0 // indirect
 | 
						github.com/theupdateframework/notary v0.7.0 // indirect
 | 
				
			||||||
	github.com/xanzy/ssh-agent v0.3.0 // indirect
 | 
					 | 
				
			||||||
	github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
 | 
					 | 
				
			||||||
	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
 | 
					 | 
				
			||||||
	github.com/xeipuuv/gojsonschema v1.2.0 // indirect
 | 
						github.com/xeipuuv/gojsonschema v1.2.0 // indirect
 | 
				
			||||||
	go.opencensus.io v0.22.3 // indirect
 | 
					 | 
				
			||||||
	golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
 | 
					 | 
				
			||||||
	golang.org/x/net v0.0.0-20210326060303-6b1517762897 // indirect
 | 
					 | 
				
			||||||
	golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
 | 
					 | 
				
			||||||
	golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect
 | 
					 | 
				
			||||||
	golang.org/x/text v0.3.4 // indirect
 | 
					 | 
				
			||||||
	google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect
 | 
					 | 
				
			||||||
	google.golang.org/grpc v1.33.2 // indirect
 | 
					 | 
				
			||||||
	google.golang.org/protobuf v1.26.0 // indirect
 | 
					 | 
				
			||||||
	gopkg.in/warnings.v0 v0.1.2 // indirect
 | 
					 | 
				
			||||||
	gopkg.in/yaml.v2 v2.4.0 // indirect
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										31
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								go.sum
									
									
									
									
									
								
							@ -21,8 +21,8 @@ cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIA
 | 
				
			|||||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
 | 
					cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
 | 
				
			||||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
 | 
					cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
 | 
				
			||||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 | 
					cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 | 
				
			||||||
coopcloud.tech/tagcmp v0.0.0-20210906102006-2a8edd82d75d h1:5jeUIiToqQ7vTlLeycdGp4Ezurd6/RTNl5K38usHtoo=
 | 
					coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb h1:Jf+Dnna2kXcNQvcA5JMp6d2Uyvg2pIVJfip9+X5FrH0=
 | 
				
			||||||
coopcloud.tech/tagcmp v0.0.0-20210906102006-2a8edd82d75d/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
 | 
					coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
 | 
				
			||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 | 
					dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 | 
				
			||||||
github.com/AlecAivazis/survey/v2 v2.3.1 h1:lzkuHA60pER7L4eYL8qQJor4bUWlJe4V0gqAT19tdOA=
 | 
					github.com/AlecAivazis/survey/v2 v2.3.1 h1:lzkuHA60pER7L4eYL8qQJor4bUWlJe4V0gqAT19tdOA=
 | 
				
			||||||
github.com/AlecAivazis/survey/v2 v2.3.1/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg=
 | 
					github.com/AlecAivazis/survey/v2 v2.3.1/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg=
 | 
				
			||||||
@ -44,6 +44,8 @@ github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ
 | 
				
			|||||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
 | 
					github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
 | 
				
			||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 | 
					github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 | 
				
			||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 | 
					github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 | 
				
			||||||
 | 
					github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 h1:vdT7QwBhJJEVNFMBNhRSFDRCB6O16T28VhvqRgqFyn8=
 | 
				
			||||||
 | 
					github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4/go.mod h1:SvXOG8ElV28oAiG9zv91SDe5+9PfIr7PPccpr8YyXNs=
 | 
				
			||||||
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
 | 
					github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
 | 
				
			||||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
 | 
					github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
 | 
				
			||||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
 | 
					github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
 | 
				
			||||||
@ -295,6 +297,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
 | 
				
			|||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 | 
					github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 | 
				
			||||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
 | 
					github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
 | 
				
			||||||
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 | 
					github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 | 
				
			||||||
 | 
					github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
 | 
				
			||||||
 | 
					github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
 | 
				
			||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 | 
					github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 | 
				
			||||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
 | 
					github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
 | 
				
			||||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
 | 
					github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
 | 
				
			||||||
@ -479,6 +483,7 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
 | 
				
			|||||||
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
 | 
					github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
 | 
				
			||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 | 
					github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 | 
				
			||||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 | 
					github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 | 
				
			||||||
 | 
					github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
 | 
				
			||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
 | 
					github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
 | 
				
			||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
 | 
					github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
 | 
				
			||||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
 | 
					github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
 | 
				
			||||||
@ -518,11 +523,13 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
 | 
				
			|||||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
 | 
					github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
 | 
				
			||||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 | 
					github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 | 
				
			||||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 | 
					github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 | 
				
			||||||
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
 | 
					 | 
				
			||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 | 
					github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 | 
				
			||||||
 | 
					github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
 | 
				
			||||||
 | 
					github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 | 
				
			||||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 | 
					github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 | 
				
			||||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
 | 
					 | 
				
			||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 | 
					github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 | 
				
			||||||
 | 
					github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 | 
				
			||||||
 | 
					github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 | 
				
			||||||
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
 | 
					github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
 | 
				
			||||||
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 | 
					github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 | 
				
			||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 | 
					github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 | 
				
			||||||
@ -534,6 +541,8 @@ github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WT
 | 
				
			|||||||
github.com/miekg/pkcs11 v1.0.3 h1:iMwmD7I5225wv84WxIG/bmxz9AXjWvTWIbM/TYHvWtw=
 | 
					github.com/miekg/pkcs11 v1.0.3 h1:iMwmD7I5225wv84WxIG/bmxz9AXjWvTWIbM/TYHvWtw=
 | 
				
			||||||
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
 | 
					github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
 | 
				
			||||||
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
 | 
					github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
 | 
				
			||||||
 | 
					github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
 | 
				
			||||||
 | 
					github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
 | 
				
			||||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 | 
					github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 | 
				
			||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 | 
					github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 | 
				
			||||||
github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 | 
					github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 | 
				
			||||||
@ -662,12 +671,16 @@ github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
 | 
				
			|||||||
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
 | 
					github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
 | 
				
			||||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
 | 
					github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
 | 
				
			||||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 | 
					github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 | 
				
			||||||
 | 
					github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 | 
				
			||||||
 | 
					github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 | 
				
			||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 | 
					github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 | 
				
			||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 | 
					github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 | 
				
			||||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
 | 
					github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
 | 
				
			||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 | 
					github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 | 
				
			||||||
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
 | 
					github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
 | 
				
			||||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
 | 
					github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
 | 
				
			||||||
 | 
					github.com/schollz/progressbar/v3 v3.8.3 h1:FnLGl3ewlDUP+YdSwveXBaXs053Mem/du+wr7XSYKl8=
 | 
				
			||||||
 | 
					github.com/schollz/progressbar/v3 v3.8.3/go.mod h1:pWnVCjSBZsT2X3nx9HfRdnCDrpbevliMeoEVhStwHko=
 | 
				
			||||||
github.com/schultz-is/passgen v1.0.1 h1:wUINzqW1Xmmy3yREHR6YTj+83VlFYjj2DIDMHzIi5TQ=
 | 
					github.com/schultz-is/passgen v1.0.1 h1:wUINzqW1Xmmy3yREHR6YTj+83VlFYjj2DIDMHzIi5TQ=
 | 
				
			||||||
github.com/schultz-is/passgen v1.0.1/go.mod h1:NnqzT2aSfvyheNQvBtlLUa0YlPFLDj60Jw2DZVwqiJk=
 | 
					github.com/schultz-is/passgen v1.0.1/go.mod h1:NnqzT2aSfvyheNQvBtlLUa0YlPFLDj60Jw2DZVwqiJk=
 | 
				
			||||||
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
 | 
					github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
 | 
				
			||||||
@ -798,8 +811,9 @@ golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPh
 | 
				
			|||||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
					golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
				
			||||||
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 | 
					golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 | 
				
			||||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 | 
					golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 | 
				
			||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
 | 
					 | 
				
			||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 | 
					golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 | 
				
			||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 | 
					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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 | 
				
			||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 | 
					golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 | 
				
			||||||
@ -947,13 +961,16 @@ golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7w
 | 
				
			|||||||
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
					golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
					golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
					golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
					golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
 | 
					 | 
				
			||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
					golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 h1:xrCZDmdtoloIiooiA9q0OQb9r8HejIHYoHGhGCe1pGg=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 | 
					golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 | 
				
			||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 | 
					golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 | 
				
			||||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
 | 
					 | 
				
			||||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
 | 
					golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
 | 
				
			||||||
 | 
					golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
 | 
				
			||||||
 | 
					golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 | 
				
			||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
					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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
				
			||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
					golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
				
			||||||
 | 
				
			|||||||
@ -9,18 +9,26 @@ import (
 | 
				
			|||||||
	"io/ioutil"
 | 
						"io/ioutil"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/pkg/client"
 | 
				
			||||||
	"coopcloud.tech/abra/pkg/config"
 | 
						"coopcloud.tech/abra/pkg/config"
 | 
				
			||||||
	"coopcloud.tech/abra/pkg/recipe"
 | 
						"coopcloud.tech/abra/pkg/recipe"
 | 
				
			||||||
	"coopcloud.tech/abra/pkg/web"
 | 
						"coopcloud.tech/abra/pkg/web"
 | 
				
			||||||
 | 
						"github.com/docker/distribution/reference"
 | 
				
			||||||
 | 
						"github.com/go-git/go-git/v5"
 | 
				
			||||||
 | 
						"github.com/go-git/go-git/v5/plumbing"
 | 
				
			||||||
	"github.com/sirupsen/logrus"
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// RecipeCatalogueURL is the only current recipe catalogue available.
 | 
					// RecipeCatalogueURL is the only current recipe catalogue available.
 | 
				
			||||||
const RecipeCatalogueURL = "https://apps.coopcloud.tech"
 | 
					const RecipeCatalogueURL = "https://apps.coopcloud.tech"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ReposMetadataURL is the recipe repository metadata
 | 
				
			||||||
 | 
					const ReposMetadataURL = "https://git.coopcloud.tech/api/v1/orgs/coop-cloud/repos"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// image represents a recipe container image.
 | 
					// image represents a recipe container image.
 | 
				
			||||||
type image struct {
 | 
					type image struct {
 | 
				
			||||||
	Image  string `json:"image"`
 | 
						Image  string `json:"image"`
 | 
				
			||||||
@ -46,24 +54,27 @@ type tag = string
 | 
				
			|||||||
// service represents a service within a recipe.
 | 
					// service represents a service within a recipe.
 | 
				
			||||||
type service = string
 | 
					type service = string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// serviceMeta represents meta info associated with a service.
 | 
					// ServiceMeta represents meta info associated with a service.
 | 
				
			||||||
type serviceMeta struct {
 | 
					type ServiceMeta struct {
 | 
				
			||||||
	Digest string `json:"digest"`
 | 
						Digest string `json:"digest"`
 | 
				
			||||||
	Image  string `json:"image"`
 | 
						Image  string `json:"image"`
 | 
				
			||||||
	Tag    string `json:"tag"`
 | 
						Tag    string `json:"tag"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecipeVersions are the versions associated with a recipe.
 | 
				
			||||||
 | 
					type RecipeVersions []map[tag]map[service]ServiceMeta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// RecipeMeta represents metadata for a recipe in the abra catalogue.
 | 
					// RecipeMeta represents metadata for a recipe in the abra catalogue.
 | 
				
			||||||
type RecipeMeta struct {
 | 
					type RecipeMeta struct {
 | 
				
			||||||
	Category      string                            `json:"category"`
 | 
						Category      string         `json:"category"`
 | 
				
			||||||
	DefaultBranch string                            `json:"default_branch"`
 | 
						DefaultBranch string         `json:"default_branch"`
 | 
				
			||||||
	Description   string                            `json:"description"`
 | 
						Description   string         `json:"description"`
 | 
				
			||||||
	Features      features                          `json:"features"`
 | 
						Features      features       `json:"features"`
 | 
				
			||||||
	Icon          string                            `json:"icon"`
 | 
						Icon          string         `json:"icon"`
 | 
				
			||||||
	Name          string                            `json:"name"`
 | 
						Name          string         `json:"name"`
 | 
				
			||||||
	Repository    string                            `json:"repository"`
 | 
						Repository    string         `json:"repository"`
 | 
				
			||||||
	Versions      []map[tag]map[service]serviceMeta `json:"versions"`
 | 
						Versions      RecipeVersions `json:"versions"`
 | 
				
			||||||
	Website       string                            `json:"website"`
 | 
						Website       string         `json:"website"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LatestVersion returns the latest version of a recipe.
 | 
					// LatestVersion returns the latest version of a recipe.
 | 
				
			||||||
@ -255,3 +266,235 @@ func GetRecipeMeta(recipeName string) (RecipeMeta, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return recipeMeta, nil
 | 
						return recipeMeta, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RepoMeta is a single recipe repo metadata.
 | 
				
			||||||
 | 
					type RepoMeta struct {
 | 
				
			||||||
 | 
						ID                        int `json:"id"`
 | 
				
			||||||
 | 
						Owner                     Owner
 | 
				
			||||||
 | 
						Name                      string      `json:"name"`
 | 
				
			||||||
 | 
						FullName                  string      `json:"full_name"`
 | 
				
			||||||
 | 
						Description               string      `json:"description"`
 | 
				
			||||||
 | 
						Empty                     bool        `json:"empty"`
 | 
				
			||||||
 | 
						Private                   bool        `json:"private"`
 | 
				
			||||||
 | 
						Fork                      bool        `json:"fork"`
 | 
				
			||||||
 | 
						Template                  bool        `json:"template"`
 | 
				
			||||||
 | 
						Parent                    interface{} `json:"parent"`
 | 
				
			||||||
 | 
						Mirror                    bool        `json:"mirror"`
 | 
				
			||||||
 | 
						Size                      int         `json:"size"`
 | 
				
			||||||
 | 
						HTMLURL                   string      `json:"html_url"`
 | 
				
			||||||
 | 
						SSHURL                    string      `json:"ssh_url"`
 | 
				
			||||||
 | 
						CloneURL                  string      `json:"clone_url"`
 | 
				
			||||||
 | 
						OriginalURL               string      `json:"original_url"`
 | 
				
			||||||
 | 
						Website                   string      `json:"website"`
 | 
				
			||||||
 | 
						StarsCount                int         `json:"stars_count"`
 | 
				
			||||||
 | 
						ForksCount                int         `json:"forks_count"`
 | 
				
			||||||
 | 
						WatchersCount             int         `json:"watchers_count"`
 | 
				
			||||||
 | 
						OpenIssuesCount           int         `json:"open_issues_count"`
 | 
				
			||||||
 | 
						OpenPRCount               int         `json:"open_pr_counter"`
 | 
				
			||||||
 | 
						ReleaseCounter            int         `json:"release_counter"`
 | 
				
			||||||
 | 
						DefaultBranch             string      `json:"default_branch"`
 | 
				
			||||||
 | 
						Archived                  bool        `json:"archived"`
 | 
				
			||||||
 | 
						CreatedAt                 string      `json:"created_at"`
 | 
				
			||||||
 | 
						UpdatedAt                 string      `json:"updated_at"`
 | 
				
			||||||
 | 
						Permissions               Permissions
 | 
				
			||||||
 | 
						HasIssues                 bool `json:"has_issues"`
 | 
				
			||||||
 | 
						InternalTracker           InternalTracker
 | 
				
			||||||
 | 
						HasWiki                   bool   `json:"has_wiki"`
 | 
				
			||||||
 | 
						HasPullRequests           bool   `json:"has_pull_requests"`
 | 
				
			||||||
 | 
						HasProjects               bool   `json:"has_projects"`
 | 
				
			||||||
 | 
						IgnoreWhitespaceConflicts bool   `json:"ignore_whitespace_conflicts"`
 | 
				
			||||||
 | 
						AllowMergeCommits         bool   `json:"allow_merge_commits"`
 | 
				
			||||||
 | 
						AllowRebase               bool   `json:"allow_rebase"`
 | 
				
			||||||
 | 
						AllowRebaseExplicit       bool   `json:"allow_rebase_explicit"`
 | 
				
			||||||
 | 
						AllowSquashMerge          bool   `json:"allow_squash_merge"`
 | 
				
			||||||
 | 
						AvatarURL                 string `json:"avatar_url"`
 | 
				
			||||||
 | 
						Internal                  bool   `json:"internal"`
 | 
				
			||||||
 | 
						MirrorInterval            string `json:"mirror_interval"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Owner is the repo organisation owner metadata.
 | 
				
			||||||
 | 
					type Owner struct {
 | 
				
			||||||
 | 
						ID         int    `json:"id"`
 | 
				
			||||||
 | 
						Login      string `json:"login"`
 | 
				
			||||||
 | 
						FullName   string `json:"full_name"`
 | 
				
			||||||
 | 
						Email      string `json:"email"`
 | 
				
			||||||
 | 
						AvatarURL  string `json:"avatar_url"`
 | 
				
			||||||
 | 
						Language   string `json:"language"`
 | 
				
			||||||
 | 
						IsAdmin    bool   `json:"is_admin"`
 | 
				
			||||||
 | 
						LastLogin  string `json:"last_login"`
 | 
				
			||||||
 | 
						Created    string `json:"created"`
 | 
				
			||||||
 | 
						Restricted bool   `json:"restricted"`
 | 
				
			||||||
 | 
						Username   string `json:"username"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Permissions is perms metadata for a repo.
 | 
				
			||||||
 | 
					type Permissions struct {
 | 
				
			||||||
 | 
						Admin bool `json:"admin"`
 | 
				
			||||||
 | 
						Push  bool `json:"push"`
 | 
				
			||||||
 | 
						Pull  bool `json:"pull"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// InternalTracker is issue tracker metadata for a repo.
 | 
				
			||||||
 | 
					type InternalTracker struct {
 | 
				
			||||||
 | 
						EnableTimeTracker                bool `json:"enable_time_tracker"`
 | 
				
			||||||
 | 
						AllowOnlyContributorsToTrackTime bool `json:"allow_only_contributors_to_track_time"`
 | 
				
			||||||
 | 
						EnableIssuesDependencies         bool `json:"enable_issue_dependencies"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RepoCatalogue represents all the recipe repo metadata.
 | 
				
			||||||
 | 
					type RepoCatalogue map[string]RepoMeta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ReadReposMetadata retrieves coop-cloud/... repo metadata from Gitea.
 | 
				
			||||||
 | 
					func ReadReposMetadata() (RepoCatalogue, error) {
 | 
				
			||||||
 | 
						reposMeta := make(RepoCatalogue)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pageIdx := 1
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							var reposList []RepoMeta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							pagedURL := fmt.Sprintf("%s?page=%v", ReposMetadataURL, pageIdx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							logrus.Debugf("fetching repo metadata from '%s'", pagedURL)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := web.ReadJSON(pagedURL, &reposList); err != nil {
 | 
				
			||||||
 | 
								return reposMeta, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if len(reposList) == 0 {
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for idx, repo := range reposList {
 | 
				
			||||||
 | 
								reposMeta[repo.Name] = reposList[idx]
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							pageIdx++
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return reposMeta, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetRecipeVersions retrieves all recipe versions.
 | 
				
			||||||
 | 
					func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
 | 
				
			||||||
 | 
						versions := RecipeVersions{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logrus.Debugf("attempting to open git repository in '%s'", recipeDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						repo, err := git.PlainOpen(recipeDir)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return versions, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						worktree, err := repo.Worktree()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logrus.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gitTags, err := repo.Tags()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return versions, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
 | 
				
			||||||
 | 
							tag := strings.TrimPrefix(string(ref.Name()), "refs/tags/")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							logrus.Debugf("processing '%s' for '%s'", tag, recipeName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							checkOutOpts := &git.CheckoutOptions{
 | 
				
			||||||
 | 
								Create: false,
 | 
				
			||||||
 | 
								Force:  true,
 | 
				
			||||||
 | 
								Branch: plumbing.ReferenceName(ref.Name()),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := worktree.Checkout(checkOutOpts); err != nil {
 | 
				
			||||||
 | 
								logrus.Debugf("failed to check out '%s' in '%s'", tag, recipeDir)
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							logrus.Debugf("successfully checked out '%s' in '%s'", ref.Name(), recipeDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							recipe, err := recipe.Get(recipeName)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							versionMeta := make(map[string]ServiceMeta)
 | 
				
			||||||
 | 
							for _, service := range recipe.Config.Services {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								img, err := reference.ParseNormalizedNamed(service.Image)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								path := reference.Path(img)
 | 
				
			||||||
 | 
								if strings.Contains(path, "library") {
 | 
				
			||||||
 | 
									path = strings.Split(path, "/")[1]
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								digest, err := client.GetTagDigest(img)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								versionMeta[service.Name] = ServiceMeta{
 | 
				
			||||||
 | 
									Digest: digest,
 | 
				
			||||||
 | 
									Image:  path,
 | 
				
			||||||
 | 
									Tag:    img.(reference.NamedTagged).Tag(),
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								logrus.Debugf("collecting digest: '%s', image: '%s', tag: '%s'", digest, path, tag)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							versions = append(versions, map[string]map[string]ServiceMeta{tag: versionMeta})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}); err != nil {
 | 
				
			||||||
 | 
							return versions, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						branch := "master"
 | 
				
			||||||
 | 
						if _, err := repo.Branch("master"); err != nil {
 | 
				
			||||||
 | 
							if _, err := repo.Branch("main"); err != nil {
 | 
				
			||||||
 | 
								logrus.Debugf("failed to select branch in '%s'", recipeDir)
 | 
				
			||||||
 | 
								logrus.Fatal(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							branch = "main"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						refName := fmt.Sprintf("refs/heads/%s", branch)
 | 
				
			||||||
 | 
						checkOutOpts := &git.CheckoutOptions{
 | 
				
			||||||
 | 
							Create: false,
 | 
				
			||||||
 | 
							Force:  true,
 | 
				
			||||||
 | 
							Branch: plumbing.ReferenceName(refName),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := worktree.Checkout(checkOutOpts); err != nil {
 | 
				
			||||||
 | 
							logrus.Debugf("failed to check out '%s' in '%s'", branch, recipeDir)
 | 
				
			||||||
 | 
							logrus.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logrus.Debugf("switched back to '%s' in '%s'", branch, recipeDir)
 | 
				
			||||||
 | 
						logrus.Debugf("collected '%s' for '%s'", versions, recipeName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return versions, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetRecipeCatalogueVersions list the recipe versions listed in the recipe catalogue.
 | 
				
			||||||
 | 
					func GetRecipeCatalogueVersions(recipeName string) ([]string, error) {
 | 
				
			||||||
 | 
						var versions []string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						catl, err := ReadRecipeCatalogue()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return versions, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if recipeMeta, exists := catl[recipeName]; exists {
 | 
				
			||||||
 | 
							for _, versionMeta := range recipeMeta.Versions {
 | 
				
			||||||
 | 
								for tag := range versionMeta {
 | 
				
			||||||
 | 
									versions = append(versions, tag)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return versions, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@ package stack
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	abraClient "coopcloud.tech/abra/pkg/client"
 | 
						abraClient "coopcloud.tech/abra/pkg/client"
 | 
				
			||||||
@ -92,6 +93,50 @@ func GetAllDeployedServices(contextName string) StackStatus {
 | 
				
			|||||||
	return StackStatus{services, nil}
 | 
						return StackStatus{services, nil}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetDeployedServicesByName filters services by name
 | 
				
			||||||
 | 
					func GetDeployedServicesByName(ctx context.Context, cl *dockerclient.Client, stackName, serviceName string) StackStatus {
 | 
				
			||||||
 | 
						filters := filters.NewArgs()
 | 
				
			||||||
 | 
						filters.Add("name", fmt.Sprintf("%s_%s", stackName, serviceName))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						services, err := cl.ServiceList(ctx, types.ServiceListOptions{Filters: filters})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return StackStatus{[]swarm.Service{}, err}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return StackStatus{services, nil}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsDeployed chekcks whether an appp is deployed or not.
 | 
				
			||||||
 | 
					func IsDeployed(ctx context.Context, cl *dockerclient.Client, stackName string) (bool, string, error) {
 | 
				
			||||||
 | 
						version := ""
 | 
				
			||||||
 | 
						isDeployed := false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						filter := filters.NewArgs()
 | 
				
			||||||
 | 
						filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						services, err := cl.ServiceList(ctx, types.ServiceListOptions{Filters: filter})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return false, version, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(services) > 0 {
 | 
				
			||||||
 | 
							for _, service := range services {
 | 
				
			||||||
 | 
								labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName)
 | 
				
			||||||
 | 
								if deployedVersion, ok := service.Spec.Labels[labelKey]; ok {
 | 
				
			||||||
 | 
									version = deployedVersion
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							logrus.Debugf("'%s' has been detected as deployed with version '%s'", stackName, version)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return true, version, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logrus.Debugf("'%s' has been detected as not deployed", stackName)
 | 
				
			||||||
 | 
						return isDeployed, version, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// pruneServices removes services that are no longer referenced in the source
 | 
					// pruneServices removes services that are no longer referenced in the source
 | 
				
			||||||
func pruneServices(ctx context.Context, cl *dockerclient.Client, namespace convert.Namespace, services map[string]struct{}) {
 | 
					func pruneServices(ctx context.Context, cl *dockerclient.Client, namespace convert.Namespace, services map[string]struct{}) {
 | 
				
			||||||
	oldServices, err := GetStackServices(ctx, cl, namespace.Name())
 | 
						oldServices, err := GetStackServices(ctx, cl, namespace.Name())
 | 
				
			||||||
 | 
				
			|||||||
@ -3,18 +3,20 @@ package compose
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io/ioutil"
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
	"path/filepath"
 | 
						"path/filepath"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"coopcloud.tech/abra/pkg/client/stack"
 | 
						"coopcloud.tech/abra/pkg/client/stack"
 | 
				
			||||||
	loader "coopcloud.tech/abra/pkg/client/stack"
 | 
						loader "coopcloud.tech/abra/pkg/client/stack"
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/pkg/config"
 | 
				
			||||||
	composetypes "github.com/docker/cli/cli/compose/types"
 | 
						composetypes "github.com/docker/cli/cli/compose/types"
 | 
				
			||||||
	"github.com/docker/distribution/reference"
 | 
						"github.com/docker/distribution/reference"
 | 
				
			||||||
	"github.com/sirupsen/logrus"
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// UpdateTag updates an image tag in-place on file system local compose files.
 | 
					// UpdateTag updates an image tag in-place on file system local compose files.
 | 
				
			||||||
func UpdateTag(pattern, image, tag string) error {
 | 
					func UpdateTag(pattern, image, tag, recipeName string) error {
 | 
				
			||||||
	composeFiles, err := filepath.Glob(pattern)
 | 
						composeFiles, err := filepath.Glob(pattern)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
@ -24,8 +26,14 @@ func UpdateTag(pattern, image, tag string) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	for _, composeFile := range composeFiles {
 | 
						for _, composeFile := range composeFiles {
 | 
				
			||||||
		opts := stack.Deploy{Composefiles: []string{composeFile}}
 | 
							opts := stack.Deploy{Composefiles: []string{composeFile}}
 | 
				
			||||||
		emptyEnv := make(map[string]string)
 | 
					
 | 
				
			||||||
		compose, err := loader.LoadComposefile(opts, emptyEnv)
 | 
							envSamplePath := path.Join(config.ABRA_DIR, "apps", recipeName, ".env.sample")
 | 
				
			||||||
 | 
							sampleEnv, err := config.ReadEnv(envSamplePath)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							compose, err := loader.LoadComposefile(opts, sampleEnv)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -74,7 +82,7 @@ func UpdateTag(pattern, image, tag string) error {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// UpdateLabel updates a label in-place on file system local compose files.
 | 
					// UpdateLabel updates a label in-place on file system local compose files.
 | 
				
			||||||
func UpdateLabel(pattern, serviceName, label string) error {
 | 
					func UpdateLabel(pattern, serviceName, label, recipeName string) error {
 | 
				
			||||||
	composeFiles, err := filepath.Glob(pattern)
 | 
						composeFiles, err := filepath.Glob(pattern)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
@ -84,8 +92,14 @@ func UpdateLabel(pattern, serviceName, label string) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	for _, composeFile := range composeFiles {
 | 
						for _, composeFile := range composeFiles {
 | 
				
			||||||
		opts := stack.Deploy{Composefiles: []string{composeFile}}
 | 
							opts := stack.Deploy{Composefiles: []string{composeFile}}
 | 
				
			||||||
		emptyEnv := make(map[string]string)
 | 
					
 | 
				
			||||||
		compose, err := loader.LoadComposefile(opts, emptyEnv)
 | 
							envSamplePath := path.Join(config.ABRA_DIR, "apps", recipeName, ".env.sample")
 | 
				
			||||||
 | 
							sampleEnv, err := config.ReadEnv(envSamplePath)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							compose, err := loader.LoadComposefile(opts, sampleEnv)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -110,7 +124,7 @@ func UpdateLabel(pattern, serviceName, label string) error {
 | 
				
			|||||||
					return err
 | 
										return err
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				old := fmt.Sprintf("coop-cloud.${STACK_NAME}.%s.version=%s", service.Name, value)
 | 
									old := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", value)
 | 
				
			||||||
				replacedBytes := strings.Replace(string(bytes), old, label, -1)
 | 
									replacedBytes := strings.Replace(string(bytes), old, label, -1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				logrus.Debugf("updating '%s' to '%s' in '%s'", old, label, compose.Filename)
 | 
									logrus.Debugf("updating '%s' to '%s' in '%s'", old, label, compose.Filename)
 | 
				
			||||||
 | 
				
			|||||||
@ -6,9 +6,9 @@ import (
 | 
				
			|||||||
	"io/ioutil"
 | 
						"io/ioutil"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
	"path/filepath"
 | 
					 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/cli/formatter"
 | 
				
			||||||
	"coopcloud.tech/abra/pkg/client/convert"
 | 
						"coopcloud.tech/abra/pkg/client/convert"
 | 
				
			||||||
	loader "coopcloud.tech/abra/pkg/client/stack"
 | 
						loader "coopcloud.tech/abra/pkg/client/stack"
 | 
				
			||||||
	stack "coopcloud.tech/abra/pkg/client/stack"
 | 
						stack "coopcloud.tech/abra/pkg/client/stack"
 | 
				
			||||||
@ -45,7 +45,12 @@ type App struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// StackName gets what the docker safe stack name is for the app
 | 
					// StackName gets what the docker safe stack name is for the app
 | 
				
			||||||
func (a App) StackName() string {
 | 
					func (a App) StackName() string {
 | 
				
			||||||
	return SanitiseAppName(a.Name)
 | 
						if _, exists := a.Env["STACK_NAME"]; exists {
 | 
				
			||||||
 | 
							return a.Env["STACK_NAME"]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						stackName := SanitiseAppName(a.Name)
 | 
				
			||||||
 | 
						a.Env["STACK_NAME"] = stackName
 | 
				
			||||||
 | 
						return stackName
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SORTING TYPES
 | 
					// SORTING TYPES
 | 
				
			||||||
@ -138,7 +143,7 @@ func LoadAppFiles(servers ...string) (AppFiles, error) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	logrus.Debugf("collecting metadata from '%v' servers: '%s'", len(servers), servers)
 | 
						logrus.Debugf("collecting metadata from '%v' servers: '%s'", len(servers), strings.Join(servers, ", "))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, server := range servers {
 | 
						for _, server := range servers {
 | 
				
			||||||
		serverDir := path.Join(ABRA_SERVER_FOLDER, server)
 | 
							serverDir := path.Join(ABRA_SERVER_FOLDER, server)
 | 
				
			||||||
@ -243,8 +248,8 @@ func GetAppNames() ([]string, error) {
 | 
				
			|||||||
	return appNames, nil
 | 
						return appNames, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CopyAppEnvSample copies the example env file for the app into the users env files
 | 
					// TemplateAppEnvSample copies the example env file for the app into the users env files
 | 
				
			||||||
func CopyAppEnvSample(appType, appName, server string) error {
 | 
					func TemplateAppEnvSample(appType, appName, server, domain, recipe string) error {
 | 
				
			||||||
	envSamplePath := path.Join(ABRA_DIR, "apps", appType, ".env.sample")
 | 
						envSamplePath := path.Join(ABRA_DIR, "apps", appType, ".env.sample")
 | 
				
			||||||
	envSample, err := ioutil.ReadFile(envSamplePath)
 | 
						envSample, err := ioutil.ReadFile(envSamplePath)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@ -256,6 +261,9 @@ func CopyAppEnvSample(appType, appName, server string) error {
 | 
				
			|||||||
		return fmt.Errorf("%s already exists?", appEnvPath)
 | 
							return fmt.Errorf("%s already exists?", appEnvPath)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						envSample = []byte(strings.Replace(string(envSample), fmt.Sprintf("%s.example.com", recipe), domain, -1))
 | 
				
			||||||
 | 
						envSample = []byte(strings.Replace(string(envSample), "example.com", domain, -1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err = ioutil.WriteFile(appEnvPath, envSample, 0755)
 | 
						err = ioutil.WriteFile(appEnvPath, envSample, 0755)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
@ -272,26 +280,48 @@ func SanitiseAppName(name string) string {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetAppStatuses queries servers to check the deployment status of given apps
 | 
					// GetAppStatuses queries servers to check the deployment status of given apps
 | 
				
			||||||
func GetAppStatuses(appFiles AppFiles) (map[string]string, error) {
 | 
					func GetAppStatuses(appFiles AppFiles) (map[string]map[string]string, error) {
 | 
				
			||||||
	statuses := map[string]string{}
 | 
						statuses := make(map[string]map[string]string)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	servers, err := GetServers()
 | 
						var unique []string
 | 
				
			||||||
	if err != nil {
 | 
						servers := make(map[string]struct{})
 | 
				
			||||||
		return statuses, err
 | 
						for _, appFile := range appFiles {
 | 
				
			||||||
 | 
							if _, ok := servers[appFile.Server]; !ok {
 | 
				
			||||||
 | 
								servers[appFile.Server] = struct{}{}
 | 
				
			||||||
 | 
								unique = append(unique, appFile.Server)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						bar := formatter.CreateProgressbar(len(servers), "querying remote servers...")
 | 
				
			||||||
	ch := make(chan stack.StackStatus, len(servers))
 | 
						ch := make(chan stack.StackStatus, len(servers))
 | 
				
			||||||
	for _, server := range servers {
 | 
						for server := range servers {
 | 
				
			||||||
		go func(s string) { ch <- stack.GetAllDeployedServices(s) }(server)
 | 
							go func(s string) {
 | 
				
			||||||
 | 
								ch <- stack.GetAllDeployedServices(s)
 | 
				
			||||||
 | 
								bar.Add(1)
 | 
				
			||||||
 | 
							}(server)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for range servers {
 | 
						for range servers {
 | 
				
			||||||
		status := <-ch
 | 
							status := <-ch
 | 
				
			||||||
		for _, service := range status.Services {
 | 
							for _, service := range status.Services {
 | 
				
			||||||
 | 
								result := make(map[string]string)
 | 
				
			||||||
			name := service.Spec.Labels[convert.LabelNamespace]
 | 
								name := service.Spec.Labels[convert.LabelNamespace]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if _, ok := statuses[name]; !ok {
 | 
								if _, ok := statuses[name]; !ok {
 | 
				
			||||||
				statuses[name] = "deployed"
 | 
									result["status"] = "deployed"
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								labelKey := fmt.Sprintf("coop-cloud.%s.version", name)
 | 
				
			||||||
 | 
								if version, ok := service.Spec.Labels[labelKey]; ok {
 | 
				
			||||||
 | 
									result["version"] = version
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									//FIXME: we only need to check containers with the version label not
 | 
				
			||||||
 | 
									//       every single container and then skip when we see no label perf gains
 | 
				
			||||||
 | 
									//       to be had here
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								statuses[name] = result
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -303,20 +333,18 @@ func GetAppStatuses(appFiles AppFiles) (map[string]string, error) {
 | 
				
			|||||||
// GetAppComposeFiles gets the list of compose files for an app which should be
 | 
					// GetAppComposeFiles gets the list of compose files for an app which should be
 | 
				
			||||||
// merged into a composetypes.Config while respecting the COMPOSE_FILE env var.
 | 
					// merged into a composetypes.Config while respecting the COMPOSE_FILE env var.
 | 
				
			||||||
func GetAppComposeFiles(recipe string, appEnv AppEnv) ([]string, error) {
 | 
					func GetAppComposeFiles(recipe string, appEnv AppEnv) ([]string, error) {
 | 
				
			||||||
 | 
						var composeFiles []string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if _, ok := appEnv["COMPOSE_FILE"]; !ok {
 | 
						if _, ok := appEnv["COMPOSE_FILE"]; !ok {
 | 
				
			||||||
		logrus.Debug("no COMPOSE_FILE detected, loading all compose files")
 | 
							logrus.Debug("no COMPOSE_FILE detected, loading compose.yml")
 | 
				
			||||||
		pattern := fmt.Sprintf("%s/%s/compose**yml", APPS_DIR, recipe)
 | 
							path := fmt.Sprintf("%s/%s/compose.yml", APPS_DIR, recipe)
 | 
				
			||||||
		composeFiles, err := filepath.Glob(pattern)
 | 
							composeFiles = append(composeFiles, path)
 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return composeFiles, err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return composeFiles, nil
 | 
							return composeFiles, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var composeFiles []string
 | 
					 | 
				
			||||||
	composeFileEnvVar := appEnv["COMPOSE_FILE"]
 | 
						composeFileEnvVar := appEnv["COMPOSE_FILE"]
 | 
				
			||||||
	envVars := strings.Split(composeFileEnvVar, ":")
 | 
						envVars := strings.Split(composeFileEnvVar, ":")
 | 
				
			||||||
	logrus.Debugf("COMPOSE_FILE detected ('%s'), loading '%s'", composeFileEnvVar, envVars)
 | 
						logrus.Debugf("COMPOSE_FILE detected ('%s'), loading '%s'", composeFileEnvVar, strings.Join(envVars, ", "))
 | 
				
			||||||
	for _, file := range strings.Split(composeFileEnvVar, ":") {
 | 
						for _, file := range strings.Split(composeFileEnvVar, ":") {
 | 
				
			||||||
		path := fmt.Sprintf("%s/%s/%s", APPS_DIR, recipe, file)
 | 
							path := fmt.Sprintf("%s/%s/%s", APPS_DIR, recipe, file)
 | 
				
			||||||
		composeFiles = append(composeFiles, path)
 | 
							composeFiles = append(composeFiles, path)
 | 
				
			||||||
 | 
				
			|||||||
@ -73,6 +73,11 @@ func getAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
 | 
				
			|||||||
	for _, file := range files {
 | 
						for _, file := range files {
 | 
				
			||||||
		// Follow any symlinks
 | 
							// Follow any symlinks
 | 
				
			||||||
		filePath := path.Join(directory, file.Name())
 | 
							filePath := path.Join(directory, file.Name())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if filepath.Ext(strings.TrimSpace(filePath)) != ".env" {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		realPath, err := filepath.EvalSymlinks(filePath)
 | 
							realPath, err := filepath.EvalSymlinks(filePath)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Warningf("broken symlink in your abra config folders: '%s'", filePath)
 | 
								logrus.Warningf("broken symlink in your abra config folders: '%s'", filePath)
 | 
				
			||||||
@ -137,7 +142,7 @@ func ReadAbraShEnvVars(abraSh string) (map[string]string, error) {
 | 
				
			|||||||
	file, err := os.Open(abraSh)
 | 
						file, err := os.Open(abraSh)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		if os.IsNotExist(err) {
 | 
							if os.IsNotExist(err) {
 | 
				
			||||||
			return envVars, fmt.Errorf("'%s' does not exist?", abraSh)
 | 
								return envVars, nil
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return envVars, err
 | 
							return envVars, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,7 @@ var validAbraConf = os.ExpandEnv("$PWD/../../tests/resources/valid_abra_config")
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// make sure these are in alphabetical order
 | 
					// make sure these are in alphabetical order
 | 
				
			||||||
var tFolders = []string{"folder1", "folder2"}
 | 
					var tFolders = []string{"folder1", "folder2"}
 | 
				
			||||||
var tFiles = []string{"bar", "foo"}
 | 
					var tFiles = []string{"bar.env", "foo.env"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var appName = "ecloud"
 | 
					var appName = "ecloud"
 | 
				
			||||||
var serverName = "evil.corp"
 | 
					var serverName = "evil.corp"
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,7 @@ func Clone(dir, url string) error {
 | 
				
			|||||||
		logrus.Debugf("'%s' does not exist, attempting to git clone from '%s'", dir, url)
 | 
							logrus.Debugf("'%s' does not exist, attempting to git clone from '%s'", dir, url)
 | 
				
			||||||
		_, err := git.PlainClone(dir, false, &git.CloneOptions{URL: url, Tags: git.AllTags})
 | 
							_, err := git.PlainClone(dir, false, &git.CloneOptions{URL: url, Tags: git.AllTags})
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logrus.Debugf("cloning from default branch failed, attempting from main branch")
 | 
								logrus.Debugf("cloning '%s' default branch failed, attempting from main branch", url)
 | 
				
			||||||
			_, err := git.PlainClone(dir, false, &git.CloneOptions{
 | 
								_, err := git.PlainClone(dir, false, &git.CloneOptions{
 | 
				
			||||||
				URL:           url,
 | 
									URL:           url,
 | 
				
			||||||
				Tags:          git.AllTags,
 | 
									Tags:          git.AllTags,
 | 
				
			||||||
@ -45,12 +45,13 @@ func EnsureUpToDate(dir string) error {
 | 
				
			|||||||
	branch := "master"
 | 
						branch := "master"
 | 
				
			||||||
	if _, err := repo.Branch("master"); err != nil {
 | 
						if _, err := repo.Branch("master"); err != nil {
 | 
				
			||||||
		if _, err := repo.Branch("main"); err != nil {
 | 
							if _, err := repo.Branch("main"); err != nil {
 | 
				
			||||||
 | 
								logrus.Debugf("failed to select branch in '%s'", dir)
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		branch = "main"
 | 
							branch = "main"
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	logrus.Debugf("choosing '%s' as main git branch for in '%s'", branch, dir)
 | 
						logrus.Debugf("choosing '%s' as main git branch in '%s'", branch, dir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	worktree, err := repo.Worktree()
 | 
						worktree, err := repo.Worktree()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@ -61,14 +62,14 @@ func EnsureUpToDate(dir string) error {
 | 
				
			|||||||
	checkOutOpts := &git.CheckoutOptions{
 | 
						checkOutOpts := &git.CheckoutOptions{
 | 
				
			||||||
		Create: false,
 | 
							Create: false,
 | 
				
			||||||
		Force:  true,
 | 
							Force:  true,
 | 
				
			||||||
		Keep:   false,
 | 
					 | 
				
			||||||
		Branch: plumbing.ReferenceName(refName),
 | 
							Branch: plumbing.ReferenceName(refName),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err := worktree.Checkout(checkOutOpts); err != nil {
 | 
						if err := worktree.Checkout(checkOutOpts); err != nil {
 | 
				
			||||||
 | 
							logrus.Debugf("failed to check out '%s' in '%s'", refName, dir)
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	logrus.Debugf("successfully checked out '%s'", branch)
 | 
						logrus.Debugf("successfully checked out '%s' in '%s'", branch, dir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	remote, err := repo.Remote("origin")
 | 
						remote, err := repo.Remote("origin")
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
				
			|||||||
@ -26,7 +26,7 @@ type Recipe struct {
 | 
				
			|||||||
// UpdateLabel updates a recipe label
 | 
					// UpdateLabel updates a recipe label
 | 
				
			||||||
func (r Recipe) UpdateLabel(serviceName, label string) error {
 | 
					func (r Recipe) UpdateLabel(serviceName, label string) error {
 | 
				
			||||||
	pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, r.Name)
 | 
						pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, r.Name)
 | 
				
			||||||
	if err := compose.UpdateLabel(pattern, serviceName, label); err != nil {
 | 
						if err := compose.UpdateLabel(pattern, serviceName, label, r.Name); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
@ -35,12 +35,39 @@ func (r Recipe) UpdateLabel(serviceName, label string) error {
 | 
				
			|||||||
// UpdateTag updates a recipe tag
 | 
					// UpdateTag updates a recipe tag
 | 
				
			||||||
func (r Recipe) UpdateTag(image, tag string) error {
 | 
					func (r Recipe) UpdateTag(image, tag string) error {
 | 
				
			||||||
	pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, r.Name)
 | 
						pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, r.Name)
 | 
				
			||||||
	if err := compose.UpdateTag(pattern, image, tag); err != nil {
 | 
						if err := compose.UpdateTag(pattern, image, tag, r.Name); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Tags list the recipe tags
 | 
				
			||||||
 | 
					func (r Recipe) Tags() ([]string, error) {
 | 
				
			||||||
 | 
						var tags []string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						recipeDir := path.Join(config.ABRA_DIR, "apps", r.Name)
 | 
				
			||||||
 | 
						repo, err := git.PlainOpen(recipeDir)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return tags, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gitTags, err := repo.Tags()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return tags, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
 | 
				
			||||||
 | 
							tags = append(tags, strings.TrimPrefix(string(ref.Name()), "refs/tags/"))
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}); err != nil {
 | 
				
			||||||
 | 
							return tags, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logrus.Debugf("detected '%s' as tags for recipe '%s'", strings.Join(tags, ", "), r.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return tags, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Get retrieves a recipe.
 | 
					// Get retrieves a recipe.
 | 
				
			||||||
func Get(recipeName string) (Recipe, error) {
 | 
					func Get(recipeName string) (Recipe, error) {
 | 
				
			||||||
	if err := EnsureExists(recipeName); err != nil {
 | 
						if err := EnsureExists(recipeName); err != nil {
 | 
				
			||||||
@ -92,10 +119,10 @@ func EnsureVersion(recipeName, version string) error {
 | 
				
			|||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	logrus.Debugf("read '%s' as tags for recipe '%s'", tags, recipeName)
 | 
						var parsedTags []string
 | 
				
			||||||
 | 
					 | 
				
			||||||
	var tagRef plumbing.ReferenceName
 | 
						var tagRef plumbing.ReferenceName
 | 
				
			||||||
	if err := tags.ForEach(func(ref *plumbing.Reference) (err error) {
 | 
						if err := tags.ForEach(func(ref *plumbing.Reference) (err error) {
 | 
				
			||||||
 | 
							parsedTags = append(parsedTags, ref.Name().Short())
 | 
				
			||||||
		if ref.Name().Short() == version {
 | 
							if ref.Name().Short() == version {
 | 
				
			||||||
			tagRef = ref.Name()
 | 
								tagRef = ref.Name()
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -104,6 +131,8 @@ func EnsureVersion(recipeName, version string) error {
 | 
				
			|||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logrus.Debugf("read '%s' as tags for recipe '%s'", strings.Join(parsedTags, ", "), recipeName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if tagRef.String() == "" {
 | 
						if tagRef.String() == "" {
 | 
				
			||||||
		return fmt.Errorf("%s is not available?", version)
 | 
							return fmt.Errorf("%s is not available?", version)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -113,12 +142,56 @@ func EnsureVersion(recipeName, version string) error {
 | 
				
			|||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	opts := &git.CheckoutOptions{Branch: tagRef, Keep: true}
 | 
						opts := &git.CheckoutOptions{
 | 
				
			||||||
 | 
							Branch: tagRef,
 | 
				
			||||||
 | 
							Create: false,
 | 
				
			||||||
 | 
							Force:  true,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	if err := worktree.Checkout(opts); err != nil {
 | 
						if err := worktree.Checkout(opts); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	logrus.Debugf("successfully checked '%s' out to '%s' in '%s'", recipeName, tagRef, recipeDir)
 | 
						logrus.Debugf("successfully checked '%s' out to '%s' in '%s'", recipeName, tagRef.Short(), recipeDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// EnsureLatest makes sure the latest commit is checkout on for a local recipe repository.
 | 
				
			||||||
 | 
					func EnsureLatest(recipeName string) error {
 | 
				
			||||||
 | 
						recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logrus.Debugf("attempting to open git repository in '%s'", recipeDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						repo, err := git.PlainOpen(recipeDir)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						worktree, err := repo.Worktree()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						branch := "master"
 | 
				
			||||||
 | 
						if _, err := repo.Branch("master"); err != nil {
 | 
				
			||||||
 | 
							if _, err := repo.Branch("main"); err != nil {
 | 
				
			||||||
 | 
								logrus.Debugf("failed to select branch in '%s'", path.Join(config.APPS_DIR, recipeName))
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							branch = "main"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						refName := fmt.Sprintf("refs/heads/%s", branch)
 | 
				
			||||||
 | 
						checkOutOpts := &git.CheckoutOptions{
 | 
				
			||||||
 | 
							Create: false,
 | 
				
			||||||
 | 
							Force:  true,
 | 
				
			||||||
 | 
							Branch: plumbing.ReferenceName(refName),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := worktree.Checkout(checkOutOpts); err != nil {
 | 
				
			||||||
 | 
							logrus.Debugf("failed to check out '%s' in '%s'", branch, recipeDir)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										24
									
								
								pkg/server/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								pkg/server/server.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					package server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"coopcloud.tech/abra/pkg/config"
 | 
				
			||||||
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateServerDir creates a server directory under ~/.abra.
 | 
				
			||||||
 | 
					func CreateServerDir(serverName string) error {
 | 
				
			||||||
 | 
						serverPath := path.Join(config.ABRA_DIR, "servers", serverName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := os.Mkdir(serverPath, 0755); err != nil {
 | 
				
			||||||
 | 
							if !os.IsExist(err) {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							logrus.Infof("'%s' already exists, moving on...", serverPath)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -6,4 +6,4 @@ Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock {
 | 
				
			|||||||
         Invoke-Expression $other | ForEach-Object {
 | 
					         Invoke-Expression $other | ForEach-Object {
 | 
				
			||||||
            [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
 | 
					            [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
 | 
				
			||||||
         }
 | 
					         }
 | 
				
			||||||
 }
 | 
					 }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
#!/usr/bin/env bash
 | 
					#!/usr/bin/env bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ABRA_VERSION="0.1.3-alpha"
 | 
					ABRA_VERSION="0.2.0-alpha"
 | 
				
			||||||
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION"
 | 
					ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function show_banner {
 | 
					function show_banner {
 | 
				
			||||||
@ -35,13 +35,11 @@ function install_abra_release {
 | 
				
			|||||||
  fi
 | 
					  fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # FIXME: support different architectures
 | 
					  # FIXME: support different architectures
 | 
				
			||||||
  release_url=$(curl -s "$ABRA_RELEASE_URL" |
 | 
					  PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m)
 | 
				
			||||||
    python3 -c "import sys, json; \
 | 
					  sed_command='s/.*"assets":\[\{[^}]*"name":"abra.*_'"$PLATFORM"'"[^}]*"browser_download_url":"([^"]*)".*\].*/\1/p'
 | 
				
			||||||
                payload = json.load(sys.stdin); \
 | 
					  release_url=$(curl -s $ABRA_RELEASE_URL | sed -En $sed_command)
 | 
				
			||||||
                url = [a['browser_download_url'] for a in payload['assets'] if 'x86_64' in a['name']][0]; \
 | 
					 | 
				
			||||||
                print(url)")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  echo "downloading $ABRA_VERSION x86_64 binary release for abra..."
 | 
					  echo "downloading $ABRA_VERSION $PLATFORM binary release for abra..."
 | 
				
			||||||
  curl --progress-bar "$release_url" --output "$HOME/.local/bin/abra"
 | 
					  curl --progress-bar "$release_url" --output "$HOME/.local/bin/abra"
 | 
				
			||||||
  chmod +x "$HOME/.local/bin/abra"
 | 
					  chmod +x "$HOME/.local/bin/abra"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -3,5 +3,5 @@ STACK := abra_installer_script
 | 
				
			|||||||
default: deploy
 | 
					default: deploy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
deploy:
 | 
					deploy:
 | 
				
			||||||
	@docker stack rm $(STACK) && \
 | 
						@DOCKER_CONTEXT=swarm.autonomic.zone docker stack rm $(STACK) && \
 | 
				
			||||||
		docker stack deploy -c compose.yml $(STACK)
 | 
							DOCKER_CONTEXT=swarm.autonomic.zone docker stack deploy -c compose.yml $(STACK)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										0
									
								
								tests/resources/test_folder/foo.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/resources/test_folder/foo.env
									
									
									
									
									
										Normal file
									
								
							
		Reference in New Issue
	
	Block a user