Compare commits

..

53 Commits

Author SHA1 Message Date
decentral1se f29278f80a chore: go mod tidy 2023-03-06 15:04:15 +01:00
renovate-bot a9a294cbb7 chore(deps): update module golang.org/x/sys to v0.6.0 2023-03-06 08:02:17 +00:00
decentral1se 73004789a4 chore: go mod tidy 2023-03-04 08:50:24 +01:00
renovate-bot 440aba77d5 chore(deps): update module github.com/go-git/go-git/v5 to v5.6.0 2023-03-01 08:02:11 +00:00
moritz e4a89bcc4f fix(kadabra): only warn if a deployed app has no published release 2023-02-28 15:34:27 +01:00
decentral1se eb07617e73 chore: publish new release 0.7.0-beta 2023-02-22 08:47:43 +01:00
decentral1se 9fca4e56fb docs: add comrade vera [ci skip] 2023-02-19 11:00:43 +01:00
decentral1se f17523010a chore: publish next tag 0.7.0-rc3-beta 2023-02-19 10:51:10 +01:00
decentral1se 3058178d84 fix: if all servers good, don't show empty table 2023-02-19 10:34:47 +01:00
decentral1se d62c4e3400 refactor: improved logging on pruning 2023-02-19 10:28:18 +01:00
decentral1se 5739758c3a fix: give more time to tear down state [ci skip] 2023-02-17 11:11:28 +01:00
decentral1se a6b5566fa6 refactor: clarify prune scope, not system wide [ci skip] 2023-02-17 11:09:44 +01:00
decentral1se 4dbe1362a8 docs: more clarity on prune functionality 2023-02-17 11:00:02 +01:00
decentral1se 98fc36c830 refactor: hopefully more robust prune logic, docs 2023-02-17 10:59:06 +01:00
decentral1se b8abc8705c docs: volumes pruning docs - more warnings 2023-02-17 10:42:38 +01:00
decentral1se 636261934f refactor: pass args in, docs, rename, lower-case logs 2023-02-17 10:23:00 +01:00
decentral1se 6381b73a6a chore: use lower-case like elsewhere 2023-02-17 10:21:56 +01:00
decentral1se 1a72e27045 refactor: add server auto-complete & cosmetics 2023-02-17 10:12:46 +01:00
decentral1se 9754c1b2d1 feat: server auto-complete on remove sub-command 2023-02-17 10:10:48 +01:00
codegod100 b14ec0cda4 review cleanups 2023-02-17 08:53:43 +00:00
codegod100 c7730ba604 Adding server prune and undeploy prune 2023-02-17 08:53:43 +00:00
decentral1se 47c61df444 docs: add comrade yksflip [ci skip] 2023-02-15 11:26:20 +01:00
decentral1se 312b93e794 fix: no gitops on recipe for "app new"
Closes coop-cloud/organising#408
2023-02-15 00:49:22 +01:00
decentral1se 992e675921 refactor: use passed down conf to decide 2023-02-15 00:35:33 +01:00
decentral1se d4f3a7be31 docs: add comrade codegod100 [ci skip] 2023-02-14 17:16:25 +01:00
codegod100 d619f399e7 Update 'cli/app/undeploy.go' 2023-02-14 13:59:35 +00:00
decentral1se 96a8cb7aff chore: go mod tidy 2023-02-14 14:29:25 +01:00
renovate-bot 9b51d22c20 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.40.0 2023-02-14 08:02:28 +00:00
philippr d789830ce4 feat: adds --since flag for abra app logs 2023-02-14 00:19:38 +01:00
decentral1se e4b4084dfd fix: stream logs without hitting git.coopcloud.tech
Medium-sized options refactor in here too!

See coop-cloud/organising#292.
2023-02-13 16:46:43 +01:00
decentral1se ff58646cfc fix: better error message when network gone 2023-02-13 12:33:00 +01:00
decentral1se eec6469ba1 fix: Change error message to reflect RECIPE -> TYPE
Closes coop-cloud/organising#409
2023-02-12 16:40:48 +01:00
decentral1se e94f947d20 fix: don't create clients twice per server
Closes coop-cloud/organising#407
2023-02-12 00:02:59 +01:00
decentral1se cccbe4a2ec fix: typo [ci skip] 2023-02-11 23:53:42 +01:00
decentral1se f53cfb6c36 fix: better error message when missing context [ci skip] 2023-02-11 23:49:01 +01:00
decentral1se f55f01a25c build: verbose local builds to show progress 2023-02-11 23:40:47 +01:00
decentral1se ce5c1a9ebb chore: 0.7.0-rc2-beta 2023-02-10 12:47:20 +01:00
decentral1se 5e3b039f93 fix: kadabra is now called kadabra not abra
Closes coop-cloud/organising#402
2023-02-10 12:45:41 +01:00
decentral1se 0e9d218bbc docs: fix comment names 2023-02-10 12:45:24 +01:00
decentral1se e1c635af86 chore: remove newline [ci skip] 2023-02-08 23:49:01 +01:00
decentral1se f6b139dfea chore: formatting pass on kadabra [ci skip] 2023-02-08 23:20:25 +01:00
decentral1se 3d2b8fa446 chore: spacing 2023-02-08 23:02:54 +01:00
decentral1se 2eebac6fc0 chore: formatting, indentation 2023-02-08 22:59:47 +01:00
decentral1se f5e2710138 chore: remove comment 2023-02-08 22:59:30 +01:00
decentral1se 986470784d chore: sort gitignore listing 2023-02-08 22:59:03 +01:00
moritz e76ed771df feat: kadabra, the app auto-updater (!268)
coop-cloud/organising#236

Autoupdater `kadabra` is ready for testing.
It should run on the server, check for available minor/patch updates and automatically upgrade the apps.

