0
0
forked from toolshed/abra

Compare commits

..

36 Commits

Author SHA1 Message Date
89e1046882 . 2024-06-26 16:41:59 +02:00
cbab9b5907 big refactor 2024-06-24 23:20:54 +02:00
b688ddc4b1 feat: introduce remote recipes 2024-06-24 12:12:07 +02:00
6cd331ebd6 secret: allow inserting secret from file and add trim flag 2024-06-22 16:49:59 +00:00
40517171f7 test: separate test for git name/email
See coop-cloud/abra#405
2024-06-22 18:46:28 +02:00
b2485cc122 feat: add git-user and git-email flags to recipe new 2024-06-22 16:38:32 +00:00
9ec99c7712 test: return/echo from git helper functions 2024-06-22 17:04:33 +02:00
aa3910f8df refactor!: drop all SSH opts / config handling
See coop-cloud/organising#601
See coop-cloud/organising#482
2024-06-21 17:16:41 +02:00
43990b6fae test: use more plumbung for git output 2024-06-21 17:10:12 +02:00
91ea2c01a5 fix: fix old app version deploy wrt. compose files
See coop-cloud/organising#617
2024-06-21 16:14:40 +02:00
316fdd3643 fix: abra app new checks out latest version
See coop-cloud/organising#618
2024-06-21 15:51:34 +02:00
e07ae8cccd chore: make format/check 2024-06-19 19:17:22 +02:00
300a4ead01 fix: stop using deprecated APIs 2024-06-19 19:14:52 +02:00
f209b6f564 chore: go get -u -t 2024-06-19 19:14:44 +02:00
791183adfe build: new deps target 2024-06-19 19:14:31 +02:00
e6b35e8524 fix(upgrade): make upgrade --chaos working again 2024-05-22 10:21:31 +02:00
8a0274cac0 fix(recipe): output correct formatted json for recipe version 2024-05-21 16:59:59 +02:00
e609924af0 feat(upgrade): add --releasenotes: show release notes and skip upgrading 2024-05-21 13:49:36 +02:00
70e2943301 fix(upgrade): only show release notes relevant for the upgrade 2024-05-21 13:49:11 +02:00
0590c1824d checkout deployed version 2024-05-14 00:07:58 +02:00
459abecfa5 only show container that should be deployed 2024-05-13 23:26:02 +02:00
183ad8f576 machine readable ps output 2024-05-13 22:08:03 +02:00
03f94da2d8 docs: add fauno [ci skip] 2024-05-01 01:20:25 +02:00
f
766f69b0fd feat: strip debug symbols
to produce smaller binaries
2024-04-30 14:05:03 -03:00
004cd70aed fix: use unique rule number & wording [ci skip] 2024-04-06 23:52:56 +02:00
a4de446f58 test: more verbose failure msg, use contains [ci skip] 2024-04-06 23:48:22 +02:00
d21c35965d fix: add warning for long secret names (!359)
A start of a fix for coop-cloud/organising#463
Putting some code out to start a discussion.  I've added a linting rule for recipes to establish a general principal but I want to put some validation into cli/app/new.go as that's the point we have both the recipe and the domain and can say for sure whether or not the secret names lengths cause a problem but that will have to wait for a bit.  Let me know if I've missed the mark somewhere

Reviewed-on: coop-cloud/abra#359
Reviewed-by: decentral1se <decentral1se@noreply.git.coopcloud.tech>
Co-authored-by: Rich M <r.p.makepeace@gmail.com>
Co-committed-by: Rich M <r.p.makepeace@gmail.com>
2024-04-06 21:41:37 +00:00
63ea58ffaa add relevant command to error message 2024-04-01 18:51:53 +01:00
2ecace3e90 fix: add missing packages on final layer
Closes coop-cloud/organising#598
2024-04-01 13:57:51 +02:00
d5ac3958a4 feat: add retries to app volume remove 2024-03-27 05:38:24 +00:00
3wc
72c20e0039 fix: make installer work again 2024-03-26 21:07:38 -03:00
575f9905f1 Revert "Revert "feat: backup revolution""
This reverts commit 2c515ce70a.
2024-03-12 10:34:40 +01:00
e3a0af5840 build: upgrade goreleaser
Closes coop-cloud/organising#474
2024-03-12 10:11:14 +01:00
9a3a39a185 chore: new 0.9.x series 2024-03-12 10:05:31 +01:00
cea56dddde fix: drop deprecated stanza (goreleaser) 2024-03-12 10:04:50 +01:00
2c515ce70a Revert "feat: backup revolution"
This reverts commit c5687dfbd7.

This is a temporary measure to facilitate a release which won't
completely explode peoples workflows (missing command logic). We
re-instate this commit after the first 0.9.x release.
2024-03-12 10:03:42 +01:00
85 changed files with 1434 additions and 1446 deletions

View File

@ -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

View File

@ -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"

View File

@ -7,6 +7,7 @@
- cassowary - cassowary
- codegod100 - codegod100
- decentral1se - decentral1se
- fauno
- frando - frando
- kawaiipunk - kawaiipunk
- knoflook - knoflook

View File

@ -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

View File

@ -53,3 +53,6 @@ test:
loc: loc:
@find . -name "*.go" | xargs wc -l @find . -name "*.go" | xargs wc -l
deps:
@go get -t -u ./...

View File

