forked from toolshed/abra
		
	Compare commits
	
		
			36 Commits
		
	
	
		
			upgrade-cl
			...
			refactor-r
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 89e1046882 | |||
| cbab9b5907 | |||
| b688ddc4b1 | |||
| 6cd331ebd6 | |||
| 40517171f7 | |||
| b2485cc122 | |||
| 9ec99c7712 | |||
| aa3910f8df | |||
| 43990b6fae | |||
| 91ea2c01a5 | |||
| 316fdd3643 | |||
| e07ae8cccd | |||
| 300a4ead01 | |||
| f209b6f564 | |||
| 791183adfe | |||
| e6b35e8524 | |||
| 8a0274cac0 | |||
| e609924af0 | |||
| 70e2943301 | |||
| 0590c1824d | |||
| 459abecfa5 | |||
| 183ad8f576 | |||
| 03f94da2d8 | |||
| 766f69b0fd | |||
| 004cd70aed | |||
| a4de446f58 | |||
| d21c35965d | |||
| 63ea58ffaa | |||
| 2ecace3e90 | |||
| d5ac3958a4 | |||
| 72c20e0039 | |||
| 575f9905f1 | |||
| e3a0af5840 | |||
| 9a3a39a185 | |||
| cea56dddde | |||
| 2c515ce70a | 
| @ -29,7 +29,7 @@ steps: | |||||||
|       event: tag |       event: tag | ||||||
|  |  | ||||||
|   - name: release |   - name: release | ||||||
|     image: goreleaser/goreleaser:v1.18.2 |     image: goreleaser/goreleaser:v1.24.0 | ||||||
|     environment: |     environment: | ||||||
|       GITEA_TOKEN: |       GITEA_TOKEN: | ||||||
|         from_secret: goreleaser_gitea_token |         from_secret: goreleaser_gitea_token | ||||||
|  | |||||||
| @ -29,6 +29,8 @@ builds: | |||||||
|     ldflags: |     ldflags: | ||||||
|       - "-X 'main.Commit={{ .Commit }}'" |       - "-X 'main.Commit={{ .Commit }}'" | ||||||
|       - "-X 'main.Version={{ .Version }}'" |       - "-X 'main.Version={{ .Version }}'" | ||||||
|  |       - "-s" | ||||||
|  |       - "-w" | ||||||
|  |  | ||||||
|   - id: kadabra |   - id: kadabra | ||||||
|     binary: kadabra |     binary: kadabra | ||||||
| @ -50,12 +52,8 @@ builds: | |||||||
|     ldflags: |     ldflags: | ||||||
|       - "-X 'main.Commit={{ .Commit }}'" |       - "-X 'main.Commit={{ .Commit }}'" | ||||||
|       - "-X 'main.Version={{ .Version }}'" |       - "-X 'main.Version={{ .Version }}'" | ||||||
|  |       - "-s" | ||||||
| archives: |       - "-w" | ||||||
|   - replacements: |  | ||||||
|       386: i386 |  | ||||||
|       amd64: x86_64 |  | ||||||
|     format: binary |  | ||||||
|  |  | ||||||
| checksum: | checksum: | ||||||
|   name_template: "checksums.txt" |   name_template: "checksums.txt" | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ | |||||||
| - cassowary | - cassowary | ||||||
| - codegod100 | - codegod100 | ||||||
| - decentral1se | - decentral1se | ||||||
|  | - fauno | ||||||
| - frando | - frando | ||||||
| - kawaiipunk | - kawaiipunk | ||||||
| - knoflook | - knoflook | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								Dockerfile
									
									
									
									
									
								
							| @ -1,23 +1,29 @@ | |||||||
|  | # Build image | ||||||
| FROM golang:1.21-alpine AS build | FROM golang:1.21-alpine AS build | ||||||
|  |  | ||||||
| ENV GOPRIVATE coopcloud.tech | ENV GOPRIVATE coopcloud.tech | ||||||
|  |  | ||||||
| RUN apk add --no-cache \ | RUN apk add --no-cache \ | ||||||
|   ca-certificates \ |  | ||||||
|   gcc \ |   gcc \ | ||||||
|   git \ |   git \ | ||||||
|   make \ |   make \ | ||||||
|   musl-dev |   musl-dev | ||||||
|  |  | ||||||
| RUN update-ca-certificates |  | ||||||
|  |  | ||||||
| COPY . /app | COPY . /app | ||||||
|  |  | ||||||
| WORKDIR /app | WORKDIR /app | ||||||
|  |  | ||||||
| RUN CGO_ENABLED=0 make build | RUN CGO_ENABLED=0 make build | ||||||
|  |  | ||||||
| FROM scratch | # Release image ("slim") | ||||||
|  | FROM alpine:3.19.1 | ||||||
|  |  | ||||||
|  | RUN apk add --no-cache \ | ||||||
|  |   ca-certificates \ | ||||||
|  |   git \ | ||||||
|  |   openssh | ||||||
|  |  | ||||||
|  | RUN update-ca-certificates | ||||||
|  |  | ||||||
| COPY --from=build /app/abra /abra | COPY --from=build /app/abra /abra | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								Makefile
									
									
									
									
									
								
							| @ -53,3 +53,6 @@ test: | |||||||
