Compare commits

...

131 Commits

Author SHA1 Message Date
d041deec68 Better logical layout for app ls -S 2023-11-09 11:14:20 -08:00
3939971d82 Fix ls -S to deal with broken/unparsable app versions more correctly 2023-11-09 10:50:40 -08:00
1d098eed0e Preliminary makefile support for building inside docker 2023-11-09 10:49:15 -08:00
74200318ab chore(deps): update module github.com/schollz/progressbar/v3 to v3.14.0 2023-11-07 08:01:11 +00:00
609656b4e1 chore(deps): update module golang.org/x/sys to v0.14.0 2023-11-06 08:00:33 +00:00
856c9f2f7d chore: go mod tidy 2023-11-04 09:37:15 +01:00
bd5cdd3443 chore(deps): update module github.com/docker/docker to v24.0.7 2023-10-30 08:00:53 +00:00
79d274e074 chore(deps): update module github.com/docker/cli to v24.0.7 2023-10-27 07:01:16 +00:00
51e3df17f1 chore(deps): update module github.com/go-git/go-git/v5 to v5.10.0 2023-10-26 07:00:33 +00:00
ccf0215495 hotfix: parse values starting with # correctly 2023-10-23 19:21:45 +02:00
254df7f2be feat: app cmd ls
See coop-cloud/organising#484
2023-10-17 21:16:31 +02:00
6a673ef101 refactor: filter by topic when building catalogue
See coop-cloud/organising#377
2023-10-16 18:42:38 +02:00
7f7f7224c6 feat: diff on release flow
Also, don't commit unstaged files.
2023-10-16 18:31:22 +02:00
f96bf9a8ac feat: recipe reset, recipe diff
See coop-cloud/organising#511
2023-10-15 12:56:52 +02:00
dcecf32999 chore: bump version for installer script [ci skip] 2023-10-11 19:31:28 +02:00
bc88dac150 test: reset before changing files 2023-10-11 19:29:19 +02:00
704c0e9c74 test: adapt failing tests to new changes 2023-10-11 18:34:08 +02:00
c9bb7e15c2 fix: bring back docker build 2023-10-10 07:27:49 +02:00
d90c9b88f1 fix: include ca-certs to avoid x509 error [ci skip] 2023-10-10 00:50:43 +02:00
69ce07f81f fix: ignore build files for docker [ci skip] 2023-10-09 23:40:41 +02:00
85b90ef80c fix: bail if --chaos and specific version
See coop-cloud/organising#503.
2023-10-09 20:54:44 +00:00
3e511446aa refactor: use app check emoji here too 2023-10-09 22:53:46 +02:00
7566b4262b fix: set go version to 1.21 2023-10-09 22:07:30 +02:00
c249c6ae9c fix: fix: trim comments that are not modifers
See coop-cloud/organising#505
2023-10-09 14:42:05 +02:00
be693e9df0 fix: trim comments that are not modifers
See coop-cloud/organising#505
2023-10-08 22:42:34 +02:00
a43125701c test: optimise default make target for abra hacking [ci skip] 2023-10-07 10:32:42 +02:00
b57edb440a fix: improve app check
See coop-cloud/organising#446
2023-10-06 10:56:33 +02:00
6fc4573a71 chore: go mod tidy 2023-10-06 09:49:03 +02:00
cbe6676881 chore(deps): update module golang.org/x/sys to v0.13.0 2023-10-06 07:00:49 +00:00
b4fd39828f test: abra-integration-test-recipe -> abra-test-recipe
See coop-cloud/abra-test-recipe#3
2023-10-05 14:22:11 +02:00
14f2d72aba refactor!: lowercase, hyphenate keys
This will potentially break scripts, so time to discuss!
2023-10-05 08:36:01 +02:00
57692ec3c9 feat: add --machine to secret ls
See coop-cloud/organising#481
2023-10-04 23:08:39 +02:00
47d3b77003 refactor: not generating here, skipping 2023-10-04 15:13:15 +02:00
8078e91e52 fix: warn if secrets not generated
See coop-cloud/organising#499
2023-10-04 15:13:14 +02:00
dc5d3a8dd6 test: build, init & test in one stage 2023-10-04 14:37:09 +02:00
ab6107610c test: skip build step, test will do it 2023-10-04 14:36:59 +02:00
e837835e00 test: remove duplicate call to EnsureCatalogue 2023-10-04 14:05:02 +02:00
c646263e9e fix: validate COMPOSE_FILE
See coop-cloud/organising#468.
See coop-cloud/organising#376.
2023-10-04 13:27:04 +02:00
422c642949 fix: ensure ipv4 is checked, not sometimes ipv6
See coop-cloud/organising#490
2023-10-04 09:29:10 +00:00
379915587c fix: don't export from within function
Also, don't explode on command function which has "export" in the name!

See coop-cloud/organising#498
2023-10-04 11:20:50 +02:00
970ae0fc4e test: use _test to avoid cyclic imports 2023-10-04 02:36:44 +02:00
d11ad61efb docs: make chaos flag description more generic [ci skip] 2023-10-04 01:34:53 +02:00
54dc696c69 build: fix targets for small local builds 2023-10-03 09:31:57 +02:00
7e3ce9c42a chore: go mod tidy 2023-10-03 09:30:26 +02:00
7751423c7d chore(deps): update module github.com/docker/distribution to v2.8.3 2023-10-03 07:00:43 +00:00
f18f0b6f82 build: set ABRA_DIR explicitly 2023-09-30 08:26:20 +02:00
892f6c0730 test: ensure catalogue is cloned 2023-09-30 08:19:16 +02:00
b53fd2689c test: add unit test for TestEnsureDomainsResolveSameIPv4 2023-09-30 08:19:02 +02:00
906bf65d47 test: moar domain check tests [ci skip] 2023-09-29 09:31:25 +02:00
1e6a6e6174 fix: app logs retrieves recipe 2023-09-27 09:19:57 +02:00
1e4f1b4ade build: disable publish image for now
It's failing for unknown reasons and block releases.

See coop-cloud/recipes-catalogue-json#6
2023-09-25 17:51:30 +02:00
306fe02d1c chore: tag 0.8.x series 2023-09-25 17:33:09 +02:00
e4610f8ad5 test: make int test script lighter [ci skip] 2023-09-25 16:45:08 +02:00
e1f900de14 test: fix app_secret generate tests [ci skip] 2023-09-25 16:32:16 +02:00
d5b18d74ef fix: use secretId to match secret names in configs 2023-09-25 15:51:15 +02:00
776a83d8d1 fix: use new GetComposeFiles API 2023-09-25 15:51:03 +02:00
810cea8269 test: bats does output for us [ci skip] 2023-09-25 12:14:35 +02:00
c0f3e6f2a4 test: integration test script [ci skip] 2023-09-25 12:00:39 +02:00
7b240059b0 test: fix app_backup recipe cleanups [ci skip] 2023-09-25 11:50:29 +02:00
c456d13881 test: fix recipe_* tests [ci skip] 2023-09-25 11:27:36 +02:00
c7c553164d test: fix refute output check [ci skip] 2023-09-25 11:21:36 +02:00
7616528f4e test: ensure app cleanup 2023-09-25 11:20:56 +02:00
6cd85f7239 test: dont assert_success for check [ci skip] 2023-09-25 11:11:29 +02:00
b1774cc44b test: fix app_check tests 2023-09-25 10:52:47 +02:00
e438fc6e8e test: reset recipe in file teardown 2023-09-25 10:52:27 +02:00
c065ceb1f0 test: secret generation & --offline/chaos handling tests 2023-09-25 10:33:15 +02:00
ce4b775428 build: require 1.18 due to slices.Contains usage 2023-09-25 10:32:41 +02:00
d02f659bf8 fix: secrets from config, --offline/chaos handling, typos
See coop-cloud/organising#464
2023-09-25 10:31:59 +02:00
f3ded88ed8 fix: app version includes tags, sorts & tests
See coop-cloud/organising#442
2023-09-24 11:19:27 +02:00
bf648eeb5d fix: recipe versions sorts, aligns & spaces 2023-09-24 11:18:26 +02:00
533edbf172 fix: recipe versions lists correctly (also -m) 2023-09-24 10:56:02 +02:00
78b8cf9725 test: fix git tag command [ci skip] 2023-09-24 00:56:00 +02:00
f0560ca975 test: no args for helpers, fix recipe_* tests [ci skip] 2023-09-23 23:57:52 +02:00
ce7b4733d7 test: tag/git helpers & refactor [ci skip] 2023-09-23 23:19:49 +02:00
575bfbb0fb test: test arguments, notes, local tag lookup 2023-09-23 09:17:24 +02:00
510ce66570 feat: version arguments, local tag lookups & release notes
See:
* coop-cloud/organising#441
* coop-cloud/organising#204
* coop-cloud/organising#493
2023-09-23 09:15:27 +02:00
82631d9ab1 fix: don't output if not tags 2023-09-23 09:15:17 +02:00
358490e939 refactor: deploy output wording 2023-09-23 09:14:45 +02:00
79b9cc9be7 fix: --offline/--chaos handlings for backup/check/cmd/restore 2023-09-22 09:47:36 +02:00
9b6eb613aa test: woops, keep unit test target default 2023-09-21 12:06:41 +02:00
8f1231e409 test: integration test for abra app upgrade [ci skip] 2023-09-21 11:52:58 +02:00
aa37c936eb test: pass arg to _checkout_recipe 2023-09-21 11:52:21 +02:00
3d1158a425 fix: don't read TIMEOUT for version= label
Closes coop-cloud/organising#451
2023-09-21 11:33:45 +02:00
8788558cf1 fix: only sync version label once
Closes coop-cloud/organising#492
2023-09-21 10:58:17 +02:00
76035e003e fix: recipe workflow with integration tests 2023-09-21 10:36:53 +02:00
b708382d26 feat: recipe lint supports --chaos 2023-09-21 09:07:00 +02:00
557b670fc5 docs: improve recipe fetch usage/desc [ci skip] 2023-09-21 08:46:33 +02:00
e116148c49 test: ensure catalogue --chaos works [ci skip]
Closes coop-cloud/organising#462.
2023-09-20 14:19:49 +02:00
d5593b69e0 test: ensure 3 commits behind, ignore output on fail [ci skip] 2023-09-20 14:10:07 +02:00
0be532692d test: moar integration tests [ci skip] 2023-09-20 13:51:06 +02:00
7a9224b2b2 chore: go mod tidy 2023-09-19 12:38:02 +02:00
e73d1a8359 chore(deps): update module gotest.tools/v3 to v3.5.1 2023-09-19 07:02:01 +00:00
f8c49c82c8 fix: skip "abra-integration-test-recipe" also 2023-09-18 14:02:38 +02:00
ab7edd2a62 refactor!: drop "record" & "server new" command
These were alpha prototypes and we'll reconsider once other layers of
Abra are more stable.
2023-09-14 16:45:01 +02:00
b1888dcf0f chore: go mod tidy 2023-09-14 09:39:28 +02:00
e5e122296f chore(deps): update module github.com/go-git/go-git/v5 to v5.9.0 2023-09-13 07:01:51 +00:00
83bf148304 chore: go mod tidy 2023-09-07 14:34:40 +02:00
d80b882b83 chore(deps): update module github.com/docker/docker to v24.0.6 2023-09-07 07:02:43 +00:00
c345c6f5f1 chore(deps): update module github.com/docker/cli to v24.0.6 2023-09-06 07:01:56 +00:00
f8c4fd72a3 chore: go mod tidy 2023-09-05 13:56:34 +02:00
10f612f998 test: more integration tests 2023-09-05 13:03:38 +02:00
58e78e4d7c fix: overridable ABRA_DIR 2023-09-05 09:58:13 +00:00
25258d3d64 fix: separate abra/kababra makefile targets 2023-09-05 09:58:13 +00:00
b3bd058962 chore: don't join if nothing to join 2023-09-05 09:58:13 +00:00
b4fd7fd77c fix: clone catalogue on initial run 2023-09-05 09:58:13 +00:00
64cfdae6b7 fix: only load client if creating secrets 2023-09-05 09:58:13 +00:00
0a765794f2 test: write initial automatic integration tests 2023-09-05 09:58:13 +00:00
18dc6e9434 feat: support abra testing mode 2023-09-05 09:58:13 +00:00
4ba4107288 chore(deps): update module golang.org/x/sys to v0.12.0 2023-09-04 07:02:01 +00:00
d9b4f4ef3b chore: go mod tidy 2023-08-26 09:58:46 +02:00
c365dcf96d chore(deps): update module github.com/hetznercloud/hcloud-go to v1.50.0 2023-08-25 07:02:00 +00:00
0c6a7cc0b8 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.49.1 2023-08-18 07:01:42 +00:00
6640cfab64 chore: go mod tidy 2023-08-13 17:42:24 +02:00
71addcd1b2 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.49.0 2023-08-13 15:41:44 +00:00
60c0e55e3d fix: don't specify refs when pulling tags
See coop-cloud/organising#477
2023-08-13 12:07:37 +00:00
e42139fd83 chore(deps): update golang docker tag to v1.21 2023-08-09 07:02:07 +00:00
2d826e47d0 chore(deps): update module golang.org/x/sys to v0.11.0 2023-08-07 07:01:55 +00:00
2db172ea5a Further changes to messages. 2023-08-04 19:22:48 +00:00
2077658f6a Attempt to replace the deploy completed message. 2023-08-04 19:22:48 +00:00
502e26b534 Change message when starting to poll for deployment status. 2023-08-04 19:22:48 +00:00
e22b692ada Add os hook for interrupt signal while waiting for service to converge. 2023-08-04 19:22:48 +00:00
5ae73f700e Merge branch 'fix-deploy-no-catalogue' 2023-08-02 10:48:54 +02:00
63d419caae Merge branch 'fix-478' 2023-08-02 10:48:46 +02:00
179b66d65c Merge branch 'fix-476' 2023-08-02 10:48:37 +02:00
c9144d90f3 refactor: integration -> manual 2023-08-02 08:45:24 +02:00
ebf5d82c56 fix: failover if no recipe meta available 2023-08-02 00:48:27 +02:00
8bb98ed0ed fix: deploy fresh recipe without versions
See coop-cloud/organising#476
2023-08-01 21:47:34 +02:00
23f5745cb8 fix: skip recipe clone / up to date sync for some commands
Continues work of 3dc5662821.
2023-08-01 21:19:20 +02:00
2cd453ae8d build: attempt to ignore goreleaser upgrades
See e42cc0f91d.
2023-08-01 19:33:36 +02:00
e42cc0f91d Revert "chore(deps): update goreleaser/goreleaser docker tag to v1.19.2"
This reverts commit 1de45a6508.

See 8fa9419c99.
2023-08-01 19:31:51 +02:00
1de45a6508 chore(deps): update goreleaser/goreleaser docker tag to v1.19.2 2023-07-31 07:02:04 +00:00
151 changed files with 6671 additions and 2763 deletions

View File

@ -1,4 +1,8 @@
Dockerfile
.dockerignore
*.swp
*.swo *.swo
*.swp
.dockerignore
Dockerfile
abra
dist
kadabra
tags

View File

@ -3,20 +3,17 @@ kind: pipeline
name: coopcloud.tech/abra name: coopcloud.tech/abra
steps: steps:
- name: make check - name: make check
image: golang:1.20 image: golang:1.21
commands: commands:
- make check - make check
- name: make build
image: golang:1.20
commands:
- make build
depends_on:
- make check
- name: make test - name: make test
image: golang:1.20 image: golang:1.21
environment:
ABRA_DIR: "/root/.abra"
commands: commands:
- make build-abra
- ./abra help # show version, initialise $ABRA_DIR
- make test - make test
depends_on: depends_on:
- make check - make check
@ -27,7 +24,6 @@ steps:
- git fetch --tags - git fetch --tags
depends_on: depends_on:
- make check - make check
- make build
- make test - make test
when: when:
event: tag event: tag

View File

@ -1,4 +0,0 @@
GANDI_TOKEN=...
HCLOUD_TOKEN=...
REGISTRY_PASSWORD=...
REGISTRY_USERNAME=...

View File

@ -1,6 +1,7 @@
go env -w GOPRIVATE=coopcloud.tech go env -w GOPRIVATE=coopcloud.tech
# export PASSWORD_STORE_DIR=$(pwd)/../../autonomic/passwords/passwords/ # export PASSWORD_STORE_DIR=$(pwd)/../../autonomic/passwords/passwords/
# export HCLOUD_TOKEN=$(pass show logins/hetzner/cicd/api_key)
# export CAPSUL_TOKEN=... # export ABRA_DIR="$HOME/.abra_test"
# export GITEA_TOKEN=... # export ABRA_TEST_DOMAIN=test.example.com
# export ABRA_SKIP_TEARDOWN=1 # for faster feedback when developing tests

2
.gitignore vendored
View File

@ -5,5 +5,5 @@
/kadabra /kadabra
abra abra
dist/ dist/
tests/integration/.abra/catalogue tests/integration/.bats
vendor/ vendor/

View File

@ -1,7 +1,7 @@
# authors # authors
> If you're looking at this and you hack on `abra` and you're not listed here, > If you're looking at this and you hack on `abra` and you're not listed here,
> please do add yourself! This is a community project, let's show some :heart: > please do add yourself! This is a community project, let's show some 💞
- 3wordchant - 3wordchant
- cassowary - cassowary

View File

@ -1,8 +1,15 @@
FROM golang:1.20-alpine AS build FROM golang:1.21-alpine AS build
ENV GOPRIVATE coopcloud.tech ENV GOPRIVATE coopcloud.tech
RUN apk add --no-cache make git gcc musl-dev RUN apk add --no-cache \
ca-certificates \
gcc \
git \
make \
musl-dev
RUN update-ca-certificates
COPY . /app COPY . /app

View File

@ -7,21 +7,34 @@ DIST_LDFLAGS := $(LDFLAGS)" -s -w"
export GOPRIVATE=coopcloud.tech export GOPRIVATE=coopcloud.tech
all: format check build test # NOTE(d1): default `make` optimised for Abra hacking
all: format check build-abra test
run: run-abra:
@go run -ldflags=$(LDFLAGS) $(ABRA) @go run -ldflags=$(LDFLAGS) $(ABRA)
install: run-kadabra:
@go run -ldflags=$(LDFLAGS) $(KADABRA)
install-abra:
@go install -ldflags=$(LDFLAGS) $(ABRA) @go install -ldflags=$(LDFLAGS) $(ABRA)
build-dev: install-kadabra:
@go build -v -ldflags=$(LDFLAGS) $(ABRA) @go install -ldflags=$(LDFLAGS) $(KADABRA)
build: build-abra:
@go build -v -ldflags=$(DIST_LDFLAGS) $(ABRA) @go build -v -ldflags=$(DIST_LDFLAGS) $(ABRA)
build-kadabra:
@go build -v -ldflags=$(DIST_LDFLAGS) $(KADABRA) @go build -v -ldflags=$(DIST_LDFLAGS) $(KADABRA)
build: build-abra build-kadabra
build-docker-abra:
docker run -it -v $(PWD):/abra golang:1.21 bash -c 'cd /abra; ./build-docker-inside.sh'
build-docker: build-docker-abra
clean: clean:
@rm '$(GOPATH)/bin/abra' @rm '$(GOPATH)/bin/abra'
@rm '$(GOPATH)/bin/kadabra' @rm '$(GOPATH)/bin/kadabra'
@ -38,10 +51,3 @@ test:
loc: loc:
@find . -name "*.go" | xargs wc -l @find . -name "*.go" | xargs wc -l
loc-author:
@git ls-files -z | \
xargs -0rn 1 -P "$$(nproc)" -I{} sh -c 'git blame -w -M -C -C --line-porcelain -- {} | grep -I --line-buffered "^author "' | \
sort -f | \
uniq -ic | \
sort -n

View File