@ -1,7 +1,7 @@
package app package app
import ( import (
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var AppCommand = cli.Command{ var AppCommand = cli.Command{
@ -10,28 +10,28 @@ var AppCommand = cli.Command{
Usage: "Manage apps", Usage: "Manage apps",
ArgsUsage: "<domain>", ArgsUsage: "<domain>",
Description: "Functionality for managing the life cycle of your apps", Description: "Functionality for managing the life cycle of your apps",
Subcommands: []*cli.Command{ Subcommands: []cli.Command{
&appBackupCommand, appBackupCommand,
&appCheckCommand, appCheckCommand,
&appCmdCommand, appCmdCommand,
&appConfigCommand, appConfigCommand,
&appCpCommand, appCpCommand,
&appDeployCommand, appDeployCommand,
&appErrorsCommand, appErrorsCommand,
&appListCommand, appListCommand,
&appLogsCommand, appLogsCommand,
&appNewCommand, appNewCommand,
&appPsCommand, appPsCommand,
&appRemoveCommand, appRemoveCommand,
&appRestartCommand, appRestartCommand,
&appRestoreCommand, appRestoreCommand,
&appRollbackCommand, appRollbackCommand,
&appRunCommand, appRunCommand,
&appSecretCommand, appSecretCommand,
&appServicesCommand, appServicesCommand,
&appUndeployCommand, appUndeployCommand,
&appUpgradeCommand, appUpgradeCommand,
&appVersionCommand, appVersionCommand,
&appVolumeCommand, appVolumeCommand,
}, },
} }

View File

@ -8,7 +8,7 @@ import (
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var snapshot string var snapshot string
@ -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)
} }
} }
@ -287,10 +305,10 @@ var appBackupCommand = cli.Command{
Aliases: []string{"b"}, Aliases: []string{"b"},
Usage: "Manage app backups", Usage: "Manage app backups",
ArgsUsage: "<domain>", ArgsUsage: "<domain>",
Subcommands: []*cli.Command{ Subcommands: []cli.Command{
&appBackupListCommand, appBackupListCommand,
&appBackupSnapshotsCommand, appBackupSnapshotsCommand,
&appBackupDownloadCommand, appBackupDownloadCommand,
&appBackupCreateCommand, appBackupCreateCommand,
}, },
} }

View File

@ -6,9 +6,8 @@ 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/v2" "github.com/urfave/cli"
) )
var appCheckCommand = cli.Command{ var appCheckCommand = cli.Command{
@ -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)
} }
} }

View File

@ -15,9 +15,8 @@ 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/v2" "github.com/urfave/cli"
) )
var appCmdCommand = cli.Command{ var appCmdCommand = cli.Command{
@ -45,10 +44,10 @@ Example:
internal.ChaosFlag, internal.ChaosFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Subcommands: []*cli.Command{&appCmdListCommand}, Subcommands: []cli.Command{appCmdListCommand},
BashComplete: func(ctx *cli.Context) { BashComplete: func(ctx *cli.Context) {
args := ctx.Args() args := ctx.Args()
switch args.Len() { switch len(args) {
case 0: case 0:
autocomplete.AppNameComplete(ctx) autocomplete.AppNameComplete(ctx)
case 1: case 1:
@ -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)
} }
} }
@ -84,7 +88,7 @@ Example:
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use --local & --user together")) internal.ShowSubcommandHelpAndError(c, errors.New("cannot use --local & --user together"))
} }
hasCmdArgs, parsedCmdArgs := parseCmdArgs(c.Args().Slice()) hasCmdArgs, parsedCmdArgs := parseCmdArgs(c.Args(), internal.LocalCmd)
abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh") abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
if _, err := os.Stat(abraSh); err != nil { if _, err := os.Stat(abraSh); err != nil {
@ -95,7 +99,7 @@ Example:
} }
if internal.LocalCmd { if internal.LocalCmd {
if !(c.Args().Len() >= 2) { if !(len(c.Args()) >= 2) {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments")) internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments"))
} }
@ -131,7 +135,7 @@ Example:
logrus.Fatal(err) logrus.Fatal(err)
} }
} else { } else {
if !(c.Args().Len() >= 3) { if !(len(c.Args()) >= 3) {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments")) internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments"))
} }
@ -180,16 +184,23 @@ Example:
}, },
} }
// Parse the command arguments from the cli args. func parseCmdArgs(args []string, isLocal bool) (bool, string) {
// Arguments should look like this: var (
// parsedCmdArgs string
// DOMAIN COMMAND -- ARGUMENT1 ARGUMENT2 ... hasCmdArgs bool
func parseCmdArgs(args []string) (bool, string) { )
if len(args) < 4 {
return false, "" if isLocal {
if len(args) > 2 {
return true, fmt.Sprintf("%s ", strings.Join(args[2:], " "))
}
} else {
if len(args) > 3 {
return true, fmt.Sprintf("%s ", strings.Join(args[3:], " "))
}
} }
return true, fmt.Sprintf("%s ", strings.Join(args[3:], " ")) return hasCmdArgs, parsedCmdArgs
} }
func cmdNameComplete(appName string) { func cmdNameComplete(appName string) {
@ -221,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)
} }
} }

View File