|  |  | ||||||
| loc: | loc: | ||||||
| 	@find . -name "*.go" | xargs wc -l | 	@find . -name "*.go" | xargs wc -l | ||||||
|  |  | ||||||
|  | deps: | ||||||
|  | 	@go get -t -u ./... | ||||||
|  | |||||||
| @ -47,22 +47,26 @@ var appBackupListCommand = cli.Command{ | |||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		app := internal.ValidateApp(c) | 		app := internal.ValidateApp(c) | ||||||
|  |  | ||||||
| 		if err := recipe.EnsureExists(app.Recipe); err != nil { | 		r, err := recipe.Get(app.Recipe) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  | 		if err := r.EnsureExists(); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		if !internal.Chaos { | ||||||
| 			if err := recipe.EnsureIsClean(app.Recipe); err != nil { | 			if err := r.EnsureIsClean(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if !internal.Offline { | 			if !internal.Offline { | ||||||
| 				if err := recipe.EnsureUpToDate(app.Recipe); err != nil { | 				if err := r.EnsureUpToDate(); err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err := recipe.EnsureLatest(app.Recipe); err != nil { | 			if err := r.EnsureLatest(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @ -110,22 +114,26 @@ var appBackupDownloadCommand = cli.Command{ | |||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		app := internal.ValidateApp(c) | 		app := internal.ValidateApp(c) | ||||||
|  |  | ||||||
| 		if err := recipe.EnsureExists(app.Recipe); err != nil { | 		r, err := recipe.Get(app.Recipe) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  | 		if err := r.EnsureExists(); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		if !internal.Chaos { | ||||||
| 			if err := recipe.EnsureIsClean(app.Recipe); err != nil { | 			if err := r.EnsureIsClean(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if !internal.Offline { | 			if !internal.Offline { | ||||||
| 				if err := recipe.EnsureUpToDate(app.Recipe); err != nil { | 				if err := r.EnsureUpToDate(); err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err := recipe.EnsureLatest(app.Recipe); err != nil { | 			if err := r.EnsureLatest(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @ -180,22 +188,27 @@ var appBackupCreateCommand = cli.Command{ | |||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		app := internal.ValidateApp(c) | 		app := internal.ValidateApp(c) | ||||||
|  |  | ||||||
| 		if err := recipe.EnsureExists(app.Recipe); err != nil { | 		r, err := recipe.Get(app.Recipe) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err := r.EnsureExists(); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		if !internal.Chaos { | ||||||
| 			if err := recipe.EnsureIsClean(app.Recipe); err != nil { | 			if err := r.EnsureIsClean(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if !internal.Offline { | 			if !internal.Offline { | ||||||
| 				if err := recipe.EnsureUpToDate(app.Recipe); err != nil { | 				if err := r.EnsureUpToDate(); err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err := recipe.EnsureLatest(app.Recipe); err != nil { | 			if err := r.EnsureLatest(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @ -238,22 +251,27 @@ var appBackupSnapshotsCommand = cli.Command{ | |||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		app := internal.ValidateApp(c) | 		app := internal.ValidateApp(c) | ||||||
|  |  | ||||||
| 		if err := recipe.EnsureExists(app.Recipe); err != nil { | 		r, err := recipe.Get(app.Recipe) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err := r.EnsureExists(); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		if !internal.Chaos { | ||||||
| 			if err := recipe.EnsureIsClean(app.Recipe); err != nil { | 			if err := r.EnsureIsClean(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if !internal.Offline { | 			if !internal.Offline { | ||||||
| 				if err := recipe.EnsureUpToDate(app.Recipe); err != nil { | 				if err := r.EnsureUpToDate(); err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err := recipe.EnsureLatest(app.Recipe); err != nil { | 			if err := r.EnsureLatest(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -6,7 +6,6 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/config" | 	"coopcloud.tech/abra/pkg/config" | ||||||
| 	"coopcloud.tech/abra/pkg/formatter" | 	"coopcloud.tech/abra/pkg/formatter" | ||||||
| 	"coopcloud.tech/abra/pkg/recipe" | 	"coopcloud.tech/abra/pkg/recipe" | ||||||
| 	recipePkg "coopcloud.tech/abra/pkg/recipe" |  | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/urfave/cli" | 	"github.com/urfave/cli" | ||||||
| ) | ) | ||||||
| @ -38,22 +37,26 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`, | |||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		app := internal.ValidateApp(c) | 		app := internal.ValidateApp(c) | ||||||
|  |  | ||||||
| 		if err := recipe.EnsureExists(app.Recipe); err != nil { | 		r, err := recipe.Get(app.Recipe) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  | 		if err := r.EnsureExists(); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		if !internal.Chaos { | ||||||
| 			if err := recipePkg.EnsureIsClean(app.Recipe); err != nil { | 			if err := r.EnsureIsClean(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if !internal.Offline { | 			if !internal.Offline { | ||||||
| 				if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil { | 				if err := r.EnsureUpToDate(); err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err := recipePkg.EnsureLatest(app.Recipe); err != nil { | 			if err := r.EnsureLatest(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -15,7 +15,6 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/client" | 	"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" | ||||||
| 	recipePkg "coopcloud.tech/abra/pkg/recipe" |  | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/urfave/cli" | 	"github.com/urfave/cli" | ||||||
| ) | ) | ||||||
| @ -60,22 +59,27 @@ Example: | |||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		app := internal.ValidateApp(c) | 		app := internal.ValidateApp(c) | ||||||
|  |  | ||||||
| 		if err := recipe.EnsureExists(app.Recipe); err != nil { | 		r, err := recipe.Get(app.Recipe) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err := r.EnsureExists(); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		if !internal.Chaos { | ||||||
| 			if err := recipePkg.EnsureIsClean(app.Recipe); err != nil { | 			if err := r.EnsureIsClean(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if !internal.Offline { | 			if !internal.Offline { | ||||||
| 				if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil { | 				if err := r.EnsureUpToDate(); err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err := recipePkg.EnsureLatest(app.Recipe); err != nil { | 			if err := r.EnsureLatest(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @ -228,22 +232,27 @@ var appCmdListCommand = cli.Command{ | |||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		app := internal.ValidateApp(c) | 		app := internal.ValidateApp(c) | ||||||
|  |  | ||||||
| 		if err := recipe.EnsureExists(app.Recipe); err != nil { | 		r, err := recipe.Get(app.Recipe) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err := r.EnsureExists(); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		if !internal.Chaos { | ||||||
| 			if err := recipePkg.EnsureIsClean(app.Recipe); err != nil { | 			if err := r.EnsureIsClean(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if !internal.Offline { | 			if !internal.Offline { | ||||||
| 				if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil { | 				if err := r.EnsureUpToDate(); err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err := recipePkg.EnsureLatest(app.Recipe); err != nil { | 			if err := r.EnsureLatest(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -56,28 +56,32 @@ recipes. | |||||||
| 			logrus.Fatal("cannot use <version> and --chaos together") | 			logrus.Fatal("cannot use <version> and --chaos together") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if err := recipe.EnsureExists(app.Recipe); err != nil { | 		r, err := recipe.Get(app.Recipe) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err := r.EnsureExists(); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		if !internal.Chaos { | ||||||
| 			if err := recipe.EnsureIsClean(app.Recipe); err != nil { | 			if err := r.EnsureIsClean(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if !internal.Offline { | 			if !internal.Offline { | ||||||
| 				if err := recipe.EnsureUpToDate(app.Recipe); err != nil { | 				if err := r.EnsureUpToDate(); err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err := recipe.EnsureLatest(app.Recipe); err != nil { | 			if err := r.EnsureLatest(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		r, err := recipe.Get(app.Recipe, internal.Offline) | 		if err := r.LoadConfig(); err != nil { | ||||||
| 		if err != nil { |  | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @ -97,6 +101,19 @@ recipes. | |||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		// NOTE(d1): check out specific version before dealing with secrets. This | ||||||
|  | 		// is because we need to deal with GetComposeFiles under the hood and these | ||||||
|  | 		// files change from version to version which therefore affects which | ||||||
|  | 		// secrets might be generated | ||||||
|  | 		version := deployedVersion | ||||||
|  | 		if specificVersion != "" { | ||||||
|  | 			version = specificVersion | ||||||
|  | 			logrus.Debugf("choosing %s as version to deploy", version) | ||||||
|  | 			if err := r.EnsureVersion(version); err != nil { | ||||||
|  | 				logrus.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		secStats, err := secret.PollSecretsStatus(cl, app) | 		secStats, err := secret.PollSecretsStatus(cl, app) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| @ -116,15 +133,6 @@ recipes. | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		version := deployedVersion |  | ||||||
| 		if specificVersion != "" { |  | ||||||
| 			version = specificVersion |  | ||||||
| 			logrus.Debugf("choosing %s as version to deploy", version) |  | ||||||
| 			if err := recipe.EnsureVersion(app.Recipe, version); err != nil { |  | ||||||
| 				logrus.Fatal(err) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if !internal.Chaos && specificVersion == "" { | 		if !internal.Chaos && specificVersion == "" { | ||||||
| 			catl, err := recipe.ReadRecipeCatalogue(internal.Offline) | 			catl, err := recipe.ReadRecipeCatalogue(internal.Offline) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| @ -137,7 +145,7 @@ recipes. | |||||||
|  |  | ||||||
| 			if len(versions) == 0 && !internal.Chaos { | 			if len(versions) == 0 && !internal.Chaos { | ||||||
| 				logrus.Warn("no published versions in catalogue, trying local recipe repository") | 				logrus.Warn("no published versions in catalogue, trying local recipe repository") | ||||||
| 				recipeVersions, err := recipe.GetRecipeVersions(app.Recipe, internal.Offline) | 				recipeVersions, err := r.GetVersions(internal.Offline) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					logrus.Warn(err) | 					logrus.Warn(err) | ||||||
| 				} | 				} | ||||||
| @ -151,11 +159,11 @@ recipes. | |||||||
| 			if len(versions) > 0 && !internal.Chaos { | 			if len(versions) > 0 && !internal.Chaos { | ||||||
| 				version = versions[len(versions)-1] | 				version = versions[len(versions)-1] | ||||||
| 				logrus.Debugf("choosing %s as version to deploy", version) | 				logrus.Debugf("choosing %s as version to deploy", version) | ||||||
| 				if err := recipe.EnsureVersion(app.Recipe, version); err != nil { | 				if err := r.EnsureVersion(version); err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| 			} else { | 			} else { | ||||||
| 				head, err := git.GetRecipeHead(app.Recipe) | 				head, err := git.GetHead(app.Recipe) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| @ -167,7 +175,7 @@ recipes. | |||||||
| 		if internal.Chaos { | 		if internal.Chaos { | ||||||
| 			logrus.Warnf("chaos mode engaged") | 			logrus.Warnf("chaos mode engaged") | ||||||
| 			var err error | 			var err error | ||||||
| 			version, err = recipe.ChaosVersion(app.Recipe) | 			version, err = r.ChaosVersion() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  | |||||||
| @ -13,7 +13,7 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/config" | 	"coopcloud.tech/abra/pkg/config" | ||||||
| 	"coopcloud.tech/abra/pkg/recipe" | 	"coopcloud.tech/abra/pkg/recipe" | ||||||
| 	stack "coopcloud.tech/abra/pkg/upstream/stack" | 	stack "coopcloud.tech/abra/pkg/upstream/stack" | ||||||
| 	"github.com/docker/docker/api/types" | 	containerTypes "github.com/docker/docker/api/types/container" | ||||||
| 	"github.com/docker/docker/api/types/filters" | 	"github.com/docker/docker/api/types/filters" | ||||||
| 	dockerClient "github.com/docker/docker/client" | 	dockerClient "github.com/docker/docker/client" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| @ -88,16 +88,19 @@ the logs. | |||||||
| } | } | ||||||
|  |  | ||||||
| func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App) error { | func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App) error { | ||||||
| 	recipe, err := recipe.Get(app.Recipe, internal.Offline) | 	r, err := recipe.Get(app.Recipe) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | 		logrus.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	if err := r.LoadConfig(); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, service := range recipe.Config.Services { | 	for _, service := range r.Config.Services { | ||||||
| 		filters := filters.NewArgs() | 		filters := filters.NewArgs() | ||||||
| 		filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name)) | 		filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name)) | ||||||
|  |  | ||||||
| 		containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters}) | 		containers, err := cl.ContainerList(context.Background(), containerTypes.ListOptions{Filters: filters}) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -15,6 +15,7 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/recipe" | 	"coopcloud.tech/abra/pkg/recipe" | ||||||
| 	"coopcloud.tech/abra/pkg/upstream/stack" | 	"coopcloud.tech/abra/pkg/upstream/stack" | ||||||
| 	"github.com/docker/docker/api/types" | 	"github.com/docker/docker/api/types" | ||||||
|  | 	containerTypes "github.com/docker/docker/api/types/container" | ||||||
| 	"github.com/docker/docker/api/types/filters" | 	"github.com/docker/docker/api/types/filters" | ||||||
| 	"github.com/docker/docker/api/types/swarm" | 	"github.com/docker/docker/api/types/swarm" | ||||||
| 	dockerClient "github.com/docker/docker/client" | 	dockerClient "github.com/docker/docker/client" | ||||||
| @ -38,7 +39,12 @@ var appLogsCommand = cli.Command{ | |||||||
| 		app := internal.ValidateApp(c) | 		app := internal.ValidateApp(c) | ||||||
| 		stackName := app.StackName() | 		stackName := app.StackName() | ||||||
|  |  | ||||||
| 		if err := recipe.EnsureExists(app.Recipe); err != nil { | 		r, err := recipe.Get(app.Recipe) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err := r.EnsureExists(); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @ -110,7 +116,7 @@ func tailLogs(cl *dockerClient.Client, app config.App, serviceNames []string) er | |||||||
| 		// collected in parallel. | 		// collected in parallel. | ||||||
| 		wg.Add(1) | 		wg.Add(1) | ||||||
| 		go func(serviceID string) { | 		go func(serviceID string) { | ||||||
| 			logs, err := cl.ServiceLogs(context.Background(), serviceID, types.ContainerLogsOptions{ | 			logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{ | ||||||
| 				ShowStderr: true, | 				ShowStderr: true, | ||||||
| 				ShowStdout: !internal.StdErrOnly, | 				ShowStdout: !internal.StdErrOnly, | ||||||
| 				Since:      internal.SinceLogs, | 				Since:      internal.SinceLogs, | ||||||
|  | |||||||
| @ -10,8 +10,6 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/config" | 	"coopcloud.tech/abra/pkg/config" | ||||||
| 	"coopcloud.tech/abra/pkg/formatter" | 	"coopcloud.tech/abra/pkg/formatter" | ||||||
| 	"coopcloud.tech/abra/pkg/jsontable" | 	"coopcloud.tech/abra/pkg/jsontable" | ||||||
| 	"coopcloud.tech/abra/pkg/recipe" |  | ||||||
| 	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" | ||||||
| 	dockerClient "github.com/docker/docker/client" | 	dockerClient "github.com/docker/docker/client" | ||||||
| @ -66,23 +64,43 @@ var appNewCommand = cli.Command{ | |||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		recipe := internal.ValidateRecipe(c) | 		r := internal.ValidateRecipe(c) | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		if !internal.Chaos { | ||||||
| 			if err := recipePkg.EnsureIsClean(recipe.Name); err != nil { | 			if err := r.EnsureIsClean(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 			if !internal.Offline { | 			if !internal.Offline { | ||||||
| 				if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil { | 				if err := r.EnsureUpToDate(); err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 			if c.Args().Get(1) == "" { | 			if c.Args().Get(1) == "" { | ||||||
| 				if err := recipePkg.EnsureLatest(recipe.Name); err != nil { | 				var version string | ||||||
|  |  | ||||||
|  | 				recipeVersions, err := r.GetVersions(internal.Offline) | ||||||
|  | 				if err != nil { | ||||||
|  | 					logrus.Fatal(err) | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				// NOTE(d1): determine whether recipe versions exist or not and check | ||||||
|  | 				// out the latest version or current HEAD | ||||||
|  | 				if len(recipeVersions) > 0 { | ||||||
|  | 					latest := recipeVersions[len(recipeVersions)-1] | ||||||
|  | 					for tag := range latest { | ||||||
|  | 						version = tag | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					if err := r.EnsureVersion(version); err != nil { | ||||||
| 						logrus.Fatal(err) | 						logrus.Fatal(err) | ||||||
| 					} | 					} | ||||||
| 				} else { | 				} else { | ||||||
| 				if err := recipePkg.EnsureVersion(recipe.Name, c.Args().Get(1)); err != nil { | 					if err := r.EnsureLatest(); err != nil { | ||||||
|  | 						logrus.Fatal(err) | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				if err := r.EnsureVersion(c.Args().Get(1)); err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| @ -92,7 +110,7 @@ var appNewCommand = cli.Command{ | |||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if err := ensureDomainFlag(recipe, internal.NewAppServer); err != nil { | 		if err := ensureDomainFlag(r.Name, internal.NewAppServer); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @ -100,7 +118,7 @@ var appNewCommand = cli.Command{ | |||||||
| 		logrus.Debugf("%s sanitised as %s for new app", internal.Domain, sanitisedAppName) | 		logrus.Debugf("%s sanitised as %s for new app", internal.Domain, sanitisedAppName) | ||||||
|  |  | ||||||
| 		if err := config.TemplateAppEnvSample( | 		if err := config.TemplateAppEnvSample( | ||||||
| 			recipe.Name, | 			r.Name, | ||||||
| 			internal.Domain, | 			internal.Domain, | ||||||
| 			internal.NewAppServer, | 			internal.NewAppServer, | ||||||
| 			internal.Domain, | 			internal.Domain, | ||||||
| @ -111,23 +129,23 @@ var appNewCommand = cli.Command{ | |||||||
| 		var secrets AppSecrets | 		var secrets AppSecrets | ||||||
| 		var secretTable *jsontable.JSONTable | 		var secretTable *jsontable.JSONTable | ||||||
| 		if internal.Secrets { | 		if internal.Secrets { | ||||||
| 			sampleEnv, err := recipe.SampleEnv() | 			sampleEnv, err := r.SampleEnv() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			composeFiles, err := config.GetComposeFiles(recipe.Name, sampleEnv) | 			composeFiles, err := config.GetComposeFiles(r.NameEscaped, sampleEnv) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample") | 			envSamplePath := path.Join(r.Dir, ".env.sample") | ||||||
| 			secretsConfig, err := secret.ReadSecretsConfig(envSamplePath, composeFiles, config.StackName(internal.Domain)) | 			secretsConfig, err := secret.ReadSecretsConfig(envSamplePath, composeFiles, config.StackName(internal.Domain)) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err := promptForSecrets(recipe.Name, secretsConfig); err != nil { | 			if err := promptForSecrets(r.Name, secretsConfig); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| @ -154,9 +172,9 @@ var appNewCommand = cli.Command{ | |||||||
|  |  | ||||||
| 		tableCol := []string{"server", "recipe", "domain"} | 		tableCol := []string{"server", "recipe", "domain"} | ||||||
| 		table := formatter.CreateTable(tableCol) | 		table := formatter.CreateTable(tableCol) | ||||||
| 		table.Append([]string{internal.NewAppServer, recipe.Name, internal.Domain}) | 		table.Append([]string{internal.NewAppServer, r.Name, internal.Domain}) | ||||||
|  |  | ||||||
| 		fmt.Println(fmt.Sprintf("A new %s app has been created! Here is an overview:", recipe.Name)) | 		fmt.Println(fmt.Sprintf("A new %s app has been created! Here is an overview:", r.Name)) | ||||||
| 		fmt.Println("") | 		fmt.Println("") | ||||||
| 		table.Render() | 		table.Render() | ||||||
| 		fmt.Println("") | 		fmt.Println("") | ||||||
| @ -183,6 +201,12 @@ type AppSecrets map[string]string | |||||||
|  |  | ||||||
| // createSecrets creates all secrets for a new app. | // createSecrets creates all secrets for a new app. | ||||||
| func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secret, sanitisedAppName string) (AppSecrets, error) { | func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secret, sanitisedAppName string) (AppSecrets, error) { | ||||||
|  | 	// NOTE(d1): trim to match app.StackName() implementation | ||||||
|  | 	if len(sanitisedAppName) > config.MAX_SANITISED_APP_NAME_LENGTH { | ||||||
|  | 		logrus.Debugf("trimming %s to %s to avoid runtime limits", sanitisedAppName, sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH]) | ||||||
|  | 		sanitisedAppName = sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH] | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	secrets, err := secret.GenerateSecrets(cl, secretsConfig, internal.NewAppServer) | 	secrets, err := secret.GenerateSecrets(cl, secretsConfig, internal.NewAppServer) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| @ -206,11 +230,11 @@ func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secr | |||||||
| } | } | ||||||
|  |  | ||||||
| // ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/ | // ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/ | ||||||
| func ensureDomainFlag(recipe recipe.Recipe, server string) error { | func ensureDomainFlag(recipeName string, server string) error { | ||||||
| 	if internal.Domain == "" && !internal.NoInput { | 	if internal.Domain == "" && !internal.NoInput { | ||||||
| 		prompt := &survey.Input{ | 		prompt := &survey.Input{ | ||||||
| 			Message: "Specify app domain", | 			Message: "Specify app domain", | ||||||
| 			Default: fmt.Sprintf("%s.%s", recipe.Name, server), | 			Default: fmt.Sprintf("%s.%s", recipeName, server), | ||||||
| 		} | 		} | ||||||
| 		if err := survey.AskOne(prompt, &internal.Domain); err != nil { | 		if err := survey.AskOne(prompt, &internal.Domain); err != nil { | ||||||
| 			return err | 			return err | ||||||
|  | |||||||
							
								
								
									
										107
									
								
								cli/app/ps.go
									
									
									
									
									
								
							
							
						
						
									
										107
									
								
								cli/app/ps.go
									
									
									
									
									
								
							| @ -2,7 +2,8 @@ package app | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"strings" | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
| @ -10,11 +11,13 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/client" | 	"coopcloud.tech/abra/pkg/client" | ||||||
| 	"coopcloud.tech/abra/pkg/config" | 	"coopcloud.tech/abra/pkg/config" | ||||||
| 	"coopcloud.tech/abra/pkg/formatter" | 	"coopcloud.tech/abra/pkg/formatter" | ||||||
| 	"coopcloud.tech/abra/pkg/service" | 	"coopcloud.tech/abra/pkg/recipe" | ||||||
|  | 	abraService "coopcloud.tech/abra/pkg/service" | ||||||
| 	stack "coopcloud.tech/abra/pkg/upstream/stack" | 	stack "coopcloud.tech/abra/pkg/upstream/stack" | ||||||
| 	"github.com/buger/goterm" | 	"github.com/buger/goterm" | ||||||
| 	dockerFormatter "github.com/docker/cli/cli/command/formatter" | 	dockerFormatter "github.com/docker/cli/cli/command/formatter" | ||||||
| 	"github.com/docker/docker/api/types" | 	containerTypes "github.com/docker/docker/api/types/container" | ||||||
|  | 	"github.com/docker/docker/api/types/filters" | ||||||
| 	dockerClient "github.com/docker/docker/client" | 	dockerClient "github.com/docker/docker/client" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/urfave/cli" | 	"github.com/urfave/cli" | ||||||
| @ -27,6 +30,7 @@ var appPsCommand = cli.Command{ | |||||||
| 	ArgsUsage:   "<domain>", | 	ArgsUsage:   "<domain>", | ||||||
| 	Description: "Show a more detailed status output of a specific deployed app", | 	Description: "Show a more detailed status output of a specific deployed app", | ||||||
| 	Flags: []cli.Flag{ | 	Flags: []cli.Flag{ | ||||||
|  | 		internal.MachineReadableFlag, | ||||||
| 		internal.WatchFlag, | 		internal.WatchFlag, | ||||||
| 		internal.DebugFlag, | 		internal.DebugFlag, | ||||||
| 	}, | 	}, | ||||||
| @ -35,12 +39,17 @@ var appPsCommand = cli.Command{ | |||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		app := internal.ValidateApp(c) | 		app := internal.ValidateApp(c) | ||||||
|  |  | ||||||
|  | 		r, err := recipe.Get(app.Recipe) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		cl, err := client.New(app.Server) | 		cl, err := client.New(app.Server) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName()) | 		isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, app.StackName()) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
| @ -49,6 +58,15 @@ var appPsCommand = cli.Command{ | |||||||
| 			logrus.Fatalf("%s is not deployed?", app.Name) | 			logrus.Fatalf("%s is not deployed?", app.Name) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		statuses, err := config.GetAppStatuses([]config.App{app}, true) | ||||||
|  | 		if statusMeta, ok := statuses[app.StackName()]; ok { | ||||||
|  | 			if _, exists := statusMeta["chaos"]; !exists { | ||||||
|  | 				if err := r.EnsureVersion(deployedVersion); err != nil { | ||||||
|  | 					logrus.Fatal(err) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		if !internal.Watch { | 		if !internal.Watch { | ||||||
| 			showPSOutput(c, app, cl) | 			showPSOutput(c, app, cl) | ||||||
| 			return nil | 			return nil | ||||||
| @ -66,36 +84,77 @@ var appPsCommand = cli.Command{ | |||||||
|  |  | ||||||
| // showPSOutput renders ps output. | // showPSOutput renders ps output. | ||||||
| func showPSOutput(c *cli.Context, app config.App, cl *dockerClient.Client) { | func showPSOutput(c *cli.Context, app config.App, cl *dockerClient.Client) { | ||||||
| 	filters, err := app.Filters(true, true) | 	composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logrus.Fatal(err) | 		logrus.Fatal(err) | ||||||
|  | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters}) | 	deployOpts := stack.Deploy{ | ||||||
|  | 		Composefiles: composeFiles, | ||||||
|  | 		Namespace:    app.StackName(), | ||||||
|  | 		Prune:        false, | ||||||
|  | 		ResolveImage: stack.ResolveImageAlways, | ||||||
|  | 	} | ||||||
|  | 	compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logrus.Fatal(err) | 		logrus.Fatal(err) | ||||||
|  | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	var tablerows [][]string | ||||||
|  | 	allContainerStats := make(map[string]map[string]string) | ||||||
|  | 	for _, service := range compose.Services { | ||||||
|  | 		filters := filters.NewArgs() | ||||||
|  | 		filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name)) | ||||||
|  |  | ||||||
|  | 		containers, err := cl.ContainerList(context.Background(), containerTypes.ListOptions{Filters: filters}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		var containerStats map[string]string | ||||||
|  |  | ||||||
|  | 		if len(containers) == 0 { | ||||||
|  | 			containerStats = map[string]string{ | ||||||
|  | 				"service name": service.Name, | ||||||
|  | 				"image":        "unknown", | ||||||
|  | 				"created":      "unknown", | ||||||
|  | 				"status":       "unknown", | ||||||
|  | 				"state":        "unknown", | ||||||
|  | 				"ports":        "unknown", | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			container := containers[0] | ||||||
|  | 			containerStats = map[string]string{ | ||||||
|  | 				"service name": abraService.ContainerToServiceName(container.Names, app.StackName()), | ||||||
|  | 				"image":        formatter.RemoveSha(container.Image), | ||||||
|  | 				"created":      formatter.HumanDuration(container.Created), | ||||||
|  | 				"status":       container.Status, | ||||||
|  | 				"state":        container.State, | ||||||
|  | 				"ports":        dockerFormatter.DisplayablePorts(container.Ports), | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		allContainerStats[containerStats["service name"]] = containerStats | ||||||
|  |  | ||||||
|  | 		var tablerow []string = []string{containerStats["service name"], containerStats["image"], containerStats["created"], containerStats["status"], containerStats["state"], containerStats["ports"]} | ||||||
|  | 		tablerows = append(tablerows, tablerow) | ||||||
|  | 	} | ||||||
|  | 	if internal.MachineReadable { | ||||||
|  | 		jsonstring, err := json.Marshal(allContainerStats) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} else { | ||||||
|  | 			fmt.Println(string(jsonstring)) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} else { | ||||||
| 		tableCol := []string{"service name", "image", "created", "status", "state", "ports"} | 		tableCol := []string{"service name", "image", "created", "status", "state", "ports"} | ||||||
| 		table := formatter.CreateTable(tableCol) | 		table := formatter.CreateTable(tableCol) | ||||||
|  | 		for _, row := range tablerows { | ||||||
| 	for _, container := range containers { | 			table.Append(row) | ||||||
| 		var containerNames []string |  | ||||||
| 		for _, containerName := range container.Names { |  | ||||||
| 			trimmed := strings.TrimPrefix(containerName, "/") |  | ||||||
| 			containerNames = append(containerNames, trimmed) |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		tableRow := []string{ |  | ||||||
| 			service.ContainerToServiceName(container.Names, app.StackName()), |  | ||||||
| 			formatter.RemoveSha(container.Image), |  | ||||||
| 			formatter.HumanDuration(container.Created), |  | ||||||
| 			container.Status, |  | ||||||
| 			container.State, |  | ||||||
| 			dockerFormatter.DisplayablePorts(container.Ports), |  | ||||||
| 		} |  | ||||||
| 		table.Append(tableRow) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 		table.Render() | 		table.Render() | ||||||
| 	} | 	} | ||||||
|  | } | ||||||
|  | |||||||
| @ -5,7 +5,6 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"log" | 	"log" | ||||||
| 	"os" | 	"os" | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
| 	"coopcloud.tech/abra/pkg/autocomplete" | 	"coopcloud.tech/abra/pkg/autocomplete" | ||||||
| @ -13,7 +12,6 @@ import ( | |||||||
| 	stack "coopcloud.tech/abra/pkg/upstream/stack" | 	stack "coopcloud.tech/abra/pkg/upstream/stack" | ||||||
| 	"github.com/AlecAivazis/survey/v2" | 	"github.com/AlecAivazis/survey/v2" | ||||||
| 	"github.com/docker/docker/api/types" | 	"github.com/docker/docker/api/types" | ||||||
| 	"github.com/docker/docker/api/types/volume" |  | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/urfave/cli" | 	"github.com/urfave/cli" | ||||||
| ) | ) | ||||||
| @ -112,28 +110,19 @@ flag. | |||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		volumeListOptions := volume.ListOptions{fs} | 		volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, fs) | ||||||
| 		volumeListOKBody, err := cl.VolumeList(context.Background(), volumeListOptions) |  | ||||||
| 		volumeList := volumeListOKBody.Volumes |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  | 		volumeNames := client.GetVolumeNames(volumeList) | ||||||
|  |  | ||||||
| 		var vols []string | 		if len(volumeNames) > 0 { | ||||||
| 		for _, vol := range volumeList { | 			err := client.RemoveVolumes(cl, context.Background(), volumeNames, internal.Force, 5) | ||||||
| 			vols = append(vols, vol.Name) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if len(vols) > 0 { |  | ||||||
| 			for _, vol := range vols { |  | ||||||
| 				err = retryFunc(5, func() error { |  | ||||||
| 					return cl.VolumeRemove(context.Background(), vol, internal.Force) // last argument is for force removing |  | ||||||
| 				}) |  | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				log.Fatalf("removing volumes failed: %s", err) | 				log.Fatalf("removing volumes failed: %s", err) | ||||||
| 			} | 			} | ||||||
| 				logrus.Info(fmt.Sprintf("volume %s removed", vol)) |  | ||||||
| 			} | 			logrus.Infof("%d volumes removed successfully", len(volumeNames)) | ||||||
| 		} else { | 		} else { | ||||||
| 			logrus.Info("no volumes to remove") | 			logrus.Info("no volumes to remove") | ||||||
| 		} | 		} | ||||||
| @ -147,21 +136,3 @@ flag. | |||||||
| 		return nil | 		return nil | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
| // retryFunc retries the given function for the given retries. After the nth |  | ||||||
| // retry it waits (n + 1)^2 seconds before the next retry (starting with n=0). |  | ||||||
| // It returns an error if the function still failed after the last retry. |  | ||||||
| func retryFunc(retries int, fn func() error) error { |  | ||||||
| 	for i := 0; i < retries; i++ { |  | ||||||
| 		err := fn() |  | ||||||
| 		if err == nil { |  | ||||||
| 			return nil |  | ||||||
| 		} |  | ||||||
| 		if i+1 < retries { |  | ||||||
| 			sleep := time.Duration(i+1) * time.Duration(i+1) |  | ||||||
| 			logrus.Infof("%s: waiting %d seconds before next retry", err, sleep) |  | ||||||
| 			time.Sleep(sleep * time.Second) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return fmt.Errorf("%d retries failed", retries) |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -33,22 +33,27 @@ var appRestoreCommand = cli.Command{ | |||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		app := internal.ValidateApp(c) | 		app := internal.ValidateApp(c) | ||||||
|  |  | ||||||
| 		if err := recipe.EnsureExists(app.Recipe); err != nil { | 		r, err := recipe.Get(app.Recipe) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err := r.EnsureExists(); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		if !internal.Chaos { | ||||||
| 			if err := recipe.EnsureIsClean(app.Recipe); err != nil { | 			if err := r.EnsureIsClean(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if !internal.Offline { | 			if !internal.Offline { | ||||||
| 				if err := recipe.EnsureUpToDate(app.Recipe); err != nil { | 				if err := r.EnsureUpToDate(); err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err := recipe.EnsureLatest(app.Recipe); err != nil { | 			if err := r.EnsureLatest(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -51,33 +51,37 @@ recipes. | |||||||
| 		app := internal.ValidateApp(c) | 		app := internal.ValidateApp(c) | ||||||
| 		stackName := app.StackName() | 		stackName := app.StackName() | ||||||
|  |  | ||||||
|  | 		r, err := recipe.Get(app.Recipe) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		specificVersion := c.Args().Get(1) | 		specificVersion := c.Args().Get(1) | ||||||
| 		if specificVersion != "" && internal.Chaos { | 		if specificVersion != "" && internal.Chaos { | ||||||
| 			logrus.Fatal("cannot use <version> and --chaos together") | 			logrus.Fatal("cannot use <version> and --chaos together") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if err := recipe.EnsureExists(app.Recipe); err != nil { | 		if err := r.EnsureExists(); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		if !internal.Chaos { | ||||||
| 			if err := recipe.EnsureIsClean(app.Recipe); err != nil { | 			if err := r.EnsureIsClean(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if !internal.Offline { | 			if !internal.Offline { | ||||||
| 				if err := recipe.EnsureUpToDate(app.Recipe); err != nil { | 				if err := r.EnsureUpToDate(); err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err := recipe.EnsureLatest(app.Recipe); err != nil { | 			if err := r.EnsureLatest(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		r, err := recipe.Get(app.Recipe, internal.Offline) | 		if err := r.LoadConfig(); err != nil { | ||||||
| 		if err != nil { |  | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @ -113,7 +117,7 @@ recipes. | |||||||
|  |  | ||||||
| 		if len(versions) == 0 && !internal.Chaos { | 		if len(versions) == 0 && !internal.Chaos { | ||||||
| 			logrus.Warn("no published versions in catalogue, trying local recipe repository") | 			logrus.Warn("no published versions in catalogue, trying local recipe repository") | ||||||
| 			recipeVersions, err := recipe.GetRecipeVersions(app.Recipe, internal.Offline) | 			recipeVersions, err := r.GetVersions(internal.Offline) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				logrus.Warn(err) | 				logrus.Warn(err) | ||||||
| 			} | 			} | ||||||
| @ -183,7 +187,7 @@ recipes. | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		if !internal.Chaos { | ||||||
| 			if err := recipe.EnsureVersion(app.Recipe, chosenDowngrade); err != nil { | 			if err := r.EnsureVersion(chosenDowngrade); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @ -191,13 +195,13 @@ recipes. | |||||||
| 		if internal.Chaos { | 		if internal.Chaos { | ||||||
| 			logrus.Warn("chaos mode engaged") | 			logrus.Warn("chaos mode engaged") | ||||||
| 			var err error | 			var err error | ||||||
| 			chosenDowngrade, err = recipe.ChaosVersion(app.Recipe) | 			chosenDowngrade, err = r.ChaosVersion() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh") | 		abraShPath := fmt.Sprintf("%s/%s/%s", r.Dir, "abra.sh") | ||||||
| 		abraShEnv, err := config.ReadAbraShEnvVars(abraShPath) | 		abraShEnv, err := config.ReadAbraShEnvVars(abraShPath) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
| 	"coopcloud.tech/abra/pkg/autocomplete" | 	"coopcloud.tech/abra/pkg/autocomplete" | ||||||
| @ -56,22 +57,27 @@ var appSecretGenerateCommand = cli.Command{ | |||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		app := internal.ValidateApp(c) | 		app := internal.ValidateApp(c) | ||||||
|  |  | ||||||
| 		if err := recipe.EnsureExists(app.Recipe); err != nil { | 		r, err := recipe.Get(app.Recipe) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err := r.EnsureExists(); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		if !internal.Chaos { | ||||||
| 			if err := recipe.EnsureIsClean(app.Recipe); err != nil { | 			if err := r.EnsureIsClean(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if !internal.Offline { | 			if !internal.Offline { | ||||||
| 				if err := recipe.EnsureUpToDate(app.Recipe); err != nil { | 				if err := r.EnsureUpToDate(); err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err := recipe.EnsureLatest(app.Recipe); err != nil { | 			if err := r.EnsureLatest(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @ -156,6 +162,8 @@ var appSecretInsertCommand = cli.Command{ | |||||||
| 	Flags: []cli.Flag{ | 	Flags: []cli.Flag{ | ||||||
| 		internal.DebugFlag, | 		internal.DebugFlag, | ||||||
| 		internal.PassFlag, | 		internal.PassFlag, | ||||||
|  | 		internal.FileFlag, | ||||||
|  | 		internal.TrimFlag, | ||||||
| 	}, | 	}, | ||||||
| 	Before:       internal.SubCommandBefore, | 	Before:       internal.SubCommandBefore, | ||||||
| 	ArgsUsage:    "<domain> <secret-name> <version> <data>", | 	ArgsUsage:    "<domain> <secret-name> <version> <data>", | ||||||
| @ -188,6 +196,18 @@ Example: | |||||||
| 		version := c.Args().Get(2) | 		version := c.Args().Get(2) | ||||||
| 		data := c.Args().Get(3) | 		data := c.Args().Get(3) | ||||||
|  |  | ||||||
|  | 		if internal.File { | ||||||
|  | 			raw, err := os.ReadFile(data) | ||||||
|  | 			if err != nil { | ||||||
|  | 				logrus.Fatalf("reading secret from file: %s", err) | ||||||
|  | 			} | ||||||
|  | 			data = string(raw) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if internal.Trim { | ||||||
|  | 			data = strings.TrimSpace(data) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		secretName := fmt.Sprintf("%s_%s_%s", app.StackName(), name, version) | 		secretName := fmt.Sprintf("%s_%s_%s", app.StackName(), name, version) | ||||||
| 		if err := client.StoreSecret(cl, secretName, data, app.Server); err != nil { | 		if err := client.StoreSecret(cl, secretName, data, app.Server); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| @ -249,22 +269,27 @@ Example: | |||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		app := internal.ValidateApp(c) | 		app := internal.ValidateApp(c) | ||||||
|  |  | ||||||
| 		if err := recipe.EnsureExists(app.Recipe); err != nil { | 		r, err := recipe.Get(app.Recipe) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err := r.EnsureExists(); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		if !internal.Chaos { | ||||||
| 			if err := recipe.EnsureIsClean(app.Recipe); err != nil { | 			if err := r.EnsureIsClean(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if !internal.Offline { | 			if !internal.Offline { | ||||||
| 				if err := recipe.EnsureUpToDate(app.Recipe); err != nil { | 				if err := r.EnsureUpToDate(); err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err := recipe.EnsureLatest(app.Recipe); err != nil { | 			if err := r.EnsureLatest(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @ -357,22 +382,27 @@ var appSecretLsCommand = cli.Command{ | |||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		app := internal.ValidateApp(c) | 		app := internal.ValidateApp(c) | ||||||
|  |  | ||||||
| 		if err := recipe.EnsureExists(app.Recipe); err != nil { | 		r, err := recipe.Get(app.Recipe) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err := r.EnsureExists(); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		if !internal.Chaos { | ||||||
| 			if err := recipe.EnsureIsClean(app.Recipe); err != nil { | 			if err := r.EnsureIsClean(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if !internal.Offline { | 			if !internal.Offline { | ||||||
| 				if err := recipe.EnsureUpToDate(app.Recipe); err != nil { | 				if err := r.EnsureUpToDate(); err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err := recipe.EnsureLatest(app.Recipe); err != nil { | 			if err := r.EnsureLatest(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -11,7 +11,7 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/formatter" | 	"coopcloud.tech/abra/pkg/formatter" | ||||||
| 	"coopcloud.tech/abra/pkg/service" | 	"coopcloud.tech/abra/pkg/service" | ||||||
| 	stack "coopcloud.tech/abra/pkg/upstream/stack" | 	stack "coopcloud.tech/abra/pkg/upstream/stack" | ||||||
| 	"github.com/docker/docker/api/types" | 	containerTypes "github.com/docker/docker/api/types/container" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/urfave/cli" | 	"github.com/urfave/cli" | ||||||
| ) | ) | ||||||
| @ -48,7 +48,7 @@ var appServicesCommand = cli.Command{ | |||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters}) | 		containers, err := cl.ContainerList(context.Background(), containerTypes.ListOptions{Filters: filters}) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -31,6 +31,7 @@ var appUpgradeCommand = cli.Command{ | |||||||
| 		internal.NoDomainChecksFlag, | 		internal.NoDomainChecksFlag, | ||||||
| 		internal.DontWaitConvergeFlag, | 		internal.DontWaitConvergeFlag, | ||||||
| 		internal.OfflineFlag, | 		internal.OfflineFlag, | ||||||
|  | 		internal.ReleaseNotesFlag, | ||||||
| 	}, | 	}, | ||||||
| 	Before: internal.SubCommandBefore, | 	Before: internal.SubCommandBefore, | ||||||
| 	Description: ` | 	Description: ` | ||||||
| @ -61,28 +62,32 @@ recipes. | |||||||
| 			logrus.Fatal("cannot use <version> and --chaos together") | 			logrus.Fatal("cannot use <version> and --chaos together") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		r, err := recipe.Get(app.Recipe) | ||||||
| 			if err := recipe.EnsureIsClean(app.Recipe); err != nil { |  | ||||||
| 				logrus.Fatal(err) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if !internal.Offline { |  | ||||||
| 				if err := recipe.EnsureUpToDate(app.Recipe); err != nil { |  | ||||||
| 					logrus.Fatal(err) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if err := recipe.EnsureLatest(app.Recipe); err != nil { |  | ||||||
| 				logrus.Fatal(err) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		recipe, err := recipePkg.Get(app.Recipe, internal.Offline) |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if err := lint.LintForErrors(recipe); err != nil { | 		if !internal.Chaos { | ||||||
|  | 			if err := r.EnsureIsClean(); err != nil { | ||||||
|  | 				logrus.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if !internal.Offline { | ||||||
|  | 				if err := r.EnsureUpToDate(); err != nil { | ||||||
|  | 					logrus.Fatal(err) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if err := r.EnsureLatest(); err != nil { | ||||||
|  | 				logrus.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err := r.LoadConfig(); err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err := lint.LintForErrors(r); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @ -114,7 +119,7 @@ recipes. | |||||||
|  |  | ||||||
| 		if len(versions) == 0 && !internal.Chaos { | 		if len(versions) == 0 && !internal.Chaos { | ||||||
| 			logrus.Warn("no published versions in catalogue, trying local recipe repository") | 			logrus.Warn("no published versions in catalogue, trying local recipe repository") | ||||||
| 			recipeVersions, err := recipePkg.GetRecipeVersions(app.Recipe, internal.Offline) | 			recipeVersions, err := r.GetVersions(internal.Offline) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				logrus.Warn(err) | 				logrus.Warn(err) | ||||||
| 			} | 			} | ||||||
| @ -193,17 +198,17 @@ recipes. | |||||||
| 		// check out the tag and then they'll appear to be missing. this covers | 		// check out the tag and then they'll appear to be missing. this covers | ||||||
| 		// when we obviously will forget to write release notes before publishing | 		// when we obviously will forget to write release notes before publishing | ||||||
| 		var releaseNotes string | 		var releaseNotes string | ||||||
|  | 		if chosenUpgrade != "" { | ||||||
|  | 			parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade) | ||||||
|  | 			if err != nil { | ||||||
|  | 				logrus.Fatal(err) | ||||||
|  | 			} | ||||||
| 			for _, version := range versions { | 			for _, version := range versions { | ||||||
| 				parsedVersion, err := tagcmp.Parse(version) | 				parsedVersion, err := tagcmp.Parse(version) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| 			parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade) | 				if parsedVersion.IsGreaterThan(parsedDeployedVersion) && parsedVersion.IsLessThan(parsedChosenUpgrade) { | ||||||
| 			if err != nil { |  | ||||||
| 				logrus.Fatal(err) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if !(parsedVersion.Equals(parsedDeployedVersion)) && parsedVersion.IsLessThan(parsedChosenUpgrade) { |  | ||||||
| 					note, err := internal.GetReleaseNotes(app.Recipe, version) | 					note, err := internal.GetReleaseNotes(app.Recipe, version) | ||||||
| 					if err != nil { | 					if err != nil { | ||||||
| 						return err | 						return err | ||||||
| @ -213,9 +218,10 @@ recipes. | |||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		if !internal.Chaos { | ||||||
| 			if err := recipePkg.EnsureVersion(app.Recipe, chosenUpgrade); err != nil { | 			if err := r.EnsureVersion(chosenUpgrade); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @ -223,13 +229,13 @@ recipes. | |||||||
| 		if internal.Chaos { | 		if internal.Chaos { | ||||||
| 			logrus.Warn("chaos mode engaged") | 			logrus.Warn("chaos mode engaged") | ||||||
| 			var err error | 			var err error | ||||||
| 			chosenUpgrade, err = recipePkg.ChaosVersion(app.Recipe) | 			chosenUpgrade, err = r.ChaosVersion() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh") | 		abraShPath := fmt.Sprintf("%s/%s/%s", r.Dir, "abra.sh") | ||||||
| 		abraShEnv, err := config.ReadAbraShEnvVars(abraShPath) | 		abraShEnv, err := config.ReadAbraShEnvVars(abraShPath) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| @ -269,6 +275,12 @@ recipes. | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		if internal.ReleaseNotes { | ||||||
|  | 			fmt.Println() | ||||||
|  | 			fmt.Print(releaseNotes) | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		if err := internal.NewVersionOverview(app, deployedVersion, chosenUpgrade, releaseNotes); err != nil { | 		if err := internal.NewVersionOverview(app, deployedVersion, chosenUpgrade, releaseNotes); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/formatter" | 	"coopcloud.tech/abra/pkg/formatter" | ||||||
| 	"coopcloud.tech/abra/pkg/recipe" | 	"coopcloud.tech/abra/pkg/recipe" | ||||||
| 	"coopcloud.tech/abra/pkg/upstream/stack" | 	"coopcloud.tech/abra/pkg/upstream/stack" | ||||||
| 	"github.com/docker/distribution/reference" | 	"github.com/distribution/reference" | ||||||
| 	"github.com/olekukonko/tablewriter" | 	"github.com/olekukonko/tablewriter" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/urfave/cli" | 	"github.com/urfave/cli" | ||||||
| @ -58,6 +58,11 @@ var appVersionCommand = cli.Command{ | |||||||
| 		app := internal.ValidateApp(c) | 		app := internal.ValidateApp(c) | ||||||
| 		stackName := app.StackName() | 		stackName := app.StackName() | ||||||
|  |  | ||||||
|  | 		r, err := recipe.Get(app.Recipe) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		cl, err := client.New(app.Server) | 		cl, err := client.New(app.Server) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| @ -78,7 +83,7 @@ var appVersionCommand = cli.Command{ | |||||||
| 			logrus.Fatalf("failed to determine version of deployed %s", app.Name) | 			logrus.Fatalf("failed to determine version of deployed %s", app.Name) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		recipeMeta, err := recipe.GetRecipeMeta(app.Recipe, internal.Offline) | 		recipeMeta, err := r.GetRecipeMeta(internal.Offline) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ package app | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"log" | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
| 	"coopcloud.tech/abra/pkg/autocomplete" | 	"coopcloud.tech/abra/pkg/autocomplete" | ||||||
| @ -131,12 +132,12 @@ Passing "--force/-f" will select all volumes for removal. Be careful. | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if len(volumesToRemove) > 0 { | 		if len(volumesToRemove) > 0 { | ||||||
| 			err = client.RemoveVolumes(cl, context.Background(), app.Server, volumesToRemove, internal.Force) | 			err := client.RemoveVolumes(cl, context.Background(), volumesToRemove, internal.Force, 5) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				logrus.Fatal(err) | 				log.Fatalf("removing volumes failed: %s", err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			logrus.Info("volumes removed successfully") | 			logrus.Infof("%d volumes removed successfully", len(volumesToRemove)) | ||||||
| 		} else { | 		} else { | ||||||
| 			logrus.Info("no volumes removed") | 			logrus.Info("no volumes removed") | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ import ( | |||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
|  | 	"os" | ||||||
| 	"path" | 	"path" | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
| @ -57,9 +58,10 @@ keys configured on your account. | |||||||
| 	BashComplete: autocomplete.RecipeNameComplete, | 	BashComplete: autocomplete.RecipeNameComplete, | ||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		recipeName := c.Args().First() | 		recipeName := c.Args().First() | ||||||
|  | 		r := recipe.Recipe{} | ||||||
|  |  | ||||||
| 		if recipeName != "" { | 		if recipeName != "" { | ||||||
| 			internal.ValidateRecipe(c) | 			r = internal.ValidateRecipe(c) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		if !internal.Chaos { | ||||||
| @ -98,12 +100,12 @@ keys configured on your account. | |||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			versions, err := recipe.GetRecipeVersions(recipeMeta.Name, internal.Offline) | 			versions, err := r.GetVersions(internal.Offline) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				logrus.Warn(err) | 				logrus.Warn(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			features, category, err := recipe.GetRecipeFeaturesAndCategory(recipeMeta.Name) | 			features, category, err := r.GetRecipeFeaturesAndCategory() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				logrus.Warn(err) | 				logrus.Warn(err) | ||||||
| 			} | 			} | ||||||
| @ -130,7 +132,7 @@ keys configured on your account. | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if recipeName == "" { | 		if recipeName == "" { | ||||||
| 			if err := ioutil.WriteFile(config.RECIPES_JSON, recipesJSON, 0764); err != nil { | 			if err := os.WriteFile(config.RECIPES_JSON, recipesJSON, 0764); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} else { | ||||||
|  | |||||||
| @ -38,6 +38,20 @@ var PassRemoveFlag = &cli.BoolFlag{ | |||||||
| 	Destination: &PassRemove, | 	Destination: &PassRemove, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | var File bool | ||||||
|  | var FileFlag = &cli.BoolFlag{ | ||||||
|  | 	Name:        "file, f", | ||||||
|  | 	Usage:       "Treat input as a file", | ||||||
|  | 	Destination: &File, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var Trim bool | ||||||
|  | var TrimFlag = &cli.BoolFlag{ | ||||||
|  | 	Name:        "trim, t", | ||||||
|  | 	Usage:       "Trim input", | ||||||
|  | 	Destination: &Trim, | ||||||
|  | } | ||||||
|  |  | ||||||
| // Force force functionality without asking. | // Force force functionality without asking. | ||||||
| var Force bool | var Force bool | ||||||
|  |  | ||||||
| @ -95,6 +109,16 @@ var OfflineFlag = &cli.BoolFlag{ | |||||||
| 	Usage:       "Prefer offline & filesystem access when possible", | 	Usage:       "Prefer offline & filesystem access when possible", | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // ReleaseNotes stores the variable from ReleaseNotesFlag. | ||||||
|  | var ReleaseNotes bool | ||||||
|  |  | ||||||
|  | // ReleaseNotesFlag turns on/off printing only release notes when upgrading. | ||||||
|  | var ReleaseNotesFlag = &cli.BoolFlag{ | ||||||
|  | 	Name:        "releasenotes, r", | ||||||
|  | 	Destination: &ReleaseNotes, | ||||||
|  | 	Usage:       "Only show release notes", | ||||||
|  | } | ||||||
|  |  | ||||||
| // MachineReadable stores the variable from MachineReadableFlag | // MachineReadable stores the variable from MachineReadableFlag | ||||||
| var MachineReadable bool | var MachineReadable bool | ||||||
|  |  | ||||||
| @ -238,6 +262,22 @@ var RemoteUserFlag = &cli.StringFlag{ | |||||||
| 	Destination: &RemoteUser, | 	Destination: &RemoteUser, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | var GitName string | ||||||
|  | var GitNameFlag = &cli.StringFlag{ | ||||||
|  | 	Name:        "git-name, gn", | ||||||
|  | 	Value:       "", | ||||||
|  | 	Usage:       "Git (user) name to do commits with", | ||||||
|  | 	Destination: &GitName, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var GitEmail string | ||||||
|  | var GitEmailFlag = &cli.StringFlag{ | ||||||
|  | 	Name:        "git-email, ge", | ||||||
|  | 	Value:       "", | ||||||
|  | 	Usage:       "Git email name to do commits with", | ||||||
|  | 	Destination: &GitEmail, | ||||||
|  | } | ||||||
|  |  | ||||||
| // SubCommandBefore wires up pre-action machinery (e.g. --debug handling). | // SubCommandBefore wires up pre-action machinery (e.g. --debug handling). | ||||||
| func SubCommandBefore(c *cli.Context) error { | func SubCommandBefore(c *cli.Context) error { | ||||||
| 	if Debug { | 	if Debug { | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/formatter" | 	"coopcloud.tech/abra/pkg/formatter" | ||||||
| 	"coopcloud.tech/abra/pkg/recipe" | 	"coopcloud.tech/abra/pkg/recipe" | ||||||
| 	"github.com/AlecAivazis/survey/v2" | 	"github.com/AlecAivazis/survey/v2" | ||||||
| 	"github.com/docker/distribution/reference" | 	"github.com/distribution/reference" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | |||||||
| @ -57,7 +57,10 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe { | |||||||
| 		ShowSubcommandHelpAndError(c, errors.New("no recipe name provided")) | 		ShowSubcommandHelpAndError(c, errors.New("no recipe name provided")) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	chosenRecipe, err := recipe.Get(recipeName, Offline) | 	r, err := recipe.Get(recipeName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logrus.Fatal(err) | ||||||
|  | 	} | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if c.Command.Name == "generate" { | 		if c.Command.Name == "generate" { | ||||||
| 			if strings.Contains(err.Error(), "missing a compose") { | 			if strings.Contains(err.Error(), "missing a compose") { | ||||||
| @ -74,7 +77,7 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe { | |||||||
|  |  | ||||||
| 	logrus.Debugf("validated %s as recipe argument", recipeName) | 	logrus.Debugf("validated %s as recipe argument", recipeName) | ||||||
|  |  | ||||||
| 	return chosenRecipe | 	return r | ||||||
| } | } | ||||||
|  |  | ||||||
| // ValidateApp ensures the app name arg is valid. | // ValidateApp ensures the app name arg is valid. | ||||||
|  | |||||||
| @ -24,9 +24,14 @@ var recipeFetchCommand = cli.Command{ | |||||||
| 	BashComplete: autocomplete.RecipeNameComplete, | 	BashComplete: autocomplete.RecipeNameComplete, | ||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		recipeName := c.Args().First() | 		recipeName := c.Args().First() | ||||||
|  |  | ||||||
| 		if recipeName != "" { | 		if recipeName != "" { | ||||||
| 			internal.ValidateRecipe(c) | 			internal.ValidateRecipe(c) | ||||||
| 			if err := recipe.Ensure(recipeName); err != nil { | 			r, err := recipe.Get(recipeName) | ||||||
|  | 			if err != nil { | ||||||
|  | 				logrus.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			if err := r.Ensure(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 			return nil | 			return nil | ||||||
| @ -39,7 +44,11 @@ var recipeFetchCommand = cli.Command{ | |||||||
|  |  | ||||||
| 		catlBar := formatter.CreateProgressbar(len(catalogue), "fetching latest recipes...") | 		catlBar := formatter.CreateProgressbar(len(catalogue), "fetching latest recipes...") | ||||||
| 		for recipeName := range catalogue { | 		for recipeName := range catalogue { | ||||||
| 			if err := recipe.Ensure(recipeName); err != nil { | 			r, err := recipe.Get(recipeName) | ||||||
|  | 			if err != nil { | ||||||
|  | 				logrus.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			if err := r.Ensure(); err != nil { | ||||||
| 				logrus.Error(err) | 				logrus.Error(err) | ||||||
| 			} | 			} | ||||||
| 			catlBar.Add(1) | 			catlBar.Add(1) | ||||||
|  | |||||||
| @ -7,7 +7,6 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/autocomplete" | 	"coopcloud.tech/abra/pkg/autocomplete" | ||||||
| 	"coopcloud.tech/abra/pkg/formatter" | 	"coopcloud.tech/abra/pkg/formatter" | ||||||
| 	"coopcloud.tech/abra/pkg/lint" | 	"coopcloud.tech/abra/pkg/lint" | ||||||
| 	recipePkg "coopcloud.tech/abra/pkg/recipe" |  | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/urfave/cli" | 	"github.com/urfave/cli" | ||||||
| ) | ) | ||||||
| @ -27,24 +26,27 @@ var recipeLintCommand = cli.Command{ | |||||||
| 	Before:       internal.SubCommandBefore, | 	Before:       internal.SubCommandBefore, | ||||||
| 	BashComplete: autocomplete.RecipeNameComplete, | 	BashComplete: autocomplete.RecipeNameComplete, | ||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		recipe := internal.ValidateRecipe(c) | 		r := internal.ValidateRecipe(c) | ||||||
|  | 		if err := r.LoadConfig(); err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		if err := recipePkg.EnsureExists(recipe.Name); err != nil { | 		if err := r.EnsureExists(); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		if !internal.Chaos { | ||||||
| 			if err := recipePkg.EnsureIsClean(recipe.Name); err != nil { | 			if err := r.EnsureIsClean(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if !internal.Offline { | 			if !internal.Offline { | ||||||
| 				if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil { | 				if err := r.EnsureUpToDate(); err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err := recipePkg.EnsureLatest(recipe.Name); err != nil { | 			if err := r.EnsureLatest(); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @ -62,7 +64,7 @@ var recipeLintCommand = cli.Command{ | |||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				skipped := false | 				skipped := false | ||||||
| 				if rule.Skip(recipe) { | 				if rule.Skip(r) { | ||||||
| 					skipped = true | 					skipped = true | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| @ -73,7 +75,7 @@ var recipeLintCommand = cli.Command{ | |||||||
|  |  | ||||||
| 				satisfied := false | 				satisfied := false | ||||||
| 				if !skipped { | 				if !skipped { | ||||||
| 					ok, err := rule.Function(recipe) | 					ok, err := rule.Function(r) | ||||||
| 					if err != nil { | 					if err != nil { | ||||||
| 						logrus.Warn(err) | 						logrus.Warn(err) | ||||||
| 					} | 					} | ||||||
|  | |||||||
| @ -4,7 +4,6 @@ import ( | |||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" |  | ||||||
| 	"os" | 	"os" | ||||||
| 	"path" | 	"path" | ||||||
| 	"text/template" | 	"text/template" | ||||||
| @ -37,6 +36,8 @@ var recipeNewCommand = cli.Command{ | |||||||
| 		internal.DebugFlag, | 		internal.DebugFlag, | ||||||
| 		internal.NoInputFlag, | 		internal.NoInputFlag, | ||||||
| 		internal.OfflineFlag, | 		internal.OfflineFlag, | ||||||
|  | 		internal.GitNameFlag, | ||||||
|  | 		internal.GitEmailFlag, | ||||||
| 	}, | 	}, | ||||||
| 	Before:    internal.SubCommandBefore, | 	Before:    internal.SubCommandBefore, | ||||||
| 	Usage:     "Create a new recipe", | 	Usage:     "Create a new recipe", | ||||||
| @ -92,14 +93,14 @@ recipe and domain in the sample environment config). | |||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err := ioutil.WriteFile(path, templated.Bytes(), 0644); err != nil { | 			if err := os.WriteFile(path, templated.Bytes(), 0644); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		newGitRepo := path.Join(config.RECIPES_DIR, recipeName) | 		newGitRepo := path.Join(config.RECIPES_DIR, recipeName) | ||||||
| 		if err := git.Init(newGitRepo, true); err != nil { | 		if err := git.Init(newGitRepo, true, internal.GitName, internal.GitEmail); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ import ( | |||||||
| 	recipePkg "coopcloud.tech/abra/pkg/recipe" | 	recipePkg "coopcloud.tech/abra/pkg/recipe" | ||||||
| 	"coopcloud.tech/tagcmp" | 	"coopcloud.tech/tagcmp" | ||||||
| 	"github.com/AlecAivazis/survey/v2" | 	"github.com/AlecAivazis/survey/v2" | ||||||
| 	"github.com/docker/distribution/reference" | 	"github.com/distribution/reference" | ||||||
| 	"github.com/go-git/go-git/v5" | 	"github.com/go-git/go-git/v5" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/urfave/cli" | 	"github.com/urfave/cli" | ||||||
| @ -108,14 +108,14 @@ your SSH keys configured on your account. | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		isClean, err := gitPkg.IsClean(recipe.Dir()) | 		isClean, err := gitPkg.IsClean(recipe.Dir) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !isClean { | 		if !isClean { | ||||||
| 			logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name) | 			logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name) | ||||||
| 			if err := gitPkg.DiffUnstaged(recipe.Dir()); err != nil { | 			if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @ -184,7 +184,7 @@ func getImageVersions(recipe recipe.Recipe) (map[string]string, error) { | |||||||
| func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error { | func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error { | ||||||
| 	var err error | 	var err error | ||||||
|  |  | ||||||
| 	directory := path.Join(config.RECIPES_DIR, recipe.Name) | 	directory := path.Join(recipe.Dir) | ||||||
| 	repo, err := git.PlainOpen(directory) | 	repo, err := git.PlainOpen(directory) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| @ -246,8 +246,7 @@ func getTagCreateOptions(tag string) (git.CreateTagOptions, error) { | |||||||
| // addReleaseNotes checks if the release/next release note exists and moves the | // addReleaseNotes checks if the release/next release note exists and moves the | ||||||
| // file to release/<tag>. | // file to release/<tag>. | ||||||
| func addReleaseNotes(recipe recipe.Recipe, tag string) error { | func addReleaseNotes(recipe recipe.Recipe, tag string) error { | ||||||
| 	repoPath := path.Join(config.RECIPES_DIR, recipe.Name) | 	tagReleaseNotePath := path.Join(recipe.Dir, "release", tag) | ||||||
| 	tagReleaseNotePath := path.Join(repoPath, "release", tag) |  | ||||||
| 	if _, err := os.Stat(tagReleaseNotePath); err == nil { | 	if _, err := os.Stat(tagReleaseNotePath); err == nil { | ||||||
| 		// Release note for current tag already exist exists. | 		// Release note for current tag already exist exists. | ||||||
| 		return nil | 		return nil | ||||||
| @ -255,7 +254,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	nextReleaseNotePath := path.Join(repoPath, "release", "next") | 	nextReleaseNotePath := path.Join(recipe.Dir, "release", "next") | ||||||
| 	if _, err := os.Stat(nextReleaseNotePath); err == nil { | 	if _, err := os.Stat(nextReleaseNotePath); err == nil { | ||||||
| 		// release/next note exists. Move it to release/<tag> | 		// release/next note exists. Move it to release/<tag> | ||||||
| 		if internal.Dry { | 		if internal.Dry { | ||||||
| @ -278,11 +277,11 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error { | |||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		err = gitPkg.Add(repoPath, path.Join("release", "next"), internal.Dry) | 		err = gitPkg.Add(recipe.Dir, path.Join("release", "next"), internal.Dry) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		err = gitPkg.Add(repoPath, path.Join("release", tag), internal.Dry) | 		err = gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| @ -311,7 +310,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	err = gitPkg.Add(repoPath, path.Join("release", tag), internal.Dry) | 	err = gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @ -325,14 +324,14 @@ func commitRelease(recipe recipe.Recipe, tag string) error { | |||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	isClean, err := gitPkg.IsClean(recipe.Dir()) | 	isClean, err := gitPkg.IsClean(recipe.Dir) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if isClean { | 	if isClean { | ||||||
| 		if !internal.Dry { | 		if !internal.Dry { | ||||||
| 			return fmt.Errorf("no changes discovered in %s, nothing to publish?", recipe.Dir()) | 			return fmt.Errorf("no changes discovered in %s, nothing to publish?", recipe.Dir) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @ -402,8 +401,7 @@ func pushRelease(recipe recipe.Recipe, tagString string) error { | |||||||
| } | } | ||||||
|  |  | ||||||
| func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error { | func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error { | ||||||
| 	directory := path.Join(config.RECIPES_DIR, recipe.Name) | 	repo, err := git.PlainOpen(recipe.Dir) | ||||||
| 	repo, err := git.PlainOpen(directory) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -199,13 +199,13 @@ likely to change. | |||||||
| 			logrus.Infof("dry run: not syncing label %s for recipe %s", nextTag, recipe.Name) | 			logrus.Infof("dry run: not syncing label %s for recipe %s", nextTag, recipe.Name) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		isClean, err := gitPkg.IsClean(recipe.Dir()) | 		isClean, err := gitPkg.IsClean(recipe.Dir) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
| 		if !isClean { | 		if !isClean { | ||||||
| 			logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name) | 			logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name) | ||||||
| 			if err := gitPkg.DiffUnstaged(recipe.Dir()); err != nil { | 			if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -18,7 +18,7 @@ import ( | |||||||
| 	recipePkg "coopcloud.tech/abra/pkg/recipe" | 	recipePkg "coopcloud.tech/abra/pkg/recipe" | ||||||
| 	"coopcloud.tech/tagcmp" | 	"coopcloud.tech/tagcmp" | ||||||
| 	"github.com/AlecAivazis/survey/v2" | 	"github.com/AlecAivazis/survey/v2" | ||||||
| 	"github.com/docker/distribution/reference" | 	"github.com/distribution/reference" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/urfave/cli" | 	"github.com/urfave/cli" | ||||||
| ) | ) | ||||||
| @ -73,19 +73,19 @@ You may invoke this command in "wizard" mode and be prompted for input: | |||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		recipe := internal.ValidateRecipe(c) | 		recipe := internal.ValidateRecipe(c) | ||||||
|  |  | ||||||
| 		if err := recipePkg.EnsureIsClean(recipe.Name); err != nil { | 		if err := recipe.EnsureIsClean(); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if err := recipePkg.EnsureExists(recipe.Name); err != nil { | 		if err := recipe.EnsureExists(); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil { | 		if err := recipe.EnsureUpToDate(); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if err := recipePkg.EnsureLatest(recipe.Name); err != nil { | 		if err := recipe.EnsureLatest(); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | |||||||
| @ -54,8 +54,9 @@ var recipeVersionCommand = cli.Command{ | |||||||
| 			logrus.Fatalf("%s has no catalogue published versions?", recipe.Name) | 			logrus.Fatalf("%s has no catalogue published versions?", recipe.Name) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		for i := len(recipeMeta.Versions) - 1; i >= 0; i-- { |  | ||||||
| 		tableCols := []string{"version", "service", "image", "tag"} | 		tableCols := []string{"version", "service", "image", "tag"} | ||||||
|  | 		aggregated_table := formatter.CreateTable(tableCols) | ||||||
|  | 		for i := len(recipeMeta.Versions) - 1; i >= 0; i-- { | ||||||
| 			table := formatter.CreateTable(tableCols) | 			table := formatter.CreateTable(tableCols) | ||||||
| 			for version, meta := range recipeMeta.Versions[i] { | 			for version, meta := range recipeMeta.Versions[i] { | ||||||
| 				var versions [][]string | 				var versions [][]string | ||||||
| @ -67,11 +68,10 @@ var recipeVersionCommand = cli.Command{ | |||||||
|  |  | ||||||
| 				for _, version := range versions { | 				for _, version := range versions { | ||||||
| 					table.Append(version) | 					table.Append(version) | ||||||
|  | 					aggregated_table.Append(version) | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				if internal.MachineReadable { | 				if !internal.MachineReadable { | ||||||
| 					table.JSONRender() |  | ||||||
| 				} else { |  | ||||||
| 					table.SetAutoMergeCellsByColumnIndex([]int{0}) | 					table.SetAutoMergeCellsByColumnIndex([]int{0}) | ||||||
| 					table.SetAlignment(tablewriter.ALIGN_LEFT) | 					table.SetAlignment(tablewriter.ALIGN_LEFT) | ||||||
| 					table.Render() | 					table.Render() | ||||||
| @ -79,6 +79,9 @@ var recipeVersionCommand = cli.Command{ | |||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 		if internal.MachineReadable { | ||||||
|  | 			aggregated_table.JSONRender() | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		return nil | 		return nil | ||||||
| 	}, | 	}, | ||||||
|  | |||||||
| @ -53,7 +53,7 @@ func cleanUp(domainName string) { | |||||||
| // Docker manages SSH connection details. These are stored to disk in | // Docker manages SSH connection details. These are stored to disk in | ||||||
| // ~/.docker. Abra can manage this completely for the user, so it's an | // ~/.docker. Abra can manage this completely for the user, so it's an | ||||||
| // implementation detail. | // implementation detail. | ||||||
| func newContext(c *cli.Context, domainName, username, port string) error { | func newContext(c *cli.Context, domainName string) error { | ||||||
| 	store := contextPkg.NewDefaultDockerContextStore() | 	store := contextPkg.NewDefaultDockerContextStore() | ||||||
| 	contexts, err := store.Store.List() | 	contexts, err := store.Store.List() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -67,9 +67,9 @@ func newContext(c *cli.Context, domainName, username, port string) error { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	logrus.Debugf("creating context with domain %s, username %s and port %s", domainName, username, port) | 	logrus.Debugf("creating context with domain %s", domainName) | ||||||
|  |  | ||||||
| 	if err := client.CreateContext(domainName, username, port); err != nil { | 	if err := client.CreateContext(domainName); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @ -158,12 +158,7 @@ developer machine. | |||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		hostConfig, err := sshPkg.GetHostConfig(domainName) | 		if err := newContext(c, domainName); err != nil { | ||||||
| 		if err != nil { |  | ||||||
| 			logrus.Fatal(err) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if err := newContext(c, domainName, hostConfig.User, hostConfig.Port); err != nil { |  | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | |||||||
| @ -316,22 +316,23 @@ func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName | |||||||
|  |  | ||||||
| // processRecipeRepoVersion clones, pulls, checks out the version and lints the | // processRecipeRepoVersion clones, pulls, checks out the version and lints the | ||||||
| // recipe repository. | // recipe repository. | ||||||
| func processRecipeRepoVersion(recipeName, version string) error { | func processRecipeRepoVersion(recipe recipe.Recipe, version string) error { | ||||||
| 	if err := recipe.EnsureExists(recipeName); err != nil { | 	if err := recipe.EnsureExists(); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := recipe.EnsureUpToDate(recipeName); err != nil { | 	if err := recipe.EnsureUpToDate(); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := recipe.EnsureVersion(recipeName, version); err != nil { | 	if err := recipe.EnsureVersion(version); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if r, err := recipe.Get(recipeName, internal.Offline); err != nil { | 	if err := recipe.LoadConfig(); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} else if err := lint.LintForErrors(r); err != nil { | 	} | ||||||
|  | 	if err := lint.LintForErrors(recipe); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @ -442,7 +443,12 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName, | |||||||
| 		Env:    env, | 		Env:    env, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err = processRecipeRepoVersion(recipeName, upgradeVersion); err != nil { | 	recipe, err := recipe.Get(recipeName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err = processRecipeRepoVersion(recipe, upgradeVersion); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										133
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										133
									
								
								go.mod
									
									
									
									
									
								
							| @ -3,118 +3,131 @@ module coopcloud.tech/abra | |||||||
| go 1.21 | go 1.21 | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52 | 	coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb | ||||||
| 	git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 | 	git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 | ||||||
| 	github.com/AlecAivazis/survey/v2 v2.3.7 | 	github.com/AlecAivazis/survey/v2 v2.3.7 | ||||||
| 	github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 | 	github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 | ||||||
| 	github.com/docker/cli v24.0.7+incompatible | 	github.com/distribution/distribution v2.8.3+incompatible | ||||||
| 	github.com/docker/distribution v2.8.3+incompatible | 	github.com/docker/cli v26.1.4+incompatible | ||||||
| 	github.com/docker/docker v24.0.7+incompatible | 	github.com/docker/docker v26.1.4+incompatible | ||||||
| 	github.com/docker/go-units v0.5.0 | 	github.com/docker/go-units v0.5.0 | ||||||
| 	github.com/go-git/go-git/v5 v5.10.0 | 	github.com/go-git/go-git/v5 v5.12.0 | ||||||
| 	github.com/google/go-cmp v0.5.9 | 	github.com/google/go-cmp v0.6.0 | ||||||
| 	github.com/moby/sys/signal v0.7.0 | 	github.com/moby/sys/signal v0.7.0 | ||||||
| 	github.com/moby/term v0.5.0 | 	github.com/moby/term v0.5.0 | ||||||
| 	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.14.1 | 	github.com/schollz/progressbar/v3 v3.14.4 | ||||||
| 	github.com/sirupsen/logrus v1.9.3 | 	github.com/sirupsen/logrus v1.9.3 | ||||||
| 	gotest.tools/v3 v3.5.1 | 	gotest.tools/v3 v3.5.1 | ||||||
| ) | ) | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	dario.cat/mergo v1.0.0 // indirect | 	dario.cat/mergo v1.0.0 // indirect | ||||||
| 	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect | 	github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect | ||||||
| 	github.com/BurntSushi/toml v1.0.0 // indirect | 	github.com/BurntSushi/toml v1.4.0 // indirect | ||||||
| 	github.com/Microsoft/go-winio v0.6.1 // indirect | 	github.com/Microsoft/go-winio v0.6.2 // indirect | ||||||
| 	github.com/Microsoft/hcsshim v0.9.2 // indirect | 	github.com/ProtonMail/go-crypto v1.0.0 // indirect | ||||||
| 	github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect |  | ||||||
| 	github.com/acomagu/bufpipe v1.0.4 // indirect |  | ||||||
| 	github.com/beorn7/perks v1.0.1 // indirect | 	github.com/beorn7/perks v1.0.1 // indirect | ||||||
| 	github.com/cespare/xxhash/v2 v2.2.0 // indirect | 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect | ||||||
| 	github.com/cloudflare/circl v1.3.3 // indirect | 	github.com/cespare/xxhash/v2 v2.3.0 // indirect | ||||||
| 	github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect | 	github.com/cloudflare/circl v1.3.9 // indirect | ||||||
| 	github.com/cyphar/filepath-securejoin v0.2.4 // indirect | 	github.com/containerd/log v0.1.0 // indirect | ||||||
|  | 	github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect | ||||||
|  | 	github.com/cyphar/filepath-securejoin v0.2.5 // indirect | ||||||
| 	github.com/davecgh/go-spew v1.1.1 // indirect | 	github.com/davecgh/go-spew v1.1.1 // indirect | ||||||
| 	github.com/distribution/reference v0.5.0 // indirect | 	github.com/distribution/reference v0.6.0 // indirect | ||||||
|  | 	github.com/docker/distribution v2.7.1+incompatible // indirect | ||||||
| 	github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect | 	github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect | ||||||
| 	github.com/docker/go-connections v0.4.0 // indirect | 	github.com/docker/go-connections v0.5.0 // indirect | ||||||
| 	github.com/docker/go-metrics v0.0.1 // indirect | 	github.com/docker/go-metrics v0.0.1 // indirect | ||||||
| 	github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect | 	github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect | ||||||
| 	github.com/emirpasic/gods v1.18.1 // indirect | 	github.com/emirpasic/gods v1.18.1 // indirect | ||||||
|  | 	github.com/felixge/httpsnoop v1.0.4 // indirect | ||||||
| 	github.com/ghodss/yaml v1.0.0 // indirect | 	github.com/ghodss/yaml v1.0.0 // indirect | ||||||
| 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect | 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect | ||||||
| 	github.com/go-git/go-billy/v5 v5.5.0 // indirect | 	github.com/go-git/go-billy/v5 v5.5.0 // indirect | ||||||
|  | 	github.com/go-logr/logr v1.4.2 // indirect | ||||||
|  | 	github.com/go-logr/stdr v1.2.2 // indirect | ||||||
| 	github.com/gogo/protobuf v1.3.2 // indirect | 	github.com/gogo/protobuf v1.3.2 // indirect | ||||||
| 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect | 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect | ||||||
| 	github.com/golang/protobuf v1.5.3 // indirect | 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect | ||||||
| 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect | 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect | ||||||
| 	github.com/imdario/mergo v0.3.12 // indirect | 	github.com/inconshreveable/mousetrap v1.1.0 // indirect | ||||||
| 	github.com/inconshreveable/mousetrap v1.0.0 // indirect |  | ||||||
| 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // 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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect | ||||||
| 	github.com/kevinburke/ssh_config v1.2.0 // indirect | 	github.com/kevinburke/ssh_config v1.2.0 // indirect | ||||||
| 	github.com/klauspost/compress v1.14.2 // indirect | 	github.com/klauspost/compress v1.17.9 // indirect | ||||||
| 	github.com/mattn/go-colorable v0.1.12 // indirect | 	github.com/mattn/go-colorable v0.1.13 // indirect | ||||||
| 	github.com/mattn/go-isatty v0.0.20 // indirect | 	github.com/mattn/go-isatty v0.0.20 // indirect | ||||||
| 	github.com/mattn/go-runewidth v0.0.14 // indirect | 	github.com/mattn/go-runewidth v0.0.15 // indirect | ||||||
| 	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect | 	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect | ||||||
| 	github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect | 	github.com/miekg/pkcs11 v1.1.1 // indirect | ||||||
| 	github.com/miekg/pkcs11 v1.0.3 // indirect |  | ||||||
| 	github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect | 	github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect | ||||||
| 	github.com/mitchellh/mapstructure v1.4.3 // indirect | 	github.com/mitchellh/mapstructure v1.5.0 // indirect | ||||||
|  | 	github.com/moby/docker-image-spec v1.3.1 // indirect | ||||||
|  | 	github.com/moby/sys/user v0.1.0 // 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/go-digest v1.0.0 // indirect | ||||||
| 	github.com/opencontainers/runc v1.1.0 // indirect | 	github.com/opencontainers/runc v1.1.13 // indirect | ||||||
| 	github.com/pjbgf/sha1cd v0.3.0 // indirect | 	github.com/pjbgf/sha1cd v0.3.0 // indirect | ||||||
| 	github.com/pmezard/go-difflib v1.0.0 // indirect | 	github.com/pmezard/go-difflib v1.0.0 // indirect | ||||||
| 	github.com/prometheus/client_model v0.3.0 // indirect | 	github.com/prometheus/client_model v0.6.1 // indirect | ||||||
| 	github.com/prometheus/common v0.42.0 // indirect | 	github.com/prometheus/common v0.54.0 // indirect | ||||||
| 	github.com/prometheus/procfs v0.10.1 // indirect | 	github.com/prometheus/procfs v0.15.1 // indirect | ||||||
| 	github.com/rivo/uniseg v0.4.4 // indirect | 	github.com/rivo/uniseg v0.4.7 // indirect | ||||||
| 	github.com/russross/blackfriday/v2 v2.1.0 // indirect | 	github.com/russross/blackfriday/v2 v2.1.0 // indirect | ||||||
| 	github.com/skeema/knownhosts v1.2.0 // indirect | 	github.com/skeema/knownhosts v1.2.2 // indirect | ||||||
| 	github.com/spf13/pflag v1.0.5 // indirect | 	github.com/spf13/pflag v1.0.5 // indirect | ||||||
| 	github.com/xanzy/ssh-agent v0.3.3 // indirect | 	github.com/xanzy/ssh-agent v0.3.3 // indirect | ||||||
| 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // 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 | ||||||
| 	golang.org/x/crypto v0.14.0 // indirect | 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect | ||||||
| 	golang.org/x/mod v0.12.0 // indirect | 	go.opentelemetry.io/otel v1.27.0 // indirect | ||||||
| 	golang.org/x/net v0.17.0 // indirect | 	go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 // indirect | ||||||
| 	golang.org/x/sync v0.3.0 // indirect | 	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect | ||||||
| 	golang.org/x/term v0.14.0 // indirect | 	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect | ||||||
| 	golang.org/x/text v0.13.0 // indirect | 	go.opentelemetry.io/otel/metric v1.27.0 // indirect | ||||||
| 	golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect | 	go.opentelemetry.io/otel/sdk v1.27.0 // indirect | ||||||
| 	golang.org/x/tools v0.13.0 // indirect | 	go.opentelemetry.io/otel/sdk/metric v1.27.0 // indirect | ||||||
| 	google.golang.org/protobuf v1.30.0 // indirect | 	go.opentelemetry.io/otel/trace v1.27.0 // indirect | ||||||
|  | 	go.opentelemetry.io/proto/otlp v1.3.1 // indirect | ||||||
|  | 	golang.org/x/crypto v0.24.0 // indirect | ||||||
|  | 	golang.org/x/net v0.26.0 // indirect | ||||||
|  | 	golang.org/x/sync v0.7.0 // indirect | ||||||
|  | 	golang.org/x/term v0.21.0 // indirect | ||||||
|  | 	golang.org/x/text v0.16.0 // indirect | ||||||
|  | 	golang.org/x/time v0.5.0 // indirect | ||||||
|  | 	google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect | ||||||
|  | 	google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect | ||||||
|  | 	google.golang.org/grpc v1.64.0 // indirect | ||||||
|  | 	google.golang.org/protobuf v1.34.2 // indirect | ||||||
| 	gopkg.in/warnings.v0 v0.1.2 // indirect | 	gopkg.in/warnings.v0 v0.1.2 // indirect | ||||||
| 	gopkg.in/yaml.v2 v2.4.0 // indirect | 	gopkg.in/yaml.v2 v2.4.0 // indirect | ||||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||||
| ) | ) | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect |  | ||||||
| 	github.com/buger/goterm v1.0.4 | 	github.com/buger/goterm v1.0.4 | ||||||
| 	github.com/containerd/containerd v1.5.9 // indirect | 	github.com/containerd/containerd v1.7.18 // indirect | ||||||
| 	github.com/containers/image v3.0.2+incompatible | 	github.com/containers/image v3.0.2+incompatible | ||||||
| 	github.com/containers/storage v1.38.2 // indirect | 	github.com/containers/storage v1.38.2 // indirect | ||||||
| 	github.com/decentral1se/passgen v1.0.1 | 	github.com/decentral1se/passgen v1.0.1 | ||||||
| 	github.com/docker/docker-credential-helpers v0.6.4 // indirect | 	github.com/docker/docker-credential-helpers v0.8.2 // indirect | ||||||
| 	github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect | 	github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect | ||||||
| 	github.com/fvbommel/sortorder v1.0.2 // indirect | 	github.com/fvbommel/sortorder v1.1.0 // 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.1 // indirect | ||||||
| 	github.com/hashicorp/go-retryablehttp v0.7.5 | 	github.com/hashicorp/go-retryablehttp v0.7.7 | ||||||
| 	github.com/klauspost/pgzip v1.2.6 | 	github.com/moby/patternmatcher v0.6.0 // indirect | ||||||
| 	github.com/moby/patternmatcher v0.5.0 // indirect |  | ||||||
| 	github.com/moby/sys/sequential v0.5.0 // indirect | 	github.com/moby/sys/sequential v0.5.0 // indirect | ||||||
| 	github.com/opencontainers/image-spec v1.0.3-0.20211202193544-a5463b7f9c84 // indirect | 	github.com/opencontainers/image-spec v1.1.0 // indirect | ||||||
| 	github.com/prometheus/client_golang v1.16.0 // indirect | 	github.com/prometheus/client_golang v1.19.1 // indirect | ||||||
| 	github.com/sergi/go-diff v1.2.0 // indirect | 	github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect | ||||||
| 	github.com/spf13/cobra v1.3.0 // indirect | 	github.com/spf13/cobra v1.8.1 // indirect | ||||||
| 	github.com/stretchr/testify v1.8.4 | 	github.com/stretchr/testify v1.9.0 | ||||||
| 	github.com/theupdateframework/notary v0.7.0 // indirect | 	github.com/theupdateframework/notary v0.7.0 // indirect | ||||||
| 	github.com/urfave/cli v1.22.9 | 	github.com/urfave/cli v1.22.15 | ||||||
| 	github.com/xeipuuv/gojsonpointer v0.0.0-20190809123943-df4f5c81cb3b // indirect | 	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect | ||||||
| 	golang.org/x/sys v0.14.0 | 	golang.org/x/sys v0.21.0 | ||||||
| ) | ) | ||||||
|  | |||||||
| @ -14,19 +14,16 @@ import ( | |||||||
|  |  | ||||||
| type Context = contextStore.Metadata | type Context = contextStore.Metadata | ||||||
|  |  | ||||||
| func CreateContext(contextName string, user string, port string) error { | // CreateContext creates a new Docker context. | ||||||
| 	host := contextName | func CreateContext(contextName string) error { | ||||||
| 	if user != "" { | 	host := fmt.Sprintf("ssh://%s", contextName) | ||||||
| 		host = fmt.Sprintf("%s@%s", user, host) |  | ||||||
| 	} |  | ||||||
| 	if port != "" { |  | ||||||
| 		host = fmt.Sprintf("%s:%s", host, port) |  | ||||||
| 	} |  | ||||||
| 	host = fmt.Sprintf("ssh://%s", host) |  | ||||||
| 	if err := createContext(contextName, host); err != nil { | 	if err := createContext(contextName, host); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	logrus.Debugf("created the %s context", contextName) | 	logrus.Debugf("created the %s context", contextName) | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ import ( | |||||||
|  |  | ||||||
| 	"github.com/containers/image/docker" | 	"github.com/containers/image/docker" | ||||||
| 	"github.com/containers/image/types" | 	"github.com/containers/image/types" | ||||||
| 	"github.com/docker/distribution/reference" | 	"github.com/distribution/reference" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // GetRegistryTags retrieves all tags of an image from a container registry. | // GetRegistryTags retrieves all tags of an image from a container registry. | ||||||
|  | |||||||
| @ -2,15 +2,17 @@ package client | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/docker/docker/api/types/filters" | 	"github.com/docker/docker/api/types/filters" | ||||||
| 	"github.com/docker/docker/api/types/volume" | 	"github.com/docker/docker/api/types/volume" | ||||||
| 	"github.com/docker/docker/client" | 	"github.com/docker/docker/client" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func GetVolumes(cl *client.Client, ctx context.Context, server string, fs filters.Args) ([]*volume.Volume, error) { | func GetVolumes(cl *client.Client, ctx context.Context, server string, fs filters.Args) ([]*volume.Volume, error) { | ||||||
| 	volumeListOptions := volume.ListOptions{fs} | 	volumeListOKBody, err := cl.VolumeList(ctx, volume.ListOptions{Filters: fs}) | ||||||
| 	volumeListOKBody, err := cl.VolumeList(ctx, volumeListOptions) |  | ||||||
| 	volumeList := volumeListOKBody.Volumes | 	volumeList := volumeListOKBody.Volumes | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return volumeList, err | 		return volumeList, err | ||||||
| @ -29,13 +31,32 @@ func GetVolumeNames(volumes []*volume.Volume) []string { | |||||||
| 	return volumeNames | 	return volumeNames | ||||||
| } | } | ||||||
|  |  | ||||||
| func RemoveVolumes(cl *client.Client, ctx context.Context, server string, volumeNames []string, force bool) error { | func RemoveVolumes(cl *client.Client, ctx context.Context, volumeNames []string, force bool, retries int) error { | ||||||
| 	for _, volName := range volumeNames { | 	for _, volName := range volumeNames { | ||||||
| 		err := cl.VolumeRemove(ctx, volName, force) | 		err := retryFunc(5, func() error { | ||||||
|  | 			return cl.VolumeRemove(context.Background(), volName, force) | ||||||
|  | 		}) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return fmt.Errorf("volume %s: %s", volName, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // retryFunc retries the given function for the given retries. After the nth | ||||||
|  | // retry it waits (n + 1)^2 seconds before the next retry (starting with n=0). | ||||||
|  | // It returns an error if the function still failed after the last retry. | ||||||
|  | func retryFunc(retries int, fn func() error) error { | ||||||
|  | 	for i := 0; i < retries; i++ { | ||||||
|  | 		err := fn() | ||||||
|  | 		if err == nil { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 		if i+1 < retries { | ||||||
|  | 			sleep := time.Duration(i+1) * time.Duration(i+1) | ||||||
|  | 			logrus.Infof("%s: waiting %d seconds before next retry", err, sleep) | ||||||
|  | 			time.Sleep(sleep * time.Second) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return fmt.Errorf("%d retries failed", retries) | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| package app | package client | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| @ -11,8 +11,8 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/formatter" | 	"coopcloud.tech/abra/pkg/formatter" | ||||||
| 	"coopcloud.tech/abra/pkg/upstream/stack" | 	"coopcloud.tech/abra/pkg/upstream/stack" | ||||||
| 	loader "coopcloud.tech/abra/pkg/upstream/stack" | 	loader "coopcloud.tech/abra/pkg/upstream/stack" | ||||||
|  | 	"github.com/distribution/reference" | ||||||
| 	composetypes "github.com/docker/cli/cli/compose/types" | 	composetypes "github.com/docker/cli/cli/compose/types" | ||||||
| 	"github.com/docker/distribution/reference" |  | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | |||||||
| @ -69,9 +69,9 @@ func (a App) StackName() string { | |||||||
| func StackName(appName string) string { | func StackName(appName string) string { | ||||||
| 	stackName := SanitiseAppName(appName) | 	stackName := SanitiseAppName(appName) | ||||||
|  |  | ||||||
| 	if len(stackName) > 45 { | 	if len(stackName) > MAX_SANITISED_APP_NAME_LENGTH { | ||||||
| 		logrus.Debugf("trimming %s to %s to avoid runtime limits", stackName, stackName[:45]) | 		logrus.Debugf("trimming %s to %s to avoid runtime limits", stackName, stackName[:MAX_SANITISED_APP_NAME_LENGTH]) | ||||||
| 		stackName = stackName[:45] | 		stackName = stackName[:MAX_SANITISED_APP_NAME_LENGTH] | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return stackName | 	return stackName | ||||||
|  | |||||||
| @ -45,7 +45,7 @@ func TestGetApp(t *testing.T) { | |||||||
|  |  | ||||||
| func TestGetComposeFiles(t *testing.T) { | func TestGetComposeFiles(t *testing.T) { | ||||||
| 	offline := true | 	offline := true | ||||||
| 	r, err := recipe.Get("abra-test-recipe", offline) | 	r, err := recipe.Get2("abra-test-recipe", offline) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| @ -92,7 +92,7 @@ func TestGetComposeFiles(t *testing.T) { | |||||||
|  |  | ||||||
| func TestGetComposeFilesError(t *testing.T) { | func TestGetComposeFilesError(t *testing.T) { | ||||||
| 	offline := true | 	offline := true | ||||||
| 	r, err := recipe.Get("abra-test-recipe", offline) | 	r, err := recipe.Get2("abra-test-recipe", offline) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -36,6 +36,9 @@ var REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud" | |||||||
| var CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json" | var CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json" | ||||||
| var SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git" | var SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git" | ||||||
|  |  | ||||||
|  | const MAX_SANITISED_APP_NAME_LENGTH = 45 | ||||||
|  | const MAX_DOCKER_SECRET_LENGTH = 64 | ||||||
|  |  | ||||||
| var BackupbotLabel = "coop-cloud.backupbot.enabled" | var BackupbotLabel = "coop-cloud.backupbot.enabled" | ||||||
|  |  | ||||||
| // envVarModifiers is a list of env var modifier strings. These are added to | // envVarModifiers is a list of env var modifier strings. These are added to | ||||||
|  | |||||||
| @ -94,7 +94,7 @@ func TestReadEnv(t *testing.T) { | |||||||
|  |  | ||||||
| func TestReadAbraShEnvVars(t *testing.T) { | func TestReadAbraShEnvVars(t *testing.T) { | ||||||
| 	offline := true | 	offline := true | ||||||
| 	r, err := recipe.Get("abra-test-recipe", offline) | 	r, err := recipe.Get2("abra-test-recipe", offline) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| @ -124,7 +124,7 @@ func TestReadAbraShEnvVars(t *testing.T) { | |||||||
|  |  | ||||||
| func TestReadAbraShCmdNames(t *testing.T) { | func TestReadAbraShCmdNames(t *testing.T) { | ||||||
| 	offline := true | 	offline := true | ||||||
| 	r, err := recipe.Get("abra-test-recipe", offline) | 	r, err := recipe.Get2("abra-test-recipe", offline) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| @ -149,7 +149,7 @@ func TestReadAbraShCmdNames(t *testing.T) { | |||||||
|  |  | ||||||
| func TestCheckEnv(t *testing.T) { | func TestCheckEnv(t *testing.T) { | ||||||
| 	offline := true | 	offline := true | ||||||
| 	r, err := recipe.Get("abra-test-recipe", offline) | 	r, err := recipe.Get2("abra-test-recipe", offline) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| @ -183,7 +183,7 @@ func TestCheckEnv(t *testing.T) { | |||||||
|  |  | ||||||
| func TestCheckEnvError(t *testing.T) { | func TestCheckEnvError(t *testing.T) { | ||||||
| 	offline := true | 	offline := true | ||||||
| 	r, err := recipe.Get("abra-test-recipe", offline) | 	r, err := recipe.Get2("abra-test-recipe", offline) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| @ -219,7 +219,7 @@ func TestCheckEnvError(t *testing.T) { | |||||||
|  |  | ||||||
| func TestEnvVarCommentsRemoved(t *testing.T) { | func TestEnvVarCommentsRemoved(t *testing.T) { | ||||||
| 	offline := true | 	offline := true | ||||||
| 	r, err := recipe.Get("abra-test-recipe", offline) | 	r, err := recipe.Get2("abra-test-recipe", offline) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| @ -251,7 +251,7 @@ func TestEnvVarCommentsRemoved(t *testing.T) { | |||||||
|  |  | ||||||
| func TestEnvVarModifiersIncluded(t *testing.T) { | func TestEnvVarModifiersIncluded(t *testing.T) { | ||||||
| 	offline := true | 	offline := true | ||||||
| 	r, err := recipe.Get("abra-test-recipe", offline) | 	r, err := recipe.Get2("abra-test-recipe", offline) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/formatter" | 	"coopcloud.tech/abra/pkg/formatter" | ||||||
| 	"github.com/AlecAivazis/survey/v2" | 	"github.com/AlecAivazis/survey/v2" | ||||||
| 	"github.com/docker/docker/api/types" | 	"github.com/docker/docker/api/types" | ||||||
|  | 	containerTypes "github.com/docker/docker/api/types/container" | ||||||
| 	"github.com/docker/docker/api/types/filters" | 	"github.com/docker/docker/api/types/filters" | ||||||
| 	"github.com/docker/docker/client" | 	"github.com/docker/docker/client" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| @ -17,7 +18,7 @@ import ( | |||||||
| // count of containers does not match 1, then a prompt is presented to let the | // count of containers does not match 1, then a prompt is presented to let the | ||||||
| // user choose. A count of 0 is handled gracefully. | // user choose. A count of 0 is handled gracefully. | ||||||
| func GetContainer(c context.Context, cl *client.Client, filters filters.Args, noInput bool) (types.Container, error) { | func GetContainer(c context.Context, cl *client.Client, filters filters.Args, noInput bool) (types.Container, error) { | ||||||
| 	containerOpts := types.ContainerListOptions{Filters: filters} | 	containerOpts := containerTypes.ListOptions{Filters: filters} | ||||||
| 	containers, err := cl.ContainerList(c, containerOpts) | 	containers, err := cl.ContainerList(c, containerOpts) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return types.Container{}, err | 		return types.Container{}, err | ||||||
|  | |||||||
| @ -1,35 +1,41 @@ | |||||||
| package git | package git | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
|  |  | ||||||
| 	"github.com/go-git/go-git/v5" | 	"github.com/go-git/go-git/v5" | ||||||
| 	gitPkg "github.com/go-git/go-git/v5" | 	"github.com/go-git/go-git/v5/plumbing/object" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Init inits a new repo and commits all the stuff if you want | // Init inits a new repo and commits all the stuff if you want | ||||||
| func Init(repoPath string, commit bool) error { | func Init(repoPath string, commit bool, gitName, gitEmail string) error { | ||||||
| 	if _, err := gitPkg.PlainInit(repoPath, false); err != nil { | 	if _, err := git.PlainInit(repoPath, false); err != nil { | ||||||
| 		logrus.Fatal(err) | 		return fmt.Errorf("git init: %s", err) | ||||||
| 	} | 	} | ||||||
| 	logrus.Debugf("initialised new git repo in %s", repoPath) | 	logrus.Debugf("initialised new git repo in %s", repoPath) | ||||||
|  |  | ||||||
| 	if commit { | 	if commit { | ||||||
| 		commitRepo, err := git.PlainOpen(repoPath) | 		commitRepo, err := git.PlainOpen(repoPath) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			return fmt.Errorf("git open: %s", err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		commitWorktree, err := commitRepo.Worktree() | 		commitWorktree, err := commitRepo.Worktree() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logrus.Fatal(err) | 			return fmt.Errorf("git worktree: %s", err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if err := commitWorktree.AddWithOptions(&git.AddOptions{All: true}); err != nil { | 		if err := commitWorktree.AddWithOptions(&git.AddOptions{All: true}); err != nil { | ||||||
| 			return err | 			return fmt.Errorf("git add: %s", err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if _, err = commitWorktree.Commit("init", &git.CommitOptions{}); err != nil { | 		var author *object.Signature | ||||||
| 			return err | 		if gitName != "" && gitEmail != "" { | ||||||
|  | 			author = &object.Signature{Name: gitName, Email: gitEmail} | ||||||
|  | 		} | ||||||
|  | 		if _, err = commitWorktree.Commit("init", &git.CommitOptions{Author: author}); err != nil { | ||||||
|  | 			return fmt.Errorf("git commit: %s", err) | ||||||
| 		} | 		} | ||||||
| 		logrus.Debugf("init committed all files for new git repo in %s", repoPath) | 		logrus.Debugf("init committed all files for new git repo in %s", repoPath) | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -4,11 +4,9 @@ import ( | |||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"os" | 	"os" | ||||||
| 	"os/user" | 	"os/user" | ||||||
| 	"path" |  | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/pkg/config" |  | ||||||
| 	"github.com/go-git/go-git/v5" | 	"github.com/go-git/go-git/v5" | ||||||
| 	gitConfigPkg "github.com/go-git/go-git/v5/config" | 	gitConfigPkg "github.com/go-git/go-git/v5/config" | ||||||
| 	"github.com/go-git/go-git/v5/plumbing" | 	"github.com/go-git/go-git/v5/plumbing" | ||||||
| @ -16,11 +14,9 @@ import ( | |||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // GetRecipeHead retrieves latest HEAD metadata. | // GetHead retrieves latest HEAD metadata. | ||||||
| func GetRecipeHead(recipeName string) (*plumbing.Reference, error) { | func GetHead(dir string) (*plumbing.Reference, error) { | ||||||
| 	recipeDir := path.Join(config.RECIPES_DIR, recipeName) | 	repo, err := git.PlainOpen(dir) | ||||||
|  |  | ||||||
| 	repo, err := git.PlainOpen(recipeDir) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/recipe" | 	"coopcloud.tech/abra/pkg/recipe" | ||||||
| 	recipePkg "coopcloud.tech/abra/pkg/recipe" | 	recipePkg "coopcloud.tech/abra/pkg/recipe" | ||||||
| 	"coopcloud.tech/tagcmp" | 	"coopcloud.tech/tagcmp" | ||||||
| 	"github.com/docker/distribution/reference" | 	"github.com/distribution/reference" | ||||||
| 	"github.com/go-git/go-git/v5" | 	"github.com/go-git/go-git/v5" | ||||||
| 	"github.com/go-git/go-git/v5/plumbing" | 	"github.com/go-git/go-git/v5/plumbing" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| @ -115,6 +115,13 @@ var LintRules = map[string][]LintRule{ | |||||||
| 			HowToResolve: "upload your recipe to git.coopcloud.tech/coop-cloud/...", | 			HowToResolve: "upload your recipe to git.coopcloud.tech/coop-cloud/...", | ||||||
| 			Function:     LintHasRecipeRepo, | 			Function:     LintHasRecipeRepo, | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Ref:          "R015", | ||||||
|  | 			Level:        "warn", | ||||||
|  | 			Description:  "long secret names", | ||||||
|  | 			HowToResolve: "reduce length of secret names to 12 chars", | ||||||
|  | 			Function:     LintSecretLengths, | ||||||
|  | 		}, | ||||||
| 	}, | 	}, | ||||||
| 	"error": { | 	"error": { | ||||||
| 		{ | 		{ | ||||||
| @ -203,7 +210,7 @@ func LintComposeVersion(recipe recipe.Recipe) (bool, error) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func LintEnvConfigPresent(recipe recipe.Recipe) (bool, error) { | func LintEnvConfigPresent(recipe recipe.Recipe) (bool, error) { | ||||||
| 	envSample := fmt.Sprintf("%s/%s/.env.sample", config.RECIPES_DIR, recipe.Name) | 	envSample := fmt.Sprintf("%s/%s/.env.sample", recipe.Dir) | ||||||
| 	if _, err := os.Stat(envSample); !os.IsNotExist(err) { | 	if _, err := os.Stat(envSample); !os.IsNotExist(err) { | ||||||
| 		return true, nil | 		return true, nil | ||||||
| 	} | 	} | ||||||
| @ -226,7 +233,7 @@ func LintAppService(recipe recipe.Recipe) (bool, error) { | |||||||
| // the recipe. This typically means that no domain is required to deploy and | // the recipe. This typically means that no domain is required to deploy and | ||||||
| // therefore no matching traefik deploy label will be present. | // therefore no matching traefik deploy label will be present. | ||||||
| func LintTraefikEnabledSkipCondition(recipe recipe.Recipe) (bool, error) { | func LintTraefikEnabledSkipCondition(recipe recipe.Recipe) (bool, error) { | ||||||
| 	envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample") | 	envSamplePath := path.Join(recipe.Dir, ".env.sample") | ||||||
| 	sampleEnv, err := config.ReadEnv(envSamplePath) | 	sampleEnv, err := config.ReadEnv(envSamplePath) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, fmt.Errorf("Unable to discover .env.sample for %s", recipe.Name) | 		return false, fmt.Errorf("Unable to discover .env.sample for %s", recipe.Name) | ||||||
| @ -351,7 +358,7 @@ func LintHasPublishedVersion(recipe recipe.Recipe) (bool, error) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func LintMetadataFilledIn(r recipe.Recipe) (bool, error) { | func LintMetadataFilledIn(r recipe.Recipe) (bool, error) { | ||||||
| 	features, category, err := recipe.GetRecipeFeaturesAndCategory(r.Name) | 	features, category, err := r.GetRecipeFeaturesAndCategory() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, err | 		return false, err | ||||||
| 	} | 	} | ||||||
| @ -401,6 +408,16 @@ func LintHasRecipeRepo(recipe recipe.Recipe) (bool, error) { | |||||||
| 	return true, nil | 	return true, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func LintSecretLengths(recipe recipe.Recipe) (bool, error) { | ||||||
|  | 	for name := range recipe.Config.Secrets { | ||||||
|  | 		if len(name) > 12 { | ||||||
|  | 			return false, fmt.Errorf("secret %s is longer than 12 characters", name) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return true, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func LintValidTags(recipe recipe.Recipe) (bool, error) { | func LintValidTags(recipe recipe.Recipe) (bool, error) { | ||||||
| 	recipeDir := path.Join(config.RECIPES_DIR, recipe.Name) | 	recipeDir := path.Join(config.RECIPES_DIR, recipe.Name) | ||||||
|  |  | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ package recipe | |||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" | 	"net/url" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path" | 	"path" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| @ -22,8 +22,8 @@ import ( | |||||||
| 	loader "coopcloud.tech/abra/pkg/upstream/stack" | 	loader "coopcloud.tech/abra/pkg/upstream/stack" | ||||||
| 	"coopcloud.tech/abra/pkg/web" | 	"coopcloud.tech/abra/pkg/web" | ||||||
| 	"coopcloud.tech/tagcmp" | 	"coopcloud.tech/tagcmp" | ||||||
|  | 	"github.com/distribution/reference" | ||||||
| 	composetypes "github.com/docker/cli/cli/compose/types" | 	composetypes "github.com/docker/cli/cli/compose/types" | ||||||
| 	"github.com/docker/distribution/reference" |  | ||||||
| 	"github.com/go-git/go-git/v5" | 	"github.com/go-git/go-git/v5" | ||||||
| 	"github.com/go-git/go-git/v5/plumbing" | 	"github.com/go-git/go-git/v5/plumbing" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| @ -130,17 +130,10 @@ type Features struct { | |||||||
| 	SSO         string `json:"sso"` | 	SSO         string `json:"sso"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // Recipe represents a recipe. |  | ||||||
| type Recipe struct { |  | ||||||
| 	Name   string |  | ||||||
| 	Config *composetypes.Config |  | ||||||
| 	Meta   RecipeMeta |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Push pushes the latest changes to a SSH URL remote. You need to have your | // Push pushes the latest changes to a SSH URL remote. You need to have your | ||||||
| // local SSH configuration for git.coopcloud.tech working for this to work | // local SSH configuration for git.coopcloud.tech working for this to work | ||||||
| func (r Recipe) Push(dryRun bool) error { | func (r Recipe) Push(dryRun bool) error { | ||||||
| 	repo, err := git.PlainOpen(r.Dir()) | 	repo, err := git.PlainOpen(r.Dir) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @ -149,21 +142,16 @@ func (r Recipe) Push(dryRun bool) error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := gitPkg.Push(r.Dir(), "origin-ssh", true, dryRun); err != nil { | 	if err := gitPkg.Push(r.Dir, "origin-ssh", true, dryRun); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // Dir retrieves the recipe repository path |  | ||||||
| func (r Recipe) Dir() string { |  | ||||||
| 	return path.Join(config.RECIPES_DIR, r.Name) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // UpdateLabel updates a recipe label | // UpdateLabel updates a recipe label | ||||||
| func (r Recipe) UpdateLabel(pattern, serviceName, label string) error { | func (r Recipe) UpdateLabel(pattern, serviceName, label string) error { | ||||||
| 	fullPattern := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, r.Name, pattern) | 	fullPattern := fmt.Sprintf("%s/%s", r.Dir, pattern) | ||||||
| 	if err := compose.UpdateLabel(fullPattern, serviceName, label, r.Name); err != nil { | 	if err := compose.UpdateLabel(fullPattern, serviceName, label, r.Name); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @ -172,7 +160,7 @@ func (r Recipe) UpdateLabel(pattern, serviceName, label string) error { | |||||||
|  |  | ||||||
| // UpdateTag updates a recipe tag | // UpdateTag updates a recipe tag | ||||||
| func (r Recipe) UpdateTag(image, tag string) (bool, error) { | func (r Recipe) UpdateTag(image, tag string) (bool, error) { | ||||||
| 	pattern := fmt.Sprintf("%s/%s/compose**yml", config.RECIPES_DIR, r.Name) | 	pattern := fmt.Sprintf("%s/compose**yml", r.Dir) | ||||||
|  |  | ||||||
| 	image = formatter.StripTagMeta(image) | 	image = formatter.StripTagMeta(image) | ||||||
|  |  | ||||||
| @ -188,7 +176,7 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) { | |||||||
| func (r Recipe) Tags() ([]string, error) { | func (r Recipe) Tags() ([]string, error) { | ||||||
| 	var tags []string | 	var tags []string | ||||||
|  |  | ||||||
| 	repo, err := git.PlainOpen(r.Dir()) | 	repo, err := git.PlainOpen(r.Dir) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return tags, err | 		return tags, err | ||||||
| 	} | 	} | ||||||
| @ -210,49 +198,51 @@ func (r Recipe) Tags() ([]string, error) { | |||||||
| 	return tags, nil | 	return tags, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // Get retrieves a recipe. | // // Get2 retrieves a recipe. | ||||||
| func Get(recipeName string, offline bool) (Recipe, error) { | // func (r Recipe) Load(offline bool) (Recipe2, error) { | ||||||
| 	if err := EnsureExists(recipeName); err != nil { | // | ||||||
| 		return Recipe{}, err | // 	meta, err := r.GetRecipeMeta(offline) | ||||||
|  | // 	if err != nil { | ||||||
|  | // 		switch err.(type) { | ||||||
|  | // 		case RecipeMissingFromCatalogue: | ||||||
|  | // 			meta = RecipeMeta{} | ||||||
|  | // 		default: | ||||||
|  | // 			return Recipe2{}, err | ||||||
|  | // 		} | ||||||
|  | // 	} | ||||||
|  | // | ||||||
|  | // 	return Recipe2{ | ||||||
|  | // 		Name:   r.Name, | ||||||
|  | // 		Config: config, | ||||||
|  | // 		Meta:   meta, | ||||||
|  | // 	}, nil | ||||||
|  | // } | ||||||
|  |  | ||||||
|  | func (r Recipe) LoadConfig() error { | ||||||
|  | 	if err := r.EnsureExists(); err != nil { | ||||||
|  | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	pattern := fmt.Sprintf("%s/%s/compose**yml", config.RECIPES_DIR, recipeName) | 	pattern := fmt.Sprintf("%s/compose**yml", r.Dir) | ||||||
| 	composeFiles, err := filepath.Glob(pattern) | 	composeFiles, err := filepath.Glob(pattern) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return Recipe{}, err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if len(composeFiles) == 0 { | 	if len(composeFiles) == 0 { | ||||||
| 		return Recipe{}, fmt.Errorf("%s is missing a compose.yml or compose.*.yml file?", recipeName) | 		return fmt.Errorf("%s is missing a compose.yml or compose.*.yml file?", r.Name) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample") |  | ||||||
| 	sampleEnv, err := config.ReadEnv(envSamplePath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return Recipe{}, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	opts := stack.Deploy{Composefiles: composeFiles} | 	opts := stack.Deploy{Composefiles: composeFiles} | ||||||
|  | 	sampleEnv, err := r.SampleEnv() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
| 	config, err := loader.LoadComposefile(opts, sampleEnv) | 	config, err := loader.LoadComposefile(opts, sampleEnv) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return Recipe{}, err | 		return err | ||||||
| 	} | 	} | ||||||
|  | 	r.Config = config | ||||||
| 	meta, err := GetRecipeMeta(recipeName, offline) | 	return nil | ||||||
| 	if err != nil { |  | ||||||
| 		switch err.(type) { |  | ||||||
| 		case RecipeMissingFromCatalogue: |  | ||||||
| 			meta = RecipeMeta{} |  | ||||||
| 		default: |  | ||||||
| 			return Recipe{}, err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return Recipe{ |  | ||||||
| 		Name:   recipeName, |  | ||||||
| 		Config: config, |  | ||||||
| 		Meta:   meta, |  | ||||||
| 	}, nil |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (r Recipe) SampleEnv() (map[string]string, error) { | func (r Recipe) SampleEnv() (map[string]string, error) { | ||||||
| @ -264,33 +254,71 @@ func (r Recipe) SampleEnv() (map[string]string, error) { | |||||||
| 	return sampleEnv, nil | 	return sampleEnv, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type Recipe struct { | ||||||
|  | 	Name        string | ||||||
|  | 	NameEscaped string | ||||||
|  | 	Dir         string | ||||||
|  | 	GitURL      string | ||||||
|  |  | ||||||
|  | 	Config *composetypes.Config | ||||||
|  | 	Meta   RecipeMeta | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Get(recipeName string) (Recipe, error) { | ||||||
|  | 	if !strings.Contains(recipeName, "/") { | ||||||
|  | 		return Recipe{ | ||||||
|  | 			Name:   recipeName, | ||||||
|  | 			GitURL: fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, recipeName), | ||||||
|  | 		}, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	u, err := url.Parse(recipeName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return Recipe{}, err | ||||||
|  | 	} | ||||||
|  | 	u.Scheme = "https" | ||||||
|  | 	u.RawPath, err = url.JoinPath(u.RawPath, ".git") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return Recipe{}, err | ||||||
|  | 	} | ||||||
|  | 	return Recipe{ | ||||||
|  | 		Name:        recipeName, | ||||||
|  | 		NameEscaped: escapeRecipeName(recipeName), | ||||||
|  | 		Dir:         path.Join(config.RECIPES_DIR, escapeRecipeName(recipeName)), | ||||||
|  | 		GitURL:      u.String(), | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func escapeRecipeName(recipeName string) string { | ||||||
|  | 	recipeName = strings.ReplaceAll(recipeName, "/", "_") | ||||||
|  | 	recipeName = strings.ReplaceAll(recipeName, ".", "_") | ||||||
|  | 	return recipeName | ||||||
|  | } | ||||||
|  |  | ||||||
| // Ensure makes sure the recipe exists, is up to date and has the latest version checked out. | // Ensure makes sure the recipe exists, is up to date and has the latest version checked out. | ||||||
| func Ensure(recipeName string) error { | func (r Recipe) Ensure() error { | ||||||
| 	if err := EnsureExists(recipeName); err != nil { | 	if err := r.EnsureExists(); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	if err := EnsureUpToDate(recipeName); err != nil { | 	if err := r.EnsureUpToDate(); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	if err := EnsureLatest(recipeName); err != nil { | 	if err := r.EnsureLatest(); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // EnsureExists ensures that a recipe is locally cloned | // EnsureExists ensures that the recipe is locally cloned | ||||||
| func EnsureExists(recipeName string) error { | func (r Recipe) EnsureExists() error { | ||||||
| 	recipeDir := path.Join(config.RECIPES_DIR, recipeName) | 	if _, err := os.Stat(r.Dir); os.IsNotExist(err) { | ||||||
|  | 		logrus.Debugf("%s does not exist, attemmpting to clone", r.Dir) | ||||||
| 	if _, err := os.Stat(recipeDir); os.IsNotExist(err) { | 		if err := gitPkg.Clone(r.Dir, r.GitURL); err != nil { | ||||||
| 		logrus.Debugf("%s does not exist, attemmpting to clone", recipeDir) |  | ||||||
| 		url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, recipeName) |  | ||||||
| 		if err := gitPkg.Clone(recipeDir, url); err != nil { |  | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := gitPkg.EnsureGitRepo(recipeDir); err != nil { | 	if err := gitPkg.EnsureGitRepo(r.Dir); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @ -298,14 +326,12 @@ func EnsureExists(recipeName string) error { | |||||||
| } | } | ||||||
|  |  | ||||||
| // EnsureVersion checks whether a specific version exists for a recipe. | // EnsureVersion checks whether a specific version exists for a recipe. | ||||||
| func EnsureVersion(recipeName, version string) error { | func (r Recipe) EnsureVersion(version string) error { | ||||||
| 	recipeDir := path.Join(config.RECIPES_DIR, recipeName) | 	if err := gitPkg.EnsureGitRepo(r.Dir); err != nil { | ||||||
|  |  | ||||||
| 	if err := gitPkg.EnsureGitRepo(recipeDir); err != nil { |  | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	repo, err := git.PlainOpen(recipeDir) | 	repo, err := git.PlainOpen(r.Dir) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @ -329,11 +355,11 @@ func EnsureVersion(recipeName, version string) error { | |||||||
|  |  | ||||||
| 	joinedTags := strings.Join(parsedTags, ", ") | 	joinedTags := strings.Join(parsedTags, ", ") | ||||||
| 	if joinedTags != "" { | 	if joinedTags != "" { | ||||||
| 		logrus.Debugf("read %s as tags for recipe %s", joinedTags, recipeName) | 		logrus.Debugf("read %s as tags for recipe %s", joinedTags, r.Name) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if tagRef.String() == "" { | 	if tagRef.String() == "" { | ||||||
| 		return fmt.Errorf("the local copy of %s doesn't seem to have version %s available?", recipeName, version) | 		return fmt.Errorf("the local copy of %s doesn't seem to have version %s available?", r.Name, version) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	worktree, err := repo.Worktree() | 	worktree, err := repo.Worktree() | ||||||
| @ -350,37 +376,33 @@ func EnsureVersion(recipeName, version string) error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	logrus.Debugf("successfully checked %s out to %s in %s", recipeName, tagRef.Short(), recipeDir) | 	logrus.Debugf("successfully checked %s out to %s in %s", r.Name, tagRef.Short(), r.Dir) | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // EnsureIsClean makes sure that the recipe repository has no unstaged changes. | // EnsureIsClean makes sure that the recipe repository has no unstaged changes. | ||||||
| func EnsureIsClean(recipeName string) error { | func (r Recipe) EnsureIsClean() error { | ||||||
| 	recipeDir := path.Join(config.RECIPES_DIR, recipeName) | 	isClean, err := gitPkg.IsClean(r.Dir) | ||||||
|  |  | ||||||
| 	isClean, err := gitPkg.IsClean(recipeDir) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("unable to check git clean status in %s: %s", recipeDir, err) | 		return fmt.Errorf("unable to check git clean status in %s: %s", r.Dir, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if !isClean { | 	if !isClean { | ||||||
| 		msg := "%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding" | 		msg := "%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding" | ||||||
| 		return fmt.Errorf(msg, recipeName, recipeDir) | 		return fmt.Errorf(msg, r.Name, r.Dir) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // EnsureLatest makes sure the latest commit is checked out for a local recipe repository | // EnsureLatest makes sure the latest commit is checked out for the local recipe repository | ||||||
| func EnsureLatest(recipeName string) error { | func (r Recipe) EnsureLatest() error { | ||||||
| 	recipeDir := path.Join(config.RECIPES_DIR, recipeName) | 	if err := gitPkg.EnsureGitRepo(r.Dir); err != nil { | ||||||
|  |  | ||||||
| 	if err := gitPkg.EnsureGitRepo(recipeDir); err != nil { |  | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	repo, err := git.PlainOpen(recipeDir) | 	repo, err := git.PlainOpen(r.Dir) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @ -390,7 +412,7 @@ func EnsureLatest(recipeName string) error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	branch, err := gitPkg.GetDefaultBranch(repo, recipeDir) | 	branch, err := gitPkg.GetDefaultBranch(repo, r.Dir) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @ -402,7 +424,7 @@ func EnsureLatest(recipeName string) error { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := worktree.Checkout(checkOutOpts); err != nil { | 	if err := worktree.Checkout(checkOutOpts); err != nil { | ||||||
| 		logrus.Debugf("failed to check out %s in %s", branch, recipeDir) | 		logrus.Debugf("failed to check out %s in %s", branch, r.Dir) | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @ -410,18 +432,17 @@ func EnsureLatest(recipeName string) error { | |||||||
| } | } | ||||||
|  |  | ||||||
| // ChaosVersion constructs a chaos mode recipe version. | // ChaosVersion constructs a chaos mode recipe version. | ||||||
| func ChaosVersion(recipeName string) (string, error) { | func (r Recipe) ChaosVersion() (string, error) { | ||||||
| 	var version string | 	var version string | ||||||
|  |  | ||||||
| 	head, err := gitPkg.GetRecipeHead(recipeName) | 	head, err := gitPkg.GetHead(r.Dir) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return version, err | 		return version, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	version = formatter.SmallSHA(head.String()) | 	version = formatter.SmallSHA(head.String()) | ||||||
|  |  | ||||||
| 	recipeDir := path.Join(config.RECIPES_DIR, recipeName) | 	isClean, err := gitPkg.IsClean(r.Dir) | ||||||
| 	isClean, err := gitPkg.IsClean(recipeDir) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return version, err | 		return version, err | ||||||
| 	} | 	} | ||||||
| @ -464,22 +485,22 @@ func GetVersionLabelLocal(recipe Recipe) (string, error) { | |||||||
| 	return label, nil | 	return label, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func GetRecipeFeaturesAndCategory(recipeName string) (Features, string, error) { | func (r Recipe) GetRecipeFeaturesAndCategory() (Features, string, error) { | ||||||
| 	feat := Features{} | 	feat := Features{} | ||||||
|  |  | ||||||
| 	var category string | 	var category string | ||||||
|  |  | ||||||
| 	readmePath := path.Join(config.RECIPES_DIR, recipeName, "README.md") | 	readmePath := path.Join(r.Dir, "README.md") | ||||||
|  |  | ||||||
| 	logrus.Debugf("attempting to open %s for recipe metadata parsing", readmePath) | 	logrus.Debugf("attempting to open %s for recipe metadata parsing", readmePath) | ||||||
|  |  | ||||||
| 	readmeFS, err := ioutil.ReadFile(readmePath) | 	readmeFS, err := os.ReadFile(readmePath) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return feat, category, err | 		return feat, category, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	readmeMetadata, err := GetStringInBetween( // Find text between delimiters | 	readmeMetadata, err := GetStringInBetween( // Find text between delimiters | ||||||
| 		recipeName, | 		r.Name, | ||||||
| 		string(readmeFS), | 		string(readmeFS), | ||||||
| 		"<!-- metadata -->", "<!-- endmetadata -->", | 		"<!-- metadata -->", "<!-- endmetadata -->", | ||||||
| 	) | 	) | ||||||
| @ -530,7 +551,7 @@ func GetRecipeFeaturesAndCategory(recipeName string) (Features, string, error) { | |||||||
| 		if strings.Contains(val, "**Image**") { | 		if strings.Contains(val, "**Image**") { | ||||||
| 			imageMetadata, err := GetImageMetadata(strings.TrimSpace( | 			imageMetadata, err := GetImageMetadata(strings.TrimSpace( | ||||||
| 				strings.TrimPrefix(val, "* **Image**:"), | 				strings.TrimPrefix(val, "* **Image**:"), | ||||||
| 			), recipeName) | 			), r.Name) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
| @ -597,38 +618,36 @@ func GetStringInBetween(recipeName, str, start, end string) (result string, err | |||||||
| } | } | ||||||
|  |  | ||||||
| // EnsureUpToDate ensures that the local repo is synced to the remote | // EnsureUpToDate ensures that the local repo is synced to the remote | ||||||
| func EnsureUpToDate(recipeName string) error { | func (r Recipe) EnsureUpToDate() error { | ||||||
| 	recipeDir := path.Join(config.RECIPES_DIR, recipeName) | 	repo, err := git.PlainOpen(r.Dir) | ||||||
|  |  | ||||||
| 	repo, err := git.PlainOpen(recipeDir) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("unable to open %s: %s", recipeDir, err) | 		return fmt.Errorf("unable to open %s: %s", r.Dir, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	remotes, err := repo.Remotes() | 	remotes, err := repo.Remotes() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("unable to read remotes in %s: %s", recipeDir, err) | 		return fmt.Errorf("unable to read remotes in %s: %s", r.Dir, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if len(remotes) == 0 { | 	if len(remotes) == 0 { | ||||||
| 		logrus.Debugf("cannot ensure %s is up-to-date, no git remotes configured", recipeName) | 		logrus.Debugf("cannot ensure %s is up-to-date, no git remotes configured", r.Name) | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	worktree, err := repo.Worktree() | 	worktree, err := repo.Worktree() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("unable to open git work tree in %s: %s", recipeDir, err) | 		return fmt.Errorf("unable to open git work tree in %s: %s", r.Dir, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	branch, err := gitPkg.CheckoutDefaultBranch(repo, recipeDir) | 	branch, err := gitPkg.CheckoutDefaultBranch(repo, r.Dir) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("unable to check out default branch in %s: %s", recipeDir, err) | 		return fmt.Errorf("unable to check out default branch in %s: %s", r.Dir, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	fetchOpts := &git.FetchOptions{Tags: git.AllTags} | 	fetchOpts := &git.FetchOptions{Tags: git.AllTags} | ||||||
| 	if err := repo.Fetch(fetchOpts); err != nil { | 	if err := repo.Fetch(fetchOpts); err != nil { | ||||||
| 		if !strings.Contains(err.Error(), "already up-to-date") { | 		if !strings.Contains(err.Error(), "already up-to-date") { | ||||||
| 			return fmt.Errorf("unable to fetch tags in %s: %s", recipeDir, err) | 			return fmt.Errorf("unable to fetch tags in %s: %s", r.Dir, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @ -640,11 +659,11 @@ func EnsureUpToDate(recipeName string) error { | |||||||
|  |  | ||||||
| 	if err := worktree.Pull(opts); err != nil { | 	if err := worktree.Pull(opts); err != nil { | ||||||
| 		if !strings.Contains(err.Error(), "already up-to-date") { | 		if !strings.Contains(err.Error(), "already up-to-date") { | ||||||
| 			return fmt.Errorf("unable to git pull in %s: %s", recipeDir, err) | 			return fmt.Errorf("unable to git pull in %s: %s", r.Dir, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	logrus.Debugf("fetched latest git changes for %s", recipeName) | 	logrus.Debugf("fetched latest git changes for %s", r.Name) | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| @ -672,7 +691,7 @@ func ReadRecipeCatalogue(offline bool) (RecipeCatalogue, error) { | |||||||
|  |  | ||||||
| // readRecipeCatalogueFS reads the catalogue from the file system. | // readRecipeCatalogueFS reads the catalogue from the file system. | ||||||
| func readRecipeCatalogueFS(target interface{}) error { | func readRecipeCatalogueFS(target interface{}) error { | ||||||
| 	recipesJSONFS, err := ioutil.ReadFile(config.RECIPES_JSON) | 	recipesJSONFS, err := os.ReadFile(config.RECIPES_JSON) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @ -724,20 +743,20 @@ func (r RecipeMissingFromCatalogue) Error() string { | |||||||
| } | } | ||||||
|  |  | ||||||
| // GetRecipeMeta retrieves the recipe metadata from the recipe catalogue. | // GetRecipeMeta retrieves the recipe metadata from the recipe catalogue. | ||||||
| func GetRecipeMeta(recipeName string, offline bool) (RecipeMeta, error) { | func (r Recipe) GetRecipeMeta(offline bool) (RecipeMeta, error) { | ||||||
| 	catl, err := ReadRecipeCatalogue(offline) | 	catl, err := ReadRecipeCatalogue(offline) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return RecipeMeta{}, err | 		return RecipeMeta{}, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	recipeMeta, ok := catl[recipeName] | 	recipeMeta, ok := catl[r.Name] | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return RecipeMeta{}, RecipeMissingFromCatalogue{ | 		return RecipeMeta{}, RecipeMissingFromCatalogue{ | ||||||
| 			err: fmt.Sprintf("recipe %s does not exist?", recipeName), | 			err: fmt.Sprintf("recipe %s does not exist?", r.Name), | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	logrus.Debugf("recipe metadata retrieved for %s", recipeName) | 	logrus.Debugf("recipe metadata retrieved for %s", r.Name) | ||||||
|  |  | ||||||
| 	return recipeMeta, nil | 	return recipeMeta, nil | ||||||
| } | } | ||||||
| @ -864,13 +883,12 @@ func ReadReposMetadata() (RepoCatalogue, error) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // GetRecipeVersions retrieves all recipe versions. | // GetRecipeVersions retrieves all recipe versions. | ||||||
| func GetRecipeVersions(recipeName string, offline bool) (RecipeVersions, error) { | func (r Recipe) GetVersions(offline bool) (RecipeVersions, error) { | ||||||
| 	versions := RecipeVersions{} | 	versions := RecipeVersions{} | ||||||
| 	recipeDir := path.Join(config.RECIPES_DIR, recipeName) |  | ||||||
|  |  | ||||||
| 	logrus.Debugf("attempting to open git repository in %s", recipeDir) | 	logrus.Debugf("attempting to open git repository in %s", r.Dir) | ||||||
|  |  | ||||||
| 	repo, err := git.PlainOpen(recipeDir) | 	repo, err := git.PlainOpen(r.Dir) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return versions, err | 		return versions, err | ||||||
| 	} | 	} | ||||||
| @ -888,7 +906,7 @@ func GetRecipeVersions(recipeName string, offline bool) (RecipeVersions, error) | |||||||
| 	if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) { | 	if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) { | ||||||
| 		tag := strings.TrimPrefix(string(ref.Name()), "refs/tags/") | 		tag := strings.TrimPrefix(string(ref.Name()), "refs/tags/") | ||||||
|  |  | ||||||
| 		logrus.Debugf("processing %s for %s", tag, recipeName) | 		logrus.Debugf("processing %s for %s", tag, r.Name) | ||||||
|  |  | ||||||
| 		checkOutOpts := &git.CheckoutOptions{ | 		checkOutOpts := &git.CheckoutOptions{ | ||||||
| 			Create: false, | 			Create: false, | ||||||
| @ -896,19 +914,18 @@ func GetRecipeVersions(recipeName string, offline bool) (RecipeVersions, error) | |||||||
| 			Branch: plumbing.ReferenceName(ref.Name()), | 			Branch: plumbing.ReferenceName(ref.Name()), | ||||||
| 		} | 		} | ||||||
| 		if err := worktree.Checkout(checkOutOpts); err != nil { | 		if err := worktree.Checkout(checkOutOpts); err != nil { | ||||||
| 			logrus.Debugf("failed to check out %s in %s", tag, recipeDir) | 			logrus.Debugf("failed to check out %s in %s", tag, r.Dir) | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		logrus.Debugf("successfully checked out %s in %s", ref.Name(), recipeDir) | 		logrus.Debugf("successfully checked out %s in %s", ref.Name(), r.Dir) | ||||||
|  |  | ||||||
| 		recipe, err := Get(recipeName, offline) | 		if err := r.LoadConfig(); err != nil { | ||||||
| 		if err != nil { |  | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		versionMeta := make(map[string]ServiceMeta) | 		versionMeta := make(map[string]ServiceMeta) | ||||||
| 		for _, service := range recipe.Config.Services { | 		for _, service := range r.Config.Services { | ||||||
|  |  | ||||||
| 			img, err := reference.ParseNormalizedNamed(service.Image) | 			img, err := reference.ParseNormalizedNamed(service.Image) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| @ -941,13 +958,14 @@ func GetRecipeVersions(recipeName string, offline bool) (RecipeVersions, error) | |||||||
| 		return versions, err | 		return versions, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	_, err = gitPkg.CheckoutDefaultBranch(repo, recipeDir) | 	_, err = gitPkg.CheckoutDefaultBranch(repo, r.Dir) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return versions, err | 		return versions, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	sortRecipeVersions(versions) | 	sortRecipeVersions(versions) | ||||||
|  |  | ||||||
| 	logrus.Debugf("collected %s for %s", versions, recipeName) | 	logrus.Debugf("collected %s for %s", versions, r.Name) | ||||||
|  |  | ||||||
| 	return versions, nil | 	return versions, nil | ||||||
| } | } | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ import ( | |||||||
|  |  | ||||||
| func TestGetVersionLabelLocalDoesNotUseTimeoutLabel(t *testing.T) { | func TestGetVersionLabelLocalDoesNotUseTimeoutLabel(t *testing.T) { | ||||||
| 	offline := true | 	offline := true | ||||||
| 	recipe, err := Get("traefik", offline) | 	recipe, err := Get2("traefik", offline) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -89,7 +89,7 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin | |||||||
| 	appEnv["STACK_NAME"] = stackName | 	appEnv["STACK_NAME"] = stackName | ||||||
|  |  | ||||||
| 	opts := stack.Deploy{Composefiles: composeFiles} | 	opts := stack.Deploy{Composefiles: composeFiles} | ||||||
| 	config, err := loader.LoadComposefile(opts, appEnv) | 	composeConfig, err := loader.LoadComposefile(opts, appEnv) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| @ -100,7 +100,7 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var enabledSecrets []string | 	var enabledSecrets []string | ||||||
| 	for _, service := range config.Services { | 	for _, service := range composeConfig.Services { | ||||||
| 		for _, secret := range service.Secrets { | 		for _, secret := range service.Secrets { | ||||||
| 			enabledSecrets = append(enabledSecrets, secret.Source) | 			enabledSecrets = append(enabledSecrets, secret.Source) | ||||||
| 		} | 		} | ||||||
| @ -112,7 +112,7 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	secretValues := map[string]Secret{} | 	secretValues := map[string]Secret{} | ||||||
| 	for secretId, secretConfig := range config.Secrets { | 	for secretId, secretConfig := range composeConfig.Secrets { | ||||||
| 		if string(secretConfig.Name[len(secretConfig.Name)-1]) == "_" { | 		if string(secretConfig.Name[len(secretConfig.Name)-1]) == "_" { | ||||||
| 			return nil, fmt.Errorf("missing version for secret? (%s)", secretId) | 			return nil, fmt.Errorf("missing version for secret? (%s)", secretId) | ||||||
| 		} | 		} | ||||||
| @ -126,6 +126,10 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin | |||||||
| 		secretVersion := secretConfig.Name[lastIdx+1:] | 		secretVersion := secretConfig.Name[lastIdx+1:] | ||||||
| 		value := Secret{Version: secretVersion, RemoteName: secretConfig.Name} | 		value := Secret{Version: secretVersion, RemoteName: secretConfig.Name} | ||||||
|  |  | ||||||
|  | 		if len(value.RemoteName) > config.MAX_DOCKER_SECRET_LENGTH { | ||||||
|  | 			return nil, fmt.Errorf("secret %s is > %d chars when combined with %s", secretId, config.MAX_DOCKER_SECRET_LENGTH, stackName) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		// Check if the length modifier is set for this secret. | 		// Check if the length modifier is set for this secret. | ||||||
| 		for envName, modifierValues := range appModifiers { | 		for envName, modifierValues := range appModifiers { | ||||||
| 			// configWithoutEnv contains the raw name as defined in the compose.yaml | 			// configWithoutEnv contains the raw name as defined in the compose.yaml | ||||||
|  | |||||||
| @ -28,3 +28,12 @@ func TestReadSecretsConfig(t *testing.T) { | |||||||
| 	assert.Equal(t, "v2", secretsFromConfig["test_pass_three"].Version) | 	assert.Equal(t, "v2", secretsFromConfig["test_pass_three"].Version) | ||||||
| 	assert.Equal(t, 0, secretsFromConfig["test_pass_three"].Length) | 	assert.Equal(t, 0, secretsFromConfig["test_pass_three"].Length) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestReadSecretsConfigWithLongDomain(t *testing.T) { | ||||||
|  | 	composeFiles := []string{"./testdir/compose.yaml"} | ||||||
|  | 	_, err := ReadSecretsConfig("./testdir/.env.sample", composeFiles, "should_break_on_forty_eight_char_stack_nameeeeee") | ||||||
|  | 	if err == nil { | ||||||
|  | 		t.Fatal("expected failure, stack name is too long") | ||||||
|  | 	} | ||||||
|  | 	assert.Contains(t, err.Error(), "is > 64 chars") | ||||||
|  | } | ||||||
|  | |||||||
| @ -2,73 +2,14 @@ package ssh | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os/exec" |  | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // HostConfig is a SSH host config. |  | ||||||
| type HostConfig struct { |  | ||||||
| 	Host         string |  | ||||||
| 	IdentityFile string |  | ||||||
| 	Port         string |  | ||||||
| 	User         string |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // String presents a human friendly output for the HostConfig. |  | ||||||
| func (h HostConfig) String() string { |  | ||||||
| 	return fmt.Sprintf( |  | ||||||
| 		"{host: %s, username: %s, port: %s, identityfile: %s}", |  | ||||||
| 		h.Host, |  | ||||||
| 		h.User, |  | ||||||
| 		h.Port, |  | ||||||
| 		h.IdentityFile, |  | ||||||
| 	) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // GetHostConfig retrieves a ~/.ssh/config config for a host using /usr/bin/ssh |  | ||||||
| // directly. We therefore maintain consistent interop with this standard |  | ||||||
| // tooling. This is useful because SSH confuses a lot of people and having to |  | ||||||
| // learn how two tools (`ssh` and `abra`) handle SSH connection details instead |  | ||||||
| // of one (just `ssh`) is Not Cool. Here's to less bug reports on this topic! |  | ||||||
| func GetHostConfig(hostname string) (HostConfig, error) { |  | ||||||
| 	var hostConfig HostConfig |  | ||||||
|  |  | ||||||
| 	out, err := exec.Command("ssh", "-G", hostname).Output() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return hostConfig, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for _, line := range strings.Split(string(out), "\n") { |  | ||||||
| 		entries := strings.Split(line, " ") |  | ||||||
| 		for idx, entry := range entries { |  | ||||||
| 			if entry == "hostname" { |  | ||||||
| 				hostConfig.Host = entries[idx+1] |  | ||||||
| 			} |  | ||||||
| 			if entry == "user" { |  | ||||||
| 				hostConfig.User = entries[idx+1] |  | ||||||
| 			} |  | ||||||
| 			if entry == "port" { |  | ||||||
| 				hostConfig.Port = entries[idx+1] |  | ||||||
| 			} |  | ||||||
| 			if entry == "identityfile" { |  | ||||||
| 				if hostConfig.IdentityFile == "" { |  | ||||||
| 					hostConfig.IdentityFile = entries[idx+1] |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	logrus.Debugf("retrieved ssh config for %s: %s", hostname, hostConfig.String()) |  | ||||||
|  |  | ||||||
| 	return hostConfig, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Fatal is a error output wrapper which aims to make SSH failures easier to | // Fatal is a error output wrapper which aims to make SSH failures easier to | ||||||
| // parse through re-wording. | // parse through re-wording. | ||||||
| func Fatal(hostname string, err error) error { | func Fatal(hostname string, err error) error { | ||||||
| 	out := err.Error() | 	out := err.Error() | ||||||
|  |  | ||||||
| 	if strings.Contains(out, "Host key verification failed.") { | 	if strings.Contains(out, "Host key verification failed.") { | ||||||
| 		return fmt.Errorf("SSH host key verification failed for %s", hostname) | 		return fmt.Errorf("SSH host key verification failed for %s", hostname) | ||||||
| 	} else if strings.Contains(out, "Could not resolve hostname") { | 	} else if strings.Contains(out, "Could not resolve hostname") { | ||||||
| @ -79,7 +20,7 @@ func Fatal(hostname string, err error) error { | |||||||
| 		return fmt.Errorf("ssh auth: permission denied for %s", hostname) | 		return fmt.Errorf("ssh auth: permission denied for %s", hostname) | ||||||
| 	} else if strings.Contains(out, "Network is unreachable") { | 	} else if strings.Contains(out, "Network is unreachable") { | ||||||
| 		return fmt.Errorf("unable to connect to %s, network is unreachable?", hostname) | 		return fmt.Errorf("unable to connect to %s, network is unreachable?", hostname) | ||||||
| 	} else { | 	} | ||||||
|  |  | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
| } |  | ||||||
|  | |||||||
| @ -16,12 +16,12 @@ import ( | |||||||
| // GetConnectionHelper returns Docker-specific connection helper for the given URL. | // GetConnectionHelper returns Docker-specific connection helper for the given URL. | ||||||
| // GetConnectionHelper returns nil without error when no helper is registered for the scheme. | // GetConnectionHelper returns nil without error when no helper is registered for the scheme. | ||||||
| // | // | ||||||
| // ssh://<user>@<host> URL requires Docker 18.09 or later on the remote host. | // ssh://<host> URL requires Docker 18.09 or later on the remote host. | ||||||
| func GetConnectionHelper(daemonURL string) (*connhelper.ConnectionHelper, error) { | func GetConnectionHelper(daemonURL string) (*connhelper.ConnectionHelper, error) { | ||||||
| 	return getConnectionHelper(daemonURL, []string{"-o ConnectTimeout=60"}) | 	return getConnectionHelper(daemonURL) | ||||||
| } | } | ||||||
|  |  | ||||||
| func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.ConnectionHelper, error) { | func getConnectionHelper(daemonURL string) (*connhelper.ConnectionHelper, error) { | ||||||
| 	url, err := url.Parse(daemonURL) | 	url, err := url.Parse(daemonURL) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| @ -35,7 +35,7 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.Conne | |||||||
|  |  | ||||||
| 		return &connhelper.ConnectionHelper{ | 		return &connhelper.ConnectionHelper{ | ||||||
| 			Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { | 			Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { | ||||||
| 				return New(ctx, "ssh", append(sshFlags, ctxConnDetails.Args("docker", "system", "dial-stdio")...)...) | 				return New(ctx, "ssh", ctxConnDetails.Args("docker", "system", "dial-stdio")...) | ||||||
| 			}, | 			}, | ||||||
| 			Host: "http://docker.example.com", | 			Host: "http://docker.example.com", | ||||||
| 		}, nil | 		}, nil | ||||||
| @ -45,6 +45,7 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.Conne | |||||||
| 	return nil, err | 	return nil, err | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // NewConnectionHelper creates new connection helper for a remote docker daemon. | ||||||
| func NewConnectionHelper(daemonURL string) (*connhelper.ConnectionHelper, error) { | func NewConnectionHelper(daemonURL string) (*connhelper.ConnectionHelper, error) { | ||||||
| 	helper, err := GetConnectionHelper(daemonURL) | 	helper, err := GetConnectionHelper(daemonURL) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -73,6 +74,7 @@ func getDockerEndpoint(host string) (docker.Endpoint, error) { | |||||||
| 	return ep, nil | 	return ep, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // GetDockerEndpointMetadataAndTLS retrieves the docker endpoint and TLS info for a remote host. | ||||||
| func GetDockerEndpointMetadataAndTLS(host string) (docker.EndpointMeta, *dCliContextStore.EndpointTLSData, error) { | func GetDockerEndpointMetadataAndTLS(host string) (docker.EndpointMeta, *dCliContextStore.EndpointTLSData, error) { | ||||||
| 	ep, err := getDockerEndpoint(host) | 	ep, err := getDockerEndpoint(host) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/docker/cli/cli/command" | 	"github.com/docker/cli/cli/command" | ||||||
| 	"github.com/docker/docker/api/types" | 	"github.com/docker/docker/api/types/container" | ||||||
| 	"github.com/docker/docker/client" | 	"github.com/docker/docker/client" | ||||||
| 	apiclient "github.com/docker/docker/client" | 	apiclient "github.com/docker/docker/client" | ||||||
| 	"github.com/moby/sys/signal" | 	"github.com/moby/sys/signal" | ||||||
| @ -22,7 +22,7 @@ func resizeTtyTo(ctx context.Context, client client.ContainerAPIClient, id strin | |||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	options := types.ResizeOptions{ | 	options := container.ResizeOptions{ | ||||||
| 		Height: height, | 		Height: height, | ||||||
| 		Width:  width, | 		Width:  width, | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -233,7 +233,7 @@ func validateExternalNetworks(ctx context.Context, client dockerClient.NetworkAP | |||||||
| 		network, err := client.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{}) | 		network, err := client.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{}) | ||||||
| 		switch { | 		switch { | ||||||
| 		case dockerClient.IsErrNotFound(err): | 		case dockerClient.IsErrNotFound(err): | ||||||
| 			return errors.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed", networkName) | 			return errors.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed, which you can do by running this on the server: docker network create -d overlay proxy", networkName) | ||||||
| 		case err != nil: | 		case err != nil: | ||||||
| 			return err | 			return err | ||||||
| 		case network.Scope != "swarm": | 		case network.Scope != "swarm": | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| #!/usr/bin/env bash | #!/usr/bin/env bash | ||||||
|  |  | ||||||
| ABRA_VERSION="0.8.1-beta" | ABRA_VERSION="0.9.0-beta" | ||||||
| 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" | ||||||
| RC_VERSION="0.8.0-rc1-beta" | RC_VERSION="0.8.0-rc1-beta" | ||||||
| RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION" | RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION" | ||||||
| @ -45,7 +45,9 @@ function install_abra_release { | |||||||
|   fi |   fi | ||||||
|  |  | ||||||
|   ARCH=$(uname -m) |   ARCH=$(uname -m) | ||||||
|   if [[ $ARCH =~ "aarch64" ]]; then |   if [[ $ARCH =~ "x86_64" ]]; then | ||||||
|  |       ARCH="amd64" | ||||||
|  |   elif [[ $ARCH =~ "aarch64" ]]; then | ||||||
|       ARCH="arm64" |       ARCH="arm64" | ||||||
|   elif [[ $ARCH =~ "armv5l" ]]; then |   elif [[ $ARCH =~ "armv5l" ]]; then | ||||||
|       ARCH="armv5" |       ARCH="armv5" | ||||||
| @ -55,7 +57,7 @@ function install_abra_release { | |||||||
|       ARCH="armv7" |       ARCH="armv7" | ||||||
|   fi |   fi | ||||||
|   PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')_$ARCH |   PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')_$ARCH | ||||||
|   FILENAME="abra_"$ABRA_VERSION"_"$PLATFORM"" |   FILENAME="abra_"$ABRA_VERSION"_"$PLATFORM".tar.gz" | ||||||
|   sed_command_rel='s/.*"assets":\[\{[^]]*"name":"'$FILENAME'"[^}]*"browser_download_url":"([^"]*)".*\].*/\1/p' |   sed_command_rel='s/.*"assets":\[\{[^]]*"name":"'$FILENAME'"[^}]*"browser_download_url":"([^"]*)".*\].*/\1/p' | ||||||
|   sed_command_checksums='s/.*"assets":\[\{[^\]*"name":"checksums.txt"[^}]*"browser_download_url":"([^"]*)".*\].*/\1/p' |   sed_command_checksums='s/.*"assets":\[\{[^\]*"name":"checksums.txt"[^}]*"browser_download_url":"([^"]*)".*\].*/\1/p' | ||||||
|  |  | ||||||
| @ -65,7 +67,7 @@ function install_abra_release { | |||||||
|  |  | ||||||
|   checksums=$(wget -q -O- $checksums_url) |   checksums=$(wget -q -O- $checksums_url) | ||||||
|   checksum=$(echo "$checksums" | grep "$FILENAME" - | sed -En 's/([0-9a-f]{64})\s+'"$FILENAME"'.*/\1/p') |   checksum=$(echo "$checksums" | grep "$FILENAME" - | sed -En 's/([0-9a-f]{64})\s+'"$FILENAME"'.*/\1/p') | ||||||
|   abra_download="/tmp/abra-download" |   abra_download="/tmp/abra-download.tar.gz" | ||||||
|  |  | ||||||
|   echo "downloading $ABRA_VERSION $PLATFORM binary release for abra..." |   echo "downloading $ABRA_VERSION $PLATFORM binary release for abra..." | ||||||
|  |  | ||||||
| @ -77,7 +79,10 @@ function install_abra_release { | |||||||
|       exit 1 |       exit 1 | ||||||
|   fi |   fi | ||||||
|   echo "$(tput setaf 2)check successful!$(tput sgr0)" |   echo "$(tput setaf 2)check successful!$(tput sgr0)" | ||||||
|   mv "$abra_download" "$HOME/.local/bin/abra" |   cd /tmp/ | ||||||
|  |   tar xf abra-download.tar.gz | ||||||
|  |   mv abra "$HOME/.local/bin/abra" | ||||||
|  |   tar tf abra-download.tar.gz | xargs rm -f | ||||||
|   chmod +x "$HOME/.local/bin/abra" |   chmod +x "$HOME/.local/bin/abra" | ||||||
|  |  | ||||||
|   x=$(echo $PATH | grep $HOME/.local/bin) |   x=$(echo $PATH | grep $HOME/.local/bin) | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| #!/usr/bin/env bash | #!/usr/bin/env bash | ||||||
|  |  | ||||||
| setup_file(){ | setup_file(){ | ||||||
|  |   load "$PWD/tests/integration/helpers/git" | ||||||
|   load "$PWD/tests/integration/helpers/common" |   load "$PWD/tests/integration/helpers/common" | ||||||
|   _common_setup |   _common_setup | ||||||
|   _add_server |   _add_server | ||||||
| @ -362,6 +363,7 @@ teardown(){ | |||||||
|   _reset_app |   _reset_app | ||||||
| } | } | ||||||
|  |  | ||||||
|  | # bats test_tags=slow | ||||||
| @test "recipe config comments not present in values" { | @test "recipe config comments not present in values" { | ||||||
|   run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input |   run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input | ||||||
|   assert_success |   assert_success | ||||||
| @ -370,3 +372,36 @@ teardown(){ | |||||||
|   assert_success |   assert_success | ||||||
|   refute_output --partial 'should be removed' |   refute_output --partial 'should be removed' | ||||||
| } | } | ||||||
|  |  | ||||||
|  | # bats test_tags=slow | ||||||
|  | @test "deploy specific version with incompatible HEAD" { | ||||||
|  |   run sed -i 's/COMPOSE_FILE="compose.yml"/COMPOSE_FILE="compose.yml:compose.extra_secret.yml"/g' \ | ||||||
|  |     "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" | ||||||
|  |   assert_success | ||||||
|  |  | ||||||
|  |   run sed -i 's/#SECRET_EXTRA_PASS_VERSION=v1/SECRET_EXTRA_PASS_VERSION=v1/g' \ | ||||||
|  |     "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" | ||||||
|  |   assert_success | ||||||
|  |  | ||||||
|  |   run $ABRA app secret generate "$TEST_APP_DOMAIN" --all | ||||||
|  |   assert_success | ||||||
|  |   assert_output --partial 'extra_pass' | ||||||
|  |  | ||||||
|  |   run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/compose.extra_secret.yml" | ||||||
|  |   assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/compose.extra_secret.yml" | ||||||
|  |  | ||||||
|  |   _git_commit | ||||||
|  |  | ||||||
|  |   # NOTE(d1): 0.1.1+1.20.2 is a previous version which includes compose.extra_secret.yml | ||||||
|  |   run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.1+1.20.2" --no-input --no-converge-checks | ||||||
|  |   assert_success | ||||||
|  |   refute_output --partial 'no such file or directory' | ||||||
|  |  | ||||||
|  |   _undeploy_app | ||||||
|  |   _reset_app | ||||||
|  |  | ||||||
|  |   run $ABRA app secret rm "$TEST_APP_DOMAIN" --all | ||||||
|  |   assert_success | ||||||
|  |  | ||||||
|  |   _reset_recipe | ||||||
|  | } | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ teardown_file(){ | |||||||
|  |  | ||||||
| setup(){ | setup(){ | ||||||
|   load "$PWD/tests/integration/helpers/common" |   load "$PWD/tests/integration/helpers/common" | ||||||
|  |   load "$PWD/tests/integration/helpers/git" | ||||||
|   _common_setup |   _common_setup | ||||||
|   _fetch_recipe |   _fetch_recipe | ||||||
| } | } | ||||||
| @ -26,14 +27,6 @@ teardown(){ | |||||||
|   run $ABRA app new --generate-bash-completion |   run $ABRA app new --generate-bash-completion | ||||||
|   assert_success |   assert_success | ||||||
|   assert_output --partial "traefik" |   assert_output --partial "traefik" | ||||||
|   assert_output --partial "abra-test-recipe" |  | ||||||
|  |  | ||||||
|   # Note: this test needs to be updated when a new version of the test recipe is published. |  | ||||||
|   run $ABRA app new abra-test-recipe --generate-bash-completion |  | ||||||
|   assert_success |  | ||||||
|   assert_output "0.1.0+1.20.0 |  | ||||||
| 0.1.1+1.20.2 |  | ||||||
| 0.2.0+1.21.0" |  | ||||||
| } | } | ||||||
|  |  | ||||||
| @test "create new app" { | @test "create new app" { | ||||||
| @ -44,8 +37,9 @@ teardown(){ | |||||||
|   assert_success |   assert_success | ||||||
|   assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" |   assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" | ||||||
|  |  | ||||||
|   run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status |   _get_head_hash | ||||||
|   assert_output --partial "up to date" |   _get_current_hash | ||||||
|  |   assert_equal "$headHash" "$currentHash" | ||||||
| } | } | ||||||
|  |  | ||||||
| @test "create new app with version" { | @test "create new app with version" { | ||||||
| @ -56,8 +50,7 @@ teardown(){ | |||||||
|   assert_success |   assert_success | ||||||
|   assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" |   assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" | ||||||
|  |  | ||||||
|   run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" log -1 |   assert_equal $(_get_tag_hash 0.1.1+1.20.2) $(_get_current_hash) | ||||||
|   assert_output --partial "453db7121c0a56a7a8f15378f18fe3bf21ccfdef" |  | ||||||
| } | } | ||||||
|  |  | ||||||
| @test "does not overwrite existing env files" { | @test "does not overwrite existing env files" { | ||||||
| @ -117,11 +110,13 @@ teardown(){ | |||||||
| } | } | ||||||
|  |  | ||||||
| @test "ensure recipe up to date if no --offline" { | @test "ensure recipe up to date if no --offline" { | ||||||
|  |   _reset_recipe | ||||||
|  |   wantHash=$(_get_n_hash 3) | ||||||
|  |  | ||||||
|   run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3 |   run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3 | ||||||
|   assert_success |   assert_success | ||||||
|  |  | ||||||
|   run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status |   assert_equal $(_get_current_hash) "$wantHash" | ||||||
|   assert_output --regexp 'behind .* 3 commits' |  | ||||||
|  |  | ||||||
|   run $ABRA app new "$TEST_RECIPE" \ |   run $ABRA app new "$TEST_RECIPE" \ | ||||||
|     --no-input \ |     --no-input \ | ||||||
| @ -130,18 +125,19 @@ teardown(){ | |||||||
|   assert_success |   assert_success | ||||||
|   assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" |   assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" | ||||||
|  |  | ||||||
|   run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status |   assert_equal $(_get_head_hash) $(_get_current_hash) | ||||||
|   assert_output --partial "up to date" |  | ||||||
|  |  | ||||||
|   _reset_recipe |   _reset_recipe | ||||||
| } | } | ||||||
|  |  | ||||||
| @test "ensure recipe not up to date if --offline" { | @test "ensure recipe not up to date if --offline" { | ||||||
|  |   _reset_recipe | ||||||
|  |   wantHash=$(_get_n_hash 3) | ||||||
|  |  | ||||||
|   run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3 |   run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3 | ||||||
|   assert_success |   assert_success | ||||||
|  |  | ||||||
|   run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status |   assert_equal $(_get_current_hash) "$wantHash" | ||||||
|   assert_output --regexp 'behind .* 3 commits' |  | ||||||
|  |  | ||||||
|   # NOTE(d1): need to use --chaos to force same commit |   # NOTE(d1): need to use --chaos to force same commit | ||||||
|   run $ABRA app new "$TEST_RECIPE" \ |   run $ABRA app new "$TEST_RECIPE" \ | ||||||
| @ -153,12 +149,12 @@ teardown(){ | |||||||
|   assert_success |   assert_success | ||||||
|   assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" |   assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" | ||||||
|  |  | ||||||
|   run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status |   assert_equal $(_get_current_hash) "$wantHash" | ||||||
|   assert_output --regexp 'behind .* 3 commits' |  | ||||||
|  |  | ||||||
|   _reset_recipe |   _reset_recipe | ||||||
| } | } | ||||||
|  |  | ||||||
|  | # bats test_tags=slow | ||||||
| @test "generate secrets" { | @test "generate secrets" { | ||||||
|   run $ABRA app new "$TEST_RECIPE" \ |   run $ABRA app new "$TEST_RECIPE" \ | ||||||
|     --no-input \ |     --no-input \ | ||||||
|  | |||||||
| @ -104,9 +104,6 @@ teardown(){ | |||||||
|  |  | ||||||
|   _undeploy_app |   _undeploy_app | ||||||
|  |  | ||||||
|   # TODO: should wait as long as volume is no longer in use |  | ||||||
|   sleep 10 |  | ||||||
|  |  | ||||||
|   run $ABRA app volume rm "$TEST_APP_DOMAIN" --no-input |   run $ABRA app volume rm "$TEST_APP_DOMAIN" --no-input | ||||||
|   assert_success |   assert_success | ||||||
|  |  | ||||||
|  | |||||||
| @ -19,6 +19,13 @@ teardown_file(){ | |||||||
|   _reset_recipe |   _reset_recipe | ||||||
| } | } | ||||||
|  |  | ||||||
|  | teardown() { | ||||||
|  |   run $ABRA app secret rm "$TEST_APP_DOMAIN" --all | ||||||
|  |   _reset_app | ||||||
|  |   _reset_recipe | ||||||
|  |   _checkout_recipe | ||||||
|  | } | ||||||
|  |  | ||||||
| setup(){ | setup(){ | ||||||
|   load "$PWD/tests/integration/helpers/common" |   load "$PWD/tests/integration/helpers/common" | ||||||
|   _common_setup |   _common_setup | ||||||
| @ -77,9 +84,6 @@ setup(){ | |||||||
|   assert_output --partial 'test_pass_one' |   assert_output --partial 'test_pass_one' | ||||||
|   assert_output --partial 'test_pass_two' |   assert_output --partial 'test_pass_two' | ||||||
|   refute_output --partial 'extra_pass' |   refute_output --partial 'extra_pass' | ||||||
|  |  | ||||||
|   run $ABRA app secret rm "$TEST_APP_DOMAIN" --all |  | ||||||
|   assert_success |  | ||||||
| } | } | ||||||
|  |  | ||||||
| @test "generate: broken if missing version" { | @test "generate: broken if missing version" { | ||||||
| @ -91,7 +95,6 @@ setup(){ | |||||||
|   assert_failure |   assert_failure | ||||||
|   assert_output --partial 'missing version' |   assert_output --partial 'missing version' | ||||||
|  |  | ||||||
|   _reset_app |  | ||||||
| } | } | ||||||
|  |  | ||||||
| @test "generate: use version from app env" { | @test "generate: use version from app env" { | ||||||
| @ -108,11 +111,6 @@ setup(){ | |||||||
|   assert_success |   assert_success | ||||||
|   assert_output --partial 'v2' |   assert_output --partial 'v2' | ||||||
|   refute_output --partial 'v1' |   refute_output --partial 'v1' | ||||||
|  |  | ||||||
|   run $ABRA app secret rm "$TEST_APP_DOMAIN" --all |  | ||||||
|   assert_success |  | ||||||
|  |  | ||||||
|   _reset_app |  | ||||||
| } | } | ||||||
|  |  | ||||||
| @test "generate: generate extra secret based on COMPOSE_FILE" { | @test "generate: generate extra secret based on COMPOSE_FILE" { | ||||||
| @ -131,11 +129,6 @@ setup(){ | |||||||
|   run docker -c "$TEST_SERVER" secret ls |   run docker -c "$TEST_SERVER" secret ls | ||||||
|   assert_success |   assert_success | ||||||
|   assert_output --partial "$TEST_APP_DOMAIN_extra_pass_v1" |   assert_output --partial "$TEST_APP_DOMAIN_extra_pass_v1" | ||||||
|  |  | ||||||
|   run $ABRA app secret rm "$TEST_APP_DOMAIN" --all |  | ||||||
|   assert_success |  | ||||||
|  |  | ||||||
|   _reset_app |  | ||||||
| } | } | ||||||
|  |  | ||||||
| @test "generate: bail if unstaged changes and no --chaos" { | @test "generate: bail if unstaged changes and no --chaos" { | ||||||
| @ -162,8 +155,6 @@ setup(){ | |||||||
|  |  | ||||||
|   run $ABRA app secret rm "$TEST_APP_DOMAIN" --all --chaos |   run $ABRA app secret rm "$TEST_APP_DOMAIN" --all --chaos | ||||||
|   assert_success |   assert_success | ||||||
|  |  | ||||||
|   _checkout_recipe |  | ||||||
| } | } | ||||||
|  |  | ||||||
| @test "generate: ensure secret name uses trimmed stack name" { | @test "generate: ensure secret name uses trimmed stack name" { | ||||||
| @ -228,9 +219,22 @@ setup(){ | |||||||
|   run $ABRA app secret ls "$TEST_APP_DOMAIN" |   run $ABRA app secret ls "$TEST_APP_DOMAIN" | ||||||
|   assert_success |   assert_success | ||||||
|   assert_output --partial 'true' |   assert_output --partial 'true' | ||||||
|  | } | ||||||
|  |  | ||||||
|   run $ABRA app secret rm "$TEST_APP_DOMAIN" test_pass_one | @test "insert: create secret from file" { | ||||||
|  |   run $ABRA app secret ls "$TEST_APP_DOMAIN" | ||||||
|   assert_success |   assert_success | ||||||
|  |   assert_output --partial 'false' | ||||||
|  |  | ||||||
|  |   run bash -c "echo bar >> $ABRA_DIR/recipes/$TEST_RECIPE/foo" | ||||||
|  |  | ||||||
|  |   run $ABRA app secret insert --file "$TEST_APP_DOMAIN" test_pass_one v1 "$ABRA_DIR/recipes/$TEST_RECIPE/foo" | ||||||
|  |   assert_success | ||||||
|  |   assert_output --partial 'successfully stored on server' | ||||||
|  |  | ||||||
|  |   run $ABRA app secret ls "$TEST_APP_DOMAIN" | ||||||
|  |   assert_success | ||||||
|  |   assert_output --partial 'true' | ||||||
| } | } | ||||||
|  |  | ||||||
| @test "rm: validate arguments" { | @test "rm: validate arguments" { | ||||||
| @ -314,9 +318,6 @@ setup(){ | |||||||
|   run $ABRA app secret ls "$TEST_APP_DOMAIN" |   run $ABRA app secret ls "$TEST_APP_DOMAIN" | ||||||
|   assert_success |   assert_success | ||||||
|   assert_output --partial 'true' |   assert_output --partial 'true' | ||||||
|  |  | ||||||
|   run $ABRA app secret rm "$TEST_APP_DOMAIN" --all |  | ||||||
|   assert_success |  | ||||||
| } | } | ||||||
|  |  | ||||||
| @test "ls: show secrets as machine readable" { | @test "ls: show secrets as machine readable" { | ||||||
| @ -330,9 +331,6 @@ setup(){ | |||||||
|   run $ABRA app secret ls "$TEST_APP_DOMAIN" --machine |   run $ABRA app secret ls "$TEST_APP_DOMAIN" --machine | ||||||
|   assert_success |   assert_success | ||||||
|   assert_output --partial '"created-on-server":"true"' |   assert_output --partial '"created-on-server":"true"' | ||||||
|  |  | ||||||
|   run $ABRA app secret rm "$TEST_APP_DOMAIN" --all |  | ||||||
|   assert_success |  | ||||||
| } | } | ||||||
|  |  | ||||||
| @test "ls: bail if unstaged changes and no --chaos" { | @test "ls: bail if unstaged changes and no --chaos" { | ||||||
|  | |||||||
| @ -78,9 +78,6 @@ teardown(){ | |||||||
|  |  | ||||||
|   _undeploy_app |   _undeploy_app | ||||||
|  |  | ||||||
|   # NOTE(d1): to let the stack come down before nuking volumes |  | ||||||
|   sleep 10 |  | ||||||
|  |  | ||||||
|   run $ABRA app volume rm "$TEST_APP_DOMAIN" --force |   run $ABRA app volume rm "$TEST_APP_DOMAIN" --force | ||||||
|   assert_success |   assert_success | ||||||
|   assert_output --partial 'volumes removed successfully' |   assert_output --partial 'volumes removed successfully' | ||||||
| @ -92,9 +89,6 @@ teardown(){ | |||||||
|  |  | ||||||
|   _undeploy_app |   _undeploy_app | ||||||
|  |  | ||||||
|   # NOTE(d1): to let the stack come down before nuking volumes |  | ||||||
|   sleep 10 |  | ||||||
|  |  | ||||||
|   run $ABRA app volume rm "$TEST_APP_DOMAIN" --force |   run $ABRA app volume rm "$TEST_APP_DOMAIN" --force | ||||||
|   assert_success |   assert_success | ||||||
|   assert_output --partial 'volumes removed successfully' |   assert_output --partial 'volumes removed successfully' | ||||||
|  | |||||||
| @ -32,6 +32,39 @@ _reset_tags() { | |||||||
| _set_git_author() { | _set_git_author() { | ||||||
|   run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" config --local user.email test@example.com |   run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" config --local user.email test@example.com | ||||||
|   assert_success |   assert_success | ||||||
|  |  | ||||||
|   run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" config --local user.name test |   run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" config --local user.name test | ||||||
|   assert_success |   assert_success | ||||||
| } | } | ||||||
|  |  | ||||||
|  | _git_commit() { | ||||||
|  |   run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" add . | ||||||
|  |   assert_success | ||||||
|  |  | ||||||
|  |   run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" commit -m "test: helpers/git.bash: _git_commit" | ||||||
|  |   assert_success | ||||||
|  | } | ||||||
|  |  | ||||||
|  | _get_tag_hash() { | ||||||
|  |   tagHash=$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-list -n 1 "$1") | ||||||
|  |   assert_success | ||||||
|  |   echo "$tagHash" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | _get_head_hash() { | ||||||
|  |   headHash=$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" show -s --format="%H" HEAD) | ||||||
|  |   assert_success | ||||||
|  |   echo "$headHash" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | _get_current_hash() { | ||||||
|  |   currentHash=$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" show -s --format="%H") | ||||||
|  |   assert_success | ||||||
|  |   echo "$currentHash" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | _get_n_hash() { | ||||||
|  |   nHash=$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" show -s --format="%H" "HEAD~$1") | ||||||
|  |   assert_success | ||||||
|  |   echo "$nHash" | ||||||
|  | } | ||||||
|  | |||||||
| @ -40,3 +40,16 @@ teardown(){ | |||||||
|   assert_success |   assert_success | ||||||
|   assert_output --partial 'A new foobar app has been created!' |   assert_output --partial 'A new foobar app has been created!' | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @test "create new recipe with git credentials" { | ||||||
|  |   run $ABRA recipe new foobar --git-name fooUser --git-email foo@example.com | ||||||
|  |   assert_success | ||||||
|  |   assert_output --partial 'Your new foobar recipe has been created' | ||||||
|  |   assert_exists "$ABRA_DIR/recipes/foobar" | ||||||
|  |  | ||||||
|  |   run bash -c 'git -C "$ABRA_DIR/recipes/foobar" log -n 1' | ||||||
|  |   assert_success | ||||||
|  |   assert_output --partial 'fooUser' | ||||||
|  |   assert_output --partial 'foo@example.com' | ||||||
|  | } | ||||||
|  |  | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	