0
0
forked from toolshed/abra

Compare commits

..

37 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
40c0fb4bac fix-integration-tests (!403)
In preparation for the new abra release, let's fix all integration tests

After merging, this needs to be cherry-picked into the release-0-9 branch.

  - [x] app_backup.bats (skip this one)
  - [x] app_check.bats (fixed by bd21014fed)
  - [x] app_cmd.bats (partially fixed in 08232b74f6), has known regression coop-cloud/organising#581
  - [x] app_config.bats (no changes needed)
  - [x] app_cp.bats (no changes needed)
  - [x] app_deploy.bats
  - [x] app_errors.bats (no changes needed)
  - [x] app_list.bats (no changes needed)
  - [x] app_logs.bats (no changes needed)
  - [x] app_new.bats (no changes needed)
  - [x] app_ps.bats (no changes needed)
  - [x] app_remove.bats (fixed by [2f29fbeb2e](coop-cloud/abra#403/commits/2f29fbeb2e018656413fa25f8615b7a98cdcb083))
  - [x] app_restart.bats (no changes needed
  - [x] app_restore.bats (fixed by [f2dd5afc38](coop-cloud/abra#403/commits/f2dd5afc38a25a8316899fa0c6d59499445868d7))
  - [x] app_rollback.bats (partially fixed by 6e99b74c24)
  - [x] app_run.bats (no changes needed)
  - [x] app_secret.bats (fixed by bd069d32f6)
  - [x] app_services.bats (no changes needed)
  - [x] app_undeploy.bats (no changes needed)
  - [x] app_upgrade.bats (no changes needed)
  - [x] app_version.bats (partially fixed by ad323ad2bd)
  - [x] app_volume.bats (fixed by [03c3823770](coop-cloud/abra#403/commits/03c38237707ae795b723180eb07a7edc84a8de35))
  - [x] autocomplete.bats (no changes needed)
  - [x] catalogue.bats (no changes needed)
  - [x] dirs.bats (no changes needed)
  - [x] install.bats (failes, but is expected)
  - [x] recipe_diff.bats (no changes needed)
  - [x] recipe_fetch.bats (no changes needed)
  - [x] recipe_lint.bats (fixed by [b6b0808066](coop-cloud/abra#403/commits/b6b0808066a11e4bcd77517ec39600d500bcb944))
  - [x] recipe_list.bats (no changes needed)
  - [x] recipe_new.bats (fixed by [0aac464ded](coop-cloud/abra#403/commits/0aac464ded6b43afb3ec37ade2f64d6191b9838f))
  - [x] recipe_release.bats (no changes needed)
  - [x] recipe_reset.bats (no changes needed)
  - [x] recipe_sync.bats (no changes needed)
  - [x] recipe_upgrade.bats (fixed by [ab86904cf4](coop-cloud/abra#403/commits/ab86904cf45db89c7c189ca1fd9971909bd446dd))
  - [x] recipe_version.bats (fixed by 81897bf4da)
  - [x] server_add.bats
  - [x] server_list.bats
  - [x] server_prune.bats (no changes needed)
  - [x] server_remove.bats
  - [x] upgrade.bats
  - [x] version.bats (no changes needed)

Co-authored-by: decentral1se <cellarspoon@riseup.net>
Reviewed-on: coop-cloud/abra#403
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2024-03-11 13:27:21 +00:00
73 changed files with 1175 additions and 1118 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

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

View File

@ -6,7 +6,6 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -38,22 +37,26 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if err := recipe.EnsureExists(app.Recipe); err != nil { r, err := recipe.Get(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
if err := r.EnsureExists(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Chaos { if !internal.Chaos {
if err := recipePkg.EnsureIsClean(app.Recipe); err != nil { if err := r.EnsureIsClean(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Offline { if !internal.Offline {
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil { if err := r.EnsureUpToDate(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
if err := recipePkg.EnsureLatest(app.Recipe); err != nil { if err := r.EnsureLatest(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }

View File

@ -15,7 +15,6 @@ import (
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -60,22 +59,27 @@ Example:
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if err := recipe.EnsureExists(app.Recipe); err != nil { r, err := recipe.Get(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
if err := r.EnsureExists(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Chaos { if !internal.Chaos {
if err := recipePkg.EnsureIsClean(app.Recipe); err != nil { if err := r.EnsureIsClean(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Offline { if !internal.Offline {
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil { if err := r.EnsureUpToDate(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
if err := recipePkg.EnsureLatest(app.Recipe); err != nil { if err := r.EnsureLatest(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
@ -228,22 +232,27 @@ var appCmdListCommand = cli.Command{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if err := recipe.EnsureExists(app.Recipe); err != nil { r, err := recipe.Get(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
if err := r.EnsureExists(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Chaos { if !internal.Chaos {
if err := recipePkg.EnsureIsClean(app.Recipe); err != nil { if err := r.EnsureIsClean(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Offline { if !internal.Offline {
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil { if err := r.EnsureUpToDate(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
if err := recipePkg.EnsureLatest(app.Recipe); err != nil { if err := r.EnsureLatest(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }

View File

@ -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,7 +13,7 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types" containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -88,16 +88,19 @@ the logs.
} }
func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App) error { func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App) error {
recipe, err := recipe.Get(app.Recipe, internal.Offline) r, err := recipe.Get(app.Recipe)
if err != nil { if err != nil {
logrus.Fatal(err)
}
if err := r.LoadConfig(); err != nil {
return err return err
} }
for _, service := range recipe.Config.Services { for _, service := range r.Config.Services {
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name)) filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name))
containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters}) containers, err := cl.ContainerList(context.Background(), containerTypes.ListOptions{Filters: filters})
if err != nil { if err != nil {
return err return err
} }

View File

@ -15,6 +15,7 @@ import (
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
@ -38,7 +39,12 @@ var appLogsCommand = cli.Command{
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
stackName := app.StackName() stackName := app.StackName()
if err := recipe.EnsureExists(app.Recipe); err != nil { r, err := recipe.Get(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
if err := r.EnsureExists(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -110,7 +116,7 @@ func tailLogs(cl *dockerClient.Client, app config.App, serviceNames []string) er
// collected in parallel. // collected in parallel.
wg.Add(1) wg.Add(1)
go func(serviceID string) { go func(serviceID string) {
logs, err := cl.ServiceLogs(context.Background(), serviceID, types.ContainerLogsOptions{ logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{
ShowStderr: true, ShowStderr: true,
ShowStdout: !internal.StdErrOnly, ShowStdout: !internal.StdErrOnly,
Since: internal.SinceLogs, Since: internal.SinceLogs,

View File

@ -10,8 +10,6 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/jsontable" "coopcloud.tech/abra/pkg/jsontable"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
@ -66,23 +64,43 @@ var appNewCommand = cli.Command{
} }
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c) r := internal.ValidateRecipe(c)
if !internal.Chaos { if !internal.Chaos {
if err := recipePkg.EnsureIsClean(recipe.Name); err != nil { if err := r.EnsureIsClean(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Offline { if !internal.Offline {
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil { if err := r.EnsureUpToDate(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
if c.Args().Get(1) == "" { if c.Args().Get(1) == "" {
if err := recipePkg.EnsureLatest(recipe.Name); err != nil { var version string
recipeVersions, err := r.GetVersions(internal.Offline)
if err != nil {
logrus.Fatal(err) 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)
}
} else {
if err := r.EnsureLatest(); err != nil {
logrus.Fatal(err)
}
}
} else { } else {
if err := recipePkg.EnsureVersion(recipe.Name, c.Args().Get(1)); err != nil { 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,11 +11,13 @@ import (
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/service" "coopcloud.tech/abra/pkg/recipe"
abraService "coopcloud.tech/abra/pkg/service"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/buger/goterm" "github.com/buger/goterm"
dockerFormatter "github.com/docker/cli/cli/command/formatter" dockerFormatter "github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types" containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
@ -27,6 +30,7 @@ var appPsCommand = cli.Command{
ArgsUsage: "<domain>", ArgsUsage: "<domain>",
Description: "Show a more detailed status output of a specific deployed app", Description: "Show a more detailed status output of a specific deployed app",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.MachineReadableFlag,
internal.WatchFlag, internal.WatchFlag,
internal.DebugFlag, internal.DebugFlag,
}, },
@ -35,12 +39,17 @@ var appPsCommand = cli.Command{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
r, err := recipe.Get(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName()) isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -49,6 +58,15 @@ var appPsCommand = cli.Command{
logrus.Fatalf("%s is not deployed?", app.Name) logrus.Fatalf("%s is not deployed?", app.Name)
} }
statuses, err := config.GetAppStatuses([]config.App{app}, true)
if statusMeta, ok := statuses[app.StackName()]; ok {
if _, exists := statusMeta["chaos"]; !exists {
if err := r.EnsureVersion(deployedVersion); err != nil {
logrus.Fatal(err)
}
}
}
if !internal.Watch { if !internal.Watch {
showPSOutput(c, app, cl) showPSOutput(c, app, cl)
return nil return nil
@ -66,36 +84,77 @@ var appPsCommand = cli.Command{
// showPSOutput renders ps output. // showPSOutput renders ps output.
func showPSOutput(c *cli.Context, app config.App, cl *dockerClient.Client) { func showPSOutput(c *cli.Context, app config.App, cl *dockerClient.Client) {
filters, err := app.Filters(true, true) composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
return
} }
containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters}) deployOpts := stack.Deploy{
Composefiles: composeFiles,
Namespace: app.StackName(),
Prune: false,
ResolveImage: stack.ResolveImageAlways,
}
compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
return
} }
tableCol := []string{"service name", "image", "created", "status", "state", "ports"} var tablerows [][]string
table := formatter.CreateTable(tableCol) 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))
for _, container := range containers { containers, err := cl.ContainerList(context.Background(), containerTypes.ListOptions{Filters: filters})
var containerNames []string if err != nil {
for _, containerName := range container.Names { logrus.Fatal(err)
trimmed := strings.TrimPrefix(containerName, "/") return
containerNames = append(containerNames, trimmed)
} }
tableRow := []string{ var containerStats map[string]string
service.ContainerToServiceName(container.Names, app.StackName()),
formatter.RemoveSha(container.Image), if len(containers) == 0 {
formatter.HumanDuration(container.Created), containerStats = map[string]string{
container.Status, "service name": service.Name,
container.State, "image": "unknown",
dockerFormatter.DisplayablePorts(container.Ports), "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),
}
} }
table.Append(tableRow) 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"}
table := formatter.CreateTable(tableCol)
for _, row := range tablerows {
table.Append(row)
}
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,7 +12,6 @@ import (
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/volume"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -112,28 +110,19 @@ flag.
logrus.Fatal(err) logrus.Fatal(err)
} }
volumeListOptions := volume.ListOptions{fs} volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, fs)
volumeListOKBody, err := cl.VolumeList(context.Background(), volumeListOptions)
volumeList := volumeListOKBody.Volumes
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
volumeNames := client.GetVolumeNames(volumeList)
var vols []string if len(volumeNames) > 0 {
for _, vol := range volumeList { err := client.RemoveVolumes(cl, context.Background(), volumeNames, internal.Force, 5)
vols = append(vols, vol.Name) if err != nil {
} log.Fatalf("removing volumes failed: %s", err)
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 {
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

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

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

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"os" "os"
"strconv" "strconv"
"strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
@ -56,22 +57,27 @@ var appSecretGenerateCommand = cli.Command{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if err := recipe.EnsureExists(app.Recipe); err != nil { r, err := recipe.Get(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
if err := r.EnsureExists(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Chaos { if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil { if err := r.EnsureIsClean(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Offline { if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil { if err := r.EnsureUpToDate(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
if err := recipe.EnsureLatest(app.Recipe); err != nil { if err := r.EnsureLatest(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
@ -156,6 +162,8 @@ var appSecretInsertCommand = cli.Command{
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.PassFlag, internal.PassFlag,
internal.FileFlag,
internal.TrimFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
ArgsUsage: "<domain> <secret-name> <version> <data>", ArgsUsage: "<domain> <secret-name> <version> <data>",
@ -188,6 +196,18 @@ Example:
version := c.Args().Get(2) version := c.Args().Get(2)
data := c.Args().Get(3) data := c.Args().Get(3)
if internal.File {
raw, err := os.ReadFile(data)
if err != nil {
logrus.Fatalf("reading secret from file: %s", err)
}
data = string(raw)
}
if internal.Trim {
data = strings.TrimSpace(data)
}
secretName := fmt.Sprintf("%s_%s_%s", app.StackName(), name, version) secretName := fmt.Sprintf("%s_%s_%s", app.StackName(), name, version)
if err := client.StoreSecret(cl, secretName, data, app.Server); err != nil { if err := client.StoreSecret(cl, secretName, data, app.Server); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -249,22 +269,27 @@ Example:
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if err := recipe.EnsureExists(app.Recipe); err != nil { r, err := recipe.Get(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
if err := r.EnsureExists(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Chaos { if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil { if err := r.EnsureIsClean(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Offline { if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil { if err := r.EnsureUpToDate(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
if err := recipe.EnsureLatest(app.Recipe); err != nil { if err := r.EnsureLatest(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
@ -357,22 +382,27 @@ var appSecretLsCommand = cli.Command{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if err := recipe.EnsureExists(app.Recipe); err != nil { r, err := recipe.Get(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
if err := r.EnsureExists(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Chaos { if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil { if err := r.EnsureIsClean(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Offline { if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil { if err := r.EnsureUpToDate(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
if err := recipe.EnsureLatest(app.Recipe); err != nil { if err := r.EnsureLatest(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }

View File

@ -11,7 +11,7 @@ import (
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/service" "coopcloud.tech/abra/pkg/service"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types" containerTypes "github.com/docker/docker/api/types/container"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -48,7 +48,7 @@ var appServicesCommand = cli.Command{
logrus.Fatal(err) logrus.Fatal(err)
} }
containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters}) containers, err := cl.ContainerList(context.Background(), containerTypes.ListOptions{Filters: filters})
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -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,29 +198,30 @@ 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
for _, version := range versions { if chosenUpgrade != "" {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
logrus.Fatal(err)
}
parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade) parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
for _, version := range versions {
if !(parsedVersion.Equals(parsedDeployedVersion)) && parsedVersion.IsLessThan(parsedChosenUpgrade) { parsedVersion, err := tagcmp.Parse(version)
note, err := internal.GetReleaseNotes(app.Recipe, version)
if err != nil { if err != nil {
return err logrus.Fatal(err)
} }
if note != "" { if parsedVersion.IsGreaterThan(parsedDeployedVersion) && parsedVersion.IsLessThan(parsedChosenUpgrade) {
releaseNotes += fmt.Sprintf("%s\n", note) note, err := internal.GetReleaseNotes(app.Recipe, version)
if err != nil {
return err
}
if note != "" {
releaseNotes += fmt.Sprintf("%s\n", note)
}
} }
} }
} }
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,7 +10,7 @@ import (
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/distribution/reference" "github.com/distribution/reference"
"github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
@ -58,6 +58,11 @@ var appVersionCommand = cli.Command{
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
stackName := app.StackName() stackName := app.StackName()
r, err := recipe.Get(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -78,7 +83,7 @@ var appVersionCommand = cli.Command{
logrus.Fatalf("failed to determine version of deployed %s", app.Name) logrus.Fatalf("failed to determine version of deployed %s", app.Name)
} }
recipeMeta, err := recipe.GetRecipeMeta(app.Recipe, internal.Offline) recipeMeta, err := r.GetRecipeMeta(internal.Offline)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

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

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

View File

@ -38,6 +38,20 @@ var PassRemoveFlag = &cli.BoolFlag{
Destination: &PassRemove, Destination: &PassRemove,
} }
var File bool
var FileFlag = &cli.BoolFlag{
Name: "file, f",
Usage: "Treat input as a file",
Destination: &File,
}
var Trim bool
var TrimFlag = &cli.BoolFlag{
Name: "trim, t",
Usage: "Trim input",
Destination: &Trim,
}
// Force force functionality without asking. // Force force functionality without asking.
var Force bool var Force bool
@ -95,6 +109,16 @@ var OfflineFlag = &cli.BoolFlag{
Usage: "Prefer offline & filesystem access when possible", Usage: "Prefer offline & filesystem access when possible",
} }
// ReleaseNotes stores the variable from ReleaseNotesFlag.
var ReleaseNotes bool
// ReleaseNotesFlag turns on/off printing only release notes when upgrading.
var ReleaseNotesFlag = &cli.BoolFlag{
Name: "releasenotes, r",
Destination: &ReleaseNotes,
Usage: "Only show release notes",
}
// MachineReadable stores the variable from MachineReadableFlag // MachineReadable stores the variable from MachineReadableFlag
var MachineReadable bool var MachineReadable bool

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

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

View File

@ -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,7 +7,6 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -27,24 +26,27 @@ var recipeLintCommand = cli.Command{
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c) r := internal.ValidateRecipe(c)
if err := r.LoadConfig(); err != nil {
logrus.Fatal(err)
}
if err := recipePkg.EnsureExists(recipe.Name); err != nil { if err := r.EnsureExists(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Chaos { if !internal.Chaos {
if err := recipePkg.EnsureIsClean(recipe.Name); err != nil { if err := r.EnsureIsClean(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Offline { if !internal.Offline {
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil { if err := r.EnsureUpToDate(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
if err := recipePkg.EnsureLatest(recipe.Name); err != nil { if err := r.EnsureLatest(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
@ -62,7 +64,7 @@ var recipeLintCommand = cli.Command{
} }
skipped := false skipped := false
if rule.Skip(recipe) { if rule.Skip(r) {
skipped = true skipped = true
} }
@ -73,7 +75,7 @@ var recipeLintCommand = cli.Command{
satisfied := false satisfied := false
if !skipped { if !skipped {
ok, err := rule.Function(recipe) ok, err := rule.Function(r)
if err != nil { if err != nil {
logrus.Warn(err) logrus.Warn(err)
} }

View File

@ -17,7 +17,7 @@ import (
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference" "github.com/distribution/reference"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
@ -108,14 +108,14 @@ your SSH keys configured on your account.
} }
} }
isClean, err := gitPkg.IsClean(recipe.Dir()) isClean, err := gitPkg.IsClean(recipe.Dir)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !isClean { if !isClean {
logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name) logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name)
if err := gitPkg.DiffUnstaged(recipe.Dir()); err != nil { if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
@ -184,7 +184,7 @@ func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error { func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error {
var err error var err error
directory := path.Join(config.RECIPES_DIR, recipe.Name) directory := path.Join(recipe.Dir)
repo, err := git.PlainOpen(directory) repo, err := git.PlainOpen(directory)
if err != nil { if err != nil {
return err return err
@ -246,8 +246,7 @@ func getTagCreateOptions(tag string) (git.CreateTagOptions, error) {
// addReleaseNotes checks if the release/next release note exists and moves the // addReleaseNotes checks if the release/next release note exists and moves the
// file to release/<tag>. // file to release/<tag>.
func addReleaseNotes(recipe recipe.Recipe, tag string) error { func addReleaseNotes(recipe recipe.Recipe, tag string) error {
repoPath := path.Join(config.RECIPES_DIR, recipe.Name) tagReleaseNotePath := path.Join(recipe.Dir, "release", tag)
tagReleaseNotePath := path.Join(repoPath, "release", tag)
if _, err := os.Stat(tagReleaseNotePath); err == nil { if _, err := os.Stat(tagReleaseNotePath); err == nil {
// Release note for current tag already exist exists. // Release note for current tag already exist exists.
return nil return nil
@ -255,7 +254,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
return err return err
} }
nextReleaseNotePath := path.Join(repoPath, "release", "next") nextReleaseNotePath := path.Join(recipe.Dir, "release", "next")
if _, err := os.Stat(nextReleaseNotePath); err == nil { if _, err := os.Stat(nextReleaseNotePath); err == nil {
// release/next note exists. Move it to release/<tag> // release/next note exists. Move it to release/<tag>
if internal.Dry { if internal.Dry {
@ -278,11 +277,11 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
if err != nil { if err != nil {
return err return err
} }
err = gitPkg.Add(repoPath, path.Join("release", "next"), internal.Dry) err = gitPkg.Add(recipe.Dir, path.Join("release", "next"), internal.Dry)
if err != nil { if err != nil {
return err return err
} }
err = gitPkg.Add(repoPath, path.Join("release", tag), internal.Dry) err = gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry)
if err != nil { if err != nil {
return err return err
} }
@ -311,7 +310,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
if err != nil { if err != nil {
return err return err
} }
err = gitPkg.Add(repoPath, path.Join("release", tag), internal.Dry) err = gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry)
if err != nil { if err != nil {
return err return err
} }
@ -325,14 +324,14 @@ func commitRelease(recipe recipe.Recipe, tag string) error {
return nil return nil
} }
isClean, err := gitPkg.IsClean(recipe.Dir()) isClean, err := gitPkg.IsClean(recipe.Dir)
if err != nil { if err != nil {
return err return err
} }
if isClean { if isClean {
if !internal.Dry { if !internal.Dry {
return fmt.Errorf("no changes discovered in %s, nothing to publish?", recipe.Dir()) return fmt.Errorf("no changes discovered in %s, nothing to publish?", recipe.Dir)
} }
} }
@ -402,8 +401,7 @@ func pushRelease(recipe recipe.Recipe, tagString string) error {
} }
func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error { func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error {
directory := path.Join(config.RECIPES_DIR, recipe.Name) repo, err := git.PlainOpen(recipe.Dir)
repo, err := git.PlainOpen(directory)
if err != nil { if err != nil {
return err return err
} }

View File

@ -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,7 +18,7 @@ import (
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference" "github.com/distribution/reference"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -73,19 +73,19 @@ You may invoke this command in "wizard" mode and be prompted for input:
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c) recipe := internal.ValidateRecipe(c)
if err := recipePkg.EnsureIsClean(recipe.Name); err != nil { if err := recipe.EnsureIsClean(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if err := recipePkg.EnsureExists(recipe.Name); err != nil { if err := recipe.EnsureExists(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil { if err := recipe.EnsureUpToDate(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if err := recipePkg.EnsureLatest(recipe.Name); err != nil { if err := recipe.EnsureLatest(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -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)
} }
tableCols := []string{"version", "service", "image", "tag"}
aggregated_table := formatter.CreateTable(tableCols)
for i := len(recipeMeta.Versions) - 1; i >= 0; i-- { for i := len(recipeMeta.Versions) - 1; i >= 0; i-- {
tableCols := []string{"version", "service", "image", "tag"}
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

@ -53,7 +53,7 @@ func cleanUp(domainName string) {
// Docker manages SSH connection details. These are stored to disk in // Docker manages SSH connection details. These are stored to disk in
// ~/.docker. Abra can manage this completely for the user, so it's an // ~/.docker. Abra can manage this completely for the user, so it's an
// implementation detail. // implementation detail.
func newContext(c *cli.Context, domainName, username, port string) error { func newContext(c *cli.Context, domainName string) error {
store := contextPkg.NewDefaultDockerContextStore() store := contextPkg.NewDefaultDockerContextStore()
contexts, err := store.Store.List() contexts, err := store.Store.List()
if err != nil { if err != nil {
@ -67,9 +67,9 @@ func newContext(c *cli.Context, domainName, username, port string) error {
} }
} }
logrus.Debugf("creating context with domain %s, username %s and port %s", domainName, username, port) logrus.Debugf("creating context with domain %s", domainName)
if err := client.CreateContext(domainName, username, port); err != nil { if err := client.CreateContext(domainName); err != nil {
return err return err
} }
@ -158,12 +158,7 @@ developer machine.
logrus.Fatal(err) logrus.Fatal(err)
} }
hostConfig, err := sshPkg.GetHostConfig(domainName) if err := newContext(c, domainName); err != nil {
if err != nil {
logrus.Fatal(err)
}
if err := newContext(c, domainName, hostConfig.User, hostConfig.Port); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -316,22 +316,23 @@ func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName
// processRecipeRepoVersion clones, pulls, checks out the version and lints the // processRecipeRepoVersion clones, pulls, checks out the version and lints the
// recipe repository. // recipe repository.
func processRecipeRepoVersion(recipeName, version string) error { func processRecipeRepoVersion(recipe recipe.Recipe, version string) error {
if err := recipe.EnsureExists(recipeName); err != nil { if err := recipe.EnsureExists(); err != nil {
return err return err
} }
if err := recipe.EnsureUpToDate(recipeName); err != nil { if err := recipe.EnsureUpToDate(); err != nil {
return err return err
} }
if err := recipe.EnsureVersion(recipeName, version); err != nil { if err := recipe.EnsureVersion(version); err != nil {
return err return err
} }
if r, err := recipe.Get(recipeName, internal.Offline); err != nil { if err := recipe.LoadConfig(); err != nil {
return err return err
} else if err := lint.LintForErrors(r); err != nil { }
if err := lint.LintForErrors(recipe); err != nil {
return err return err
} }
@ -442,7 +443,12 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName,
Env: env, Env: env,
} }
if err = processRecipeRepoVersion(recipeName, upgradeVersion); err != nil { recipe, err := recipe.Get(recipeName)
if err != nil {
return err
}
if err = processRecipeRepoVersion(recipe, upgradeVersion); err != nil {
return err return err
} }

133
go.mod
View File

@ -3,118 +3,131 @@ module coopcloud.tech/abra
go 1.21 go 1.21
require ( require (
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52 coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355
github.com/AlecAivazis/survey/v2 v2.3.7 github.com/AlecAivazis/survey/v2 v2.3.7
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
github.com/docker/cli v24.0.7+incompatible github.com/distribution/distribution v2.8.3+incompatible
github.com/docker/distribution v2.8.3+incompatible github.com/docker/cli v26.1.4+incompatible
github.com/docker/docker v24.0.7+incompatible github.com/docker/docker v26.1.4+incompatible
github.com/docker/go-units v0.5.0 github.com/docker/go-units v0.5.0
github.com/go-git/go-git/v5 v5.10.0 github.com/go-git/go-git/v5 v5.12.0
github.com/google/go-cmp v0.5.9 github.com/google/go-cmp v0.6.0
github.com/moby/sys/signal v0.7.0 github.com/moby/sys/signal v0.7.0
github.com/moby/term v0.5.0 github.com/moby/term v0.5.0
github.com/olekukonko/tablewriter v0.0.5 github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.14.1 github.com/schollz/progressbar/v3 v3.14.4
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
gotest.tools/v3 v3.5.1 gotest.tools/v3 v3.5.1
) )
require ( require (
dario.cat/mergo v1.0.0 // indirect dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/BurntSushi/toml v1.0.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/hcsshim v0.9.2 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
github.com/acomagu/bufpipe v1.0.4 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cloudflare/circl v1.3.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/cloudflare/circl v1.3.9 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/containerd/log v0.1.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/cyphar/filepath-securejoin v0.2.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.5.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/ghodss/yaml v1.0.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.14.2 // indirect github.com/klauspost/compress v1.17.9 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/miekg/pkcs11 v1.0.3 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/runc v1.1.0 // indirect github.com/opencontainers/runc v1.1.13 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/common v0.54.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.4.4 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/skeema/knownhosts v1.2.0 // indirect github.com/skeema/knownhosts v1.2.2 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/crypto v0.14.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect
golang.org/x/mod v0.12.0 // indirect go.opentelemetry.io/otel v1.27.0 // indirect
golang.org/x/net v0.17.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 // indirect
golang.org/x/sync v0.3.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect
golang.org/x/term v0.14.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
golang.org/x/text v0.13.0 // indirect go.opentelemetry.io/otel/metric v1.27.0 // indirect
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect go.opentelemetry.io/otel/sdk v1.27.0 // indirect
golang.org/x/tools v0.13.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.27.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect go.opentelemetry.io/otel/trace v1.27.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/term v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect
google.golang.org/grpc v1.64.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
require ( require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect
github.com/buger/goterm v1.0.4 github.com/buger/goterm v1.0.4
github.com/containerd/containerd v1.5.9 // indirect github.com/containerd/containerd v1.7.18 // indirect
github.com/containers/image v3.0.2+incompatible github.com/containers/image v3.0.2+incompatible
github.com/containers/storage v1.38.2 // indirect github.com/containers/storage v1.38.2 // indirect
github.com/decentral1se/passgen v1.0.1 github.com/decentral1se/passgen v1.0.1
github.com/docker/docker-credential-helpers v0.6.4 // indirect github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/fvbommel/sortorder v1.0.2 // indirect github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 github.com/hashicorp/go-retryablehttp v0.7.7
github.com/klauspost/pgzip v1.2.6 github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/patternmatcher v0.5.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect github.com/moby/sys/sequential v0.5.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20211202193544-a5463b7f9c84 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect github.com/prometheus/client_golang v1.19.1 // indirect
github.com/sergi/go-diff v1.2.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/spf13/cobra v1.3.0 // indirect github.com/spf13/cobra v1.8.1 // indirect
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.9.0
github.com/theupdateframework/notary v0.7.0 // indirect github.com/theupdateframework/notary v0.7.0 // indirect
github.com/urfave/cli v1.22.9 github.com/urfave/cli v1.22.15
github.com/xeipuuv/gojsonpointer v0.0.0-20190809123943-df4f5c81cb3b // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
golang.org/x/sys v0.14.0 golang.org/x/sys v0.21.0
) )

630
go.sum

File diff suppressed because it is too large Load Diff

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

@ -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,8 +1,8 @@
#!/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.1-beta" RC_VERSION="0.8.0-rc1-beta"
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION" RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION"
for arg in "$@"; do for arg in "$@"; do
@ -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

@ -70,13 +70,13 @@ setup(){
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
run $ABRA app check "$TEST_APP_DOMAIN" run $ABRA app check "$TEST_APP_DOMAIN"
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
refute_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
_reset_recipe _reset_recipe
} }
@ -86,7 +86,7 @@ setup(){
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 1' assert_output --partial "Your branch is behind 'origin/main' by 1 commit"
# NOTE(d1): we can't quite tell if this will fail or not in the future, so, # NOTE(d1): we can't quite tell if this will fail or not in the future, so,
# since it isn't an important part of what we're testing here, we don't check # since it isn't an important part of what we're testing here, we don't check
@ -94,7 +94,7 @@ setup(){
run $ABRA app check "$TEST_APP_DOMAIN" --offline run $ABRA app check "$TEST_APP_DOMAIN" --offline
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 1' assert_output --partial "Your branch is behind 'origin/main' by 1 commit"
_reset_recipe _reset_recipe
} }

View File

@ -58,7 +58,7 @@ test_cmd_export"
assert_success assert_success
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE" assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local run $ABRA app cmd --local "$TEST_APP_DOMAIN" test_cmd
assert_success assert_success
assert_output --partial 'baz' assert_output --partial 'baz'
@ -70,7 +70,7 @@ test_cmd_export"
assert_success assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo" assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local run $ABRA app cmd --local "$TEST_APP_DOMAIN" test_cmd
assert_failure assert_failure
assert_output --partial 'locally unstaged changes' assert_output --partial 'locally unstaged changes'
@ -83,7 +83,7 @@ test_cmd_export"
assert_success assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo" assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local --chaos run $ABRA app cmd --local --chaos "$TEST_APP_DOMAIN" test_cmd
assert_success assert_success
assert_output --partial 'baz' assert_output --partial 'baz'
@ -96,14 +96,14 @@ test_cmd_export"
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local run $ABRA app cmd --local "$TEST_APP_DOMAIN" test_cmd
assert_success assert_success
assert_output --partial 'baz' assert_output --partial 'baz'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
refute_output --partial 'behind 3' assert_output --partial "up to date"
_reset_recipe "$TEST_RECIPE" _reset_recipe "$TEST_RECIPE"
} }
@ -113,14 +113,14 @@ test_cmd_export"
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local --offline run $ABRA app cmd --local --offline "$TEST_APP_DOMAIN" test_cmd
assert_success assert_success
assert_output --partial 'baz' assert_output --partial 'baz'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
_reset_recipe "$TEST_RECIPE" _reset_recipe "$TEST_RECIPE"
} }
@ -132,13 +132,13 @@ test_cmd_export"
} }
@test "error if missing arguments when passing --local" { @test "error if missing arguments when passing --local" {
run $ABRA app cmd "$TEST_APP_DOMAIN" --local run $ABRA app cmd --local "$TEST_APP_DOMAIN"
assert_failure assert_failure
assert_output --partial 'missing arguments' assert_output --partial 'missing arguments'
} }
@test "cannot use --local and --user at same time" { @test "cannot use --local and --user at same time" {
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local --user root run $ABRA app cmd --local --user root "$TEST_APP_DOMAIN" test_cmd
assert_failure assert_failure
assert_output --partial 'cannot use --local & --user together' assert_output --partial 'cannot use --local & --user together'
} }
@ -147,7 +147,7 @@ test_cmd_export"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/abra.sh" run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/abra.sh"
assert_success assert_success
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local --chaos run $ABRA app cmd --local --chaos "$TEST_APP_DOMAIN" test_cmd
assert_failure assert_failure
assert_output --partial "$ABRA_DIR/recipes/$TEST_RECIPE/abra.sh does not exist" assert_output --partial "$ABRA_DIR/recipes/$TEST_RECIPE/abra.sh does not exist"
@ -155,25 +155,25 @@ test_cmd_export"
} }
@test "error if missing command" { @test "error if missing command" {
run $ABRA app cmd "$TEST_APP_DOMAIN" doesnt_exist --local run $ABRA app cmd --local "$TEST_APP_DOMAIN" doesnt_exist
assert_failure assert_failure
assert_output --partial "doesn't have a doesnt_exist function" assert_output --partial "doesn't have a doesnt_exist function"
} }
@test "run --local command" { @test "run --local command" {
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local run $ABRA app cmd --local "$TEST_APP_DOMAIN" test_cmd
assert_success assert_success
assert_output --partial 'baz' assert_output --partial 'baz'
} }
@test "run command with single arg" { @test "run command with single arg" {
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd_arg --local -- bing run $ABRA app cmd --local "$TEST_APP_DOMAIN" test_cmd_arg -- bing
assert_success assert_success
assert_output --partial 'bing' assert_output --partial 'bing'
} }
@test "run command with several args" { @test "run command with several args" {
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd_args --local -- bong bang run $ABRA app cmd --local "$TEST_APP_DOMAIN" test_cmd_args -- bong bang
assert_success assert_success
assert_output --partial 'bong bang' assert_output --partial 'bong bang'
} }

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
@ -16,6 +17,7 @@ teardown_file(){
setup(){ setup(){
load "$PWD/tests/integration/helpers/common" load "$PWD/tests/integration/helpers/common"
_common_setup _common_setup
_reset_recipe
} }
teardown(){ teardown(){
@ -82,13 +84,13 @@ teardown(){
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
refute_output --partial 'behind 3' refute_output --regexp 'behind .* 3 commits'
_reset_recipe _reset_recipe
_undeploy_app _undeploy_app
@ -100,7 +102,7 @@ teardown(){
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
# NOTE(d1): need to use --chaos to force same commit # NOTE(d1): need to use --chaos to force same commit
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
@ -108,7 +110,7 @@ teardown(){
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
_undeploy_app _undeploy_app
_reset_recipe _reset_recipe
@ -116,6 +118,9 @@ teardown(){
# bats test_tags=slow # bats test_tags=slow
@test "deploy latest commit if no published versions and no --chaos" { @test "deploy latest commit if no published versions and no --chaos" {
# TODO(d1): fix with a new test recipe which has no published versions?
skip "known issue, abra-test-recipe has published versions now"
latestCommit="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)" latestCommit="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)"
_remove_tags _remove_tags
@ -140,7 +145,7 @@ teardown(){
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
threeCommitsBack="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)" threeCommitsBack="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)"
@ -273,6 +278,10 @@ teardown(){
} }
@test "ensure domain is checked" { @test "ensure domain is checked" {
if [[ "$TEST_SERVER" == "default" ]]; then
skip "domain checks are disabled for local server"
fi
appDomain="custom-html.DOESNTEXIST" appDomain="custom-html.DOESNTEXIST"
run $ABRA app new custom-html \ run $ABRA app new custom-html \
@ -354,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
@ -362,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 "Your branch is up to date with 'origin/main'." _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 --partial "Your branch is behind 'origin/main' by 3 commits, and can be fast-forwarded."
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 "Your branch is up to date with 'origin/main'."
_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 --partial "Your branch is behind 'origin/main' by 3 commits, and can be fast-forwarded."
# NOTE(d1): need to use --chaos to force same commit # 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 --partial "Your branch is behind 'origin/main' by 3 commits, and can be fast-forwarded."
_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,7 +104,7 @@ teardown(){
_undeploy_app _undeploy_app
run $ABRA app volume rm "$TEST_APP_DOMAIN" run $ABRA app volume rm "$TEST_APP_DOMAIN" --no-input
assert_success assert_success
run $ABRA app volume ls "$TEST_APP_DOMAIN" run $ABRA app volume ls "$TEST_APP_DOMAIN"

View File

@ -109,13 +109,13 @@ teardown(){
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
run $ABRA app restore "$TEST_APP_DOMAIN" app DOESNTEXIST run $ABRA app restore "$TEST_APP_DOMAIN" app
assert_failure assert_failure
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
refute_output --partial 'behind 3' assert_output --partial "up to date"
} }
@test "ensure recipe not up to date if --offline" { @test "ensure recipe not up to date if --offline" {
@ -126,19 +126,19 @@ teardown(){
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
run $ABRA app restore "$TEST_APP_DOMAIN" app DOESNTEXIST --offline run $ABRA app restore "$TEST_APP_DOMAIN" app --offline
assert_failure assert_failure
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$latestCommit" run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$latestCommit"
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
refute_output --partial 'behind 3' assert_output --partial "HEAD detached at $latestCommit"
} }
@test "error if missing service" { @test "error if missing service" {

View File

@ -50,13 +50,13 @@ teardown(){
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
run $ABRA app rollback "$TEST_APP_DOMAIN" --no-input --no-converge-checks run $ABRA app rollback "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_failure assert_failure
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
refute_output --partial 'behind 3' assert_output --partial "up to date"
} }
@test "ensure recipe not up to date if --offline" { @test "ensure recipe not up to date if --offline" {
@ -67,14 +67,14 @@ teardown(){
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
run $ABRA app rollback "$TEST_APP_DOMAIN" \ run $ABRA app rollback "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --offline --no-input --no-converge-checks --offline
assert_failure assert_failure
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$latestCommit" run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$latestCommit"
assert_success assert_success
@ -131,7 +131,7 @@ teardown(){
latestCommit="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)" latestCommit="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)"
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos --no-input --chaos
assert_success assert_success
assert_output --partial "$latestCommit" assert_output --partial "$latestCommit"
assert_output --partial 'chaos' assert_output --partial 'chaos'

View File

@ -8,7 +8,7 @@ setup_file(){
run $ABRA app new "$TEST_RECIPE" \ run $ABRA app new "$TEST_RECIPE" \
--no-input \ --no-input \
--server "$TEST_SERVER" \ --server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN" \ --domain "$TEST_APP_DOMAIN"
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"
} }
@ -19,11 +19,11 @@ teardown_file(){
_reset_recipe _reset_recipe
} }
teardown(){ teardown() {
# https://github.com/bats-core/bats-core/issues/383#issuecomment-738628888 run $ABRA app secret rm "$TEST_APP_DOMAIN" --all
if [[ -z "${BATS_TEST_COMPLETED}" ]]; then _reset_app
_undeploy_app _reset_recipe
fi _checkout_recipe
} }
setup(){ setup(){
@ -84,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" {
@ -98,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" {
@ -115,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" {
@ -138,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" {
@ -169,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" {
@ -235,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" {
@ -321,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" {
@ -337,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

@ -59,6 +59,8 @@ teardown(){
# bats test_tags=slow # bats test_tags=slow
@test "error if not in catalogue" { @test "error if not in catalogue" {
skip "known issue, see https://git.coopcloud.tech/coop-cloud/recipes-catalogue-json/issues/6"
_deploy_app _deploy_app
run $ABRA app version "$TEST_APP_DOMAIN" run $ABRA app version "$TEST_APP_DOMAIN"
@ -92,7 +94,7 @@ teardown(){
assert_success assert_success
# NOTE(d1): to let the stack come down before nuking volumes # NOTE(d1): to let the stack come down before nuking volumes
sleep 3 sleep 5
run $ABRA app volume remove "$appDomain" --no-input run $ABRA app volume remove "$appDomain" --no-input
assert_success assert_success

View File

@ -78,9 +78,6 @@ teardown(){
_undeploy_app _undeploy_app
# NOTE(d1): to let the stack come down before nuking volumes
sleep 5
run $ABRA app volume rm "$TEST_APP_DOMAIN" --force run $ABRA app volume rm "$TEST_APP_DOMAIN" --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 5
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

@ -49,7 +49,7 @@ _reset_app(){
run $ABRA app new "$TEST_RECIPE" \ run $ABRA app new "$TEST_RECIPE" \
--no-input \ --no-input \
--server "$TEST_SERVER" \ --server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN" \ --domain "$TEST_APP_DOMAIN"
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

@ -11,7 +11,11 @@ _add_server() {
} }
_rm_server() { _rm_server() {
run $ABRA server remove --no-input "$TEST_SERVER" if [[ "$TEST_SERVER" == "default" ]]; then
run rm -rf "$ABRA_DIR/servers/default"
else
run $ABRA server remove --no-input "$TEST_SERVER"
fi
assert_success assert_success
assert_not_exists "$ABRA_DIR/servers/$TEST_SERVER" assert_not_exists "$ABRA_DIR/servers/$TEST_SERVER"
} }

View File

@ -66,13 +66,13 @@ setup() {
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
run $ABRA recipe lint "$TEST_RECIPE" run $ABRA recipe lint "$TEST_RECIPE"
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
refute_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
_reset_recipe _reset_recipe
} }
@ -82,13 +82,13 @@ setup() {
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
run $ABRA recipe lint "$TEST_RECIPE" --offline run $ABRA recipe lint "$TEST_RECIPE" --offline
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
_reset_recipe _reset_recipe
} }

View File

@ -23,14 +23,14 @@ teardown(){
} }
@test "create new recipe" { @test "create new recipe" {
run $ABRA recipe new foobar --git-name foo --git-email foo@example.com run $ABRA recipe new foobar
assert_success assert_success
assert_output --partial 'Your new foobar recipe has been created' assert_output --partial 'Your new foobar recipe has been created'
assert_exists "$ABRA_DIR/recipes/foobar" assert_exists "$ABRA_DIR/recipes/foobar"
} }
@test "create new app from new recipe" { @test "create new app from new recipe" {
run $ABRA recipe new foobar --git-name foo --git-email foo@example.com run $ABRA recipe new foobar
assert_success assert_success
run $ABRA app new foobar \ run $ABRA app new foobar \
@ -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'
}

View File

@ -61,14 +61,14 @@ setup(){
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input run $ABRA recipe upgrade "$TEST_RECIPE" --no-input
assert_success assert_success
assert_output --partial 'can upgrade service: app' assert_output --partial 'can upgrade service: app'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
refute_output --partial 'behind 3' assert_output --regexp 'behind .* 3 commits'
_reset_recipe _reset_recipe
} }

View File

@ -12,6 +12,8 @@ setup() {
} }
@test "error if not present in catalogue" { @test "error if not present in catalogue" {
skip "known issue, see https://git.coopcloud.tech/coop-cloud/recipes-catalogue-json/issues/6"
run $ABRA recipe versions "$TEST_RECIPE" run $ABRA recipe versions "$TEST_RECIPE"
assert_failure assert_failure
assert_output --partial "is not published on the catalogue" assert_output --partial "is not published on the catalogue"