@ -20,7 +20,7 @@ func TestParseCmdArgs(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
ok, parsed := parseCmdArgs(test.input) ok, parsed := parseCmdArgs(test.input, false)
if ok != test.shouldParse { if ok != test.shouldParse {
t.Fatalf("[%s] should not parse", strings.Join(test.input, " ")) t.Fatalf("[%s] should not parse", strings.Join(test.input, " "))
} }

View File

@ -10,7 +10,7 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appConfigCommand = cli.Command{ var appConfigCommand = cli.Command{

View File

@ -22,7 +22,7 @@ import (
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appCpCommand = cli.Command{ var appCpCommand = cli.Command{

View File

@ -17,7 +17,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/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appDeployCommand = cli.Command{ var appDeployCommand = cli.Command{
@ -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)
} }

View File

@ -13,11 +13,11 @@ 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"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appErrorsCommand = cli.Command{ var appErrorsCommand = cli.Command{
@ -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
} }

View File

@ -13,21 +13,19 @@ import (
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var status bool var status bool
var statusFlag = &cli.BoolFlag{ var statusFlag = &cli.BoolFlag{
Name: "status", Name: "status, S",
Aliases: []string{"S"},
Usage: "Show app deployment status", Usage: "Show app deployment status",
Destination: &status, Destination: &status,
} }
var recipeFilter string var recipeFilter string
var recipeFlag = &cli.StringFlag{ var recipeFlag = &cli.StringFlag{
Name: "recipe", Name: "recipe, r",
Aliases: []string{"r"},
Value: "", Value: "",
Usage: "Show apps of a specific recipe", Usage: "Show apps of a specific recipe",
Destination: &recipeFilter, Destination: &recipeFilter,
@ -35,8 +33,7 @@ var recipeFlag = &cli.StringFlag{
var listAppServer string var listAppServer string
var listAppServerFlag = &cli.StringFlag{ var listAppServerFlag = &cli.StringFlag{
Name: "server", Name: "server, s",
Aliases: []string{"s"},
Value: "", Value: "",
Usage: "Show apps of a specific server", Usage: "Show apps of a specific server",
Destination: &listAppServer, Destination: &listAppServer,

View File

@ -15,11 +15,12 @@ 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"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appLogsCommand = cli.Command{ var appLogsCommand = cli.Command{
@ -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,

View File

@ -10,13 +10,11 @@ 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"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appNewDescription = ` var appNewDescription = `
@ -58,7 +56,7 @@ var appNewCommand = cli.Command{
ArgsUsage: "[<recipe>] [<version>]", ArgsUsage: "[<recipe>] [<version>]",
BashComplete: func(ctx *cli.Context) { BashComplete: func(ctx *cli.Context) {
args := ctx.Args() args := ctx.Args()
switch args.Len() { switch len(args) {
case 0: case 0:
autocomplete.RecipeNameComplete(ctx) autocomplete.RecipeNameComplete(ctx)
case 1: case 1:
@ -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

View File

@ -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,14 +11,16 @@ 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/v2" "github.com/urfave/cli"
) )
var appPsCommand = cli.Command{ var appPsCommand = cli.Command{
@ -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()
} }
}

View File

@ -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,9 +12,8 @@ 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/v2" "github.com/urfave/cli"
) )
var appRemoveCommand = cli.Command{ var appRemoveCommand = cli.Command{
@ -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)
}

View File

@ -11,7 +11,7 @@ import (
upstream "coopcloud.tech/abra/pkg/upstream/service" upstream "coopcloud.tech/abra/pkg/upstream/service"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appRestartCommand = cli.Command{ var appRestartCommand = cli.Command{

View File

@ -8,7 +8,7 @@ import (
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var targetPath string var targetPath string
@ -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)
} }
} }

View File

@ -15,7 +15,7 @@ import (
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appRollbackCommand = cli.Command{ var appRollbackCommand = cli.Command{
@ -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)

View File

@ -14,7 +14,7 @@ import (
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var user string var user string
@ -45,11 +45,11 @@ var appRunCommand = cli.Command{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if c.Args().Len() < 2 { if len(c.Args()) < 2 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <service> provided?")) internal.ShowSubcommandHelpAndError(c, errors.New("no <service> provided?"))
} }
if c.Args().Len() < 3 { if len(c.Args()) < 3 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <args> provided?")) internal.ShowSubcommandHelpAndError(c, errors.New("no <args> provided?"))
} }
@ -68,7 +68,7 @@ var appRunCommand = cli.Command{
logrus.Fatal(err) logrus.Fatal(err)
} }
cmd := c.Args().Slice()[2:] cmd := c.Args()[2:]
execCreateOpts := types.ExecConfig{ execCreateOpts := types.ExecConfig{
AttachStderr: true, AttachStderr: true,
AttachStdin: true, AttachStdin: true,

View File

@ -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"
@ -17,7 +18,7 @@ import (
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var ( var (
@ -56,27 +57,32 @@ 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)
} }
} }
if c.Args().Len() == 1 && !allSecrets { if len(c.Args()) == 1 && !allSecrets {
err := errors.New("missing arguments <secret>/<version> or '--all'") err := errors.New("missing arguments <secret>/<version> or '--all'")
internal.ShowSubcommandHelpAndError(c, err) internal.ShowSubcommandHelpAndError(c, 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>",
@ -175,7 +183,7 @@ Example:
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if c.Args().Len() != 4 { if len(c.Args()) != 4 {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments?")) internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments?"))
} }
@ -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)
} }
} }
@ -419,10 +449,10 @@ var appSecretCommand = cli.Command{
Aliases: []string{"s"}, Aliases: []string{"s"},
Usage: "Manage app secrets", Usage: "Manage app secrets",
ArgsUsage: "<domain>", ArgsUsage: "<domain>",
Subcommands: []*cli.Command{ Subcommands: []cli.Command{
&appSecretGenerateCommand, appSecretGenerateCommand,
&appSecretInsertCommand, appSecretInsertCommand,
&appSecretRmCommand, appSecretRmCommand,
&appSecretLsCommand, appSecretLsCommand,
}, },
} }

View File

@ -11,9 +11,9 @@ 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/v2" "github.com/urfave/cli"
) )
var appServicesCommand = cli.Command{ var appServicesCommand = cli.Command{
@ -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)
} }

View File

@ -14,7 +14,7 @@ import (
"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"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var prune bool var prune bool

View File

@ -15,7 +15,7 @@ import (
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appUpgradeCommand = cli.Command{ var appUpgradeCommand = cli.Command{
@ -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)
} }

View File

@ -10,10 +10,10 @@ 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/v2" "github.com/urfave/cli"
) )
func sortServiceByName(versions [][]string) func(i, j int) bool { func sortServiceByName(versions [][]string) func(i, j int) bool {
@ -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)
} }