Co-authored-by: Moritz <moritz.m@local-it.org>
Reviewed-on: coop-cloud/abra#268
2023-02-08 18:53:04 +00:00
decentral1se f28af5e42f fix: use correctly formatted comments 2023-02-08 11:28:38 +01:00
decentral1se fdf4854b0c fix: unbork comments
Was breaking the build but not anymore!
2023-02-08 11:20:30 +01:00
decentral1se 6b9512d09c build: docker dev builds depend on check too 2023-02-08 11:16:54 +01:00
decentral1se 21a86731d0 build: dont test/build if check fails
Save cycles for small mistakes.
2023-02-08 11:13:20 +01:00
decentral1se 91102e6607 build: not so useful anymore, also broken 2023-02-08 11:12:03 +01:00
decentral1se fadafda0b8 fix: make test suite work again 2023-02-08 11:11:39 +01:00
decentral1se c03cf76702 chore: gofmt import statements 2023-02-08 10:56:39 +01:00
48 changed files with 1060 additions and 139 deletions
+4 -14
View File
@@ -11,27 +11,15 @@ steps:
image: golang:1.20
commands:
- make build
depends_on:
- make check
- name: make test
image: golang:1.20
commands:
- make test
- name: notify on failure
image: plugins/matrix
settings:
homeserver: https://matrix.autonomic.zone
roomid: "IFazIpLtxiScqbHqoa:autonomic.zone"
userid: "@autono-bot:autonomic.zone"
accesstoken:
from_secret: autono_bot_access_token
depends_on:
- make check
- make build
- make test
when:
status:
- failure
- name: fetch
image: docker:git
@@ -73,6 +61,8 @@ steps:
event:
exclude:
- pull_request
depends_on:
- make check
volumes:
- name: deps
+1
View File
@@ -2,6 +2,7 @@
.e2e.env
.envrc
.vscode/
/kadabra
abra
dist/
tests/integration/.abra/catalogue
+31 -3
View File
@@ -1,16 +1,19 @@
---
project_name: abra
gitea_urls:
api: https://git.coopcloud.tech/api/v1
download: https://git.coopcloud.tech/
skip_tls_verify: false
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
- id: abra
binary: abra
dir: cmd/abra
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
@@ -26,15 +29,40 @@ builds:
ldflags:
- "-X 'main.Commit={{ .Commit }}'"
- "-X 'main.Version={{ .Version }}'"
- id: kadabra
binary: kadabra
dir: cmd/kadabra
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- 386
- amd64
- arm
- arm64
goarm:
- 5
- 6
- 7
ldflags:
- "-X 'main.Commit={{ .Commit }}'"
- "-X 'main.Version={{ .Version }}'"
archives:
- replacements:
386: i386
amd64: x86_64
format: binary
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: desc
filters:
+3
View File
@@ -5,9 +5,12 @@
- 3wordchant
- cassowary
- codegod100
- decentral1se
- frando
- kawaiipunk
- knoflook
- moritz
- roxxers
- vera
- yksflip
-2
View File
@@ -9,8 +9,6 @@ COPY . /app
WORKDIR /app
RUN CGO_ENABLED=0 make build
# RUN rm go.sum && go get -v -d -u ./... && \
# make build
FROM scratch
+1 -1
View File
@@ -1,5 +1,5 @@
Abra: The Co-op Cloud utility belt
Copyright (C) 2022 Co-op Cloud <helo@coopcloud.tech>
Copyright (C) 2022 Co-op Cloud <helo@coopcloud.tech>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
+12 -7
View File
@@ -1,8 +1,10 @@
ABRA := ./cmd/abra
COMMIT := $(shell git rev-list -1 HEAD)
GOPATH := $(shell go env GOPATH)
LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
ABRA := ./cmd/abra
KADABRA := ./cmd/kadabra
COMMIT := $(shell git rev-list -1 HEAD)
GOPATH := $(shell go env GOPATH)
LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
DIST_LDFLAGS := $(LDFLAGS)" -s -w"
export GOPRIVATE=coopcloud.tech
all: format check build test
@@ -14,19 +16,22 @@ install:
@go install -ldflags=$(LDFLAGS) $(ABRA)
build-dev:
@go build -ldflags=$(LDFLAGS) $(ABRA)
@go build -v -ldflags=$(LDFLAGS) $(ABRA)
build:
@go build -ldflags=$(DIST_LDFLAGS) $(ABRA)
@go build -v -ldflags=$(DIST_LDFLAGS) $(ABRA)
@go build -v -ldflags=$(DIST_LDFLAGS) $(KADABRA)
clean:
@rm '$(GOPATH)/bin/abra'
@rm '$(GOPATH)/bin/kadabra'
format:
@gofmt -s -w .
check:
@test -z $$(gofmt -l .) || (echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1)
@test -z $$(gofmt -l .) || \
(echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1)
test:
@go test ./... -cover -v
+3 -1
View File
@@ -16,6 +16,7 @@ import (
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
@@ -72,13 +73,14 @@ This single file can be used to restore your app. See "abra app restore" for mor
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
conf := runtime.New()
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
recipe, err := recipe.Get(app.Recipe)
recipe, err := recipe.Get(app.Recipe, conf)
if err != nil {
logrus.Fatal(err)
}
+6 -4
View File
@@ -12,6 +12,7 @@ import (
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
@@ -55,6 +56,7 @@ the logs.
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
conf := runtime.New()
cl, err := client.New(app.Server)
if err != nil {
@@ -71,14 +73,14 @@ the logs.
}
if !internal.Watch {
if err := checkErrors(c, cl, app); err != nil {
if err := checkErrors(c, cl, app, conf); err != nil {
logrus.Fatal(err)
}
return nil
}
for {
if err := checkErrors(c, cl, app); err != nil {
if err := checkErrors(c, cl, app, conf); err != nil {
logrus.Fatal(err)
}
time.Sleep(2 * time.Second)
@@ -88,8 +90,8 @@ the logs.
},
}
func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App) error {
recipe, err := recipe.Get(app.Recipe)
func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App, conf *runtime.Config) error {
recipe, err := recipe.Get(app.Recipe, conf)
if err != nil {
return err
}
+10 -4
View File
@@ -11,6 +11,7 @@ import (
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/runtime"
"coopcloud.tech/abra/pkg/service"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
@@ -20,12 +21,14 @@ import (
)
var logOpts = types.ContainerLogsOptions{
Details: false,
Follow: true,
ShowStderr: true,
ShowStdout: true,
Tail: "20",
Since: "",
Until: "",
Timestamps: true,
Follow: true,
Tail: "20",
Details: false,
}
// stackLogs lists logs for all stack services
@@ -74,18 +77,21 @@ var appLogsCommand = cli.Command{
Usage: "Tail app logs",
Flags: []cli.Flag{
internal.StdErrOnlyFlag,
internal.SinceLogsFlag,
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
app := internal.ValidateApp(c, runtime.WithEnsureRecipeExists(false))
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
logOpts.Since = internal.SinceLogs
serviceName := c.Args().Get(1)
if serviceName == "" {
logrus.Debugf("tailing logs for all %s services", app.Recipe)
+3 -1
View File
@@ -12,6 +12,7 @@ import (
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
@@ -55,6 +56,7 @@ Example:
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
conf := runtime.New()
cl, err := client.New(app.Server)
if err != nil {
@@ -77,7 +79,7 @@ Example:
}
}
recipe, err := recipe.Get(app.Recipe)
recipe, err := recipe.Get(app.Recipe, conf)
if err != nil {
logrus.Fatal(err)
}
+4 -2
View File
@@ -8,6 +8,7 @@ import (
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
@@ -49,14 +50,15 @@ recipes.
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName()
conf := runtime.New()
if !internal.Chaos {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
if err := recipe.EnsureUpToDate(app.Recipe, conf); err != nil {
logrus.Fatal(err)
}
}
r, err := recipe.Get(app.Recipe)
r, err := recipe.Get(app.Recipe, conf)
if err != nil {
logrus.Fatal(err)
}
+83 -7
View File
@@ -2,15 +2,82 @@ package app
import (
"context"
"fmt"
"time"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var prune bool
var pruneFlag = &cli.BoolFlag{
Name: "prune, p",
Destination: &prune,
Usage: "Prunes unused containers, networks, and dangling images for an app",
}
// pruneApp runs the equivalent of a "docker system prune" but only filtering
// against resources connected with the app deployment. It is not a system wide
// prune. Volumes are not pruned to avoid unwated data loss.
func pruneApp(c *cli.Context, cl *dockerClient.Client, app config.App) error {
stackName := app.StackName()
ctx := context.Background()
for {
logrus.Debugf("polling for %s stack, waiting to be undeployed...", stackName)
services, err := stack.GetStackServices(ctx, cl, stackName)
if err != nil {
return err
}
if len(services) == 0 {
logrus.Debugf("%s undeployed, moving on with pruning logic", stackName)
time.Sleep(time.Second) // give runtime more time to tear down related state
break
}
time.Sleep(time.Second)
}
pruneFilters := filters.NewArgs()
stackSearch := fmt.Sprintf("%s*", stackName)
pruneFilters.Add("label", stackSearch)
cr, err := cl.ContainersPrune(ctx, pruneFilters)
if err != nil {
return err
}
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
logrus.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed)
nr, err := cl.NetworksPrune(ctx, pruneFilters)
if err != nil {
return err
}
logrus.Infof("networks pruned: %d", len(nr.NetworksDeleted))
ir, err := cl.ImagesPrune(ctx, pruneFilters)
if err != nil {
return err
}
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
logrus.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed)
return nil
}
var appUndeployCommand = cli.Command{
Name: "undeploy",
Aliases: []string{"un"},
@@ -18,18 +85,22 @@ var appUndeployCommand = cli.Command{
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
pruneFlag,
},
Before: internal.SubCommandBefore,
Usage: "Undeploy an app",
Before: internal.SubCommandBefore,
Usage: "Undeploy an app",
BashComplete: autocomplete.AppNameComplete,
Description: `
This does not destroy any of the application data. However, you should remain
vigilant, as your swarm installation will consider any previously attached
volumes as eligiblef or pruning once undeployed.
This does not destroy any of the application data.
However, you should remain vigilant, as your swarm installation will consider
any previously attached volumes as eligible for pruning once undeployed.
Passing "-p/--prune" does not remove those volumes.
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName()
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
@@ -55,7 +126,12 @@ volumes as eligiblef or pruning once undeployed.
logrus.Fatal(err)
}
if prune {
if err := pruneApp(c, cl, app); err != nil {
logrus.Fatal(err)
}
}
return nil
},
BashComplete: autocomplete.AppNameComplete,
}
+4 -2
View File
@@ -10,6 +10,7 @@ import (
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
@@ -52,6 +53,7 @@ recipes.
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName()
conf := runtime.New()
cl, err := client.New(app.Server)
if err != nil {
@@ -59,12 +61,12 @@ recipes.
}
if !internal.Chaos {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
if err := recipe.EnsureUpToDate(app.Recipe, conf); err != nil {
logrus.Fatal(err)
}
}
r, err := recipe.Get(app.Recipe)
r, err := recipe.Get(app.Recipe, conf)
if err != nil {
logrus.Fatal(err)
}
+3 -1
View File
@@ -8,6 +8,7 @@ import (
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
@@ -48,6 +49,7 @@ version.
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName()
conf := runtime.New()
cl, err := client.New(app.Server)
if err != nil {
@@ -69,7 +71,7 @@ version.
logrus.Fatalf("%s is not deployed?", app.Name)
}
recipeMeta, err := recipe.GetRecipeMeta(app.Recipe)
recipeMeta, err := recipe.GetRecipeMeta(app.Recipe, conf)
if err != nil {
logrus.Fatal(err)
}
+6 -3
View File
@@ -13,6 +13,7 @@ import (
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
@@ -54,8 +55,10 @@ keys configured on your account.
ArgsUsage: "[<recipe>]",
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
conf := runtime.New()
if recipeName != "" {
internal.ValidateRecipe(c, true)
internal.ValidateRecipe(c)
}
if err := catalogue.EnsureUpToDate(); err != nil {
@@ -79,7 +82,7 @@ keys configured on your account.
if !internal.SkipUpdates {
logrus.Warn(logMsg)
if err := recipe.UpdateRepositories(repos, recipeName); err != nil {
if err := recipe.UpdateRepositories(repos, recipeName, conf); err != nil {
logrus.Fatal(err)
}
}
@@ -97,7 +100,7 @@ keys configured on your account.
continue
}
versions, err := recipe.GetRecipeVersions(recipeMeta.Name)
versions, err := recipe.GetRecipeVersions(recipeMeta.Name, conf)
if err != nil {
logrus.Warn(err)
}
+1
View File
@@ -189,6 +189,7 @@ func newAbraApp(version, commit string) *cli.App {
return nil
}
return app
}
+8
View File
@@ -328,6 +328,14 @@ var StdErrOnlyFlag = &cli.BoolFlag{
Destination: &StdErrOnly,
}
var SinceLogs string
var SinceLogsFlag = &cli.StringFlag{
Name: "since, S",
Value: "",
Usage: "tail logs since YYYY-MM-DDTHH:MM:SSZ",
Destination: &SinceLogs,
}
var DontWaitConverge bool
var DontWaitConvergeFlag = &cli.BoolFlag{
Name: "no-converge-checks, c",
+5 -3
View File
@@ -15,6 +15,7 @@ import (
"coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
@@ -24,6 +25,7 @@ import (
// DeployAction is the main command-line action for this package
func DeployAction(c *cli.Context) error {
app := ValidateApp(c)
conf := runtime.New()
cl, err := client.New(app.Server)
if err != nil {
@@ -31,12 +33,12 @@ func DeployAction(c *cli.Context) error {
}
if !Chaos {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
if err := recipe.EnsureUpToDate(app.Recipe, conf); err != nil {
logrus.Fatal(err)
}
}
r, err := recipe.Get(app.Recipe)
r, err := recipe.Get(app.Recipe, conf)
if err != nil {
logrus.Fatal(err)
}
@@ -83,7 +85,7 @@ func DeployAction(c *cli.Context) error {
}
version = formatter.SmallSHA(head.String())
logrus.Warn("no versions detected, using latest commit")
if err := recipe.EnsureLatest(app.Recipe); err != nil {
if err := recipe.EnsureLatest(app.Recipe, conf); err != nil {
logrus.Fatal(err)
}
}
+4 -2
View File
@@ -11,6 +11,7 @@ import (
"coopcloud.tech/abra/pkg/jsontable"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"coopcloud.tech/abra/pkg/secret"
"github.com/AlecAivazis/survey/v2"
dockerClient "github.com/docker/docker/client"
@@ -119,9 +120,10 @@ func ensureServerFlag() error {
// NewAction is the new app creation logic
func NewAction(c *cli.Context) error {
recipe := ValidateRecipeWithPrompt(c, false)
recipe := ValidateRecipeWithPrompt(c, runtime.WithEnsureRecipeLatest(false))
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
conf := runtime.New(runtime.WithEnsureRecipeLatest(false))
if err := recipePkg.EnsureUpToDate(recipe.Name, conf); err != nil {
logrus.Fatal(err)
}
+14 -14
View File
@@ -9,6 +9,7 @@ import (
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
@@ -18,14 +19,15 @@ import (
var AppName string
// ValidateRecipe ensures the recipe arg is valid.
func ValidateRecipe(c *cli.Context, ensureLatest bool) recipe.Recipe {
func ValidateRecipe(c *cli.Context, opts ...runtime.Option) recipe.Recipe {
recipeName := c.Args().First()
conf := runtime.New(opts...)
if recipeName == "" {
ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
}
chosenRecipe, err := recipe.Get(recipeName)
chosenRecipe, err := recipe.Get(recipeName, conf)
if err != nil {
if c.Command.Name == "generate" {
if strings.Contains(err.Error(), "missing a compose") {
@@ -40,10 +42,8 @@ func ValidateRecipe(c *cli.Context, ensureLatest bool) recipe.Recipe {
}
}
if ensureLatest {
if err := recipe.EnsureLatest(recipeName); err != nil {
logrus.Fatal(err)
}
if err := recipe.EnsureLatest(recipeName, conf); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("validated %s as recipe argument", recipeName)
@@ -53,8 +53,9 @@ func ValidateRecipe(c *cli.Context, ensureLatest bool) recipe.Recipe {
// ValidateRecipeWithPrompt ensures a recipe argument is present before
// validating, asking for input if required.
func ValidateRecipeWithPrompt(c *cli.Context, ensureLatest bool) recipe.Recipe {
func ValidateRecipeWithPrompt(c *cli.Context, opts ...runtime.Option) recipe.Recipe {
recipeName := c.Args().First()
conf := runtime.New(opts...)
if recipeName == "" && !NoInput {
var recipes []string
@@ -102,15 +103,13 @@ func ValidateRecipeWithPrompt(c *cli.Context, ensureLatest bool) recipe.Recipe {
ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
}
chosenRecipe, err := recipe.Get(recipeName)
chosenRecipe, err := recipe.Get(recipeName, conf)
if err != nil {
logrus.Fatal(err)
}
if ensureLatest {
if err := recipe.EnsureLatest(recipeName); err != nil {
logrus.Fatal(err)
}
if err := recipe.EnsureLatest(recipeName, conf); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("validated %s as recipe argument", recipeName)
@@ -119,8 +118,9 @@ func ValidateRecipeWithPrompt(c *cli.Context, ensureLatest bool) recipe.Recipe {
}
// ValidateApp ensures the app name arg is valid.
func ValidateApp(c *cli.Context) config.App {
func ValidateApp(c *cli.Context, opts ...runtime.Option) config.App {
appName := c.Args().First()
conf := runtime.New(opts...)
if AppName != "" {
appName = AppName
@@ -136,7 +136,7 @@ func ValidateApp(c *cli.Context) config.App {
logrus.Fatal(err)
}
if err := recipe.EnsureExists(app.Recipe); err != nil {
if err := recipe.EnsureExists(app.Recipe, conf); err != nil {
logrus.Fatal(err)
}
+5 -2
View File
@@ -4,6 +4,7 @@ import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@@ -21,8 +22,10 @@ var recipeFetchCommand = cli.Command{
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
conf := runtime.New()
if recipeName != "" {
internal.ValidateRecipe(c, true)
internal.ValidateRecipe(c)
return nil // ValidateRecipe ensures latest checkout
}
@@ -31,7 +34,7 @@ var recipeFetchCommand = cli.Command{
logrus.Fatal(err)
}
if err := recipe.UpdateRepositories(repos, recipeName); err != nil {
if err := recipe.UpdateRepositories(repos, recipeName, conf); err != nil {
logrus.Fatal(err)
}
+4 -2
View File
@@ -8,6 +8,7 @@ import (
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@@ -24,9 +25,10 @@ var recipeLintCommand = cli.Command{
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c, true)
recipe := internal.ValidateRecipe(c)
conf := runtime.New()
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
if err := recipePkg.EnsureUpToDate(recipe.Name, conf); err != nil {
logrus.Fatal(err)
}
+2 -1
View File
@@ -13,6 +13,7 @@ import (
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference"
@@ -58,7 +59,7 @@ your SSH keys configured on your account.
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeWithPrompt(c, false)
recipe := internal.ValidateRecipeWithPrompt(c, runtime.WithEnsureRecipeLatest(false))
imagesTmp, err := getImageVersions(recipe)
if err != nil {
+2 -1
View File
@@ -8,6 +8,7 @@ import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/runtime"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/go-git/go-git/v5"
@@ -41,7 +42,7 @@ auto-generate it for you. The <recipe> configuration will be updated on the
local file system.
`,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeWithPrompt(c, false)
recipe := internal.ValidateRecipeWithPrompt(c, runtime.WithEnsureRecipeLatest(false))
mainApp, err := internal.GetMainAppImage(recipe)
if err != nil {
+4 -2
View File
@@ -14,6 +14,7 @@ import (
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference"
@@ -59,9 +60,10 @@ You may invoke this command in "wizard" mode and be prompted for input:
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeWithPrompt(c, true)
recipe := internal.ValidateRecipeWithPrompt(c)
conf := runtime.New()
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
if err := recipePkg.EnsureUpToDate(recipe.Name, conf); err != nil {
logrus.Fatal(err)
}
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@@ -20,7 +21,7 @@ var recipeVersionCommand = cli.Command{
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c, false)
recipe := internal.ValidateRecipe(c, runtime.WithEnsureRecipeLatest(false))
catalogue, err := recipePkg.ReadRecipeCatalogue()
if err != nil {
+11 -5
View File
@@ -39,11 +39,7 @@ var serverListCommand = cli.Command{
tableColumns := []string{"name", "host", "user", "port"}
table := formatter.CreateTable(tableColumns)
if internal.MachineReadable {
defer table.JSONRender()
} else {
defer table.Render()
}
serverNames, err := config.ReadServerNames()
if err != nil {
logrus.Fatal(err)
@@ -84,6 +80,16 @@ var serverListCommand = cli.Command{
}
}
if internal.MachineReadable {
table.JSONRender()
} else {
if problemsFilter && table.NumLines() == 0 {
logrus.Info("all servers wired up correctly 👏")
} else {
table.Render()
}
}
return nil
},
}
+100
View File
@@ -0,0 +1,100 @@
package server
import (
"context"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var allFilter bool
var allFilterFlag = &cli.BoolFlag{
Name: "all, a",
Usage: "Remove all unused images not just dangling ones",
Destination: &allFilter,
}
var volunesFilter bool
var volumesFilterFlag = &cli.BoolFlag{
Name: "volumes, v",
Usage: "Prune volumes. This will remove app data, Be Careful!",
Destination: &volunesFilter,
}
var serverPruneCommand = cli.Command{
Name: "prune",
Aliases: []string{"p"},
Usage: "Prune a managed server; Runs a docker system prune",
Description: `
Prunes unused containers, networks, and dangling images.
If passing "-v/--volumes" then volumes not connected with a deployed app will
also be removed. This can result in unwanted data loss if not used carefully.
`,
ArgsUsage: "[<server>]",
Flags: []cli.Flag{
allFilterFlag,
volumesFilterFlag,
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.ServerNameComplete,
Action: func(c *cli.Context) error {
var args filters.Args
serverName := internal.ValidateServer(c)
cl, err := client.New(serverName)
if err != nil {
logrus.Fatal(err)
}
ctx := context.Background()
cr, err := cl.ContainersPrune(ctx, args)
if err != nil {
logrus.Fatal(err)
}
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
logrus.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed)
nr, err := cl.NetworksPrune(ctx, args)
if err != nil {
logrus.Fatal(err)
}
logrus.Infof("networks pruned: %d", len(nr.NetworksDeleted))
pruneFilters := filters.NewArgs()
if allFilter {
pruneFilters.Add("dangling", "false")
}
ir, err := cl.ImagesPrune(ctx, pruneFilters)
if err != nil {
logrus.Fatal(err)
}
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
logrus.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed)
if volunesFilter {
vr, err := cl.VolumesPrune(ctx, args)
if err != nil {
logrus.Fatal(err)
}
volSpaceReclaimed := formatter.ByteCountSI(vr.SpaceReclaimed)
logrus.Infof("volumes pruned: %d; space reclaimed: %s", len(vr.VolumesDeleted), volSpaceReclaimed)
}
return nil
},
}
+3 -1
View File
@@ -7,6 +7,7 @@ import (
"path/filepath"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
@@ -124,7 +125,8 @@ like tears in rain.
internal.HetznerCloudNameFlag,
internal.HetznerCloudAPITokenFlag,
},
Before: internal.SubCommandBefore,
Before: internal.SubCommandBefore,
BashComplete: autocomplete.ServerNameComplete,
Action: func(c *cli.Context) error {
serverName := internal.ValidateServer(c)
+1
View File
@@ -22,5 +22,6 @@ recipes, see available flags on "abra server add" for more.
serverAddCommand,
serverListCommand,
serverRemoveCommand,
serverPruneCommand,
},
}
+498
View File
@@ -0,0 +1,498 @@
package updater
import (
"context"
"fmt"
"os"
"strconv"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"coopcloud.tech/abra/pkg/upstream/convert"
"coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerclient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
const SERVER = "localhost"
var majorUpdate bool
var majorFlag = &cli.BoolFlag{
Name: "major, m",
Usage: "Also check for major updates",
Destination: &majorUpdate,
}
var updateAll bool
var allFlag = &cli.BoolFlag{
Name: "all, a",
Usage: "Update all deployed apps",
Destination: &updateAll,
}
// Notify checks for available upgrades
var Notify = cli.Command{
Name: "notify",
Aliases: []string{"n"},
Usage: "Check for available upgrades",
Flags: []cli.Flag{
internal.DebugFlag,
majorFlag,
},
Before: internal.SubCommandBefore,
Description: `
It reads the deployed app versions and looks for new versions in the recipe
catalogue. If a new patch/minor version is available, a notification is
printed. To include major versions use the --major flag.
`,
Action: func(c *cli.Context) error {
cl, err := client.New("default")
if err != nil {
logrus.Fatal(err)
}
stacks, err := stack.GetStacks(cl)
if err != nil {
logrus.Fatal(err)
}
for _, stackInfo := range stacks {
stackName := stackInfo.Name
recipeName, err := getLabel(cl, stackName, "recipe")
if err != nil {
logrus.Fatal(err)
}
if recipeName != "" {
_, err = getLatestUpgrade(cl, stackName, recipeName)
if err != nil {
logrus.Fatal(err)
}
}
}
return nil
},
}
// UpgradeApp upgrades apps.
var UpgradeApp = cli.Command{
Name: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade apps",
ArgsUsage: "<stack-name> <recipe>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.ChaosFlag,
majorFlag,
allFlag,
},
Before: internal.SubCommandBefore,
Description: `
Upgrade an app by specifying its stack name and recipe. By passing "--all"
instead, every deployed app is upgraded. For each apps with enabled auto
updates the deployed version is compared with the current recipe catalogue
version. If a new patch/minor version is available, the app is upgraded. To
include major versions use the "--major" flag. Don't do that, it will probably
break things. Only apps that are not deployed with "--chaos" are upgraded, to
update chaos deployments use the "--chaos" flag. Use it with care.
`,
Action: func(c *cli.Context) error {
cl, err := client.New("default")
if err != nil {
logrus.Fatal(err)
}
conf := runtime.New()
if !updateAll {
stackName := c.Args().Get(0)
recipeName := c.Args().Get(1)
err = tryUpgrade(cl, stackName, recipeName, conf)
if err != nil {
logrus.Fatal(err)
}
return nil
}
stacks, err := stack.GetStacks(cl)
if err != nil {
logrus.Fatal(err)
}
for _, stackInfo := range stacks {
stackName := stackInfo.Name
recipeName, err := getLabel(cl, stackName, "recipe")
if err != nil {
logrus.Fatal(err)
}
err = tryUpgrade(cl, stackName, recipeName, conf)
if err != nil {
logrus.Fatal(err)
}
}
return nil
},
}
// getLabel reads docker labels in the format of "coop-cloud.${STACK_NAME}.${LABEL}".
func getLabel(cl *dockerclient.Client, stackName string, label string) (string, error) {
filter := filters.NewArgs()
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter})
if err != nil {
return "", err
}
for _, service := range services {
labelKey := fmt.Sprintf("coop-cloud.%s.%s", stackName, label)
if labelValue, ok := service.Spec.Labels[labelKey]; ok {
return labelValue, nil
}
}
logrus.Debugf("no %s label found for %s", label, stackName)
return "", nil
}
// getBoolLabel reads a boolean docker label.
func getBoolLabel(cl *dockerclient.Client, stackName string, label string) (bool, error) {
lableValue, err := getLabel(cl, stackName, label)
if err != nil {
return false, err
}
if lableValue != "" {
value, err := strconv.ParseBool(lableValue)
if err != nil {
return false, err
}
return value, nil
}
logrus.Debugf("Boolean label %s could not be found for %s, set default to false.", label, stackName)
return false, nil
}
// getEnv reads env variables from docker services.
func getEnv(cl *dockerclient.Client, stackName string) (config.AppEnv, error) {
envMap := make(map[string]string)
filter := filters.NewArgs()
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter})
if err != nil {
return nil, err
}
for _, service := range services {
envList := service.Spec.TaskTemplate.ContainerSpec.Env
for _, envString := range envList {
splitString := strings.SplitN(envString, "=", 2)
if len(splitString) != 2 {
logrus.Debugf("can't separate key from value: %s (this variable is probably unset)", envString)
continue
}
k := splitString[0]
v := splitString[1]
logrus.Debugf("For %s read env %s with value: %s from docker service", stackName, k, v)
envMap[k] = v
}
}
return envMap, nil
}
// getLatestUpgrade returns the latest available version for an app respecting
// the "--major" flag if it is newer than the currently deployed version.
func getLatestUpgrade(cl *dockerclient.Client, stackName string, recipeName string) (string, error) {
deployedVersion, err := getDeployedVersion(cl, stackName, recipeName)
if err != nil {
return "", err
}
availableUpgrades, err := getAvailableUpgrades(cl, stackName, recipeName, deployedVersion)
if err != nil {
return "", err
}
if len(availableUpgrades) == 0 {
logrus.Debugf("no available upgrades for %s", stackName)
return "", nil
}
var chosenUpgrade string
if len(availableUpgrades) > 0 {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
logrus.Infof("%s (%s) can be upgraded from version %s to %s", stackName, recipeName, deployedVersion, chosenUpgrade)
}
return chosenUpgrade, nil
}
// getDeployedVersion returns the currently deployed version of an app.
func getDeployedVersion(cl *dockerclient.Client, stackName string, recipeName string) (string, error) {
logrus.Debugf("Retrieve deployed version whether %s is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
return "", err
}
if !isDeployed {
return "", fmt.Errorf("%s is not deployed?", stackName)
}
if deployedVersion == "unknown" {
return "", fmt.Errorf("failed to determine deployed version of %s", stackName)
}
return deployedVersion, nil
}
// getAvailableUpgrades returns all available versions of an app that are newer
// than the deployed version. It only includes major upgrades if the "--major"
// flag is set.
func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName string,
deployedVersion string) ([]string, error) {
catl, err := recipe.ReadRecipeCatalogue()
if err != nil {
return nil, err
}
versions, err := recipe.GetRecipeCatalogueVersions(recipeName, catl)
if err != nil {
return nil, err
}
if len(versions) == 0 {
logrus.Warnf("no published releases for %s in the recipe catalogue?", recipeName)
return nil, nil
}
var availableUpgrades []string
for _, version := range versions {
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
if err != nil {
return nil, err
}
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
return nil, err
}
versionDelta, err := parsedDeployedVersion.UpgradeDelta(parsedVersion)
if err != nil {
return nil, err
}
if 0 < versionDelta.UpgradeType() && (versionDelta.UpgradeType() < 4 || majorUpdate) {
availableUpgrades = append(availableUpgrades, version)
}
}
logrus.Debugf("Available updates for %s: %s", stackName, availableUpgrades)
return availableUpgrades, nil
}
// processRecipeRepoVersion clones, pulls, checks out the version and lints the
// recipe repository.
func processRecipeRepoVersion(recipeName, version string, conf *runtime.Config) error {
if err := recipe.EnsureExists(recipeName, conf); err != nil {
return err
}
if err := recipe.EnsureUpToDate(recipeName, conf); err != nil {
return err
}
if err := recipe.EnsureVersion(recipeName, version); err != nil {
return err
}
if r, err := recipe.Get(recipeName, conf); err != nil {
return err
} else if err := lint.LintForErrors(r); err != nil {
return err
}
return nil
}
// mergeAbraShEnv merges abra.sh env vars into the app env vars.
func mergeAbraShEnv(recipeName string, env config.AppEnv) error {
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, recipeName, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
return err
}
for k, v := range abraShEnv {
logrus.Debugf("read v:%s k: %s", v, k)
env[k] = v
}
return nil
}
// createDeployConfig merges and enriches the compose config for the deployment.
func createDeployConfig(recipeName string, stackName string, env config.AppEnv) (*composetypes.Config, stack.Deploy, error) {
env["STACK_NAME"] = stackName
deployOpts := stack.Deploy{
Namespace: stackName,
Prune: false,
ResolveImage: stack.ResolveImageAlways,
}
composeFiles, err := config.GetAppComposeFiles(recipeName, env)
if err != nil {
return nil, deployOpts, err
}
deployOpts.Composefiles = composeFiles
compose, err := config.GetAppComposeConfig(stackName, deployOpts, env)
if err != nil {
return nil, deployOpts, err
}
config.ExposeAllEnv(stackName, compose, env)
// after the upgrade the deployment won't be in chaos state anymore
config.SetChaosLabel(compose, stackName, false)
config.SetRecipeLabel(compose, stackName, recipeName)
config.SetUpdateLabel(compose, stackName, env)
return compose, deployOpts, nil
}
// tryUpgrade performs the upgrade if all the requirements are fulfilled.
func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string, conf *runtime.Config) error {
if recipeName == "" {
logrus.Debugf("Don't update %s due to missing recipe name", stackName)
return nil
}
chaos, err := getBoolLabel(cl, stackName, "chaos")
if err != nil {
return err
}
if chaos && !internal.Chaos {
logrus.Debugf("Don't update %s due to chaos deployment.", stackName)
return nil
}
updatesEnabled, err := getBoolLabel(cl, stackName, "autoupdate")
if err != nil {
return err
}
if !updatesEnabled {
logrus.Debugf("Don't update %s due to disabling auto updates or missing ENABLE_AUTOUPDATE env.", stackName)
return nil
}
upgradeVersion, err := getLatestUpgrade(cl, stackName, recipeName)
if err != nil {
return err
}
if upgradeVersion == "" {
logrus.Debugf("Don't update %s due to no new version.", stackName)
return nil
}
err = upgrade(cl, stackName, recipeName, upgradeVersion, conf)
return err
}
// upgrade performs all necessary steps to upgrade an app.
func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion string, conf *runtime.Config) error {
env, err := getEnv(cl, stackName)
if err != nil {
return err
}
app := config.App{
Name: stackName,
Recipe: recipeName,
Server: SERVER,
Env: env,
}
if err = processRecipeRepoVersion(recipeName, upgradeVersion, conf); err != nil {
return err
}
if err = mergeAbraShEnv(recipeName, app.Env); err != nil {
return err
}
compose, deployOpts, err := createDeployConfig(recipeName, stackName, app.Env)
if err != nil {
return err
}
logrus.Infof("Upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion)
err = stack.RunDeploy(cl, deployOpts, compose, stackName, true)
return err
}
func newAbraApp(version, commit string) *cli.App {
app := &cli.App{
Name: "kadabra",
Usage: `The Co-op Cloud auto-updater
____ ____ _ _
/ ___|___ ___ _ __ / ___| | ___ _ _ __| |
| | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |
| |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |
\____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|
|_|
`,
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Commands: []cli.Command{
Notify,
UpgradeApp,
},
}
app.Before = func(c *cli.Context) error {
logrus.Debugf("kadabra version %s, commit %s", version, commit)
return nil
}
return app
}
// RunApp runs CLI abra app.
func RunApp(version, commit string) {
app := newAbraApp(version, commit)
if err := app.Run(os.Args); err != nil {
logrus.Fatal(err)
}
}
+2 -2
View File
@@ -5,10 +5,10 @@ import (
"coopcloud.tech/abra/cli"
)
// Version is the current version of Abra
// Version is the current version of Abra.
var Version string
// Commit is the current git commit of Abra
// Commit is the current git commit of Abra.
var Commit string
func main() {
+23
View File
@@ -0,0 +1,23 @@
// Package main provides the command-line entrypoint.
package main
import (
"coopcloud.tech/abra/cli/updater"
)
// Version is the current version of Kadabra.
var Version string
// Commit is the current git commit of Kadabra.
var Commit string
func main() {
if Version == "" {
Version = "dev"
}
if Commit == "" {
Commit = " "
}
updater.RunApp(Version, Commit)
}
+3 -3
View File
@@ -11,8 +11,8 @@ require (
github.com/docker/distribution v2.8.1+incompatible
github.com/docker/docker v20.10.23+incompatible
github.com/docker/go-units v0.5.0
github.com/go-git/go-git/v5 v5.5.2
github.com/hetznercloud/hcloud-go v1.39.0
github.com/go-git/go-git/v5 v5.6.0
github.com/hetznercloud/hcloud-go v1.40.0
github.com/moby/sys/signal v0.7.0
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
github.com/olekukonko/tablewriter v0.0.5
@@ -46,5 +46,5 @@ require (
github.com/urfave/cli v1.22.9
github.com/xeipuuv/gojsonpointer v0.0.0-20190809123943-df4f5c81cb3b // indirect
golang.org/x/crypto v0.5.0 // indirect
golang.org/x/sys v0.5.0
golang.org/x/sys v0.6.0
)
+17 -8
View File
@@ -414,8 +414,8 @@ github.com/go-git/go-billy/v5 v5.4.0 h1:Vaw7LaSTRJOUric7pe4vnzBSgyuf2KrLsu2Y4ZpQ
github.com/go-git/go-billy/v5 v5.4.0/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
github.com/go-git/go-git-fixtures/v4 v4.3.1 h1:y5z6dd3qi8Hl+stezc8p3JxDkoTRqMAlKnXHuzrfjTQ=
github.com/go-git/go-git-fixtures/v4 v4.3.1/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
github.com/go-git/go-git/v5 v5.5.2 h1:v8lgZa5k9ylUw+OR/roJHTxR4QItsNFI5nKtAXFuynw=
github.com/go-git/go-git/v5 v5.5.2/go.mod h1:BE5hUJ5yaV2YMxhmaP4l6RBQ08kMxKSPD4BlxtH7OjI=
github.com/go-git/go-git/v5 v5.6.0 h1:JvBdYfcttd+0kdpuWO7KTu0FYgCf5W0t5VwkWGobaa4=
github.com/go-git/go-git/v5 v5.6.0/go.mod h1:6nmJ0tJ3N4noMV1Omv7rC5FG3/o8Cm51TB4CJp7mRmE=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -605,8 +605,8 @@ github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hetznercloud/hcloud-go v1.39.0 h1:RUlzI458nGnPR6dlcZlrsGXYC1hQlFbKdm8tVtEQQB0=
github.com/hetznercloud/hcloud-go v1.39.0/go.mod h1:mepQwR6va27S3UQthaEPGS86jtzSY9xWL1e9dyxXpgA=
github.com/hetznercloud/hcloud-go v1.40.0 h1:rchS+LVd80GuAOQUM5pLs0Fn7tb4PflPCuxskLYc1JQ=
github.com/hetznercloud/hcloud-go v1.40.0/go.mod h1:jcKomw7kDIjrJ9FI72S/RdRPK8X8arYzEnXjRM/uXT8=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@@ -744,6 +744,7 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/sys/mount v0.2.0 h1:WhCW5B355jtxndN5ovugJlMFJawbUODuW8fSnEH6SSM=
github.com/moby/sys/mount v0.2.0/go.mod h1:aAivFE2LB3W4bACsUXChRHQ0qKWsetY4Y9V7sxOougM=
@@ -833,8 +834,8 @@ github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrap
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pjbgf/sha1cd v0.2.3 h1:uKQP/7QOzNtKYH7UTohZLcjF5/55EnTw0jO/Ru4jZwI=
github.com/pjbgf/sha1cd v0.2.3/go.mod h1:HOK9QrgzdHpbc2Kzip0Q1yi3M2MFGPADtR6HjG65m5M=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -1045,6 +1046,7 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/arch v0.1.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -1069,6 +1071,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
@@ -1109,6 +1112,7 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1164,6 +1168,7 @@ golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
@@ -1310,16 +1315,18 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
@@ -1407,6 +1414,7 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1652,6 +1660,7 @@ k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAG
k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
+19 -3
View File
@@ -9,7 +9,7 @@ import (
"github.com/urfave/cli"
)
// AppNameComplete copletes app names
// AppNameComplete copletes app names.
func AppNameComplete(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
@@ -25,7 +25,7 @@ func AppNameComplete(c *cli.Context) {
}
}
// RecipeNameComplete completes recipe names
// RecipeNameComplete completes recipe names.
func RecipeNameComplete(c *cli.Context) {
catl, err := recipe.ReadRecipeCatalogue()
if err != nil {
@@ -41,7 +41,23 @@ func RecipeNameComplete(c *cli.Context) {
}
}
// SubcommandComplete completes subcommands.
// ServerNameComplete completes server names.
func ServerNameComplete(c *cli.Context) {
files, err := config.LoadAppFiles("")
if err != nil {
logrus.Fatal(err)
}
if c.NArg() > 0 {
return
}
for _, appFile := range files {
fmt.Println(appFile.Server)
}
}
// SubcommandComplete completes sub-commands.
func SubcommandComplete(c *cli.Context) {
if c.NArg() > 0 {
return
+1 -1
View File
@@ -26,7 +26,7 @@ func New(serverName string) (*client.Client, error) {
if serverName != "default" {
context, err := GetContext(serverName)
if err != nil {
return nil, err
return nil, fmt.Errorf("unknown server, run \"abra server add %s\"?", serverName)
}
ctxEndpoint, err := contextPkg.GetContextEndpoint(context)
+2 -9
View File
@@ -173,7 +173,7 @@ func newApp(env AppEnv, name string, appFile AppFile) (App, error) {
if !exists {
recipe, exists = env["TYPE"]
if !exists {
return App{}, fmt.Errorf("%s is missing the RECIPE env var", name)
return App{}, fmt.Errorf("%s is missing the TYPE env var?", name)
}
}
@@ -369,13 +369,6 @@ func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]str
}
}
for server := range servers {
// validate that all server connections work
if _, err := client.New(server); err != nil {
return statuses, err
}
}
var bar *progressbar.ProgressBar
if !MachineReadable {
bar = formatter.CreateProgressbar(len(servers), "querying remote servers...")
@@ -499,7 +492,7 @@ func SetRecipeLabel(compose *composetypes.Config, stackName string, recipe strin
func SetChaosLabel(compose *composetypes.Config, stackName string, chaos bool) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("set label 'coop-cloud.%s.chaos' to %s for %s", stackName, chaos, stackName)
logrus.Debugf("set label 'coop-cloud.%s.chaos' to %v for %s", stackName, chaos, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", stackName)
service.Deploy.Labels[labelKey] = strconv.FormatBool(chaos)
}
+1 -1
View File
@@ -54,7 +54,7 @@ func TestGetAllFoldersInDirectory(t *testing.T) {
}
func TestGetAllFilesInDirectory(t *testing.T) {
files, err := getAllFilesInDirectory(testFolder)
files, err := GetAllFilesInDirectory(testFolder)
if err != nil {
t.Fatal(err)
}
+19
View File
@@ -1,6 +1,7 @@
package formatter
import (
"fmt"
"os"
"strings"
"time"
@@ -70,3 +71,21 @@ func StripTagMeta(image string) string {
return image
}
// ByteCountSI presents a human friendly representation of a byte count. See
// https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format.
func ByteCountSI(b uint64) string {
const unit = 1000
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := uint64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
}
+4 -4
View File
@@ -2,8 +2,9 @@ package jsontable
import (
"fmt"
"github.com/olekukonko/tablewriter"
"io"
"github.com/olekukonko/tablewriter"
)
// A quick-and-dirty proxy/emulator of tablewriter to enable more easy machine readable output
@@ -184,8 +185,7 @@ func (t *JSONTable) AppendBulk(rows [][]string) {
}
}
/// Stuff we should implement but we just proxy for now.
// Stuff we should implement but we just proxy for now.
func (t *JSONTable) SetAutoMergeCellsByColumnIndex(cols []int) {
// FIXME
t.tbl.SetAutoMergeCellsByColumnIndex(cols)
@@ -196,7 +196,7 @@ func (t *JSONTable) SetAutoMergeCells(auto bool) {
t.tbl.SetAutoMergeCells(auto)
}
//// Stub functions
// Stub functions
func (t *JSONTable) SetAutoWrapText(auto bool) {
t.tbl.SetAutoWrapText(auto)
return
+4 -4
View File
@@ -13,7 +13,7 @@ var TestLine = []string{"1", "2"}
var TestGroup = [][]string{{"1", "2", "3"}, {"a", "teohunteohu", "c", "d"}, {"☺", "☹"}}
var TestKeys = []string{"key0", "key1", "key2"}
/// test creation
// test creation
func TestNewTable(t *testing.T) {
var b bytes.Buffer
tbl := NewJSONTable(&b)
@@ -22,7 +22,7 @@ func TestNewTable(t *testing.T) {
}
}
/// test adding things
// test adding things
func TestTableAdd(t *testing.T) {
var b bytes.Buffer
tbl := NewJSONTable(&b)
@@ -39,7 +39,7 @@ func TestTableAdd(t *testing.T) {
}
}
/// test JSON output is parsable
// test JSON output is parsable
func TestJsonParsable(t *testing.T) {
var b bytes.Buffer
tbl := NewJSONTable(&b)
@@ -58,7 +58,7 @@ func TestJsonParsable(t *testing.T) {
}
}
/// test identical commands to a tablewriter and jsontable produce the same rendered output
// test identical commands to a tablewriter and jsontable produce the same rendered output
func TestTableWriter(t *testing.T) {
var bjson bytes.Buffer
var btable bytes.Buffer
+27 -16
View File
@@ -18,6 +18,7 @@ import (
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/limit"
"coopcloud.tech/abra/pkg/runtime"
"coopcloud.tech/abra/pkg/upstream/stack"
loader "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/abra/pkg/web"
@@ -206,8 +207,8 @@ func (r Recipe) Tags() ([]string, error) {
}
// Get retrieves a recipe.
func Get(recipeName string) (Recipe, error) {
if err := EnsureExists(recipeName); err != nil {
func Get(recipeName string, conf *runtime.Config) (Recipe, error) {
if err := EnsureExists(recipeName, conf); err != nil {
return Recipe{}, err
}
@@ -233,7 +234,7 @@ func Get(recipeName string) (Recipe, error) {
return Recipe{}, err
}
meta, err := GetRecipeMeta(recipeName)
meta, err := GetRecipeMeta(recipeName, conf)
if err != nil {
if strings.Contains(err.Error(), "does not exist") {
meta = RecipeMeta{}
@@ -250,7 +251,11 @@ func Get(recipeName string) (Recipe, error) {
}
// EnsureExists ensures that a recipe is locally cloned
func EnsureExists(recipeName string) error {
func EnsureExists(recipeName string, conf *runtime.Config) error {
if !conf.EnsureRecipeExists {
return nil
}
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
if _, err := os.Stat(recipeDir); os.IsNotExist(err) {
@@ -310,8 +315,7 @@ func EnsureVersion(recipeName, version string) error {
logrus.Debugf("read %s as tags for recipe %s", strings.Join(parsedTags, ", "), recipeName)
if tagRef.String() == "" {
logrus.Warnf("no published release discovered for %s", recipeName)
return nil
return fmt.Errorf("no published release discovered for %s", recipeName)
}
worktree, err := repo.Worktree()
@@ -334,7 +338,11 @@ func EnsureVersion(recipeName, version string) error {
}
// EnsureLatest makes sure the latest commit is checked out for a local recipe repository
func EnsureLatest(recipeName string) error {
func EnsureLatest(recipeName string, conf *runtime.Config) error {
if !conf.EnsureRecipeLatest {
return nil
}
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
isClean, err := gitPkg.IsClean(recipeDir)
@@ -362,7 +370,7 @@ func EnsureLatest(recipeName string) error {
return err
}
meta, err := GetRecipeMeta(recipeName)
meta, err := GetRecipeMeta(recipeName, conf)
if err != nil {
return err
}
@@ -579,7 +587,11 @@ func GetStringInBetween(recipeName, str, start, end string) (result string, err
}
// EnsureUpToDate ensures that the local repo is synced to the remote
func EnsureUpToDate(recipeName string) error {
func EnsureUpToDate(recipeName string, conf *runtime.Config) error {
if !conf.EnsureRecipeLatest {
return nil
}
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
isClean, err := gitPkg.IsClean(recipeDir)
@@ -791,7 +803,7 @@ func VersionsOfService(recipe, serviceName string) ([]string, error) {
}
// GetRecipeMeta retrieves the recipe metadata from the recipe catalogue.
func GetRecipeMeta(recipeName string) (RecipeMeta, error) {
func GetRecipeMeta(recipeName string, conf *runtime.Config) (RecipeMeta, error) {
catl, err := ReadRecipeCatalogue()
if err != nil {
return RecipeMeta{}, err
@@ -802,7 +814,7 @@ func GetRecipeMeta(recipeName string) (RecipeMeta, error) {
return RecipeMeta{}, fmt.Errorf("recipe %s does not exist?", recipeName)
}
if err := EnsureExists(recipeName); err != nil {
if err := EnsureExists(recipeName, conf); err != nil {
return RecipeMeta{}, err
}
@@ -924,9 +936,8 @@ func ReadReposMetadata() (RepoCatalogue, error) {
}
// GetRecipeVersions retrieves all recipe versions.
func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
func GetRecipeVersions(recipeName string, conf *runtime.Config) (RecipeVersions, error) {
versions := RecipeVersions{}
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
logrus.Debugf("attempting to open git repository in %s", recipeDir)
@@ -963,7 +974,7 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
logrus.Debugf("successfully checked out %s in %s", ref.Name(), recipeDir)
recipe, err := Get(recipeName)
recipe, err := Get(recipeName, conf)
if err != nil {
return err
}
@@ -1028,7 +1039,7 @@ func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]stri
}
// UpdateRepositories clones and updates all recipe repositories locally.
func UpdateRepositories(repos RepoCatalogue, recipeName string) error {
func UpdateRepositories(repos RepoCatalogue, recipeName string, conf *runtime.Config) error {
var barLength int
if recipeName != "" {
barLength = 1
@@ -1062,7 +1073,7 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string) error {
logrus.Fatal(err)
}
if err := EnsureUpToDate(rm.Name); err != nil {
if err := EnsureUpToDate(rm.Name, conf); err != nil {
logrus.Fatal(err)
}
+61
View File
@@ -0,0 +1,61 @@
package runtime
import "github.com/sirupsen/logrus"
// Config is an internal configuration modifier. It can be instantiated on a
// command call and can be changed on the fly to help make decisions further
// down in the internals, e.g. whether or not to clone the recipe locally or
// not.
type Config struct {
EnsureRecipeExists bool // ensure that the recipe is cloned locally
EnsureRecipeLatest bool // ensure the local recipe has latest changes
}
// Option modified a Config. The convetion for passing Options to functions is
// so far, only used in internal/validate.go. A Config is then constructed and
// passed down further into the code. This may change in the future but this is
// at least the abstraction so far.
type Option func(c *Config)
// New instantiates a new Config.
func New(opts ...Option) *Config {
conf := &Config{
EnsureRecipeExists: true,
}
for _, optFunc := range opts {
optFunc(conf)
}
return conf
}
// WithEnsureRecipeExists determines whether or not we should be cloning the
// local recipe or not. This can be useful for being more adaptable to offline
// scenarios.
func WithEnsureRecipeExists(ensureRecipeExists bool) Option {
return func(c *Config) {
if ensureRecipeExists {
logrus.Debugf("runtime config: EnsureRecipeExists = %v, ensuring recipes are cloned", ensureRecipeExists)
} else {
logrus.Debugf("runtime config: EnsureRecipeExists = %v, not cloning recipes", ensureRecipeExists)
}
c.EnsureRecipeExists = ensureRecipeExists
}
}
// WithEnsureRecipeLatest determines whether we should update the local recipes
// remotely via Git. This can be useful when e.g. ensuring we have the latest
// changes before making new ones.
func WithEnsureRecipeLatest(ensureRecipeLatest bool) Option {
return func(c *Config) {
if ensureRecipeLatest {
logrus.Debugf("runtime config: EnsureRecipeLatest = %v, ensuring recipes have latest changes", ensureRecipeLatest)
} else {
logrus.Debugf("runtime config: EnsureRecipeLatest = %v, leaving recipes alone", ensureRecipeLatest)
}
c.EnsureRecipeLatest = ensureRecipeLatest
}
}
+2
View File
@@ -77,6 +77,8 @@ func Fatal(hostname string, err error) error {
return fmt.Errorf("connection timed out for %s", hostname)
} else if strings.Contains(out, "Permission denied") {
return fmt.Errorf("ssh auth: permission denied for %s", hostname)
} else if strings.Contains(out, "Network is unreachable") {
return fmt.Errorf("unable to connect to %s, network is unreachable?", hostname)
} else {
return err
}
+35
View File
@@ -10,6 +10,7 @@ import (
"coopcloud.tech/abra/pkg/upstream/convert"
"github.com/docker/cli/cli/command/service/progress"
"github.com/docker/cli/cli/command/stack/formatter"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
@@ -484,3 +485,37 @@ If a service is failing to even start, try smoke out the error with:
`, appName, timeout, appName, appName, appName))
}
}
// Copypasta from https://github.com/docker/cli/blob/master/cli/command/stack/swarm/list.go
// GetStacks lists the swarm stacks.
func GetStacks(cl *dockerClient.Client) ([]*formatter.Stack, error) {
services, err := cl.ServiceList(
context.Background(),
types.ServiceListOptions{Filters: getAllStacksFilter()})
if err != nil {
return nil, err
}
m := make(map[string]*formatter.Stack)
for _, service := range services {
labels := service.Spec.Labels
name, ok := labels[convert.LabelNamespace]
if !ok {
return nil, errors.Errorf("cannot get label %s for service %s",
convert.LabelNamespace, service.ID)
}
ztack, ok := m[name]
if !ok {
m[name] = &formatter.Stack{
Name: name,
Services: 1,
}
} else {
ztack.Services++
}
}
var stacks []*formatter.Stack
for _, stack := range m {
stacks = append(stacks, stack)
}
return stacks, nil
}
+2 -2
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
ABRA_VERSION="0.6.0-beta"
ABRA_VERSION="0.7.0-beta"
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION"
RC_VERSION="0.7.0-rc1-beta"
RC_VERSION="0.7.0-beta"
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION"
for arg in "$@"; do