@ -8,6 +8,6 @@ The Co-op Cloud utility belt 🎩🐇
<a href="https://github.com/egonelbre/gophers"><img align="right" width="150" src="https://github.com/egonelbre/gophers/raw/master/.thumb/sketch/adventure/poking-fire.png"/></a> <a href="https://github.com/egonelbre/gophers"><img align="right" width="150" src="https://github.com/egonelbre/gophers/raw/master/.thumb/sketch/adventure/poking-fire.png"/></a>
`abra` is the flagship client & command-line tool for Co-op Cloud. It has been developed specifically for the purpose of making the day-to-day operations of [operators](https://docs.coopcloud.tech/operators/) and [maintainers](https://docs.coopcloud.tech/maintainers/) pleasant & convenient. It is libre software, written in [Go](https://go.dev) and maintained and extended by the community :heart: `abra` is the flagship client & command-line tool for Co-op Cloud. It has been developed specifically for the purpose of making the day-to-day operations of [operators](https://docs.coopcloud.tech/operators/) and [maintainers](https://docs.coopcloud.tech/maintainers/) pleasant & convenient. It is libre software, written in [Go](https://go.dev) and maintained and extended by the community 💖
Please see [docs.coopcloud.tech/abra](https://docs.coopcloud.tech/abra) for help on install, upgrade, hacking, troubleshooting & more! Please see [docs.coopcloud.tech/abra](https://docs.coopcloud.tech/abra) for help on install, upgrade, hacking, troubleshooting & more!

19
build-docker-inside.sh Executable file
View File

@ -0,0 +1,19 @@
#!/bin/bash
if [ ! -f .envrc ]; then
. .envrc.sample
else
. .envrc
fi
git config --global --add safe.directory /abra # work around funky file permissions
# fixme for some reason we need to do this
go get coopcloud.tech/abra/pkg/upstream/commandconn
go get github.com/sirupsen/logrus@v1.9.3
go get github.com/cloudflare/circl/dh/x25519@v1.3.3
go get github.com/mattn/go-runewidth@v0.0.14
go get go get coopcloud.tech/abra/pkg/config
go get github.com/mattn/go-colorable@v0.1.12
go get coopcloud.tech/abra/pkg/config
#
make build

View File

@ -15,8 +15,7 @@ import (
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container" containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"coopcloud.tech/abra/pkg/upstream/container" "coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
@ -42,6 +41,7 @@ var appBackupCommand = cli.Command{
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.OfflineFlag, internal.OfflineFlag,
internal.ChaosFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
@ -72,19 +72,29 @@ This file is a compressed archive which contains all backup paths. To see paths,
This single file can be used to restore your app. See "abra app restore" for more. This single file can be used to restore your app. See "abra app restore" for more.
`, `,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
cl, err := client.New(app.Server) recipe, err := recipePkg.Get(app.Recipe, internal.Offline)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
recipe, err := recipe.Get(app.Recipe, conf) if !internal.Chaos {
if err != nil { if err := recipePkg.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Offline {
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipePkg.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
backupConfigs := make(map[string]backupConfig) backupConfigs := make(map[string]backupConfig)
for _, service := range recipe.Config.Services { for _, service := range recipe.Config.Services {
if backupsEnabled, ok := service.Deploy.Labels["backupbot.backup"]; ok { if backupsEnabled, ok := service.Deploy.Labels["backupbot.backup"]; ok {
@ -114,6 +124,11 @@ This single file can be used to restore your app. See "abra app restore" for mor
} }
} }
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
serviceName := c.Args().Get(1) serviceName := c.Args().Get(1)
if serviceName != "" { if serviceName != "" {
backupConfig, ok := backupConfigs[serviceName] backupConfig, ok := backupConfigs[serviceName]

View File

@ -1,14 +1,12 @@
package app package app
import ( import (
"os"
"path"
"strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/runtime" "coopcloud.tech/abra/pkg/formatter"
"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"
) )
@ -16,45 +14,68 @@ import (
var appCheckCommand = cli.Command{ var appCheckCommand = cli.Command{
Name: "check", Name: "check",
Aliases: []string{"chk"}, Aliases: []string{"chk"},
Usage: "Check if app is configured correctly", Usage: "Ensure an app is well configured",
Description: `
This command compares env vars in both the app ".env" and recipe ".env.sample"
file.
The goal is to ensure that recipe ".env.sample" env vars are defined in your
app ".env" file. Only env var definitions in the ".env.sample" which are
uncommented, e.g. "FOO=bar" are checked. If an app ".env" file does not include
these env vars, then "check" will complain.
Recipe maintainers may or may not provide defaults for env vars within their
recipes regardless of commenting or not (e.g. through the use of
${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
ArgsUsage: "<domain>", ArgsUsage: "<domain>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.ChaosFlag,
internal.OfflineFlag, internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
envSamplePath := path.Join(config.RECIPES_DIR, app.Recipe, ".env.sample") if err := recipe.EnsureExists(app.Recipe); err != nil {
if _, err := os.Stat(envSamplePath); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("%s does not exist?", envSamplePath)
}
logrus.Fatal(err) logrus.Fatal(err)
} }
envSample, err := config.ReadEnv(envSamplePath) if !internal.Chaos {
if err := recipePkg.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipePkg.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
tableCol := []string{"recipe env sample", "app env"}
table := formatter.CreateTable(tableCol)
envVars, err := config.CheckEnv(app)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
var missing []string for _, envVar := range envVars {
for k := range envSample { if envVar.Present {
if _, ok := app.Env[k]; !ok { table.Append([]string{envVar.Name, "✅"})
missing = append(missing, k) } else {
table.Append([]string{envVar.Name, "❌"})
} }
} }
if len(missing) > 0 { table.Render()
missingEnvVars := strings.Join(missing, ", ")
logrus.Fatalf("%s is missing %s", app.Path, missingEnvVars)
}
logrus.Infof("all necessary environment variables defined for %s", app.Name)
return nil return nil
}, },
BashComplete: autocomplete.AppNameComplete,
} }

View File

@ -6,13 +6,15 @@ import (
"os" "os"
"os/exec" "os/exec"
"path" "path"
"sort"
"strings" "strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/runtime" "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"
) )
@ -21,8 +23,7 @@ var appCmdCommand = cli.Command{
Name: "command", Name: "command",
Aliases: []string{"cmd"}, Aliases: []string{"cmd"},
Usage: "Run app commands", Usage: "Run app commands",
Description: ` Description: `Run an app specific command.
Run an app specific command.
These commands are bash functions, defined in the abra.sh of the recipe itself. These commands are bash functions, defined in the abra.sh of the recipe itself.
They can be run within the context of a service (e.g. app) or locally on your They can be run within the context of a service (e.g. app) or locally on your
@ -40,18 +41,33 @@ Example:
internal.RemoteUserFlag, internal.RemoteUserFlag,
internal.TtyFlag, internal.TtyFlag,
internal.OfflineFlag, internal.OfflineFlag,
internal.ChaosFlag,
}, },
BashComplete: autocomplete.AppNameComplete,
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Subcommands: []cli.Command{appCmdListCommand},
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
cl, err := client.New(app.Server) if err := recipe.EnsureExists(app.Recipe); err != nil {
if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Chaos {
if err := recipePkg.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipePkg.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if internal.LocalCmd && internal.RemoteUser != "" { if internal.LocalCmd && internal.RemoteUser != "" {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use --local & --user together")) internal.ShowSubcommandHelpAndError(c, errors.New("cannot use --local & --user together"))
} }
@ -67,6 +83,10 @@ Example:
} }
if internal.LocalCmd { if internal.LocalCmd {
if !(len(c.Args()) >= 2) {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments"))
}
cmdName := c.Args().Get(1) cmdName := c.Args().Get(1)
if err := internal.EnsureCommand(abraSh, app.Recipe, cmdName); err != nil { if err := internal.EnsureCommand(abraSh, app.Recipe, cmdName); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -78,6 +98,7 @@ Example:
for k, v := range app.Env { for k, v := range app.Env {
exportEnv = exportEnv + fmt.Sprintf("%s='%s'; ", k, v) exportEnv = exportEnv + fmt.Sprintf("%s='%s'; ", k, v)
} }
var sourceAndExec string var sourceAndExec string
if hasCmdArgs { if hasCmdArgs {
logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs) logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs)
@ -98,6 +119,10 @@ Example:
logrus.Fatal(err) logrus.Fatal(err)
} }
} else { } else {
if !(len(c.Args()) >= 3) {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments"))
}
targetServiceName := c.Args().Get(1) targetServiceName := c.Args().Get(1)
cmdName := c.Args().Get(2) cmdName := c.Args().Get(2)
@ -129,6 +154,11 @@ Example:
logrus.Debug("did not detect any command arguments") logrus.Debug("did not detect any command arguments")
} }
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
if err := internal.RunCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil { if err := internal.RunCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -156,3 +186,53 @@ func parseCmdArgs(args []string, isLocal bool) (bool, string) {
return hasCmdArgs, parsedCmdArgs return hasCmdArgs, parsedCmdArgs
} }
var appCmdListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List all available commands",
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
internal.ChaosFlag,
},
BashComplete: autocomplete.AppNameComplete,
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Chaos {
if err := recipePkg.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipePkg.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
cmdNames, err := config.ReadAbraShCmdNames(abraShPath)
if err != nil {
logrus.Fatal(err)
}
sort.Strings(cmdNames)
for _, cmdName := range cmdNames {
fmt.Println(cmdName)
}
return nil
},
}

View File

@ -20,9 +20,9 @@ var appConfigCommand = cli.Command{
ArgsUsage: "<domain>", ArgsUsage: "<domain>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
appName := c.Args().First() appName := c.Args().First()
@ -61,5 +61,4 @@ var appConfigCommand = cli.Command{
return nil return nil
}, },
BashComplete: autocomplete.AppNameComplete,
} }

View File

@ -12,7 +12,6 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/container" "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/runtime"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
@ -28,10 +27,9 @@ var appCpCommand = cli.Command{
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Usage: "Copy files to/from a running app service", Usage: "Copy files to/from a deployed app service",
Description: ` Description: `
Copy files to and from any app service file system. Copy files to and from any app service file system.
@ -43,14 +41,9 @@ And if you want to copy that file back to your current working directory locally
abra app cp <domain> app:/myfile.txt . abra app cp <domain> app:/myfile.txt .
`, `,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
src := c.Args().Get(1) src := c.Args().Get(1)
dst := c.Args().Get(2) dst := c.Args().Get(2)
@ -92,19 +85,23 @@ And if you want to copy that file back to your current working directory locally
logrus.Debugf("assuming transfer is going TO the container") logrus.Debugf("assuming transfer is going TO the container")
} }
if !isToContainer { if isToContainer {
if _, err := os.Stat(dstPath); os.IsNotExist(err) { if _, err := os.Stat(srcPath); os.IsNotExist(err) {
logrus.Fatalf("%s does not exist locally?", dstPath) logrus.Fatalf("%s does not exist locally?", srcPath)
} }
} }
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
if err := configureAndCp(c, cl, app, srcPath, dstPath, service, isToContainer); err != nil { if err := configureAndCp(c, cl, app, srcPath, dstPath, service, isToContainer); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
return nil return nil
}, },
BashComplete: autocomplete.AppNameComplete,
} }
func configureAndCp( func configureAndCp(
@ -126,10 +123,6 @@ func configureAndCp(
logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server) logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server)
if isToContainer { if isToContainer {
if _, err := os.Stat(srcPath); err != nil {
logrus.Fatalf("%s does not exist?", srcPath)
}
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip} toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
content, err := archive.TarWithOptions(srcPath, toTarOpts) content, err := archive.TarWithOptions(srcPath, toTarOpts)
if err != nil { if err != nil {

View File

@ -3,14 +3,10 @@ package app
import ( import (
"context" "context"
"fmt" "fmt"
"io/ioutil"
"os"
"path"
"strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/runtime" "coopcloud.tech/abra/pkg/secret"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
@ -20,8 +16,6 @@ import (
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -30,7 +24,7 @@ var appDeployCommand = cli.Command{
Name: "deploy", Name: "deploy",
Aliases: []string{"d"}, Aliases: []string{"d"},
Usage: "Deploy an app", Usage: "Deploy an app",
ArgsUsage: "<domain>", ArgsUsage: "<domain> [<version>]",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
@ -48,28 +42,41 @@ for this you need to look at the "abra app upgrade <domain>" command.
You may pass "--force" to re-deploy the same version again. This can be useful You may pass "--force" to re-deploy the same version again. This can be useful
if the container runtime has gotten into a weird state. if the container runtime has gotten into a weird state.
Chas mode ("--chaos") will deploy your local checkout of a recipe as-is, Chaos mode ("--chaos") will deploy your local checkout of a recipe as-is,
including unstaged changes and can be useful for live hacking and testing new including unstaged changes and can be useful for live hacking and testing new
recipes. recipes.
`, `,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
stackName := app.StackName() stackName := app.StackName()
cl, err := client.New(app.Server) specificVersion := c.Args().Get(1)
if err != nil { if specificVersion != "" && internal.Chaos {
logrus.Fatal("cannot use <version> and --chaos together")
}
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Chaos { if !internal.Chaos {
if err := recipe.EnsureUpToDate(app.Recipe, conf); err != nil { 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) logrus.Fatal(err)
} }
} }
r, err := recipe.Get(app.Recipe, conf) if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
r, err := recipe.Get(app.Recipe, internal.Offline)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -80,11 +87,27 @@ recipes.
logrus.Debugf("checking whether %s is already deployed", stackName) logrus.Debugf("checking whether %s is already deployed", stackName)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName) isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
secStats, err := secret.PollSecretsStatus(cl, app)
if err != nil {
logrus.Fatal(err)
}
for _, secStat := range secStats {
if !secStat.CreatedOnRemote {
logrus.Fatalf("unable to deploy, secrets not generated (%s)?", secStat.LocalName)
}
}
if isDeployed { if isDeployed {
if internal.Force || internal.Chaos { if internal.Force || internal.Chaos {
logrus.Warnf("%s is already deployed but continuing (--force/--chaos)", app.Name) logrus.Warnf("%s is already deployed but continuing (--force/--chaos)", app.Name)
@ -94,8 +117,16 @@ recipes.
} }
version := deployedVersion version := deployedVersion
if version == "unknown" && !internal.Chaos { if specificVersion != "" {
catl, err := recipe.ReadRecipeCatalogue(conf) 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 == "" {
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -103,7 +134,21 @@ recipes.
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if len(versions) > 0 {
if len(versions) == 0 && !internal.Chaos {
logrus.Warn("no published versions in catalogue, trying local recipe repository")
recipeVersions, err := recipe.GetRecipeVersions(app.Recipe, internal.Offline)
if err != nil {
logrus.Warn(err)
}
for _, recipeVersion := range recipeVersions {
for version := range recipeVersion {
versions = append(versions, version)
}
}
}
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 := recipe.EnsureVersion(app.Recipe, version); err != nil {
@ -116,22 +161,6 @@ recipes.
} }
version = formatter.SmallSHA(head.String()) version = formatter.SmallSHA(head.String())
logrus.Warn("no versions detected, using latest commit") logrus.Warn("no versions detected, using latest commit")
if err := recipe.EnsureLatest(app.Recipe, conf); err != nil {
logrus.Fatal(err)
}
}
}
if version == "unknown" && !internal.Chaos {
logrus.Debugf("choosing %s as version to deploy", version)
if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
logrus.Fatal(err)
}
}
if version != "unknown" && !internal.Chaos {
if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
logrus.Fatal(err)
} }
} }
@ -153,10 +182,11 @@ recipes.
app.Env[k] = v app.Env[k] = v
} }
composeFiles, err := config.GetAppComposeFiles(app.Recipe, app.Env) composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
deployOpts := stack.Deploy{ deployOpts := stack.Deploy{
Composefiles: composeFiles, Composefiles: composeFiles,
Namespace: stackName, Namespace: stackName,
@ -167,13 +197,25 @@ recipes.
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
config.ExposeAllEnv(stackName, compose, app.Env) config.ExposeAllEnv(stackName, compose, app.Env)
config.SetRecipeLabel(compose, stackName, app.Recipe) config.SetRecipeLabel(compose, stackName, app.Recipe)
config.SetChaosLabel(compose, stackName, internal.Chaos) config.SetChaosLabel(compose, stackName, internal.Chaos)
config.SetChaosVersionLabel(compose, stackName, version) config.SetChaosVersionLabel(compose, stackName, version)
config.SetUpdateLabel(compose, stackName, app.Env) config.SetUpdateLabel(compose, stackName, app.Env)
if err := DeployOverview(app, version, "continue with deployment?"); err != nil { envVars, err := config.CheckEnv(app)
if err != nil {
logrus.Fatal(err)
}
for _, envVar := range envVars {
if !envVar.Present {
logrus.Warnf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain)
}
}
if err := internal.DeployOverview(app, version, "continue with deployment?"); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -203,175 +245,10 @@ recipes.
postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"] postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"]
if ok && !internal.DontWaitConverge { if ok && !internal.DontWaitConverge {
logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds) logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds)
if err := PostCmds(cl, app, postDeployCmds); err != nil { if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
logrus.Fatalf("attempting to run post deploy commands, saw: %s", err) logrus.Fatalf("attempting to run post deploy commands, saw: %s", err)
} }
} }
return nil return nil
}, },
} }
// PostCmds parses a string of commands and executes them inside of the respective services
// the commands string must have the following format:
// "<service> <command> <arguments>|<service> <command> <arguments>|... "
func PostCmds(cl *dockerClient.Client, app config.App, commands string) error {
abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf(fmt.Sprintf("%s does not exist for %s?", abraSh, app.Name))
}
return err
}
for _, command := range strings.Split(commands, "|") {
commandParts := strings.Split(command, " ")
if len(commandParts) < 2 {
return fmt.Errorf(fmt.Sprintf("not enough arguments: %s", command))
}
targetServiceName := commandParts[0]
cmdName := commandParts[1]
parsedCmdArgs := ""
if len(commandParts) > 2 {
parsedCmdArgs = fmt.Sprintf("%s ", strings.Join(commandParts[2:], " "))
}
logrus.Infof("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName)
if err := internal.EnsureCommand(abraSh, app.Recipe, cmdName); err != nil {
return err
}
serviceNames, err := config.GetAppServiceNames(app.Name)
if err != nil {
return err
}
matchingServiceName := false
for _, serviceName := range serviceNames {
if serviceName == targetServiceName {
matchingServiceName = true
}
}
if !matchingServiceName {
return fmt.Errorf(fmt.Sprintf("no service %s for %s?", targetServiceName, app.Name))
}
logrus.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName)
internal.Tty = true
if err := internal.RunCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil {
return err
}
}
return nil
}
// DeployOverview shows a deployment overview
func DeployOverview(app config.App, version, message string) error {
tableCol := []string{"server", "recipe", "config", "domain", "version"}
table := formatter.CreateTable(tableCol)
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
}
server := app.Server
if app.Server == "default" {
server = "local"
}
table.Append([]string{server, app.Recipe, deployConfig, app.Domain, version})
table.Render()
if internal.NoInput {
return nil
}
response := false
prompt := &survey.Confirm{
Message: message,
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
logrus.Fatal("exiting as requested")
}
return nil
}
// NewVersionOverview shows an upgrade or downgrade overview
func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes string) error {
tableCol := []string{"server", "recipe", "config", "domain", "current version", "to be deployed"}
table := formatter.CreateTable(tableCol)
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
}
server := app.Server
if app.Server == "default" {
server = "local"
}
table.Append([]string{server, app.Recipe, deployConfig, app.Domain, currentVersion, newVersion})
table.Render()
if releaseNotes == "" {
var err error
releaseNotes, err = GetReleaseNotes(app.Recipe, newVersion)
if err != nil {
return err
}
}
if releaseNotes != "" && newVersion != "" {
fmt.Println()
fmt.Println(fmt.Sprintf("%s release notes:\n\n%s", newVersion, releaseNotes))
} else {
logrus.Warnf("no release notes available for %s", newVersion)
}
if internal.NoInput {
return nil
}
response := false
prompt := &survey.Confirm{
Message: "continue with deployment?",
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
logrus.Fatal("exiting as requested")
}
return nil
}
// GetReleaseNotes prints release notes for a recipe version
func GetReleaseNotes(recipeName, version string) (string, error) {
if version == "" {
return "", nil
}
fpath := path.Join(config.RECIPES_DIR, recipeName, "release", version)
if _, err := os.Stat(fpath); !os.IsNotExist(err) {
releaseNotes, err := ioutil.ReadFile(fpath)
if err != nil {
return "", err
}
return string(releaseNotes), nil
}
return "", nil
}

View File

@ -12,7 +12,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"
"coopcloud.tech/abra/pkg/runtime"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
@ -56,8 +55,7 @@ the logs.
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
@ -74,25 +72,23 @@ the logs.
} }
if !internal.Watch { if !internal.Watch {
if err := checkErrors(c, cl, app, conf); err != nil { if err := checkErrors(c, cl, app); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
return nil return nil
} }
for { for {
if err := checkErrors(c, cl, app, conf); err != nil { if err := checkErrors(c, cl, app); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
} }
return nil
}, },
} }
func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App, conf *runtime.Config) error { func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App) error {
recipe, err := recipe.Get(app.Recipe, conf) recipe, err := recipe.Get(app.Recipe, internal.Offline)
if err != nil { if err != nil {
return err return err
} }

View File

@ -11,7 +11,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"
"coopcloud.tech/abra/pkg/runtime"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
@ -84,8 +83,6 @@ can take some time.
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline))
appFiles, err := config.LoadAppFiles(listAppServer) appFiles, err := config.LoadAppFiles(listAppServer)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -113,7 +110,7 @@ can take some time.
logrus.Fatal(err) logrus.Fatal(err)
} }
catl, err = recipe.ReadRecipeCatalogue(conf) catl, err = recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -179,13 +176,14 @@ can take some time.
appStats.AutoUpdate = autoUpdate appStats.AutoUpdate = autoUpdate
var newUpdates []string var newUpdates []string
if version != "unknown" {
updates, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil {
logrus.Fatal(err)
}
if (version != "unknown") {
parsedVersion, err := tagcmp.Parse(version) parsedVersion, err := tagcmp.Parse(version)
if (err != nil) {
logrus.Warning("Can't parse ", app.Recipe, " version ", version, " ", err)
} else {
updates, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -193,7 +191,8 @@ can take some time.
for _, update := range updates { for _, update := range updates {
parsedUpdate, err := tagcmp.Parse(update) parsedUpdate, err := tagcmp.Parse(update)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Warning("can't parse ", app.Recipe," update version ", update, " ", err)
continue
} }
if update != version && parsedUpdate.IsGreaterThan(parsedVersion) { if update != version && parsedUpdate.IsGreaterThan(parsedVersion) {
@ -201,6 +200,7 @@ can take some time.
} }
} }
} }
}
if len(newUpdates) == 0 { if len(newUpdates) == 0 {
if version == "unknown" { if version == "unknown" {

View File

@ -11,8 +11,9 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/runtime" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/service" "coopcloud.tech/abra/pkg/service"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
@ -79,19 +80,31 @@ var appLogsCommand = cli.Command{
internal.StdErrOnlyFlag, internal.StdErrOnlyFlag,
internal.SinceLogsFlag, internal.SinceLogsFlag,
internal.DebugFlag, internal.DebugFlag,
internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf) stackName := app.StackName()
if err := recipe.EnsureExists(app.Recipe); 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, stackName)
if err != nil {
logrus.Fatal(err)
}
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
logOpts.Since = internal.SinceLogs logOpts.Since = internal.SinceLogs
serviceName := c.Args().Get(1) serviceName := c.Args().Get(1)

View File

@ -5,7 +5,6 @@ import (
"path" "path"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
@ -13,7 +12,6 @@ import (
"coopcloud.tech/abra/pkg/jsontable" "coopcloud.tech/abra/pkg/jsontable"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"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"
@ -54,16 +52,27 @@ var appNewCommand = cli.Command{
internal.PassFlag, internal.PassFlag,
internal.SecretsFlag, internal.SecretsFlag,
internal.OfflineFlag, internal.OfflineFlag,
internal.ChaosFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
ArgsUsage: "[<recipe>]", ArgsUsage: "[<recipe>]",
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) recipe := internal.ValidateRecipe(c)
recipe := internal.ValidateRecipeWithPrompt(c, conf)
if err := recipePkg.EnsureUpToDate(recipe.Name, conf); err != nil { if !internal.Chaos {
if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Offline {
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err)
}
}
if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
logrus.Fatal(err)
}
}
if err := ensureServerFlag(); err != nil { if err := ensureServerFlag(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -85,7 +94,26 @@ var appNewCommand = cli.Command{
logrus.Fatal(err) logrus.Fatal(err)
} }
if err := promptForSecrets(internal.Domain); err != nil { var secrets AppSecrets
var secretTable *jsontable.JSONTable
if internal.Secrets {
sampleEnv, err := recipe.SampleEnv(config.ReadEnvOptions{})
if err != nil {
logrus.Fatal(err)
}
composeFiles, err := config.GetComposeFiles(recipe.Name, sampleEnv)
if err != nil {
logrus.Fatal(err)
}
envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample")
secretsConfig, err := secret.ReadSecretsConfig(envSamplePath, composeFiles, recipe.Name)
if err != nil {
return err
}
if err := promptForSecrets(recipe.Name, secretsConfig); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -94,20 +122,16 @@ var appNewCommand = cli.Command{
logrus.Fatal(err) logrus.Fatal(err)
} }
var secrets AppSecrets secrets, err = createSecrets(cl, secretsConfig, sanitisedAppName)
var secretTable *jsontable.JSONTable
if internal.Secrets {
secrets, err := createSecrets(cl, sanitisedAppName)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
secretCols := []string{"Name", "Value"} secretCols := []string{"Name", "Value"}
secretTable = formatter.CreateTable(secretCols) secretTable = formatter.CreateTable(secretCols)
for secret := range secrets { for name, val := range secrets {
secretTable.Append([]string{secret, secrets[secret]}) secretTable.Append([]string{name, val})
} }
} }
if internal.NewAppServer == "default" { if internal.NewAppServer == "default" {
@ -118,7 +142,6 @@ var appNewCommand = cli.Command{
table := formatter.CreateTable(tableCol) table := formatter.CreateTable(tableCol)
table.Append([]string{internal.NewAppServer, recipe.Name, internal.Domain}) table.Append([]string{internal.NewAppServer, recipe.Name, internal.Domain})
fmt.Println("")
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:", recipe.Name))
fmt.Println("") fmt.Println("")
table.Render() table.Render()
@ -128,40 +151,31 @@ var appNewCommand = cli.Command{
fmt.Println("") fmt.Println("")
fmt.Println("You can deploy this app by running the following:") fmt.Println("You can deploy this app by running the following:")
fmt.Println(fmt.Sprintf("\n abra app deploy %s", internal.Domain)) fmt.Println(fmt.Sprintf("\n abra app deploy %s", internal.Domain))
fmt.Println("")
if len(secrets) > 0 { if len(secrets) > 0 {
fmt.Println("")
fmt.Println("Here are your generated secrets:") fmt.Println("Here are your generated secrets:")
fmt.Println("") fmt.Println("")
secretTable.Render() secretTable.Render()
fmt.Println("") logrus.Warn("generated secrets are not shown again, please take note of them NOW")
logrus.Warn("generated secrets are not shown again, please take note of them *now*")
} }
return nil return nil
}, },
BashComplete: autocomplete.RecipeNameComplete,
} }
// AppSecrets represents all app secrest // AppSecrets represents all app secrest
type AppSecrets map[string]string 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, sanitisedAppName string) (AppSecrets, error) { func createSecrets(cl *dockerClient.Client, secretsConfig map[string]string, sanitisedAppName string) (AppSecrets, error) {
appEnvPath := path.Join( // NOTE(d1): trim to match app.StackName() implementation
config.ABRA_DIR, if len(sanitisedAppName) > 45 {
"servers", logrus.Debugf("trimming %s to %s to avoid runtime limits", sanitisedAppName, sanitisedAppName[:45])
internal.NewAppServer, sanitisedAppName = sanitisedAppName[:45]
fmt.Sprintf("%s.env", internal.Domain),
)
appEnv, err := config.ReadEnv(appEnvPath)
if err != nil {
return nil, err
} }
secretEnvVars := secret.ReadSecretEnvVars(appEnv) secrets, err := secret.GenerateSecrets(cl, secretsConfig, sanitisedAppName, internal.NewAppServer)
secrets, err := secret.GenerateSecrets(cl, secretEnvVars, sanitisedAppName, internal.NewAppServer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -179,6 +193,7 @@ func createSecrets(cl *dockerClient.Client, sanitisedAppName string) (AppSecrets
} }
} }
} }
return secrets, nil return secrets, nil
} }
@ -202,15 +217,9 @@ func ensureDomainFlag(recipe recipe.Recipe, server string) error {
} }
// promptForSecrets asks if we should generate secrets for a new app. // promptForSecrets asks if we should generate secrets for a new app.
func promptForSecrets(appName string) error { func promptForSecrets(recipeName string, secretsConfig map[string]string) error {
app, err := app.Get(appName) if len(secretsConfig) == 0 {
if err != nil { logrus.Debugf("%s has no secrets to generate, skipping...", recipeName)
return err
}
secretEnvVars := secret.ReadSecretEnvVars(app.Env)
if len(secretEnvVars) == 0 {
logrus.Debugf("%s has no secrets to generate, skipping...", app.Recipe)
return nil return nil
} }

View File

@ -10,7 +10,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/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/runtime"
"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/buger/goterm" "github.com/buger/goterm"
@ -30,13 +29,11 @@ var appPsCommand = cli.Command{
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.WatchFlag, internal.WatchFlag,
internal.DebugFlag, internal.DebugFlag,
internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {

View File

@ -8,7 +8,6 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/runtime"
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"
@ -49,8 +48,7 @@ flag.
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
if !internal.Force && !internal.NoInput { if !internal.Force && !internal.NoInput {
response := false response := false

View File

@ -8,7 +8,6 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/runtime"
upstream "coopcloud.tech/abra/pkg/upstream/service" upstream "coopcloud.tech/abra/pkg/upstream/service"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -28,8 +27,7 @@ var appRestartCommand = cli.Command{
Description: `This command restarts a service within a deployed app.`, Description: `This command restarts a service within a deployed app.`,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
serviceNameShort := c.Args().Get(1) serviceNameShort := c.Args().Get(1)
if serviceNameShort == "" { if serviceNameShort == "" {
@ -42,6 +40,15 @@ var appRestartCommand = cli.Command{
logrus.Fatal(err) logrus.Fatal(err)
} }
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
logrus.Fatal(err)
}
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
serviceName := fmt.Sprintf("%s_%s", app.StackName(), serviceNameShort) serviceName := fmt.Sprintf("%s_%s", app.StackName(), serviceNameShort)
logrus.Debugf("attempting to scale %s to 0 (restart logic)", serviceName) logrus.Debugf("attempting to scale %s to 0 (restart logic)", serviceName)

View File

@ -12,7 +12,7 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container" containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/container" "coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
@ -36,6 +36,7 @@ var appRestoreCommand = cli.Command{
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.OfflineFlag, internal.OfflineFlag,
internal.ChaosFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
@ -49,21 +50,37 @@ restoring the backup.
Unlike "abra app backup", restore must be run on a per-service basis. You can Unlike "abra app backup", restore must be run on a per-service basis. You can
not restore all services in one go. Backup files produced by Abra are not restore all services in one go. Backup files produced by Abra are
compressed archives which use absolute paths. This allows Abra to restore compressed archives which use absolute paths. This allows Abra to restore
according to standard tar command logic. according to standard tar command logic, i.e. the backup will be restored to
the path it was originally backed up from.
Example: Example:
abra app restore example.com app ~/.abra/backups/example_com_app_609341138.tar.gz abra app restore example.com app ~/.abra/backups/example_com_app_609341138.tar.gz
`, `,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
cl, err := client.New(app.Server) recipe, err := recipe.Get(app.Recipe, internal.Offline)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Chaos {
if err := recipePkg.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipePkg.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
serviceName := c.Args().Get(1) serviceName := c.Args().Get(1)
if serviceName == "" { if serviceName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("missing <service>?")) internal.ShowSubcommandHelpAndError(c, errors.New("missing <service>?"))
@ -80,11 +97,6 @@ Example:
} }
} }
recipe, err := recipe.Get(app.Recipe, conf)
if err != nil {
logrus.Fatal(err)
}
restoreConfigs := make(map[string]restoreConfig) restoreConfigs := make(map[string]restoreConfig)
for _, service := range recipe.Config.Services { for _, service := range recipe.Config.Services {
if restoreEnabled, ok := service.Deploy.Labels["backupbot.restore"]; ok { if restoreEnabled, ok := service.Deploy.Labels["backupbot.restore"]; ok {
@ -114,6 +126,11 @@ Example:
rsConfig = restoreConfig{} rsConfig = restoreConfig{}
} }
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
if err := runRestore(cl, app, backupPath, serviceName, rsConfig); err != nil { if err := runRestore(cl, app, backupPath, serviceName, rsConfig); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -170,8 +187,8 @@ func runRestore(cl *dockerClient.Client, app config.App, backupPath, serviceName
return err return err
} }
// we use absolute paths so tar knows what to do. it will restore files // NOTE(d1): we use absolute paths so tar knows what to do. it will restore
// according to the paths set in the compresed archive // files according to the paths set in the compressed archive
restorePath := "/" restorePath := "/"
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false} copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}

View File

@ -8,7 +8,6 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
@ -23,7 +22,7 @@ var appRollbackCommand = cli.Command{
Name: "rollback", Name: "rollback",
Aliases: []string{"rl"}, Aliases: []string{"rl"},
Usage: "Roll an app back to a previous version", Usage: "Roll an app back to a previous version",
ArgsUsage: "<domain>", ArgsUsage: "<domain> [<version>]",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
@ -43,23 +42,41 @@ useful if the container runtime has gotten into a weird state.
This action could be destructive, please ensure you have a copy of your app This action could be destructive, please ensure you have a copy of your app
data beforehand. data beforehand.
Chas mode ("--chaos") will deploy your local checkout of a recipe as-is, Chaos mode ("--chaos") will deploy your local checkout of a recipe as-is,
including unstaged changes and can be useful for live hacking and testing new including unstaged changes and can be useful for live hacking and testing new
recipes. recipes.
`, `,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
stackName := app.StackName() stackName := app.StackName()
specificVersion := c.Args().Get(1)
if specificVersion != "" && internal.Chaos {
logrus.Fatal("cannot use <version> and --chaos together")
}
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Chaos { if !internal.Chaos {
if err := recipe.EnsureUpToDate(app.Recipe, conf); err != nil { 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) logrus.Fatal(err)
} }
} }
r, err := recipe.Get(app.Recipe, conf) if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
r, err := recipe.Get(app.Recipe, internal.Offline)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -84,7 +101,7 @@ recipes.
logrus.Fatalf("%s is not deployed?", app.Name) logrus.Fatalf("%s is not deployed?", app.Name)
} }
catl, err := recipe.ReadRecipeCatalogue(conf) catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -95,16 +112,40 @@ recipes.
} }
if len(versions) == 0 && !internal.Chaos { if len(versions) == 0 && !internal.Chaos {
logrus.Fatalf("no published releases for %s in the recipe catalogue?", app.Recipe) logrus.Warn("no published versions in catalogue, trying local recipe repository")
recipeVersions, err := recipe.GetRecipeVersions(app.Recipe, internal.Offline)
if err != nil {
logrus.Warn(err)
}
for _, recipeVersion := range recipeVersions {
for version := range recipeVersion {
versions = append(versions, version)
}
}
} }
var availableDowngrades []string var availableDowngrades []string
if deployedVersion == "unknown" { if deployedVersion == "unknown" {
availableDowngrades = versions availableDowngrades = versions
logrus.Warnf("failed to determine version of deployed %s", app.Name) logrus.Warnf("failed to determine deployed version of %s", app.Name)
} }
if deployedVersion != "unknown" && !internal.Chaos { if specificVersion != "" {
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
if err != nil {
logrus.Fatal(err)
}
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil {
logrus.Fatal(err)
}
if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) || parsedSpecificVersion.Equals(parsedDeployedVersion) {
logrus.Fatalf("%s is not a downgrade for %s?", deployedVersion, specificVersion)
}
availableDowngrades = append(availableDowngrades, specificVersion)
}
if deployedVersion != "unknown" && !internal.Chaos && specificVersion == "" {
for _, version := range versions { for _, version := range versions {
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion) parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
if err != nil { if err != nil {
@ -114,12 +155,12 @@ recipes.
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if parsedVersion != parsedDeployedVersion && parsedVersion.IsLessThan(parsedDeployedVersion) { if parsedVersion.IsLessThan(parsedDeployedVersion) && !(parsedVersion.Equals(parsedDeployedVersion)) {
availableDowngrades = append(availableDowngrades, version) availableDowngrades = append(availableDowngrades, version)
} }
} }
if len(availableDowngrades) == 0 { if len(availableDowngrades) == 0 && !internal.Force {
logrus.Info("no available downgrades, you're on oldest ✌️") logrus.Info("no available downgrades, you're on oldest ✌️")
return nil return nil
} }
@ -127,9 +168,9 @@ recipes.
var chosenDowngrade string var chosenDowngrade string
if len(availableDowngrades) > 0 && !internal.Chaos { if len(availableDowngrades) > 0 && !internal.Chaos {
if internal.Force || internal.NoInput { if internal.Force || internal.NoInput || specificVersion != "" {
chosenDowngrade = availableDowngrades[len(availableDowngrades)-1] chosenDowngrade = availableDowngrades[len(availableDowngrades)-1]
logrus.Debugf("choosing %s as version to downgrade to (--force)", chosenDowngrade) logrus.Debugf("choosing %s as version to downgrade to (--force/--no-input)", chosenDowngrade)
} else { } else {
prompt := &survey.Select{ prompt := &survey.Select{
Message: fmt.Sprintf("Please select a downgrade (current version: %s):", deployedVersion), Message: fmt.Sprintf("Please select a downgrade (current version: %s):", deployedVersion),
@ -165,7 +206,7 @@ recipes.
app.Env[k] = v app.Env[k] = v
} }
composeFiles, err := config.GetAppComposeFiles(app.Recipe, app.Env) composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -185,7 +226,8 @@ recipes.
config.SetChaosVersionLabel(compose, stackName, chosenDowngrade) config.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
config.SetUpdateLabel(compose, stackName, app.Env) config.SetUpdateLabel(compose, stackName, app.Env)
if err := NewVersionOverview(app, deployedVersion, chosenDowngrade, ""); err != nil { // NOTE(d1): no release notes implemeneted for rolling back
if err := internal.NewVersionOverview(app, deployedVersion, chosenDowngrade, ""); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -9,7 +9,6 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
containerPkg "coopcloud.tech/abra/pkg/container" containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/runtime"
"coopcloud.tech/abra/pkg/upstream/container" "coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
@ -38,15 +37,13 @@ var appRunCommand = cli.Command{
internal.DebugFlag, internal.DebugFlag,
noTTYFlag, noTTYFlag,
userFlag, userFlag,
internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
ArgsUsage: "<domain> <service> <args>...", ArgsUsage: "<domain> <service> <args>...",
Usage: "Run a command in a service container", Usage: "Run a command in a service container",
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
if len(c.Args()) < 2 { if len(c.Args()) < 2 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <service> provided?")) internal.ShowSubcommandHelpAndError(c, errors.New("no <service> provided?"))

View File

@ -12,7 +12,7 @@ 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/runtime" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
@ -43,19 +43,35 @@ var appSecretGenerateCommand = cli.Command{
internal.DebugFlag, internal.DebugFlag,
allSecretsFlag, allSecretsFlag,
internal.PassFlag, internal.PassFlag,
internal.MachineReadableFlag,
internal.OfflineFlag, internal.OfflineFlag,
internal.ChaosFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
cl, err := client.New(app.Server) if err := recipe.EnsureExists(app.Recipe); err != nil {
if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Chaos {
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)
}
}
if len(c.Args()) == 1 && !allSecrets { if len(c.Args()) == 1 && !allSecrets {
err := errors.New("missing arguments <secret>/<version> or '--all'") err := errors.New("missing arguments <secret>/<version> or '--all'")
internal.ShowSubcommandHelpAndError(c, err) internal.ShowSubcommandHelpAndError(c, err)
@ -66,18 +82,26 @@ var appSecretGenerateCommand = cli.Command{
internal.ShowSubcommandHelpAndError(c, err) internal.ShowSubcommandHelpAndError(c, err)
} }
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil {
logrus.Fatal(err)
}
secretsConfig, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.Recipe)
if err != nil {
logrus.Fatal(err)
}
secretsToCreate := make(map[string]string) secretsToCreate := make(map[string]string)
secretEnvVars := secret.ReadSecretEnvVars(app.Env)
if allSecrets { if allSecrets {
secretsToCreate = secretEnvVars secretsToCreate = secretsConfig
} else { } else {
secretName := c.Args().Get(1) secretName := c.Args().Get(1)
secretVersion := c.Args().Get(2) secretVersion := c.Args().Get(2)
matches := false matches := false
for sec := range secretEnvVars { for name := range secretsConfig {
parsed := secret.ParseSecretEnvVarName(sec) if secretName == name {
if secretName == parsed { secretsToCreate[name] = secretVersion
secretsToCreate[sec] = secretVersion
matches = true matches = true
} }
} }
@ -87,6 +111,11 @@ var appSecretGenerateCommand = cli.Command{
} }
} }
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
secretVals, err := secret.GenerateSecrets(cl, secretsToCreate, app.StackName(), app.Server) secretVals, err := secret.GenerateSecrets(cl, secretsToCreate, app.StackName(), app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -110,8 +139,13 @@ var appSecretGenerateCommand = cli.Command{
for name, val := range secretVals { for name, val := range secretVals {
table.Append([]string{name, val}) table.Append([]string{name, val})
} }
if internal.MachineReadable {
table.JSONRender()
} else {
table.Render() table.Render()
logrus.Warn("generated secrets are not shown again, please take note of them *now*") }
logrus.Warn("generated secrets are not shown again, please take note of them NOW")
return nil return nil
}, },
@ -124,7 +158,6 @@ var appSecretInsertCommand = cli.Command{
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.PassFlag, internal.PassFlag,
internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
ArgsUsage: "<domain> <secret-name> <version> <data>", ArgsUsage: "<domain> <secret-name> <version> <data>",
@ -142,18 +175,17 @@ Example:
`, `,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
if len(c.Args()) != 4 {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments?"))
}
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if len(c.Args()) != 4 {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments?"))
}
name := c.Args().Get(1) name := c.Args().Get(1)
version := c.Args().Get(2) version := c.Args().Get(2)
data := c.Args().Get(3) data := c.Args().Get(3)
@ -204,6 +236,7 @@ var appSecretRmCommand = cli.Command{
rmAllSecretsFlag, rmAllSecretsFlag,
internal.PassRemoveFlag, internal.PassRemoveFlag,
internal.OfflineFlag, internal.OfflineFlag,
internal.ChaosFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
ArgsUsage: "<domain> [<secret-name>]", ArgsUsage: "<domain> [<secret-name>]",
@ -216,9 +249,37 @@ Example:
abra app secret remove myapp db_pass abra app secret remove myapp db_pass
`, `,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
secrets := secret.ReadSecretEnvVars(app.Env) if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Chaos {
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)
}
}
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil {
logrus.Fatal(err)
}
secretsConfig, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.Recipe)
if err != nil {
logrus.Fatal(err)
}
if c.Args().Get(1) != "" && rmAllSecrets { if c.Args().Get(1) != "" && rmAllSecrets {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<secret-name>' and '--all' together")) internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<secret-name>' and '--all' together"))
@ -250,15 +311,13 @@ Example:
match := false match := false
secretToRm := c.Args().Get(1) secretToRm := c.Args().Get(1)
for sec := range secrets { for secretName, secretValue := range secretsConfig {
secretName := secret.ParseSecretEnvVarName(sec) val, err := secret.ParseSecretValue(secretValue)
secVal, err := secret.ParseSecretEnvVarValue(secrets[sec])
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, secVal.Version) secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, val.Version)
if _, ok := remoteSecretNames[secretRemoteName]; ok { if _, ok := remoteSecretNames[secretRemoteName]; ok {
if secretToRm != "" { if secretToRm != "" {
if secretName == secretToRm { if secretName == secretToRm {
@ -296,61 +355,70 @@ var appSecretLsCommand = cli.Command{
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.OfflineFlag, internal.OfflineFlag,
internal.ChaosFlag,
internal.MachineReadableFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Usage: "List all secrets", Usage: "List all secrets",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
secrets := secret.ReadSecretEnvVars(app.Env)
tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"} if err := recipe.EnsureExists(app.Recipe); err != nil {
table := formatter.CreateTable(tableCol) logrus.Fatal(err)
}
if !internal.Chaos {
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)
}
}
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
filters, err := app.Filters(false, false) tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"}
table := formatter.CreateTable(tableCol)
secStats, err := secret.PollSecretsStatus(cl, app)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters}) for _, secStat := range secStats {
if err != nil { tableRow := []string{
logrus.Fatal(err) secStat.LocalName,
secStat.Version,
secStat.RemoteName,
strconv.FormatBool(secStat.CreatedOnRemote),
} }
remoteSecretNames := make(map[string]bool)
for _, cont := range secretList {
remoteSecretNames[cont.Spec.Annotations.Name] = true
}
for sec := range secrets {
createdRemote := false
secretName := secret.ParseSecretEnvVarName(sec)
secVal, err := secret.ParseSecretEnvVarValue(secrets[sec])
if err != nil {
logrus.Fatal(err)
}
secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, secVal.Version)
if _, ok := remoteSecretNames[secretRemoteName]; ok {
createdRemote = true
}
tableRow := []string{secretName, secVal.Version, secretRemoteName, strconv.FormatBool(createdRemote)}
table.Append(tableRow) table.Append(tableRow)
} }
if table.NumLines() > 0 { if table.NumLines() > 0 {
if internal.MachineReadable {
table.JSONRender()
} else {
table.Render() table.Render()
}
} else { } else {
logrus.Warnf("no secrets stored for %s", app.Name) logrus.Warnf("no secrets stored for %s", app.Name)
} }
return nil return nil
}, },
BashComplete: autocomplete.AppNameComplete,
} }
var appSecretCommand = cli.Command{ var appSecretCommand = cli.Command{

View File

@ -9,7 +9,6 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/runtime"
"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" "github.com/docker/docker/api/types"
@ -24,13 +23,11 @@ var appServicesCommand = cli.Command{
ArgsUsage: "<domain>", ArgsUsage: "<domain>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {

View File

@ -10,7 +10,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/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/runtime"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"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"
@ -87,7 +86,6 @@ var appUndeployCommand = cli.Command{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
pruneFlag, pruneFlag,
internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Usage: "Undeploy an app", Usage: "Undeploy an app",
@ -101,8 +99,7 @@ any previously attached volumes as eligible for pruning once undeployed.
Passing "-p/--prune" does not remove those volumes. Passing "-p/--prune" does not remove those volumes.
`, `,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
stackName := app.StackName() stackName := app.StackName()
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
@ -121,7 +118,7 @@ Passing "-p/--prune" does not remove those volumes.
logrus.Fatalf("%s is not deployed?", app.Name) logrus.Fatalf("%s is not deployed?", app.Name)
} }
if err := DeployOverview(app, deployedVersion, "continue with undeploy?"); err != nil { if err := internal.DeployOverview(app, deployedVersion, "continue with undeploy?"); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -10,7 +10,7 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime" recipePkg "coopcloud.tech/abra/pkg/recipe"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
@ -22,7 +22,7 @@ var appUpgradeCommand = cli.Command{
Name: "upgrade", Name: "upgrade",
Aliases: []string{"up"}, Aliases: []string{"up"},
Usage: "Upgrade an app", Usage: "Upgrade an app",
ArgsUsage: "<domain>", ArgsUsage: "<domain> [<version>]",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
@ -47,37 +47,52 @@ useful if the container runtime has gotten into a weird state.
This action could be destructive, please ensure you have a copy of your app This action could be destructive, please ensure you have a copy of your app
data beforehand. data beforehand.
Chas mode ("--chaos") will deploy your local checkout of a recipe as-is, Chaos mode ("--chaos") will deploy your local checkout of a recipe as-is,
including unstaged changes and can be useful for live hacking and testing new including unstaged changes and can be useful for live hacking and testing new
recipes. recipes.
`, `,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
stackName := app.StackName() stackName := app.StackName()
specificVersion := c.Args().Get(1)
if specificVersion != "" && internal.Chaos {
logrus.Fatal("cannot use <version> and --chaos together")
}
if !internal.Chaos {
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 {
logrus.Fatal(err)
}
if err := lint.LintForErrors(recipe); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("checking whether %s is already deployed", stackName)
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Chaos {
if err := recipe.EnsureUpToDate(app.Recipe, conf); err != nil {
logrus.Fatal(err)
}
}
r, err := recipe.Get(app.Recipe, conf)
if err != nil {
logrus.Fatal(err)
}
if err := lint.LintForErrors(r); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("checking whether %s is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName) isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -87,37 +102,62 @@ recipes.
logrus.Fatalf("%s is not deployed?", app.Name) logrus.Fatalf("%s is not deployed?", app.Name)
} }
catl, err := recipe.ReadRecipeCatalogue(conf) catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl) versions, err := recipePkg.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if len(versions) == 0 && !internal.Chaos { if len(versions) == 0 && !internal.Chaos {
logrus.Fatalf("no published releases for %s in the recipe catalogue?", app.Recipe) logrus.Warn("no published versions in catalogue, trying local recipe repository")
recipeVersions, err := recipePkg.GetRecipeVersions(app.Recipe, internal.Offline)
if err != nil {
logrus.Warn(err)
}
for _, recipeVersion := range recipeVersions {
for version := range recipeVersion {
versions = append(versions, version)
}
}
} }
var availableUpgrades []string var availableUpgrades []string
if deployedVersion == "unknown" { if deployedVersion == "unknown" {
availableUpgrades = versions availableUpgrades = versions
logrus.Warnf("failed to determine version of deployed %s", app.Name) logrus.Warnf("failed to determine deployed version of %s", app.Name)
} }
if deployedVersion != "unknown" && !internal.Chaos { if specificVersion != "" {
for _, version := range versions {
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion) parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil {
logrus.Fatal(err)
}
if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) || parsedSpecificVersion.Equals(parsedDeployedVersion) {
logrus.Fatalf("%s is not an upgrade for %s?", deployedVersion, specificVersion)
}
availableUpgrades = append(availableUpgrades, specificVersion)
}
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
if err != nil {
logrus.Fatal(err)
}
if deployedVersion != "unknown" && !internal.Chaos && specificVersion == "" {
for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version) parsedVersion, err := tagcmp.Parse(version)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if parsedVersion.IsGreaterThan(parsedDeployedVersion) { if parsedVersion.IsGreaterThan(parsedDeployedVersion) && !(parsedVersion.Equals(parsedDeployedVersion)) {
availableUpgrades = append(availableUpgrades, version) availableUpgrades = append(availableUpgrades, version)
} }
} }
@ -130,7 +170,7 @@ recipes.
var chosenUpgrade string var chosenUpgrade string
if len(availableUpgrades) > 0 && !internal.Chaos { if len(availableUpgrades) > 0 && !internal.Chaos {
if internal.Force || internal.NoInput { if internal.Force || internal.NoInput || specificVersion != "" {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1] chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
logrus.Debugf("choosing %s as version to upgrade to", chosenUpgrade) logrus.Debugf("choosing %s as version to upgrade to", chosenUpgrade)
} else { } else {
@ -152,13 +192,30 @@ recipes.
// if release notes written after git tag published, read them before we // if release notes written after git tag published, read them before we
// 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
releaseNotes, err := GetReleaseNotes(app.Recipe, chosenUpgrade) var releaseNotes string
for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
logrus.Fatal(err)
}
parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade)
if err != nil {
logrus.Fatal(err)
}
if !(parsedVersion.Equals(parsedDeployedVersion)) && parsedVersion.IsLessThan(parsedChosenUpgrade) {
note, err := internal.GetReleaseNotes(app.Recipe, version)
if err != nil { if err != nil {
return err return err
} }
if note != "" {
releaseNotes += fmt.Sprintf("%s\n", note)
}
}
}
if !internal.Chaos { if !internal.Chaos {
if err := recipe.EnsureVersion(app.Recipe, chosenUpgrade); err != nil { if err := recipePkg.EnsureVersion(app.Recipe, chosenUpgrade); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
@ -166,7 +223,7 @@ 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 = recipe.ChaosVersion(app.Recipe) chosenUpgrade, err = recipePkg.ChaosVersion(app.Recipe)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -181,7 +238,7 @@ recipes.
app.Env[k] = v app.Env[k] = v
} }
composeFiles, err := config.GetAppComposeFiles(app.Recipe, app.Env) composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -201,7 +258,18 @@ recipes.
config.SetChaosVersionLabel(compose, stackName, chosenUpgrade) config.SetChaosVersionLabel(compose, stackName, chosenUpgrade)
config.SetUpdateLabel(compose, stackName, app.Env) config.SetUpdateLabel(compose, stackName, app.Env)
if err := NewVersionOverview(app, deployedVersion, chosenUpgrade, releaseNotes); err != nil { envVars, err := config.CheckEnv(app)
if err != nil {
logrus.Fatal(err)
}
for _, envVar := range envVars {
if !envVar.Present {
logrus.Warnf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain)
}
}
if err := internal.NewVersionOverview(app, deployedVersion, chosenUpgrade, releaseNotes); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -218,12 +286,11 @@ recipes.
postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"] postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"]
if ok && !internal.DontWaitConverge { if ok && !internal.DontWaitConverge {
logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds) logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds)
if err := PostCmds(cl, app, postDeployCmds); err != nil { if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
logrus.Fatalf("attempting to run post deploy commands, saw: %s", err) logrus.Fatalf("attempting to run post deploy commands, saw: %s", err)
} }
} }
return nil return nil
}, },
BashComplete: autocomplete.AppNameComplete,
} }

View File

@ -2,19 +2,30 @@ package app
import ( import (
"context" "context"
"sort"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/olekukonko/tablewriter"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
func sortServiceByName(versions [][]string) func(i, j int) bool {
return func(i, j int) bool {
// NOTE(d1): corresponds to the `tableCol` definition below
if versions[i][1] == "app" {
return true
}
return versions[i][1] < versions[j][1]
}
}
// getImagePath returns the image name // getImagePath returns the image name
func getImagePath(image string) (string, error) { func getImagePath(image string) (string, error) {
img, err := reference.ParseNormalizedNamed(image) img, err := reference.ParseNormalizedNamed(image)
@ -41,15 +52,10 @@ var appVersionCommand = cli.Command{
internal.OfflineFlag, internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Usage: "Show app versions", Usage: "Show version info of a deployed app",
Description: ` BashComplete: autocomplete.AppNameComplete,
Show all information about versioning related to a deployed app. This includes
the individual image names, tags and digests. But also the Co-op Cloud recipe
version.
`,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
stackName := app.StackName() stackName := app.StackName()
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
@ -64,15 +70,15 @@ version.
logrus.Fatal(err) logrus.Fatal(err)
} }
if deployedVersion == "unknown" {
logrus.Fatalf("failed to determine version of deployed %s", app.Name)
}
if !isDeployed { if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name) logrus.Fatalf("%s is not deployed?", app.Name)
} }
recipeMeta, err := recipe.GetRecipeMeta(app.Recipe, conf) if deployedVersion == "unknown" {
logrus.Fatalf("failed to determine version of deployed %s", app.Name)
}
recipeMeta, err := recipe.GetRecipeMeta(app.Recipe, internal.Offline)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -88,17 +94,24 @@ version.
logrus.Fatalf("could not retrieve deployed version (%s) from recipe catalogue?", deployedVersion) logrus.Fatalf("could not retrieve deployed version (%s) from recipe catalogue?", deployedVersion)
} }
tableCol := []string{"version", "service", "image"} tableCol := []string{"version", "service", "image", "tag"}
table := formatter.CreateTable(tableCol) table := formatter.CreateTable(tableCol)
table.SetAutoMergeCellsByColumnIndex([]int{0})
var versions [][]string
for serviceName, versionMeta := range versionsMeta { for serviceName, versionMeta := range versionsMeta {
table.Append([]string{deployedVersion, serviceName, versionMeta.Image}) versions = append(versions, []string{deployedVersion, serviceName, versionMeta.Image, versionMeta.Tag})
} }
sort.Slice(versions, sortServiceByName(versions))
for _, version := range versions {
table.Append(version)
}
table.SetAutoMergeCellsByColumnIndex([]int{0})
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.Render() table.Render()
return nil return nil
}, },
BashComplete: autocomplete.AppNameComplete,
} }

View File

@ -7,7 +7,7 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/runtime" "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
@ -20,14 +20,12 @@ var appVolumeListCommand = cli.Command{
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Usage: "List volumes associated with an app", Usage: "List volumes associated with an app",
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
@ -83,18 +81,26 @@ Passing "--force/-f" will select all volumes for removal. Be careful.
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
internal.ForceFlag, internal.ForceFlag,
internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) app := internal.ValidateApp(c)
app := internal.ValidateApp(c, conf)
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())
if err != nil {
logrus.Fatal(err)
}
if isDeployed {
logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name)
}
filters, err := app.Filters(false, true) filters, err := app.Filters(false, true)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -124,16 +130,19 @@ Passing "--force/-f" will select all volumes for removal. Be careful.
volumesToRemove = volumeNames volumesToRemove = volumeNames
} }
if len(volumesToRemove) > 0 {
err = client.RemoveVolumes(cl, context.Background(), app.Server, volumesToRemove, internal.Force) err = client.RemoveVolumes(cl, context.Background(), app.Server, volumesToRemove, internal.Force)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Info("volumes removed successfully") logrus.Info("volumes removed successfully")
} else {
logrus.Info("no volumes removed")
}
return nil return nil
}, },
BashComplete: autocomplete.AppNameComplete,
} }
var appVolumeCommand = cli.Command{ var appVolumeCommand = cli.Command{

View File

@ -13,7 +13,6 @@ import (
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"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"
@ -29,6 +28,7 @@ var catalogueGenerateCommand = cli.Command{
internal.PublishFlag, internal.PublishFlag,
internal.DryFlag, internal.DryFlag,
internal.SkipUpdatesFlag, internal.SkipUpdatesFlag,
internal.ChaosFlag,
internal.OfflineFlag, internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
@ -54,19 +54,21 @@ that you have permission to git push to these repositories and have your SSH
keys configured on your account. keys configured on your account.
`, `,
ArgsUsage: "[<recipe>]", ArgsUsage: "[<recipe>]",
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline))
recipeName := c.Args().First() recipeName := c.Args().First()
if recipeName != "" { if recipeName != "" {
internal.ValidateRecipe(c, conf) internal.ValidateRecipe(c)
} }
if err := catalogue.EnsureUpToDate(conf); err != nil { if !internal.Chaos {
if err := catalogue.EnsureIsClean(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
}
repos, err := recipe.ReadReposMetadata(conf) repos, err := recipe.ReadReposMetadata()
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -83,7 +85,7 @@ keys configured on your account.
if !internal.SkipUpdates { if !internal.SkipUpdates {
logrus.Warn(logMsg) logrus.Warn(logMsg)
if err := recipe.UpdateRepositories(repos, recipeName, conf); err != nil { if err := recipe.UpdateRepositories(repos, recipeName); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
@ -96,12 +98,7 @@ keys configured on your account.
continue continue
} }
if _, exists := catalogue.CatalogueSkipList[recipeMeta.Name]; exists { versions, err := recipe.GetRecipeVersions(recipeMeta.Name, internal.Offline)
catlBar.Add(1)
continue
}
versions, err := recipe.GetRecipeVersions(recipeMeta.Name, conf)
if err != nil { if err != nil {
logrus.Warn(err) logrus.Warn(err)
} }
@ -137,7 +134,7 @@ keys configured on your account.
logrus.Fatal(err) logrus.Fatal(err)
} }
} else { } else {
catlFS, err := recipe.ReadRecipeCatalogue(conf) catlFS, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -171,7 +168,7 @@ keys configured on your account.
} }
msg := "chore: publish new catalogue release changes" msg := "chore: publish new catalogue release changes"
if err := gitPkg.Commit(cataloguePath, "**.json", msg, internal.Dry); err != nil { if err := gitPkg.Commit(cataloguePath, msg, internal.Dry); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -211,7 +208,6 @@ keys configured on your account.
return nil return nil
}, },
BashComplete: autocomplete.RecipeNameComplete,
} }
// CatalogueCommand defines the `abra catalogue` command and sub-commands. // CatalogueCommand defines the `abra catalogue` command and sub-commands.

View File

@ -12,9 +12,9 @@ import (
"coopcloud.tech/abra/cli/catalogue" "coopcloud.tech/abra/cli/catalogue"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/cli/recipe" "coopcloud.tech/abra/cli/recipe"
"coopcloud.tech/abra/cli/record"
"coopcloud.tech/abra/cli/server" "coopcloud.tech/abra/cli/server"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
cataloguePkg "coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/web" "coopcloud.tech/abra/pkg/web"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -158,7 +158,6 @@ func newAbraApp(version, commit string) *cli.App {
server.ServerCommand, server.ServerCommand,
recipe.RecipeCommand, recipe.RecipeCommand,
catalogue.CatalogueCommand, catalogue.CatalogueCommand,
record.RecordCommand,
UpgradeCommand, UpgradeCommand,
AutoCompleteCommand, AutoCompleteCommand,
}, },
@ -170,10 +169,10 @@ func newAbraApp(version, commit string) *cli.App {
app.Before = func(c *cli.Context) error { app.Before = func(c *cli.Context) error {
paths := []string{ paths := []string{
config.ABRA_DIR, config.ABRA_DIR,
path.Join(config.SERVERS_DIR), config.SERVERS_DIR,
path.Join(config.RECIPES_DIR), config.RECIPES_DIR,
path.Join(config.VENDOR_DIR), config.VENDOR_DIR,
path.Join(config.BACKUP_DIR), config.BACKUP_DIR,
} }
for _, path := range paths { for _, path := range paths {
@ -185,6 +184,10 @@ func newAbraApp(version, commit string) *cli.App {
} }
} }
if err := cataloguePkg.EnsureCatalogue(); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("abra version %s, commit %s", version, commit) logrus.Debugf("abra version %s, commit %s", version, commit)
return nil return nil

View File

@ -54,7 +54,7 @@ var Chaos bool
// ChaosFlag turns on/off chaos functionality. // ChaosFlag turns on/off chaos functionality.
var ChaosFlag = &cli.BoolFlag{ var ChaosFlag = &cli.BoolFlag{
Name: "chaos, C", Name: "chaos, C",
Usage: "Deploy uncommitted recipes changes. Use with care!", Usage: "Proceed with uncommitted recipes changes. Use with care!",
Destination: &Chaos, Destination: &Chaos,
} }
@ -68,17 +68,6 @@ var TtyFlag = &cli.BoolFlag{
Destination: &Tty, Destination: &Tty,
} }
// DNSProvider specifies a DNS provider.
var DNSProvider string
// DNSProviderFlag selects a DNS provider.
var DNSProviderFlag = &cli.StringFlag{
Name: "provider, p",
Value: "",
Usage: "DNS provider",
Destination: &DNSProvider,
}
var NoInput bool var NoInput bool
var NoInputFlag = &cli.BoolFlag{ var NoInputFlag = &cli.BoolFlag{
Name: "no-input, n", Name: "no-input, n",
@ -86,163 +75,6 @@ var NoInputFlag = &cli.BoolFlag{
Destination: &NoInput, Destination: &NoInput,
} }
var DNSType string
var DNSTypeFlag = &cli.StringFlag{
Name: "record-type, rt",
Value: "",
Usage: "Domain name record type (e.g. A)",
Destination: &DNSType,
}
var DNSName string
var DNSNameFlag = &cli.StringFlag{
Name: "record-name, rn",
Value: "",
Usage: "Domain name record name (e.g. mysubdomain)",
Destination: &DNSName,
}
var DNSValue string
var DNSValueFlag = &cli.StringFlag{
Name: "record-value, rv",
Value: "",
Usage: "Domain name record value (e.g. 192.168.1.1)",
Destination: &DNSValue,
}
var DNSTTL string
var DNSTTLFlag = &cli.StringFlag{
Name: "record-ttl, rl",
Value: "600s",
Usage: "Domain name TTL value (seconds)",
Destination: &DNSTTL,
}
var DNSPriority int
var DNSPriorityFlag = &cli.IntFlag{
Name: "record-priority, rp",
Value: 10,
Usage: "Domain name priority value",
Destination: &DNSPriority,
}
var ServerProvider string
var ServerProviderFlag = &cli.StringFlag{
Name: "provider, p",
Usage: "3rd party server provider",
Destination: &ServerProvider,
}
var CapsulInstanceURL string
var CapsulInstanceURLFlag = &cli.StringFlag{
Name: "capsul-url, cu",
Value: "yolo.servers.coop",
Usage: "capsul instance URL",
Destination: &CapsulInstanceURL,
}
var CapsulName string
var CapsulNameFlag = &cli.StringFlag{
Name: "capsul-name, cn",
Value: "",
Usage: "capsul name",
Destination: &CapsulName,
}
var CapsulType string
var CapsulTypeFlag = &cli.StringFlag{
Name: "capsul-type, ct",
Value: "f1-xs",
Usage: "capsul type",
Destination: &CapsulType,
}
var CapsulImage string
var CapsulImageFlag = &cli.StringFlag{
Name: "capsul-image, ci",
Value: "debian10",
Usage: "capsul image",
Destination: &CapsulImage,
}
var CapsulSSHKeys cli.StringSlice
var CapsulSSHKeysFlag = &cli.StringSliceFlag{
Name: "capsul-ssh-keys, cs",
Usage: "capsul SSH key",
Value: &CapsulSSHKeys,
}
var CapsulAPIToken string
var CapsulAPITokenFlag = &cli.StringFlag{
Name: "capsul-token, ca",
Usage: "capsul API token",
EnvVar: "CAPSUL_TOKEN",
Destination: &CapsulAPIToken,
}
var HetznerCloudName string
var HetznerCloudNameFlag = &cli.StringFlag{
Name: "hetzner-name, hn",
Value: "",
Usage: "hetzner cloud name",
Destination: &HetznerCloudName,
}
var HetznerCloudType string
var HetznerCloudTypeFlag = &cli.StringFlag{
Name: "hetzner-type, ht",
Usage: "hetzner cloud type",
Destination: &HetznerCloudType,
Value: "cx11",
}
var HetznerCloudImage string
var HetznerCloudImageFlag = &cli.StringFlag{
Name: "hetzner-image, hi",
Usage: "hetzner cloud image",
Value: "debian-10",
Destination: &HetznerCloudImage,
}
var HetznerCloudSSHKeys cli.StringSlice
var HetznerCloudSSHKeysFlag = &cli.StringSliceFlag{
Name: "hetzner-ssh-keys, hs",
Usage: "hetzner cloud SSH keys (e.g. me@foo.com)",
Value: &HetznerCloudSSHKeys,
}
var HetznerCloudLocation string
var HetznerCloudLocationFlag = &cli.StringFlag{
Name: "hetzner-location, hl",
Usage: "hetzner cloud server location",
Value: "hel1",
Destination: &HetznerCloudLocation,
}
var HetznerCloudAPIToken string
var HetznerCloudAPITokenFlag = &cli.StringFlag{
Name: "hetzner-token, ha",
Usage: "hetzner cloud API token",
EnvVar: "HCLOUD_TOKEN",
Destination: &HetznerCloudAPIToken,
}
// Debug stores the variable from DebugFlag. // Debug stores the variable from DebugFlag.
var Debug bool var Debug bool

173
cli/internal/deploy.go Normal file
View File

@ -0,0 +1,173 @@
package internal
import (
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"github.com/AlecAivazis/survey/v2"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
// NewVersionOverview shows an upgrade or downgrade overview
func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes string) error {
tableCol := []string{"server", "recipe", "config", "domain", "current version", "to be deployed"}
table := formatter.CreateTable(tableCol)
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
}
server := app.Server
if app.Server == "default" {
server = "local"
}
table.Append([]string{server, app.Recipe, deployConfig, app.Domain, currentVersion, newVersion})
table.Render()
if releaseNotes != "" && newVersion != "" {
fmt.Println()
fmt.Print(releaseNotes)
} else {
logrus.Warnf("no release notes available for %s", newVersion)
}
if NoInput {
return nil
}
response := false
prompt := &survey.Confirm{
Message: "continue with deployment?",
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
logrus.Fatal("exiting as requested")
}
return nil
}
// GetReleaseNotes prints release notes for a recipe version
func GetReleaseNotes(recipeName, version string) (string, error) {
if version == "" {
return "", nil
}
fpath := path.Join(config.RECIPES_DIR, recipeName, "release", version)
if _, err := os.Stat(fpath); !os.IsNotExist(err) {
releaseNotes, err := ioutil.ReadFile(fpath)
if err != nil {
return "", err
}
withTitle := fmt.Sprintf("%s release notes:\n%s", version, string(releaseNotes))
return withTitle, nil
}
return "", nil
}
// PostCmds parses a string of commands and executes them inside of the respective services
// the commands string must have the following format:
// "<service> <command> <arguments>|<service> <command> <arguments>|... "
func PostCmds(cl *dockerClient.Client, app config.App, commands string) error {
abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf(fmt.Sprintf("%s does not exist for %s?", abraSh, app.Name))
}
return err
}
for _, command := range strings.Split(commands, "|") {
commandParts := strings.Split(command, " ")
if len(commandParts) < 2 {
return fmt.Errorf(fmt.Sprintf("not enough arguments: %s", command))
}
targetServiceName := commandParts[0]
cmdName := commandParts[1]
parsedCmdArgs := ""
if len(commandParts) > 2 {
parsedCmdArgs = fmt.Sprintf("%s ", strings.Join(commandParts[2:], " "))
}
logrus.Infof("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName)
if err := EnsureCommand(abraSh, app.Recipe, cmdName); err != nil {
return err
}
serviceNames, err := config.GetAppServiceNames(app.Name)
if err != nil {
return err
}
matchingServiceName := false
for _, serviceName := range serviceNames {
if serviceName == targetServiceName {
matchingServiceName = true
}
}
if !matchingServiceName {
return fmt.Errorf(fmt.Sprintf("no service %s for %s?", targetServiceName, app.Name))
}
logrus.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName)
Tty = true
if err := RunCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil {
return err
}
}
return nil
}
// DeployOverview shows a deployment overview
func DeployOverview(app config.App, version, message string) error {
tableCol := []string{"server", "recipe", "config", "domain", "version"}
table := formatter.CreateTable(tableCol)
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
}
server := app.Server
if app.Server == "default" {
server = "local"
}
table.Append([]string{server, app.Recipe, deployConfig, app.Domain, version})
table.Render()
if NoInput {
return nil
}
response := false
prompt := &survey.Confirm{
Message: message,
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
logrus.Fatal("exiting as requested")
}
return nil
}

View File

@ -2,60 +2,24 @@ package internal
import ( import (
"errors" "errors"
"fmt"
"os"
"strings" "strings"
"coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
// ValidateRecipe ensures the recipe arg is valid. // ValidateRecipe ensures the recipe arg is valid.
func ValidateRecipe(c *cli.Context, conf *runtime.Config) recipe.Recipe { func ValidateRecipe(c *cli.Context) recipe.Recipe {
recipeName := c.Args().First()
if recipeName == "" {
ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
}
chosenRecipe, err := recipe.Get(recipeName, conf)
if err != nil {
if c.Command.Name == "generate" {
if strings.Contains(err.Error(), "missing a compose") {
logrus.Fatal(err)
}
logrus.Warn(err)
} else {
if strings.Contains(err.Error(), "template_driver is not allowed") {
logrus.Warnf("ensure %s recipe compose.* files include \"version: '3.8'\"", recipeName)
}
logrus.Fatalf("unable to validate recipe: %s", err)
}
}
if err := recipe.EnsureLatest(recipeName, conf); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("validated %s as recipe argument", recipeName)
return chosenRecipe
}
// ValidateRecipeWithPrompt ensures a recipe argument is present before
// validating, asking for input if required.
func ValidateRecipeWithPrompt(c *cli.Context, conf *runtime.Config) recipe.Recipe {
recipeName := c.Args().First() recipeName := c.Args().First()
if recipeName == "" && !NoInput { if recipeName == "" && !NoInput {
var recipes []string var recipes []string
catl, err := recipe.ReadRecipeCatalogue(conf) catl, err := recipe.ReadRecipeCatalogue(Offline)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -93,13 +57,19 @@ func ValidateRecipeWithPrompt(c *cli.Context, conf *runtime.Config) recipe.Recip
ShowSubcommandHelpAndError(c, errors.New("no recipe name provided")) ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
} }
chosenRecipe, err := recipe.Get(recipeName, conf) chosenRecipe, err := recipe.Get(recipeName, Offline)
if err != nil { if err != nil {
if c.Command.Name == "generate" {
if strings.Contains(err.Error(), "missing a compose") {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Warn(err)
if err := recipe.EnsureLatest(recipeName, conf); err != nil { } else {
logrus.Fatal(err) if strings.Contains(err.Error(), "template_driver is not allowed") {
logrus.Warnf("ensure %s recipe compose.* files include \"version: '3.8'\"", recipeName)
}
logrus.Fatalf("unable to validate recipe: %s", err)
}
} }
logrus.Debugf("validated %s as recipe argument", recipeName) logrus.Debugf("validated %s as recipe argument", recipeName)
@ -108,7 +78,7 @@ func ValidateRecipeWithPrompt(c *cli.Context, conf *runtime.Config) recipe.Recip
} }
// ValidateApp ensures the app name arg is valid. // ValidateApp ensures the app name arg is valid.
func ValidateApp(c *cli.Context, conf *runtime.Config) config.App { func ValidateApp(c *cli.Context) config.App {
appName := c.Args().First() appName := c.Args().First()
if appName == "" { if appName == "" {
@ -120,10 +90,6 @@ func ValidateApp(c *cli.Context, conf *runtime.Config) config.App {
logrus.Fatal(err) logrus.Fatal(err)
} }
if err := recipe.EnsureExists(app.Recipe, conf); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("validated %s as app argument", appName) logrus.Debugf("validated %s as app argument", appName)
return app return app
@ -192,309 +158,15 @@ func ValidateServer(c *cli.Context) string {
} }
} }
if !matched {
ShowSubcommandHelpAndError(c, errors.New("server doesn't exist?"))
}
if serverName == "" { if serverName == "" {
ShowSubcommandHelpAndError(c, errors.New("no server provided")) ShowSubcommandHelpAndError(c, errors.New("no server provided"))
} }
if !matched {
ShowSubcommandHelpAndError(c, errors.New("server doesn't exist?"))
}
logrus.Debugf("validated %s as server argument", serverName) logrus.Debugf("validated %s as server argument", serverName)
return serverName return serverName
} }
// EnsureDNSProvider ensures a DNS provider is chosen.
func EnsureDNSProvider() error {
if DNSProvider == "" && !NoInput {
prompt := &survey.Select{
Message: "Select DNS provider",
Options: []string{"gandi"},
}
if err := survey.AskOne(prompt, &DNSProvider); err != nil {
return err
}
}
if DNSProvider == "" {
return fmt.Errorf("missing DNS provider?")
}
return nil
}
// EnsureDNSTypeFlag ensures a DNS type flag is present.
func EnsureDNSTypeFlag(c *cli.Context) error {
if DNSType == "" && !NoInput {
prompt := &survey.Input{
Message: "Specify DNS record type",
Default: "A",
}
if err := survey.AskOne(prompt, &DNSType); err != nil {
return err
}
}
if DNSType == "" {
ShowSubcommandHelpAndError(c, errors.New("no record type provided"))
}
return nil
}
// EnsureDNSNameFlag ensures a DNS name flag is present.
func EnsureDNSNameFlag(c *cli.Context) error {
if DNSName == "" && !NoInput {
prompt := &survey.Input{
Message: "Specify DNS record name",
Default: "mysubdomain",
}
if err := survey.AskOne(prompt, &DNSName); err != nil {
return err
}
}
if DNSName == "" {
ShowSubcommandHelpAndError(c, errors.New("no record name provided"))
}
return nil
}
// EnsureDNSValueFlag ensures a DNS value flag is present.
func EnsureDNSValueFlag(c *cli.Context) error {
if DNSValue == "" && !NoInput {
prompt := &survey.Input{
Message: "Specify DNS record value",
Default: "192.168.1.2",
}
if err := survey.AskOne(prompt, &DNSValue); err != nil {
return err
}
}
if DNSValue == "" {
ShowSubcommandHelpAndError(c, errors.New("no record value provided"))
}
return nil
}
// EnsureZoneArgument ensures a zone argument is present.
func EnsureZoneArgument(c *cli.Context) (string, error) {
zone := c.Args().First()
if zone == "" && !NoInput {
prompt := &survey.Input{
Message: "Specify a domain name zone",
Default: "example.com",
}
if err := survey.AskOne(prompt, &zone); err != nil {
return zone, err
}
}
if zone == "" {
ShowSubcommandHelpAndError(c, errors.New("no zone value provided"))
}
return zone, nil
}
// EnsureServerProvider ensures a 3rd party server provider is chosen.
func EnsureServerProvider() error {
if ServerProvider == "" && !NoInput {
prompt := &survey.Select{
Message: "Select server provider",
Options: []string{"capsul", "hetzner-cloud"},
}
if err := survey.AskOne(prompt, &ServerProvider); err != nil {
return err
}
}
if ServerProvider == "" {
return fmt.Errorf("missing server provider?")
}
return nil
}
// EnsureNewCapsulVPSFlags ensure all flags are present.
func EnsureNewCapsulVPSFlags(c *cli.Context) error {
if CapsulName == "" && !NoInput {
prompt := &survey.Input{
Message: "specify capsul name",
}
if err := survey.AskOne(prompt, &CapsulName); err != nil {
return err
}
}
if !NoInput {
prompt := &survey.Input{
Message: "specify capsul instance URL",
Default: CapsulInstanceURL,
}
if err := survey.AskOne(prompt, &CapsulInstanceURL); err != nil {
return err
}
}
if !NoInput {
prompt := &survey.Input{
Message: "specify capsul type",
Default: CapsulType,
}
if err := survey.AskOne(prompt, &CapsulType); err != nil {
return err
}
}
if !NoInput {
prompt := &survey.Input{
Message: "specify capsul image",
Default: CapsulImage,
}
if err := survey.AskOne(prompt, &CapsulImage); err != nil {
return err
}
}
if len(CapsulSSHKeys.Value()) == 0 && !NoInput {
var sshKeys string
prompt := &survey.Input{
Message: "specify capsul SSH keys (e.g. me@foo.com)",
Default: "",
}
if err := survey.AskOne(prompt, &CapsulSSHKeys); err != nil {
return err
}
CapsulSSHKeys = cli.StringSlice(strings.Split(sshKeys, ","))
}
if CapsulAPIToken == "" && !NoInput {
token, ok := os.LookupEnv("CAPSUL_TOKEN")
if !ok {
prompt := &survey.Input{
Message: "specify capsul API token",
}
if err := survey.AskOne(prompt, &CapsulAPIToken); err != nil {
return err
}
} else {
CapsulAPIToken = token
}
}
if CapsulName == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul name?"))
}
if CapsulInstanceURL == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul instance url?"))
}
if CapsulType == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul type?"))
}
if CapsulImage == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul image?"))
}
if len(CapsulSSHKeys.Value()) == 0 {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul ssh keys?"))
}
if CapsulAPIToken == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul API token?"))
}
return nil
}
// EnsureNewHetznerCloudVPSFlags ensure all flags are present.
func EnsureNewHetznerCloudVPSFlags(c *cli.Context) error {
if HetznerCloudName == "" && !NoInput {
prompt := &survey.Input{
Message: "specify hetzner cloud VPS name",
}
if err := survey.AskOne(prompt, &HetznerCloudName); err != nil {
return err
}
}
if !NoInput {
prompt := &survey.Input{
Message: "specify hetzner cloud VPS type",
Default: HetznerCloudType,
}
if err := survey.AskOne(prompt, &HetznerCloudType); err != nil {
return err
}
}
if !NoInput {
prompt := &survey.Input{
Message: "specify hetzner cloud VPS image",
Default: HetznerCloudImage,
}
if err := survey.AskOne(prompt, &HetznerCloudImage); err != nil {
return err
}
}
if len(HetznerCloudSSHKeys.Value()) == 0 && !NoInput {
var sshKeys string
prompt := &survey.Input{
Message: "specify hetzner cloud SSH keys (e.g. me@foo.com)",
Default: "",
}
if err := survey.AskOne(prompt, &sshKeys); err != nil {
return err
}
HetznerCloudSSHKeys = cli.StringSlice(strings.Split(sshKeys, ","))
}
if !NoInput {
prompt := &survey.Input{
Message: "specify hetzner cloud VPS location",
Default: HetznerCloudLocation,
}
if err := survey.AskOne(prompt, &HetznerCloudLocation); err != nil {
return err
}
}
if HetznerCloudAPIToken == "" && !NoInput {
token, ok := os.LookupEnv("HCLOUD_TOKEN")
if !ok {
prompt := &survey.Input{
Message: "specify hetzner cloud API token",
}
if err := survey.AskOne(prompt, &HetznerCloudAPIToken); err != nil {
return err
}
} else {
HetznerCloudAPIToken = token
}
}
if HetznerCloudName == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud VPS name?"))
}
if HetznerCloudType == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud VPS type?"))
}
if HetznerCloudImage == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud image?"))
}
if HetznerCloudLocation == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud VPS location?"))
}
if HetznerCloudAPIToken == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud API token?"))
}
return nil
}

40
cli/recipe/diff.go Normal file
View File

@ -0,0 +1,40 @@
package recipe
import (
"path"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var recipeDiffCommand = cli.Command{
Name: "diff",
Usage: "Show unstaged changes in recipe config",
Description: "Due to limitations in our underlying Git dependency, this command requires /usr/bin/git.",
Aliases: []string{"d"},
ArgsUsage: "<recipe>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
if recipeName != "" {
internal.ValidateRecipe(c)
}
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
if err := gitPkg.DiffUnstaged(recipeDir); err != nil {
logrus.Fatal(err)
}
return nil
},
}

View File

@ -4,38 +4,38 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
var recipeFetchCommand = cli.Command{ var recipeFetchCommand = cli.Command{
Name: "fetch", Name: "fetch",
Usage: "Fetch recipe local copies", Usage: "Fetch recipe(s)",
Aliases: []string{"f"}, Aliases: []string{"f"},
ArgsUsage: "[<recipe>]", ArgsUsage: "[<recipe>]",
Description: "Fetchs all recipes without arguments.", Description: "Retrieves all recipes if no <recipe> argument is passed",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.OfflineFlag, internal.NoInputFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline))
recipeName := c.Args().First() recipeName := c.Args().First()
if recipeName != "" { if recipeName != "" {
internal.ValidateRecipe(c, conf) internal.ValidateRecipe(c)
return nil // ValidateRecipe ensures latest checkout
} }
repos, err := recipe.ReadReposMetadata(conf) if err := recipe.EnsureExists(recipeName); err != nil {
if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if err := recipe.UpdateRepositories(repos, recipeName, conf); err != nil { if err := recipe.EnsureUpToDate(recipeName); err != nil {
logrus.Fatal(err)
}
if err := recipe.EnsureLatest(recipeName); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -8,7 +8,6 @@ import (
"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" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -22,17 +21,34 @@ var recipeLintCommand = cli.Command{
internal.DebugFlag, internal.DebugFlag,
internal.OnlyErrorFlag, internal.OnlyErrorFlag,
internal.OfflineFlag, internal.OfflineFlag,
internal.NoInputFlag,
internal.ChaosFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) recipe := internal.ValidateRecipe(c)
recipe := internal.ValidateRecipe(c, conf)
if err := recipePkg.EnsureUpToDate(recipe.Name, conf); err != nil { if err := recipePkg.EnsureExists(recipe.Name); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Chaos {
if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err)
}
}
if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
logrus.Fatal(err)
}
}
tableCol := []string{"ref", "rule", "severity", "satisfied", "skipped", "resolve"} tableCol := []string{"ref", "rule", "severity", "satisfied", "skipped", "resolve"}
table := formatter.CreateTable(tableCol) table := formatter.CreateTable(tableCol)
@ -52,7 +68,7 @@ var recipeLintCommand = cli.Command{
skippedOutput := "-" skippedOutput := "-"
if skipped { if skipped {
skippedOutput = "yes" skippedOutput = ""
} }
satisfied := false satisfied := false
@ -71,9 +87,9 @@ var recipeLintCommand = cli.Command{
} }
} }
satisfiedOutput := "yes" satisfiedOutput := ""
if !satisfied { if !satisfied {
satisfiedOutput = "NO" satisfiedOutput = ""
if skipped { if skipped {
satisfiedOutput = "-" satisfiedOutput = "-"
} }

View File

@ -9,7 +9,6 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -34,9 +33,7 @@ var recipeListCommand = cli.Command{
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
catl, err := recipe.ReadRecipeCatalogue(conf)
if err != nil { if err != nil {
logrus.Fatal(err.Error()) logrus.Fatal(err.Error())
} }

View File

@ -30,5 +30,7 @@ manner. Abra supports convenient automation for recipe maintainenace, see the
recipeSyncCommand, recipeSyncCommand,
recipeUpgradeCommand, recipeUpgradeCommand,
recipeVersionCommand, recipeVersionCommand,
recipeResetCommand,
recipeDiffCommand,
}, },
} }

View File

@ -13,7 +13,6 @@ import (
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
@ -60,8 +59,7 @@ your SSH keys configured on your account.
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) recipe := internal.ValidateRecipe(c)
recipe := internal.ValidateRecipeWithPrompt(c, conf)
imagesTmp, err := getImageVersions(recipe) imagesTmp, err := getImageVersions(recipe)
if err != nil { if err != nil {
@ -108,6 +106,18 @@ your SSH keys configured on your account.
} }
} }
isClean, err := gitPkg.IsClean(recipe.Dir())
if err != nil {
logrus.Fatal(err)
}
if !isClean {
logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name)
if err := gitPkg.DiffUnstaged(recipe.Dir()); err != nil {
logrus.Fatal(err)
}
}
if len(tags) > 0 { if len(tags) > 0 {
logrus.Warnf("previous git tags detected, assuming this is a new semver release") logrus.Warnf("previous git tags detected, assuming this is a new semver release")
if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil { if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil {
@ -246,7 +256,7 @@ func commitRelease(recipe recipe.Recipe, tag string) error {
msg := fmt.Sprintf("chore: publish %s release", tag) msg := fmt.Sprintf("chore: publish %s release", tag)
repoPath := path.Join(config.RECIPES_DIR, recipe.Name) repoPath := path.Join(config.RECIPES_DIR, recipe.Name)
if err := gitPkg.Commit(repoPath, ".", msg, internal.Dry); err != nil { if err := gitPkg.Commit(repoPath, msg, internal.Dry); err != nil {
return err return err
} }

56
cli/recipe/reset.go Normal file
View File

@ -0,0 +1,56 @@
package recipe
import (
"path"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var recipeResetCommand = cli.Command{
Name: "reset",
Usage: "Remove all unstaged changes from recipe config",
Description: "WARNING, this will delete your changes. Be Careful.",
Aliases: []string{"rs"},
ArgsUsage: "<recipe>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
if recipeName != "" {
internal.ValidateRecipe(c)
}
repoPath := path.Join(config.RECIPES_DIR, recipeName)
repo, err := git.PlainOpen(repoPath)
if err != nil {
logrus.Fatal(err)
}
ref, err := repo.Head()
if err != nil {
logrus.Fatal(err)
}
worktree, err := repo.Worktree()
if err != nil {
logrus.Fatal(err)
}
opts := &git.ResetOptions{Commit: ref.Hash(), Mode: git.HardReset}
if err := worktree.Reset(opts); err != nil {
logrus.Fatal(err)
}
return nil
},
}

View File

@ -8,7 +8,7 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/runtime" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
@ -29,7 +29,6 @@ var recipeSyncCommand = cli.Command{
internal.MajorFlag, internal.MajorFlag,
internal.MinorFlag, internal.MinorFlag,
internal.PatchFlag, internal.PatchFlag,
internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Description: ` Description: `
@ -42,9 +41,9 @@ Where <version> can be specifed on the command-line or Abra can attempt to
auto-generate it for you. The <recipe> configuration will be updated on the auto-generate it for you. The <recipe> configuration will be updated on the
local file system. local file system.
`, `,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) recipe := internal.ValidateRecipe(c)
recipe := internal.ValidateRecipeWithPrompt(c, conf)
mainApp, err := internal.GetMainAppImage(recipe) mainApp, err := internal.GetMainAppImage(recipe)
if err != nil { if err != nil {
@ -66,6 +65,9 @@ local file system.
nextTag := c.Args().Get(1) nextTag := c.Args().Get(1)
if len(tags) == 0 && nextTag == "" { if len(tags) == 0 && nextTag == "" {
logrus.Warnf("no git tags found for %s", recipe.Name) logrus.Warnf("no git tags found for %s", recipe.Name)
if internal.NoInput {
logrus.Fatalf("unable to continue, input required for initial version")
}
fmt.Println(fmt.Sprintf(` fmt.Println(fmt.Sprintf(`
The following options are two types of initial semantic version that you can The following options are two types of initial semantic version that you can
pick for %s that will be published in the recipe catalogue. This follows the pick for %s that will be published in the recipe catalogue. This follows the
@ -197,7 +199,17 @@ 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())
if err != nil {
logrus.Fatal(err)
}
if !isClean {
logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name)
if err := gitPkg.DiffUnstaged(recipe.Dir()); err != nil {
logrus.Fatal(err)
}
}
return nil return nil
}, },
BashComplete: autocomplete.RecipeNameComplete,
} }

View File

@ -14,8 +14,8 @@ import (
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
@ -58,7 +58,6 @@ You may invoke this command in "wizard" mode and be prompted for input:
abra recipe upgrade abra recipe upgrade
`, `,
BashComplete: autocomplete.RecipeNameComplete,
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
@ -68,14 +67,25 @@ You may invoke this command in "wizard" mode and be prompted for input:
internal.MajorFlag, internal.MajorFlag,
internal.MachineReadableFlag, internal.MachineReadableFlag,
internal.AllTagsFlag, internal.AllTagsFlag,
internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) recipe := internal.ValidateRecipe(c)
recipe := internal.ValidateRecipeWithPrompt(c, conf)
if err := recipePkg.EnsureUpToDate(recipe.Name, conf); err != nil { if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
logrus.Fatal(err)
}
if err := recipePkg.EnsureExists(recipe.Name); err != nil {
logrus.Fatal(err)
}
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err)
}
if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -185,7 +195,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
continue // skip on to the next tag and don't update any compose files continue // skip on to the next tag and don't update any compose files
} }
catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name, conf) catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name, internal.Offline)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -317,6 +327,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
} }
fmt.Println(string(jsonstring)) fmt.Println(string(jsonstring))
return nil return nil
} }
@ -327,6 +338,18 @@ You may invoke this command in "wizard" mode and be prompted for input:
} }
} }
} }
isClean, err := gitPkg.IsClean(recipeDir)
if err != nil {
logrus.Fatal(err)
}
if !isClean {
logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name)
if err := gitPkg.DiffUnstaged(recipeDir); err != nil {
logrus.Fatal(err)
}
}
return nil return nil
}, },
} }

View File

@ -1,57 +1,83 @@
package recipe package recipe
import ( import (
"fmt"
"sort"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime" "github.com/olekukonko/tablewriter"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
func sortServiceByName(versions [][]string) func(i, j int) bool {
return func(i, j int) bool {
// NOTE(d1): corresponds to the `tableCol` definition below
if versions[i][1] == "app" {
return true
}
return versions[i][1] < versions[j][1]
}
}
var recipeVersionCommand = cli.Command{ var recipeVersionCommand = cli.Command{
Name: "versions", Name: "versions",
Aliases: []string{"v"}, Aliases: []string{"v"},
Usage: "List recipe versions", Usage: "List recipe versions",
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Description: "Versions are read from the recipe catalogue.",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.OfflineFlag, internal.OfflineFlag,
internal.NoInputFlag,
internal.MachineReadableFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline)) recipe := internal.ValidateRecipe(c)
recipe := internal.ValidateRecipe(c, conf)
catalogue, err := recipePkg.ReadRecipeCatalogue(conf) catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
recipeMeta, ok := catalogue[recipe.Name] recipeMeta, ok := catl[recipe.Name]
if !ok { if !ok {
logrus.Fatalf("%s recipe doesn't exist?", recipe.Name) logrus.Fatalf("%s is not published on the catalogue?", recipe.Name)
} }
tableCol := []string{"Version", "Service", "Image", "Tag"} if len(recipeMeta.Versions) == 0 {
table := formatter.CreateTable(tableCol) logrus.Fatalf("%s has no catalogue published versions?", recipe.Name)
}
for i := len(recipeMeta.Versions) - 1; i >= 0; i-- { for i := len(recipeMeta.Versions) - 1; i >= 0; i-- {
for tag, meta := range recipeMeta.Versions[i] { tableCols := []string{"version", "service", "image", "tag"}
table := formatter.CreateTable(tableCols)
for version, meta := range recipeMeta.Versions[i] {
var versions [][]string
for service, serviceMeta := range meta { for service, serviceMeta := range meta {
table.Append([]string{tag, service, serviceMeta.Image, serviceMeta.Tag}) versions = append(versions, []string{version, service, serviceMeta.Image, serviceMeta.Tag})
}
}
} }
table.SetAutoMergeCells(true) sort.Slice(versions, sortServiceByName(versions))
if table.NumLines() > 0 { for _, version := range versions {
table.Render() table.Append(version)
}
if internal.MachineReadable {
table.JSONRender()
} else { } else {
logrus.Fatalf("%s has no published versions?", recipe.Name) table.SetAutoMergeCellsByColumnIndex([]int{0})
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.Render()
fmt.Println()
}
}
} }
return nil return nil

View File

@ -1,83 +0,0 @@
package record
import (
"context"
"fmt"
"strconv"
"coopcloud.tech/abra/cli/internal"
gandiPkg "coopcloud.tech/abra/pkg/dns/gandi"
"coopcloud.tech/abra/pkg/formatter"
"github.com/libdns/gandi"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// RecordListCommand lists domains.
var RecordListCommand = cli.Command{
Name: "list",
Usage: "List domain name records",
Aliases: []string{"ls"},
ArgsUsage: "<zone>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.DNSProviderFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `
List all domain name records managed by a 3rd party provider for a specific
zone.
You must specify a zone (e.g. example.com) under which your domain name records
are listed. This zone must already be created on your provider account.
`,
Action: func(c *cli.Context) error {
if err := internal.EnsureDNSProvider(); err != nil {
logrus.Fatal(err)
}
zone, err := internal.EnsureZoneArgument(c)
if err != nil {
logrus.Fatal(err)
}
var provider gandi.Provider
switch internal.DNSProvider {
case "gandi":
provider, err = gandiPkg.New()
if err != nil {
logrus.Fatal(err)
}
default:
logrus.Fatalf("%s is not a supported DNS provider", internal.DNSProvider)
}
records, err := provider.GetRecords(context.Background(), zone)
if err != nil {
logrus.Fatal(err)
}
tableCol := []string{"type", "name", "value", "TTL", "priority"}
table := formatter.CreateTable(tableCol)
for _, record := range records {
value := record.Value
if len(record.Value) > 30 {
value = fmt.Sprintf("%s...", record.Value[:30])
}
table.Append([]string{
record.Type,
record.Name,
value,
record.TTL.String(),
strconv.Itoa(record.Priority),
})
}
table.Render()
return nil
},
}

View File

@ -1,149 +0,0 @@
package record
import (
"context"
"fmt"
"strconv"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/dns"
gandiPkg "coopcloud.tech/abra/pkg/dns/gandi"
"coopcloud.tech/abra/pkg/formatter"
"github.com/libdns/gandi"
"github.com/libdns/libdns"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// RecordNewCommand creates a new domain name record.
var RecordNewCommand = cli.Command{
Name: "new",
Usage: "Create a new domain record",
Aliases: []string{"n"},
ArgsUsage: "<zone>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.DNSProviderFlag,
internal.DNSTypeFlag,
internal.DNSNameFlag,
internal.DNSValueFlag,
internal.DNSTTLFlag,
internal.DNSPriorityFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `
Create a new domain name record for a specific zone.
You must specify a zone (e.g. example.com) under which your domain name records
are listed. This zone must already be created on your provider account.
Example:
abra record new foo.com -p gandi -t A -n myapp -v 192.168.178.44
You may also invoke this command in "wizard" mode and be prompted for input:
abra record new
`,
Action: func(c *cli.Context) error {
zone, err := internal.EnsureZoneArgument(c)
if err != nil {
logrus.Fatal(err)
}
if err := internal.EnsureDNSProvider(); err != nil {
logrus.Fatal(err)
}
var provider gandi.Provider
switch internal.DNSProvider {
case "gandi":
provider, err = gandiPkg.New()
if err != nil {
logrus.Fatal(err)
}
default:
logrus.Fatalf("%s is not a supported DNS provider", internal.DNSProvider)
}
if err := internal.EnsureDNSTypeFlag(c); err != nil {
logrus.Fatal(err)
}
if err := internal.EnsureDNSNameFlag(c); err != nil {
logrus.Fatal(err)
}
if err := internal.EnsureDNSValueFlag(c); err != nil {
logrus.Fatal(err)
}
ttl, err := dns.GetTTL(internal.DNSTTL)
if err != nil {
return err
}
record := libdns.Record{
Type: internal.DNSType,
Name: internal.DNSName,
Value: internal.DNSValue,
TTL: ttl,
}
if internal.DNSType == "MX" || internal.DNSType == "SRV" || internal.DNSType == "URI" {
record.Priority = internal.DNSPriority
}
records, err := provider.GetRecords(context.Background(), zone)
if err != nil {
logrus.Fatal(err)
}
for _, existingRecord := range records {
if existingRecord.Type == record.Type &&
existingRecord.Name == record.Name &&
existingRecord.Value == record.Value {
logrus.Fatalf("%s record for %s already exists?", record.Type, zone)
}
}
createdRecords, err := provider.SetRecords(
context.Background(),
zone,
[]libdns.Record{record},
)
if err != nil {
logrus.Fatal(err)
}
if len(createdRecords) == 0 {
logrus.Fatal("provider library reports that no record was created?")
}
createdRecord := createdRecords[0]
tableCol := []string{"type", "name", "value", "TTL", "priority"}
table := formatter.CreateTable(tableCol)
value := createdRecord.Value
if len(createdRecord.Value) > 30 {
value = fmt.Sprintf("%s...", createdRecord.Value[:30])
}
table.Append([]string{
createdRecord.Type,
createdRecord.Name,
value,
createdRecord.TTL.String(),
strconv.Itoa(createdRecord.Priority),
})
table.Render()
logrus.Info("record created")
return nil
},
}

View File

@ -1,37 +0,0 @@
package record
import (
"github.com/urfave/cli"
)
// RecordCommand supports managing DNS entries.
var RecordCommand = cli.Command{
Name: "record",
Usage: "Manage domain name records",
Aliases: []string{"rc"},
ArgsUsage: "<record>",
Description: `
Manage domain name records via 3rd party providers such as Gandi DNS. It
supports listing, creating and removing all types of records that you might
need for managing Co-op Cloud apps.
The following providers are supported:
Gandi DNS https://www.gandi.net
You need an account with such a provider already. Typically, you need to
provide an API token on the Abra command-line when using these commands so that
you can authenticate with your provider account.
New providers can be integrated, we welcome change sets. See the underlying DNS
library documentation for more. It supports many existing providers and allows
to implement new provider support easily.
https://pkg.go.dev/github.com/libdns/libdns
`,
Subcommands: []cli.Command{
RecordListCommand,
RecordNewCommand,
RecordRemoveCommand,
},
}

View File

@ -1,137 +0,0 @@
package record
import (
"context"
"fmt"
"strconv"
"coopcloud.tech/abra/cli/internal"
gandiPkg "coopcloud.tech/abra/pkg/dns/gandi"
"coopcloud.tech/abra/pkg/formatter"
"github.com/AlecAivazis/survey/v2"
"github.com/libdns/gandi"
"github.com/libdns/libdns"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// RecordRemoveCommand lists domains.
var RecordRemoveCommand = cli.Command{
Name: "remove",
Usage: "Remove a domain name record",
Aliases: []string{"rm"},
ArgsUsage: "<zone>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.DNSProviderFlag,
internal.DNSTypeFlag,
internal.DNSNameFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `
Remove a domain name record for a specific zone.
It uses the type of record and name to match existing records and choose one
for deletion. You must specify a zone (e.g. example.com) under which your
domain name records are listed. This zone must already be created on your
provider account.
Example:
abra record remove foo.com -p gandi -t A -n myapp
You may also invoke this command in "wizard" mode and be prompted for input:
abra record rm
`,
Action: func(c *cli.Context) error {
zone, err := internal.EnsureZoneArgument(c)
if err != nil {
logrus.Fatal(err)
}
if err := internal.EnsureDNSProvider(); err != nil {
logrus.Fatal(err)
}
var provider gandi.Provider
switch internal.DNSProvider {
case "gandi":
provider, err = gandiPkg.New()
if err != nil {
logrus.Fatal(err)
}
default:
logrus.Fatalf("%s is not a supported DNS provider", internal.DNSProvider)
}
if err := internal.EnsureDNSTypeFlag(c); err != nil {
logrus.Fatal(err)
}
if err := internal.EnsureDNSNameFlag(c); err != nil {
logrus.Fatal(err)
}
records, err := provider.GetRecords(context.Background(), zone)
if err != nil {
logrus.Fatal(err)
}
var toDelete libdns.Record
for _, record := range records {
if record.Type == internal.DNSType && record.Name == internal.DNSName {
toDelete = record
break
}
}
if (libdns.Record{}) == toDelete {
logrus.Fatal("provider library reports no matching record?")
}
tableCol := []string{"type", "name", "value", "TTL", "priority"}
table := formatter.CreateTable(tableCol)
value := toDelete.Value
if len(toDelete.Value) > 30 {
value = fmt.Sprintf("%s...", toDelete.Value[:30])
}
table.Append([]string{
toDelete.Type,
toDelete.Name,
value,
toDelete.TTL.String(),
strconv.Itoa(toDelete.Priority),
})
table.Render()
if !internal.NoInput {
response := false
prompt := &survey.Confirm{
Message: "continue with record deletion?",
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
logrus.Fatal("exiting as requested")
}
}
_, err = provider.DeleteRecords(context.Background(), zone, []libdns.Record{toDelete})
if err != nil {
logrus.Fatal(err)
}
logrus.Info("record successfully deleted")
return nil
},
}

View File

@ -118,7 +118,6 @@ developer machine.
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
localFlag, localFlag,
internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
ArgsUsage: "<domain>", ArgsUsage: "<domain>",

View File

@ -60,6 +60,17 @@ var serverListCommand = cli.Command{
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if sp.Host == "" {
sp.Host = "unknown"
}
if sp.User == "" {
sp.User = "unknown"
}
if sp.Port == "" {
sp.Port = "unknown"
}
row = []string{serverName, sp.Host, sp.User, sp.Port} row = []string{serverName, sp.Host, sp.User, sp.Port}
} }
} }
@ -73,8 +84,11 @@ var serverListCommand = cli.Command{
} }
if problemsFilter { if problemsFilter {
if row[1] == "unknown" { for _, val := range row {
if val == "unknown" {
table.Append(row) table.Append(row)
break
}
} }
} else { } else {
table.Append(row) table.Append(row)

View File

@ -1,261 +0,0 @@
package server
import (
"context"
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/libcapsul"
"github.com/AlecAivazis/survey/v2"
"github.com/hetznercloud/hcloud-go/hcloud"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
func newHetznerCloudVPS(c *cli.Context) error {
if err := internal.EnsureNewHetznerCloudVPSFlags(c); err != nil {
return err
}
client := hcloud.NewClient(hcloud.WithToken(internal.HetznerCloudAPIToken))
var sshKeysRaw []string
var sshKeys []*hcloud.SSHKey
for _, sshKey := range c.StringSlice("hetzner-ssh-keys") {
if sshKey == "" {
continue
}
sshKey, _, err := client.SSHKey.GetByName(context.Background(), sshKey)
if err != nil {
return err
}
sshKeys = append(sshKeys, sshKey)
sshKeysRaw = append(sshKeysRaw, sshKey.Name)
}
serverOpts := hcloud.ServerCreateOpts{
Name: internal.HetznerCloudName,
ServerType: &hcloud.ServerType{Name: internal.HetznerCloudType},
Image: &hcloud.Image{Name: internal.HetznerCloudImage},
SSHKeys: sshKeys,
Location: &hcloud.Location{Name: internal.HetznerCloudLocation},
}
sshKeyIDs := strings.Join(sshKeysRaw, "\n")
if sshKeyIDs == "" {
sshKeyIDs = "N/A (password auth)"
}
tableColumns := []string{"name", "type", "image", "ssh-keys", "location"}
table := formatter.CreateTable(tableColumns)
table.Append([]string{
internal.HetznerCloudName,
internal.HetznerCloudType,
internal.HetznerCloudImage,
sshKeyIDs,
internal.HetznerCloudLocation,
})
table.Render()
response := false
prompt := &survey.Confirm{
Message: "continue with hetzner cloud VPS creation?",
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
logrus.Fatal("exiting as requested")
}
res, _, err := client.Server.Create(context.Background(), serverOpts)
if err != nil {
return err
}
var rootPassword string
if len(sshKeys) > 0 {
rootPassword = "N/A (using SSH keys)"
} else {
rootPassword = res.RootPassword
}
ip := res.Server.PublicNet.IPv4.IP.String()
fmt.Println(fmt.Sprintf(`
Your new Hetzner Cloud VPS has successfully been created! Here are the details:
name: %s
IP address: %s
root password: %s
You can access this new VPS via SSH using the following command:
ssh root@%s
Please note, this server is not managed by Abra yet (i.e. "abra server ls" will
not list this server)! You will need to assign a domain name record (manually
or by using "abra record new") and add the server to your Abra configuration
("abra server add") to have a working server that you can deploy Co-op Cloud
apps to.
When setting up domain name records, you probably want to set up the following
2 A records. This supports deploying apps to your root domain (e.g.
example.com) and other apps on sub-domains (e.g. foo.example.com,
bar.example.com).
@ 1800 IN A %s
* 1800 IN A %s
`,
internal.HetznerCloudName, ip, rootPassword,
ip, ip, ip,
))
return nil
}
func newCapsulVPS(c *cli.Context) error {
if err := internal.EnsureNewCapsulVPSFlags(c); err != nil {
return err
}
capsulCreateURL := fmt.Sprintf("https://%s/api/capsul/create", internal.CapsulInstanceURL)
var sshKeys []string
for _, sshKey := range c.StringSlice("capsul-ssh-keys") {
if sshKey == "" {
continue
}
sshKeys = append(sshKeys, sshKey)
}
tableColumns := []string{"instance", "name", "type", "image", "ssh-keys"}
table := formatter.CreateTable(tableColumns)
table.Append([]string{
internal.CapsulInstanceURL,
internal.CapsulName,
internal.CapsulType,
internal.CapsulImage,
strings.Join(sshKeys, "\n"),
})
table.Render()
response := false
prompt := &survey.Confirm{
Message: "continue with capsul creation?",
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
logrus.Fatal("exiting as requested")
}
capsulClient := libcapsul.New(capsulCreateURL, internal.CapsulAPIToken)
resp, err := capsulClient.Create(
internal.CapsulName,
internal.CapsulType,
internal.CapsulImage,
sshKeys,
)
if err != nil {
return err
}
fmt.Println(fmt.Sprintf(`
Your new Capsul has successfully been created! Here are the details:
Capsul name: %s
Capsul ID: %v
You will need to log into your Capsul instance web interface to retrieve the IP
address. You can learn all about how to get SSH access to your new Capsul on:
%s/about-ssh
Please note, this server is not managed by Abra yet (i.e. "abra server ls" will
not list this server)! You will need to assign a domain name record (manually
or by using "abra record new") and add the server to your Abra configuration
("abra server add") to have a working server that you can deploy Co-op Cloud
apps to.
When setting up domain name records, you probably want to set up the following
2 A records. This supports deploying apps to your root domain (e.g.
example.com) and other apps on sub-domains (e.g. foo.example.com,
bar.example.com).
@ 1800 IN A <your-capsul-ip>
* 1800 IN A <your-capsul-ip>
`, internal.CapsulName, resp.ID, internal.CapsulInstanceURL))
return nil
}
var serverNewCommand = cli.Command{
Name: "new",
Aliases: []string{"n"},
Usage: "Create a new server using a 3rd party provider",
Description: `
Create a new server via a 3rd party provider.
The following providers are supported:
Capsul https://git.cyberia.club/Cyberia/capsul-flask
Hetzner Cloud https://docs.hetzner.com/cloud
You may invoke this command in "wizard" mode and be prompted for input:
abra record new
API tokens are read from the environment if specified, e.g.
export HCLOUD_TOKEN=...
`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ServerProviderFlag,
internal.OfflineFlag,
// Capsul
internal.CapsulInstanceURLFlag,
internal.CapsulTypeFlag,
internal.CapsulImageFlag,
internal.CapsulSSHKeysFlag,
internal.CapsulAPITokenFlag,
// Hetzner
internal.HetznerCloudNameFlag,
internal.HetznerCloudTypeFlag,
internal.HetznerCloudImageFlag,
internal.HetznerCloudSSHKeysFlag,
internal.HetznerCloudLocationFlag,
internal.HetznerCloudAPITokenFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
if err := internal.EnsureServerProvider(); err != nil {
logrus.Fatal(err)
}
switch internal.ServerProvider {
case "capsul":
if err := newCapsulVPS(c); err != nil {
logrus.Fatal(err)
}
case "hetzner-cloud":
if err := newHetznerCloudVPS(c); err != nil {
logrus.Fatal(err)
}
}
return nil
},
}

View File

@ -20,12 +20,12 @@ var allFilterFlag = &cli.BoolFlag{
Destination: &allFilter, Destination: &allFilter,
} }
var volunesFilter bool var volumesFilter bool
var volumesFilterFlag = &cli.BoolFlag{ var volumesFilterFlag = &cli.BoolFlag{
Name: "volumes, v", Name: "volumes, v",
Usage: "Prune volumes. This will remove app data, Be Careful!", Usage: "Prune volumes. This will remove app data, Be Careful!",
Destination: &volunesFilter, Destination: &volumesFilter,
} }
var serverPruneCommand = cli.Command{ var serverPruneCommand = cli.Command{
@ -35,7 +35,7 @@ var serverPruneCommand = cli.Command{
Description: ` Description: `
Prunes unused containers, networks, and dangling images. Prunes unused containers, networks, and dangling images.
If passing "-v/--volumes" then volumes not connected with a deployed app will If passing "-v/--volumes" then volumes not connected to a deployed app will
also be removed. This can result in unwanted data loss if not used carefully. also be removed. This can result in unwanted data loss if not used carefully.
`, `,
ArgsUsage: "[<server>]", ArgsUsage: "[<server>]",
@ -44,12 +44,11 @@ also be removed. This can result in unwanted data loss if not used carefully.
volumesFilterFlag, volumesFilterFlag,
internal.DebugFlag, internal.DebugFlag,
internal.OfflineFlag, internal.OfflineFlag,
internal.NoInputFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.ServerNameComplete, BashComplete: autocomplete.ServerNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
var args filters.Args
serverName := internal.ValidateServer(c) serverName := internal.ValidateServer(c)
cl, err := client.New(serverName) cl, err := client.New(serverName)
@ -57,6 +56,8 @@ also be removed. This can result in unwanted data loss if not used carefully.
logrus.Fatal(err) logrus.Fatal(err)
} }
var args filters.Args
ctx := context.Background() ctx := context.Background()
cr, err := cl.ContainersPrune(ctx, args) cr, err := cl.ContainersPrune(ctx, args)
if err != nil { if err != nil {
@ -75,6 +76,7 @@ also be removed. This can result in unwanted data loss if not used carefully.
pruneFilters := filters.NewArgs() pruneFilters := filters.NewArgs()
if allFilter { if allFilter {
logrus.Debugf("removing all images, not only dangling ones")
pruneFilters.Add("dangling", "false") pruneFilters.Add("dangling", "false")
} }
@ -86,7 +88,7 @@ also be removed. This can result in unwanted data loss if not used carefully.
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed) imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
logrus.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed) logrus.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed)
if volunesFilter { if volumesFilter {
vr, err := cl.VolumesPrune(ctx, args) vr, err := cl.VolumesPrune(ctx, args)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)

View File

@ -1,8 +1,6 @@
package server package server
import ( import (
"context"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -10,168 +8,31 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"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"
"github.com/AlecAivazis/survey/v2"
"github.com/hetznercloud/hcloud-go/hcloud"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
var rmServer bool
var rmServerFlag = &cli.BoolFlag{
Name: "server, s",
Usage: "remove the actual server also",
Destination: &rmServer,
}
func rmHetznerCloudVPS(c *cli.Context) error {
if internal.HetznerCloudName == "" && !internal.NoInput {
prompt := &survey.Input{
Message: "specify hetzner cloud VPS name",
}
if err := survey.AskOne(prompt, &internal.HetznerCloudName); err != nil {
return err
}
}
if internal.HetznerCloudAPIToken == "" && !internal.NoInput {
token, ok := os.LookupEnv("HCLOUD_TOKEN")
if !ok {
prompt := &survey.Input{
Message: "specify hetzner cloud API token",
}
if err := survey.AskOne(prompt, &internal.HetznerCloudAPIToken); err != nil {
return err
}
} else {
internal.HetznerCloudAPIToken = token
}
}
client := hcloud.NewClient(hcloud.WithToken(internal.HetznerCloudAPIToken))
server, _, err := client.Server.Get(context.Background(), internal.HetznerCloudName)
if err != nil {
return err
}
if server == nil {
logrus.Fatalf("library provider reports that %s doesn't exist?", internal.HetznerCloudName)
}
fmt.Println(fmt.Sprintf(`
You have requested that Abra delete the following server (%s). Please be
absolutely sure that this is indeed the server that you would like to have
removed. There will be no going back once you confirm, the server will be
destroyed.
`, server.Name))
tableColumns := []string{"name", "type", "image", "location"}
table := formatter.CreateTable(tableColumns)
table.Append([]string{
server.Name,
server.ServerType.Name,
server.Image.Name,
server.Datacenter.Name,
})
table.Render()
response := false
prompt := &survey.Confirm{
Message: "continue with hetzner cloud VPS removal?",
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
logrus.Fatal("exiting as requested")
}
_, err = client.Server.Delete(context.Background(), server)
if err != nil {
return err
}
logrus.Infof("%s has been deleted from your hetzner cloud account", internal.HetznerCloudName)
return nil
}
var serverRemoveCommand = cli.Command{ var serverRemoveCommand = cli.Command{
Name: "remove", Name: "remove",
Aliases: []string{"rm"}, Aliases: []string{"rm"},
ArgsUsage: "[<server>]", ArgsUsage: "<server>",
Usage: "Remove a managed server", Usage: "Remove a managed server",
Description: ` Description: `Remove a managed server.
Remova a server from Abra management.
Depending on whether you used a 3rd party provider to create this server ("abra Abra will remove the internal bookkeeping (~/.abra/servers/...) and underlying
server new"), you can also destroy the virtual server as well. Pass client connection context. This server will then be lost in time, like tears in
"--server/-s" to tell Abra to try to delete this VPS. rain.
Otherwise, Abra will remove the internal bookkeeping (~/.abra/servers/...) and
underlying client connection context. This server will then be lost in time,
like tears in rain.
`, `,
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
rmServerFlag,
internal.ServerProviderFlag,
internal.OfflineFlag, internal.OfflineFlag,
// Hetzner
internal.HetznerCloudNameFlag,
internal.HetznerCloudAPITokenFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.ServerNameComplete, BashComplete: autocomplete.ServerNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
serverName := internal.ValidateServer(c) serverName := internal.ValidateServer(c)
warnMsg := `Did not pass -s/--server for actual server deletion, prompting!
Abra doesn't currently know if it helped you create this server with one of the
3rd party integrations (e.g. Capsul). You have a choice here to actually,
really and finally destroy this server using those integrations. If you want to
do this, choose Yes.
If you just want to remove the server config files & context, choose No.
`
if !rmServer {
logrus.Warn(fmt.Sprintf(warnMsg))
response := false
prompt := &survey.Confirm{
Message: "delete actual live server?",
}
if err := survey.AskOne(prompt, &response); err != nil {
logrus.Fatal(err)
}
if response {
logrus.Info("setting -s/--server and attempting to remove actual server")
rmServer = true
}
}
if rmServer {
if err := internal.EnsureServerProvider(); err != nil {
logrus.Fatal(err)
}
switch internal.ServerProvider {
case "capsul":
logrus.Warn("capsul provider does not support automatic removal yet, sorry!")
case "hetzner-cloud":
if err := rmHetznerCloudVPS(c); err != nil {
logrus.Fatal(err)
}
}
}
if err := client.DeleteContext(serverName); err != nil { if err := client.DeleteContext(serverName); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -9,16 +9,7 @@ var ServerCommand = cli.Command{
Name: "server", Name: "server",
Aliases: []string{"s"}, Aliases: []string{"s"},
Usage: "Manage servers", Usage: "Manage servers",
Description: `
Create, manage and remove servers using 3rd party integrations.
Servers can be created from scratch using the "abra server new" command. If you
already have a server, you can add it to your configuration using "abra server
add". Abra can provision servers so that they are ready to deploy Co-op Cloud
recipes, see available flags on "abra server add" for more.
`,
Subcommands: []cli.Command{ Subcommands: []cli.Command{
serverNewCommand,
serverAddCommand, serverAddCommand,
serverListCommand, serverListCommand,
serverRemoveCommand, serverRemoveCommand,

View File

@ -12,7 +12,6 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"coopcloud.tech/abra/pkg/upstream/convert" "coopcloud.tech/abra/pkg/upstream/convert"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
@ -58,8 +57,6 @@ catalogue. If a new patch/minor version is available, a notification is
printed. To include major versions use the --major flag. printed. To include major versions use the --major flag.
`, `,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline))
cl, err := client.New("default") cl, err := client.New("default")
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -78,7 +75,7 @@ printed. To include major versions use the --major flag.
} }
if recipeName != "" { if recipeName != "" {
_, err = getLatestUpgrade(cl, stackName, recipeName, conf) _, err = getLatestUpgrade(cl, stackName, recipeName)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -113,8 +110,6 @@ break things. Only apps that are not deployed with "--chaos" are upgraded, to
update chaos deployments use the "--chaos" flag. Use it with care. update chaos deployments use the "--chaos" flag. Use it with care.
`, `,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
conf := runtime.New(runtime.WithOffline(internal.Offline))
cl, err := client.New("default") cl, err := client.New("default")
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -123,7 +118,7 @@ update chaos deployments use the "--chaos" flag. Use it with care.
if !updateAll { if !updateAll {
stackName := c.Args().Get(0) stackName := c.Args().Get(0)
recipeName := c.Args().Get(1) recipeName := c.Args().Get(1)
err = tryUpgrade(cl, stackName, recipeName, conf) err = tryUpgrade(cl, stackName, recipeName)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -143,7 +138,7 @@ update chaos deployments use the "--chaos" flag. Use it with care.
logrus.Fatal(err) logrus.Fatal(err)
} }
err = tryUpgrade(cl, stackName, recipeName, conf) err = tryUpgrade(cl, stackName, recipeName)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -227,14 +222,13 @@ func getEnv(cl *dockerclient.Client, stackName string) (config.AppEnv, error) {
// getLatestUpgrade returns the latest available version for an app respecting // getLatestUpgrade returns the latest available version for an app respecting
// the "--major" flag if it is newer than the currently deployed version. // the "--major" flag if it is newer than the currently deployed version.
func getLatestUpgrade(cl *dockerclient.Client, stackName string, func getLatestUpgrade(cl *dockerclient.Client, stackName string, recipeName string) (string, error) {
recipeName string, conf *runtime.Config) (string, error) {
deployedVersion, err := getDeployedVersion(cl, stackName, recipeName) deployedVersion, err := getDeployedVersion(cl, stackName, recipeName)
if err != nil { if err != nil {
return "", err return "", err
} }
availableUpgrades, err := getAvailableUpgrades(cl, stackName, recipeName, deployedVersion, conf) availableUpgrades, err := getAvailableUpgrades(cl, stackName, recipeName, deployedVersion)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -277,8 +271,8 @@ func getDeployedVersion(cl *dockerclient.Client, stackName string, recipeName st
// than the deployed version. It only includes major upgrades if the "--major" // than the deployed version. It only includes major upgrades if the "--major"
// flag is set. // flag is set.
func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName string, func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName string,
deployedVersion string, conf *runtime.Config) ([]string, error) { deployedVersion string) ([]string, error) {
catl, err := recipe.ReadRecipeCatalogue(conf) catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -322,12 +316,12 @@ 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, conf *runtime.Config) error { func processRecipeRepoVersion(recipeName, version string) error {
if err := recipe.EnsureExists(recipeName, conf); err != nil { if err := recipe.EnsureExists(recipeName); err != nil {
return err return err
} }
if err := recipe.EnsureUpToDate(recipeName, conf); err != nil { if err := recipe.EnsureUpToDate(recipeName); err != nil {
return err return err
} }
@ -335,7 +329,7 @@ func processRecipeRepoVersion(recipeName, version string, conf *runtime.Config)
return err return err
} }
if r, err := recipe.Get(recipeName, conf); err != nil { if r, err := recipe.Get(recipeName, internal.Offline); err != nil {
return err return err
} else if err := lint.LintForErrors(r); err != nil { } else if err := lint.LintForErrors(r); err != nil {
return err return err
@ -370,7 +364,7 @@ func createDeployConfig(recipeName string, stackName string, env config.AppEnv)
ResolveImage: stack.ResolveImageAlways, ResolveImage: stack.ResolveImageAlways,
} }
composeFiles, err := config.GetAppComposeFiles(recipeName, env) composeFiles, err := config.GetComposeFiles(recipeName, env)
if err != nil { if err != nil {
return nil, deployOpts, err return nil, deployOpts, err
} }
@ -392,7 +386,7 @@ func createDeployConfig(recipeName string, stackName string, env config.AppEnv)
} }
// tryUpgrade performs the upgrade if all the requirements are fulfilled. // tryUpgrade performs the upgrade if all the requirements are fulfilled.
func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string, conf *runtime.Config) error { func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
if recipeName == "" { if recipeName == "" {
logrus.Debugf("don't update %s due to missing recipe name", stackName) logrus.Debugf("don't update %s due to missing recipe name", stackName)
return nil return nil
@ -418,7 +412,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string, conf *run
return nil return nil
} }
upgradeVersion, err := getLatestUpgrade(cl, stackName, recipeName, conf) upgradeVersion, err := getLatestUpgrade(cl, stackName, recipeName)
if err != nil { if err != nil {
return err return err
} }
@ -428,14 +422,14 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string, conf *run
return nil return nil
} }
err = upgrade(cl, stackName, recipeName, upgradeVersion, conf) err = upgrade(cl, stackName, recipeName, upgradeVersion)
return err return err
} }
// upgrade performs all necessary steps to upgrade an app. // upgrade performs all necessary steps to upgrade an app.
func upgrade(cl *dockerclient.Client, stackName, recipeName, func upgrade(cl *dockerclient.Client, stackName, recipeName,
upgradeVersion string, conf *runtime.Config) error { upgradeVersion string) error {
env, err := getEnv(cl, stackName) env, err := getEnv(cl, stackName)
if err != nil { if err != nil {
return err return err
@ -448,7 +442,7 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName,
Env: env, Env: env,
} }
if err = processRecipeRepoVersion(recipeName, upgradeVersion, conf); err != nil { if err = processRecipeRepoVersion(recipeName, upgradeVersion); err != nil {
return err return err
} }

94
go.mod
View File

@ -1,28 +1,98 @@
module coopcloud.tech/abra module coopcloud.tech/abra
go 1.16 go 1.21
require ( require (
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52 coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52
github.com/AlecAivazis/survey/v2 v2.3.7 github.com/AlecAivazis/survey/v2 v2.3.7
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731094149-b031ea1211e7 github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731094149-b031ea1211e7
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.5+incompatible github.com/docker/cli v24.0.7+incompatible
github.com/docker/distribution v2.8.2+incompatible github.com/docker/distribution v2.8.3+incompatible
github.com/docker/docker v24.0.5+incompatible github.com/docker/docker v24.0.7+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.8.1 github.com/go-git/go-git/v5 v5.10.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.13.1 github.com/schollz/progressbar/v3 v3.14.0
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
gotest.tools/v3 v3.5.0 gotest.tools/v3 v3.5.1
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/BurntSushi/toml v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/hcsshim v0.9.2 // 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/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.5.0 // 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-metrics v0.0.1 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/emirpasic/gods v1.18.1 // 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/go-billy/v5 v5.5.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.14.2 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/miekg/pkcs11 v1.0.3 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/runc v1.1.0 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/skeema/knownhosts v1.2.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/term v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
golang.org/x/tools v0.13.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )
require ( require (
coopcloud.tech/libcapsul v0.0.0-20230605070824-878af473f07b
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect 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.5.9 // indirect
@ -35,18 +105,16 @@ require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/mux v1.8.0 // indirect
github.com/hashicorp/go-retryablehttp v0.7.4 github.com/hashicorp/go-retryablehttp v0.7.4
github.com/hetznercloud/hcloud-go v1.48.0
github.com/klauspost/pgzip v1.2.6 github.com/klauspost/pgzip v1.2.6
github.com/libdns/gandi v1.0.2
github.com/libdns/libdns v0.2.1
github.com/moby/patternmatcher v0.5.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.0.3-0.20211202193544-a5463b7f9c84 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/prometheus/client_golang v1.16.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect
github.com/spf13/cobra v1.3.0 // indirect github.com/spf13/cobra v1.3.0 // indirect
github.com/stretchr/testify v1.8.4
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.9
github.com/xeipuuv/gojsonpointer v0.0.0-20190809123943-df4f5c81cb3b // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190809123943-df4f5c81cb3b // indirect
golang.org/x/sys v0.10.0 golang.org/x/sys v0.14.0
) )

154
go.sum
View File

@ -34,7 +34,6 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
@ -47,8 +46,6 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
coopcloud.tech/libcapsul v0.0.0-20230605070824-878af473f07b h1:ORxAmzrd6SSlSGm/RdPM2TSTTe5+QWp7rqzjEY/pIKA=
coopcloud.tech/libcapsul v0.0.0-20230605070824-878af473f07b/go.mod h1:6u7ekg+v+yL07QtU7E+K+WqK9LKDDqTF4s+PrIXZ+QM=
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52 h1:cyFFOl0tKe+dVHt8saejG8xoff33eQiHxFCVzRpPUjM= coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52 h1:cyFFOl0tKe+dVHt8saejG8xoff33eQiHxFCVzRpPUjM=
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ= coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
@ -110,8 +107,8 @@ github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb0
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs= github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg=
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
@ -121,13 +118,10 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/O
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
github.com/alecthomas/kingpin/v2 v2.3.1/go.mod h1:oYL5vtsvEHZGHxU7DMp32Dvx+qL+ptGn6lWaot2vCNE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
@ -170,7 +164,6 @@ github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7N
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@ -324,8 +317,9 @@ github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
@ -341,18 +335,20 @@ github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8l
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v24.0.5+incompatible h1:WeBimjvS0eKdH4Ygx+ihVq1Q++xg36M/rMi4aXAvodc= github.com/docker/cli v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1xfI36MSkFg=
github.com/docker/cli v24.0.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v24.0.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY= github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
github.com/docker/docker v24.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o= github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o=
github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c=
@ -377,9 +373,8 @@ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:Htrtb
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@ -420,26 +415,20 @@ github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo= github.com/go-git/go-git/v5 v5.10.0 h1:F0x3xXrAWmhwtzoCokU4IMPcBdncG+HAAqi9FcOOjbQ=
github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A= github.com/go-git/go-git/v5 v5.10.0/go.mod h1:1FOZ/pQnqw24ghP2n7cunVl0ON55BsjPYvhWHvZGhoo=
github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 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-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
@ -526,7 +515,6 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=
@ -621,8 +609,6 @@ 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/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.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hetznercloud/hcloud-go v1.48.0 h1:b6x0ABNYJr8zVX9sE1kCNMU/sndzI8vmZjxEWEr+Gn0=
github.com/hetznercloud/hcloud-go v1.48.0/go.mod h1:VzDWThl47lOnZXY0q5/LPFD+M62pfe/52TV+mOrpp9Q=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= 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/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@ -640,7 +626,6 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE= github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE=
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc= github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc=
@ -650,7 +635,6 @@ github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht
github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@ -662,7 +646,6 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
@ -695,11 +678,6 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/libdns/gandi v1.0.2 h1:1Ts8UpI1x5PVKpOjKC7Dn4+EObndz9gm6vdZnloHSKQ=
github.com/libdns/gandi v1.0.2/go.mod h1:hxpbQKcQFgQrTS5lV4tAgn6QoL6HcCnoBJaW5nOW4Sk=
github.com/libdns/libdns v0.1.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@ -762,7 +740,6 @@ 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 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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/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/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/patternmatcher v0.5.0 h1:YCZgJOeULcxLw1Q+sVR636pmS7sPEn1Qo2iAN6M7DBo= github.com/moby/patternmatcher v0.5.0 h1:YCZgJOeULcxLw1Q+sVR636pmS7sPEn1Qo2iAN6M7DBo=
github.com/moby/patternmatcher v0.5.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/patternmatcher v0.5.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
@ -789,7 +766,6 @@ github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
@ -814,6 +790,8 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
@ -856,7 +834,6 @@ github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -876,9 +853,6 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
@ -895,9 +869,6 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@ -911,8 +882,6 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
@ -920,12 +889,9 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@ -940,7 +906,6 @@ github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
@ -952,7 +917,6 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM= github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM=
@ -996,8 +960,6 @@ github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@ -1005,8 +967,6 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
@ -1050,7 +1010,6 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xhit/go-str2duration v1.2.0/go.mod h1:3cPSlfZlUHVlneIVfePFWcJZsuwf+P1v2SRTV4cUmp4=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -1086,7 +1045,6 @@ 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/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.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= 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-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-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@ -1109,13 +1067,10 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/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.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -1153,10 +1108,9 @@ 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.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= 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-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -1207,23 +1161,15 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 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.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1241,8 +1187,6 @@ golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -1254,11 +1198,10 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1318,7 +1261,6 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -1349,7 +1291,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -1366,34 +1307,24 @@ golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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-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.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.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-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1406,9 +1337,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -1481,9 +1411,9 @@ 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.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/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.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/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -1642,8 +1572,6 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
@ -1688,12 +1616,11 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@ -1732,7 +1659,6 @@ 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/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= 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/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/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 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= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=

View File

@ -5,7 +5,6 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -28,12 +27,7 @@ func AppNameComplete(c *cli.Context) {
// RecipeNameComplete completes recipe names. // RecipeNameComplete completes recipe names.
func RecipeNameComplete(c *cli.Context) { func RecipeNameComplete(c *cli.Context) {
// defaults since we can't take arguments here... this means auto-completion catl, err := recipe.ReadRecipeCatalogue(false)
// of recipe names always access the network if e.g. the catalogue needs
// cloning / updating
conf := runtime.New()
catl, err := recipe.ReadRecipeCatalogue(conf)
if err != nil { if err != nil {
logrus.Warn(err) logrus.Warn(err)
} }
@ -74,7 +68,6 @@ func SubcommandComplete(c *cli.Context) {
"autocomplete", "autocomplete",
"catalogue", "catalogue",
"recipe", "recipe",
"record",
"server", "server",
"upgrade", "upgrade",
} }

View File

@ -8,58 +8,15 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/runtime"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// CatalogueSkipList is all the repos that are not recipes.
var CatalogueSkipList = map[string]bool{
"abra": true,
"abra-apps": true,
"abra-aur": true,
"abra-bash": true,
"abra-capsul": true,
"abra-gandi": true,
"abra-hetzner": true,
"apps": true,
"aur-abra-git": true,
"auto-recipes-catalogue-json": true,
"auto-mirror": true,
"backup-bot": true,
"backup-bot-two": true,
"beta.coopcloud.tech": true,
"comrade-renovate-bot": true,
"coopcloud.tech": true,
"coturn": true,
"docker-cp-deploy": true,
"docker-dind-bats-kcov": true,
"docs.coopcloud.tech": true,
"drone-abra": true,
"example": true,
"gardening": true,
"go-abra": true,
"organising": true,
"pyabra": true,
"radicle-seed-node": true,
"recipes-catalogue-json": true,
"recipes-wishlist": true,
"recipes.coopcloud.tech": true,
"stack-ssh-deploy": true,
"swarm-cronjob": true,
"tagcmp": true,
"traefik-cert-dumper": true,
"tyop": true,
}
// EnsureCatalogue ensures that the catalogue is cloned locally & present. // EnsureCatalogue ensures that the catalogue is cloned locally & present.
func EnsureCatalogue(conf *runtime.Config) error { func EnsureCatalogue() error {
catalogueDir := path.Join(config.ABRA_DIR, "catalogue") catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) { if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) {
if conf.Offline { logrus.Warnf("local recipe catalogue is missing, retrieving now")
return fmt.Errorf("no local copy of the catalogue available, network access required")
}
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME) url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME)
if err := gitPkg.Clone(catalogueDir, url); err != nil { if err := gitPkg.Clone(catalogueDir, url); err != nil {
return err return err
@ -71,9 +28,8 @@ func EnsureCatalogue(conf *runtime.Config) error {
return nil return nil
} }
// EnsureUpToDate ensures that the local catalogue has no unstaged changes as // EnsureIsClean makes sure that the catalogue has no unstaged changes.
// is up to date. This is useful to run before doing catalogue generation. func EnsureIsClean() error {
func EnsureUpToDate(conf *runtime.Config) error {
isClean, err := gitPkg.IsClean(config.CATALOGUE_DIR) isClean, err := gitPkg.IsClean(config.CATALOGUE_DIR)
if err != nil { if err != nil {
return err return err
@ -84,11 +40,11 @@ func EnsureUpToDate(conf *runtime.Config) error {
return fmt.Errorf(msg, config.CATALOGUE_DIR) return fmt.Errorf(msg, config.CATALOGUE_DIR)
} }
if conf.Offline {
logrus.Debug("attempting to use local catalogue without access network (\"--offline\")")
return nil return nil
} }
// EnsureUpToDate ensures that the local catalogue is up to date.
func EnsureUpToDate() error {
repo, err := git.PlainOpen(config.CATALOGUE_DIR) repo, err := git.PlainOpen(config.CATALOGUE_DIR)
if err != nil { if err != nil {
return err return err

View File

@ -29,7 +29,7 @@ func UpdateTag(pattern, image, tag, recipeName string) (bool, error) {
opts := stack.Deploy{Composefiles: []string{composeFile}} opts := stack.Deploy{Composefiles: []string{composeFile}}
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample") envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath) sampleEnv, err := config.ReadEnv(envSamplePath, config.ReadEnvOptions{})
if err != nil { if err != nil {
return false, err return false, err
} }
@ -97,7 +97,7 @@ func UpdateLabel(pattern, serviceName, label, recipeName string) error {
opts := stack.Deploy{Composefiles: []string{composeFile}} opts := stack.Deploy{Composefiles: []string{composeFile}}
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample") envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath) sampleEnv, err := config.ReadEnv(envSamplePath, config.ReadEnvOptions{})
if err != nil { if err != nil {
return err return err
} }
@ -122,7 +122,7 @@ func UpdateLabel(pattern, serviceName, label, recipeName string) error {
discovered := false discovered := false
for oldLabel, value := range service.Deploy.Labels { for oldLabel, value := range service.Deploy.Labels {
if strings.HasPrefix(oldLabel, "coop-cloud") { if strings.HasPrefix(oldLabel, "coop-cloud") && strings.Contains(oldLabel, "version") {
discovered = true discovered = true
bytes, err := ioutil.ReadFile(composeFile) bytes, err := ioutil.ReadFile(composeFile)

View File

@ -76,7 +76,7 @@ func (a App) StackName() string {
func (a App) Filters(appendServiceNames, exactMatch bool) (filters.Args, error) { func (a App) Filters(appendServiceNames, exactMatch bool) (filters.Args, error) {
filters := filters.NewArgs() filters := filters.NewArgs()
composeFiles, err := GetAppComposeFiles(a.Recipe, a.Env) composeFiles, err := GetComposeFiles(a.Recipe, a.Env)
if err != nil { if err != nil {
return filters, err return filters, err
} }
@ -149,15 +149,15 @@ func (a ByName) Less(i, j int) bool {
return strings.ToLower(a[i].Name) < strings.ToLower(a[j].Name) return strings.ToLower(a[i].Name) < strings.ToLower(a[j].Name)
} }
func readAppEnvFile(appFile AppFile, name AppName) (App, error) { func ReadAppEnvFile(appFile AppFile, name AppName) (App, error) {
env, err := ReadEnv(appFile.Path) env, err := ReadEnv(appFile.Path, ReadEnvOptions{})
if err != nil { if err != nil {
return App{}, fmt.Errorf("env file for %s couldn't be read: %s", name, err.Error()) return App{}, fmt.Errorf("env file for %s couldn't be read: %s", name, err.Error())
} }
logrus.Debugf("read env %s from %s", env, appFile.Path) logrus.Debugf("read env %s from %s", env, appFile.Path)
app, err := newApp(env, name, appFile) app, err := NewApp(env, name, appFile)
if err != nil { if err != nil {
return App{}, fmt.Errorf("env file for %s has issues: %s", name, err.Error()) return App{}, fmt.Errorf("env file for %s has issues: %s", name, err.Error())
} }
@ -165,8 +165,8 @@ func readAppEnvFile(appFile AppFile, name AppName) (App, error) {
return app, nil return app, nil
} }
// newApp creates new App object // NewApp creates new App object
func newApp(env AppEnv, name string, appFile AppFile) (App, error) { func NewApp(env AppEnv, name string, appFile AppFile) (App, error) {
domain := env["DOMAIN"] domain := env["DOMAIN"]
recipe, exists := env["RECIPE"] recipe, exists := env["RECIPE"]
@ -232,7 +232,7 @@ func GetApp(apps AppFiles, name AppName) (App, error) {
return App{}, fmt.Errorf("cannot find app with name %s", name) return App{}, fmt.Errorf("cannot find app with name %s", name)
} }
app, err := readAppEnvFile(appFile, name) app, err := ReadAppEnvFile(appFile, name)
if err != nil { if err != nil {
return App{}, err return App{}, err
} }
@ -277,7 +277,7 @@ func GetAppServiceNames(appName string) ([]string, error) {
return serviceNames, err return serviceNames, err
} }
composeFiles, err := GetAppComposeFiles(app.Recipe, app.Env) composeFiles, err := GetComposeFiles(app.Recipe, app.Env)
if err != nil { if err != nil {
return serviceNames, err return serviceNames, err
} }
@ -326,7 +326,7 @@ func TemplateAppEnvSample(recipeName, appName, server, domain string) error {
} }
appEnvPath := path.Join(ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName)) appEnvPath := path.Join(ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName))
if _, err := os.Stat(appEnvPath); os.IsExist(err) { if _, err := os.Stat(appEnvPath); !os.IsNotExist(err) {
return fmt.Errorf("%s already exists?", appEnvPath) return fmt.Errorf("%s already exists?", appEnvPath)
} }
@ -437,26 +437,56 @@ func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]str
return statuses, nil return statuses, nil
} }
// GetAppComposeFiles gets the list of compose files for an app which should be // ensurePathExists ensures that a path exists.
// merged into a composetypes.Config while respecting the COMPOSE_FILE env var. func ensurePathExists(path string) error {
func GetAppComposeFiles(recipe string, appEnv AppEnv) ([]string, error) { if _, err := os.Stat(path); err != nil && os.IsNotExist(err) {
return err
}
return nil
}
// GetComposeFiles gets the list of compose files for an app (or recipe if you
// don't already have an app) which should be merged into a composetypes.Config
// while respecting the COMPOSE_FILE env var.
func GetComposeFiles(recipe string, appEnv AppEnv) ([]string, error) {
var composeFiles []string var composeFiles []string
if _, ok := appEnv["COMPOSE_FILE"]; !ok { composeFileEnvVar, ok := appEnv["COMPOSE_FILE"]
logrus.Debug("no COMPOSE_FILE detected, loading compose.yml") if !ok {
path := fmt.Sprintf("%s/%s/compose.yml", RECIPES_DIR, recipe) path := fmt.Sprintf("%s/%s/compose.yml", RECIPES_DIR, recipe)
if err := ensurePathExists(path); err != nil {
return composeFiles, err
}
logrus.Debugf("no COMPOSE_FILE detected, loading default: %s", path)
composeFiles = append(composeFiles, path) composeFiles = append(composeFiles, path)
return composeFiles, nil return composeFiles, nil
} }
composeFileEnvVar := appEnv["COMPOSE_FILE"] if !strings.Contains(composeFileEnvVar, ":") {
envVars := strings.Split(composeFileEnvVar, ":") path := fmt.Sprintf("%s/%s/%s", RECIPES_DIR, recipe, composeFileEnvVar)
logrus.Debugf("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", ")) if err := ensurePathExists(path); err != nil {
for _, file := range strings.Split(composeFileEnvVar, ":") { return composeFiles, err
}
logrus.Debugf("COMPOSE_FILE detected, loading %s", path)
composeFiles = append(composeFiles, path)
return composeFiles, nil
}
numComposeFiles := strings.Count(composeFileEnvVar, ":") + 1
envVars := strings.SplitN(composeFileEnvVar, ":", numComposeFiles)
if len(envVars) != numComposeFiles {
return composeFiles, fmt.Errorf("COMPOSE_FILE (=\"%s\") parsing failed?", composeFileEnvVar)
}
for _, file := range envVars {
path := fmt.Sprintf("%s/%s/%s", RECIPES_DIR, recipe, file) path := fmt.Sprintf("%s/%s/%s", RECIPES_DIR, recipe, file)
if err := ensurePathExists(path); err != nil {
return composeFiles, err
}
composeFiles = append(composeFiles, path) composeFiles = append(composeFiles, path)
} }
logrus.Debugf("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", "))
logrus.Debugf("retrieved %s configs for %s", strings.Join(composeFiles, ", "), recipe) logrus.Debugf("retrieved %s configs for %s", strings.Join(composeFiles, ", "), recipe)
return composeFiles, nil return composeFiles, nil

View File

@ -1,36 +1,108 @@
package config package config_test
import ( import (
"fmt"
"reflect" "reflect"
"testing" "testing"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"github.com/stretchr/testify/assert"
) )
func TestNewApp(t *testing.T) { func TestNewApp(t *testing.T) {
app, err := newApp(expectedAppEnv, appName, expectedAppFile) app, err := config.NewApp(ExpectedAppEnv, AppName, ExpectedAppFile)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !reflect.DeepEqual(app, expectedApp) { if !reflect.DeepEqual(app, ExpectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp) t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, ExpectedApp)
} }
} }
func TestReadAppEnvFile(t *testing.T) { func TestReadAppEnvFile(t *testing.T) {
app, err := readAppEnvFile(expectedAppFile, appName) app, err := config.ReadAppEnvFile(ExpectedAppFile, AppName)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !reflect.DeepEqual(app, expectedApp) { if !reflect.DeepEqual(app, ExpectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp) t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, ExpectedApp)
} }
} }
func TestGetApp(t *testing.T) { func TestGetApp(t *testing.T) {
app, err := GetApp(expectedAppFiles, appName) app, err := config.GetApp(ExpectedAppFiles, AppName)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !reflect.DeepEqual(app, expectedApp) { if !reflect.DeepEqual(app, ExpectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp) t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, ExpectedApp)
}
}
func TestGetComposeFiles(t *testing.T) {
offline := true
r, err := recipe.Get("abra-test-recipe", offline)
if err != nil {
t.Fatal(err)
}
tests := []struct {
appEnv map[string]string
composeFiles []string
}{
{
map[string]string{},
[]string{
fmt.Sprintf("%s/%s/compose.yml", config.RECIPES_DIR, r.Name),
},
},
{
map[string]string{"COMPOSE_FILE": "compose.yml"},
[]string{
fmt.Sprintf("%s/%s/compose.yml", config.RECIPES_DIR, r.Name),
},
},
{
map[string]string{"COMPOSE_FILE": "compose.extra_secret.yml"},
[]string{
fmt.Sprintf("%s/%s/compose.extra_secret.yml", config.RECIPES_DIR, r.Name),
},
},
{
map[string]string{"COMPOSE_FILE": "compose.yml:compose.extra_secret.yml"},
[]string{
fmt.Sprintf("%s/%s/compose.yml", config.RECIPES_DIR, r.Name),
fmt.Sprintf("%s/%s/compose.extra_secret.yml", config.RECIPES_DIR, r.Name),
},
},
}
for _, test := range tests {
composeFiles, err := config.GetComposeFiles(r.Name, test.appEnv)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, composeFiles, test.composeFiles)
}
}
func TestGetComposeFilesError(t *testing.T) {
offline := true
r, err := recipe.Get("abra-test-recipe", offline)
if err != nil {
t.Fatal(err)
}
tests := []struct{ appEnv map[string]string }{
{map[string]string{"COMPOSE_FILE": "compose.yml::compose.foo.yml"}},
{map[string]string{"COMPOSE_FILE": "doesnt.exist.yml"}},
}
for _, test := range tests {
_, err := config.GetComposeFiles(r.Name, test.appEnv)
if err == nil {
t.Fatalf("should have failed: %v", test.appEnv)
}
} }
} }

View File

@ -8,13 +8,24 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"regexp"
"sort"
"strings" "strings"
"github.com/Autonomic-Cooperative/godotenv" "github.com/Autonomic-Cooperative/godotenv"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
var ABRA_DIR = os.ExpandEnv("$HOME/.abra") // getBaseDir retrieves the Abra base directory.
func getBaseDir() string {
home := os.ExpandEnv("$HOME/.abra")
if customAbraDir, exists := os.LookupEnv("ABRA_DIR"); exists && customAbraDir != "" {
home = customAbraDir
}
return home
}
var ABRA_DIR = getBaseDir()
var SERVERS_DIR = path.Join(ABRA_DIR, "servers") var SERVERS_DIR = path.Join(ABRA_DIR, "servers")
var RECIPES_DIR = path.Join(ABRA_DIR, "recipes") var RECIPES_DIR = path.Join(ABRA_DIR, "recipes")
var VENDOR_DIR = path.Join(ABRA_DIR, "vendor") var VENDOR_DIR = path.Join(ABRA_DIR, "vendor")
@ -25,6 +36,11 @@ 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"
// envVarModifiers is a list of env var modifier strings. These are added to
// env vars as comments and modify their processing by Abra, e.g. determining
// how long secrets should be.
var envVarModifiers = []string{"length"}
// GetServers retrieves all servers. // GetServers retrieves all servers.
func GetServers() ([]string, error) { func GetServers() ([]string, error) {
var servers []string var servers []string
@ -39,18 +55,43 @@ func GetServers() ([]string, error) {
return servers, nil return servers, nil
} }
// ReadEnv loads an app envivornment into a map. // ReadEnvOptions modifies the ReadEnv processing of env vars.
func ReadEnv(filePath string) (AppEnv, error) { type ReadEnvOptions struct {
var envFile AppEnv IncludeModifiers bool
}
envFile, err := godotenv.Read(filePath) // ContainsEnvVarModifier determines if an env var contains a modifier.
func ContainsEnvVarModifier(envVar string) bool {
for _, mod := range envVarModifiers {
if strings.Contains(envVar, fmt.Sprintf("%s=", mod)) {
return true
}
}
return false
}
// ReadEnv loads an app envivornment into a map.
func ReadEnv(filePath string, opts ReadEnvOptions) (AppEnv, error) {
var envVars AppEnv
envVars, err := godotenv.Read(filePath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
logrus.Debugf("read %s from %s", envFile, filePath) // for idx, envVar := range envVars {
// if strings.Contains(envVar, "#") {
// if opts.IncludeModifiers && ContainsEnvVarModifier(envVar) {
// continue
// }
// vals := strings.Split(envVar, "#")
// envVars[idx] = strings.TrimSpace(vals[0])
// }
// }
return envFile, nil logrus.Debugf("read %s from %s", envVars, filePath)
return envVars, nil
} }
// ReadServerNames retrieves all server names. // ReadServerNames retrieves all server names.
@ -140,22 +181,107 @@ func ReadAbraShEnvVars(abraSh string) (map[string]string, error) {
} }
return envVars, err return envVars, err
} }
defer file.Close()
exportRegex, err := regexp.Compile(`^export\s+(\w+=\w+)`)
if err != nil {
return envVars, err
}
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() txt := scanner.Text()
if strings.Contains(line, "export") { if exportRegex.MatchString(txt) {
splitVals := strings.Split(line, "export ") splitVals := strings.Split(txt, "export ")
envVarDef := splitVals[len(splitVals)-1] envVarDef := splitVals[len(splitVals)-1]
keyVal := strings.Split(envVarDef, "=") keyVal := strings.Split(envVarDef, "=")
if len(keyVal) != 2 { if len(keyVal) != 2 {
return envVars, fmt.Errorf("couldn't parse %s", line) return envVars, fmt.Errorf("couldn't parse %s", txt)
} }
envVars[keyVal[0]] = keyVal[1] envVars[keyVal[0]] = keyVal[1]
} }
} }
if len(envVars) > 0 {
logrus.Debugf("read %s from %s", envVars, abraSh) logrus.Debugf("read %s from %s", envVars, abraSh)
} else {
logrus.Debugf("read 0 env var exports from %s", abraSh)
}
return envVars, nil return envVars, nil
} }
type EnvVar struct {
Name string
Present bool
}
func CheckEnv(app App) ([]EnvVar, error) {
var envVars []EnvVar
envSamplePath := path.Join(RECIPES_DIR, app.Recipe, ".env.sample")
if _, err := os.Stat(envSamplePath); err != nil {
if os.IsNotExist(err) {
return envVars, fmt.Errorf("%s does not exist?", envSamplePath)
}
return envVars, err
}
envSample, err := ReadEnv(envSamplePath, ReadEnvOptions{})
if err != nil {
return envVars, err
}
var keys []string
for key := range envSample {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
if _, ok := app.Env[key]; ok {
envVars = append(envVars, EnvVar{Name: key, Present: true})
} else {
envVars = append(envVars, EnvVar{Name: key, Present: false})
}
}
return envVars, nil
}
// ReadAbraShCmdNames reads the names of commands.
func ReadAbraShCmdNames(abraSh string) ([]string, error) {
var cmdNames []string
file, err := os.Open(abraSh)
if err != nil {
if os.IsNotExist(err) {
return cmdNames, nil
}
return cmdNames, err
}
defer file.Close()
cmdNameRegex, err := regexp.Compile(`(\w+)(\(\).*\{)`)
if err != nil {
return cmdNames, err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
matches := cmdNameRegex.FindStringSubmatch(line)
if len(matches) > 0 {
cmdNames = append(cmdNames, matches[1])
}
}
if len(cmdNames) > 0 {
logrus.Debugf("read %s from %s", strings.Join(cmdNames, " "), abraSh)
} else {
logrus.Debugf("read 0 command names from %s", abraSh)
}
return cmdNames, nil
}

View File

@ -1,60 +1,63 @@
package config package config_test
import ( import (
"fmt"
"os" "os"
"path" "path"
"reflect" "reflect"
"slices"
"strings" "strings"
"testing" "testing"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
) )
var testFolder = os.ExpandEnv("$PWD/../../tests/resources/test_folder") var TestFolder = os.ExpandEnv("$PWD/../../tests/resources/test_folder")
var validAbraConf = os.ExpandEnv("$PWD/../../tests/resources/valid_abra_config") var ValidAbraConf = os.ExpandEnv("$PWD/../../tests/resources/valid_abra_config")
// make sure these are in alphabetical order // make sure these are in alphabetical order
var tFolders = []string{"folder1", "folder2"} var TFolders = []string{"folder1", "folder2"}
var tFiles = []string{"bar.env", "foo.env"} var TFiles = []string{"bar.env", "foo.env"}
var appName = "ecloud" var AppName = "ecloud"
var serverName = "evil.corp" var ServerName = "evil.corp"
var expectedAppEnv = AppEnv{ var ExpectedAppEnv = config.AppEnv{
"DOMAIN": "ecloud.evil.corp", "DOMAIN": "ecloud.evil.corp",
"RECIPE": "ecloud", "RECIPE": "ecloud",
} }
var expectedApp = App{ var ExpectedApp = config.App{
Name: appName, Name: AppName,
Recipe: expectedAppEnv["RECIPE"], Recipe: ExpectedAppEnv["RECIPE"],
Domain: expectedAppEnv["DOMAIN"], Domain: ExpectedAppEnv["DOMAIN"],
Env: expectedAppEnv, Env: ExpectedAppEnv,
Path: expectedAppFile.Path, Path: ExpectedAppFile.Path,
Server: expectedAppFile.Server, Server: ExpectedAppFile.Server,
} }
var expectedAppFile = AppFile{ var ExpectedAppFile = config.AppFile{
Path: path.Join(validAbraConf, "servers", serverName, appName+".env"), Path: path.Join(ValidAbraConf, "servers", ServerName, AppName+".env"),
Server: serverName, Server: ServerName,
} }
var expectedAppFiles = map[string]AppFile{ var ExpectedAppFiles = map[string]config.AppFile{
appName: expectedAppFile, AppName: ExpectedAppFile,
} }
// var expectedServerNames = []string{"evil.corp"}
func TestGetAllFoldersInDirectory(t *testing.T) { func TestGetAllFoldersInDirectory(t *testing.T) {
folders, err := GetAllFoldersInDirectory(testFolder) folders, err := config.GetAllFoldersInDirectory(TestFolder)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !reflect.DeepEqual(folders, tFolders) { if !reflect.DeepEqual(folders, TFolders) {
t.Fatalf("did not get expected folders. Expected: (%s), Got: (%s)", strings.Join(tFolders, ","), strings.Join(folders, ",")) t.Fatalf("did not get expected folders. Expected: (%s), Got: (%s)", strings.Join(TFolders, ","), strings.Join(folders, ","))
} }
} }
func TestGetAllFilesInDirectory(t *testing.T) { func TestGetAllFilesInDirectory(t *testing.T) {
files, err := GetAllFilesInDirectory(testFolder) files, err := config.GetAllFilesInDirectory(TestFolder)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -62,23 +65,208 @@ func TestGetAllFilesInDirectory(t *testing.T) {
for _, file := range files { for _, file := range files {
fileNames = append(fileNames, file.Name()) fileNames = append(fileNames, file.Name())
} }
if !reflect.DeepEqual(fileNames, tFiles) { if !reflect.DeepEqual(fileNames, TFiles) {
t.Fatalf("did not get expected files. Expected: (%s), Got: (%s)", strings.Join(tFiles, ","), strings.Join(fileNames, ",")) t.Fatalf("did not get expected files. Expected: (%s), Got: (%s)", strings.Join(TFiles, ","), strings.Join(fileNames, ","))
} }
} }
func TestReadEnv(t *testing.T) { func TestReadEnv(t *testing.T) {
env, err := ReadEnv(expectedAppFile.Path) env, err := config.ReadEnv(ExpectedAppFile.Path, config.ReadEnvOptions{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !reflect.DeepEqual(env, expectedAppEnv) { if !reflect.DeepEqual(env, ExpectedAppEnv) {
t.Fatalf( t.Fatalf(
"did not get expected application settings. Expected: DOMAIN=%s RECIPE=%s; Got: DOMAIN=%s RECIPE=%s", "did not get expected application settings. Expected: DOMAIN=%s RECIPE=%s; Got: DOMAIN=%s RECIPE=%s",
expectedAppEnv["DOMAIN"], ExpectedAppEnv["DOMAIN"],
expectedAppEnv["RECIPE"], ExpectedAppEnv["RECIPE"],
env["DOMAIN"], env["DOMAIN"],
env["RECIPE"], env["RECIPE"],
) )
} }
} }
func TestReadAbraShEnvVars(t *testing.T) {
offline := true
r, err := recipe.Get("abra-test-recipe", offline)
if err != nil {
t.Fatal(err)
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, r.Name, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
t.Fatal(err)
}
if len(abraShEnv) == 0 {
t.Error("at least one env var should be exported")
}
if _, ok := abraShEnv["INNER_FOO"]; ok {
t.Error("INNER_FOO should not be exported")
}
if _, ok := abraShEnv["INNER_BAZ"]; ok {
t.Error("INNER_BAZ should not be exported")
}
if _, ok := abraShEnv["OUTER_FOO"]; !ok {
t.Error("OUTER_FOO should be exported")
}
}
func TestReadAbraShCmdNames(t *testing.T) {
offline := true
r, err := recipe.Get("abra-test-recipe", offline)
if err != nil {
t.Fatal(err)
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, r.Name, "abra.sh")
cmdNames, err := config.ReadAbraShCmdNames(abraShPath)
if err != nil {
t.Fatal(err)
}
if len(cmdNames) == 0 {
t.Error("at least one command name should be found")
}
expectedCmdNames := []string{"test_cmd", "test_cmd_args"}
for _, cmdName := range expectedCmdNames {
if !slices.Contains(cmdNames, cmdName) {
t.Fatalf("%s should have been found in %s", cmdName, abraShPath)
}
}
}
func TestCheckEnv(t *testing.T) {
offline := true
r, err := recipe.Get("abra-test-recipe", offline)
if err != nil {
t.Fatal(err)
}
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample")
envSample, err := config.ReadEnv(envSamplePath, config.ReadEnvOptions{})
if err != nil {
t.Fatal(err)
}
app := config.App{
Name: "test-app",
Recipe: r.Name,
Domain: "example.com",
Env: envSample,
Path: "example.com.env",
Server: "example.com",
}
envVars, err := config.CheckEnv(app)
if err != nil {
t.Fatal(err)
}
for _, envVar := range envVars {
if !envVar.Present {
t.Fatalf("%s should be present", envVar.Name)
}
}
}
func TestCheckEnvError(t *testing.T) {
offline := true
r, err := recipe.Get("abra-test-recipe", offline)
if err != nil {
t.Fatal(err)
}
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample")
envSample, err := config.ReadEnv(envSamplePath, config.ReadEnvOptions{})
if err != nil {
t.Fatal(err)
}
delete(envSample, "DOMAIN")
app := config.App{
Name: "test-app",
Recipe: r.Name,
Domain: "example.com",
Env: envSample,
Path: "example.com.env",
Server: "example.com",
}
envVars, err := config.CheckEnv(app)
if err != nil {
t.Fatal(err)
}
for _, envVar := range envVars {
if envVar.Name == "DOMAIN" && envVar.Present {
t.Fatalf("%s should not be present", envVar.Name)
}
}
}
func TestContainsEnvVarModifier(t *testing.T) {
if ok := config.ContainsEnvVarModifier("FOO=bar # bing"); ok {
t.Fatal("FOO contains no env var modifier")
}
if ok := config.ContainsEnvVarModifier("FOO=bar # length=3"); !ok {
t.Fatal("FOO contains an env var modifier (length)")
}
}
func TestEnvVarCommentsRemoved(t *testing.T) {
offline := true
r, err := recipe.Get("abra-test-recipe", offline)
if err != nil {
t.Fatal(err)
}
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample")
envSample, err := config.ReadEnv(envSamplePath, config.ReadEnvOptions{})
if err != nil {
t.Fatal(err)
}
envVar, exists := envSample["WITH_COMMENT"]
if !exists {
t.Fatal("WITH_COMMENT env var should be present in .env.sample")
}
if strings.Contains(envVar, "should be removed") {
t.Fatalf("comment from '%s' should be removed", envVar)
}
envVar, exists = envSample["SECRET_TEST_PASS_TWO_VERSION"]
if !exists {
t.Fatal("WITH_COMMENT env var should be present in .env.sample")
}
if strings.Contains(envVar, "length") {
t.Fatal("comment from env var SECRET_TEST_PASS_TWO_VERSION should have been removed")
}
}
func TestEnvVarModifiersIncluded(t *testing.T) {
offline := true
r, err := recipe.Get("abra-test-recipe", offline)
if err != nil {
t.Fatal(err)
}
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample")
envSample, err := config.ReadEnv(envSamplePath, config.ReadEnvOptions{IncludeModifiers: true})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(envSample["SECRET_TEST_PASS_TWO_VERSION"], "length") {
t.Fatal("comment from env var SECRET_TEST_PASS_TWO_VERSION should not be removed")
}
}

View File

@ -3,39 +3,20 @@ package dns
import ( import (
"fmt" "fmt"
"net" "net"
"os"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
) )
// NewToken constructs a new DNS provider token.
func NewToken(provider, providerTokenEnvVar string) (string, error) {
if token, present := os.LookupEnv(providerTokenEnvVar); present {
return token, nil
}
logrus.Debugf("no %s in environment, asking via stdin", providerTokenEnvVar)
var token string
prompt := &survey.Input{
Message: fmt.Sprintf("%s API token?", provider),
}
if err := survey.AskOne(prompt, &token); err != nil {
return "", err
}
return token, nil
}
// EnsureIPv4 ensures that an ipv4 address is set for a domain name // EnsureIPv4 ensures that an ipv4 address is set for a domain name
func EnsureIPv4(domainName string) (string, error) { func EnsureIPv4(domainName string) (string, error) {
ipv4, err := net.ResolveIPAddr("ip", domainName) ipv4, err := net.ResolveIPAddr("ip4", domainName)
if err != nil { if err != nil {
return "", err return "", err
} }
// NOTE(d1): e.g. when there is only an ipv6 record available
if ipv4 == nil {
return "", fmt.Errorf("unable to resolve ipv4 address for %s", domainName)
}
return ipv4.String(), nil return ipv4.String(), nil
} }
@ -72,12 +53,3 @@ func EnsureDomainsResolveSameIPv4(domainName, server string) (string, error) {
return ipv4, nil return ipv4, nil
} }
// GetTTL parses a ttl string into a duration
func GetTTL(ttl string) (time.Duration, error) {
val, err := time.ParseDuration(ttl)
if err != nil {
return val, err
}
return val, nil
}

64
pkg/dns/dns_test.go Normal file
View File

@ -0,0 +1,64 @@
package dns
import (
"fmt"
"testing"
"gotest.tools/v3/assert"
)
func TestEnsureDomainsResolveSameIPv4(t *testing.T) {
tests := []struct {
domainName string
serverName string
shouldValidate bool
}{
// NOTE(d1): DNS records get checked, so use something that is maintained
// within the federation. if you're here because of a failing test, try
// `dig +short <domain>` to ensure stuff matches first! If flakyness
// becomes an issue we can look into mocking
{"docs.coopcloud.tech", "coopcloud.tech", true},
{"docs.coopcloud.tech", "swarm.autonomic.zone", true},
// NOTE(d1): special case handling for "--local"
{"", "default", true},
{"", "local", true},
{"", "", false},
{"123", "", false},
}
for _, test := range tests {
_, err := EnsureDomainsResolveSameIPv4(test.domainName, test.serverName)
if err != nil && test.shouldValidate {
t.Fatal(err)
}
if err == nil && !test.shouldValidate {
t.Fatal(fmt.Errorf("should have failed but did not: %v", test))
}
}
}
func TestEnsureIpv4(t *testing.T) {
// NOTE(d1): DNS records get checked, so use something that is maintained
// within the federation. if you're here because of a failing test, try `dig
// +short <domain>` to ensure stuff matches first! If flakyness becomes an
// issue we can look into mocking
domainName := "collabora.ostrom.collective.tools"
serverName := "ostrom.collective.tools"
for i := 0; i < 15; i++ {
domainIpv4, err := EnsureIPv4(domainName)
if err != nil {
t.Fatal(err)
}
serverIpv4, err := EnsureIPv4(serverName)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, domainIpv4, serverIpv4)
}
}

View File

@ -1,15 +0,0 @@
package gandi
import (
"coopcloud.tech/abra/pkg/dns"
"github.com/libdns/gandi"
)
// New constructs a new DNs provider.
func New() (gandi.Provider, error) {
token, err := dns.NewToken("Gandi", "GANDI_TOKEN")
if err != nil {
return gandi.Provider{}, err
}
return gandi.Provider{APIToken: token}, nil
}

View File

@ -8,7 +8,7 @@ import (
) )
// Commit runs a git commit // Commit runs a git commit
func Commit(repoPath, glob, commitMessage string, dryRun bool) error { func Commit(repoPath, commitMessage string, dryRun bool) error {
if commitMessage == "" { if commitMessage == "" {
return fmt.Errorf("no commit message specified?") return fmt.Errorf("no commit message specified?")
} }
@ -33,17 +33,8 @@ func Commit(repoPath, glob, commitMessage string, dryRun bool) error {
} }
if !dryRun { if !dryRun {
err = commitWorktree.AddGlob(glob) // NOTE(d1): `All: true` does not include untracked files
if err != nil { _, err = commitWorktree.Commit(commitMessage, &git.CommitOptions{All: true})
return err
}
logrus.Debugf("staged %s for commit", glob)
} else {
logrus.Debugf("dry run: did not stage %s for commit", glob)
}
if !dryRun {
_, err = commitWorktree.Commit(commitMessage, &git.CommitOptions{})
if err != nil { if err != nil {
return err return err
} }

42
pkg/git/diff.go Normal file
View File

@ -0,0 +1,42 @@
package git
import (
"fmt"
"os/exec"
"github.com/sirupsen/logrus"
)
// getGitDiffArgs builds the `git diff` invocation args. It removes the usage
// of a pager and ensures that colours are specified even when Git might detect
// otherwise.
func getGitDiffArgs(repoPath string) []string {
return []string{
"-C",
repoPath,
"--no-pager",
"-c",
"color.diff=always",
"diff",
}
}
// DiffUnstaged shows a `git diff`. Due to limitations in the underlying go-git
// library, this implementation requires the /usr/bin/git binary. It gracefully
// skips if it cannot find the command on the system.
func DiffUnstaged(path string) error {
if _, err := exec.LookPath("git"); err != nil {
logrus.Warnf("unable to locate git command, cannot output diff")
return nil
}
gitDiffArgs := getGitDiffArgs(path)
diff, err := exec.Command("git", gitDiffArgs...).Output()
if err != nil {
return nil
}
fmt.Print(string(diff))
return nil
}

View File

@ -3,6 +3,7 @@ package jsontable
import ( import (
"fmt" "fmt"
"io" "io"
"strings"
"github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter"
) )
@ -109,6 +110,9 @@ func (t *JSONTable) _JSONRenderInner() {
} }
writeChar(t.out, '{') writeChar(t.out, '{')
for keyidx, key := range t.keys { for keyidx, key := range t.keys {
key := strings.ToLower(key)
key = strings.ReplaceAll(key, " ", "-")
value := "nil" value := "nil"
if keyidx < len(row) { if keyidx < len(row) {
value = row[keyidx] value = row[keyidx]
@ -138,10 +142,8 @@ func (t *JSONTable) JSONRender() {
if t.hasCaption { if t.hasCaption {
fmt.Fprintf(t.out, "\"%s\":\"%s\",", t.captionLabel, t.caption) fmt.Fprintf(t.out, "\"%s\":\"%s\",", t.captionLabel, t.caption)
} }
fmt.Fprintf(t.out, "\"%s\":", t.dataLabel) fmt.Fprintf(t.out, "\"%s\":", t.dataLabel)
} }
// write list // write list
@ -191,6 +193,12 @@ func (t *JSONTable) SetAutoMergeCellsByColumnIndex(cols []int) {
t.tbl.SetAutoMergeCellsByColumnIndex(cols) t.tbl.SetAutoMergeCellsByColumnIndex(cols)
} }
// Stuff we should implement but we just proxy for now.
func (t *JSONTable) SetAlignment(align int) {
// FIXME
t.tbl.SetAlignment(align)
}
func (t *JSONTable) SetAutoMergeCells(auto bool) { func (t *JSONTable) SetAutoMergeCells(auto bool) {
// FIXME // FIXME
t.tbl.SetAutoMergeCells(auto) t.tbl.SetAutoMergeCells(auto)

View File

@ -9,7 +9,6 @@ import (
"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" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/runtime"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
@ -228,7 +227,7 @@ func LintAppService(recipe recipe.Recipe) (bool, error) {
// 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(config.RECIPES_DIR, recipe.Name, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath) sampleEnv, err := config.ReadEnv(envSamplePath, config.ReadEnvOptions{})
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)
} }
@ -334,11 +333,7 @@ func LintImagePresent(recipe recipe.Recipe) (bool, error) {
} }
func LintHasPublishedVersion(recipe recipe.Recipe) (bool, error) { func LintHasPublishedVersion(recipe recipe.Recipe) (bool, error) {
// defaults since we can't take arguments here... this means this lint rule catl, err := recipePkg.ReadRecipeCatalogue(false)
// always access the network if e.g. the catalogue needs cloning / updating
conf := runtime.New()
catl, err := recipePkg.ReadRecipeCatalogue(conf)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -7,6 +7,7 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"slices"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@ -17,7 +18,6 @@ import (
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/limit" "coopcloud.tech/abra/pkg/limit"
"coopcloud.tech/abra/pkg/runtime"
"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"
"coopcloud.tech/abra/pkg/web" "coopcloud.tech/abra/pkg/web"
@ -25,7 +25,6 @@ import (
composetypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
gitConfig "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -33,7 +32,7 @@ import (
// RecipeCatalogueURL is the only current recipe catalogue available. // RecipeCatalogueURL is the only current recipe catalogue available.
const RecipeCatalogueURL = "https://recipes.coopcloud.tech/recipes.json" const RecipeCatalogueURL = "https://recipes.coopcloud.tech/recipes.json"
// ReposMetadataURL is the recipe repository metadata // ReposMetadataURL is the recipe repository metadata.
const ReposMetadataURL = "https://git.coopcloud.tech/api/v1/orgs/coop-cloud/repos" const ReposMetadataURL = "https://git.coopcloud.tech/api/v1/orgs/coop-cloud/repos"
// tag represents a git tag. // tag represents a git tag.
@ -65,6 +64,11 @@ type RecipeMeta struct {
Website string `json:"website"` Website string `json:"website"`
} }
// TopicMeta represents a list of topics for a repository.
type TopicMeta struct {
Topics []string `json:"topics"`
}
// LatestVersion returns the latest version of a recipe. // LatestVersion returns the latest version of a recipe.
func (r RecipeMeta) LatestVersion() string { func (r RecipeMeta) LatestVersion() string {
var version string var version string
@ -207,8 +211,8 @@ func (r Recipe) Tags() ([]string, error) {
} }
// Get retrieves a recipe. // Get retrieves a recipe.
func Get(recipeName string, conf *runtime.Config) (Recipe, error) { func Get(recipeName string, offline bool) (Recipe, error) {
if err := EnsureExists(recipeName, conf); err != nil { if err := EnsureExists(recipeName); err != nil {
return Recipe{}, err return Recipe{}, err
} }
@ -223,7 +227,7 @@ func Get(recipeName string, conf *runtime.Config) (Recipe, error) {
} }
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample") envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath) sampleEnv, err := config.ReadEnv(envSamplePath, config.ReadEnvOptions{})
if err != nil { if err != nil {
return Recipe{}, err return Recipe{}, err
} }
@ -234,11 +238,12 @@ func Get(recipeName string, conf *runtime.Config) (Recipe, error) {
return Recipe{}, err return Recipe{}, err
} }
meta, err := GetRecipeMeta(recipeName, conf) meta, err := GetRecipeMeta(recipeName, offline)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "does not exist") { switch err.(type) {
case RecipeMissingFromCatalogue:
meta = RecipeMeta{} meta = RecipeMeta{}
} else { default:
return Recipe{}, err return Recipe{}, err
} }
} }
@ -250,15 +255,20 @@ func Get(recipeName string, conf *runtime.Config) (Recipe, error) {
}, nil }, nil
} }
func (r Recipe) SampleEnv(opts config.ReadEnvOptions) (map[string]string, error) {
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath, opts)
if err != nil {
return sampleEnv, fmt.Errorf("unable to discover .env.sample for %s", r.Name)
}
return sampleEnv, nil
}
// EnsureExists ensures that a recipe is locally cloned // EnsureExists ensures that a recipe is locally cloned
func EnsureExists(recipeName string, conf *runtime.Config) error { func EnsureExists(recipeName string) error {
recipeDir := path.Join(config.RECIPES_DIR, recipeName) recipeDir := path.Join(config.RECIPES_DIR, recipeName)
if _, err := os.Stat(recipeDir); os.IsNotExist(err) { if _, err := os.Stat(recipeDir); os.IsNotExist(err) {
if conf.Offline {
return fmt.Errorf("no local copy of %s available, network access required", recipeName)
}
logrus.Debugf("%s does not exist, attemmpting to clone", recipeDir) logrus.Debugf("%s does not exist, attemmpting to clone", recipeDir)
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, recipeName) url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, recipeName)
if err := gitPkg.Clone(recipeDir, url); err != nil { if err := gitPkg.Clone(recipeDir, url); err != nil {
@ -277,15 +287,6 @@ func EnsureExists(recipeName string, conf *runtime.Config) error {
func EnsureVersion(recipeName, version string) error { func EnsureVersion(recipeName, version string) error {
recipeDir := path.Join(config.RECIPES_DIR, recipeName) recipeDir := path.Join(config.RECIPES_DIR, recipeName)
isClean, err := gitPkg.IsClean(recipeDir)
if err != nil {
return err
}
if !isClean {
return fmt.Errorf("%s has locally unstaged changes", recipeName)
}
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil { if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err return err
} }
@ -312,7 +313,10 @@ func EnsureVersion(recipeName, version string) error {
return err return err
} }
logrus.Debugf("read %s as tags for recipe %s", strings.Join(parsedTags, ", "), recipeName) joinedTags := strings.Join(parsedTags, ", ")
if joinedTags != "" {
logrus.Debugf("read %s as tags for recipe %s", joinedTags, recipeName)
}
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?", recipeName, version)
@ -337,25 +341,31 @@ func EnsureVersion(recipeName, version string) error {
return nil return nil
} }
// EnsureLatest makes sure the latest commit is checked out for a local recipe repository // EnsureIsClean makes sure that the recipe repository has no unstaged changes.
func EnsureLatest(recipeName string, conf *runtime.Config) error { func EnsureIsClean(recipeName string) error {
recipeDir := path.Join(config.RECIPES_DIR, recipeName) recipeDir := path.Join(config.RECIPES_DIR, recipeName)
isClean, err := gitPkg.IsClean(recipeDir) isClean, err := gitPkg.IsClean(recipeDir)
if err != nil { if err != nil {
return err return fmt.Errorf("unable to check git clean status in %s: %s", recipeDir, err)
} }
if !isClean { if !isClean {
return fmt.Errorf("%s has locally unstaged changes", recipeName) msg := "%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding"
return fmt.Errorf(msg, recipeName, recipeDir)
} }
return nil
}
// EnsureLatest makes sure the latest commit is checked out for a local recipe repository
func EnsureLatest(recipeName string) error {
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil { if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err return err
} }
logrus.Debugf("attempting to open git repository in %s", recipeDir)
repo, err := git.PlainOpen(recipeDir) repo, err := git.PlainOpen(recipeDir)
if err != nil { if err != nil {
return err return err
@ -366,21 +376,11 @@ func EnsureLatest(recipeName string, conf *runtime.Config) error {
return err return err
} }
meta, err := GetRecipeMeta(recipeName, conf) branch, err := gitPkg.GetDefaultBranch(repo, recipeDir)
if err != nil { if err != nil {
return err return err
} }
var branch plumbing.ReferenceName
if meta.DefaultBranch != "" {
branch = plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", meta.DefaultBranch))
} else {
branch, err = gitPkg.GetDefaultBranch(repo, recipeDir)
if err != nil {
return err
}
}
checkOutOpts := &git.CheckoutOptions{ checkOutOpts := &git.CheckoutOptions{
Create: false, Create: false,
Force: true, Force: true,
@ -437,7 +437,7 @@ func GetVersionLabelLocal(recipe Recipe) (string, error) {
for _, service := range recipe.Config.Services { for _, service := range recipe.Config.Services {
for label, value := range service.Deploy.Labels { for label, value := range service.Deploy.Labels {
if strings.HasPrefix(label, "coop-cloud") { if strings.HasPrefix(label, "coop-cloud") && strings.Contains(label, "version") {
return value, nil return value, nil
} }
} }
@ -583,24 +583,9 @@ 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, conf *runtime.Config) error { func EnsureUpToDate(recipeName string) error {
recipeDir := path.Join(config.RECIPES_DIR, recipeName) recipeDir := path.Join(config.RECIPES_DIR, recipeName)
isClean, err := gitPkg.IsClean(recipeDir)
if err != nil {
return fmt.Errorf("unable to check git clean status in %s: %s", recipeDir, err)
}
if !isClean {
msg := "%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding"
return fmt.Errorf(msg, recipeName, recipeDir)
}
if conf.Offline {
logrus.Debug("attempting to use local recipe without access network (\"--offline\")")
return nil
}
repo, err := git.PlainOpen(recipeDir) 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", recipeDir, err)
@ -626,12 +611,7 @@ func EnsureUpToDate(recipeName string, conf *runtime.Config) error {
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", recipeDir, err)
} }
fetchOpts := &git.FetchOptions{ fetchOpts := &git.FetchOptions{Tags: git.AllTags}
Tags: git.AllTags,
RefSpecs: []gitConfig.RefSpec{
gitConfig.RefSpec(fmt.Sprintf("%s:%s", branch, branch)),
},
}
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", recipeDir, err)
@ -656,16 +636,18 @@ func EnsureUpToDate(recipeName string, conf *runtime.Config) error {
} }
// ReadRecipeCatalogue reads the recipe catalogue. // ReadRecipeCatalogue reads the recipe catalogue.
func ReadRecipeCatalogue(conf *runtime.Config) (RecipeCatalogue, error) { func ReadRecipeCatalogue(offline bool) (RecipeCatalogue, error) {
recipes := make(RecipeCatalogue) recipes := make(RecipeCatalogue)
if err := catalogue.EnsureCatalogue(conf); err != nil { if err := catalogue.EnsureCatalogue(); err != nil {
return nil, err return nil, err
} }
if err := catalogue.EnsureUpToDate(conf); err != nil { if !offline {
if err := catalogue.EnsureUpToDate(); err != nil {
return nil, err return nil, err
} }
}
if err := readRecipeCatalogueFS(&recipes); err != nil { if err := readRecipeCatalogueFS(&recipes); err != nil {
return nil, err return nil, err
@ -691,10 +673,10 @@ func readRecipeCatalogueFS(target interface{}) error {
} }
// VersionsOfService lists the version of a service. // VersionsOfService lists the version of a service.
func VersionsOfService(recipe, serviceName string, conf *runtime.Config) ([]string, error) { func VersionsOfService(recipe, serviceName string, offline bool) ([]string, error) {
var versions []string var versions []string
catalogue, err := ReadRecipeCatalogue(conf) catalogue, err := ReadRecipeCatalogue(offline)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -719,20 +701,26 @@ func VersionsOfService(recipe, serviceName string, conf *runtime.Config) ([]stri
return versions, nil return versions, nil
} }
// RecipeMissingFromCatalogue signifies a recipe is not present in the catalogue.
type RecipeMissingFromCatalogue struct{ err string }
// Error outputs the error message.
func (r RecipeMissingFromCatalogue) Error() string {
return r.err
}
// GetRecipeMeta retrieves the recipe metadata from the recipe catalogue. // GetRecipeMeta retrieves the recipe metadata from the recipe catalogue.
func GetRecipeMeta(recipeName string, conf *runtime.Config) (RecipeMeta, error) { func GetRecipeMeta(recipeName string, offline bool) (RecipeMeta, error) {
catl, err := ReadRecipeCatalogue(conf) catl, err := ReadRecipeCatalogue(offline)
if err != nil { if err != nil {
return RecipeMeta{}, err return RecipeMeta{}, err
} }
recipeMeta, ok := catl[recipeName] recipeMeta, ok := catl[recipeName]
if !ok { if !ok {
return RecipeMeta{}, fmt.Errorf("recipe %s does not exist?", recipeName) return RecipeMeta{}, RecipeMissingFromCatalogue{
err: fmt.Sprintf("recipe %s does not exist?", recipeName),
} }
if err := EnsureExists(recipeName, conf); err != nil {
return RecipeMeta{}, err
} }
logrus.Debugf("recipe metadata retrieved for %s", recipeName) logrus.Debugf("recipe metadata retrieved for %s", recipeName)
@ -818,11 +806,7 @@ type InternalTracker struct {
type RepoCatalogue map[string]RepoMeta type RepoCatalogue map[string]RepoMeta
// ReadReposMetadata retrieves coop-cloud/... repo metadata from Gitea. // ReadReposMetadata retrieves coop-cloud/... repo metadata from Gitea.
func ReadReposMetadata(conf *runtime.Config) (RepoCatalogue, error) { func ReadReposMetadata() (RepoCatalogue, error) {
if conf.Offline {
return nil, fmt.Errorf("network access required to query recipes metadata")
}
reposMeta := make(RepoCatalogue) reposMeta := make(RepoCatalogue)
pageIdx := 1 pageIdx := 1
@ -844,8 +828,17 @@ func ReadReposMetadata(conf *runtime.Config) (RepoCatalogue, error) {
} }
for idx, repo := range reposList { for idx, repo := range reposList {
var topicMeta TopicMeta
topicsURL := getReposTopicUrl(repo.Name)
if err := web.ReadJSON(topicsURL, &topicMeta); err != nil {
return reposMeta, err
}
if slices.Contains(topicMeta.Topics, "recipe") && repo.Name != "example" {
reposMeta[repo.Name] = reposList[idx] reposMeta[repo.Name] = reposList[idx]
} }
}
pageIdx++ pageIdx++
bar.Add(1) bar.Add(1)
@ -857,7 +850,7 @@ func ReadReposMetadata(conf *runtime.Config) (RepoCatalogue, error) {
} }
// GetRecipeVersions retrieves all recipe versions. // GetRecipeVersions retrieves all recipe versions.
func GetRecipeVersions(recipeName string, conf *runtime.Config) (RecipeVersions, error) { func GetRecipeVersions(recipeName string, offline bool) (RecipeVersions, error) {
versions := RecipeVersions{} versions := RecipeVersions{}
recipeDir := path.Join(config.RECIPES_DIR, recipeName) recipeDir := path.Join(config.RECIPES_DIR, recipeName)
@ -895,7 +888,7 @@ func GetRecipeVersions(recipeName string, conf *runtime.Config) (RecipeVersions,
logrus.Debugf("successfully checked out %s in %s", ref.Name(), recipeDir) logrus.Debugf("successfully checked out %s in %s", ref.Name(), recipeDir)
recipe, err := Get(recipeName, conf) recipe, err := Get(recipeName, offline)
if err != nil { if err != nil {
return err return err
} }
@ -1002,11 +995,7 @@ func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]stri
} }
// UpdateRepositories clones and updates all recipe repositories locally. // UpdateRepositories clones and updates all recipe repositories locally.
func UpdateRepositories(repos RepoCatalogue, recipeName string, conf *runtime.Config) error { func UpdateRepositories(repos RepoCatalogue, recipeName string) error {
if conf.Offline {
return fmt.Errorf("network access required to update recipes")
}
var barLength int var barLength int
if recipeName != "" { if recipeName != "" {
barLength = 1 barLength = 1
@ -1028,22 +1017,12 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string, conf *runtime.Co
retrieveBar.Add(1) retrieveBar.Add(1)
return return
} }
if _, exists := catalogue.CatalogueSkipList[rm.Name]; exists {
ch <- rm.Name
retrieveBar.Add(1)
return
}
recipeDir := path.Join(config.RECIPES_DIR, rm.Name) recipeDir := path.Join(config.RECIPES_DIR, rm.Name)
if err := gitPkg.Clone(recipeDir, rm.CloneURL); err != nil { if err := gitPkg.Clone(recipeDir, rm.CloneURL); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if err := EnsureUpToDate(rm.Name, conf); err != nil {
logrus.Fatal(err)
}
ch <- rm.Name ch <- rm.Name
retrieveBar.Add(1) retrieveBar.Add(1)
}(repoMeta) }(repoMeta)
@ -1055,3 +1034,8 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string, conf *runtime.Co
return nil return nil
} }
// getReposTopicUrl retrieves the repository specific topic listing.
func getReposTopicUrl(repoName string) string {
return fmt.Sprintf("https://git.coopcloud.tech/api/v1/repos/coop-cloud/%s/topics", repoName)
}

31
pkg/recipe/recipe_test.go Normal file
View File

@ -0,0 +1,31 @@
package recipe
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetVersionLabelLocalDoesNotUseTimeoutLabel(t *testing.T) {
offline := true
recipe, err := Get("traefik", offline)
if err != nil {
t.Fatal(err)
}
for i := 1; i < 1000; i++ {
label, err := GetVersionLabelLocal(recipe)
if err != nil {
t.Fatal(err)
}
// NOTE(d1): this is potentially quite a brittle unit test as it needs to
// hardcode the default timeout label to ensure that the label parser never
// returns it. hopefully this won't fail too often! if you're here because
// of a failure, just update the `defaultTimeoutLabel` value & permalink
// below
// https://git.coopcloud.tech/coop-cloud/traefik/src/commit/ac3a47fe8ca3ef92db84f64cfedfbb348000faee/.env.sample#L2
defaultTimeoutLabel := "300"
assert.NotEqual(t, label, defaultTimeoutLabel)
}
}

View File

@ -1,28 +0,0 @@
package runtime
import "github.com/sirupsen/logrus"
type Config struct {
Offline bool
}
type Option func(c *Config)
func New(opts ...Option) *Config {
conf := &Config{Offline: false}
for _, optFunc := range opts {
optFunc(conf)
}
return conf
}
func WithOffline(offline bool) Option {
return func(c *Config) {
if offline {
logrus.Debugf("runtime config: attempting to run in offline mode")
}
c.Offline = offline
}
}

View File

@ -4,15 +4,19 @@
package secret package secret
import ( import (
"context"
"fmt" "fmt"
"regexp" "slices"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/upstream/stack"
loader "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/decentral1se/passgen" "github.com/decentral1se/passgen"
"github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -60,40 +64,56 @@ func GeneratePassphrases(count uint) ([]string, error) {
return passphrases, nil return passphrases, nil
} }
// ReadSecretEnvVars reads secret env vars from an app env var config. // ReadSecretsConfig reads secret names/versions from the recipe config. The
func ReadSecretEnvVars(appEnv config.AppEnv) map[string]string { // function generalises appEnv/composeFiles because some times you have an app
secretEnvVars := make(map[string]string) // and some times you don't (as the caller). We need to be able to handle the
// "app new" case where we pass in the .env.sample and the "secret generate"
// case where the app is created.
func ReadSecretsConfig(appEnvPath string, composeFiles []string, recipeName string) (map[string]string, error) {
secretConfigs := make(map[string]string)
for envVar := range appEnv { appEnv, err := config.ReadEnv(appEnvPath, config.ReadEnvOptions{IncludeModifiers: true})
regex := regexp.MustCompile(`^SECRET.*VERSION.*`) if err != nil {
if string(regex.Find([]byte(envVar))) != "" { return secretConfigs, err
secretEnvVars[envVar] = appEnv[envVar] }
opts := stack.Deploy{Composefiles: composeFiles}
config, err := loader.LoadComposefile(opts, appEnv)
if err != nil {
return secretConfigs, err
}
var enabledSecrets []string
for _, service := range config.Services {
for _, secret := range service.Secrets {
enabledSecrets = append(enabledSecrets, secret.Source)
} }
} }
logrus.Debugf("read %s as secrets from %s", secretEnvVars, appEnv) if len(enabledSecrets) == 0 {
logrus.Debugf("not generating app secrets, none enabled in recipe config")
return secretEnvVars return secretConfigs, nil
} }
func ParseSecretEnvVarName(secretEnvVar string) string { for secretId, secretConfig := range config.Secrets {
withoutPrefix := strings.TrimPrefix(secretEnvVar, "SECRET_") if string(secretConfig.Name[len(secretConfig.Name)-1]) == "_" {
withoutSuffix := strings.TrimSuffix(withoutPrefix, "_VERSION") return secretConfigs, fmt.Errorf("missing version for secret? (%s)", secretId)
name := strings.ToLower(withoutSuffix)
logrus.Debugf("parsed %s as name from %s", name, secretEnvVar)
return name
} }
func ParseGeneratedSecretName(secret string, appEnv config.App) string { if !(slices.Contains(enabledSecrets, secretId)) {
name := fmt.Sprintf("%s_", appEnv.StackName()) logrus.Warnf("%s not enabled in recipe config, skipping", secretId)
withoutAppName := strings.TrimPrefix(secret, name) continue
idx := strings.LastIndex(withoutAppName, "_")
parsed := withoutAppName[:idx]
logrus.Debugf("parsed %s as name from %s", parsed, secret)
return parsed
} }
func ParseSecretEnvVarValue(secret string) (secretValue, error) { lastIdx := strings.LastIndex(secretConfig.Name, "_")
secretVersion := secretConfig.Name[lastIdx+1:]
secretConfigs[secretId] = secretVersion
}
return secretConfigs, nil
}
func ParseSecretValue(secret string) (secretValue, error) {
values := strings.Split(secret, "#") values := strings.Split(secret, "#")
if len(values) == 0 { if len(values) == 0 {
return secretValue{}, fmt.Errorf("unable to parse %s", secret) return secretValue{}, fmt.Errorf("unable to parse %s", secret)
@ -118,30 +138,29 @@ func ParseSecretEnvVarValue(secret string) (secretValue, error) {
} }
// GenerateSecrets generates secrets locally and sends them to a remote server for storage. // GenerateSecrets generates secrets locally and sends them to a remote server for storage.
func GenerateSecrets(cl *dockerClient.Client, secretEnvVars map[string]string, appName, server string) (map[string]string, error) { func GenerateSecrets(cl *dockerClient.Client, secretsFromConfig map[string]string, appName, server string) (map[string]string, error) {
secrets := make(map[string]string) secrets := make(map[string]string)
var mutex sync.Mutex var mutex sync.Mutex
var wg sync.WaitGroup var wg sync.WaitGroup
ch := make(chan error, len(secretEnvVars)) ch := make(chan error, len(secretsFromConfig))
for secretEnvVar := range secretEnvVars { for n, v := range secretsFromConfig {
wg.Add(1) wg.Add(1)
go func(s string) { go func(secretName, secretValue string) {
defer wg.Done() defer wg.Done()
secretName := ParseSecretEnvVarName(s) parsedSecretValue, err := ParseSecretValue(secretValue)
secretValue, err := ParseSecretEnvVarValue(secretEnvVars[s])
if err != nil { if err != nil {
ch <- err ch <- err
return return
} }
secretRemoteName := fmt.Sprintf("%s_%s_%s", appName, secretName, secretValue.Version) secretRemoteName := fmt.Sprintf("%s_%s_%s", appName, secretName, parsedSecretValue.Version)
logrus.Debugf("attempting to generate and store %s on %s", secretRemoteName, server) logrus.Debugf("attempting to generate and store %s on %s", secretRemoteName, server)
if secretValue.Length > 0 { if parsedSecretValue.Length > 0 {
passwords, err := GeneratePasswords(1, uint(secretValue.Length)) passwords, err := GeneratePasswords(1, uint(parsedSecretValue.Length))
if err != nil { if err != nil {
ch <- err ch <- err
return return
@ -182,12 +201,12 @@ func GenerateSecrets(cl *dockerClient.Client, secretEnvVars map[string]string, a
secrets[secretName] = passphrases[0] secrets[secretName] = passphrases[0]
} }
ch <- nil ch <- nil
}(secretEnvVar) }(n, v)
} }
wg.Wait() wg.Wait()
for range secretEnvVars { for range secretsFromConfig {
err := <-ch err := <-ch
if err != nil { if err != nil {
return nil, err return nil, err
@ -198,3 +217,66 @@ func GenerateSecrets(cl *dockerClient.Client, secretEnvVars map[string]string, a
return secrets, nil return secrets, nil
} }
type secretStatus struct {
LocalName string
RemoteName string
Version string
CreatedOnRemote bool
}
type secretStatuses []secretStatus
// PollSecretsStatus checks status of secrets by comparing the local recipe
// config and deploymend server state.
func PollSecretsStatus(cl *dockerClient.Client, app config.App) (secretStatuses, error) {
var secStats secretStatuses
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil {
return secStats, err
}
secretsConfig, err := ReadSecretsConfig(app.Path, composeFiles, app.Recipe)
if err != nil {
return secStats, err
}
filters, err := app.Filters(false, false)
if err != nil {
return secStats, err
}
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters})
if err != nil {
return secStats, err
}
remoteSecretNames := make(map[string]bool)
for _, cont := range secretList {
remoteSecretNames[cont.Spec.Annotations.Name] = true
}
for secretName, secretValue := range secretsConfig {
createdRemote := false
val, err := ParseSecretValue(secretValue)
if err != nil {
return secStats, err
}
secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, val.Version)
if _, ok := remoteSecretNames[secretRemoteName]; ok {
createdRemote = true
}
secStats = append(secStats, secretStatus{
LocalName: secretName,
RemoteName: secretRemoteName,
Version: val.Version,
CreatedOnRemote: createdRemote,
})
}
return secStats, nil
}

42
pkg/secret/secret_test.go Normal file
View File

@ -0,0 +1,42 @@
package secret
import (
"path"
"testing"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/stack"
loader "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/stretchr/testify/assert"
)
func TestReadSecretsConfig(t *testing.T) {
offline := true
recipe, err := recipe.Get("matrix-synapse", offline)
if err != nil {
t.Fatal(err)
}
sampleEnv, err := recipe.SampleEnv(config.ReadEnvOptions{})
if err != nil {
t.Fatal(err)
}
composeFiles := []string{path.Join(config.RECIPES_DIR, recipe.Name, "compose.yml")}
envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample")
secretsFromConfig, err := ReadSecretsConfig(envSamplePath, composeFiles, recipe.Name)
if err != nil {
t.Fatal(err)
}
opts := stack.Deploy{Composefiles: composeFiles}
config, err := loader.LoadComposefile(opts, sampleEnv)
if err != nil {
t.Fatal(err)
}
for secretId := range config.Secrets {
assert.Contains(t, secretsFromConfig, secretId)
}
}

View File

@ -420,7 +420,7 @@ func convertServiceSecrets(
return nil, err return nil, err
} }
// NOTE(d1): strip # length=... modifiers // NOTE(d1): strip all comments
if strings.Contains(obj.Name, "#") { if strings.Contains(obj.Name, "#") {
vals := strings.Split(obj.Name, "#") vals := strings.Split(obj.Name, "#")
obj.Name = strings.TrimSpace(vals[0]) obj.Name = strings.TrimSpace(vals[0])

View File

@ -5,7 +5,8 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"strings" "os"
"os/signal"
"time" "time"
"coopcloud.tech/abra/pkg/upstream/convert" "coopcloud.tech/abra/pkg/upstream/convert"
@ -414,7 +415,7 @@ func deployServices(
return nil return nil
} }
logrus.Infof("waiting for services to converge: %s", strings.Join(serviceNames, ", ")) logrus.Infof("Waiting for %s to deploy... please hold 🤚", appName)
ch := make(chan error, len(serviceIDs)) ch := make(chan error, len(serviceIDs))
for serviceID, serviceName := range serviceIDs { for serviceID, serviceName := range serviceIDs {
logrus.Debugf("waiting on %s to converge", serviceName) logrus.Debugf("waiting on %s to converge", serviceName)
@ -431,7 +432,7 @@ func deployServices(
logrus.Debugf("assuming %s converged successfully", serviceID) logrus.Debugf("assuming %s converged successfully", serviceID)
} }
logrus.Info("services converged 👌") logrus.Infof("Successfully deployed %s", appName)
return nil return nil
} }
@ -454,6 +455,10 @@ func WaitOnService(ctx context.Context, cl *dockerClient.Client, serviceID, appN
errChan := make(chan error, 1) errChan := make(chan error, 1)
pipeReader, pipeWriter := io.Pipe() pipeReader, pipeWriter := io.Pipe()
sigintChannel := make(chan os.Signal, 1)
signal.Notify(sigintChannel, os.Interrupt)
defer signal.Stop(sigintChannel)
go func() { go func() {
errChan <- progress.ServiceProgress(ctx, cl, serviceID, pipeWriter) errChan <- progress.ServiceProgress(ctx, cl, serviceID, pipeWriter)
}() }()
@ -465,6 +470,12 @@ func WaitOnService(ctx context.Context, cl *dockerClient.Client, serviceID, appN
select { select {
case err := <-errChan: case err := <-errChan:
return err return err
case <-sigintChannel:
return fmt.Errorf(fmt.Sprintf(`
Not waiting for %s to deploy. The deployment is ongoing...
If you want to stop the deployment, try:
abra app undeploy %s`, appName, appName))
case <-time.After(timeout): case <-time.After(timeout):
return fmt.Errorf(fmt.Sprintf(` return fmt.Errorf(fmt.Sprintf(`
%s has not converged (%s second timeout reached). %s has not converged (%s second timeout reached).
@ -481,7 +492,7 @@ And inspect the logs with:
abra app logs %s abra app logs %s
If a service is failing to even start, try smoke out the error with: If a service is failing to even start, try to smoke out the error with:
abra app errors --watch %s abra app errors --watch %s

View File

@ -8,6 +8,7 @@
"gomodTidy" "gomodTidy"
], ],
"ignoreDeps": [ "ignoreDeps": [
"github.com/urfave/cli" "github.com/urfave/cli",
"goreleaser/goreleaser"
] ]
} }

View File

@ -1,8 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
ABRA_VERSION="0.7.0-beta" ABRA_VERSION="0.8.1-beta"
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION" ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION"
RC_VERSION="0.8.0-rc2-beta" RC_VERSION="0.8.1-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

11
scripts/release/test.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
set -e
for f in $(find ./tests/integration -name "*.bats"); do
bats -Tp "$f"
res=$?
if [[ "$res" -ne 0 ]]; then
exit 1
fi
done

10
tests/README.md Normal file
View File

@ -0,0 +1,10 @@
# Test suite
* Unit testing is done in the packages themselves, run `find . -name
"*_test.go"` to find those. Use `make test` to run the entire unit test
suite. Some unit tests require mocked files which are located in
`./tests/resources.`
* Integration tests are in `./tests/integration`. Please see [these
docs](https://docs.coopcloud.tech/abra/hack/#integration-tests) for
instructions and tips on how to run them.

View File

@ -1,4 +0,0 @@
GANDI_TOKEN=...
HCLOUD_TOKEN=...
REGISTRY_PASSWORD=...
REGISTRY_USERNAME=...

View File

@ -1 +0,0 @@
logs

View File

@ -1,28 +0,0 @@
# integration tests
> You need to be a member of Autonomic Co-op to run these tests, sorry!
`testfunctions.sh` contains the functions necessary to save and manipulate
logs. Run `test_all.sh logdir` to run tests specified in that file and save the
logs to `logdir`.
When creating new tests, make sure the test command is a one-liner (you can use
`;` to separate commands). Include `testfunctions.sh` and then write your tests
like this:
```
run_test '$ABRA other stuff here'
```
By default, the testing script will ask after every command if the execution
succeeded. If you reply `n`, it will log the test in the `logdir`. If you want
all tests to run without questions, run `export logall=yes` before executing
the test script.
To run tests, you'll need to prepare your environment:
```
cp .envrc.sample .envrc # fill out values...
direnv allow
./test_all.sh logs
```

View File

@ -1,14 +0,0 @@
#!/bin/bash
source ./testfunctions.sh
source ./common.sh
run_test '$ABRA app ls'
run_test '$ABRA app ls --status'
run_test '$ABRA app ls --type wordpress'
run_test '$ABRA app ls --type wordpress --server swarm.autonomic.zone'
run_test '$ABRA app ls --type wordpress --server swarm.autonomic.zone --status'

View File

@ -0,0 +1,173 @@
#!/usr/bin/env bash
setup_file() {
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file() {
_rm_app
_rm_server
_reset_recipe
}
setup() {
load "$PWD/tests/integration/helpers/common"
_common_setup
}
teardown(){
# https://github.com/bats-core/bats-core/issues/383#issuecomment-738628888
if [[ -z "${BATS_TEST_COMPLETED}" ]]; then
_undeploy_app
fi
}
@test "retrieve recipe if missing" {
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE"
assert_success
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
run $ABRA app backup "$TEST_APP_DOMAIN"
assert_failure
assert_output --partial 'no containers matching'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
}
@test "bail if unstaged changes and no --chaos" {
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_success
assert_output --partial 'foo'
run $ABRA app backup "$TEST_APP_DOMAIN" app
assert_failure
assert_output --partial 'locally unstaged changes'
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
_checkout_recipe
}
# bats test_tags=slow
@test "do not bail if unstaged changes and --chaos" {
run bash -c 'echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"'
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_success
assert_output --partial 'foo'
run $ABRA app deploy "$TEST_APP_DOMAIN" --chaos --no-input
assert_success
run $ABRA app backup "$TEST_APP_DOMAIN" app --chaos
assert_success
assert_output --partial 'running backup for the app service'
_undeploy_app
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@test "ensure recipe up to date if no --offline" {
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3'
run $ABRA app backup "$TEST_APP_DOMAIN" --debug
assert_failure
assert_output --partial 'no containers matching'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
refute_output --partial 'behind 3'
_reset_recipe
}
@test "ensure recipe not up to date if --offline" {
latestCommit="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)"
refute [ -z "$latestCommit" ];
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3'
run $ABRA app backup "$TEST_APP_DOMAIN" --debug --offline
assert_failure
assert_output --partial 'no containers matching'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$latestCommit"
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
refute_output --partial 'behind 3'
_reset_recipe
}
@test "detect backup labels" {
run $ABRA app backup "$TEST_APP_DOMAIN" --debug
assert_failure
assert_output --partial 'no containers matching'
assert_output --partial 'detected backup paths'
assert_output --partial 'detected pre-hook command'
assert_output --partial 'detected post-hook command'
}
@test "error if backups not enabled" {
run sed -i '/backupbot.backup=true/d' "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"
assert_success
run $ABRA app backup "$TEST_APP_DOMAIN" app --chaos
assert_failure
assert_output --partial 'no backup config for app'
_checkout_recipe
}
@test "error if backup paths not configured" {
run sed -i '/backupbot.backup.path=.*/d' "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"
assert_success
run $ABRA app backup "$TEST_APP_DOMAIN" app --chaos
assert_failure
assert_output --partial 'backup paths are empty for app?'
_checkout_recipe
}
# bats test_tags=slow
@test "backup single service" {
_deploy_app
run $ABRA app backup "$TEST_APP_DOMAIN" app
assert_success
assert_output --partial 'running backup for the app service'
sanitisedDomainName="${TEST_APP_DOMAIN//./_}"
assert_output --partial "_$sanitisedDomainName_app"
assert_exists "$ABRA_DIR/backups"
assert bash -c "ls $ABRA_DIR/backups | grep -q $1_$sanitisedDomainName_app"
_undeploy_app
}

View File

@ -0,0 +1,126 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
@test "validate app argument" {
run $ABRA app check
assert_failure
assert_output --partial 'no app provided'
run $ABRA app check DOESNTEXIST
assert_failure
assert_output --partial 'cannot find app'
}
@test "retrieve recipe if missing" {
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE"
assert_success
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
run $ABRA app check "$TEST_APP_DOMAIN"
assert_success
refute_output --partial '❌'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
}
@test "bail if unstaged changes and no --chaos" {
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app check "$TEST_APP_DOMAIN"
assert_failure
assert_output --partial 'locally unstaged changes'
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@test "do not bail if unstaged changes and --chaos" {
run bash -c 'echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"'
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app check "$TEST_APP_DOMAIN" --chaos
assert_success
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@test "ensure recipe up to date if no --offline" {
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3'
run $ABRA app check "$TEST_APP_DOMAIN"
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
refute_output --partial 'behind 3'
_reset_recipe
}
@test "ensure recipe not up to date if --offline" {
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~1
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 1'
# 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
# it
run $ABRA app check "$TEST_APP_DOMAIN" --offline
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 1'
_reset_recipe
}
@test "error if missing .env.sample" {
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/.env.sample"
assert_success
run $ABRA app check "$TEST_APP_DOMAIN" --chaos
assert_failure
assert_output --partial '.env.sample does not exist?'
_checkout_recipe
}
@test "error if missing env var" {
run $ABRA app check "$TEST_APP_DOMAIN"
assert_success
refute_output --partial '❌'
run bash -c 'echo "NEW_VAR=foo" >> "$ABRA_DIR/recipes/$TEST_RECIPE/.env.sample"'
assert_success
run $ABRA app check "$TEST_APP_DOMAIN" --chaos
assert_success
assert_output --partial '❌'
_checkout_recipe
}

View File

@ -0,0 +1,183 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
teardown(){
# https://github.com/bats-core/bats-core/issues/383#issuecomment-738628888
if [[ -z "${BATS_TEST_COMPLETED}" ]]; then
_undeploy_app
fi
}
@test "validate app argument" {
run $ABRA app cmd
assert_failure
assert_output --partial 'no app provided'
run $ABRA app cmd DOESNTEXIST
assert_failure
assert_output --partial 'cannot find app'
}
@test "retrieve recipe if missing" {
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE"
assert_success
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local
assert_success
assert_output --partial 'baz'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
}
@test "bail if unstaged changes and no --chaos" {
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local
assert_failure
assert_output --partial 'locally unstaged changes'
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@test "do not bail if unstaged changes and --chaos" {
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local --chaos
assert_success
assert_output --partial 'baz'
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@test "ensure recipe up to date if no --offline" {
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3'
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local
assert_success
assert_output --partial 'baz'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
refute_output --partial 'behind 3'
_reset_recipe "$TEST_RECIPE"
}
@test "ensure recipe not up to date if --offline" {
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3'
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local --offline
assert_success
assert_output --partial 'baz'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3'
_reset_recipe "$TEST_RECIPE"
}
@test "error if missing arguments without passing --local" {
run $ABRA app cmd "$TEST_APP_DOMAIN"
assert_failure
assert_output --partial 'missing arguments'
}
@test "error if missing arguments when passing --local" {
run $ABRA app cmd "$TEST_APP_DOMAIN" --local
assert_failure
assert_output --partial 'missing arguments'
}
@test "cannot use --local and --user at same time" {
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local --user root
assert_failure
assert_output --partial 'cannot use --local & --user together'
}
@test "error if missing abra.sh" {
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/abra.sh"
assert_success
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local --chaos
assert_failure
assert_output --partial "$ABRA_DIR/recipes/$TEST_RECIPE/abra.sh does not exist"
_checkout_recipe
}
@test "error if missing command" {
run $ABRA app cmd "$TEST_APP_DOMAIN" doesnt_exist --local
assert_failure
assert_output --partial "doesn't have a doesnt_exist function"
}
@test "run --local command" {
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd --local
assert_success
assert_output --partial 'baz'
}
@test "run command with single arg" {
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd_arg --local -- bing
assert_success
assert_output --partial 'bing'
}
@test "run command with several args" {
run $ABRA app cmd "$TEST_APP_DOMAIN" test_cmd_args --local -- bong bang
assert_success
assert_output --partial 'bong bang'
}
# bats test_tags=slow
@test "run command on service" {
_deploy_app
run $ABRA app cmd "$TEST_APP_DOMAIN" app test_cmd
assert_success
assert_output --partial 'baz'
_undeploy_app
}
# bats test_tags=slow
@test "error if missing service" {
_deploy_app
run $ABRA app cmd "$TEST_APP_DOMAIN" doesnt_exist test_cmd
assert_failure
assert_output --partial 'no service doesnt_exist'
_undeploy_app
}

View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
}
teardown_file(){
_rm_server
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
@test "validate app argument" {
run $ABRA app config
assert_failure
assert_output --partial 'no app provided'
run $ABRA app config DOESNTEXIST
assert_failure
assert_output --partial 'cannot find app'
}

View File

@ -0,0 +1,130 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
teardown(){
# https://github.com/bats-core/bats-core/issues/383#issuecomment-738628888
if [[ -z "${BATS_TEST_COMPLETED}" ]]; then
_undeploy_app
fi
}
@test "validate app argument" {
run $ABRA app cp
assert_failure
assert_output --partial 'no app provided'
run $ABRA app cp DOESNTEXIST
assert_failure
assert_output --partial 'cannot find app'
}
@test "error if missing src/dest arguments" {
run $ABRA app cp "$TEST_APP_DOMAIN"
assert_failure
assert_output --partial 'missing <src> argument'
run $ABRA app cp "$TEST_APP_DOMAIN" myfile.txt
assert_failure
assert_output --partial 'missing <dest> argument'
}
@test "either src/dest has correct syntax" {
run $ABRA app cp "$TEST_APP_DOMAIN" myfile.txt app
assert_failure
assert_output --partial 'arguments must take $SERVICE:$PATH form'
run $ABRA app cp "$TEST_APP_DOMAIN" app .
assert_failure
assert_output --partial 'arguments must take $SERVICE:$PATH form'
}
@test "detect 'coming FROM' syntax" {
run $ABRA app cp "$TEST_APP_DOMAIN" app:/myfile.txt . --debug
assert_failure
assert_output --partial 'coming FROM the container'
}
@test "detect 'going TO' syntax" {
run $ABRA app cp "$TEST_APP_DOMAIN" myfile.txt app:/somewhere --debug
assert_failure
assert_output --partial 'going TO the container'
}
@test "error if local file missing" {
run $ABRA app cp "$TEST_APP_DOMAIN" myfile.txt app:/somewhere
assert_failure
assert_output --partial 'myfile.txt does not exist locally?'
}
# bats test_tags=slow
@test "error if service doesn't exist" {
_deploy_app
run bash -c "echo foo >> $BATS_TMPDIR/myfile.txt"
assert_success
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/myfile.txt" doesnt_exist:/
assert_failure
assert_output --partial 'no containers matching'
run rm -rf "$BATS_TMPDIR/myfile.txt"
assert_success
_undeploy_app
}
# bats test_tags=slow
@test "copy to container" {
_deploy_app
run bash -c "echo foo >> $BATS_TMPDIR/myfile.txt"
assert_success
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/myfile.txt" app:/etc
assert_success
run rm -rf "$BATS_TMPDIR/myfile.txt"
assert_success
_undeploy_app
}
# bats test_tags=slow
@test "copy from container" {
_deploy_app
run bash -c "echo foo >> $BATS_TMPDIR/myfile.txt"
assert_success
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/myfile.txt" app:/etc
assert_success
run rm -rf "$BATS_TMPDIR/myfile.txt"
assert_success
run $ABRA app cp "$TEST_APP_DOMAIN" app:/etc/myfile.txt "$BATS_TMPDIR"
assert_success
assert_exists "$BATS_TMPDIR/myfile.txt"
assert bash -c "cat $BATS_TMPDIR/myfile.txt | grep -q foo"
run rm -rf "$BATS_TMPDIR/myfile.txt"
assert_success
_undeploy_app
}

View File

@ -0,0 +1,364 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
teardown(){
# https://github.com/bats-core/bats-core/issues/383#issuecomment-738628888
if [[ -z "${BATS_TEST_COMPLETED}" ]]; then
_undeploy_app
fi
}
@test "validate app argument" {
run $ABRA app deploy
assert_failure
assert_output --partial 'no app provided'
run $ABRA app deploy DOESNTEXIST
assert_failure
assert_output --partial 'cannot find app'
}
@test "bail if unstaged changes and no --chaos" {
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_success
assert_output --partial 'foo'
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
assert_failure
assert_output --partial 'locally unstaged changes'
refute_output --partial 'chaos'
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
_checkout_recipe
}
# bats test_tags=slow
@test "do not bail if unstaged changes and --chaos" {
run bash -c 'echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"'
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_success
assert_output --partial 'foo'
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--chaos --no-input --no-converge-checks
assert_success
assert_output --partial 'chaos'
_undeploy_app
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
# bats test_tags=slow
@test "ensure recipe up to date if no --offline" {
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3'
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
refute_output --partial 'behind 3'
_reset_recipe
_undeploy_app
}
# bats test_tags=slow
@test "ensure recipe not up to date if --offline" {
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3'
# NOTE(d1): need to use --chaos to force same commit
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos --offline
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3'
_undeploy_app
_reset_recipe
}
# bats test_tags=slow
@test "deploy latest commit if no published versions and no --chaos" {
latestCommit="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)"
_remove_tags
# NOTE(d1): need to pass --offline to stop tags being pulled again
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --offline
assert_success
assert_output --partial "$latestCommit"
assert_output --partial 'using latest commit'
refute_output --partial 'chaos'
_undeploy_app
_reset_tags
}
# bats test_tags=slow
@test "ensure same commit if --chaos" {
latestCommit="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3'
threeCommitsBack="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)"
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
refute_output --partial "$latestCommit"
assert_output --partial "$threeCommitsBack"
assert_output --partial 'chaos'
_undeploy_app
_reset_recipe
}
# bats test_tags=slow
@test "retrieve recipe if missing" {
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE"
assert_success
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
_undeploy_app
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
}
@test "no deploy if lint error" {
run sed -i '/traefik.enable=.*/d' "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_failure
assert_output --partial 'failed lint checks'
_checkout_recipe
}
# bats test_tags=slow
@test "error if already deployed and no --force/--chaos" {
_deploy_app
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks
assert_failure
assert_output --partial 'already deployed'
_undeploy_app
}
# bats test_tags=slow
@test "re-deploy deployed app if --force/--chaos" {
_deploy_app
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --force
assert_success
assert_output --partial 'already deployed but continuing'
assert_output --partial '--force'
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
assert_output --partial 'already deployed but continuing'
assert_output --partial '--chaos'
_undeploy_app
}
# bats test_tags=slow
@test "deploy latest version from catalogue if no --chaos" {
latestVersion=$(jq -r '.gitea.versions[-1] | keys[0]' < "$ABRA_DIR/catalogue/recipes.json")
refute [ -z "$latestVersion" ];
run $ABRA app new gitea \
--no-input \
--server "$TEST_SERVER" \
--domain "gitea.$TEST_SERVER" \
--secrets
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/gitea.$TEST_SERVER.env"
run $ABRA app deploy "gitea.$TEST_SERVER" --no-input --no-converge-checks
assert_success
assert_output --partial "$latestVersion"
run $ABRA app undeploy "gitea.$TEST_SERVER" --no-input
assert_success
run $ABRA app secret remove "gitea.$TEST_SERVER" --all --no-input
assert_success
# NOTE(d1): to let the stack come down before nuking volumes
sleep 5
run $ABRA app volume remove "gitea.$TEST_SERVER" --no-input
assert_success
run $ABRA app remove "gitea.$TEST_SERVER" --no-input
assert_success
assert_not_exists "$ABRA_DIR/servers/$TEST_SERVER/gitea.$TEST_SERVER.env"
}
# bats test_tags=slow
@test "skip domain check if missing DOMAIN=" {
run cp "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$BATS_TMPDIR/$TEST_APP_DOMAIN.env"
assert_success
assert_exists "$BATS_TMPDIR/$TEST_APP_DOMAIN.env"
run grep -q "DOMAIN=" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run sed -i '/DOMAIN=.*/d' "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run grep -q "DOMAIN=" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_failure
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial 'no DOMAIN=... configured for app'
run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
assert_success
run mv "$BATS_TMPDIR/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "ensure domain is checked" {
appDomain="custom-html.DOESNTEXIST"
run $ABRA app new custom-html \
--no-input \
--server "$TEST_SERVER" \
--domain "$appDomain"
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$appDomain.env"
run $ABRA app deploy "$appDomain" --no-input
assert_failure
assert_output --partial 'no such host'
}
# bats test_tags=slow
@test "skip domain check when requested" {
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --no-domain-checks
assert_success
assert_output --partial 'skipping domain checks as requested'
_undeploy_app
}
@test "error if specific version does not exist" {
run $ABRA app deploy "$TEST_APP_DOMAIN" DOESNTEXIST --no-input --no-converge-checks
assert_failure
assert_output --partial "doesn't seem to have version DOESNTEXIST available"
}
# bats test_tags=slow
@test "deploy specific version" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input
assert_success
assert_output --partial "0.2.0+1.21.0"
_undeploy_app
}
@test "bail out if specific version and chaos" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--chaos --no-input --no-converge-checks
assert_failure
assert_output --partial 'cannot use'
}
# bats test_tags=slow
@test "COMPOSE_FILE with \$COMPOSE_FILE override works" {
_reset_recipe
run sed -i 's/#COMPOSE_FILE="$COMPOSE_FILE:compose.extra_env.yml"/COMPOSE_FILE="$COMPOSE_FILE:compose.extra_env.yml"/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
# NOTE(d1): --chaos used to bypass versions and access compose.extra_env.yml
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
assert_output --partial "compose.yml"
assert_output --partial "compose.extra_env.yml"
_undeploy_app
_reset_app
}
@test "error if no secrets generated" {
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 deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_failure
assert_output --partial 'unable to deploy, secrets not generated'
_reset_app
}
@test "recipe config comments not present in values" {
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
assert_success
run $ABRA app run "$TEST_APP_DOMAIN" app env
assert_success
refute_output --partial 'should be removed'
}

View File

@ -0,0 +1,44 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
@test "validate app argument" {
run $ABRA app errors
assert_failure
assert_output --partial 'no app provided'
run $ABRA app errors DOESNTEXIST
assert_failure
assert_output --partial 'cannot find app'
}
@test "error if not deployed" {
run $ABRA app errors "$TEST_APP_DOMAIN"
assert_failure
assert_output --partial 'is not deployed'
}
# bats test_tags=slow
@test "report errors" {
_deploy_app
run $ABRA app errors "$TEST_APP_DOMAIN"
assert_success
_undeploy_app
}

View File

@ -0,0 +1,137 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
teardown(){
# https://github.com/bats-core/bats-core/issues/383#issuecomment-738628888
if [[ -z "${BATS_TEST_COMPLETED}" ]]; then
_undeploy_app
fi
}
@test "list without status" {
run $ABRA app ls
assert_success
assert_output --partial "$TEST_SERVER"
assert_output --partial "$TEST_APP_DOMAIN"
}
# bats test_tags=slow
@test "list with status" {
run $ABRA app ls --status
assert_success
assert_output --partial "$TEST_SERVER"
assert_output --partial "$TEST_APP_DOMAIN"
assert_output --partial "unknown"
_deploy_app
run $ABRA app ls --status
assert_success
assert_output --partial "$TEST_SERVER"
assert_output --partial "$TEST_APP_DOMAIN"
assert_output --partial "deployed"
_undeploy_app
}
@test "filter by server" {
run mkdir -p "$ABRA_DIR/servers/foo.com"
assert_success
assert_exists "$ABRA_DIR/servers/foo.com"
run cp \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" \
"$ABRA_DIR/servers/foo.com/app.foo.com.env"
assert_exists "$ABRA_DIR/servers/foo.com/app.foo.com.env"
run $ABRA app ls
assert_success
assert_output --partial "$TEST_SERVER"
assert_output --partial "foo.com"
run $ABRA app ls --server foo.com
assert_success
refute_output --partial "server: $TEST_SERVER |"
assert_output --partial "server: foo.com |"
run rm -rf "$ABRA_DIR/servers/foo.com"
assert_success
assert_not_exists "$ABRA_DIR/servers/foo.com"
}
@test "filter by recipe" {
run mkdir -p "$ABRA_DIR/servers/foo.com"
assert_success
assert_exists "$ABRA_DIR/servers/foo.com"
run cp \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" \
"$ABRA_DIR/servers/foo.com/app.foo.com.env"
assert_exists "$ABRA_DIR/servers/foo.com/app.foo.com.env"
run sed -i "s/TYPE=$TEST_RECIPE/TYPE=foo-recipe/g" "$ABRA_DIR/servers/foo.com/app.foo.com.env"
assert grep -q "TYPE=foo-recipe" "$ABRA_DIR/servers/foo.com/app.foo.com.env"
run $ABRA app ls
assert_success
assert_output --partial "$TEST_RECIPE"
assert_output --partial "foo-recipe"
run $ABRA app ls --recipe foo-recipe
assert_success
refute_output --partial "$TEST_RECIPE"
assert_output --partial "foo-recipe"
}
@test "server stats are correct" {
run $ABRA app ls
assert_success
assert_output --partial "server: $TEST_SERVER"
assert_output --partial "total apps: 1"
run mkdir -p "$ABRA_DIR/servers/foo.com"
assert_success
assert_exists "$ABRA_DIR/servers/foo.com"
run cp \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" \
"$ABRA_DIR/servers/foo.com/app.foo.com.env"
assert_exists "$ABRA_DIR/servers/foo.com/app.foo.com.env"
run $ABRA app ls
assert_success
assert_output --partial "$TEST_SERVER"
assert_output --partial "foo.com"
assert_output --partial "total servers: 2"
assert_output --partial "total apps: 2"
run rm -rf "$ABRA_DIR/servers/foo.com"
assert_success
assert_not_exists "$ABRA_DIR/servers/foo.com"
}
@test "output is machine readable" {
run $ABRA app ls --machine
expectedOutput='{"'
expectedOutput+="$TEST_SERVER"
expectedOutput+='":{"apps":'
assert_output --partial "$expectedOutput"
}

View File

@ -0,0 +1,46 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
@test "validate app argument" {
run $ABRA app logs
assert_failure
assert_output --partial 'no app provided'
run $ABRA app logs DOESNTEXIST
assert_failure
assert_output --partial 'cannot find app'
}
@test "error if not deployed" {
run $ABRA app logs "$TEST_APP_DOMAIN"
assert_failure
assert_output --partial 'is not deployed'
}
@test "retrieve recipe if missing" {
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE"
assert_success
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
run $ABRA app logs "$TEST_APP_DOMAIN"
assert_failure
assert_output --partial 'is not deployed'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
}

View File

@ -0,0 +1,145 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
}
teardown_file(){
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_fetch_recipe
}
teardown(){
_rm_app
}
@test "create new app" {
run $ABRA app new "$TEST_RECIPE" \
--no-input \
--server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN"
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
}
@test "does not overwrite existing env files" {
_new_app
run $ABRA app new "$TEST_RECIPE" \
--no-input \
--server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN"
assert_failure
assert_output --partial 'already exists'
}
@test "bail if unstaged changes and no --chaos" {
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_success
assert_output --partial 'foo'
run $ABRA app new "$TEST_RECIPE" \
--no-input \
--server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN"
assert_failure
assert_output --partial 'locally unstaged changes'
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@test "do not bail if unstaged changes and --chaos" {
run bash -c 'echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"'
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_success
assert_output --partial 'foo'
run $ABRA app new "$TEST_RECIPE" \
--no-input \
--chaos \
--server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN" \
--secrets
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@test "ensure recipe up to date if no --offline" {
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3'
run $ABRA app new "$TEST_RECIPE" \
--no-input \
--server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN" \
--secrets
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
refute_output --partial 'behind 3'
_reset_recipe
}
@test "ensure recipe not up to date if --offline" {
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3'
# NOTE(d1): need to use --chaos to force same commit
run $ABRA app new "$TEST_RECIPE" \
--no-input \
--offline \
--chaos \
--server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN" \
--secrets
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3'
_reset_recipe
}
@test "generate secrets" {
run $ABRA app new "$TEST_RECIPE" \
--no-input \
--server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN" \
--secrets
assert_success
assert_output --partial 'generated secrets'
assert_output --partial 'test_pass_one'
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
run $ABRA app secret ls "$TEST_APP_DOMAIN"
assert_success
assert_output --partial 'test_pass_one'
}

View File

@ -0,0 +1,53 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
teardown(){
# https://github.com/bats-core/bats-core/issues/383#issuecomment-738628888
if [[ -z "${BATS_TEST_COMPLETED}" ]]; then
_undeploy_app
fi
}
@test "validate app argument" {
run $ABRA app ps
assert_failure
assert_output --partial 'no app provided'
run $ABRA app ps DOESNTEXIST
assert_failure
assert_output --partial 'cannot find app'
}
@test "error if not deployed" {
run $ABRA app ps "$TEST_APP_DOMAIN"
assert_failure
assert_output --partial 'is not deployed'
}
# bats test_tags=slow
@test "show ps report" {
_deploy_app
run $ABRA app ps "$TEST_APP_DOMAIN"
assert_success
assert_output --partial 'app'
assert_output --partial 'healthy'
_undeploy_app
}

Some files were not shown because too many files have changed in this diff Show More