View File

@ -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"
@ -10,7 +11,7 @@ import (
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appVolumeListCommand = cli.Command{ var appVolumeListCommand = cli.Command{
@ -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")
} }
@ -150,8 +151,8 @@ var appVolumeCommand = cli.Command{
Aliases: []string{"vl"}, Aliases: []string{"vl"},
Usage: "Manage app volumes", Usage: "Manage app volumes",
ArgsUsage: "<domain>", ArgsUsage: "<domain>",
Subcommands: []*cli.Command{ Subcommands: []cli.Command{
&appVolumeListCommand, appVolumeListCommand,
&appVolumeRemoveCommand, appVolumeRemoveCommand,
}, },
} }

View File

@ -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"
@ -15,7 +16,7 @@ import (
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"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/v2" "github.com/urfave/cli"
) )
var catalogueGenerateCommand = cli.Command{ var catalogueGenerateCommand = cli.Command{
@ -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 {
@ -217,7 +219,7 @@ var CatalogueCommand = cli.Command{
Aliases: []string{"c"}, Aliases: []string{"c"},
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Description: "This command helps recipe packagers interact with the recipe catalogue", Description: "This command helps recipe packagers interact with the recipe catalogue",
Subcommands: []*cli.Command{ Subcommands: []cli.Command{
&catalogueGenerateCommand, catalogueGenerateCommand,
}, },
} }

View File

@ -18,7 +18,7 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/web" "coopcloud.tech/abra/pkg/web"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// AutoCompleteCommand helps people set up auto-complete in their shells // AutoCompleteCommand helps people set up auto-complete in their shells
@ -153,13 +153,13 @@ func newAbraApp(version, commit string) *cli.App {
|_| |_|
`, `,
Version: fmt.Sprintf("%s-%s", version, commit[:7]), Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Commands: []*cli.Command{ Commands: []cli.Command{
&app.AppCommand, app.AppCommand,
&server.ServerCommand, server.ServerCommand,
&recipe.RecipeCommand, recipe.RecipeCommand,
&catalogue.CatalogueCommand, catalogue.CatalogueCommand,
&UpgradeCommand, UpgradeCommand,
&AutoCompleteCommand, AutoCompleteCommand,
}, },
BashComplete: autocomplete.SubcommandComplete, BashComplete: autocomplete.SubcommandComplete,
} }

View File

@ -5,7 +5,7 @@ import (
logrusStack "github.com/Gurpartap/logrus-stack" logrusStack "github.com/Gurpartap/logrus-stack"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// Secrets stores the variable from SecretsFlag // Secrets stores the variable from SecretsFlag
@ -13,8 +13,7 @@ var Secrets bool
// SecretsFlag turns on/off automatically generating secrets // SecretsFlag turns on/off automatically generating secrets
var SecretsFlag = &cli.BoolFlag{ var SecretsFlag = &cli.BoolFlag{
Name: "secrets", Name: "secrets, S",
Aliases: []string{"S"},
Usage: "Automatically generate secrets", Usage: "Automatically generate secrets",
Destination: &Secrets, Destination: &Secrets,
} }
@ -24,8 +23,7 @@ var Pass bool
// PassFlag turns on/off storing generated secrets in pass // PassFlag turns on/off storing generated secrets in pass
var PassFlag = &cli.BoolFlag{ var PassFlag = &cli.BoolFlag{
Name: "pass", Name: "pass, p",
Aliases: []string{"p"},
Usage: "Store the generated secrets in a local pass store", Usage: "Store the generated secrets in a local pass store",
Destination: &Pass, Destination: &Pass,
} }
@ -35,19 +33,31 @@ var PassRemove bool
// PassRemoveFlag turns on/off removing generated secrets from pass // PassRemoveFlag turns on/off removing generated secrets from pass
var PassRemoveFlag = &cli.BoolFlag{ var PassRemoveFlag = &cli.BoolFlag{
Name: "pass", Name: "pass, p",
Aliases: []string{"p"},
Usage: "Remove generated secrets from a local pass store", Usage: "Remove generated secrets from a local pass store",
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
// ForceFlag turns on/off force functionality. // ForceFlag turns on/off force functionality.
var ForceFlag = &cli.BoolFlag{ var ForceFlag = &cli.BoolFlag{
Name: "force", Name: "force, f",
Aliases: []string{"f"},
Usage: "Perform action without further prompt. Use with care!", Usage: "Perform action without further prompt. Use with care!",
Destination: &Force, Destination: &Force,
} }
@ -57,8 +67,7 @@ var Chaos bool
// ChaosFlag turns on/off chaos functionality. // ChaosFlag turns on/off chaos functionality.
var ChaosFlag = &cli.BoolFlag{ var ChaosFlag = &cli.BoolFlag{
Name: "chaos", Name: "chaos, C",
Aliases: []string{"C"},
Usage: "Proceed with uncommitted recipes changes. Use with care!", Usage: "Proceed with uncommitted recipes changes. Use with care!",
Destination: &Chaos, Destination: &Chaos,
} }
@ -68,29 +77,24 @@ var Tty bool
// TtyFlag turns on/off tty mode. // TtyFlag turns on/off tty mode.
var TtyFlag = &cli.BoolFlag{ var TtyFlag = &cli.BoolFlag{
Name: "tty", Name: "tty, T",
Aliases: []string{"T"},
Usage: "Disables TTY mode to run this command from a script.", Usage: "Disables TTY mode to run this command from a script.",
Destination: &Tty, Destination: &Tty,
} }
var ( var NoInput bool
NoInput bool var NoInputFlag = &cli.BoolFlag{
NoInputFlag = &cli.BoolFlag{ Name: "no-input, n",
Name: "no-input",
Aliases: []string{"n"},
Usage: "Toggle non-interactive mode", Usage: "Toggle non-interactive mode",
Destination: &NoInput, Destination: &NoInput,
} }
)
// Debug stores the variable from DebugFlag. // Debug stores the variable from DebugFlag.
var Debug bool var Debug bool
// DebugFlag turns on/off verbose logging down to the DEBUG level. // DebugFlag turns on/off verbose logging down to the DEBUG level.
var DebugFlag = &cli.BoolFlag{ var DebugFlag = &cli.BoolFlag{
Name: "debug", Name: "debug, d",
Aliases: []string{"d"},
Destination: &Debug, Destination: &Debug,
Usage: "Show DEBUG messages", Usage: "Show DEBUG messages",
} }
@ -100,19 +104,27 @@ var Offline bool
// DebugFlag turns on/off offline mode. // DebugFlag turns on/off offline mode.
var OfflineFlag = &cli.BoolFlag{ var OfflineFlag = &cli.BoolFlag{
Name: "offline", Name: "offline, o",
Aliases: []string{"o"},
Destination: &Offline, Destination: &Offline,
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
// MachineReadableFlag turns on/off machine readable output where supported // MachineReadableFlag turns on/off machine readable output where supported
var MachineReadableFlag = &cli.BoolFlag{ var MachineReadableFlag = &cli.BoolFlag{
Name: "machine", Name: "machine, m",
Aliases: []string{"m"},
Destination: &MachineReadable, Destination: &MachineReadable,
Usage: "Output in a machine-readable format (where supported)", Usage: "Output in a machine-readable format (where supported)",
} }
@ -122,185 +134,149 @@ var RC bool
// RCFlag chooses the latest release candidate for install // RCFlag chooses the latest release candidate for install
var RCFlag = &cli.BoolFlag{ var RCFlag = &cli.BoolFlag{
Name: "rc", Name: "rc, r",
Aliases: []string{"c"},
Destination: &RC, Destination: &RC,
Usage: "Install the latest release candidate", Usage: "Install the latest release candidate",
} }
var ( var Major bool
Major bool var MajorFlag = &cli.BoolFlag{
MajorFlag = &cli.BoolFlag{ Name: "major, x",
Name: "major",
Aliases: []string{"x"},
Usage: "Increase the major part of the version", Usage: "Increase the major part of the version",
Destination: &Major, Destination: &Major,
} }
)
var ( var Minor bool
Minor bool var MinorFlag = &cli.BoolFlag{
MinorFlag = &cli.BoolFlag{ Name: "minor, y",
Name: "minor",
Aliases: []string{"y"},
Usage: "Increase the minor part of the version", Usage: "Increase the minor part of the version",
Destination: &Minor, Destination: &Minor,
} }
)
var ( var Patch bool
Patch bool var PatchFlag = &cli.BoolFlag{
PatchFlag = &cli.BoolFlag{ Name: "patch, z",
Name: "patch",
Aliases: []string{"z"},
Usage: "Increase the patch part of the version", Usage: "Increase the patch part of the version",
Destination: &Patch, Destination: &Patch,
} }
)
var ( var Dry bool
Dry bool var DryFlag = &cli.BoolFlag{
DryFlag = &cli.BoolFlag{ Name: "dry-run, r",
Name: "dry-run",
Aliases: []string{"r"},
Usage: "Only reports changes that would be made", Usage: "Only reports changes that would be made",
Destination: &Dry, Destination: &Dry,
} }
)
var ( var Publish bool
Publish bool var PublishFlag = &cli.BoolFlag{
PublishFlag = &cli.BoolFlag{ Name: "publish, p",
Name: "publish",
Aliases: []string{"p"},
Usage: "Publish changes to git.coopcloud.tech", Usage: "Publish changes to git.coopcloud.tech",
Destination: &Publish, Destination: &Publish,
} }
)
var ( var Domain string
Domain string var DomainFlag = &cli.StringFlag{
DomainFlag = &cli.StringFlag{ Name: "domain, D",
Name: "domain",
Aliases: []string{"D"},
Value: "", Value: "",
Usage: "Choose a domain name", Usage: "Choose a domain name",
Destination: &Domain, Destination: &Domain,
} }
)
var ( var NewAppServer string
NewAppServer string var NewAppServerFlag = &cli.StringFlag{
NewAppServerFlag = &cli.StringFlag{ Name: "server, s",
Name: "server",
Aliases: []string{"s"},
Value: "", Value: "",
Usage: "Show apps of a specific server", Usage: "Show apps of a specific server",
Destination: &NewAppServer, Destination: &NewAppServer,
} }
)
var ( var NoDomainChecks bool
NoDomainChecks bool var NoDomainChecksFlag = &cli.BoolFlag{
NoDomainChecksFlag = &cli.BoolFlag{ Name: "no-domain-checks, D",
Name: "no-domain-checks",
Aliases: []string{"D"},
Usage: "Disable app domain sanity checks", Usage: "Disable app domain sanity checks",
Destination: &NoDomainChecks, Destination: &NoDomainChecks,
} }
)
var ( var StdErrOnly bool
StdErrOnly bool var StdErrOnlyFlag = &cli.BoolFlag{
StdErrOnlyFlag = &cli.BoolFlag{ Name: "stderr, s",
Name: "stderr",
Aliases: []string{"s"},
Usage: "Only tail stderr", Usage: "Only tail stderr",
Destination: &StdErrOnly, Destination: &StdErrOnly,
} }
)
var ( var SinceLogs string
SinceLogs string var SinceLogsFlag = &cli.StringFlag{
SinceLogsFlag = &cli.StringFlag{ Name: "since, S",
Name: "since",
Aliases: []string{"S"},
Value: "", Value: "",
Usage: "tail logs since YYYY-MM-DDTHH:MM:SSZ", Usage: "tail logs since YYYY-MM-DDTHH:MM:SSZ",
Destination: &SinceLogs, Destination: &SinceLogs,
} }
)
var ( var DontWaitConverge bool
DontWaitConverge bool var DontWaitConvergeFlag = &cli.BoolFlag{
DontWaitConvergeFlag = &cli.BoolFlag{ Name: "no-converge-checks, c",
Name: "no-converge-checks",
Aliases: []string{"c"},
Usage: "Don't wait for converge logic checks", Usage: "Don't wait for converge logic checks",
Destination: &DontWaitConverge, Destination: &DontWaitConverge,
} }
)
var ( var Watch bool
Watch bool var WatchFlag = &cli.BoolFlag{
WatchFlag = &cli.BoolFlag{ Name: "watch, w",
Name: "watch",
Aliases: []string{"w"},
Usage: "Watch status by polling repeatedly", Usage: "Watch status by polling repeatedly",
Destination: &Watch, Destination: &Watch,
} }
)
var ( var OnlyErrors bool
OnlyErrors bool var OnlyErrorFlag = &cli.BoolFlag{
OnlyErrorFlag = &cli.BoolFlag{ Name: "errors, e",
Name: "errors",
Aliases: []string{"e"},
Usage: "Only show errors", Usage: "Only show errors",
Destination: &OnlyErrors, Destination: &OnlyErrors,
} }
)
var ( var SkipUpdates bool
SkipUpdates bool var SkipUpdatesFlag = &cli.BoolFlag{
SkipUpdatesFlag = &cli.BoolFlag{ Name: "skip-updates, s",
Name: "skip-updates",
Aliases: []string{"s"},
Usage: "Skip updating recipe repositories", Usage: "Skip updating recipe repositories",
Destination: &SkipUpdates, Destination: &SkipUpdates,
} }
)
var ( var AllTags bool
AllTags bool var AllTagsFlag = &cli.BoolFlag{
AllTagsFlag = &cli.BoolFlag{ Name: "all-tags, a",
Name: "all-tags",
Aliases: []string{"a"},
Usage: "List all tags, not just upgrades", Usage: "List all tags, not just upgrades",
Destination: &AllTags, Destination: &AllTags,
} }
)
var ( var LocalCmd bool
LocalCmd bool var LocalCmdFlag = &cli.BoolFlag{
LocalCmdFlag = &cli.BoolFlag{ Name: "local, l",
Name: "local",
Aliases: []string{"l"},
Usage: "Run command locally", Usage: "Run command locally",
Destination: &LocalCmd, Destination: &LocalCmd,
} }
)
var ( var RemoteUser string
RemoteUser string var RemoteUserFlag = &cli.StringFlag{
RemoteUserFlag = &cli.StringFlag{ Name: "user, u",
Name: "user",
Aliases: []string{"u"},
Value: "", Value: "",
Usage: "User to run command within a service context", Usage: "User to run command within a service context",
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 {

View File

@ -4,7 +4,7 @@ import (
"os" "os"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// ShowSubcommandHelpAndError exits the program on error, logs the error to the // ShowSubcommandHelpAndError exits the program on error, logs the error to the

View File

@ -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"
) )

View File

@ -9,7 +9,7 @@ import (
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// ValidateRecipe ensures the recipe arg is valid. // ValidateRecipe ensures the recipe arg is valid.
@ -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.
@ -120,9 +123,9 @@ func ValidateDomain(c *cli.Context) string {
// ValidateSubCmdFlags ensures flag order conforms to correct order // ValidateSubCmdFlags ensures flag order conforms to correct order
func ValidateSubCmdFlags(c *cli.Context) bool { func ValidateSubCmdFlags(c *cli.Context) bool {
for argIdx, arg := range c.Args().Slice() { for argIdx, arg := range c.Args() {
if !strings.HasPrefix(arg, "--") { if !strings.HasPrefix(arg, "--") {
for _, flag := range c.Args().Slice()[argIdx:] { for _, flag := range c.Args()[argIdx:] {
if strings.HasPrefix(flag, "--") { if strings.HasPrefix(flag, "--") {
return false return false
} }

View File

@ -8,7 +8,7 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var recipeDiffCommand = cli.Command{ var recipeDiffCommand = cli.Command{

View File

@ -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/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var recipeFetchCommand = cli.Command{ var recipeFetchCommand = cli.Command{
@ -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)

View File

@ -7,9 +7,8 @@ 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/v2" "github.com/urfave/cli"
) )
var recipeLintCommand = cli.Command{ var recipeLintCommand = cli.Command{
@ -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)
} }

View File

@ -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"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var pattern string var pattern string

View File

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path" "path"
"text/template" "text/template"
@ -13,7 +12,7 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/git"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// recipeMetadata is the recipe metadata for the README.md // recipeMetadata is the recipe metadata for the README.md
@ -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)
} }

View File

@ -1,7 +1,7 @@
package recipe package recipe
import ( import (
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// RecipeCommand defines all recipe related sub-commands. // RecipeCommand defines all recipe related sub-commands.
@ -21,16 +21,16 @@ sure the recipe is in good working order and the config upgraded in a timely
manner. Abra supports convenient automation for recipe maintainenace, see the manner. Abra supports convenient automation for recipe maintainenace, see the
"abra recipe upgrade", "abra recipe sync" and "abra recipe release" commands. "abra recipe upgrade", "abra recipe sync" and "abra recipe release" commands.
`, `,
Subcommands: []*cli.Command{ Subcommands: []cli.Command{
&recipeFetchCommand, recipeFetchCommand,
&recipeLintCommand, recipeLintCommand,
&recipeListCommand, recipeListCommand,
&recipeNewCommand, recipeNewCommand,
&recipeReleaseCommand, recipeReleaseCommand,
&recipeSyncCommand, recipeSyncCommand,
&recipeUpgradeCommand, recipeUpgradeCommand,
&recipeVersionCommand, recipeVersionCommand,
&recipeResetCommand, recipeResetCommand,
&recipeDiffCommand, recipeDiffCommand,
}, },
} }

View File

@ -17,10 +17,10 @@ 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/v2" "github.com/urfave/cli"
) )
var recipeReleaseCommand = cli.Command{ var recipeReleaseCommand = cli.Command{
@ -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
} }

View File

@ -8,7 +8,7 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"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/v2" "github.com/urfave/cli"
) )
var recipeResetCommand = cli.Command{ var recipeResetCommand = cli.Command{

View File

@ -14,7 +14,7 @@ import (
"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"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var recipeSyncCommand = cli.Command{ var recipeSyncCommand = cli.Command{
@ -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)
} }
} }

View File

@ -18,9 +18,9 @@ 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/v2" "github.com/urfave/cli"
) )
type imgPin struct { type imgPin struct {
@ -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)
} }

View File

@ -10,7 +10,7 @@ import (
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
func sortServiceByName(versions [][]string) func(i, j int) bool { func sortServiceByName(versions [][]string) func(i, j int) bool {
@ -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
}, },

View File

@ -13,13 +13,12 @@ import (
"coopcloud.tech/abra/pkg/server" "coopcloud.tech/abra/pkg/server"
sshPkg "coopcloud.tech/abra/pkg/ssh" sshPkg "coopcloud.tech/abra/pkg/ssh"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var local bool var local bool
var localFlag = &cli.BoolFlag{ var localFlag = &cli.BoolFlag{
Name: "local", Name: "local, l",
Aliases: []string{"l"},
Usage: "Use local server", Usage: "Use local server",
Destination: &local, Destination: &local,
} }
@ -54,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 {
@ -68,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
} }
@ -123,7 +122,7 @@ developer machine.
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
ArgsUsage: "<domain>", ArgsUsage: "<domain>",
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if c.Args().Len() > 0 && local || !internal.ValidateSubCmdFlags(c) { if len(c.Args()) > 0 && local || !internal.ValidateSubCmdFlags(c) {
err := errors.New("cannot use <domain> and --local together") err := errors.New("cannot use <domain> and --local together")
internal.ShowSubcommandHelpAndError(c, err) internal.ShowSubcommandHelpAndError(c, err)
} }
@ -159,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)
} }

View File

@ -9,7 +9,7 @@ import (
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"github.com/docker/cli/cli/connhelper/ssh" "github.com/docker/cli/cli/connhelper/ssh"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var problemsFilter bool var problemsFilter bool

View File

@ -9,7 +9,7 @@ import (
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var allFilter bool var allFilter bool

View File

@ -9,7 +9,7 @@ import (
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var serverRemoveCommand = cli.Command{ var serverRemoveCommand = cli.Command{

View File

@ -1,7 +1,7 @@
package server package server
import ( import (
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// ServerCommand defines the `abra server` command and its subcommands // ServerCommand defines the `abra server` command and its subcommands
@ -9,10 +9,10 @@ var ServerCommand = cli.Command{
Name: "server", Name: "server",
Aliases: []string{"s"}, Aliases: []string{"s"},
Usage: "Manage servers", Usage: "Manage servers",
Subcommands: []*cli.Command{ Subcommands: []cli.Command{
&serverAddCommand, serverAddCommand,
&serverListCommand, serverListCommand,
&serverRemoveCommand, serverRemoveCommand,
&serverPruneCommand, serverPruneCommand,
}, },
} }

View File

@ -21,28 +21,24 @@ import (
dockerclient "github.com/docker/docker/client" dockerclient "github.com/docker/docker/client"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
const SERVER = "localhost" const SERVER = "localhost"
var ( var majorUpdate bool
majorUpdate bool var majorFlag = &cli.BoolFlag{
majorFlag = &cli.BoolFlag{ Name: "major, m",
Name: "major",
Aliases: []string{"m"},
Usage: "Also check for major updates", Usage: "Also check for major updates",
Destination: &majorUpdate, Destination: &majorUpdate,
} }
updateAll bool var updateAll bool
allFlag = &cli.BoolFlag{ var allFlag = &cli.BoolFlag{
Name: "all", Name: "all, a",
Aliases: []string{"a"},
Usage: "Update all deployed apps", Usage: "Update all deployed apps",
Destination: &updateAll, Destination: &updateAll,
} }
)
// Notify checks for available upgrades // Notify checks for available upgrades
var Notify = cli.Command{ var Notify = cli.Command{
@ -275,8 +271,7 @@ func getDeployedVersion(cl *dockerclient.Client, stackName string, recipeName st
// than the deployed version. It only includes major upgrades if the "--major" // than the deployed version. It only includes major upgrades if the "--major"
// flag is set. // flag is set.
func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName string, func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName string,
deployedVersion string, deployedVersion string) ([]string, error) {
) ([]string, error) {
catl, err := recipe.ReadRecipeCatalogue(internal.Offline) catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil { if err != nil {
return nil, err return nil, err
@ -321,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
} }
@ -434,8 +430,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
// upgrade performs all necessary steps to upgrade an app. // upgrade performs all necessary steps to upgrade an app.
func upgrade(cl *dockerclient.Client, stackName, recipeName, func upgrade(cl *dockerclient.Client, stackName, recipeName,
upgradeVersion string, upgradeVersion string) error {
) error {
env, err := getEnv(cl, stackName) env, err := getEnv(cl, stackName)
if err != nil { if err != nil {
return err return err
@ -448,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
} }
@ -480,9 +480,9 @@ func newAbraApp(version, commit string) *cli.App {
|_| |_|
`, `,
Version: fmt.Sprintf("%s-%s", version, commit[:7]), Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Commands: []*cli.Command{ Commands: []cli.Command{
&Notify, Notify,
&UpgradeApp, UpgradeApp,
}, },
} }

135
go.mod
View File

@ -3,120 +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.3.2 // 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.3 // 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/urfave/cli/v2 v2.27.1 // 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
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect
golang.org/x/crypto v0.14.0 // indirect go.opentelemetry.io/otel v1.27.0 // indirect
golang.org/x/mod v0.12.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 // indirect
golang.org/x/net v0.17.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect
golang.org/x/sync v0.3.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
golang.org/x/term v0.14.0 // indirect go.opentelemetry.io/otel/metric v1.27.0 // indirect
golang.org/x/text v0.13.0 // indirect go.opentelemetry.io/otel/sdk v1.27.0 // indirect
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect go.opentelemetry.io/otel/sdk/metric v1.27.0 // indirect
golang.org/x/tools v0.13.0 // indirect go.opentelemetry.io/otel/trace v1.27.0 // indirect
google.golang.org/protobuf v1.30.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
) )

637
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// AppNameComplete copletes app names. // AppNameComplete copletes app names.

View File

@ -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
} }

View File

@ -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.

View File

@ -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)
}

View File

@ -1,4 +1,4 @@
package app package client
import ( import (
"fmt" "fmt"

View File

@ -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"
) )

View File

@ -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

View File

@ -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)
} }

View File

@ -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

View File

@ -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)
} }

View File

@ -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

View File

@ -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)
} }

View File

@ -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
} }

View File

@ -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)

View File

@ -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
} }

View File

@ -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)
} }

View File

@ -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

View File

@ -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")
}

View File

@ -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
} }
}

View File

@ -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 {

View File

@ -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,
} }

View File

@ -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":

View File

@ -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)

View File

@ -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
}

View File

@ -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 \

View File

@ -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

View File

@ -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" {

View File

@ -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'

View File

@ -1,18 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
_new_app() { _new_app() {
run $ABRA app new \ run $ABRA app new "$TEST_RECIPE" \
--no-input \ --no-input \
--server "$TEST_SERVER" \ --server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN" \ --domain "$TEST_APP_DOMAIN" \
--secrets \ --secrets
"$TEST_RECIPE"
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"
} }
_deploy_app() { _deploy_app() {
run $ABRA app deploy --no-input "$TEST_APP_DOMAIN" run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
assert_success assert_success
run $ABRA app ls --server "$TEST_SERVER" --status run $ABRA app ls --server "$TEST_SERVER" --status
@ -22,7 +21,7 @@ _deploy_app() {
} }
_undeploy_app() { _undeploy_app() {
run $ABRA app undeploy --no-input "$TEST_APP_DOMAIN" run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
assert_success assert_success
run $ABRA app ls --server "$TEST_SERVER" --status run $ABRA app ls --server "$TEST_SERVER" --status
@ -35,10 +34,10 @@ _rm_app() {
# NOTE(d1): not asserting outcomes on teardown here since some might fail # NOTE(d1): not asserting outcomes on teardown here since some might fail
# depending on what the test created. all commands run through anyway # depending on what the test created. all commands run through anyway
if [[ -f "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" ]]; then if [[ -f "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" ]]; then
run $ABRA app undeploy --no-input "$TEST_APP_DOMAIN" run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
run $ABRA app secret remove --all --no-input "$TEST_APP_DOMAIN" run $ABRA app secret remove "$TEST_APP_DOMAIN" --all --no-input
run $ABRA app volume remove --no-input "$TEST_APP_DOMAIN" run $ABRA app volume remove "$TEST_APP_DOMAIN" --no-input
run $ABRA app remove --no-input "$TEST_APP_DOMAIN" run $ABRA app remove "$TEST_APP_DOMAIN" --no-input
fi fi
} }
@ -47,11 +46,10 @@ _reset_app(){
assert_success assert_success
assert_not_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" assert_not_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
run $ABRA app new \ run $ABRA app new "$TEST_RECIPE" \
--no-input \ --no-input \
--server "$TEST_SERVER" \ --server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN" \ --domain "$TEST_APP_DOMAIN"
"$TEST_RECIPE"
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"
} }

View File

@ -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"
}

View File

@ -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'
}