Compare commits

...

1176 Commits

Author SHA1 Message Date
e3a0af5840 build: upgrade goreleaser
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#474
2024-03-12 10:11:14 +01:00
9a3a39a185 chore: new 0.9.x series
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-03-12 10:05:31 +01:00
cea56dddde fix: drop deprecated stanza (goreleaser) 2024-03-12 10:04:50 +01:00
2c515ce70a Revert "feat: backup revolution"
This reverts commit c5687dfbd7.

This is a temporary measure to facilitate a release which won't
completely explode peoples workflows (missing command logic). We
re-instate this commit after the first 0.9.x release.
2024-03-12 10:03:42 +01:00
40c0fb4bac fix-integration-tests (!403)
All checks were successful
continuous-integration/drone/push Build is passing
In preparation for the new abra release, let's fix all integration tests

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

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

Co-authored-by: decentral1se <cellarspoon@riseup.net>
Reviewed-on: coop-cloud/abra#403
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2024-03-11 13:27:21 +00:00
0643df6d73 feat: fetch all recipes when no recipe is specified (!401)
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#530

Reviewed-on: coop-cloud/abra#401
Reviewed-by: decentral1se <decentral1se@noreply.git.coopcloud.tech>
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2024-01-24 15:01:33 +00:00
e9b99fe921 make installer save abra-download to /tmp/ directory
All checks were successful
continuous-integration/drone/push Build is passing
the current location of download is ~/.local/bin/ but this
conflicts with some security tools
2024-01-24 14:27:09 +00:00
4920dfedb3 fix: retry docker volume remove (!399)
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#509

Reviewed-on: coop-cloud/abra#399
Reviewed-by: decentral1se <decentral1se@noreply.git.coopcloud.tech>
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2024-01-19 15:09:00 +00:00
0a3624c15b feat: add version input to abra app new (!400)
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#519

Reviewed-on: coop-cloud/abra#400
Reviewed-by: decentral1se <decentral1se@noreply.git.coopcloud.tech>
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2024-01-19 15:08:41 +00:00
c5687dfbd7 feat: backup revolution
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/organising#485
2024-01-12 22:01:08 +01:00
ca91abbed9 fix: correct append service name logic in Filters function (!396)
All checks were successful
continuous-integration/drone/push Build is passing
This fixes a regression introduced by #395

Reviewed-on: coop-cloud/abra#396
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2023-12-22 12:08:12 +00:00
d4727db8f9 feat: abra app logs shows task errors (!395)
All checks were successful
continuous-integration/drone/push Build is passing
The log command now checks for the ready state in the task list. If it is not ready. It shows the task logs. This might look like this:
```
ERRO[0000] Service abra-test-recipe_default_app: State rejected: No such image: ngaaaax:1.21.0
ERRO[0000] Service abra-test-recipe_default_app: State preparing:
ERRO[0000] Service abra-test-recipe_default_app: State rejected: No such image: ngaaaax:1.21.0
ERRO[0000] Service abra-test-recipe_default_app: State rejected: No such image: ngaaaax:1.21.0
ERRO[0000] Service abra-test-recipe_default_app: State rejected: No such image: ngaaaax:1.21.0
```

Closes coop-cloud/organising#518

Reviewed-on: coop-cloud/abra#395
Reviewed-by: decentral1se <decentral1se@noreply.git.coopcloud.tech>
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2023-12-14 13:15:24 +00:00
af8cd1f67a feat: abra release now asks for a release note (!393)
All checks were successful
continuous-integration/drone/push Build is passing
This implements coop-cloud/organising#540 by checking if a`release/next` file exists and if so moves it to `release/<tag>`. When no release notes exists it prompts for them.

Reviewed-on: coop-cloud/abra#393
Reviewed-by: moritz <moritz.m@local-it.org>
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2023-12-12 14:46:20 +00:00
cdd7516e54 chore: go mod tidy [ci skip] 2023-12-04 22:56:58 +01:00
99e3ed416f fix: secret name generation when secretId is not part of the secret name
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-04 21:52:09 +00:00
02b726db02 add comments to better explain how the length modifier gets added to the secret
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-04 17:30:26 +00:00
2de6934322 feat: abra app cp enhancements
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-02 15:39:27 +00:00
cb49cf06d1 chore: drop old godotenv pointers [ci skip]
Follows 9affda8a70
2023-12-02 13:02:24 +01:00
9affda8a70 chore: update godotenv fork commit pointer
All checks were successful
continuous-integration/drone/push Build is passing
Follows coop-cloud/abra#391
2023-12-02 12:59:42 +01:00
3957b7c965 proper env modifiers support
All checks were successful
continuous-integration/drone/push Build is passing
This implements proper modifier support in the env file using this new fork of the godotenv library. The modifier implementation is quite basic for but can be improved later if needed. See this commit for the actual implementation.

Because we are now using proper modifer parsing, it does not affect the parsing of value, so this is possible again:
```
MY_VAR="#foo"
```
Closes coop-cloud/organising#535
2023-12-01 11:03:52 +00:00
0d83339d80 fix(ssh): increase connection timeout #482
All checks were successful
continuous-integration/drone/push Build is passing
see coop-cloud/organising#482
2023-11-30 16:35:53 +01:00
6e54ec7213 test: skip failing test for now
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/organising#535.
2023-11-28 11:42:36 +01:00
66b40a9189 fix: just run it in place [ci skip] 2023-11-27 11:25:01 +01:00
049f02f063 docs: add p4u1 [ci skip] 2023-11-27 11:23:03 +01:00
15857e6453 fix: clean up after cp'ing script [ci skip]
Follows 31e0ed75b0.
2023-11-27 11:21:46 +01:00
31e0ed75b0 build: target for docker building
Some checks failed
continuous-integration/drone/push Build is failing
Adapted from coop-cloud/abra#384.

Thanks @cas.
2023-11-27 11:15:59 +01:00
b1d3fcbb0b add integration test
Some checks failed
continuous-integration/drone/push Build is failing
2023-11-27 10:01:33 +00:00
7b6134f35e add bash completion for abra cmd 2023-11-27 10:01:33 +00:00
316b59b465 test: support local-first testing
Some checks failed
continuous-integration/drone/push Build is failing
Cherry-picked from coop-cloud/abra#389

Thanks @p4u1.
2023-11-27 10:41:46 +01:00
92b073d5b6 chore: go mod tidy
Some checks failed
continuous-integration/drone/push Build is failing
2023-11-27 10:28:43 +01:00
9b0dd933b5 chore(deps): update module github.com/schollz/progressbar/v3 to v3.14.1
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-11-10 08:00:52 +00:00
f255fa1555 chore(deps): update module github.com/hashicorp/go-retryablehttp to v0.7.5
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-11-09 08:00:33 +00:00
74200318ab chore(deps): update module github.com/schollz/progressbar/v3 to v3.14.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-11-07 08:01:11 +00:00
609656b4e1 chore(deps): update module golang.org/x/sys to v0.14.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-11-06 08:00:33 +00:00
856c9f2f7d chore: go mod tidy
Some checks failed
continuous-integration/drone/push Build is failing
2023-11-04 09:37:15 +01:00
bd5cdd3443 chore(deps): update module github.com/docker/docker to v24.0.7
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-10-30 08:00:53 +00:00
79d274e074 chore(deps): update module github.com/docker/cli to v24.0.7
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-10-27 07:01:16 +00:00
51e3df17f1 chore(deps): update module github.com/go-git/go-git/v5 to v5.10.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-10-26 07:00:33 +00:00
ccf0215495 hotfix: parse values starting with # correctly
Some checks failed
continuous-integration/drone/push Build is failing
2023-10-23 19:21:45 +02:00
254df7f2be feat: app cmd ls
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See coop-cloud/organising#484
2023-10-17 21:16:31 +02:00
6a673ef101 refactor: filter by topic when building catalogue
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See coop-cloud/organising#377
2023-10-16 18:42:38 +02:00
7f7f7224c6 feat: diff on release flow
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Also, don't commit unstaged files.
2023-10-16 18:31:22 +02:00
f96bf9a8ac feat: recipe reset, recipe diff
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
All checks were successful
continuous-integration/drone/push Build is passing
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
All checks were successful
continuous-integration/drone/push Build is passing
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
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/organising#503.
2023-10-09 20:54:44 +00:00
3e511446aa refactor: use app check emoji here too
All checks were successful
continuous-integration/drone/push Build is passing
2023-10-09 22:53:46 +02:00
7566b4262b fix: set go version to 1.21
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-09 22:07:30 +02:00
c249c6ae9c fix: fix: trim comments that are not modifers
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See coop-cloud/organising#505
2023-10-09 14:42:05 +02:00
be693e9df0 fix: trim comments that are not modifers
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
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
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See coop-cloud/organising#446
2023-10-06 10:56:33 +02:00
6fc4573a71 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-10-06 09:49:03 +02:00
cbe6676881 chore(deps): update module golang.org/x/sys to v0.13.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-10-06 07:00:49 +00:00
b4fd39828f test: abra-integration-test-recipe -> abra-test-recipe
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/abra-test-recipe#3
2023-10-05 14:22:11 +02:00
14f2d72aba refactor!: lowercase, hyphenate keys
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-10-04 14:05:02 +02:00
c646263e9e fix: validate COMPOSE_FILE
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
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
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/organising#490
2023-10-04 09:29:10 +00:00
379915587c fix: don't export from within function
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
All checks were successful
continuous-integration/drone/push Build is passing
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
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-10-03 07:00:43 +00:00
f18f0b6f82 build: set ABRA_DIR explicitly
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-09-27 09:19:57 +02:00
1e4f1b4ade build: disable publish image for now
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone Build is failing
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
Some checks reported errors
continuous-integration/drone/push Build was killed
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
All checks were successful
continuous-integration/drone/push Build is passing
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
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
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
All checks were successful
continuous-integration/drone/push Build is passing
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
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
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
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-22 09:47:36 +02:00
9b6eb613aa test: woops, keep unit test target default
All checks were successful
continuous-integration/drone/push Build is passing
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
Some checks failed
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#451
2023-09-21 11:33:45 +02:00
8788558cf1 fix: only sync version label once
Some checks failed
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#492
2023-09-21 10:58:17 +02:00
76035e003e fix: recipe workflow with integration tests
Some checks failed
continuous-integration/drone/push Build is failing
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]
Some checks failed
continuous-integration/drone/pr Build is failing
2023-09-20 13:51:06 +02:00
7a9224b2b2 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-19 12:38:02 +02:00
e73d1a8359 chore(deps): update module gotest.tools/v3 to v3.5.1
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-09-19 07:02:01 +00:00
f8c49c82c8 fix: skip "abra-integration-test-recipe" also
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-18 14:02:38 +02:00
ab7edd2a62 refactor!: drop "record" & "server new" command
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-14 09:39:28 +02:00
e5e122296f chore(deps): update module github.com/go-git/go-git/v5 to v5.9.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-09-13 07:01:51 +00:00
83bf148304 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-07 14:34:40 +02:00
d80b882b83 chore(deps): update module github.com/docker/docker to v24.0.6
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-09-07 07:02:43 +00:00
c345c6f5f1 chore(deps): update module github.com/docker/cli to v24.0.6
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-09-06 07:01:56 +00:00
f8c4fd72a3 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-05 13:56:34 +02:00
10f612f998 test: more integration tests
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-09-05 13:03:38 +02:00
58e78e4d7c fix: overridable ABRA_DIR
Some checks failed
continuous-integration/drone/push Build is failing
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
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-09-04 07:02:01 +00:00
d9b4f4ef3b chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-26 09:58:46 +02:00
c365dcf96d chore(deps): update module github.com/hetznercloud/hcloud-go to v1.50.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-08-25 07:02:00 +00:00
0c6a7cc0b8 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.49.1
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-08-18 07:01:42 +00:00
6640cfab64 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-13 17:42:24 +02:00
71addcd1b2 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.49.0
Some checks failed
continuous-integration/drone/push Build is failing
2023-08-13 15:41:44 +00:00
60c0e55e3d fix: don't specify refs when pulling tags
Some checks failed
continuous-integration/drone/push Build is failing
See coop-cloud/organising#477
2023-08-13 12:07:37 +00:00
e42139fd83 chore(deps): update golang docker tag to v1.21
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-08-09 07:02:07 +00:00
2d826e47d0 chore(deps): update module golang.org/x/sys to v0.11.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-08-07 07:01:55 +00:00
2db172ea5a Further changes to messages.
All checks were successful
continuous-integration/drone/push Build is passing
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'
Some checks reported errors
continuous-integration/drone/push Build was killed
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
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-08-02 00:48:27 +02:00
8bb98ed0ed fix: deploy fresh recipe without versions
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
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
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Continues work of 3dc5662821.
2023-08-01 21:19:20 +02:00
2cd453ae8d build: attempt to ignore goreleaser upgrades
Some checks failed
continuous-integration/drone/push Build is failing
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
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2023-07-31 07:02:04 +00:00
55c7aca3c0 chore: publish 0.8.0-rc2-beta
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-07-29 00:31:49 +02:00
8fa9419c99 build: pin to goreleaser v18 [ci skip]
See coop-cloud/organising#474
2023-07-29 00:22:01 +02:00
73ad0a802e Revert "build: replacements is deprecated"
This reverts commit 473cae0146.

Aiming to freeze on an old version of goreleaser for now.
2023-07-29 00:14:08 +02:00
798fd2336c chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-27 21:48:49 +02:00
70e65d6667 chore(deps): update module github.com/go-git/go-git/v5 to v5.8.1
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-07-27 07:06:22 +00:00
efc9602808 chore: welcome comrade rix [ci skip] 2023-07-26 09:59:22 +02:00
1e110f1375 docs: wording [ci skip] 2023-07-26 09:58:30 +02:00
473cae0146 build: replacements is deprecated
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-07-26 09:18:52 +02:00
2da859896a fix: point to rc1 [ci skip] 2023-07-26 08:53:39 +02:00
ab00578ee1 chore: publish 0.8.0-rc1-beta
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-07-26 08:52:33 +02:00
3dc5662821 fix: improved offline support
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#471.
2023-07-26 08:16:07 +02:00
ab64eb2e8d fix: only use git to update local catalogue
See coop-cloud/organising#321.
2023-07-25 21:13:04 +02:00
4f22228aab feat: lint for lightweight tags
See coop-cloud/organising#433
2023-07-25 20:38:29 +02:00
a7f1af7476 refactor: drop internal deploy package 2023-07-25 18:03:37 +02:00
949510d4c3 revert: always clone latest recipe changes
Some checks failed
continuous-integration/drone/push Build is failing
This change was about trying to optimise for offline scenarios but
caused a lot of issues for the online case. It needs to be thought
through again.

See coop-cloud/organising#471.

Closes coop-cloud/organising#432.
2023-07-25 15:05:01 +00:00
9f478dac1d fix: list downgrades/upgrades in correct order
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
Now that we have correct sorting of versions:

  coop-cloud/organising#427

We don't need to reverse sort. Only for showing prompts when the
latest should be the first. Otherwise, logic can follow the sorted
order, the last item in the list is the latest upgrade.

Related:

  coop-cloud/organising#444

Also fix `upgrade` to actually show the latest version
2023-07-25 15:08:32 +02:00
69f38ea445 fix: always show overview, even with -f
coop-cloud/organising#444
2023-07-25 15:08:10 +02:00
0582147874 fix: better error message for missing local tag
Aiming to help the following scenario better:

coop-cloud/organising#444 (comment)
2023-07-25 15:07:29 +02:00
bdeeb75973 fix: upgrade force logic parity with deploy force logic
coop-cloud/organising#444 (comment)
2023-07-25 15:06:50 +02:00
2518e65e3e chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-25 10:22:02 +02:00
8354c92654 Merge remote-tracking branch 'origin/renovate/main-github.com-docker-docker-24.x' 2023-07-25 10:21:16 +02:00
173e81b885 chore(deps): update module github.com/docker/docker to v24.0.5
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-07-25 07:05:53 +00:00
d91731518b chore(deps): update module github.com/docker/cli to v24.0.5
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-07-25 07:05:47 +00:00
2bfee5058d chore(deps): update module github.com/go-git/go-git/v5 to v5.8.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-07-24 07:03:29 +00:00
a7ce71d6cf Fix formatting.
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-15 08:15:46 +00:00
10f60fee1d Replace deprecated system.TempFileSequential with os.CreateTemp 2023-07-15 08:15:46 +00:00
6025ab443f Update volume list options. 2023-07-15 08:15:46 +00:00
43ecf35449 Change CommonOptions (deprecated) to ClientOptions and remove unneeded parameters. 2023-07-15 08:15:46 +00:00
4d2a1065d2 Replace types.volume with new volume type 2023-07-15 08:15:46 +00:00
0b67500cab Add docker v24 and associated dependencies. 2023-07-15 08:15:46 +00:00
e0c3a06182 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-14 19:47:09 +02:00
a86ba4e97b chore(deps): update module github.com/hetznercloud/hcloud-go to v2
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-07-14 07:03:04 +00:00
b5b3395138 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.48.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-07-13 07:03:02 +00:00
502b78ef5c chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-05 09:50:38 +02:00
3e2b4dae6a chore(deps): update module golang.org/x/sys to v0.10.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-07-05 07:02:41 +00:00
573fe403b3 chore(deps): update module gotest.tools/v3 to v3.5.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-06-30 07:02:40 +00:00
76862e9d66 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-06-22 16:44:53 +02:00
e8e337a608 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.47.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-06-22 07:02:07 +00:00
500389c5f5 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.46.1
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-06-19 07:02:59 +00:00
dea665652c chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-06-16 18:10:02 +02:00
e8cf84b523 chore(deps): update module golang.org/x/sys to v0.9.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-06-16 07:03:47 +00:00
fab25a6124 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.46.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-06-15 07:03:10 +00:00
e71377539c chore(deps): update module github.com/alecaivazis/survey/v2 to v2.3.7
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-06-14 07:02:51 +00:00
497ecf476a docs: wording [ci skip] 2023-06-12 00:09:52 +02:00
ff1c043ec5 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-06-07 10:45:17 +02:00
c4d2e297f8 Merge remote-tracking branch 'origin/renovate/main-coopcloud.tech-libcapsul-digest' 2023-06-07 10:44:35 +02:00
e98b8e3666 chore(deps): update module github.com/hashicorp/go-retryablehttp to v0.7.4
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-06-07 07:02:31 +00:00
f5835fe404 chore(deps): update coopcloud.tech/libcapsul digest to 878af47
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-06-07 07:02:13 +00:00
07bbe9394f chore(deps): update module github.com/sirupsen/logrus to v1.9.3
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-06-05 07:02:38 +00:00
6974681af5 fix: improve error message
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-29 14:57:41 +02:00
73250fb899 chore(deps): update module github.com/go-git/go-git/v5 to v5.7.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-24 07:02:28 +00:00
4ce377cffe chore(deps): update module github.com/sirupsen/logrus to v1.9.2
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-19 07:03:02 +00:00
c7dd029689 chore(deps): update module github.com/docker/cli to v20.10.25
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-18 07:02:30 +00:00
51319d2ae2 chore(deps): update module github.com/docker/docker to v20.10.25
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-16 07:03:23 +00:00
d1c2343a54 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.45.1
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-15 07:03:03 +00:00
135ffde0e5 chore(deps): update module github.com/docker/distribution to v2.8.2
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-12 07:03:02 +00:00
6e4dd51b27 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-08 11:42:50 +02:00
81b652718b chore(deps): update module github.com/hetznercloud/hcloud-go to v1.44.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-08 07:02:38 +00:00
442f46e17f chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-05 10:59:22 +02:00
574794d4e8 Merge remote-tracking branch 'origin/renovate/main-golang.org-x-sys-0.x' 2023-05-05 10:58:25 +02:00
88184125c4 chore(deps): update module golang.org/x/sys to v0.8.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-05 07:03:15 +00:00
8a4baa66ee chore(deps): update module github.com/klauspost/pgzip to v1.2.6
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-05 07:03:01 +00:00
16ecbd0291 chore(deps): update module github.com/moby/term to v0.5.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-03 07:02:46 +00:00
f65b262c11 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.43.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-04-28 07:03:28 +00:00
c5d9d88359 Add some minor tweaks to machine readable pathway in recipe upgrade
Some checks failed
continuous-integration/drone/push Build is failing
2023-04-27 16:45:57 +00:00
87e5909363 Make -m imply -n in recipe/upgrade 2023-04-27 16:45:57 +00:00
152c5d4563 Add machine output for recipe/upgrade
- Normal faff related to calling external libraries with structs thnx go
- Ouputs json now
2023-04-27 16:45:57 +00:00
34b274bc52 recipe/upgrade: Refactor upgradability list to make output easier
For future, we can print the struct as JSON.
2023-04-27 16:45:57 +00:00
62f8103fc2 recipe/upgrade: Add non-interactive mode.
Add support for -n which just outputs the list of compatible tags for each image.
2023-04-27 16:45:57 +00:00
2dcbfa1d65 chore(deps): update module github.com/coreos/go-semver to v0.3.1
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-04-26 07:02:52 +00:00
049da94629 fix(version): semver version ordering (!293)
All checks were successful
continuous-integration/drone/push Build is passing
Solves coop-cloud/organising#427

This fix sorts the recipe versions at the catalogue generation and the versions that are received from the catalogue.

Co-authored-by: Moritz <moritz.m@local-it.org>
Reviewed-on: coop-cloud/abra#293
2023-04-26 06:38:15 +00:00
b2739dcdf2 fix(deploy) post deploy cmds
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-18 19:05:46 +02:00
343b2bfb91 docs: go doc badge [ci skip] 2023-04-14 23:31:21 +02:00
17aeed6dbd chore: go mod tidy [ci skip] 2023-04-14 19:09:53 +02:00
27cac81830 fix(app): fix app list chaos field
All checks were successful
continuous-integration/drone/push Build is passing
show only the chaos version if the app is a chaos deploy
2023-04-14 18:01:08 +02:00
31ec322c55 feat(deploy): set timeout via label (!290)
All checks were successful
continuous-integration/drone/push Build is passing
Solves coop-cloud/organising#437

A timeout can be specified globally for a recipe using this label:
`coop-cloud.${STACK_NAME}.timeout=${TIMEOUT:-120}`. This sets the default timeout to 120s. An app specific timeout can be set using the env `TIMEOUT`.

Co-authored-by: Moritz <moritz.m@local-it.org>
Reviewed-on: coop-cloud/abra#290
2023-04-14 14:44:18 +00:00
18615eaaef Post-deploy abra.sh hooks (!292)
All checks were successful
continuous-integration/drone/push Build is passing
This solves coop-cloud/organising#235

Co-authored-by: Moritz <moritz.m@local-it.org>
Reviewed-on: coop-cloud/abra#292
2023-04-14 14:12:31 +00:00
5e508538f3 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.42.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-04-13 07:02:01 +00:00
9e05000476 fix(kadabra): always pull new recipe version
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-06 17:22:33 +02:00
f088a0d327 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-06 10:00:14 +02:00
3832403c97 chore(deps): update module github.com/docker/docker to v20.10.24
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-04-06 07:01:59 +00:00
47058c897c chore(deps): update module github.com/docker/cli to v20.10.24
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-04-05 07:02:26 +00:00
5d4c7f8ef0 chore(deps): update module golang.org/x/sys to v0.7.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-04-04 16:33:38 +00:00
ee4315adb3 fix(rm): remove volumes correctly during app removal
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-30 13:40:44 +02:00
9ade250f01 feat(cmd): add --tty flag to run commands from a script
Some checks failed
continuous-integration/drone/push Build is failing
2023-03-29 14:25:08 +02:00
81b032be85 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-21 09:19:02 +01:00
5409990a68 chore(deps): update module github.com/go-git/go-git/v5 to v5.6.1
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-03-17 08:03:41 +00:00
b1595c0ec9 chore(deps): update module github.com/schollz/progressbar/v3 to v3.13.1
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-03-15 08:02:29 +00:00
6c99a2980b chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-07 16:35:20 +01:00
a9405a36c6 Merge remote-tracking branch 'origin/renovate/main-github.com-hetznercloud-hcloud-go-1.x' 2023-03-07 16:34:23 +01:00
15a417d9bd fix(list): fix output of chaos + chaos-version merge
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-07 13:46:19 +01:00
0ce8b3a5c2 Merge pull request 'app ls --status shows more detailles about the deployment state' (!280) from detailed_app_list into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: coop-cloud/abra#280
2023-03-07 12:31:38 +00:00
edff63b446 Revert "review: change label autoupdate -> auto-update"
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
This reverts commit 74baa76f5ee5e5dd7b71b1f14be97cc40dfc611b.
2023-03-07 13:24:46 +01:00
d5979436c1 review: merge chaos + chaos_version column 2023-03-07 13:24:46 +01:00
cb33edaac3 review: change label autoupdate -> auto-update 2023-03-07 13:24:46 +01:00
e9879e2226 review: label convention chaos_version -> chaos-version 2023-03-07 13:24:46 +01:00
5428ebf43b review: avoid stackName recalculation 2023-03-07 13:24:46 +01:00
d120299929 feat(list): show autoupdate state 2023-03-07 13:24:46 +01:00
3753357ef8 feat(list): show chaos status and chaos version 2023-03-07 13:24:46 +01:00
611430aab2 Set chaos version label for each deployed or upgraded app 2023-03-07 13:24:46 +01:00
f56b02b951 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.41.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-03-07 08:02:02 +00:00
f29278f80a chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-06 15:04:15 +01:00
a9a294cbb7 chore(deps): update module golang.org/x/sys to v0.6.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-03-06 08:02:17 +00:00
73004789a4 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-04 08:50:24 +01:00
440aba77d5 chore(deps): update module github.com/go-git/go-git/v5 to v5.6.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-03-01 08:02:11 +00:00
e4a89bcc4f fix(kadabra): only warn if a deployed app has no published release
All checks were successful
continuous-integration/drone/push Build is passing
2023-02-28 15:34:27 +01:00
eb07617e73 chore: publish new release 0.7.0-beta
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-02-22 08:47:43 +01:00
9fca4e56fb docs: add comrade vera [ci skip] 2023-02-19 11:00:43 +01:00
f17523010a chore: publish next tag 0.7.0-rc3-beta
Some checks failed
continuous-integration/drone/push Build is failing
2023-02-19 10:51:10 +01:00
3058178d84 fix: if all servers good, don't show empty table
All checks were successful
continuous-integration/drone/push Build is passing
2023-02-19 10:34:47 +01:00
d62c4e3400 refactor: improved logging on pruning
All checks were successful
continuous-integration/drone/push Build is passing
2023-02-19 10:28:18 +01:00
5739758c3a fix: give more time to tear down state [ci skip] 2023-02-17 11:11:28 +01:00
a6b5566fa6 refactor: clarify prune scope, not system wide [ci skip] 2023-02-17 11:09:44 +01:00
4dbe1362a8 docs: more clarity on prune functionality
All checks were successful
continuous-integration/drone/push Build is passing
2023-02-17 11:00:02 +01:00
98fc36c830 refactor: hopefully more robust prune logic, docs 2023-02-17 10:59:06 +01:00
b8abc8705c docs: volumes pruning docs - more warnings 2023-02-17 10:42:38 +01:00
636261934f refactor: pass args in, docs, rename, lower-case logs 2023-02-17 10:23:00 +01:00
6381b73a6a chore: use lower-case like elsewhere 2023-02-17 10:21:56 +01:00
1a72e27045 refactor: add server auto-complete & cosmetics 2023-02-17 10:12:46 +01:00
9754c1b2d1 feat: server auto-complete on remove sub-command
All checks were successful
continuous-integration/drone/push Build is passing
2023-02-17 10:10:48 +01:00
b14ec0cda4 review cleanups
All checks were successful
continuous-integration/drone/push Build is passing
2023-02-17 08:53:43 +00:00
c7730ba604 Adding server prune and undeploy prune 2023-02-17 08:53:43 +00:00
47c61df444 docs: add comrade yksflip [ci skip] 2023-02-15 11:26:20 +01:00
312b93e794 fix: no gitops on recipe for "app new"
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#408
2023-02-15 00:49:22 +01:00
992e675921 refactor: use passed down conf to decide 2023-02-15 00:35:33 +01:00
d4f3a7be31 docs: add comrade codegod100 [ci skip] 2023-02-14 17:16:25 +01:00
d619f399e7 Update 'cli/app/undeploy.go'
All checks were successful
continuous-integration/drone/push Build is passing
2023-02-14 13:59:35 +00:00
96a8cb7aff chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-02-14 14:29:25 +01:00
9b51d22c20 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.40.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-02-14 08:02:28 +00:00
d789830ce4 feat: adds --since flag for abra app logs
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-02-14 00:19:38 +01:00
e4b4084dfd fix: stream logs without hitting git.coopcloud.tech
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Medium-sized options refactor in here too!

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

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

Co-authored-by: Moritz <moritz.m@local-it.org>
Reviewed-on: coop-cloud/abra#268
2023-02-08 18:53:04 +00:00
f28af5e42f fix: use correctly formatted comments
All checks were successful
continuous-integration/drone/push Build is passing
2023-02-08 11:28:38 +01:00
fdf4854b0c fix: unbork comments
Some checks failed
continuous-integration/drone/push Build is failing
Was breaking the build but not anymore!
2023-02-08 11:20:30 +01:00
6b9512d09c build: docker dev builds depend on check too 2023-02-08 11:16:54 +01:00
21a86731d0 build: dont test/build if check fails
Some checks failed
continuous-integration/drone/push Build is failing
Save cycles for small mistakes.
2023-02-08 11:13:20 +01:00
91102e6607 build: not so useful anymore, also broken 2023-02-08 11:12:03 +01:00
fadafda0b8 fix: make test suite work again 2023-02-08 11:11:39 +01:00
c03cf76702 chore: gofmt import statements
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-02-08 10:56:39 +01:00
ebb748b7e7 chore: publish next tag 0.7.0-rc1-beta
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-02-08 10:28:54 +01:00
2b3dbee24c chore: go mod tidy
Some checks failed
continuous-integration/drone/push Build is failing
2023-02-07 22:20:11 +01:00
a448cfdd0d fix: revert bogus dependabot changes
Revert "chore(deps): update module github.com/docker/cli to v23"
This reverts commit 5ee6eb53b2.

Revert "chore(deps): update module github.com/docker/docker to v23"
This reverts commit 7b2880d425.
2023-02-07 22:19:28 +01:00
5ee6eb53b2 chore(deps): update module github.com/docker/cli to v23
Some checks failed
continuous-integration/drone/push Build is failing
2023-02-07 21:16:18 +00:00
7b2880d425 chore(deps): update module github.com/docker/docker to v23
Some checks failed
continuous-integration/drone/push Build is failing
2023-02-07 21:16:06 +00:00
928d6f5d7f chore(deps): update module golang.org/x/sys to v0.5.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-02-07 08:02:03 +00:00
29fa607190 fix: restrict pulling to specific branch
Some checks failed
continuous-integration/drone/push Build is failing
2023-02-02 21:12:50 +01:00
7c541ffdfa fix: better error handling in EnsureUpToDate 2023-02-02 21:12:24 +01:00
7ccc4b4c08 fix: woops, remove that print statement
Some checks failed
continuous-integration/drone/push Build is failing
2023-02-02 21:00:31 +01:00
ef4df35995 fix: don't check twice (called in EnsureUpToDate)
Some checks failed
continuous-integration/drone/push Build is failing
2023-02-02 20:59:04 +01:00
71a9155042 fix: specify refs when fetching tags
See coop-cloud/organising#397
2023-02-02 20:58:38 +01:00
2a88491d7c fix: catch errors here too
Some checks failed
continuous-integration/drone/push Build is failing
See coop-cloud/abra#266
2023-02-02 20:26:19 +01:00
bf79552204 fix: improve permission denied message
Some checks failed
continuous-integration/drone/push Build is failing
2023-02-02 20:07:45 +01:00
0a7fa54759 fix: cant pass client here
Some checks failed
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#396
2023-02-02 20:06:49 +01:00
7c1a97be72 refactor!: consolidate SSH handling
Some checks failed
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#389.
Closes coop-cloud/organising#341.
Closes coop-cloud/organising#326.
Closes coop-cloud/organising#380.
Closes coop-cloud/organising#360.
2023-02-02 08:37:14 +00:00
f20fbbc913 chore(deps): update golang docker tag to v1.20
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-02-02 08:02:02 +00:00
76717531bd resolve PR: include the service info in the log message
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-31 16:15:11 +01:00
6774893412 add env ENABLE_AUTO_UPDATE as label to enable/disable the auto update process
Some checks reported errors
continuous-integration/drone/pr Build was killed
2023-01-31 16:12:02 +01:00
ebb86391af add a label to signal that a deploy is a chaos deploy (!265)
Some checks failed
continuous-integration/drone/push Build is failing
Resolves coop-cloud/organising#390 by adding the following label `coop-cloud.${STACK_NAME}.chaos=true` (according to the version label).
This is required for the auto updater coop-cloud/organising#236 (comment)

Co-authored-by: Moritz <moritz.m@local-it.org>
Reviewed-on: coop-cloud/abra#265
2023-01-31 15:06:35 +00:00
50db39424c add a label to signal that a deploy is connected with a recipe (!264)
Some checks failed
continuous-integration/drone/push Build is failing
Resolves coop-cloud/organising#391 by adding the following label `coop-cloud.${STACK_NAME}.recipe=${RECIPE}` (according to the version label).
This is required for the auto updater coop-cloud/organising#236 (comment)

Co-authored-by: Moritz <moritz.m@local-it.org>
Reviewed-on: coop-cloud/abra#264
2023-01-31 14:35:43 +00:00
ca1ea32c46 Expose all env vars to app container. (!263)
Some checks failed
continuous-integration/drone/push Build is failing
Resolves coop-cloud/organising#393 and is required for the auto updater coop-cloud/organising#236 (comment)

Co-authored-by: Moritz <moritz.m@local-it.org>
Reviewed-on: coop-cloud/abra#263
2023-01-31 14:13:43 +00:00
32851d4d99 fix: always fetch all repository tags
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-01-31 11:52:15 +01:00
c47aa49373 fix: improved missing context message
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-24 10:48:53 +01:00
cdee6b00c4 docs: better auto-completion help
Some checks failed
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#328
2023-01-23 19:01:00 +01:00
a3e9383a4a docs: wording [ci skip] 2023-01-23 18:48:51 +01:00
b4cce7dcf4 fix: better warning if flying < 3.8 compose spec
Some checks failed
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#350
2023-01-23 18:42:23 +01:00
b089109c94 fix: more robust docker context problem handling
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
See coop-cloud/organising#325
See coop-cloud/organising#340
2023-01-23 14:56:34 +01:00
27e0708ac7 fix: don't delete server dir on cleanup if not empty
Part of coop-cloud/organising#325.
2023-01-23 13:56:27 +01:00
a93786c6be fix!: make "app rm" more explicit & simpler
We point users to "app volume/secret remove" for more specific deletion
of other app data resources now. The idea is that if you lose the env
file locally, then you can't clean up anything after. So it is handy to
have a sort of WARNING barrier to deleting that file. This flow is the
only way to get Abra to delete your local env file. It now feels more
documented and sufficiently scary in the UI/UX to merit that. Hopefully
addresses the ticket sufficiently.

Closes coop-cloud/organising#335.
2023-01-23 13:29:46 +01:00
521570224b Merge branch 'filter-servers-by-recipe'
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-23 09:33:31 +01:00
c72462e0b6 fix: no domain checks if no DOMAIN=... configured
Closes coop-cloud/organising#353
2023-01-23 09:33:12 +01:00
54646650c7 fix!: disable traefik linting when DOMAIN isn't present
Some checks failed
continuous-integration/drone/push Build is failing
Also reformats the linting output to be more readable.

Closes coop-cloud/organising#319.
2023-01-23 08:31:00 +00:00
903aac9d7a feat: recipe fetch command
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build was killed
Also may have rooted out another go-git cloning bug 🙄

Closes coop-cloud/organising#365
2023-01-23 09:26:53 +01:00
49865c6a97 feat: app services command
Some checks reported errors
continuous-integration/drone/push Build was killed
Closes coop-cloud/organising#372
2023-01-23 08:25:17 +00:00
a694c8c20e feat: filter server by recipe
Closes coop-cloud/organising#363
2023-01-23 00:54:22 +01:00
d4a42d8378 fix: error out if no backup configs found
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-01-22 18:50:45 +01:00
e16ca45fa7 fix!: better backup file names
Closes coop-cloud/organising#366
2023-01-22 18:50:27 +01:00
32de2ee5de fix: ensure catalogue is clean/up-to-date
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#367
2023-01-22 17:52:36 +01:00
834d41ef50 docs: wording [ci skip] 2023-01-22 10:07:58 +01:00
6fe5aed408 fix!: remove digest handling
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#379
2023-01-22 08:54:13 +01:00
03041b88d0 chore: gofmt
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-21 23:26:23 +01:00
9338afb492 chore: go mod tidy
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-20 10:16:14 +01:00
313ae0dbe2 chore(deps): update module github.com/docker/cli to v20.10.23
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-20 09:12:52 +00:00
0dc7ec8570 chore(deps): update module github.com/docker/docker to v20.10.23
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-01-20 08:02:52 +00:00
8a1a3aeb12 ci: automerge & run tidy [ci skip] 2023-01-18 17:28:36 +01:00
3wc
910469cfa0 chore: switch to dev image by default
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-15 19:48:07 -08:00
3wc
4f055096e9 chore: fix Drone build, ignore auto-recipes-catalogue-json
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-15 18:16:53 -08:00
3wc
6c93f980dc chore: tweak docker build
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-01-15 18:08:22 -08:00
3wc
57f52bbf33 chore: disable go cache for now, parallelise build
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-15 17:16:32 -08:00
3wc
9f5620d881 chore: attempt to fix drone build
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-01-15 17:11:50 -08:00
3wc
44c4555aae chore: attempt to enable go caching for docker image build
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-01-15 17:10:57 -08:00
3wc
025d1e0a8c chore: tweak drone image building 2023-01-15 17:10:52 -08:00
3wc
f484021148 feat: add docker image, auto-built using CI 2023-01-15 17:10:45 -08:00
3wc
1403eac72c fix: parse "Status" field during catalogue generate 2023-01-15 17:10:45 -08:00
a6e23938eb Add tests to jsontable.
- Test major functionality of jsontable
- Fix bug discovered in testing.
2023-01-15 17:10:36 -08:00
cae0d9ef79 Introduce a JSON output table mechanic
- Create JSONTable as a proxy/extension to tablewriter which can also output JSON.
- Implement machine readable output for `server list` and `recipe list`
2023-01-12 21:15:14 +00:00
89fcb5b216 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-06 10:05:20 +01:00
56b3e9bb19 chore(deps): update module github.com/go-git/go-git/v5 to v5.5.2
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-01-06 08:02:14 +00:00
9aa4a98b0b chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-05 17:45:33 +01:00
5fbba0c934 Merge remote-tracking branch 'origin/renovate/main-golang.org-x-sys-0.x' 2023-01-05 17:44:53 +01:00
d772f4b2c6 Merge remote-tracking branch 'origin/renovate/main-golang.org-x-crypto-0.x' 2023-01-05 17:44:31 +01:00
7513fbd57d chore(deps): update module golang.org/x/sys to v0.4.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-01-05 08:02:28 +00:00
9082761f86 chore(deps): update module golang.org/x/crypto to v0.5.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-01-05 08:02:09 +00:00
a3bd6e14d0 chore(deps): update module github.com/schollz/progressbar/v3 to v3.13.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-01-05 08:01:57 +00:00
49dd702d98 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-04 09:36:35 +01:00
e4cd5e3efe Merge remote-tracking branch 'origin/renovate/main-github.com-hetznercloud-hcloud-go-1.x' 2023-01-04 09:36:13 +01:00
1db4602020 chore(deps): update module github.com/hashicorp/go-retryablehttp to v0.7.2
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-01-04 08:01:58 +00:00
b50718050b chore(deps): update module github.com/hetznercloud/hcloud-go to v1.39.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-12-30 08:01:53 +00:00
3wc
9e39e1dc88 docs: fix typo in error message
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-22 19:27:42 -08:00
1a3a53cfc2 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-19 09:06:28 +01:00
5f53d591f8 chore(deps): update module github.com/docker/docker to v20.10.22 2022-12-19 09:06:27 +01:00
d7013518cc chore(deps): update module github.com/docker/cli to v20.10.22
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-12-19 08:01:53 +00:00
3wc
b204b289d1 fix: disable progress bar with machine-readable output
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-12-16 10:20:51 -08:00
3a0d9f7ed7 chore: 0.6.0-beta release
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2022-12-13 16:09:36 +01:00
e794c17fb4 chore: authors add & sort [ci skip] 2022-12-13 16:07:05 +01:00
e788ac21f6 fix: keep abra working if recipe catalogue is offline (!235)
All checks were successful
continuous-integration/drone/push Build is passing
Co-authored-by: Moritz <moritz.m@local-it.org>
Reviewed-on: coop-cloud/abra#235
2022-12-13 14:42:45 +00:00
4e78b060e0 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-12 10:50:38 +01:00
4fada9c1b7 Merge remote-tracking branch 'origin/renovate/main-github.com-hetznercloud-hcloud-go-1.x' 2022-12-12 10:50:18 +01:00
08d26e1a39 Merge remote-tracking branch 'origin/renovate/main-github.com-schollz-progressbar-v3-3.x' 2022-12-12 10:49:42 +01:00
581b28a2b1 Merge remote-tracking branch 'origin/renovate/main-golang.org-x-crypto-0.x' 2022-12-12 10:49:32 +01:00
e4d58849ce chore(deps): update module github.com/go-git/go-git/v5 to v5.5.1
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-12-12 08:01:31 +00:00
5e8b9d9bf7 chore(deps): update module golang.org/x/crypto to v0.4.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-12-08 08:01:33 +00:00
11dd665794 chore(deps): update module github.com/schollz/progressbar/v3 to v3.12.2
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-12-07 08:01:51 +00:00
ba163e9bf3 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.38.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-12-06 08:01:41 +00:00
09048ee223 Done did make format
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-05 18:03:13 +00:00
19a055b59b Add myself to the AUTHORS.md 2022-12-05 18:03:13 +00:00
1b28a07e17 Minor stylistic improvements to MR output in list. 2022-12-05 18:03:13 +00:00
82866cd213 Partial implementation of machine readable output.
- Implement global flag for machine readable output.
- Add machine readable output (as JSON) to list command.
2022-12-05 18:03:13 +00:00
47f3d2638b chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-05 09:30:46 +01:00
a3b894320a Merge remote-tracking branch 'origin/renovate/main-golang.org-x-sys-0.x' 2022-12-05 09:29:33 +01:00
9424a58c52 chore(deps): update module golang.org/x/sys to v0.3.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-12-05 08:01:48 +00:00
1751ba534e chore(deps): update module github.com/go-git/go-git/v5 to v5.5.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-12-05 08:01:25 +00:00
3wc
a21d431541 fix: don't panic() 😅
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-24 17:33:59 +00:00
3wc
8fad34e430 fix: switch back to replacing <recipe>.example.com
Fixes #355
2022-11-24 17:33:59 +00:00
a036de3c26 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-17 13:23:19 +01:00
4c2109e8ce chore(deps): update module golang.org/x/crypto to v0.3.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-11-17 08:01:01 +00:00
7f745ff19f feat(cmd)!: run abra.sh commands with /bin/bash if available.
All checks were successful
continuous-integration/drone/push Build is passing
BREAKING CHANGE: abra.sh commands that depend on /bin/sh will break

Closes coop-cloud/organising#357.

See coop-cloud/abra#229.
2022-11-15 23:01:57 +01:00
521d3d1259 feat(autocomplete): add autocompletion for fish shell
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-15 22:24:34 +01:00
14187449a5 fix: fork passgen
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/organising#358
2022-11-14 15:18:54 +01:00
2037f4cc19 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-11 17:40:42 +01:00
05d492d30b chore(deps): update module github.com/hetznercloud/hcloud-go to v1.37.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-11-11 08:01:11 +00:00
9591e91ed6 feat(cmd): make env variables accessible for local abra.sh commands
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-10 11:12:35 +00:00
f6f587e506 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-10 11:47:16 +01:00
4f28dbee87 chore(deps): update module golang.org/x/crypto to v0.2.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-11-10 08:01:07 +00:00
ad1cc038e3 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.36.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-11-09 08:01:00 +00:00
15dbd85d25 chore(deps): update module golang.org/x/sys to v0.2.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-11-08 08:00:59 +00:00
2a97955586 chore(deps): update module github.com/schollz/progressbar/v3 to v3.12.1
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-11-07 08:00:59 +00:00
9e44d1dfba chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-04 14:52:56 +01:00
87ad8e2761 chore(deps): update module github.com/schollz/progressbar/v3 to v3.12.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-11-03 08:01:03 +00:00
cfe703b15d chore(deps): update module github.com/docker/cli to v20.10.21
Some checks failed
continuous-integration/drone/push Build is failing
2022-10-27 08:44:38 +00:00
96503fa9e9 chore(deps): update module github.com/docker/docker to v20.10.21
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-10-26 07:01:18 +00:00
07d49d8566 chore go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-22 14:19:10 +02:00
5a7c25375a Merge remote-tracking branch 'origin/renovate/main-gotest.tools-v3-3.x' 2022-10-22 14:18:35 +02:00
652143e76c Merge remote-tracking branch 'origin/renovate/main-golang.org-x-sys-0.x' 2022-10-22 14:18:25 +02:00
8afce6eebf Merge remote-tracking branch 'origin/renovate/main-golang.org-x-crypto-0.x' 2022-10-22 14:17:35 +02:00
d3e6c9dc94 Merge remote-tracking branch 'origin/renovate/main-github.com-docker-docker-20.x' 2022-10-22 14:17:26 +02:00
4fd0ca3dd1 chore(deps): update module golang.org/x/crypto to v0.1.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-10-20 07:00:57 +00:00
dc0b6c2c8c chore(deps): update module github.com/docker/docker to v20.10.20
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-10-19 07:00:57 +00:00
54f242baf7 chore(deps): update module github.com/docker/cli to v20.10.20
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-10-19 07:00:51 +00:00
07620c7d89 chore(deps): update module golang.org/x/sys to v0.1.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-10-18 07:01:15 +00:00
1cae4cce4e chore(deps): update module gotest.tools/v3 to v3.4.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-10-10 07:01:50 +00:00
9347ade82c chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2022-09-20 10:42:53 +02:00
3fa18a8050 Merge remote-tracking branch 'origin/renovate/main-github.com-schollz-progressbar-v3-3.x' 2022-09-20 10:42:25 +02:00
4ac67662a2 Merge remote-tracking branch 'origin/renovate/main-github.com-hetznercloud-hcloud-go-1.x' 2022-09-20 10:42:17 +02:00
d1be4077c5 Merge remote-tracking branch 'origin/renovate/main-github.com-gliderlabs-ssh-0.x' 2022-09-20 10:42:09 +02:00
5a88c34a7c Merge remote-tracking branch 'origin/renovate/main-github.com-docker-go-units-0.x' 2022-09-20 10:41:57 +02:00
2e452e3213 Merge remote-tracking branch 'origin/renovate/main-github.com-docker-docker-20.x' 2022-09-20 10:41:30 +02:00
9d16a8e10c Merge remote-tracking branch 'origin/renovate/main-github.com-docker-cli-20.x' 2022-09-20 10:41:22 +02:00
8755a6c3b4 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.35.3
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-09-20 07:01:14 +00:00
8cee8ae33a chore(deps): update module github.com/schollz/progressbar/v3 to v3.11.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-09-12 07:01:55 +00:00
15b138e026 chore(deps): update module github.com/docker/docker to v20.10.18
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-09-12 07:01:39 +00:00
4a8ed36dea chore(deps): update module github.com/docker/cli to v20.10.18
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-09-12 07:01:28 +00:00
7d0c3cc496 chore(deps): update module github.com/alecaivazis/survey/v2 to v2.3.6
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-09-12 07:01:18 +00:00
3cf479ffd5 chore(deps): update module github.com/docker/go-units to v0.5.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-09-01 07:01:24 +00:00
d402050a40 chore(deps): update module github.com/gliderlabs/ssh to v0.3.5
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-09-01 07:01:15 +00:00
664edce09d build: fix matching to ignore deps upgrade [ci skip] 2022-08-15 12:32:19 +02:00
e41caa891d fix: dont check ip on server when it is local
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#334.
2022-08-14 22:20:17 +02:00
42a6818ff4 fix: app cmd parsing, usage & tests
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Note: the integration tests don't work due to ValidateApp still
attempting to validate the host key for the test app which doesn't
exist. This will be fixed in a future commit.
2022-08-14 16:18:58 +02:00
8f709c05bf build: ignore merges, chores & sort 2022-08-12 01:11:25 +02:00
a4ebf7befc docs: add frando & fix intro [ci skip] 2022-08-11 17:50:19 +02:00
8458e61d17 fix: branch checking logic
See https://github.com/go-git/go-git/issues/518 for why this is needed.
2022-08-11 17:49:22 +02:00
b42d5bf113 fix: ignore until coop-cloud/organising#336 is fixed [ci skip]
See coop-cloud/organising#336
2022-08-04 12:39:04 +03:00
f684c6d6e4 fix: drop back to urfave@v1.22.5 for parsing fix
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/organising#336
2022-08-03 14:40:01 +03:00
6593baf9f4 chore(deps): update golang docker tag to v1.19
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-08-03 07:01:11 +00:00
50123f3810 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-02 11:25:13 +03:00
d132e87f14 Merge remote-tracking branch 'origin/renovate/main-github.com-schollz-progressbar-v3-3.x' 2022-08-02 11:24:03 +03:00
37a1c3fb85 chore(deps): update module github.com/schollz/progressbar/v3 to v3.9.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-08-01 07:01:38 +00:00
c8183aa6d1 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.35.2
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-08-01 07:01:21 +00:00
4711de29ae chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-21 15:03:17 +03:00
b719aaba41 Merge remote-tracking branch 'origin/renovate/main-github.com-sirupsen-logrus-1.x' 2022-07-21 15:02:25 +03:00
074c51b672 chore(deps): update module github.com/sirupsen/logrus to v1.9.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-07-20 07:01:14 +00:00
1aa6be704a chore(deps): update module github.com/schollz/progressbar/v3 to v3.8.7
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-07-20 07:01:06 +00:00
e8e3cb8598 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-14 11:53:22 +02:00
85fec6b107 Merge remote-tracking branch 'origin/renovate/main-gotest.tools-v3-3.x' 2022-07-14 11:51:41 +02:00
12dbb061a9 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.35.1
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-07-05 07:01:24 +00:00
351bd7d4ba chore(deps): update module gotest.tools/v3 to v3.3.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-06-20 07:01:21 +00:00
cdc7037c25 chore: go mod tidy [ci skip] 2022-06-15 13:56:43 +02:00
682237c98e chore(deps): update module github.com/alecaivazis/survey/v2 to v2.3.5
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-06-08 07:01:45 +00:00
08d97be43a chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2022-06-07 09:09:08 +02:00
786dfde27e Merge commit 'c153c5d' into main 2022-06-07 09:08:55 +02:00
6e012b910e chore(deps): update module github.com/docker/docker to v20.10.17
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-06-07 07:01:42 +00:00
c153c5da2e chore(deps): update module github.com/docker/cli to v20.10.17
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-06-07 07:01:28 +00:00
0540e42168 alpha -> beta
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-31 10:23:49 +02:00
4bc95a5b52 chore: go mod tidy [ci skip] 2022-05-16 16:22:21 +02:00
febc6e2874 Merge remote-tracking branch 'origin/renovate/main-github.com-docker-docker-20.x' into main 2022-05-16 16:22:12 +02:00
b2c990bf12 Merge remote-tracking branch 'origin/renovate/main-github.com-docker-cli-20.x' into main 2022-05-16 16:22:06 +02:00
3b8893502a docs: re-word on docstrings [ci skip] 2022-05-13 16:44:49 +02:00
e0a0378f73 chore(deps): update module github.com/docker/docker to v20.10.16
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-05-13 07:01:43 +00:00
0837045d44 chore(deps): update module github.com/docker/cli to v20.10.16
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-05-13 07:01:33 +00:00
cd8137a7d8 chore: go mod tidy [ci skip] 2022-05-10 16:15:08 +02:00
ece4537a2d Merge remote-tracking branch 'origin/renovate/main-github.com-gliderlabs-ssh-0.x' into main 2022-05-10 16:14:45 +02:00
16fe1b68c6 fix: thread app name & stack name correctly
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-10 12:10:36 +02:00
e37f235fd4 chore(deps): update module github.com/gliderlabs/ssh to v0.3.4
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-05-10 07:01:27 +00:00
0423ce7e84 fix: working link [ci skip] 2022-05-10 08:32:12 +02:00
d46ac22bd7 chore: go mod tidy [ci skip] 2022-05-09 14:09:14 +02:00
cef5cd8611 Merge remote-tracking branch 'origin/renovate/main-github.com-docker-docker-20.x' into main 2022-05-09 14:04:16 +02:00
8b38dac9ab chore(deps): update module github.com/docker/docker to v20.10.15
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-05-06 07:01:51 +00:00
89fc875088 chore(deps): update module github.com/docker/cli to v20.10.15
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-05-06 07:01:31 +00:00
026a9ba2d7 chore: go mod tidy [ci skip] 2022-05-05 15:13:20 +02:00
99f2b9c6dc chore(deps): update module github.com/urfave/cli to v1.22.9
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-05-05 07:01:30 +00:00
578e91eeec chore: publish next tag 0.5.0-alpha
Some checks reported errors
continuous-integration/drone/push Build was killed
2022-05-03 17:22:54 +02:00
49f79dbd45 fix!: new catalogue URL
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-03 17:08:52 +02:00
574d556bb9 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2022-04-30 18:28:42 +02:00
801aad64df Merge remote-tracking branch 'origin/renovate/main-gotest.tools-v3-3.x' into main 2022-04-30 18:28:22 +02:00
b0a0829712 Merge remote-tracking branch 'origin/renovate/main-github.com-urfave-cli-1.x' into main 2022-04-30 18:28:15 +02:00
6aae06c3ec chore(deps): update module github.com/urfave/cli to v1.22.8
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-04-29 07:01:30 +00:00
d0c6fa5b45 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.33.2
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-04-27 07:02:33 +00:00
c947354ee3 chore(deps): update module gotest.tools/v3 to v3.2.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-04-25 07:01:44 +00:00
9b7e5752fb chore: go mod tidy [ci skip] 2022-04-22 11:25:08 +02:00
9bc51629d4 chore(deps): update module github.com/urfave/cli to v1.22.7
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-04-22 07:01:22 +00:00
4ba15df9b7 chore: 0.4.1-alpha
Some checks reported errors
continuous-integration/drone/push Build was killed
2022-04-21 15:47:39 +02:00
5721b357a2 fix: per service logs
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build was killed
2022-04-21 15:40:23 +02:00
6140abbcac fix: sync to latest before commits come in
All checks were successful
continuous-integration/drone/push Build is passing
Follows from a4989e3834
2022-04-20 11:42:24 +00:00
996255188b Revert "fix: ensure we're on latest for recipe release dance"
This reverts commit 3c4bb6a55e.
2022-04-20 11:42:24 +00:00
11d78234b2 installer: add 32 bit arm support
All checks were successful
continuous-integration/drone/push Build is passing
2022-04-20 13:37:51 +02:00
c214937e4a installer: download on aarch64
All checks were successful
continuous-integration/drone/push Build is passing
2022-04-20 13:13:50 +02:00
3a3f41988b chore: publish 0.4.0-alpha
All checks were successful
continuous-integration/drone/push Build is passing
2022-04-19 14:36:56 +02:00
f6690a80bd build: upx release script [ci skip] 2022-04-19 14:34:06 +02:00
2337c4648b chore: remove unused command 2022-04-19 14:32:34 +02:00
a1190f1352 fix: show which service is getting backed up [ci skip] 2022-04-19 13:50:23 +02:00
e421922f5b fix: restore uses absolute paths & better docs
All checks were successful
continuous-integration/drone/push Build is passing
2022-04-19 13:21:12 +02:00
10d5705d1a docs: better backup docs 2022-04-19 13:20:48 +02:00
a4f1634b24 fix: backups get gzip, absolute paths, single archive file 2022-04-19 12:52:30 +02:00
cbd924060f fix: better local changes message
All checks were successful
continuous-integration/drone/push Build is passing
2022-04-19 10:29:05 +02:00
3c4bb6a55e fix: ensure we're on latest for recipe release dance
Closes coop-cloud/organising#313.
2022-04-19 10:28:49 +02:00
a0d7a76f9d fix: better error messages for release failures
See coop-cloud/organising#313
2022-04-19 10:20:35 +02:00
c71efb46ba feat: arm builds [ci skip]
See coop-cloud/organising#312
2022-04-19 10:06:14 +02:00
ce69967ec5 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2022-04-18 10:42:39 +02:00
1a04439b1f chore(deps): update module github.com/hashicorp/go-retryablehttp to v0.7.1
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-04-14 07:01:24 +00:00
979f417a63 chore: gpl this sucka [ci skip] 2022-04-05 12:18:34 +02:00
b27acb2f61 feat: backup/restore [ci skip]
All checks were successful
continuous-integration/drone/pr Build is passing
See coop-cloud/organising#30.
2022-04-03 18:24:09 +02:00
622ecc4885 docs: drop slash [ci skip] 2022-04-01 23:18:22 +02:00
ed5bbda811 docs: wording & emoji [ci skip] 2022-04-01 23:14:57 +02:00
7b627ea518 docs: nice gopher [ci skip] 2022-04-01 23:12:24 +02:00
1ac66da83f chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2022-04-01 10:21:16 +02:00
061de96b62 chore(deps): update module github.com/kevinburke/ssh_config to v1.2.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-04-01 07:01:23 +00:00
6998298d32 chore: publish next tag 0.4.0-alpha-rc8
Some checks reported errors
continuous-integration/drone/push Build was killed
2022-03-30 16:28:55 +02:00
323f4467c8 fix: filtering requires case-by-case handling
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build was killed
See https://github.com/moby/moby/issues/32985.
2022-03-30 16:25:38 +02:00
e8e41850b5 fix: pass args to local function invocations too
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-03-30 11:31:16 +02:00
0e23ec53d7 refactor!: simple validation only 2022-03-30 11:30:51 +02:00
b943a8b9b1 feat: allow choosing user on remote commands 2022-03-30 11:30:36 +02:00
acc665f054 chore: publish next tag 0.4.0-alpha-rc7
Some checks reported errors
continuous-integration/drone/push Build was killed
2022-03-27 21:33:30 +02:00
860f1d6376 feat: bring back scripts interface
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/organising#301.
2022-03-27 19:30:48 +00:00
2122f0e67c fix: avoid short command alias conflicts 2022-03-27 19:30:48 +00:00
6aa23a76a1 fix: more precise filtering
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#305.
2022-03-27 19:30:36 +00:00
338360096c feat: pass domain to new app envs
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/organising#304.
2022-03-27 21:06:48 +02:00
7a8c7cd50f ci: drop static check
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-27 13:51:40 +02:00
bafc8a8e34 chore: go mod tidy
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-26 15:23:27 +01:00
3d44d8c9fd Merge remote-tracking branch 'origin/renovate/main-github.com-docker-docker-20.x' into main 2022-03-26 15:22:31 +01:00
b8b4616498 Merge remote-tracking branch 'origin/renovate/main-github.com-docker-cli-20.x' into main 2022-03-26 15:22:18 +01:00
da97117929 chore(deps): update module github.com/docker/docker to v20.10.14
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-03-24 08:01:35 +00:00
978297c464 chore(deps): update module github.com/docker/cli to v20.10.14
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-03-24 08:01:27 +00:00
11da4808fc chore(deps): update module github.com/alecaivazis/survey/v2 to v2.3.4
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-03-24 08:01:21 +00:00
4023e6a066 fix: wait until app created to check for secrets
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-18 11:10:15 +01:00
f432bfdd23 fix: warn when no repo on git
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-18 10:13:24 +01:00
848e17578d chore(deps): update golang docker tag to v1.18
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build was killed
2022-03-16 08:01:41 +00:00
1615130929 fix: skip prompt for no passwords
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-15 10:54:05 +01:00
7f315315f0 fix: better prompts & matching for secret removal
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-13 10:59:19 +01:00
6a50981120 fix: match on generation of single secret 2022-03-13 10:50:35 +01:00
c67471e6ca fix: show which secret was generated 2022-03-13 10:45:08 +01:00
f0fc1027e5 feat: more info on volumes. skip driver info
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-12 17:11:05 +01:00
c66695d55e fix: return err not logrus + new lines 2022-03-12 17:02:04 +01:00
262009701e fix: guard against concurrent write errors 2022-03-12 16:59:45 +01:00
b31cb6b866 feat: prompt for secret generation
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#302.
2022-03-12 16:47:19 +01:00
f39e186b66 fix: match Force/NoInput where needed
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-12 16:15:20 +01:00
a8f35bdf2f fix: handle NoInput for volume removal 2022-03-12 16:09:05 +01:00
6e1e02ac28 chore: use same flag docs style 2022-03-12 16:08:44 +01:00
16fc5ee54b fix: can't force remove if it is already deployed 2022-03-12 16:08:26 +01:00
37a1fcc4af fix: delete all secrets if force/noinput 2022-03-12 16:01:42 +01:00
a9b522719f fix: use name not stack name for pass storage 2022-03-12 16:01:31 +01:00
ce70932a1c feat: single char short flag for volumes removal 2022-03-12 16:01:14 +01:00
d61e104536 fix: look at removal flag for pass logic 2022-03-12 15:48:43 +01:00
d5f30a3ae4 fix: use removal flag with correct help 2022-03-12 15:48:26 +01:00
2555096510 feat: short flags for run command 2022-03-12 15:42:29 +01:00
3797292b20 fix: no domain/converge check for deploy/upgrade/rollback 2022-03-12 15:36:43 +01:00
6333815b71 fix: remove unused flag 2022-03-12 15:32:23 +01:00
793a850fd5 refactor!: short flags for server add 2022-03-12 15:30:43 +01:00
42c1450384 refactor!: prefer short flags on release 2022-03-12 15:28:33 +01:00
a2377882f6 refacator!: use single char short flags 2022-03-12 15:27:19 +01:00
e78b395662 feat: new short flag for RC upgrading 2022-03-12 15:24:19 +01:00
cdec834ca9 reformat: remove extra line in CLI help 2022-03-12 10:20:37 +01:00
b4b0b464bd fix: only delete secrets from specific app
Some checks failed
continuous-integration/drone/push Build is failing
See coop-cloud/organising#300.
2022-03-12 09:39:30 +01:00
d8a1b0ccc1 doc: indicate storage location of secret in logs 2022-03-12 09:39:15 +01:00
3fbd381f55 fix: add pass remove flag & show name is optional 2022-03-12 09:17:24 +01:00
d3e127e5c8 fix: retain backwards compat with TYPE/RECIPE change
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-11 19:37:50 +01:00
e9cfb076c6 fix: strip length modifiers
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/organising#297.
2022-03-11 16:40:10 +01:00
8ccf856110 fix: lay out generated secrets with warning/clarification 2022-03-11 16:39:34 +01:00
d0945aa09d fix: handle NoInput for app removal 2022-03-11 16:39:20 +01:00
123619219e chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-11 09:17:37 +01:00
a27410952e Merge remote-tracking branch 'origin/renovate/main-github.com-docker-docker-20.x' into main 2022-03-11 09:17:15 +01:00
13e0392af6 chore(deps): update module github.com/docker/docker to v20.10.13
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-03-11 08:01:57 +00:00
99a6135f72 chore(deps): update module github.com/docker/cli to v20.10.13
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-03-11 08:01:45 +00:00
a6b52c1354 chore: go mod tidy [ci skip] 2022-03-09 12:28:26 +01:00
fa51459191 chore(deps): update module github.com/docker/distribution to v2.8.1
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-03-09 08:01:26 +00:00
c529988427 feat: output success for secret insert [ci skip] 2022-03-08 18:10:37 +01:00
231cc3c718 fix: use StackName to filter volumes
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-08 18:04:47 +01:00
3381b8936d fix: better error handling & proper context deletion for server rm
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-24 15:57:52 +01:00
823f869f1d fix: error out correctly from ValidateDomain 2022-02-24 15:57:40 +01:00
ecbeacf10f fix: prompt for container choice correctly on run [ci skip] 2022-02-22 11:47:36 +01:00
3f838038d5 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-22 10:52:14 +01:00
91b4e021d0 chore(deps): update module github.com/containers/image to v5
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-02-22 08:01:12 +00:00
598e87dca2 chore: skip new repositories
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-21 08:46:30 +00:00
001511876d chore: go mod tidy 2022-02-21 08:46:30 +00:00
b295958c17 fix: handle all container registries
See coop-cloud/organising#258

This fixes also how we read the digest of the image. I think it was
wrong before. Some registries restrict reading this info and we now just
default to "unknown" for that case.

This also appears to bring a wave of new dependencies due to the generic
handling logic of containers/... package. The abra binary is now 1mb
larger.

The catalogue generation is now slower unfortunately. But it is more
robust.

The generic logic looks in ~/.docker/config.json for log in details, so
you don't have to pass those in manually on the CLI anymore. We just
read those defaults. You can "docker login" to get credentials setup in
that file. Since most folks won't generate the catalogue, this seems
fine for now.
2022-02-21 08:46:30 +00:00
2fbdcfb958 refactor: try the meta for default branch too
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Sometimes the Branch(...) call gets confused with state in the
repository. Its more robust to use the default value we get from gitea.

See coop-cloud/organising#299.
2022-02-20 18:07:49 +01:00
09ac74d205 fix: check out default branch from tags
All checks were successful
continuous-integration/drone/push Build is passing
Also fix error handling to match function signatures.
2022-02-18 11:17:43 +01:00
5da4afa0ec fix: only ensure latest after cloning
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-18 09:55:07 +01:00
9d5e805748 chore: go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-16 13:53:09 +01:00
770ae5ed9b chore(deps): update module github.com/moby/sys/signal to v0.7.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-02-16 08:01:33 +00:00
e056d8dc44 fix: de-dupe dns resolver logging, more concise [ci skip] 2022-02-14 18:06:06 +01:00
c3442354e7 fix: skip dupe ipv4 check, done in EnsureDomainsResolveSameIPv4
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-14 17:44:15 +01:00
6b2a0011af fix: remove dupe logging on catalogue reading [ci skip] 2022-02-14 17:37:25 +01:00
46fca7cfa7 docs: less ambig wording [ci skip] 2022-02-14 17:35:42 +01:00
82d560a946 fix: prompt for input on app cp
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-14 17:10:53 +01:00
fc5107865b fix: typo
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-10 10:59:19 +01:00
53ed1fc545 chore: go mod tidy
Some checks failed
continuous-integration/drone/push Build is failing
2022-02-09 09:59:23 +01:00
cc9e3d4e60 chore(deps): update module github.com/docker/distribution to v2.8.0 2022-02-09 09:59:23 +01:00
0557284461 fix: use new repo name
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-09 08:58:51 +00:00
b5f23d3791 feat: show latest published version on sync
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-09 08:58:20 +00:00
2b2dcc01b4 fix: dont checkout latest if we dont have a copy
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-09 09:54:02 +01:00
0a208d049e chore: go mod tidy + patch upgrades
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-04 10:50:55 +01:00
141711ecd0 Merge remote-tracking branch 'origin/renovate/main-github.com-schollz-progressbar-v3-3.x' into main 2022-02-04 10:50:36 +01:00
cd46d71ce4 chore(deps): update module github.com/schollz/progressbar/v3 to v3.8.6
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-02-04 08:01:17 +00:00
6fa090352d chore(deps): update module github.com/buger/goterm to v1.0.4
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is running
continuous-integration/drone/push Build is failing
2022-02-04 08:01:11 +00:00
227c02cd09 refactor!: make common flags single char again
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-03 14:19:51 +01:00
bfeda40e34 fix: catch more ssh failure modes with help
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-03 13:43:11 +01:00
5237c7ed50 docs: focus more on straight ssh docs for server add 2022-02-03 13:42:49 +01:00
4e09f3b9a8 refactor: migrate authors to dedicated file [ci skip] 2022-02-02 21:00:00 +01:00
dfb32cbb68 fix: type -> recipe [ci skip] 2022-02-02 20:48:12 +01:00
bdd9b0a1aa fix: ensure recipes on latest for lint/generate
All checks were successful
continuous-integration/drone/push Build is passing
Follows b2d17a1829.
2022-01-29 14:06:25 +01:00
b2d17a1829 fix: ensure latest checked out for recipe upgrade
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-29 13:35:42 +01:00
c905376472 refactor!: use "config" instead of "compose" [ci skip] 2022-01-27 12:24:33 +01:00
d316de218c feat: include recipe in deploy & friends overview 2022-01-27 12:23:02 +01:00
123475bd36 chore: remove old files [ci skip] 2022-01-27 12:14:01 +01:00
58e98f490d refactor!: type -> recipes
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2022-01-27 12:06:32 +01:00
224b8865bf test: newlines for output when Y'ing & N'ing
Some checks failed
continuous-integration/drone/pr Build is running
continuous-integration/drone/push Build is failing
2022-01-27 12:05:22 +01:00
8fb9f42f13 test: add remaining scripts 2022-01-27 12:05:21 +01:00
dc5e2a5b24 test: fix pwd usage, PWD doesn't exist 2022-01-27 12:05:21 +01:00
40b4ef5ab2 test: disable debug, its too much noise 2022-01-27 12:05:21 +01:00
4a912ae3bc test: show how to run all tests 2022-01-27 12:05:21 +01:00
1150fcc595 test: remove manual test guide, using semi-automated now 2022-01-27 12:05:20 +01:00
45224d1349 test: use new flags + order for record/server 2022-01-27 12:05:20 +01:00
7a40e2d616 fix: remove duplicate flags on "server new" 2022-01-27 12:05:20 +01:00
2277e4ef72 refactor!: remove no-input flag where not needed 2022-01-27 12:05:19 +01:00
c0c3d9fe76 refactor!: make dry-run flag more convenient 2022-01-27 12:05:19 +01:00
2493921ade refactor!: de-duplicate record flags 2022-01-27 12:05:19 +01:00
22f9cf2be4 refactor: remove unused flag 2022-01-27 12:05:18 +01:00
a23124aede feat: auto strip domain names to avoid runtime limits
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-27 10:33:21 +00:00
e670844b56 refactor!: app name -> domain 2022-01-27 10:33:21 +00:00
bc1729c5ca trim docs, point to new docs [ci skip] 2022-01-27 10:30:28 +01:00
fa8611b115 fix: respect NoInput on "app cp" & use app to get StackName
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-25 11:39:38 +01:00
415df981ff test: long flags, drop docker, use run_tests for all tests
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-24 16:49:51 +01:00
57728e58e8 test: improve semi-manual testing
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-01-21 16:48:42 +01:00
c7062e0494 fix: initial subcmd completion
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Broken by migration to v1 API.
2022-01-20 11:42:04 +01:00
cff7534bf9 chore: publish 0.4.0-alpha-rc6
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-19 13:33:32 +01:00
13e582349c fix: correctly override with ~/.ssh/config if failing to connect
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-19 13:28:57 +01:00
b1b9612e01 fix: dont try to parse empty values on status lookup
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-19 12:38:41 +01:00
afeee1270e test: break up integration, rejig manual 2022-01-19 12:17:09 +01:00
cb210d0c81 docs: pass on flag/help strings
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-19 11:21:06 +01:00
9f2bb3f74f refactor!: remove auto dns, too magic, too broken 2022-01-19 11:20:51 +01:00
a33767f848 refactor!: drop auto traefik deploy, rarely works
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-19 11:08:43 +01:00
a1abe5c6be refactor!: drop backup/restore for now
This will be done with the bot from now on.
2022-01-19 11:06:54 +01:00
672b44f965 test: remove since we're not supporting that in abra now 2022-01-19 11:04:28 +01:00
6d9573ec7e test: more help for how to do this 2022-01-19 11:04:15 +01:00
53cd3b8b71 fix: drop duplicate flags 2022-01-19 10:58:09 +01:00
b9ec41647b fix: when upgrading, skip over bad tags, don't error out
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-19 10:40:55 +01:00
f4b563528f docs: point to new option for better assurance on tag listing 2022-01-19 10:40:37 +01:00
f9a2c1d58f refactor: put StripTagMeta into formatter package
Avoid circular import.
2022-01-19 10:40:14 +01:00
7a66a90ecb fix!: change dry-run alias to not conflict with debug 2022-01-18 17:13:28 +01:00
0e688f1407 refactor!: migrate to urfave/cli v1
All checks were successful
continuous-integration/drone/push Build is passing
Better flexible flags handling.
2022-01-18 14:38:20 +01:00
c6db9ee355 chore: publish 0.4.0-alpha-rc5
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-18 11:39:02 +01:00
7733637767 fix: ensure catalogue cloned for catalogue reliant commands
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-18 11:19:33 +01:00
88f9796aaf fix: let us know if not pushing changes without dry-run (recipe release)
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-18 10:55:07 +01:00
6cdba0f9de fix: commit changes if dry-run not present (recipe release) 2022-01-18 10:54:54 +01:00
199aa5f4e3 fix: read password length from env files
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-17 22:34:32 +01:00
9b26c24a5f docs: drop that, not happening 2022-01-17 22:27:25 +01:00
ca75654769 fix: read correct app file name for secret generation
Stack name is only an internal docker concept now.
2022-01-17 22:17:59 +01:00
fc2d83d203 fix: better error message for missing server 2022-01-17 22:04:11 +01:00
2f4f288a46 feat: -a/--all-tags for listing all tags on recipe upgrade 2022-01-17 21:59:31 +01:00
e98f00d354 chore: go mod tidy 2022-01-17 21:50:25 +01:00
b4c2773b87 chore(deps): update module gotest.tools/v3 to v3.1.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-01-17 08:01:18 +00:00
3aec5d1d7e fix: ignore new test repo
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-12 16:11:18 +01:00
e0fa1b6995 fix: let users know what was deleted
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-06 11:47:10 +01:00
b69ab0df65 fix: chaos mode fixed for upgrade/rollback
All checks were successful
continuous-integration/drone/push Build is passing
Follows 4b7ec6384c.
2022-01-06 10:32:24 +01:00
69a7d37fb7 chore: release 0.4.0-alpha-rc4
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-06 10:04:43 +01:00
87649cbbd0 docs: more manual test cases [ci skip] 2022-01-05 19:37:41 +01:00
4b7ec6384c fix: fix chaos mode for deployment
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-05 19:21:41 +01:00
b22b63c2ba fix: only output if volumes selected for removal
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-05 19:00:09 +01:00
d9f3a11265 fix: gracefully handle missing tag for syncing
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-05 18:04:46 +01:00
d7cf11b876 fix: further fixes for gracefully handling missing tag
All checks were successful
continuous-integration/drone/push Build is passing
Follows 1b37d2d5f5.
2022-01-05 17:58:15 +01:00
d7e1b2947a fix: skip failed image parse for upgrade and move on 2022-01-05 17:57:11 +01:00
1b37d2d5f5 fix: handle tags without images gracefully
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-05 17:32:58 +01:00
74dfb12fd6 refactor: centralise tag meta stripping 2022-01-05 17:32:33 +01:00
49ccf2d204 fix: also show skip for non semver tags
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-04 22:49:36 +01:00
76adc45431 docs: match typically log message style 2022-01-04 22:49:23 +01:00
e38a0078f3 chore: publish 0.4.0-alpha-rc3
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-04 15:34:10 +01:00
25b44dc54e refactor!: use lowercase option to match others
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-04 12:25:45 +01:00
0c2f6fb676 fix: app autocomplete for secret commands 2022-01-04 12:24:37 +01:00
10e4a8b97f fix: handle StackName/AppName correctly for new app creation
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-04 11:56:29 +01:00
eed2756784 fix: new app table colume matches usual order now 2022-01-04 11:56:17 +01:00
b61b8f0d2a fix: always check for deployed status when removing
All checks were successful
continuous-integration/drone/push Build is passing
You can't delete regardless of -f if an app is deployed, the runtime
will error out. Best just deal with this for all cases then on our side.
2022-01-04 11:38:07 +01:00
763e7b5bff fix: use StackName for querying via Docker 2022-01-04 11:37:45 +01:00
d5ab9aedbf docs: match other abort command outputs 2022-01-04 11:37:35 +01:00
2ebb00c9d4 docs: confirm prompt matches language of command 2022-01-04 11:37:04 +01:00
6d76b3646a fix: use spaces like the rest [ci skip] 2022-01-03 18:41:11 +01:00
636dc82258 chore: 0.4.x rc2
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-03 16:37:19 +01:00
66d5453248 docs: recommend more helper commands for deploy timeout 2022-01-03 16:33:28 +01:00
ba9abcb0d7 fix: increase converge timeout 2022-01-03 16:33:18 +01:00
a1cbf21f61 fix: handle "uknown" version on deployment
Fixes pre-deploy overview version listing.
2022-01-03 16:32:03 +01:00
bd1da39374 fix: show latest version when up-to-date 2022-01-03 16:31:30 +01:00
8b90519bc9 test: more manual test examples 2022-01-03 16:31:16 +01:00
65feda7f1d fix: dont lookup release notes if no version passed 2022-01-03 16:14:56 +01:00
64e223a810 fix: dont display non-existant release notes if no version 2022-01-03 16:14:44 +01:00
379e01d855 fix: use installer without progress bar [ci skip]
Doesn't look well when invoked from "bash -c '...'" when we run "abra
upgrade". The progress bar shoots down the page and you miss the intro
banner.
2022-01-02 20:39:11 +01:00
a421c0dca5 test: use new name [ci skip] 2022-01-02 20:18:37 +01:00
abf56f9054 chore: publish 0.4.0-alpha-rc1
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-02 20:05:53 +01:00
4dec3c4646 fix: show order as in other tables
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-02 16:25:18 +01:00
c900cebc30 fix: fix filtering by type for output
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-02 16:21:22 +01:00
30209de3e2 fix: correct url for commit [ci skip] 2022-01-02 16:01:03 +01:00
625747d048 fix: get right url
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-02 15:54:46 +01:00
a71b070921 feat: support skipping upgrades 2022-01-02 15:46:35 +01:00
33ff04c686 fix: dont list if no volumes
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-02 15:20:17 +01:00
c69a3c23c5 fix: show app arg 2022-01-02 15:19:40 +01:00
0b46909961 fix: dont output if no secrets 2022-01-02 15:19:30 +01:00
832e8e5a96 test: finish first draft of manual test plan 2022-01-02 15:19:12 +01:00
abf83aa641 test: finish first pass on core integration script 2022-01-02 15:04:49 +01:00
1df69aa259 refactor: more shuffling test infra around [ci skip] 2022-01-02 14:59:46 +01:00
7596a67ad5 refactor: refocus the script purpose 2022-01-02 14:05:02 +01:00
93c7612efc feat: allow to only destroy remote server 2022-01-02 01:52:49 +01:00
2c78ac22e0 fix: handle missing ssh keys (pass auth) 2022-01-02 01:52:33 +01:00
13661c72ce test: more example env vars 2022-01-02 01:52:09 +01:00
454092644a test: debug + catalogue/recipe commands [ci skip] 2022-01-01 22:04:04 +01:00
224c0c38db fix: setup git for e2e testing 2022-01-01 22:03:53 +01:00
560e0eab86 fix: ensure catalogue is present 2022-01-01 22:01:16 +01:00
b92fdbbd52 fix: use right arg
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-01 21:46:48 +01:00
0a550363b8 fix: correctly count recipes 2022-01-01 21:46:38 +01:00
3119220c21 fix: better error 2022-01-01 21:46:24 +01:00
49f565e5db test: start on integration script
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-01 21:36:00 +01:00
94522178b1 fix: handle noinput case 2022-01-01 21:34:58 +01:00
810bc27967 fix: dont assume ipv4 exists 2022-01-01 21:34:49 +01:00
35d95fb9fb docs: better example 2022-01-01 21:34:33 +01:00
d26fabe8ef fix: handle zone argument correctly 2022-01-01 21:34:21 +01:00
84bf3ffa50 fix: use right variable 2022-01-01 21:34:07 +01:00
575485ec7a refactor: more portable wget usage 2022-01-01 21:33:50 +01:00
0b17292219 fix: revert to existing tags for testing purposes [ci skip] 2022-01-01 20:52:17 +01:00
fffd8b2647 docs: add missing 'the' 2022-01-01 19:56:32 +01:00
c07128b308 refactor: drop integration tests [ci skip]
Will use script instead.
2022-01-01 19:56:24 +01:00
929ff88013 fix: handle missing versions
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-01 17:37:34 +01:00
0353427c71 fix: adapt to new unkown version marker
Follows 7a0d18ceb6.
2022-01-01 17:37:10 +01:00
7a0d18ceb6 fix: show unknown insteaf of empty for missing version
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-01 17:23:21 +01:00
8992050409 docs: dont metion git explicitly in user messages 2022-01-01 17:23:04 +01:00
abd094387f fix: use scale for restarting
The other approach wasn't working. Duplicating containers on restart.
You'd end up with 2 containers per restart...
2022-01-01 17:22:35 +01:00
a556ca625b fix: handle StackName / Name correctly 2022-01-01 17:22:19 +01:00
1b7836009f test: spec out check tests [ci skip] 2021-12-31 17:19:30 +01:00
eb3509ab3f refactor: drop uneccessary structs
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-31 17:12:09 +01:00
87851d26f7 chore: makefile default runs more common tasks 2021-12-31 17:11:54 +01:00
c4f344b50a refactor: move to manual dir [ci skip] 2021-12-31 16:56:18 +01:00
60e4dfd9cb refactor!: use lowercase like the rest style
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-31 16:53:58 +01:00
d957adb675 docs: update the release description
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-31 16:48:03 +01:00
5254af0fe4 fix: handle no changes edge case for recipe release
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-31 13:45:01 +01:00
ce96269be0 fix: more fixed for dry mode, this time tested :)
Follows 299276c383.
2021-12-31 13:37:03 +01:00
299276c383 fix: handle dry run output result correctly
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-31 13:17:50 +01:00
866cdd1f29 feat: service name in ps output
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-31 12:59:31 +01:00
95d385c420 fix: GetService & handling missing services 2021-12-31 12:49:31 +01:00
605e2553b8 docs: expand errors docs
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-31 12:10:11 +01:00
1245827dff fix: handle %s correctly
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-31 12:05:40 +01:00
9bdb07463c fix: handle filtered server list with sort
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-30 02:06:04 +01:00
be26f80f03 fix: maintain sorted output
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-30 01:07:21 +01:00
930ff68bb2 refactor: drop unused function
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-30 00:42:37 +01:00
62441acf03 refactor: use SmallSHA 2021-12-30 00:41:21 +01:00
7460668ef4 fix: explain for single repo case too
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 03:42:44 +01:00
047d0e6fbc fix: working url
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 03:42:02 +01:00
8785f66391 feat: link direct to tag 2021-12-28 03:40:18 +01:00
24882e95b4 fix: take version from sync when releasing 2021-12-28 03:40:02 +01:00
1fd0941239 refactor: improved version choice flow 2021-12-28 03:19:32 +01:00
26a11533b4 feat: link directly to new commit
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 02:37:35 +01:00
b4f48c3c59 feat: show release notes on upgrade
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 02:31:21 +01:00
43e68a99b0 refactor: reverse list function finally 2021-12-28 02:31:06 +01:00
bac6fb0fa8 docs: better wording 2021-12-28 02:01:50 +01:00
dc9c9715ce fix: remove duplication 2021-12-28 02:01:43 +01:00
1f91b3bb03 fix: add prompt before publishing
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 01:51:39 +01:00
a700aca23d fix: add autocomplete for app run
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 01:37:41 +01:00
5cacd09a04 refactor: remove old/non-urgen/resolved FIXMEs 2021-12-28 01:35:40 +01:00
6a98024a2b refactor: drop old/upstream TODOs 2021-12-28 01:31:50 +01:00
e85117be22 docs: capitalistion, style 2021-12-28 01:27:58 +01:00
fb24357d38 refactor: merge top-level into one file 2021-12-28 01:26:40 +01:00
f5d2d3adf6 refactor: formatter gets own package 2021-12-28 01:24:23 +01:00
07119b0575 refactor: less files, they werent used generally 2021-12-28 01:08:44 +01:00
d2a6e35986 refactor: rename to flags 2021-12-28 01:04:51 +01:00
0aa37fcee8 refactor!: simplifying publish logic
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-27 19:56:27 +01:00
eb1b6be4c5 fix: auto-config ssh urls and push to them
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-27 18:06:56 +01:00
b98397144a fix: wording 2021-12-27 18:06:46 +01:00
4c186678b8 fix: clone https url by default
Catalogue package had to be merged into the recipe package due to too
many circular import errors. Also, use https url for cloning, assume
folks don't have ssh setup by default (the whole reason for the
refactor).
2021-12-27 16:45:56 +01:00
b1d9d9d858 refactor: wording & short options
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-27 16:12:29 +01:00
a06043375d refactor: remove unused flag 2021-12-27 16:07:57 +01:00
3eef1e8587 feat: filter recipes list
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-27 11:00:04 +01:00
37e48f262b fix: better wording
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-27 04:17:30 +01:00
06cc5d1cc3 fix: only update when really needed
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-27 04:10:12 +01:00
c13f438580 refactor: remove old code 2021-12-27 04:03:53 +01:00
5cd4317580 fix: more performant ps'in 2021-12-27 04:00:37 +01:00
2ba1ec3df0 fix: x-platform loop output
See coop-cloud/organising#178.
2021-12-27 03:55:42 +01:00
34cdb9c9d8 fix: check for deployment when ps'in 2021-12-27 03:53:45 +01:00
9c281d8608 fix: flags for logging in
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-27 03:27:05 +01:00
321ba1e0ec fix: template without weird breakages 2021-12-27 03:14:48 +01:00
c5a74e9f6b fix: template env files too
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-26 04:38:34 +01:00
f8191ac248 refactor: go with domains as default 2021-12-26 04:24:12 +01:00
027c8a1420 fix: better recipe meta defaults
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-26 04:10:50 +01:00
cdc08ae95a fix: much hacking, maybe fixed catalogue generation
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-26 04:02:40 +01:00
3f35510507 fix: runtime caching for catalogue generation 2021-12-26 04:01:02 +01:00
9f70a69bbf feat: skip git syncing on catalogue generation 2021-12-26 03:46:26 +01:00
b0834925a3 fix: log in correctly
See coop-cloud/abra#139.
2021-12-26 03:44:29 +01:00
86d87253c5 fix: pass name correctly
Follows from 9cc2554846
2021-12-26 00:15:03 +01:00
17340a79da refactor: more local var 2021-12-26 00:14:48 +01:00
779c810521 refactor: less quotes, less verbose 2021-12-26 00:14:32 +01:00
9cc2554846 fix: don't run twice 2021-12-26 00:02:46 +01:00
9a1cf258a5 fix: check published version properly
Resulted in a refactor to a new lint package.
2021-12-26 00:00:19 +01:00
ba8138079f fix: use one function for up-to-date checks 2021-12-25 23:45:52 +01:00
8735a8f0ea feat: lint before deploy/upgrade/rollback
See coop-cloud/organising#254.
2021-12-25 23:35:45 +01:00
a84a5bc320 feat: more robust linting
See coop-cloud/organising#254.
2021-12-25 23:22:50 +01:00
ae0e7b8e4c fix: dont wrap for table output 2021-12-25 17:22:40 +01:00
c0caf14d74 fix: more meta for listing recipes 2021-12-25 17:17:41 +01:00
d66c558b5c fix: dont render if no versions 2021-12-25 17:12:41 +01:00
c8541e1b9d fix: show latest first 2021-12-25 17:12:34 +01:00
653b6c6d49 fix: autocomplete for recipe versions 2021-12-25 17:12:22 +01:00
e2c3bc35c3 fix: handle missing label 2021-12-25 17:02:47 +01:00
6937bfbb0d fix: if no remotes, skip on 2021-12-25 16:56:21 +01:00
decfe095fe feat: improved recipe creation 2021-12-25 16:56:20 +01:00
4283f130a2 refactor: apps -> recipes 2021-12-25 14:04:07 +01:00
3b5354b2a5 refactor: less quotes
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-25 02:03:09 +01:00
14400d4ed8 fix: sync recipes from remotes
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-24 16:06:29 +01:00
dddf84d92b fix: avoid default value for idf
We could default to ~/.ssh/id_rsa but if that doesn't exist, then we'll
just be confusing people in the logs. Best is to just rely on the
ssh-agent which overrides this anyway. We will document this.

See coop-cloud/organising#277
2021-12-24 15:39:44 +01:00
fefb042716 fix: shorter timeout on deploy
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-24 02:26:02 +01:00
ab8db8df64 feat: deploy --no-converge-checks & finish app errors 2021-12-24 02:23:46 +01:00
20f7a18caa fix: add missing env file 2021-12-24 02:23:03 +01:00
58a24a50e1 WIP: app errors 2021-12-24 01:40:39 +01:00
e839f100df fix: move that back, still wrong but less wrong 2021-12-24 01:32:42 +01:00
41a757b7ed fix: only show when success is for sure 2021-12-24 00:44:50 +01:00
4b4298caf1 fix: better wording 2021-12-24 00:44:49 +01:00
8e8c241fdf refactor: less quotes 2021-12-24 00:44:49 +01:00
9b8ff1ddcd fix: get branch is now more robust 2021-12-24 00:44:44 +01:00
a85cfe40d0 WIP: app errors 2021-12-24 00:25:53 +01:00
fc29ca6fce refactor: less quotes 2021-12-24 00:25:45 +01:00
cfb02f45ed test: add test files 2021-12-24 00:25:33 +01:00
696172ad48 WIP: half-baked errors implementation
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-23 21:45:59 +01:00
4089949a3f fix: add state 2021-12-23 21:14:15 +01:00
a75b01e78a fix: use app name instead
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-23 19:34:50 +01:00
014d32112e fix: ensure tags & commits are pushed
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-23 02:24:43 +01:00
a7894cbda9 fix: better explanation 2021-12-23 02:10:57 +01:00
e03761f251 fix: include image too
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-23 01:56:09 +01:00
190c1033e6 fix: handle skipping
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-23 01:46:57 +01:00
15d1e9dee0 refactor: less quotes 2021-12-23 01:41:29 +01:00
0362928840 fix!: parse ttl correctly 2021-12-23 01:41:12 +01:00
844961d016 chore: add kawaiipunk
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/abra#145.
2021-12-23 01:16:36 +01:00
d0cc51b829 fix: point to correct var 2021-12-23 01:16:07 +01:00
606b5ac3e4 fix: less long ttl 2021-12-23 01:16:07 +01:00
6f1bf258b3 Fixed typo in abra ac bash output
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-23 00:15:28 +00:00
7a5aa1b005 test: make them work again
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-23 01:06:56 +01:00
db453f0ab1 feat: auto flag for dns
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 20:46:50 +01:00
a07e71f7df fix: grand ssh, provisioning, perms refactor
Some checks failed
continuous-integration/drone/push Build is failing
See coop-cloud/organising#280.
See coop-cloud/organising#273.
2021-12-22 20:08:15 +01:00
4c6d52c426 fix: clean up if things go wrong 2021-12-22 14:01:49 +01:00
327c5adef2 refactor: less quotes 2021-12-22 13:55:22 +01:00
0dc8425a27 fix: use wget, error out on missing deps
See coop-cloud/organising#280.
2021-12-22 13:54:13 +01:00
48c965bb21 refactor: less quotes
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 02:50:16 +01:00
5513754c22 fix: push tags
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 02:01:48 +01:00
3a27d9d9fb fix: remove unexpanded var
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 01:50:17 +01:00
04b58230ea fix: release functionality working again
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 01:36:41 +01:00
1b9097f9f3 fix: show where we're going 2021-12-22 01:36:29 +01:00
3d100093dc refactor: readability 2021-12-22 01:36:17 +01:00
ef4383209e fix: handle more appropriately
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 01:18:16 +01:00
74f688350b fix: actually call function
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 01:03:36 +01:00
737a22aacc refactor: less quotes
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 01:02:43 +01:00
56a1e7f8c4 feat: stderr only for logs 2021-12-22 01:02:36 +01:00
6be2f36334 WIP app errors place holder
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 00:48:00 +01:00
a18d0e290d docs: more context on vol rm
Some checks failed
continuous-integration/drone/push Build is failing
See coop-cloud/organising#265.
2021-12-22 00:12:12 +01:00
7e0feec311 fix: add autocomplete for vol ls 2021-12-22 00:08:26 +01:00
29a4d05944 fix: more info on multiselect
See coop-cloud/organising#265.
2021-12-22 00:07:49 +01:00
b72bad955a feat: no domain checks flag
See coop-cloud/organising#281.
2021-12-21 23:57:20 +01:00
e9b4541c91 fix: better explanation 2021-12-21 23:50:28 +01:00
5b1b16d64a refactor: less quotes 2021-12-21 23:48:46 +01:00
ec7223146b docs: better timeout error 2021-12-21 23:48:32 +01:00
fa45264ea0 refactor: the grand recipe release refactor 2021-12-21 19:25:44 +01:00
f57222d6aa docs: improve once again, maybe clearer 2021-12-21 17:52:20 +01:00
28d10928a4 chore: go mod tidy 2021-12-21 17:50:45 +01:00
0f4da38f98 Merge remote-tracking branch 'origin/renovate/main-github.com-schollz-progressbar-v3-3.x' into main 2021-12-21 17:50:31 +01:00
11c2d1efe6 chore(deps): update module github.com/schollz/progressbar/v3 to v3.8.5
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-12-21 08:01:41 +00:00
2b1cc9f6dd docs: less quotes, more clarity on init 2021-12-21 02:28:14 +01:00
6100a636a6 fix: respect NoInput and avoid crashing on init 2021-12-21 02:27:25 +01:00
ddbf923338 fix: catch this case correctly 2021-12-21 02:27:06 +01:00
c1a00520dc fix: stop if no tags in place 2021-12-21 02:08:51 +01:00
0dc4b2beef refactor: less quotes, spacing for style 2021-12-21 02:04:56 +01:00
f75284364d docs: better wording 2021-12-21 02:04:40 +01:00
fbc3b48d39 fix: autocomplete recipes 2021-12-21 02:04:31 +01:00
6f0d8b190d fix: better spacing 2021-12-21 02:04:19 +01:00
fc3742212c fix: more reliable syncing 2021-12-21 01:48:37 +01:00
fccbd7c7d7 chore: style lines 2021-12-21 01:48:21 +01:00
2457b5fe95 fix: return corrent error handling 2021-12-21 01:47:50 +01:00
72df640d99 fix: avoid that repo as well 2021-12-21 01:47:38 +01:00
ae9e66c319 docs: less quotes, different quotes 2021-12-20 01:05:51 +01:00
3589a7d56e docs: explain tags 2021-12-20 00:59:48 +01:00
8d499c0810 fix: find local only apps 2021-12-20 00:50:09 +01:00
cb2bb3f532 docs: uppercase 2021-12-20 00:49:54 +01:00
0a903f041f refactor: less quotes 2021-12-20 00:49:36 +01:00
053a06ccba refactor: less quotes 2021-12-20 00:15:55 +01:00
398deec272 docs: improved recipe maintainer docs 2021-12-20 00:15:42 +01:00
bf82bc9c7f feat: add dryflag, implement push for catalogue generate 2021-12-19 23:59:40 +01:00
217d4bc2cc docs: rewording 2021-12-19 23:59:20 +01:00
9c8e6b63a6 refactor: match logging for dry run 2021-12-19 23:51:04 +01:00
5113db1612 refactor: centralise git commit machinery 2021-12-19 23:51:03 +01:00
66666e30b7 fix: take care of -n here 2021-12-19 23:36:03 +01:00
88d4984248 docs: wording 2021-12-19 23:29:05 +01:00
bc34be4357 chore: go mod tidy 2021-12-19 23:25:17 +01:00
3d1aa55587 Merge commit 'd999ced' into main 2021-12-19 23:24:40 +01:00
e7469acf5b Merge commit 'b603069' into main 2021-12-19 23:24:29 +01:00
a293179e89 refactor: use config var for path 2021-12-19 23:24:10 +01:00
b912e73c5e fix: get bar length right 2021-12-19 23:23:46 +01:00
4c66e44b3a fix: use new recipes.json path 2021-12-19 23:17:46 +01:00
033bad3d10 fix: handle empty image meta 2021-12-19 23:14:43 +01:00
a750344653 refactor: better wording 2021-12-19 23:14:29 +01:00
f5caf5587a refactor: fix log style and add recipe context 2021-12-19 23:08:03 +01:00
fdc9e8b5fd refactor: improved log messages and less quotes 2021-12-19 23:02:58 +01:00
75edcabb23 fix: show progress on meta reading 2021-12-19 22:57:38 +01:00
fa0a63c11d refactor: ensure type, drop comment 2021-12-19 22:45:08 +01:00
3d3eefb2fe fix: bail out definitely on that error
See coop-cloud/organising#278.
2021-12-19 22:44:19 +01:00
6998a87eef docs: more help for setting up 2021-12-19 16:33:24 +01:00
b71a379788 docs: be a little less intense 2021-12-19 16:33:15 +01:00
ba217dccbd chore: point to new 0.4 release (coming soon) 2021-12-19 16:30:38 +01:00
45259b3266 refactor: drop comment 2021-12-19 16:29:28 +01:00
59b80d5def refactor: make this flag more general 2021-12-19 16:26:45 +01:00
8f6e1de1a1 refactor: merge catalogue/catalogue, catalogue/generate 2021-12-19 16:26:27 +01:00
cd0d3b8892 chore: remove old test file 2021-12-19 16:20:42 +01:00
0d1f65daac docs: add missing docstring 2021-12-19 16:19:42 +01:00
cf1b46fa61 refactor: move flags into internal/common 2021-12-19 16:18:50 +01:00
0fe0ffbafa refactor: move flags to internal/common 2021-12-19 16:15:45 +01:00
af3def7267 chore: spacing for style 2021-12-19 16:08:28 +01:00
c7de9c0719 docs: add description 2021-12-19 16:07:41 +01:00
cf5ee4e682 refactor: put URLs into vars 2021-12-19 16:06:07 +01:00
9ddf69b988 refactor: move flag to internal/common 2021-12-19 16:01:20 +01:00
a925da8dee docs: marker for author ack 2021-12-19 15:58:33 +01:00
06f8078866 refactor: move flag to internal/common 2021-12-19 15:57:12 +01:00
467947edf2 docs: show how to test 2021-12-19 15:57:11 +01:00
512cd9d85b refactor: new line to follow other docs 2021-12-19 15:57:08 +01:00
b8e2d1de67 refactor: move function into web package 2021-12-19 15:57:00 +01:00
3b7a8e6498 docs: add missing docstrings 2021-12-19 15:56:59 +01:00
5bae262a79 refactor: drop this, it's working solid, less verbose 2021-12-19 15:56:52 +01:00
6ad253b866 docs: point to autocomplete 2021-12-19 15:44:09 +01:00
b603069514 chore(deps): update module github.com/docker/docker to v20.10.12
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-12-14 08:01:21 +00:00
d999cedd97 chore(deps): update module github.com/docker/cli to v20.10.12
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-12-14 08:01:10 +00:00
8215bb455b fix: warn if secrets still exist
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-13 12:29:26 +01:00
37ab9a9c08 fix: improve ls output
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#252.
2021-12-12 17:51:58 +01:00
48dd9cdeed fix: simplify ps output
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-12 02:21:46 +01:00
d02e1f247f fix: better version output
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#253.
2021-12-12 02:16:01 +01:00
d087a60e09 Revert "fix: dont throw away changes"
All checks were successful
continuous-integration/drone/push Build is passing
This reverts commit dd0f328a65.

Part of coop-cloud/organising#282.
2021-12-12 02:04:13 +01:00
48e16c414c fix: use correct error format
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-12 01:56:43 +01:00
f3e55e5023 fix: support registry login details
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-12 01:52:28 +01:00
ae6adace50 refactor: autocomplete package
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-12 00:17:39 +01:00
32dcddb631 fix: select containers if we find multiple 2021-12-12 00:04:37 +01:00
3dbd343600 fix: dont double append root path
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-11 20:24:38 +01:00
8393f4b134 fix: log discovered paths 2021-12-11 20:24:29 +01:00
8e56607cc9 fix: use default 2021-12-11 20:13:55 +01:00
85a543afac fix: maybe more robust gitignore checks
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-11 20:11:59 +01:00
665396b679 fix: join path correctly
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-11 20:01:30 +01:00
870c561fee Revert "Revert "fix: include ignored files""
This reverts commit 9be78bc5fa.

Attempting to fix this once again.
2021-12-11 19:53:35 +01:00
3fb43ffa2c Revert "fix: match exact on filtering" [ci skip]
This reverts commit 2bc2f8630b.

This breaks other stuff. Reverting!
2021-12-09 14:12:16 +01:00
2bc2f8630b fix: match exact on filtering
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-06 01:26:04 +01:00
6094dfaf92 docs: help with dns
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#274.
2021-12-05 01:45:21 +01:00
3789e56404 fix: prompt for server deletion
Closes coop-cloud/organising#275.
2021-12-05 01:39:25 +01:00
2db5378418 fix: dont add .git dirs
Closes coop-cloud/organising#276.
2021-12-05 01:30:23 +01:00
7d8f3f1fab fix: less loose permissions, less +x
Closes coop-cloud/organising#283.
2021-12-05 01:18:31 +01:00
9be78bc5fa Revert "fix: include ignored files"
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
This reverts commit aea5cc69c3.
2021-12-03 11:39:56 +01:00
6c87d501e6 fix(installer): drop double echo
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-11-30 12:07:40 +01:00
930c29f4a2 fix: switch order of command
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-26 22:24:55 +01:00
1d6c3e98e4 fix: only query deployed app
Closes coop-cloud/organising#266.
2021-11-26 22:24:41 +01:00
a90f3b7463 fix: easier logs
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#270.
2021-11-26 22:14:29 +01:00
962f566228 fix: go on with missing tag
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#264.
2021-11-26 21:34:21 +01:00
9896c57399 chore: drop ' in messages [ci skip] 2021-11-26 21:34:10 +01:00
748d607ddc fix: better converge output
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#263.
2021-11-26 21:24:15 +01:00
3901258a96 fix: better message for existing swarm
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#259.
2021-11-26 21:07:49 +01:00
4347083f98 docs: better message [ci skip] 2021-11-26 21:04:58 +01:00
4641a942d8 chore: drop comment [ci skip] 2021-11-26 21:02:29 +01:00
3wc
759a00eeb3 fix: less fussy catalogue generation
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-24 13:48:17 +02:00
3wc
d1526fad21 fix: skip drone-abra and recipes in catalogue 2021-11-24 13:48:17 +02:00
6ef15e0a26 fix: remove fish from autocomplete
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-11-24 12:11:35 +01:00
dd0f328a65 fix: dont throw away changes
All checks were successful
continuous-integration/drone/push Build is passing
Part of coop-cloud/organising#226.
2021-11-22 21:11:59 +01:00
aea5cc69c3 fix: include ignored files
Part of coop-cloud/organising#226.
2021-11-22 21:11:59 +01:00
3wc
b02475eca5 Merge branch 'catalogue-metadata'
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-22 20:41:34 +02:00
3wc
d0a30f6b7b refactor: code style / error handling improvements
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-22 20:37:12 +02:00
3wc
8635922b9f fix: don't clobber recipe changes during generate
Closes #255
2021-11-22 20:37:12 +02:00
3wc
9d62fff074 feat: recipe generate: load category and features 2021-11-22 20:37:12 +02:00
711c4e5ee8 fix: warn on invalid envs for catalogue generation
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#256.
2021-11-22 18:38:59 +01:00
cb32e88cde fix: support retryable http clients
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#257.
2021-11-22 18:28:18 +01:00
a18729bf98 fix: ensure changes are check for
All checks were successful
continuous-integration/drone/push Build is passing
Part of coop-cloud/organising#255.
2021-11-22 17:49:31 +01:00
dbf84b7640 fix: validate this recipe
Part of coop-cloud/organising#255.
2021-11-22 17:49:14 +01:00
3wc
75db249053 fix: don't include traefik-cert-dumper in catalogue
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-22 16:15:51 +02:00
fdf4fc6737 fix: ensure validation takes place
All checks were successful
continuous-integration/drone/push Build is passing
Part of coop-cloud/organising#243 (comment).
2021-11-21 15:00:04 +01:00
ef6a9abba9 fix: ensure clean slate for re-deploy
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-21 14:42:38 +01:00
ce57d5ed54 fix: merge messages 2021-11-21 14:42:22 +01:00
3b01b1bb2e docs: explain docker context also
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-21 14:11:27 +01:00
fbdb792795 fix: add app name to ps output + docs
All checks were successful
continuous-integration/drone/push Build is passing
Part of coop-cloud/organising#252.
2021-11-21 14:07:19 +01:00
900f40f07a fix: add app name to list output
Part of coop-cloud/organising#252.
2021-11-21 13:43:21 +01:00
ecd2a63f0a fix: counts apps + drop versions meta without -S 2021-11-21 13:40:23 +01:00
304b70639f fix: only check catalogue once
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-19 15:50:29 +01:00
d821975aa2 fix: dont check servers so many times 2021-11-19 15:50:17 +01:00
1b836dbab6 fix: better borked ssh config message
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/organising#243.
2021-11-19 15:29:54 +01:00
fc51cf7775 docs: improve wording [ci skip] 2021-11-19 15:29:54 +01:00
a7ebcd8950 chore: bump for new RC
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-18 21:18:40 +01:00
e589709cb0 fix: attempt to include IdentityFile if available
All checks were successful
continuous-integration/drone/push Build is passing
This is part of trying to debug:

    coop-cloud/organising#250

And also part of:

    coop-cloud/docs.coopcloud.tech#27

Where I now try to specify the same logic as `ssh -i <my-key-path>` in
the underlying connection logic. This should help with being more
explicit about what key is being used via the SSH config file.
2021-11-18 21:16:10 +01:00
56c3e070f5 fix: log what keys are loaded with the ssh-agent
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#249.
2021-11-18 20:04:57 +01:00
cc37615d83 refactor: move debug to internal 2021-11-18 20:04:40 +01:00
0b37f63248 chore(deps): go mod tidy
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-18 09:49:25 +01:00
9c3a06a7d9 chore(deps): update module github.com/docker/docker to v20.10.11 2021-11-18 09:49:25 +01:00
cdef8b5ea5 chore(deps): update module github.com/docker/cli to v20.10.11 2021-11-18 09:49:25 +01:00
cba261b18c chore(deps): update module github.com/hetznercloud/hcloud-go to v1.33.1 2021-11-18 09:49:25 +01:00
1f6e4fa4a3 fix: ensure to init/commit the new recipe repo
All checks were successful
continuous-integration/drone/push Build is passing
Part of coop-cloud/organising#247.
2021-11-15 18:55:13 +01:00
4a245c3e02 fix: ensure .git repo exists
Part of coop-cloud/organising#247.
2021-11-15 18:55:13 +01:00
299faa1adf refactor: move file pulling/pushing logic to internal
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-11-15 16:48:23 +01:00
704e773a16 chore(deps): run go mod tidy
All checks were successful
continuous-integration/drone Build is passing
2021-11-15 09:20:04 +01:00
7143d09fd4 Merge remote-tracking branch 'origin/renovate/main-github.com-docker-cli-20.x' into main 2021-11-15 09:19:40 +01:00
4e76d49c80 Merge remote-tracking branch 'origin/renovate/main-github.com-docker-docker-20.x' into main 2021-11-15 09:19:30 +01:00
c9dff0c3bd Merge remote-tracking branch 'origin/renovate/main-github.com-gliderlabs-ssh-0.x' into main 2021-11-15 09:19:19 +01:00
e77e72a9e6 Merge remote-tracking branch 'origin/renovate/main-github.com-hetznercloud-hcloud-go-1.x' into main 2021-11-15 09:19:05 +01:00
af6f759c92 chore(deps): update module github.com/moby/sys/signal to v0.6.0 2021-11-15 08:16:57 +00:00
034295332c chore(deps): update module github.com/kevinburke/ssh_config to v1 2021-11-15 08:16:33 +00:00
dac2489e6d chore(deps): update module github.com/hetznercloud/hcloud-go to v1.33.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-11-15 08:01:39 +00:00
7bdc1946a2 chore(deps): update module github.com/gliderlabs/ssh to v0.3.3
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-11-15 08:01:30 +00:00
2439643895 chore(deps): update module github.com/docker/docker to v20.10.10
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2021-11-15 08:01:22 +00:00
0876f677d1 chore(deps): update module github.com/docker/cli to v20.10.10
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-11-15 08:01:17 +00:00
31dafb3ae4 chore(deps): update module github.com/alecaivazis/survey/v2 to v2.3.2
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-11-15 08:01:13 +00:00
915083b426 fix: time out on 60 sec + of converge checks
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/organising#246.
2021-11-14 23:15:35 +01:00
486a1717e7 fix: dont attempt to clone is local repo is there
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/organising#247.
2021-11-14 22:54:55 +01:00
9122c0a9b8 fix: ensure domain/server resolve to same ipv4
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/organising#227 (comment).
2021-11-14 22:47:18 +01:00
85ff04202f fix: ensure ipv4 is present for app deploys
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/organising#227.
2021-11-13 23:04:58 +01:00
ecba4e01f1 feat: autocomplete for app cp app names
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-13 22:50:45 +01:00
751b187df6 fix: check local path exists
See coop-cloud/organising#245.
2021-11-13 22:50:45 +01:00
f74261dbe6 docs: document app cp command syntax
See coop-cloud/organising#245.
2021-11-13 22:50:45 +01:00
2600a8137c chore(deps): add renovate.json
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-11-13 20:26:28 +00:00
b6a6163eff chore: skip new repo + sort [ci skip] 2021-11-13 20:55:50 +01:00
c25b2b17df feat: upgrade to rc from abra
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-11-13 17:34:20 +01:00
713308e0b8 docs: reinstate install docs on README [ci skip] 2021-11-12 08:57:30 +01:00
fcbf41ee95 chore: use alpha format
Some checks failed
continuous-integration/drone/push Build is failing
2021-11-12 08:25:38 +01:00
5add4ccc1b refactor(installer): remove doubled code for RC
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-11-11 17:40:14 +01:00
9220a8c09b feat(installer): download rc with --rc
All checks were successful
continuous-integration/drone/pr Build is passing
2021-11-11 17:10:48 +01:00
f78a04109c fix: clarify when deploy done [ci skip] 2021-11-10 09:15:52 +01:00
b67ad02f87 feat: rudimentary deploy status checking
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/organising#209.
2021-11-10 09:06:55 +01:00
215431696e feat: implement app restart
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#239.
2021-11-10 07:52:45 +01:00
cd361237e7 Revert "Revert "test: remove broken tests for client""
All checks were successful
continuous-integration/drone/push Build is passing
This reverts commit 59031595ea.

Argh, reverted this by accident, heres another one!
2021-11-09 18:25:28 +01:00
db10c7b849 feat: run wizard mode on recipe upgrade [ci skip] 2021-11-09 18:06:06 +01:00
d38f82ebe7 docs: drop recipe [ci skip] 2021-11-09 18:05:53 +01:00
59031595ea Revert "test: remove broken tests for client"
This reverts commit 17a5f1529a.
2021-11-09 17:58:31 +01:00
6f26b51f3e fix: only check host keys on requested hosts
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/organising#242.
2021-11-09 17:44:13 +01:00
17a5f1529a test: remove broken tests for client
Some checks reported errors
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build was killed
2021-11-09 13:03:33 +01:00
2ba6445daa test: go verbose on testing [ci skip] 2021-11-09 11:36:24 +01:00
edb427a7ae feat: implement host key checking
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#237.
2021-11-08 15:37:23 +01:00
3dc186e231 chore: make comment more general [ci skip] 2021-11-07 00:13:03 +01:00
1467ae5007 feat: teach catalogue generate to use git
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-07 00:03:01 +01:00
2b9395be1a feat: make sync use wizard mode
All checks were successful
continuous-integration/drone/push Build is passing
Some bugs squashed while testing this extensively.
2021-11-06 23:40:22 +01:00
a539033b55 docs: use consistent naming [ci skip] 2021-11-06 22:38:29 +01:00
63d9703d9d feat: make release use wizard mode
All checks were successful
continuous-integration/drone/push Build is passing
Some bugs squashed while testing this extensively.
2021-11-06 22:36:01 +01:00
f9726b6643 WIP: temporarily avoid SSH host key checking
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#234.
Closes coop-cloud/organising#142.
2021-11-05 12:33:32 +01:00
4a0761926c chore: avoid reverts in the change logi [ci skip] 2021-11-03 10:13:45 +01:00
de7054fd74 fix: use x-platform code for pdeathsig
Some checks reported errors
continuous-integration/drone/push Build was killed
This might cause the macosx build not to fail, I hope.

See https://github.com/docker/cli/tree/v20.10.10/cli/connhelper/commandconn
2021-11-03 09:57:35 +01:00
0e0e2db755 chore: publish new version
Some checks reported errors
continuous-integration/drone/push Build was killed
2021-11-03 09:44:11 +01:00
04e24022f5 feat: auto-deploy traefik prototype
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#212.
2021-11-03 09:41:20 +01:00
c227972c12 WIP: make "abra app deploy" callable by code
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#212.
2021-11-03 09:21:15 +01:00
911f22233f refactor: use better name for file 2021-11-03 09:11:30 +01:00
7d8e2d9dd1 WIP: make "abra app new" callable by code
All checks were successful
continuous-integration/drone/push Build is passing
Part of coop-cloud/organising#212.
2021-11-03 09:10:13 +01:00
f041083604 feat: support hetzner cloud server removal
All checks were successful
continuous-integration/drone/push Build is passing
Part of coop-cloud/organising#212.
2021-11-03 08:34:36 +01:00
f57ae1e904 fix: remove debug statements
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#217.
2021-11-03 07:56:26 +01:00
49a87cae2e fix: use more robust output cmd 2021-11-03 07:56:19 +01:00
f0de18a7f0 fix: use echo style + fix formatting
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-03 07:48:30 +01:00
1caef09cd2 feat: autocomplete helper command
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#216.
2021-11-03 07:28:18 +01:00
e4e606efb0 feat: catalogue generate now rate limits
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#231.
2021-11-03 06:53:38 +01:00
08aca28d9d chore: upgrade tagcmp + run mod tidy 2021-11-03 06:29:06 +01:00
f02ea7ca0d feat: add recipe version pinning
closes: coop-cloud/organising#186
2021-11-03 05:28:23 +00:00
3d3c4b3aae fix: add new repo to skip list
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-02 21:52:11 +01:00
e37b49201f fix: use IdleConnTimeout/ConnectTimeout
All checks were successful
continuous-integration/drone/push Build is passing
This is an attempt to set sensible timeouts on abra connections. This
might not be the last word on this but it seems that SSH connections now
bail out correctly and other kinds of commands don't explode (e.g.
logs).

Closes coop-cloud/organising#222.
Closes coop-cloud/organising#218.
2021-11-02 15:49:11 +01:00
ede5a59562 Revert c76601c9ce
This is already handled and does not need to be run again.
2021-11-02 15:47:09 +01:00
fc2deda1f6 Revert "fix: drop copy/pasta, keep timeouts"
This reverts commit a170e26e27.

Attempting to add more nuanced timeout logic.
2021-11-02 15:18:17 +01:00
c76601c9ce fix: ensure version for regular deploy
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-02 15:16:19 +01:00
7f176d8e2f fix: ensure logging for status checks
Closes coop-cloud/organising#226.
2021-11-02 15:15:52 +01:00
9b704b002b fix: include app arg in docs
All checks were successful
continuous-integration/drone/push Build is passing
Follow up to bd92c52eed.
2021-11-02 14:54:53 +01:00
ab02c5f0dd feat: support better domain defaults
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#221.
2021-11-02 14:44:16 +01:00
f2b02e39a7 fix: allow config to open broken env files
Some checks failed
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#223.
2021-11-02 14:38:53 +01:00
31f6bd06a5 fix: use correct formatting function
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-02 14:24:40 +01:00
bd92c52eed fix: document secret names more coherently
Some checks failed
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#215.
2021-11-02 14:21:55 +01:00
0486091768 fix: handle flags order validatio better
Some checks failed
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#214.
2021-11-02 14:08:54 +01:00
3b77607f36 fix: better error messages for missing repos
Some checks failed
continuous-integration/drone/push Build is failing
2021-11-02 13:36:40 +01:00
f833ccb864 fix: handle recipe name passing correctly
Some checks failed
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#224.
2021-11-02 13:33:46 +01:00
7022f42711 fix: docs and fix for new recipes
Some checks failed
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#228.
2021-11-02 13:29:58 +01:00
c76bd25c1d Revert "chore: tweak libdns/gandi go.sum entry >.<"
Some checks failed
continuous-integration/drone/push Build is failing
This reverts commit a6b5ac3410.

Mystery checksum ping/pong issue goes on.
2021-11-02 13:23:15 +01:00
3wc
a6b5ac3410 chore: tweak libdns/gandi go.sum entry >.<
Some checks failed
continuous-integration/drone/push Build is failing
2021-11-02 14:17:26 +02:00
71225d2099 feat(installer): add hashsum checking
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2021-10-26 12:29:53 +02:00
5d59d12d75 refactor(installer): use more precise sed command 2021-10-26 11:54:10 +02:00
d56400eea8 fix: bail out on unstage changes for plain --force
Some checks failed
continuous-integration/drone/push Build is failing
2021-10-26 10:52:26 +02:00
b3496ad286 fix: log correctly on provisioning
Some checks failed
continuous-integration/drone/push Build is failing
2021-10-26 01:30:23 +02:00
066b2b9373 fix: stream output from remote ssh commands 2021-10-26 01:30:10 +02:00
aec11bda28 fix: add ssh conn time outs 2021-10-26 00:33:18 +02:00
9a513a0700 fix: --local/--provision works 2021-10-26 00:27:45 +02:00
9f3ab0de9e refactor: drop VPS 2021-10-26 00:27:32 +02:00
e26afb97af fix: support empty ssh keys 2021-10-26 00:27:22 +02:00
960e47437c fix: show defaults, dont set 2021-10-26 00:25:14 +02:00
8e3f90a7f3 fix: server inputs handling + better logging 2021-10-25 23:48:49 +02:00
1d7cb0d9b6 fix: ensure client connections work 2021-10-25 23:48:19 +02:00
4d2a2d42fb fix: ensure provider is set
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-25 20:01:20 +02:00
bdae61ed51 docs: taking a pass on sub cmd docs 2021-10-25 19:58:50 +02:00
766e3008f6 fix: remove duplicate check [ci skip] 2021-10-25 19:51:55 +02:00
383f857f4a feat(installer): check if ~/local/.bin is in $PATH
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-10-25 18:14:10 +02:00
3d46ce6db2 refactor: more seamless SSH connections
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-25 11:13:41 +02:00
9e0d77d5c6 refactor: better SSH connection details handling
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-25 10:42:39 +02:00
f9e2d24550 docs: clarify when this can be connected to
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-25 10:09:55 +02:00
8772217f41 fix: working provisioning post chaos testing
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-25 10:06:16 +02:00
a7970132c2 fix: server/record improved output + interactivity
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-25 09:02:24 +02:00
2d091a6b00 refactor: name to match logic 2021-10-25 09:02:13 +02:00
147687d7ce fix: handle inputs for server new correctly 2021-10-25 08:23:29 +02:00
9a0e12258a feat: provision docker installation
Some checks failed
continuous-integration/drone/push Build is failing
2021-10-24 23:15:38 +02:00
1396f15c78 chore: new loc count by author
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-24 18:08:00 +02:00
2e2560dea7 docs: fix typos [ci skip] 2021-10-22 13:37:31 +02:00
c789a70653 docs: add additional op [ci skip] 2021-10-22 13:36:30 +02:00
8f55330210 docs: further server docs [ci skip] 2021-10-22 13:35:53 +02:00
d54a45bef7 docs: try to clarify that further [ci skip] 2021-10-22 13:31:14 +02:00
fdc0246f1d feat: server rm more functional
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-22 12:01:17 +02:00
a394618965 chore: those can break as well, include 2021-10-22 11:43:41 +02:00
8cd9f2700f refactor!: server add provisions/deploys traefik 2021-10-22 11:43:07 +02:00
b72fa28ddb feat: server list expands connection string 2021-10-22 10:41:19 +02:00
313e3beb1e refactor!: abra server interface more coherent
This follows our app new UX and interactive mode design.
2021-10-22 10:31:33 +02:00
94c7f59113 fix: dont use e.g. if already has default 2021-10-22 09:23:28 +02:00
5ae06bbd42 refactor!: abra domain -> abra record + prompts
This reconciles the fact that we manage records and not domains which
was a bad first naming take on this imho. Now it is clear that we are
manipulating domain name records and not entire zones.

The UX of record creation/deletion now mirrors the UX of new apps. All
the things are prompted for.
2021-10-22 08:58:18 +02:00
9f9248b987 feat: select prompt for recipes on app new 2021-10-22 08:21:46 +02:00
2bb4a9c063 docs: fix flag name [ci skip] 2021-10-21 20:58:01 +02:00
0c8dba0681 docs: try handles directly [ci skip] 2021-10-21 20:53:04 +02:00
a491332c1c feat: support no-input mode for deploy ops 2021-10-21 20:48:45 +02:00
6a75ffc051 docs: shape up release docs [ci skip] 2021-10-21 20:37:04 +02:00
5261d1a033 chore: drop unused dep [ci skip] 2021-10-21 20:17:48 +02:00
a458a5d9f7 docs: mark upstreams for all upstreams
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-21 19:54:43 +02:00
5ce2419354 docs: mark new pkg for upstream [ci skip] 2021-10-21 19:41:20 +02:00
963f8dcc73 fix: recover tests from overzealous cleanup
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-21 19:40:26 +02:00
dc04cf5ff7 chore: migrate all upstream code to own dir 2021-10-21 19:35:13 +02:00
80921c9f55 fix: remove cruft + readme pass + document forks
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-21 18:35:24 +02:00
8b15f2de5b chore: publish new release
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-10-21 16:03:19 +02:00
cdb76e7276 fix: catch multiple containers correctly
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-21 16:01:54 +02:00
a170e26e27 fix: drop copy/pasta, keep timeouts
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-10-21 15:42:50 +02:00
03b1882b81 chore: publish new tag
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2021-10-21 15:17:34 +02:00
2fcdaca75f fix: dont duplicate info output
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-21 15:13:24 +02:00
c5f44cf340 feat: show undploy overview
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-21 15:10:43 +02:00
7a5ad65178 fix: load timeout before other opts
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-21 15:06:03 +02:00
6d4ee3de0d fix: force flag works for upgrade
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-21 11:44:47 +02:00
63318fb6ff fix: handle chaos mode correctly for deploy
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#210.
2021-10-21 10:19:30 +02:00
07ffa08a07 chore: remove unused files
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-20 21:04:09 +02:00
0e5e7490b3 docs: some rewording and clarifying
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-20 17:52:54 +02:00
640032b8fe fix: remove duplicate version command
All checks were successful
continuous-integration/drone/push Build is passing
We can use --version/-v instead.
2021-10-20 17:48:50 +02:00
39babea963 docs: remove that missing feature [ci skip] 2021-10-20 17:36:41 +02:00
07613f5163 fix: devendor capsul code
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#155.
2021-10-20 17:34:01 +02:00
7f1d9eeaec fix: check if record already exists
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-20 16:56:34 +02:00
02d24104e1 feat: domain CRUD complete with Gandi provider
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-20 16:52:19 +02:00
da8d72620a test: warning not to test cli [ci skip] 2021-10-20 10:15:55 +01:00
96ccadc70f refactor: move making app struct to construct func
All checks were successful
continuous-integration/drone/push Build is passing
makes the code cleaner and easier to grab the app struct for testing
2021-10-20 09:45:38 +01:00
8703370785 WIP: domain create
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-20 00:05:57 +02:00
7d8c53299d docs: more domain command docs hacking 2021-10-20 00:05:49 +02:00
0110aceb1f docs: rewording 2021-10-19 23:03:12 +02:00
aec1e4520d fix: handle missing containers
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#198.
2021-10-19 22:50:43 +02:00
74bcb99c70 fix: use this weird default
Closes coop-cloud/organising#207.
2021-10-19 22:43:43 +02:00
dd4f2b48ec fix: explode when wrong provider chosen
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-19 10:19:31 +02:00
7f3f41ede4 docs: dns list docs
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-18 22:20:11 +02:00
597b4b586e WIP: domain listing with Gandi
All checks were successful
continuous-integration/drone/push Build is passing
Rethinking the interface already.
2021-10-18 22:16:29 +02:00
7ea3df45d4 WIP: dns support via libdns
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-18 20:35:43 +02:00
5941ed9728 fix: handle exceptions 2021-10-18 20:35:32 +02:00
d1e42752e2 fix: set connection timeouts + clean up bad contexts
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#205.
2021-10-18 10:48:43 +02:00
9dfbd21c61 fix: parse args correctly for validation
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-18 09:43:32 +02:00
9526d1fde6 fix: ensure we have version checked out on deploy
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-18 09:30:43 +02:00
62cc7ef92d feat: upgrade/downgrade support chaos mode
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-18 08:57:25 +02:00
c5a7a831d2 docs: chaos mode flag docs 2021-10-18 08:35:59 +02:00
4aae186f5f chore: squash formatting issue
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-18 08:27:39 +02:00
2f9b11f389 feat: support deploying with chaos mode
Some checks failed
continuous-integration/drone/push Build is failing
2021-10-18 08:14:06 +02:00
6d42e72f16 fix: allow for client creation on default context
Some checks failed
continuous-integration/drone/push Build is failing
See coop-cloud/organising#206.
2021-10-17 23:50:44 +02:00
5be190e110 fix: check that docker is installed on local add 2021-10-17 23:50:28 +02:00
c1390f232e fix: show "local" instead of "default" 2021-10-17 23:50:12 +02:00
3wc
95e19f03c4 fix: make release not crash on missing images
Some checks failed
continuous-integration/drone/push Build is failing
2021-10-16 18:57:21 +02:00
dc040a0b38 chore: change test context names
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-10-16 13:26:03 +02:00
e6e2e5214f test: add tests for pkg/client/client.go 2021-10-16 13:04:57 +02:00
61452b5f32 docs: add README.md to document testing 2021-10-16 12:26:43 +02:00
78460ac0ba test: increatse client/context.go coverage to 90% 2021-10-16 11:41:41 +02:00
0615c3f745 fix: support downgrade/upgrade for unknown versions
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-15 09:58:45 +02:00
3wc
e820e0219d docs: how to enable bash autocomplete from source
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-14 22:37:32 +02:00
205 changed files with 17362 additions and 4766 deletions

View File

@ -1,40 +0,0 @@
{{ range .Versions }}
<a name="{{ .Tag.Name }}"></a>
## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }}
> {{ datetime "2006-01-02" .Tag.Date }}
{{ range .CommitGroups -}}
### {{ .Title }}
{{ range .Commits -}}
* {{ .Subject }}
{{ end }}
{{ end -}}
{{- if .RevertCommits -}}
### Reverts
{{ range .RevertCommits -}}
* {{ .Revert.Header }}
{{ end }}
{{ end -}}
{{- if .MergeCommits -}}
### Pull Requests
{{ range .MergeCommits -}}
* {{ .Header }}
{{ end }}
{{ end -}}
{{- if .NoteGroups -}}
{{ range .NoteGroups -}}
### {{ .Title }}
{{ range .Notes }}
{{ .Body }}
{{ end }}
{{ end -}}
{{ end -}}
{{ end -}}

View File

@ -1,27 +0,0 @@
style: github
template: CHANGELOG.tpl.md
info:
title: CHANGELOG
repository_url: https://git.autonomic.zone:2222/coop-cloud/go-abra
options:
commits:
# filters:
# Type:
# - feat
# - fix
# - perf
# - refactor
commit_groups:
# title_maps:
# feat: Features
# fix: Bug Fixes
# perf: Performance Improvements
# refactor: Code Refactoring
header:
pattern: "^(\\w*)\\:\\s(.*)$"
pattern_maps:
- Type
- Subject
notes:
keywords:
- BREAKING CHANGE

8
.dockerignore Normal file
View File

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

View File

@ -3,45 +3,20 @@ kind: pipeline
name: coopcloud.tech/abra
steps:
- name: make check
image: golang:1.17
image: golang:1.21
commands:
- make check
- name: make static
image: golang:1.17
ignore: true # until we decide we all want this check
environment:
STATIC_CHECK_URL: honnef.co/go/tools/cmd/staticcheck
STATIC_CHECK_VERSION: v0.2.0
commands:
- go install $STATIC_CHECK_URL@$STATIC_CHECK_VERSION
- make static
- name: make build
image: golang:1.17
commands:
- make build
- name: make test
image: golang:1.17
image: golang:1.21
environment:
ABRA_DIR: "/root/.abra"
commands:
- make build-abra
- ./abra help # show version, initialise $ABRA_DIR
- make test
- name: notify on failure
image: plugins/matrix
settings:
homeserver: https://matrix.autonomic.zone
roomid: "IFazIpLtxiScqbHqoa:autonomic.zone"
userid: "@autono-bot:autonomic.zone"
accesstoken:
from_secret: autono_bot_access_token
depends_on:
- make check
- make build
- make test
when:
status:
- failure
- name: fetch
image: docker:git
@ -49,13 +24,12 @@ steps:
- git fetch --tags
depends_on:
- make check
- make build
- make test
when:
event: tag
- name: release
image: golang:1.17
image: goreleaser/goreleaser:v1.24.0
environment:
GITEA_TOKEN:
from_secret: goreleaser_gitea_token
@ -63,12 +37,29 @@ steps:
- name: deps
path: /go
commands:
- curl -sL https://git.io/goreleaser | bash
- goreleaser release
depends_on:
- fetch
when:
event: tag
- name: publish image
image: plugins/docker
settings:
auto_tag: true
username: 3wordchant
password:
from_secret: git_coopcloud_tech_token_3wc
repo: git.coopcloud.tech/coop-cloud/abra
tags: dev
registry: git.coopcloud.tech
when:
event:
exclude:
- pull_request
depends_on:
- make check
volumes:
- name: deps
temp: {}

View File

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

10
.gitignore vendored
View File

@ -1,5 +1,9 @@
abra
.vscode/
vendor/
*fmtcoverage.html
.e2e.env
.envrc
.vscode/
/kadabra
abra
dist/
tests/integration/.bats
vendor/

View File

@ -1,40 +1,70 @@
---
project_name: abra
gitea_urls:
api: https://git.coopcloud.tech/api/v1
download: https://git.coopcloud.tech/
skip_tls_verify: false
before:
hooks:
- go mod tidy
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
- id: abra
binary: abra
dir: cmd/abra
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- 386
- amd64
- arm
- arm64
goarm:
- 5
- 6
- 7
ldflags:
- "-X 'main.Commit={{ .Commit }}'"
- "-X 'main.Version={{ .Version }}'"
archives:
- replacements:
386: i386
amd64: x86_64
format: binary
- id: kadabra
binary: kadabra
dir: cmd/kadabra
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- 386
- amd64
- arm
- arm64
goarm:
- 5
- 6
- 7
ldflags:
- "-X 'main.Commit={{ .Commit }}'"
- "-X 'main.Version={{ .Version }}'"
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: desc
filters:
exclude:
- "^Merge"
- "^Revert"
- "^WIP:"
- "^chore:"
- "^docs:"
- "^refactor:"
- "^chore(deps):"
- "^style:"
- "^test:"
- "^tests:"

18
AUTHORS.md Normal file
View File

@ -0,0 +1,18 @@
# authors
> 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 💞
- 3wordchant
- cassowary
- codegod100
- decentral1se
- frando
- kawaiipunk
- knoflook
- moritz
- p4u1
- rix
- roxxers
- vera
- yksflip

24
Dockerfile Normal file
View File

@ -0,0 +1,24 @@
FROM golang:1.21-alpine AS build
ENV GOPRIVATE coopcloud.tech
RUN apk add --no-cache \
ca-certificates \
gcc \
git \
make \
musl-dev
RUN update-ca-certificates
COPY . /app
WORKDIR /app
RUN CGO_ENABLED=0 make build
FROM scratch
COPY --from=build /app/abra /abra
ENTRYPOINT ["/abra"]

15
LICENSE Normal file
View File

@ -0,0 +1,15 @@
Abra: The Co-op Cloud utility belt
Copyright (C) 2022 Co-op Cloud <helo@coopcloud.tech>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

View File

@ -1,38 +1,55 @@
ABRA := ./cmd/abra
COMMIT := $(shell git rev-list -1 HEAD)
GOPATH := $(shell go env GOPATH)
LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
ABRA := ./cmd/abra
KADABRA := ./cmd/kadabra
COMMIT := $(shell git rev-list -1 HEAD)
GOPATH := $(shell go env GOPATH)
GOVERSION := 1.21
LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
DIST_LDFLAGS := $(LDFLAGS)" -s -w"
export GOPRIVATE=coopcloud.tech
all: run test install build clean format check static
# NOTE(d1): default `make` optimised for Abra hacking
all: format check build-abra test
run:
run-abra:
@go run -ldflags=$(LDFLAGS) $(ABRA)
install:
run-kadabra:
@go run -ldflags=$(LDFLAGS) $(KADABRA)
install-abra:
@go install -ldflags=$(LDFLAGS) $(ABRA)
build-dev:
@go build -ldflags=$(LDFLAGS) $(ABRA)
install-kadabra:
@go install -ldflags=$(LDFLAGS) $(KADABRA)
build:
@go build -ldflags=$(DIST_LDFLAGS) $(ABRA)
build-abra:
@go build -v -ldflags=$(DIST_LDFLAGS) $(ABRA)
build-kadabra:
@go build -v -ldflags=$(DIST_LDFLAGS) $(KADABRA)
build: build-abra build-kadabra
build-docker-abra:
@docker run -it -v $(PWD):/abra golang:$(GOVERSION) \
bash -c 'cd /abra; ./scripts/docker/build.sh'
build-docker: build-docker-abra
clean:
@rm '$(GOPATH)/bin/abra'
@rm '$(GOPATH)/bin/kadabra'
format:
@gofmt -s -w .
check:
@test -z $$(gofmt -l .) || (echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1)
static:
@staticcheck $(ABRA)
@test -z $$(gofmt -l .) || \
(echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1)
test:
@go test ./... -cover
@go test ./... -cover -v
loc:
@find . -name "*.go" | xargs wc -l

101
README.md
View File

@ -1,104 +1,13 @@
# abra
> https://coopcloud.tech
# `abra`
[![Build Status](https://build.coopcloud.tech/api/badges/coop-cloud/abra/status.svg?ref=refs/heads/main)](https://build.coopcloud.tech/coop-cloud/abra)
[![Go Report Card](https://goreportcard.com/badge/git.coopcloud.tech/coop-cloud/abra)](https://goreportcard.com/report/git.coopcloud.tech/coop-cloud/abra)
[![Go Reference](https://pkg.go.dev/badge/coopcloud.tech/abra.svg)](https://pkg.go.dev/coopcloud.tech/abra)
The Co-op Cloud utility belt 🎩🐇
`abra` is a command-line tool for managing your own [Co-op Cloud](https://coopcloud.tech). It can provision new servers, create applications, deploy them, run backup and restore operations and a whole lot of other things. It is the go-to tool for day-to-day operations when managing a Co-op Cloud instance.
<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>
## Install
`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 💖
### Arch-based Linux Distros
[abra (coming-soon)](https://aur.archlinux.org/packages/abra/) or for the latest version on git [abra-git](https://aur.archlinux.org/packages/abra-git/)
```sh
yay -S abra-git # or abra
```
### Debian-based Linux Distros
**Coming Soon**
### Homebrew
**Coming Soon**
### Build from source
```sh
git clone https://git.coopcloud.tech/coop-cloud/abra
cd abra
go env -w GOPRIVATE=coopcloud.tech
make install
```
The abra binary will be in `$GOPATH/bin`.
## Autocompletion
**bash**
Copy `scripts/autocomplete/bash` into `/etc/bash_completion.d/` and rename
it to abra.
```
sudo cp scripts/autocomplete/bash /etc/bash_completion.d/abra
source /etc/bash_completion.d/abra
```
**(fi)zsh**
(fi)zsh doesn't have an autocompletion folder by default but you can create one, then copy `scripts/autocomplete/zsh` into it and add a couple lines to your `~/.zshrc` or `~/.fizsh/.fizshrc`
```
sudo mkdir /etc/zsh/completion.d/
sudo cp scripts/autocomplete/zsh /etc/zsh/completion.d/abra
echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc
```
(replace .zshrc with ~/.fizsh/.fizshrc if you use fizsh)
## Hacking
Install direnv, run `cp .envrc.sample .envrc`, then run `direnv allow` in this directory. This will set coopcloud repos as private due to [this bug.](https://git.coopcloud.tech/coop-cloud/coopcloud.tech/issues/20#issuecomment-8201). Or you can run `go env -w GOPRIVATE=coopcloud.tech` but I'm not sure how persistent this is.
Install [Go >= 1.16](https://golang.org/doc/install) and then:
- `make build` to build
- `./abra` to run commands
- `make test` will run tests
Our [Drone CI configuration](.drone.yml) runs a number of sanity on each pushed commit. See the [Makefile](./Makefile) for more handy targets.
Please use the [conventional commit format](https://www.conventionalcommits.org/en/v1.0.0/) for your commits so we can automate our change log.
## Versioning
We use [goreleaser](https://goreleaser.com) to help us automate releases. We use [semver](https://semver.org) for versioning all releases of the tool. While we are still in the public alpha release phase, we will maintain a `0.y.z-alpha` format. Change logs are generated from our commit logs. We are still working this out and aim to refine our release praxis as we go.
For developers, while using this `-alpha` format, the `y` part is the "major" version part. So, if you make breaking changes, you increment that and _not_ the `x` part. So, if you're on `0.1.0-alpha`, then you'd go to `0.1.1-alpha` for a backwards compatible change and `0.2.0-alpha` for a backwards incompatible change.
## Making a new release
- Change `ABRA_VERSION` to match the new tag in [`scripts`](./scripts/installer/installer) (use [semver](https://semver.org))
- Commit that change (e.g. `git commit -m 'chore: publish next tag 0.3.1-alpha'`)
- Make a new tag (e.g. `git tag 0.y.z-alpha`)
- Push the new tag (e.g. `git push && git push --tags`)
- Wait until the build finishes on [build.coopcloud.tech](https://build.coopcloud.tech/coop-cloud/abra)
- Deploy the new installer script (e.g. `cd ./scripts/installer && make`)
- Check the release worked, (e.g. `abra upgrade; abra version`)
## Fork maintenance
We maintain a fork of [godotenv](https://github.com/Autonomic-Cooperative/godotenv) for two features:
1. multi-line env var support
2. inline comment parsing
You can upgrade the version here by running `go get github.com/Autonomic-Cooperative/godotenv@<commit>` where `<commit>` is the
latest commit you want to pin to. We are aiming to migrate to YAML format for the environment configuration, so this should only
be a temporary thing.
Please see [docs.coopcloud.tech/abra](https://docs.coopcloud.tech/abra) for help on install, upgrade, hacking, troubleshooting & more!

View File

@ -1,38 +1,37 @@
package app
import (
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
// AppCommand defines the `abra app` command and ets subcommands
var AppCommand = &cli.Command{
Name: "app",
Usage: "Manage apps",
Aliases: []string{"a"},
ArgsUsage: "<app>",
Description: `
This command provides all the functionality you need to manage the life cycle
of your apps. From initial deployment, day-2 operations (e.g. backup/restore)
to scaling apps up and spinning them down.
`,
Subcommands: []*cli.Command{
appNewCommand,
appConfigCommand,
appDeployCommand,
appUpgradeCommand,
appUndeployCommand,
var AppCommand = cli.Command{
Name: "app",
Aliases: []string{"a"},
Usage: "Manage apps",
ArgsUsage: "<domain>",
Description: "Functionality for managing the life cycle of your apps",
Subcommands: []cli.Command{
appBackupCommand,
appRestoreCommand,
appRemoveCommand,
appCheckCommand,
appListCommand,
appPsCommand,
appLogsCommand,
appCmdCommand,
appConfigCommand,
appCpCommand,
appRunCommand,
appDeployCommand,
appErrorsCommand,
appListCommand,
appLogsCommand,
appNewCommand,
appPsCommand,
appRemoveCommand,
appRestartCommand,
appRestoreCommand,
appRollbackCommand,
appRunCommand,
appSecretCommand,
appVolumeCommand,
appServicesCommand,
appUndeployCommand,
appUpgradeCommand,
appVersionCommand,
appVolumeCommand,
},
}

View File

@ -1,87 +1,414 @@
package app
import (
"errors"
"archive/tar"
"context"
"fmt"
"io/ioutil"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"time"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/archive"
"github.com/klauspost/pgzip"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var backupAllServices bool
var backupAllServicesFlag = &cli.BoolFlag{
Name: "all",
Value: false,
Destination: &backupAllServices,
Aliases: []string{"a"},
Usage: "Backup all services",
type backupConfig struct {
preHookCmd string
postHookCmd string
backupPaths []string
}
var appBackupCommand = &cli.Command{
var appBackupCommand = cli.Command{
Name: "backup",
Usage: "Backup an app",
Aliases: []string{"b"},
Flags: []cli.Flag{backupAllServicesFlag},
ArgsUsage: "<service>",
Aliases: []string{"bk"},
Usage: "Run app backup",
ArgsUsage: "<domain> [<service>]",
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Description: `
Run an app backup.
A backup command and pre/post hook commands are defined in the recipe
configuration. Abra reads this configuration and run the comands in the context
of the deployed services. Pass <service> if you only want to back up a single
service. All backups are placed in the ~/.abra/backups directory.
A single backup file is produced for all backup paths specified for a service.
If we have the following backup configuration:
- "backupbot.backup.path=/var/lib/foo,/var/lib/bar"
And we run "abra app backup example.com app", Abra will produce a file that
looks like:
~/.abra/backups/example_com_app_609341138.tar.gz
This file is a compressed archive which contains all backup paths. To see paths, run:
tar -tf ~/.abra/backups/example_com_app_609341138.tar.gz
(Make sure to change the name of the backup file)
This single file can be used to restore your app. See "abra app restore" for more.
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if c.Args().Get(1) != "" && backupAllServices {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<service>' and '--all' together"))
}
abraSh := path.Join(config.ABRA_DIR, "apps", app.Type, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("'%s' does not exist?", abraSh)
}
logrus.Fatal(err)
}
sourceCmd := fmt.Sprintf("source %s", abraSh)
execCmd := "abra_backup"
if !backupAllServices {
serviceName := c.Args().Get(1)
if serviceName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no service(s) target provided"))
}
execCmd = fmt.Sprintf("abra_backup_%s", serviceName)
}
bytes, err := ioutil.ReadFile(abraSh)
recipe, err := recipePkg.Get(app.Recipe, internal.Offline)
if err != nil {
logrus.Fatal(err)
}
if !strings.Contains(string(bytes), execCmd) {
logrus.Fatalf("%s doesn't have a '%s' function", app.Type, execCmd)
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)
}
}
sourceAndExec := fmt.Sprintf("%s; %s", sourceCmd, execCmd)
cmd := exec.Command("bash", "-c", sourceAndExec)
if err := internal.RunCmd(cmd); err != nil {
backupConfigs := make(map[string]backupConfig)
for _, service := range recipe.Config.Services {
if backupsEnabled, ok := service.Deploy.Labels["backupbot.backup"]; ok {
if backupsEnabled == "true" {
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), service.Name)
bkConfig := backupConfig{}
logrus.Debugf("backup config detected for %s", fullServiceName)
if paths, ok := service.Deploy.Labels["backupbot.backup.path"]; ok {
logrus.Debugf("detected backup paths for %s: %s", fullServiceName, paths)
bkConfig.backupPaths = strings.Split(paths, ",")
}
if preHookCmd, ok := service.Deploy.Labels["backupbot.backup.pre-hook"]; ok {
logrus.Debugf("detected pre-hook command for %s: %s", fullServiceName, preHookCmd)
bkConfig.preHookCmd = preHookCmd
}
if postHookCmd, ok := service.Deploy.Labels["backupbot.backup.post-hook"]; ok {
logrus.Debugf("detected post-hook command for %s: %s", fullServiceName, postHookCmd)
bkConfig.postHookCmd = postHookCmd
}
backupConfigs[service.Name] = bkConfig
}
}
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
serviceName := c.Args().Get(1)
if serviceName != "" {
backupConfig, ok := backupConfigs[serviceName]
if !ok {
logrus.Fatalf("no backup config for %s? does %s exist?", serviceName, serviceName)
}
logrus.Infof("running backup for the %s service", serviceName)
if err := runBackup(cl, app, serviceName, backupConfig); err != nil {
logrus.Fatal(err)
}
} else {
if len(backupConfigs) == 0 {
logrus.Fatalf("no backup configs discovered for %s?", app.Name)
}
for serviceName, backupConfig := range backupConfigs {
logrus.Infof("running backup for the %s service", serviceName)
if err := runBackup(cl, app, serviceName, backupConfig); err != nil {
logrus.Fatal(err)
}
}
}
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}
// TimeStamp generates a file name friendly timestamp.
func TimeStamp() string {
ts := time.Now().UTC().Format(time.RFC3339)
return strings.Replace(ts, ":", "-", -1)
}
// runBackup does the actual backup logic.
func runBackup(cl *dockerClient.Client, app config.App, serviceName string, bkConfig backupConfig) error {
if len(bkConfig.backupPaths) == 0 {
return fmt.Errorf("backup paths are empty for %s?", serviceName)
}
// FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli()
if err != nil {
return err
}
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, true)
if err != nil {
return err
}
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
if bkConfig.preHookCmd != "" {
splitCmd := internal.SafeSplit(bkConfig.preHookCmd)
logrus.Debugf("split pre-hook command for %s into %s", fullServiceName, splitCmd)
preHookExecOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: splitCmd,
Detach: false,
Tty: true,
}
if err := container.RunExec(dcli, cl, targetContainer.ID, &preHookExecOpts); err != nil {
return fmt.Errorf("failed to run %s on %s: %s", bkConfig.preHookCmd, targetContainer.ID, err.Error())
}
logrus.Infof("succesfully ran %s pre-hook command: %s", fullServiceName, bkConfig.preHookCmd)
}
var tempBackupPaths []string
for _, remoteBackupPath := range bkConfig.backupPaths {
sanitisedPath := strings.ReplaceAll(remoteBackupPath, "/", "_")
localBackupPath := filepath.Join(config.BACKUP_DIR, fmt.Sprintf("%s%s_%s.tar.gz", fullServiceName, sanitisedPath, TimeStamp()))
logrus.Debugf("temporarily backing up %s:%s to %s", fullServiceName, remoteBackupPath, localBackupPath)
logrus.Infof("backing up %s:%s", fullServiceName, remoteBackupPath)
content, _, err := cl.CopyFromContainer(context.Background(), targetContainer.ID, remoteBackupPath)
if err != nil {
logrus.Debugf("failed to copy %s from container: %s", remoteBackupPath, err.Error())
if err := cleanupTempArchives(tempBackupPaths); err != nil {
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
}
return fmt.Errorf("failed to copy %s from container: %s", remoteBackupPath, err.Error())
}
defer content.Close()
_, srcBase := archive.SplitPathDirEntry(remoteBackupPath)
preArchive := archive.RebaseArchiveEntries(content, srcBase, remoteBackupPath)
if err := copyToFile(localBackupPath, preArchive); err != nil {
logrus.Debugf("failed to create tar archive (%s): %s", localBackupPath, err.Error())
if err := cleanupTempArchives(tempBackupPaths); err != nil {
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
}
return fmt.Errorf("failed to create tar archive (%s): %s", localBackupPath, err.Error())
}
tempBackupPaths = append(tempBackupPaths, localBackupPath)
}
logrus.Infof("compressing and merging archives...")
if err := mergeArchives(tempBackupPaths, fullServiceName); err != nil {
logrus.Debugf("failed to merge archive files: %s", err.Error())
if err := cleanupTempArchives(tempBackupPaths); err != nil {
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
}
return fmt.Errorf("failed to merge archive files: %s", err.Error())
}
if err := cleanupTempArchives(tempBackupPaths); err != nil {
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
}
if bkConfig.postHookCmd != "" {
splitCmd := internal.SafeSplit(bkConfig.postHookCmd)
logrus.Debugf("split post-hook command for %s into %s", fullServiceName, splitCmd)
postHookExecOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: splitCmd,
Detach: false,
Tty: true,
}
if err := container.RunExec(dcli, cl, targetContainer.ID, &postHookExecOpts); err != nil {
return err
}
logrus.Infof("succesfully ran %s post-hook command: %s", fullServiceName, bkConfig.postHookCmd)
}
return nil
}
func copyToFile(outfile string, r io.Reader) error {
tmpFile, err := os.CreateTemp(filepath.Dir(outfile), ".tar_temp")
if err != nil {
return err
}
tmpPath := tmpFile.Name()
_, err = io.Copy(tmpFile, r)
tmpFile.Close()
if err != nil {
os.Remove(tmpPath)
return err
}
if err = os.Rename(tmpPath, outfile); err != nil {
os.Remove(tmpPath)
return err
}
return nil
}
func cleanupTempArchives(tarPaths []string) error {
for _, tarPath := range tarPaths {
if err := os.RemoveAll(tarPath); err != nil {
return err
}
logrus.Debugf("remove temporary archive file %s", tarPath)
}
return nil
}
func mergeArchives(tarPaths []string, serviceName string) error {
var out io.Writer
var cout *pgzip.Writer
localBackupPath := filepath.Join(config.BACKUP_DIR, fmt.Sprintf("%s_%s.tar.gz", serviceName, TimeStamp()))
fout, err := os.Create(localBackupPath)
if err != nil {
return fmt.Errorf("Failed to open %s: %s", localBackupPath, err)
}
defer fout.Close()
out = fout
cout = pgzip.NewWriter(out)
out = cout
tw := tar.NewWriter(out)
for _, tarPath := range tarPaths {
if err := addTar(tw, tarPath); err != nil {
return fmt.Errorf("failed to merge %s: %v", tarPath, err)
}
}
if err := tw.Close(); err != nil {
return fmt.Errorf("failed to close tar writer %v", err)
}
if cout != nil {
if err := cout.Flush(); err != nil {
return fmt.Errorf("failed to flush: %s", err)
} else if err = cout.Close(); err != nil {
return fmt.Errorf("failed to close compressed writer: %s", err)
}
}
logrus.Infof("backed up %s to %s", serviceName, localBackupPath)
return nil
}
func addTar(tw *tar.Writer, pth string) (err error) {
var tr *tar.Reader
var rc io.ReadCloser
var hdr *tar.Header
if tr, rc, err = openTarFile(pth); err != nil {
return
}
for {
if hdr, err = tr.Next(); err != nil {
if err == io.EOF {
err = nil
}
break
}
if err = tw.WriteHeader(hdr); err != nil {
break
} else if _, err = io.Copy(tw, tr); err != nil {
break
}
}
if err == nil {
err = rc.Close()
} else {
rc.Close()
}
return
}
func openTarFile(pth string) (tr *tar.Reader, rc io.ReadCloser, err error) {
var fin *os.File
var n int
buff := make([]byte, 1024)
if fin, err = os.Open(pth); err != nil {
return
}
if n, err = fin.Read(buff); err != nil {
fin.Close()
return
} else if n == 0 {
fin.Close()
err = fmt.Errorf("%s is empty", pth)
return
}
if _, err = fin.Seek(0, 0); err != nil {
fin.Close()
return
}
rc = fin
tr = tar.NewReader(rc)
return tr, rc, nil
}

View File

@ -1,64 +1,81 @@
package app
import (
"fmt"
"os"
"path"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var appCheckCommand = &cli.Command{
Name: "check",
Usage: "Check if app is configured correctly",
Aliases: []string{"c"},
ArgsUsage: "<service>",
var appCheckCommand = cli.Command{
Name: "check",
Aliases: []string{"chk"},
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>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.ChaosFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
envSamplePath := path.Join(config.ABRA_DIR, "apps", app.Type, ".env.sample")
if _, err := os.Stat(envSamplePath); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("'%s' does not exist?", envSamplePath)
}
if err := recipe.EnsureExists(app.Recipe); err != nil {
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 {
logrus.Fatal(err)
}
var missing []string
for k := range envSample {
if _, ok := app.Env[k]; !ok {
missing = append(missing, k)
for _, envVar := range envVars {
if envVar.Present {
table.Append([]string{envVar.Name, "✅"})
} else {
table.Append([]string{envVar.Name, "❌"})
}
}
if len(missing) > 0 {
missingEnvVars := strings.Join(missing, ", ")
logrus.Fatalf("%s is missing %s", app.Path, missingEnvVars)
}
logrus.Infof("all necessary environment variables defined for '%s'", app.Name)
table.Render()
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}

273
cli/app/cmd.go Normal file
View File

@ -0,0 +1,273 @@
package app
import (
"errors"
"fmt"
"os"
"os/exec"
"path"
"sort"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var appCmdCommand = cli.Command{
Name: "command",
Aliases: []string{"cmd"},
Usage: "Run app commands",
Description: `Run an app specific command.
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
work station by passing "--local". Arguments can be passed into these functions
using the "-- <args>" syntax.
Example:
abra app cmd example.com app create_user -- me@example.com
`,
ArgsUsage: "<domain> [<service>] <command> [-- <args>]",
Flags: []cli.Flag{
internal.DebugFlag,
internal.LocalCmdFlag,
internal.RemoteUserFlag,
internal.TtyFlag,
internal.OfflineFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
Subcommands: []cli.Command{appCmdListCommand},
BashComplete: func(ctx *cli.Context) {
args := ctx.Args()
switch len(args) {
case 0:
autocomplete.AppNameComplete(ctx)
case 1:
autocomplete.ServiceNameComplete(args.Get(0))
case 2:
cmdNameComplete(args.Get(0))
}
},
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)
}
}
if internal.LocalCmd && internal.RemoteUser != "" {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use --local & --user together"))
}
hasCmdArgs, parsedCmdArgs := parseCmdArgs(c.Args(), internal.LocalCmd)
abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("%s does not exist for %s?", abraSh, app.Name)
}
logrus.Fatal(err)
}
if internal.LocalCmd {
if !(len(c.Args()) >= 2) {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments"))
}
cmdName := c.Args().Get(1)
if err := internal.EnsureCommand(abraSh, app.Recipe, cmdName); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("--local detected, running %s on local work station", cmdName)
var exportEnv string
for k, v := range app.Env {
exportEnv = exportEnv + fmt.Sprintf("%s='%s'; ", k, v)
}
var sourceAndExec string
if hasCmdArgs {
logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs)
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s %s", app.Name, app.StackName(), exportEnv, abraSh, cmdName, parsedCmdArgs)
} else {
logrus.Debug("did not detect any command arguments")
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s", app.Name, app.StackName(), exportEnv, abraSh, cmdName)
}
shell := "/bin/bash"
if _, err := os.Stat(shell); errors.Is(err, os.ErrNotExist) {
logrus.Debugf("%s does not exist locally, use /bin/sh as fallback", shell)
shell = "/bin/sh"
}
cmd := exec.Command(shell, "-c", sourceAndExec)
if err := internal.RunCmd(cmd); err != nil {
logrus.Fatal(err)
}
} else {
if !(len(c.Args()) >= 3) {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments"))
}
targetServiceName := c.Args().Get(1)
cmdName := c.Args().Get(2)
if err := internal.EnsureCommand(abraSh, app.Recipe, cmdName); err != nil {
logrus.Fatal(err)
}
serviceNames, err := config.GetAppServiceNames(app.Name)
if err != nil {
logrus.Fatal(err)
}
matchingServiceName := false
for _, serviceName := range serviceNames {
if serviceName == targetServiceName {
matchingServiceName = true
}
}
if !matchingServiceName {
logrus.Fatalf("no service %s for %s?", targetServiceName, app.Name)
}
logrus.Debugf("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName)
if hasCmdArgs {
logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs)
} else {
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 {
logrus.Fatal(err)
}
}
return nil
},
}
func parseCmdArgs(args []string, isLocal bool) (bool, string) {
var (
parsedCmdArgs string
hasCmdArgs bool
)
if isLocal {
if len(args) > 2 {
return true, fmt.Sprintf("%s ", strings.Join(args[2:], " "))
}
} else {
if len(args) > 3 {
return true, fmt.Sprintf("%s ", strings.Join(args[3:], " "))
}
}
return hasCmdArgs, parsedCmdArgs
}
func cmdNameComplete(appName string) {
app, err := app.Get(appName)
if err != nil {
return
}
cmdNames, _ := getShCmdNames(app)
if err != nil {
return
}
for _, n := range cmdNames {
fmt.Println(n)
}
}
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)
}
}
cmdNames, err := getShCmdNames(app)
if err != nil {
logrus.Fatal(err)
}
for _, cmdName := range cmdNames {
fmt.Println(cmdName)
}
return nil
},
}
func getShCmdNames(app config.App) ([]string, error) {
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
cmdNames, err := config.ReadAbraShCmdNames(abraShPath)
if err != nil {
return nil, err
}
sort.Strings(cmdNames)
return cmdNames, nil
}

31
cli/app/cmd_test.go Normal file
View File

@ -0,0 +1,31 @@
package app
import (
"strings"
"testing"
)
func TestParseCmdArgs(t *testing.T) {
tests := []struct {
input []string
shouldParse bool
expectedOutput string
}{
// `--` is not parsed when passed in from the command-line e.g. -- foo bar baz
// so we need to eumlate that as missing when testing if bash args are passed in
// see https://git.coopcloud.tech/coop-cloud/organising/issues/336 for more
{[]string{"foo.com", "app", "test"}, false, ""},
{[]string{"foo.com", "app", "test", "foo"}, true, "foo "},
{[]string{"foo.com", "app", "test", "foo", "bar", "baz"}, true, "foo bar baz "},
}
for _, test := range tests {
ok, parsed := parseCmdArgs(test.input, false)
if ok != test.shouldParse {
t.Fatalf("[%s] should not parse", strings.Join(test.input, " "))
}
if parsed != test.expectedOutput {
t.Fatalf("%s does not match %s", parsed, test.expectedOutput)
}
}
}

View File

@ -1,23 +1,44 @@
package app
import (
"fmt"
"errors"
"os"
"os/exec"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var appConfigCommand = &cli.Command{
Name: "config",
Aliases: []string{"c"},
Usage: "Edit app config",
var appConfigCommand = cli.Command{
Name: "config",
Aliases: []string{"cfg"},
Usage: "Edit app config",
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
appName := c.Args().First()
if appName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no app provided"))
}
files, err := config.LoadAppFiles("")
if err != nil {
logrus.Fatal(err)
}
appFile, exists := files[appName]
if !exists {
logrus.Fatalf("cannot find app with name %s", appName)
}
ed, ok := os.LookupEnv("EDITOR")
if !ok {
@ -30,7 +51,7 @@ var appConfigCommand = &cli.Command{
}
}
cmd := exec.Command(ed, app.Path)
cmd := exec.Command(ed, appFile.Path)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
@ -40,16 +61,4 @@ var appConfigCommand = &cli.Command{
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}

View File

@ -1,26 +1,52 @@
package app
import (
"context"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/archive"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var appCpCommand = &cli.Command{
var appCpCommand = cli.Command{
Name: "cp",
Aliases: []string{"c"},
ArgsUsage: "<src> <dst>",
Usage: "Copy files to/from a running app service",
ArgsUsage: "<domain> <src> <dst>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
Usage: "Copy files to/from a deployed app service",
Description: `
Copy files to and from any app service file system.
If you want to copy a myfile.txt to the root of the app service:
abra app cp <domain> myfile.txt app:/
And if you want to copy that file back to your current working directory locally:
abra app cp <domain> app:/myfile.txt .
`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
@ -28,48 +54,12 @@ var appCpCommand = &cli.Command{
dst := c.Args().Get(2)
if src == "" {
logrus.Fatal("missing <src> argument")
} else if dst == "" {
}
if dst == "" {
logrus.Fatal("missing <dest> argument")
}
parsedSrc := strings.SplitN(src, ":", 2)
parsedDst := strings.SplitN(dst, ":", 2)
errorMsg := "one of <src>/<dest> arguments must take $SERVICE:$PATH form"
if len(parsedSrc) == 2 && len(parsedDst) == 2 {
logrus.Fatal(errorMsg)
} else if len(parsedSrc) != 2 {
if len(parsedDst) != 2 {
logrus.Fatal(errorMsg)
}
} else if len(parsedDst) != 2 {
if len(parsedSrc) != 2 {
logrus.Fatal(errorMsg)
}
}
var service string
var srcPath string
var dstPath string
isToContainer := false // <container:src> <dst>
if len(parsedSrc) == 2 {
service = parsedSrc[0]
srcPath = parsedSrc[1]
dstPath = dst
logrus.Debugf("assuming transfer is coming FROM the container")
} else if len(parsedDst) == 2 {
service = parsedDst[0]
dstPath = parsedDst[1]
srcPath = src
isToContainer = true // <src> <container:dst>
logrus.Debugf("assuming transfer is going TO the container")
}
appFiles, err := config.LoadAppFiles("")
if err != nil {
logrus.Fatal(err)
}
appEnv, err := config.GetApp(appFiles, app.Name)
srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst)
if err != nil {
logrus.Fatal(err)
}
@ -79,46 +69,311 @@ var appCpCommand = &cli.Command{
logrus.Fatal(err)
}
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("%s_%s", appEnv.StackName(), service))
containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
container, err := containerPkg.GetContainerFromStackAndService(cl, app.StackName(), service)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server)
if toContainer {
err = copyToContainer(cl, container.ID, srcPath, dstPath)
} else {
err = copyFromContainer(cl, container.ID, srcPath, dstPath)
}
if err != nil {
logrus.Fatal(err)
}
if len(containers) != 1 {
logrus.Fatalf("expected 1 container but got %v", len(containers))
}
container := containers[0]
logrus.Debugf("retrieved '%s' as target container on '%s'", formatter.ShortenID(container.ID), app.Server)
if isToContainer {
if _, err := os.Stat(srcPath); err != nil {
logrus.Fatalf("'%s' does not exist?", srcPath)
}
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
content, err := archive.TarWithOptions(srcPath, toTarOpts)
if err != nil {
logrus.Fatal(err)
}
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(c.Context, container.ID, dstPath, content, copyOpts); err != nil {
logrus.Fatal(err)
}
} else {
content, _, err := cl.CopyFromContainer(c.Context, container.ID, srcPath)
if err != nil {
logrus.Fatal(err)
}
defer content.Close()
fromTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
if err := archive.Untar(content, dstPath, fromTarOpts); err != nil {
logrus.Fatal(err)
}
}
return nil
},
}
var errServiceMissing = errors.New("one of <src>/<dest> arguments must take $SERVICE:$PATH form")
// parseSrcAndDst parses src and dest string. One of src or dst must be of the form $SERVICE:$PATH
func parseSrcAndDst(src, dst string) (srcPath string, dstPath string, service string, toContainer bool, err error) {
parsedSrc := strings.SplitN(src, ":", 2)
parsedDst := strings.SplitN(dst, ":", 2)
if len(parsedSrc)+len(parsedDst) != 3 {
return "", "", "", false, errServiceMissing
}
if len(parsedSrc) == 2 {
return parsedSrc[1], dst, parsedSrc[0], false, nil
}
if len(parsedDst) == 2 {
return src, parsedDst[1], parsedDst[0], true, nil
}
return "", "", "", false, errServiceMissing
}
// copyToContainer copies a file or directory from the local file system to the container.
// See the possible copy modes and their documentation.
func copyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath string) error {
srcStat, err := os.Stat(srcPath)
if err != nil {
return fmt.Errorf("local %s ", err)
}
dstStat, err := cl.ContainerStatPath(context.Background(), containerID, dstPath)
dstExists := true
if err != nil {
if errdefs.IsNotFound(err) {
dstExists = false
} else {
return fmt.Errorf("remote path: %s", err)
}
}
mode, err := copyMode(srcPath, dstPath, srcStat.Mode(), dstStat.Mode, dstExists)
if err != nil {
return err
}
movePath := ""
switch mode {
case CopyModeDirToDir:
// Add the src directory to the destination path
_, srcDir := path.Split(srcPath)
dstPath = path.Join(dstPath, srcDir)
// Make sure the dst directory exits.
dcli, err := command.NewDockerCli()
if err != nil {
return err
}
if err := container.RunExec(dcli, cl, containerID, &types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: []string{"mkdir", "-p", dstPath},
Detach: false,
Tty: true,
}); err != nil {
return fmt.Errorf("create remote directory: %s", err)
}
case CopyModeFileToFile:
// Remove the file component from the path, since docker can only copy
// to a directory.
dstPath, _ = path.Split(dstPath)
case CopyModeFileToFileRename:
// Copy the file to the temp directory and move it to its dstPath
// afterwards.
movePath = dstPath
dstPath = "/tmp"
}
toTarOpts := &archive.TarOptions{IncludeSourceDir: true, NoOverwriteDirNonDir: true, Compression: archive.Gzip}
content, err := archive.TarWithOptions(srcPath, toTarOpts)
if err != nil {
return err
}
logrus.Debugf("copy %s from local to %s on container", srcPath, dstPath)
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(context.Background(), containerID, dstPath, content, copyOpts); err != nil {
return err
}
if movePath != "" {
_, srcFile := path.Split(srcPath)
dcli, err := command.NewDockerCli()
if err != nil {
return err
}
if err := container.RunExec(dcli, cl, containerID, &types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: []string{"mv", path.Join("/tmp", srcFile), movePath},
Detach: false,
Tty: true,
}); err != nil {
return fmt.Errorf("create remote directory: %s", err)
}
}
return nil
}
// copyFromContainer copies a file or directory from the given container to the local file system.
// See the possible copy modes and their documentation.
func copyFromContainer(cl *dockerClient.Client, containerID, srcPath, dstPath string) error {
srcStat, err := cl.ContainerStatPath(context.Background(), containerID, srcPath)
if err != nil {
if errdefs.IsNotFound(err) {
return fmt.Errorf("remote: %s does not exist", srcPath)
} else {
return fmt.Errorf("remote path: %s", err)
}
}
dstStat, err := os.Stat(dstPath)
dstExists := true
var dstMode os.FileMode
if err != nil {
if os.IsNotExist(err) {
dstExists = false
} else {
return fmt.Errorf("remote path: %s", err)
}
} else {
dstMode = dstStat.Mode()
}
mode, err := copyMode(srcPath, dstPath, srcStat.Mode, dstMode, dstExists)
if err != nil {
return err
}
moveDstDir := ""
moveDstFile := ""
switch mode {
case CopyModeFileToFile:
// Remove the file component from the path, since docker can only copy
// to a directory.
dstPath, _ = path.Split(dstPath)
case CopyModeFileToFileRename:
// Copy the file to the temp directory and move it to its dstPath
// afterwards.
moveDstFile = dstPath
dstPath = "/tmp"
case CopyModeFilesToDir:
// Copy the directory to the temp directory and move it to its
// dstPath afterwards.
moveDstDir = path.Join(dstPath, "/")
dstPath = "/tmp"
// Make sure the temp directory always gets removed
defer os.Remove(path.Join("/tmp"))
}
content, _, err := cl.CopyFromContainer(context.Background(), containerID, srcPath)
if err != nil {
return fmt.Errorf("copy: %s", err)
}
defer content.Close()
if err := archive.Untar(content, dstPath, &archive.TarOptions{
NoOverwriteDirNonDir: true,
Compression: archive.Gzip,
NoLchown: true,
}); err != nil {
return fmt.Errorf("untar: %s", err)
}
if moveDstFile != "" {
_, srcFile := path.Split(strings.TrimSuffix(srcPath, "/"))
if err := moveFile(path.Join("/tmp", srcFile), moveDstFile); err != nil {
return err
}
}
if moveDstDir != "" {
_, srcDir := path.Split(strings.TrimSuffix(srcPath, "/"))
if err := moveDir(path.Join("/tmp", srcDir), moveDstDir); err != nil {
return err
}
}
return nil
}
var (
ErrCopyDirToFile = fmt.Errorf("can't copy dir to file")
ErrDstDirNotExist = fmt.Errorf("destination directory does not exist")
)
type CopyMode int
const (
// Copy a src file to a dest file. The src and dest file names are the same.
// <dir_src>/<file> + <dir_dst>/<file> -> <dir_dst>/<file>
CopyModeFileToFile = CopyMode(iota)
// Copy a src file to a dest file. The src and dest file names are not the same.
// <dir_src>/<file_src> + <dir_dst>/<file_dst> -> <dir_dst>/<file_dst>
CopyModeFileToFileRename
// Copy a src file to dest directory. The dest file gets created in the dest
// folder with the src filename.
// <dir_src>/<file> + <dir_dst> -> <dir_dst>/<file>
CopyModeFileToDir
// Copy a src directory to dest directory.
// <dir_src> + <dir_dst> -> <dir_dst>/<dir_src>
CopyModeDirToDir
// Copy all files in the src directory to the dest directory. This works recursively.
// <dir_src>/ + <dir_dst> -> <dir_dst>/<files_from_dir_src>
CopyModeFilesToDir
)
// copyMode takes a src and dest path and file mode to determine the copy mode.
// See the possible copy modes and their documentation.
func copyMode(srcPath, dstPath string, srcMode os.FileMode, dstMode os.FileMode, dstExists bool) (CopyMode, error) {
_, srcFile := path.Split(srcPath)
_, dstFile := path.Split(dstPath)
if srcMode.IsDir() {
if !dstExists {
return -1, ErrDstDirNotExist
}
if dstMode.IsDir() {
if strings.HasSuffix(srcPath, "/") {
return CopyModeFilesToDir, nil
}
return CopyModeDirToDir, nil
}
return -1, ErrCopyDirToFile
}
if dstMode.IsDir() {
return CopyModeFileToDir, nil
}
if srcFile != dstFile {
return CopyModeFileToFileRename, nil
}
return CopyModeFileToFile, nil
}
// moveDir moves all files from a source path to the destination path recursively.
func moveDir(sourcePath, destPath string) error {
return filepath.Walk(sourcePath, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
newPath := path.Join(destPath, strings.TrimPrefix(p, sourcePath))
if info.IsDir() {
err := os.Mkdir(newPath, info.Mode())
if err != nil {
if os.IsExist(err) {
return nil
}
return err
}
}
if info.Mode().IsRegular() {
return moveFile(p, newPath)
}
return nil
})
}
// moveFile moves a file from a source path to a destination path.
func moveFile(sourcePath, destPath string) error {
inputFile, err := os.Open(sourcePath)
if err != nil {
return err
}
outputFile, err := os.Create(destPath)
if err != nil {
inputFile.Close()
return err
}
defer outputFile.Close()
_, err = io.Copy(outputFile, inputFile)
inputFile.Close()
if err != nil {
return err
}
// Remove file after succesfull copy.
err = os.Remove(sourcePath)
if err != nil {
return err
}
return nil
}

113
cli/app/cp_test.go Normal file
View File

@ -0,0 +1,113 @@
package app
import (
"os"
"testing"
)
func TestParse(t *testing.T) {
tests := []struct {
src string
dst string
srcPath string
dstPath string
service string
toContainer bool
err error
}{
{src: "foo", dst: "bar", err: errServiceMissing},
{src: "app:foo", dst: "app:bar", err: errServiceMissing},
{src: "app:foo", dst: "bar", srcPath: "foo", dstPath: "bar", service: "app", toContainer: false},
{src: "foo", dst: "app:bar", srcPath: "foo", dstPath: "bar", service: "app", toContainer: true},
}
for i, tc := range tests {
srcPath, dstPath, service, toContainer, err := parseSrcAndDst(tc.src, tc.dst)
if srcPath != tc.srcPath {
t.Errorf("[%d] srcPath: want (%s), got(%s)", i, tc.srcPath, srcPath)
}
if dstPath != tc.dstPath {
t.Errorf("[%d] dstPath: want (%s), got(%s)", i, tc.dstPath, dstPath)
}
if service != tc.service {
t.Errorf("[%d] service: want (%s), got(%s)", i, tc.service, service)
}
if toContainer != tc.toContainer {
t.Errorf("[%d] toConainer: want (%t), got(%t)", i, tc.toContainer, toContainer)
}
if err == nil && tc.err != nil && err.Error() != tc.err.Error() {
t.Errorf("[%d] err: want (%s), got(%s)", i, tc.err, err)
}
}
}
func TestCopyMode(t *testing.T) {
tests := []struct {
srcPath string
dstPath string
srcMode os.FileMode
dstMode os.FileMode
dstExists bool
mode CopyMode
err error
}{
{
srcPath: "foo.txt",
dstPath: "foo.txt",
srcMode: os.ModePerm,
dstMode: os.ModePerm,
dstExists: true,
mode: CopyModeFileToFile,
},
{
srcPath: "foo.txt",
dstPath: "bar.txt",
srcMode: os.ModePerm,
dstExists: true,
mode: CopyModeFileToFileRename,
},
{
srcPath: "foo",
dstPath: "foo",
srcMode: os.ModeDir,
dstMode: os.ModeDir,
dstExists: true,
mode: CopyModeDirToDir,
},
{
srcPath: "foo/",
dstPath: "foo",
srcMode: os.ModeDir,
dstMode: os.ModeDir,
dstExists: true,
mode: CopyModeFilesToDir,
},
{
srcPath: "foo",
dstPath: "foo",
srcMode: os.ModeDir,
dstExists: false,
mode: -1,
err: ErrDstDirNotExist,
},
{
srcPath: "foo",
dstPath: "foo",
srcMode: os.ModeDir,
dstMode: os.ModePerm,
dstExists: true,
mode: -1,
err: ErrCopyDirToFile,
},
}
for i, tc := range tests {
mode, err := copyMode(tc.srcPath, tc.dstPath, tc.srcMode, tc.dstMode, tc.dstExists)
if mode != tc.mode {
t.Errorf("[%d] mode: want (%d), got(%d)", i, tc.mode, mode)
}
if err != tc.err {
t.Errorf("[%d] err: want (%s), got(%s)", i, tc.err, err)
}
}
}

View File

@ -1,83 +1,179 @@
package app
import (
"context"
"fmt"
"strings"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/secret"
"coopcloud.tech/abra/pkg/client"
stack "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/dns"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe"
"github.com/AlecAivazis/survey/v2"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var appDeployCommand = &cli.Command{
Name: "deploy",
Aliases: []string{"d"},
Usage: "Deploy an app",
var appDeployCommand = cli.Command{
Name: "deploy",
Aliases: []string{"d"},
Usage: "Deploy an app",
ArgsUsage: "<domain> [<version>]",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
internal.ChaosFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `
This command deploys a new instance of an app. It does not support changing the
version of an existing deployed app, for this you need to look at the "abra app
upgrade <app>" command.
Deploy an app. It does not support incrementing the version of a deployed app,
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
if the container runtime has gotten into a weird state or your doing some live
hacking.
if the container runtime has gotten into a weird state.
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
recipes.
`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
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 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)
}
}
r, err := recipe.Get(app.Recipe, internal.Offline)
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)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("checking whether '%s' is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
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 internal.Force {
logrus.Infof("continuing with deployment due to '--force/-f' being set")
if internal.Force || internal.Chaos {
logrus.Warnf("%s is already deployed but continuing (--force/--chaos)", app.Name)
} else {
logrus.Fatalf("'%s' is already deployed", stackName)
logrus.Fatalf("%s is already deployed", app.Name)
}
}
version := deployedVersion
if version == "" {
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type)
if err != nil {
if specificVersion != "" {
version = specificVersion
logrus.Debugf("choosing %s as version to deploy", version)
if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
logrus.Fatal(err)
}
if len(versions) > 0 {
version = versions[len(versions)-1]
logrus.Infof("choosing '%s' as version to deploy", version)
if err := recipe.EnsureVersion(app.Type, version); err != nil {
logrus.Fatal(err)
}
} else {
version = "latest commit"
logrus.Warning("no versions detected, using latest commit")
if err := recipe.EnsureLatest(app.Type); err != nil {
logrus.Fatal(err)
}
}
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh")
if !internal.Chaos && specificVersion == "" {
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
logrus.Fatal(err)
}
versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil {
logrus.Fatal(err)
}
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]
logrus.Debugf("choosing %s as version to deploy", version)
if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
logrus.Fatal(err)
}
} else {
head, err := git.GetRecipeHead(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
version = formatter.SmallSHA(head.String())
logrus.Warn("no versions detected, using latest commit")
}
}
if internal.Chaos {
logrus.Warnf("chaos mode engaged")
var err error
version, err = recipe.ChaosVersion(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
logrus.Fatal(err)
@ -86,10 +182,11 @@ hacking.
app.Env[k] = v
}
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil {
logrus.Fatal(err)
}
deployOpts := stack.Deploy{
Composefiles: composeFiles,
Namespace: stackName,
@ -101,84 +198,57 @@ hacking.
logrus.Fatal(err)
}
if err := DeployOverview(app, version); err != nil {
config.ExposeAllEnv(stackName, compose, app.Env)
config.SetRecipeLabel(compose, stackName, app.Recipe)
config.SetChaosLabel(compose, stackName, internal.Chaos)
config.SetChaosVersionLabel(compose, stackName, version)
config.SetUpdateLabel(compose, stackName, app.Env)
envVars, err := config.CheckEnv(app)
if err != nil {
logrus.Fatal(err)
}
if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
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)
}
if !internal.NoDomainChecks {
domainName, ok := app.Env["DOMAIN"]
if ok {
if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
logrus.Fatal(err)
}
} else {
logrus.Warn("skipping domain checks as no DOMAIN=... configured for app")
}
} else {
logrus.Warn("skipping domain checks as requested")
}
stack.WaitTimeout, err = config.GetTimeoutFromLabel(compose, stackName)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
if err := stack.RunDeploy(cl, deployOpts, compose, app.Name, internal.DontWaitConverge); err != nil {
logrus.Fatal(err)
}
postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"]
if ok && !internal.DontWaitConverge {
logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds)
if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
logrus.Fatalf("attempting to run post deploy commands, saw: %s", err)
}
}
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}
// DeployOverview shows a deployment overview
func DeployOverview(app config.App, version string) error {
tableCol := []string{"server", "compose", "domain", "stack", "version"}
table := abraFormatter.CreateTable(tableCol)
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
}
table.Append([]string{app.Server, deployConfig, app.Domain, app.StackName(), version})
table.Render()
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
}
// NewVersionOverview shows an upgrade or downgrade overview
func NewVersionOverview(app config.App, currentVersion, newVersion string) error {
tableCol := []string{"server", "compose", "domain", "stack", "current version", "to be deployed"}
table := abraFormatter.CreateTable(tableCol)
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
}
table.Append([]string{app.Server, deployConfig, app.Domain, app.StackName(), currentVersion, newVersion})
table.Render()
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
}

142
cli/app/errors.go Normal file
View File

@ -0,0 +1,142 @@
package app
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var appErrorsCommand = cli.Command{
Name: "errors",
Usage: "List errors for a deployed app",
ArgsUsage: "<domain>",
Description: `
List errors for a deployed app.
This is a best-effort implementation and an attempt to gather a number of tips
& tricks for finding errors together into one convenient command. When an app
is failing to deploy or having issues, it could be a lot of things.
This command currently takes into account:
Is the service deployed?
Is the service killed by an OOM error?
Is the service reporting an error (like in "ps --no-trunc" output)
Is the service healthcheck failing? what are the healthcheck logs?
Got any more ideas? Please let us know:
https://git.coopcloud.tech/coop-cloud/organising/issues/new/choose
This command is best accompanied by "abra app logs <domain>" which may reveal
further information which can help you debug the cause of an app failure via
the logs.
`,
Aliases: []string{"e"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.WatchFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
cl, err := client.New(app.Server)
if err != nil {
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)
}
if !internal.Watch {
if err := checkErrors(c, cl, app); err != nil {
logrus.Fatal(err)
}
return nil
}
for {
if err := checkErrors(c, cl, app); err != nil {
logrus.Fatal(err)
}
time.Sleep(2 * time.Second)
}
},
}
func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App) error {
recipe, err := recipe.Get(app.Recipe, internal.Offline)
if err != nil {
return err
}
for _, service := range recipe.Config.Services {
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name))
containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters})
if err != nil {
return err
}
if len(containers) == 0 {
logrus.Warnf("%s is not up, something seems wrong", service.Name)
continue
}
container := containers[0]
containerState, err := cl.ContainerInspect(context.Background(), container.ID)
if err != nil {
logrus.Fatal(err)
}
if containerState.State.OOMKilled {
logrus.Warnf("%s has been killed due to an out of memory error", service.Name)
}
if containerState.State.Error != "" {
logrus.Warnf("%s reports this error: %s", service.Name, containerState.State.Error)
}
if containerState.State.Health != nil {
if containerState.State.Health.Status != "healthy" {
logrus.Warnf("%s healthcheck status is %s", service.Name, containerState.State.Health.Status)
logrus.Warnf("%s healthcheck has failed %s times", service.Name, strconv.Itoa(containerState.State.Health.FailingStreak))
for _, log := range containerState.State.Health.Log {
logrus.Warnf("%s healthcheck logs: %s", service.Name, strings.TrimSpace(log.Output))
}
}
}
}
return nil
}
func getServiceName(names []string) string {
containerName := strings.Join(names, " ")
trimmed := strings.TrimPrefix(containerName, "/")
return strings.Split(trimmed, ".")[0]
}

View File

@ -1,120 +1,183 @@
package app
import (
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var status bool
var statusFlag = &cli.BoolFlag{
Name: "status",
Aliases: []string{"S"},
Value: false,
Name: "status, S",
Usage: "Show app deployment status",
Destination: &status,
}
var appType string
var typeFlag = &cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
var recipeFilter string
var recipeFlag = &cli.StringFlag{
Name: "recipe, r",
Value: "",
Usage: "Show apps of a specific type",
Destination: &appType,
Usage: "Show apps of a specific recipe",
Destination: &recipeFilter,
}
var listAppServer string
var listAppServerFlag = &cli.StringFlag{
Name: "server",
Aliases: []string{"s"},
Name: "server, s",
Value: "",
Usage: "Show apps of a specific server",
Destination: &listAppServer,
}
var appListCommand = &cli.Command{
Name: "list",
Usage: "List all managed apps",
type appStatus struct {
Server string `json:"server"`
Recipe string `json:"recipe"`
AppName string `json:"appName"`
Domain string `json:"domain"`
Status string `json:"status"`
Chaos string `json:"chaos"`
ChaosVersion string `json:"chaosVersion"`
AutoUpdate string `json:"autoUpdate"`
Version string `json:"version"`
Upgrade string `json:"upgrade"`
}
type serverStatus struct {
Apps []appStatus `json:"apps"`
AppCount int `json:"appCount"`
VersionCount int `json:"versionCount"`
UnversionedCount int `json:"unversionedCount"`
LatestCount int `json:"latestCount"`
UpgradeCount int `json:"upgradeCount"`
}
var appListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List all managed apps",
Description: `
This command looks at your local file system listing of apps and servers (e.g.
in ~/.abra/) to generate a report of all your apps.
Read the local file system listing of apps and servers (e.g. ~/.abra/) to
generate a report of all your apps.
By passing the "--status/-S" flag, you can query all your servers for the
actual live deployment status. Depending on how many servers you manage, this
can take some time.
`,
Aliases: []string{"ls"},
`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.MachineReadableFlag,
statusFlag,
listAppServerFlag,
typeFlag,
recipeFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
appFiles, err := config.LoadAppFiles(listAppServer)
if err != nil {
logrus.Fatal(err)
}
apps, err := config.GetApps(appFiles)
apps, err := config.GetApps(appFiles, recipeFilter)
if err != nil {
logrus.Fatal(err)
}
sort.Sort(config.ByServerAndType(apps))
sort.Sort(config.ByServerAndRecipe(apps))
statuses := make(map[string]map[string]string)
tableCol := []string{"Server", "Type", "Domain"}
var catl recipe.RecipeCatalogue
if status {
tableCol = append(tableCol, "Status", "Version", "Updates")
statuses, err = config.GetAppStatuses(appFiles)
alreadySeen := make(map[string]bool)
for _, app := range apps {
if _, ok := alreadySeen[app.Server]; !ok {
alreadySeen[app.Server] = true
}
}
statuses, err = config.GetAppStatuses(apps, internal.MachineReadable)
if err != nil {
logrus.Fatal(err)
}
catl, err = recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
logrus.Fatal(err)
}
}
table := abraFormatter.CreateTable(tableCol)
table.SetAutoMergeCellsByColumnIndex([]int{0})
var (
versionedAppsCount int
unversionedAppsCount int
onLatestCount int
canUpgradeCount int
)
var totalServersCount int
var totalAppsCount int
allStats := make(map[string]serverStatus)
for _, app := range apps {
var tableRow []string
if app.Type == appType || appType == "" {
// If type flag is set, check for it, if not, Type == ""
tableRow = []string{app.Server, app.Type, app.Domain}
var stats serverStatus
var ok bool
if stats, ok = allStats[app.Server]; !ok {
stats = serverStatus{}
if recipeFilter == "" {
// count server, no filtering
totalServersCount++
}
}
if app.Recipe == recipeFilter || recipeFilter == "" {
if recipeFilter != "" {
// only count server if matches filter
totalServersCount++
}
appStats := appStatus{}
stats.AppCount++
totalAppsCount++
if status {
stackName := app.StackName()
status := "unknown"
version := "unknown"
if statusMeta, ok := statuses[stackName]; ok {
chaos := "unknown"
chaosVersion := "unknown"
autoUpdate := "unknown"
if statusMeta, ok := statuses[app.StackName()]; ok {
if currentVersion, exists := statusMeta["version"]; exists {
version = currentVersion
if currentVersion != "" {
version = currentVersion
}
}
if chaosDeploy, exists := statusMeta["chaos"]; exists {
chaos = chaosDeploy
}
if chaosDeployVersion, exists := statusMeta["chaosVersion"]; exists {
chaosVersion = chaosDeployVersion
}
if autoUpdateState, exists := statusMeta["autoUpdate"]; exists {
autoUpdate = autoUpdateState
}
if statusMeta["status"] != "" {
status = statusMeta["status"]
}
tableRow = append(tableRow, status, version)
versionedAppsCount++
stats.VersionCount++
} else {
tableRow = append(tableRow, status, version)
unversionedAppsCount++
stats.UnversionedCount++
}
appStats.Status = status
appStats.Chaos = chaos
appStats.ChaosVersion = chaosVersion
appStats.Version = version
appStats.AutoUpdate = autoUpdate
var newUpdates []string
if version != "unknown" {
updates, err := catalogue.GetRecipeCatalogueVersions(app.Type)
updates, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil {
logrus.Fatal(err)
}
@ -138,35 +201,99 @@ can take some time.
if len(newUpdates) == 0 {
if version == "unknown" {
tableRow = append(tableRow, "unknown")
appStats.Upgrade = "unknown"
} else {
tableRow = append(tableRow, "on latest")
onLatestCount++
appStats.Upgrade = "latest"
stats.LatestCount++
}
} else {
// FIXME: jeezus golang why do you not have a list reverse function
for i, j := 0, len(newUpdates)-1; i < j; i, j = i+1, j-1 {
newUpdates[i], newUpdates[j] = newUpdates[j], newUpdates[i]
}
tableRow = append(tableRow, strings.Join(newUpdates, "\n"))
canUpgradeCount++
newUpdates = internal.ReverseStringList(newUpdates)
appStats.Upgrade = strings.Join(newUpdates, "\n")
stats.UpgradeCount++
}
}
appStats.Server = app.Server
appStats.Recipe = app.Recipe
appStats.AppName = app.Name
appStats.Domain = app.Domain
stats.Apps = append(stats.Apps, appStats)
}
table.Append(tableRow)
allStats[app.Server] = stats
}
stats := fmt.Sprintf(
"Total apps: %v | Versioned: %v | Unversioned: %v | On latest: %v | Can upgrade: %v",
len(apps),
versionedAppsCount,
unversionedAppsCount,
onLatestCount,
canUpgradeCount,
)
if internal.MachineReadable {
jsonstring, err := json.Marshal(allStats)
if err != nil {
logrus.Fatal(err)
} else {
fmt.Println(string(jsonstring))
}
return nil
}
table.SetCaption(true, stats)
table.Render()
alreadySeen := make(map[string]bool)
for _, app := range apps {
if _, ok := alreadySeen[app.Server]; ok {
continue
}
serverStat := allStats[app.Server]
tableCol := []string{"recipe", "domain"}
if status {
tableCol = append(tableCol, []string{"status", "chaos", "version", "upgrade", "autoupdate"}...)
}
table := formatter.CreateTable(tableCol)
for _, appStat := range serverStat.Apps {
tableRow := []string{appStat.Recipe, appStat.Domain}
if status {
chaosStatus := appStat.Chaos
if chaosStatus != "unknown" {
chaosEnabled, err := strconv.ParseBool(chaosStatus)
if err != nil {
logrus.Fatal(err)
}
if chaosEnabled && appStat.ChaosVersion != "unknown" {
chaosStatus = appStat.ChaosVersion
}
}
tableRow = append(tableRow, []string{appStat.Status, chaosStatus, appStat.Version, appStat.Upgrade, appStat.AutoUpdate}...)
}
table.Append(tableRow)
}
if table.NumLines() > 0 {
table.Render()
if status {
fmt.Println(fmt.Sprintf(
"server: %s | total apps: %v | versioned: %v | unversioned: %v | latest: %v | upgrade: %v",
app.Server,
serverStat.AppCount,
serverStat.VersionCount,
serverStat.UnversionedCount,
serverStat.LatestCount,
serverStat.UpgradeCount,
))
} else {
fmt.Println(fmt.Sprintf("server: %s | total apps: %v", app.Server, serverStat.AppCount))
}
}
if len(allStats) > 1 && table.NumLines() > 0 {
fmt.Println() // newline separator for multiple servers
}
alreadySeen[app.Server] = true
}
if len(allStats) > 1 {
fmt.Println(fmt.Sprintf("total servers: %v | total apps: %v ", totalServersCount, totalAppsCount))
}
return nil
},

View File

@ -1,48 +1,128 @@
package app
import (
"fmt"
"context"
"io"
"os"
"slices"
"sync"
"time"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
// stackLogs lists logs for all stack services
func stackLogs(c *cli.Context, stackName string, client *dockerClient.Client) {
filters := filters.NewArgs()
filters.Add("name", stackName)
serviceOpts := types.ServiceListOptions{Filters: filters}
services, err := client.ServiceList(c.Context, serviceOpts)
var appLogsCommand = cli.Command{
Name: "logs",
Aliases: []string{"l"},
ArgsUsage: "<domain> [<service>]",
Usage: "Tail app logs",
Flags: []cli.Flag{
internal.StdErrOnlyFlag,
internal.SinceLogsFlag,
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName()
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
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)
}
serviceName := c.Args().Get(1)
serviceNames := []string{}
if serviceName != "" {
serviceNames = []string{serviceName}
}
err = tailLogs(cl, app, serviceNames)
if err != nil {
logrus.Fatal(err)
}
return nil
},
}
// tailLogs prints logs for the given app with optional service names to be
// filtered on. It also checks if the latest task is not runnning and then
// prints the past tasks.
func tailLogs(cl *dockerClient.Client, app config.App, serviceNames []string) error {
f, err := app.Filters(true, false, serviceNames...)
if err != nil {
logrus.Fatal(err)
return err
}
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: f})
if err != nil {
return err
}
var wg sync.WaitGroup
for _, service := range services {
wg.Add(1)
go func(s string) {
logOpts := types.ContainerLogsOptions{
Details: true,
Follow: true,
ShowStderr: true,
ShowStdout: true,
Tail: "20",
Timestamps: true,
filters := filters.NewArgs()
filters.Add("name", service.Spec.Name)
tasks, err := cl.TaskList(context.Background(), types.TaskListOptions{Filters: f})
if err != nil {
return err
}
if len(tasks) > 0 {
// Need to sort the tasks by the CreatedAt field in the inverse order.
// Otherwise they are in the reversed order and not sorted properly.
slices.SortFunc[[]swarm.Task](tasks, func(t1, t2 swarm.Task) int {
return int(t2.Meta.CreatedAt.Unix() - t1.Meta.CreatedAt.Unix())
})
lastTask := tasks[0].Status
if lastTask.State != swarm.TaskStateRunning {
for _, task := range tasks {
logrus.Errorf("[%s] %s State %s: %s", service.Spec.Name, task.Meta.CreatedAt.Format(time.RFC3339), task.Status.State, task.Status.Err)
}
}
logs, err := client.ServiceLogs(c.Context, s, logOpts)
}
// Collect the logs in a go routine, so the logs from all services are
// collected in parallel.
wg.Add(1)
go func(serviceID string) {
logs, err := cl.ServiceLogs(context.Background(), serviceID, types.ContainerLogsOptions{
ShowStderr: true,
ShowStdout: !internal.StdErrOnly,
Since: internal.SinceLogs,
Until: "",
Timestamps: true,
Follow: true,
Tail: "20",
Details: false,
})
if err != nil {
logrus.Fatal(err)
}
// defer after err check as any err returns a nil io.ReadCloser
defer logs.Close()
_, err = io.Copy(os.Stdout, logs)
@ -51,74 +131,9 @@ func stackLogs(c *cli.Context, stackName string, client *dockerClient.Client) {
}
}(service.ID)
}
// Wait for all log streams to be closed.
wg.Wait()
os.Exit(0)
}
var appLogsCommand = &cli.Command{
Name: "logs",
Aliases: []string{"l"},
ArgsUsage: "[<service>]",
Usage: "Tail app logs",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
serviceName := c.Args().Get(1)
if serviceName == "" {
logrus.Debug("tailing logs for all app services")
stackLogs(c, app.StackName(), cl)
}
logrus.Debugf("tailing logs for '%s'", serviceName)
service := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
filters := filters.NewArgs()
filters.Add("name", service)
serviceOpts := types.ServiceListOptions{Filters: filters}
services, err := cl.ServiceList(c.Context, serviceOpts)
if err != nil {
logrus.Fatal(err)
}
if len(services) != 1 {
logrus.Fatalf("expected 1 service but got %v", len(services))
}
logOpts := types.ContainerLogsOptions{
Details: true,
Follow: true,
ShowStderr: true,
ShowStdout: true,
Tail: "20",
Timestamps: true,
}
logs, err := cl.ServiceLogs(c.Context, services[0].ID, logOpts)
if err != nil {
logrus.Fatal(err)
}
// defer after err check as any err returns a nil io.ReadCloser
defer logs.Close()
_, err = io.Copy(os.Stdout, logs)
if err != nil && err != io.EOF {
logrus.Fatal(err)
}
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
return nil
}

View File

@ -4,51 +4,27 @@ import (
"fmt"
"path"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/jsontable"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret"
"github.com/AlecAivazis/survey/v2"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
type secrets map[string]string
var domain string
var domainFlag = &cli.StringFlag{
Name: "domain",
Aliases: []string{"d"},
Value: "",
Usage: "Choose a domain name",
Destination: &domain,
}
var newAppServer string
var newAppServerFlag = &cli.StringFlag{
Name: "server",
Aliases: []string{"s"},
Value: "",
Usage: "Show apps of a specific server",
Destination: &newAppServer,
}
var newAppName string
var newAppNameFlag = &cli.StringFlag{
Name: "app-name",
Aliases: []string{"a"},
Value: "",
Usage: "Choose an app name",
Destination: &newAppName,
}
var appNewDescription = `
This command takes a recipe and uses it to create a new app. This new app
configuration is stored in your ~/.abra directory under the appropriate server.
Take a recipe and uses it to create a new app. This new app configuration is
stored in your ~/.abra directory under the appropriate server.
This command does not deploy your app for you. You will need to run "abra app
deploy <app>" to do so.
deploy <domain>" to do so.
You can see what recipes are available (i.e. values for the <recipe> argument)
by running "abra recipe ls".
@ -63,44 +39,207 @@ pass store (see passwordstore.org for more). The pass command must be available
on your $PATH.
`
var appNewCommand = &cli.Command{
var appNewCommand = cli.Command{
Name: "new",
Usage: "Create a new app",
Aliases: []string{"n"},
Usage: "Create a new app",
Description: appNewDescription,
Flags: []cli.Flag{
newAppServerFlag,
domainFlag,
newAppNameFlag,
internal.DebugFlag,
internal.NoInputFlag,
internal.NewAppServerFlag,
internal.DomainFlag,
internal.PassFlag,
internal.SecretsFlag,
internal.OfflineFlag,
internal.ChaosFlag,
},
ArgsUsage: "<recipe>",
Action: action,
BashComplete: func(c *cli.Context) {
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Warn(err)
Before: internal.SubCommandBefore,
ArgsUsage: "[<recipe>] [<version>]",
BashComplete: func(ctx *cli.Context) {
args := ctx.Args()
switch len(args) {
case 0:
autocomplete.RecipeNameComplete(ctx)
case 1:
autocomplete.RecipeVersionComplete(ctx.Args().Get(0))
}
if c.NArg() > 0 {
return
},
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
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 c.Args().Get(1) == "" {
if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
logrus.Fatal(err)
}
} else {
if err := recipePkg.EnsureVersion(recipe.Name, c.Args().Get(1)); err != nil {
logrus.Fatal(err)
}
}
}
for name := range catl {
fmt.Println(name)
if err := ensureServerFlag(); err != nil {
logrus.Fatal(err)
}
if err := ensureDomainFlag(recipe, internal.NewAppServer); err != nil {
logrus.Fatal(err)
}
sanitisedAppName := config.SanitiseAppName(internal.Domain)
logrus.Debugf("%s sanitised as %s for new app", internal.Domain, sanitisedAppName)
if err := config.TemplateAppEnvSample(
recipe.Name,
internal.Domain,
internal.NewAppServer,
internal.Domain,
); err != nil {
logrus.Fatal(err)
}
var secrets AppSecrets
var secretTable *jsontable.JSONTable
if internal.Secrets {
sampleEnv, err := recipe.SampleEnv()
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, config.StackName(internal.Domain))
if err != nil {
return err
}
if err := promptForSecrets(recipe.Name, secretsConfig); err != nil {
logrus.Fatal(err)
}
cl, err := client.New(internal.NewAppServer)
if err != nil {
logrus.Fatal(err)
}
secrets, err = createSecrets(cl, secretsConfig, sanitisedAppName)
if err != nil {
logrus.Fatal(err)
}
secretCols := []string{"Name", "Value"}
secretTable = formatter.CreateTable(secretCols)
for name, val := range secrets {
secretTable.Append([]string{name, val})
}
}
if internal.NewAppServer == "default" {
internal.NewAppServer = "local"
}
tableCol := []string{"server", "recipe", "domain"}
table := formatter.CreateTable(tableCol)
table.Append([]string{internal.NewAppServer, recipe.Name, internal.Domain})
fmt.Println(fmt.Sprintf("A new %s app has been created! Here is an overview:", recipe.Name))
fmt.Println("")
table.Render()
fmt.Println("")
fmt.Println("You can configure this app by running the following:")
fmt.Println(fmt.Sprintf("\n abra app config %s", internal.Domain))
fmt.Println("")
fmt.Println("You can deploy this app by running the following:")
fmt.Println(fmt.Sprintf("\n abra app deploy %s", internal.Domain))
if len(secrets) > 0 {
fmt.Println("")
fmt.Println("Here are your generated secrets:")
fmt.Println("")
secretTable.Render()
logrus.Warn("generated secrets are not shown again, please take note of them NOW")
}
return nil
},
}
// AppSecrets represents all app secrest
type AppSecrets map[string]string
// createSecrets creates all secrets for a new app.
func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secret, sanitisedAppName string) (AppSecrets, error) {
secrets, err := secret.GenerateSecrets(cl, secretsConfig, internal.NewAppServer)
if err != nil {
return nil, err
}
if internal.Pass {
for secretName := range secrets {
secretValue := secrets[secretName]
if err := secret.PassInsertSecret(
secretValue,
secretName,
internal.Domain,
internal.NewAppServer,
); err != nil {
return nil, err
}
}
}
return secrets, nil
}
// ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
func ensureDomainFlag() error {
if domain == "" {
func ensureDomainFlag(recipe recipe.Recipe, server string) error {
if internal.Domain == "" && !internal.NoInput {
prompt := &survey.Input{
Message: "Specify app domain",
Default: fmt.Sprintf("%s.%s", recipe.Name, server),
}
if err := survey.AskOne(prompt, &domain); err != nil {
if err := survey.AskOne(prompt, &internal.Domain); err != nil {
return err
}
}
if internal.Domain == "" {
return fmt.Errorf("no domain provided")
}
return nil
}
// promptForSecrets asks if we should generate secrets for a new app.
func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret) error {
if len(secretsConfig) == 0 {
logrus.Debugf("%s has no secrets to generate, skipping...", recipeName)
return nil
}
if !internal.Secrets && !internal.NoInput {
prompt := &survey.Confirm{
Message: "Generate app secrets?",
}
if err := survey.AskOne(prompt, &internal.Secrets); err != nil {
return err
}
}
return nil
}
@ -111,120 +250,19 @@ func ensureServerFlag() error {
return err
}
if newAppServer == "" {
if internal.NewAppServer == "" && !internal.NoInput {
prompt := &survey.Select{
Message: "Select app server:",
Options: servers,
}
if err := survey.AskOne(prompt, &newAppServer); err != nil {
if err := survey.AskOne(prompt, &internal.NewAppServer); err != nil {
return err
}
}
return nil
}
// ensureServerFlag checks if the AppName flag was used. if not, asks the user for it.
func ensureAppNameFlag() error {
if newAppName == "" {
prompt := &survey.Input{
Message: "Specify app name:",
Default: config.SanitiseAppName(domain),
}
if err := survey.AskOne(prompt, &newAppName); err != nil {
return err
}
if internal.NewAppServer == "" {
return fmt.Errorf("no server provided")
}
return nil
}
// createSecrets creates all secrets for a new app.
func createSecrets(sanitisedAppName string) (secrets, error) {
appEnvPath := path.Join(config.ABRA_DIR, "servers", newAppServer, fmt.Sprintf("%s.env", sanitisedAppName))
appEnv, err := config.ReadEnv(appEnvPath)
if err != nil {
return nil, err
}
secretEnvVars := secret.ReadSecretEnvVars(appEnv)
secrets, err := secret.GenerateSecrets(secretEnvVars, sanitisedAppName, newAppServer)
if err != nil {
return nil, err
}
if internal.Pass {
for secretName := range secrets {
secretValue := secrets[secretName]
if err := secret.PassInsertSecret(secretValue, secretName, sanitisedAppName, newAppServer); err != nil {
return nil, err
}
}
}
return secrets, nil
}
// action is the main command-line action for this package
func action(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
if err := config.EnsureAbraDirExists(); err != nil {
logrus.Fatal(err)
}
if err := ensureServerFlag(); err != nil {
logrus.Fatal(err)
}
if err := ensureDomainFlag(); err != nil {
logrus.Fatal(err)
}
if err := ensureAppNameFlag(); err != nil {
logrus.Fatal(err)
}
sanitisedAppName := config.SanitiseAppName(newAppName)
if len(sanitisedAppName) > 45 {
logrus.Fatalf("'%s' cannot be longer than 45 characters", sanitisedAppName)
}
logrus.Debugf("'%s' sanitised as '%s' for new app", newAppName, sanitisedAppName)
if err := config.TemplateAppEnvSample(recipe.Name, newAppName, newAppServer, domain, recipe.Name); err != nil {
logrus.Fatal(err)
}
if internal.Secrets {
secrets, err := createSecrets(sanitisedAppName)
if err != nil {
logrus.Fatal(err)
}
secretCols := []string{"Name", "Value"}
secretTable := abraFormatter.CreateTable(secretCols)
for secret := range secrets {
secretTable.Append([]string{secret, secrets[secret]})
}
if len(secrets) > 0 {
defer secretTable.Render()
}
}
tableCol := []string{"Name", "Domain", "Type", "Server"}
table := abraFormatter.CreateTable(tableCol)
table.Append([]string{sanitisedAppName, domain, recipe.Name, newAppServer})
fmt.Println("")
fmt.Println(fmt.Sprintf("New '%s' created! Here is your new app overview:", recipe.Name))
fmt.Println("")
table.Render()
fmt.Println("")
fmt.Println("You can configure this app by running the following:")
fmt.Println(fmt.Sprintf("\n abra app config %s", sanitisedAppName))
fmt.Println("")
fmt.Println("You can deploy this app by running the following:")
fmt.Println(fmt.Sprintf("\n abra app deploy %s", sanitisedAppName))
fmt.Println("")
return nil
}

View File

@ -1,82 +1,83 @@
package app
import (
"fmt"
"context"
"strings"
"time"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/docker/cli/cli/command/formatter"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/service"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/buger/goterm"
dockerFormatter "github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var watch bool
var watchFlag = &cli.BoolFlag{
Name: "watch",
Aliases: []string{"w"},
Value: false,
Usage: "Watch status by polling repeatedly",
Destination: &watch,
}
var appPsCommand = &cli.Command{
Name: "ps",
Usage: "Check app status",
Aliases: []string{"p"},
var appPsCommand = cli.Command{
Name: "ps",
Aliases: []string{"p"},
Usage: "Check app status",
ArgsUsage: "<domain>",
Description: "Show a more detailed status output of a specific deployed app",
Flags: []cli.Flag{
watchFlag,
internal.WatchFlag,
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
if !watch {
showPSOutput(c)
app := internal.ValidateApp(c)
cl, err := client.New(app.Server)
if err != nil {
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)
}
if !internal.Watch {
showPSOutput(c, app, cl)
return nil
}
// TODO: how do we make this update in-place in an x-platform way?
goterm.Clear()
for {
showPSOutput(c)
goterm.MoveCursor(1, 1)
showPSOutput(c, app, cl)
goterm.Flush()
time.Sleep(2 * time.Second)
}
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}
// showPSOutput renders ps output.
func showPSOutput(c *cli.Context) {
app := internal.ValidateApp(c)
cl, err := client.New(app.Server)
func showPSOutput(c *cli.Context, app config.App, cl *dockerClient.Client) {
filters, err := app.Filters(true, true)
if err != nil {
logrus.Fatal(err)
}
filters := filters.NewArgs()
filters.Add("name", app.StackName())
containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters})
if err != nil {
logrus.Fatal(err)
}
tableCol := []string{"image", "created", "status", "ports", "names"}
table := abraFormatter.CreateTable(tableCol)
tableCol := []string{"service name", "image", "created", "status", "state", "ports"}
table := formatter.CreateTable(tableCol)
for _, container := range containers {
var containerNames []string
@ -86,11 +87,12 @@ func showPSOutput(c *cli.Context) {
}
tableRow := []string{
abraFormatter.RemoveSha(container.Image),
abraFormatter.HumanDuration(container.Created),
service.ContainerToServiceName(container.Names, app.StackName()),
formatter.RemoveSha(container.Image),
formatter.HumanDuration(container.Created),
container.Status,
formatter.DisplayablePorts(container.Ports),
strings.Join(containerNames, "\n"),
container.State,
dockerFormatter.DisplayablePorts(container.Ports),
}
table.Append(tableRow)
}

View File

@ -1,76 +1,88 @@
package app
import (
"context"
"fmt"
"log"
"os"
"time"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/volume"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
// Volumes stores the variable from VolumesFlag
var Volumes bool
var appRemoveCommand = cli.Command{
Name: "remove",
Aliases: []string{"rm"},
ArgsUsage: "<domain>",
Usage: "Remove all app data, locally and remotely",
Description: `
This command removes everything related to an app which is already undeployed.
// VolumesFlag is used to specify if volumes should be deleted when deleting an app
var VolumesFlag = &cli.BoolFlag{
Name: "volumes",
Value: false,
Destination: &Volumes,
}
By default, it will prompt for confirmation before proceeding. All secrets,
volumes and the local app env file will be deleted.
var appRemoveCommand = &cli.Command{
Name: "remove",
Usage: "Remove an already undeployed app",
Aliases: []string{"rm"},
Only run this command when you are sure you want to completely remove the app
and all associated app data. This is a destructive action, Be Careful!
If you would like to delete specific volumes or secrets, please use removal
sub-commands under "app volume" and "app secret" instead.
Please note, if you delete the local app env file without removing volumes and
secrets first, Abra will *not* be able to help you remove them afterwards.
To delete everything without prompt, use the "--force/-f" or the "--no-input/n"
flag.
`,
Flags: []cli.Flag{
VolumesFlag,
internal.ForceFlag,
internal.DebugFlag,
internal.NoInputFlag,
internal.OfflineFlag,
},
BashComplete: autocomplete.AppNameComplete,
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if !internal.Force {
if !internal.Force && !internal.NoInput {
response := false
prompt := &survey.Confirm{
Message: fmt.Sprintf("about to delete %s, are you sure?", app.Name),
}
msg := "ALERTA ALERTA: this will completely remove %s data and configurations locally and remotely, are you sure?"
prompt := &survey.Confirm{Message: fmt.Sprintf(msg, app.Name)}
if err := survey.AskOne(prompt, &response); err != nil {
logrus.Fatal(err)
}
if !response {
logrus.Fatal("user aborted app removal")
logrus.Fatal("aborting as requested")
}
}
appFiles, err := config.LoadAppFiles("")
if err != nil {
logrus.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
if !internal.Force {
// FIXME: only query for app we are interested in, not all of them!
statuses, err := config.GetAppStatuses(appFiles)
if err != nil {
logrus.Fatal(err)
}
if statuses[app.Name]["status"] == "deployed" {
logrus.Fatalf("'%s' is still deployed. Run \"abra app %s undeploy\" or pass --force", app.Name, app.Name)
}
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)
}
fs := filters.NewArgs()
fs.Add("name", app.Name)
secretList, err := cl.SecretList(c.Context, types.SecretListOptions{Filters: fs})
fs, err := app.Filters(false, false)
if err != nil {
logrus.Fatal(err)
}
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: fs})
if err != nil {
logrus.Fatal(err)
}
@ -84,20 +96,8 @@ var appRemoveCommand = &cli.Command{
}
if len(secrets) > 0 {
var secretNamesToRemove []string
if !internal.Force {
secretsPrompt := &survey.MultiSelect{
Message: "which secrets do you want to remove?",
Options: secretNames,
Default: secretNames,
}
if err := survey.AskOne(secretsPrompt, &secretNamesToRemove); err != nil {
logrus.Fatal(err)
}
}
for _, name := range secretNamesToRemove {
err := cl.SecretRemove(c.Context, secrets[name])
for _, name := range secretNames {
err := cl.SecretRemove(context.Background(), secrets[name])
if err != nil {
logrus.Fatal(err)
}
@ -107,7 +107,13 @@ var appRemoveCommand = &cli.Command{
logrus.Info("no secrets to remove")
}
volumeListOKBody, err := cl.VolumeList(c.Context, fs)
fs, err = app.Filters(false, true)
if err != nil {
logrus.Fatal(err)
}
volumeListOptions := volume.ListOptions{fs}
volumeListOKBody, err := cl.VolumeList(context.Background(), volumeListOptions)
volumeList := volumeListOKBody.Volumes
if err != nil {
logrus.Fatal(err)
@ -119,50 +125,43 @@ var appRemoveCommand = &cli.Command{
}
if len(vols) > 0 {
if Volumes {
var removeVols []string
if !internal.Force {
volumesPrompt := &survey.MultiSelect{
Message: "which volumes do you want to remove?",
Options: vols,
Default: vols,
}
if err := survey.AskOne(volumesPrompt, &removeVols); err != nil {
logrus.Fatal(err)
}
for _, vol := range vols {
err = retryFunc(5, func() error {
return cl.VolumeRemove(context.Background(), vol, internal.Force) // last argument is for force removing
})
if err != nil {
log.Fatalf("removing volumes failed: %s", err)
}
for _, vol := range removeVols {
err := cl.VolumeRemove(c.Context, vol, internal.Force) // last argument is for force removing
if err != nil {
logrus.Fatal(err)
}
logrus.Info(fmt.Sprintf("volume %s removed", vol))
}
} else {
logrus.Info("no volumes were removed")
logrus.Info(fmt.Sprintf("volume %s removed", vol))
}
} else {
logrus.Info("no volumes to remove")
}
err = os.Remove(app.Path)
if err != nil {
if err = os.Remove(app.Path); err != nil {
logrus.Fatal(err)
}
logrus.Info(fmt.Sprintf("file: %s removed", app.Path))
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}
// retryFunc retries the given function for the given retries. After the nth
// retry it waits (n + 1)^2 seconds before the next retry (starting with n=0).
// It returns an error if the function still failed after the last retry.
func retryFunc(retries int, fn func() error) error {
for i := 0; i < retries; i++ {
err := fn()
if err == nil {
return nil
}
if i+1 < retries {
sleep := time.Duration(i+1) * time.Duration(i+1)
logrus.Infof("%s: waiting %d seconds before next retry", err, sleep)
time.Sleep(sleep * time.Second)
}
}
return fmt.Errorf("%d retries failed", retries)
}

26
cli/app/remove_test.go Normal file
View File

@ -0,0 +1,26 @@
package app
import (
"fmt"
"testing"
)
func TestRetryFunc(t *testing.T) {
err := retryFunc(1, func() error { return nil })
if err != nil {
t.Errorf("should not return an error: %s", err)
}
i := 0
fn := func() error {
i++
return fmt.Errorf("oh no, something went wrong!")
}
err = retryFunc(2, fn)
if err == nil {
t.Error("should return an error")
}
if i != 2 {
t.Errorf("The function should have been called 1 times, got %d", i)
}
}

80
cli/app/restart.go Normal file
View File

@ -0,0 +1,80 @@
package app
import (
"context"
"errors"
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
upstream "coopcloud.tech/abra/pkg/upstream/service"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var appRestartCommand = cli.Command{
Name: "restart",
Aliases: []string{"re"},
Usage: "Restart an app",
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `This command restarts a service within a deployed app.`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
serviceNameShort := c.Args().Get(1)
if serviceNameShort == "" {
err := errors.New("missing service?")
internal.ShowSubcommandHelpAndError(c, err)
}
cl, err := client.New(app.Server)
if err != nil {
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)
logrus.Debugf("attempting to scale %s to 0 (restart logic)", serviceName)
if err := upstream.RunServiceScale(context.Background(), cl, serviceName, 0); err != nil {
logrus.Fatal(err)
}
if err := stack.WaitOnService(context.Background(), cl, serviceName, app.Name); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("%s has been scaled to 0 (restart logic)", serviceName)
logrus.Debugf("attempting to scale %s to 1 (restart logic)", serviceName)
if err := upstream.RunServiceScale(context.Background(), cl, serviceName, 1); err != nil {
logrus.Fatal(err)
}
if err := stack.WaitOnService(context.Background(), cl, serviceName, app.Name); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("%s has been scaled to 1 (restart logic)", serviceName)
logrus.Infof("%s service successfully restarted", serviceNameShort)
return nil
},
}

View File

@ -1,79 +1,223 @@
package app
import (
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/archive"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var restoreAllServices bool
var restoreAllServicesFlag = &cli.BoolFlag{
Name: "all",
Value: false,
Destination: &restoreAllServices,
Aliases: []string{"a"},
Usage: "Restore all services",
type restoreConfig struct {
preHookCmd string
postHookCmd string
}
var appRestoreCommand = &cli.Command{
var appRestoreCommand = cli.Command{
Name: "restore",
Usage: "Restore an app from a backup",
Aliases: []string{"r"},
Flags: []cli.Flag{restoreAllServicesFlag},
ArgsUsage: "<service> [<backup file>]",
Aliases: []string{"rs"},
Usage: "Run app restore",
ArgsUsage: "<domain> <service> <file>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Description: `
Run an app restore.
Pre/post hook commands are defined in the recipe configuration. Abra reads this
configuration and run the comands in the context of the service before
restoring the backup.
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
compressed archives which use absolute paths. This allows Abra to restore
according to standard tar command logic, i.e. the backup will be restored to
the path it was originally backed up from.
Example:
abra app restore example.com app ~/.abra/backups/example_com_app_609341138.tar.gz
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if c.Args().Len() > 1 && restoreAllServices {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use <service>/<backup file> and '--all' together"))
}
abraSh := path.Join(config.ABRA_DIR, "apps", app.Type, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("'%s' does not exist?", abraSh)
}
logrus.Fatal(err)
}
sourceCmd := fmt.Sprintf("source %s", abraSh)
execCmd := "abra_restore"
if !restoreAllServices {
serviceName := c.Args().Get(1)
if serviceName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no service(s) target provided"))
}
execCmd = fmt.Sprintf("abra_restore_%s", serviceName)
}
bytes, err := ioutil.ReadFile(abraSh)
recipe, err := recipe.Get(app.Recipe, internal.Offline)
if err != nil {
logrus.Fatal(err)
}
if !strings.Contains(string(bytes), execCmd) {
logrus.Fatalf("%s doesn't have a '%s' function", app.Type, execCmd)
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)
}
}
backupFile := c.Args().Get(2)
if backupFile != "" {
execCmd = fmt.Sprintf("%s %s", execCmd, backupFile)
serviceName := c.Args().Get(1)
if serviceName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("missing <service>?"))
}
sourceAndExec := fmt.Sprintf("%s; %s", sourceCmd, execCmd)
cmd := exec.Command("bash", "-c", sourceAndExec)
if err := internal.RunCmd(cmd); err != nil {
backupPath := c.Args().Get(2)
if backupPath == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("missing <file>?"))
}
if _, err := os.Stat(backupPath); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("%s doesn't exist?", backupPath)
}
}
restoreConfigs := make(map[string]restoreConfig)
for _, service := range recipe.Config.Services {
if restoreEnabled, ok := service.Deploy.Labels["backupbot.restore"]; ok {
if restoreEnabled == "true" {
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), service.Name)
rsConfig := restoreConfig{}
logrus.Debugf("restore config detected for %s", fullServiceName)
if preHookCmd, ok := service.Deploy.Labels["backupbot.restore.pre-hook"]; ok {
logrus.Debugf("detected pre-hook command for %s: %s", fullServiceName, preHookCmd)
rsConfig.preHookCmd = preHookCmd
}
if postHookCmd, ok := service.Deploy.Labels["backupbot.restore.post-hook"]; ok {
logrus.Debugf("detected post-hook command for %s: %s", fullServiceName, postHookCmd)
rsConfig.postHookCmd = postHookCmd
}
restoreConfigs[service.Name] = rsConfig
}
}
}
rsConfig, ok := restoreConfigs[serviceName]
if !ok {
rsConfig = restoreConfig{}
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
if err := runRestore(cl, app, backupPath, serviceName, rsConfig); err != nil {
logrus.Fatal(err)
}
return nil
},
}
// runRestore does the actual restore logic.
func runRestore(cl *dockerClient.Client, app config.App, backupPath, serviceName string, rsConfig restoreConfig) error {
// FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli()
if err != nil {
return err
}
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, true)
if err != nil {
return err
}
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
if rsConfig.preHookCmd != "" {
splitCmd := internal.SafeSplit(rsConfig.preHookCmd)
logrus.Debugf("split pre-hook command for %s into %s", fullServiceName, splitCmd)
preHookExecOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: splitCmd,
Detach: false,
Tty: true,
}
if err := container.RunExec(dcli, cl, targetContainer.ID, &preHookExecOpts); err != nil {
return err
}
logrus.Infof("succesfully ran %s pre-hook command: %s", fullServiceName, rsConfig.preHookCmd)
}
backupReader, err := os.Open(backupPath)
if err != nil {
return err
}
content, err := archive.DecompressStream(backupReader)
if err != nil {
return err
}
// NOTE(d1): we use absolute paths so tar knows what to do. it will restore
// files according to the paths set in the compressed archive
restorePath := "/"
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(context.Background(), targetContainer.ID, restorePath, content, copyOpts); err != nil {
return err
}
logrus.Infof("restored %s to %s", backupPath, fullServiceName)
if rsConfig.postHookCmd != "" {
splitCmd := internal.SafeSplit(rsConfig.postHookCmd)
logrus.Debugf("split post-hook command for %s into %s", fullServiceName, splitCmd)
postHookExecOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: splitCmd,
Detach: false,
Tty: true,
}
if err := container.RunExec(dcli, cl, targetContainer.ID, &postHookExecOpts); err != nil {
return err
}
logrus.Infof("succesfully ran %s post-hook command: %s", fullServiceName, rsConfig.postHookCmd)
}
return nil
}

View File

@ -1,125 +1,203 @@
package app
import (
"context"
"fmt"
"coopcloud.tech/abra/pkg/catalogue"
stack "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var appRollbackCommand = &cli.Command{
var appRollbackCommand = cli.Command{
Name: "rollback",
Aliases: []string{"rl"},
Usage: "Roll an app back to a previous version",
Aliases: []string{"r", "downgrade"},
ArgsUsage: "<app>",
ArgsUsage: "<domain> [<version>]",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
internal.ChaosFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `
This command rolls an app back to a previous version if one exists.
You may pass "--force/-f" to downgrade to the same version again. This can be
useful if the container runtime has gotten into a weird state or your doing
some live hacking.
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
data beforehand - see "abra app backup <app>" for more.
data beforehand.
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
recipes.
`,
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
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 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)
}
}
r, err := recipe.Get(app.Recipe, internal.Offline)
if err != nil {
logrus.Fatal(err)
}
if err := lint.LintForErrors(r); err != nil {
logrus.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("checking whether '%s' is already deployed", stackName)
logrus.Debugf("checking whether %s is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
logrus.Fatal(err)
}
if deployedVersion == "" {
logrus.Fatalf("failed to determine version of deployed '%s'", app.Name)
}
if !isDeployed {
logrus.Fatalf("'%s' is not deployed?", app.Name)
logrus.Fatalf("%s is not deployed?", app.Name)
}
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type)
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
logrus.Fatal(err)
}
versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil {
logrus.Fatal(err)
}
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)
}
}
}
var availableDowngrades []string
for _, version := range versions {
if deployedVersion == "unknown" {
availableDowngrades = versions
logrus.Warnf("failed to determine deployed version of %s", app.Name)
}
if specificVersion != "" {
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
if err != nil {
logrus.Fatal(err)
}
parsedVersion, err := tagcmp.Parse(version)
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil {
logrus.Fatal(err)
}
if parsedVersion != parsedDeployedVersion && parsedVersion.IsLessThan(parsedDeployedVersion) {
availableDowngrades = append(availableDowngrades, version)
if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) || parsedSpecificVersion.Equals(parsedDeployedVersion) {
logrus.Fatalf("%s is not a downgrade for %s?", deployedVersion, specificVersion)
}
availableDowngrades = append(availableDowngrades, specificVersion)
}
if len(availableDowngrades) == 0 {
logrus.Fatal("no available downgrades, you're on latest")
}
if deployedVersion != "unknown" && !internal.Chaos && specificVersion == "" {
for _, version := range versions {
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
if err != nil {
logrus.Fatal(err)
}
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
logrus.Fatal(err)
}
if parsedVersion.IsLessThan(parsedDeployedVersion) && !(parsedVersion.Equals(parsedDeployedVersion)) {
availableDowngrades = append(availableDowngrades, version)
}
}
// FIXME: jeezus golang why do you not have a list reverse function
for i, j := 0, len(availableDowngrades)-1; i < j; i, j = i+1, j-1 {
availableDowngrades[i], availableDowngrades[j] = availableDowngrades[j], availableDowngrades[i]
if len(availableDowngrades) == 0 && !internal.Force {
logrus.Info("no available downgrades, you're on oldest ✌️")
return nil
}
}
var chosenDowngrade string
if !internal.Force {
prompt := &survey.Select{
Message: fmt.Sprintf("Please select a downgrade (current version: '%s'):", deployedVersion),
Options: availableDowngrades,
}
if err := survey.AskOne(prompt, &chosenDowngrade); err != nil {
return err
if len(availableDowngrades) > 0 && !internal.Chaos {
if internal.Force || internal.NoInput || specificVersion != "" {
chosenDowngrade = availableDowngrades[len(availableDowngrades)-1]
logrus.Debugf("choosing %s as version to downgrade to (--force/--no-input)", chosenDowngrade)
} else {
prompt := &survey.Select{
Message: fmt.Sprintf("Please select a downgrade (current version: %s):", deployedVersion),
Options: internal.ReverseStringList(availableDowngrades),
}
if err := survey.AskOne(prompt, &chosenDowngrade); err != nil {
return err
}
}
}
if internal.Force {
chosenDowngrade = availableDowngrades[0]
logrus.Debugf("choosing '%s' as version to downgrade to", chosenDowngrade)
if !internal.Chaos {
if err := recipe.EnsureVersion(app.Recipe, chosenDowngrade); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureVersion(app.Type, chosenDowngrade); err != nil {
logrus.Fatal(err)
if internal.Chaos {
logrus.Warn("chaos mode engaged")
var err error
chosenDowngrade, err = recipe.ChaosVersion(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh")
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
logrus.Fatal(err)
@ -128,7 +206,7 @@ data beforehand - see "abra app backup <app>" for more.
app.Env[k] = v
}
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil {
logrus.Fatal(err)
}
@ -142,14 +220,18 @@ data beforehand - see "abra app backup <app>" for more.
if err != nil {
logrus.Fatal(err)
}
config.ExposeAllEnv(stackName, compose, app.Env)
config.SetRecipeLabel(compose, stackName, app.Recipe)
config.SetChaosLabel(compose, stackName, internal.Chaos)
config.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
config.SetUpdateLabel(compose, stackName, app.Env)
if !internal.Force {
if err := NewVersionOverview(app, deployedVersion, chosenDowngrade); err != nil {
logrus.Fatal(err)
}
// NOTE(d1): no release notes implemeneted for rolling back
if err := internal.NewVersionOverview(app, deployedVersion, chosenDowngrade, ""); err != nil {
logrus.Fatal(err)
}
if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil {
logrus.Fatal(err)
}

View File

@ -1,51 +1,55 @@
package app
import (
"context"
"errors"
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/client/container"
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var user string
var userFlag = &cli.StringFlag{
Name: "user",
Name: "user, u",
Value: "",
Destination: &user,
}
var noTTY bool
var noTTYFlag = &cli.BoolFlag{
Name: "no-tty",
Value: false,
Name: "no-tty, t",
Destination: &noTTY,
}
var appRunCommand = &cli.Command{
Name: "run",
var appRunCommand = cli.Command{
Name: "run",
Aliases: []string{"r"},
Flags: []cli.Flag{
internal.DebugFlag,
noTTYFlag,
userFlag,
},
Aliases: []string{"r"},
ArgsUsage: "<service> <args>...",
Usage: "Run a command in a service container",
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> <service> <args>...",
Usage: "Run a command in a service container",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if c.Args().Len() < 2 {
if len(c.Args()) < 2 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <service> provided?"))
}
if c.Args().Len() < 3 {
if len(c.Args()) < 3 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <args> provided?"))
}
@ -55,18 +59,16 @@ var appRunCommand = &cli.Command{
}
serviceName := c.Args().Get(1)
stackAndServiceName := fmt.Sprintf("^%s_%s", app.StackName(), serviceName)
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("%s_%s", app.StackName(), serviceName))
filters.Add("name", stackAndServiceName)
containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, false)
if err != nil {
logrus.Fatal(err)
}
if len(containers) > 1 {
logrus.Fatalf("expected 1 container but got %d", len(containers))
}
cmd := c.Args().Slice()[2:]
cmd := c.Args()[2:]
execCreateOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
@ -83,41 +85,16 @@ var appRunCommand = &cli.Command{
execCreateOpts.Tty = false
}
// FIXME: an absolutely monumental hack to instantiate another command-line
// client withing our command-line client so that we pass something down
// the tubes that satisfies the necessary interface requirements. We should
// refactor our vendored container code to not require all this cruft. For
// now, It Works.
// FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli()
if err != nil {
logrus.Fatal(err)
}
if err := container.RunExec(dcli, cl, containers[0].ID, &execCreateOpts); err != nil {
if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
logrus.Fatal(err)
}
return nil
},
BashComplete: func(c *cli.Context) {
switch c.NArg() {
case 0:
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
for _, a := range appNames {
fmt.Println(a)
}
case 1:
appName := c.Args().First()
serviceNames, err := config.GetAppServiceNames(appName)
if err != nil {
logrus.Warn(err)
}
for _, s := range serviceNames {
fmt.Println(s)
}
}
},
}

View File

@ -1,41 +1,82 @@
package app
import (
"context"
"errors"
"fmt"
"os"
"strconv"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var allSecrets bool
var allSecretsFlag = &cli.BoolFlag{
Name: "all",
Aliases: []string{"A"},
Value: false,
Destination: &allSecrets,
Usage: "Generate all secrets",
}
var (
allSecrets bool
allSecretsFlag = &cli.BoolFlag{
Name: "all, a",
Destination: &allSecrets,
Usage: "Generate all secrets",
}
)
var appSecretGenerateCommand = &cli.Command{
var (
rmAllSecrets bool
rmAllSecretsFlag = &cli.BoolFlag{
Name: "all, a",
Destination: &rmAllSecrets,
Usage: "Remove all secrets",
}
)
var appSecretGenerateCommand = cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate secrets",
ArgsUsage: "<secret> <version>",
Flags: []cli.Flag{allSecretsFlag, internal.PassFlag},
ArgsUsage: "<domain> <secret> <version>",
Flags: []cli.Flag{
internal.DebugFlag,
allSecretsFlag,
internal.PassFlag,
internal.MachineReadableFlag,
internal.OfflineFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if c.Args().Len() == 1 && !allSecrets {
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)
}
}
if len(c.Args()) == 1 && !allSecrets {
err := errors.New("missing arguments <secret>/<version> or '--all'")
internal.ShowSubcommandHelpAndError(c, err)
}
@ -45,33 +86,42 @@ var appSecretGenerateCommand = &cli.Command{
internal.ShowSubcommandHelpAndError(c, err)
}
secretsToCreate := make(map[string]string)
secretEnvVars := secret.ReadSecretEnvVars(app.Env)
if allSecrets {
secretsToCreate = secretEnvVars
} else {
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil {
logrus.Fatal(err)
}
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
if err != nil {
logrus.Fatal(err)
}
if !allSecrets {
secretName := c.Args().Get(1)
secretVersion := c.Args().Get(2)
matches := false
for sec := range secretEnvVars {
parsed := secret.ParseSecretEnvVarName(sec)
if secretName == parsed {
secretsToCreate[sec] = secretVersion
}
s, ok := secrets[secretName]
if !ok {
logrus.Fatalf("%s doesn't exist in the env config?", secretName)
}
if !matches {
logrus.Fatalf("'%s' doesn't exist in the env config?", secretName)
s.Version = secretVersion
secrets = map[string]secret.Secret{
secretName: s,
}
}
secretVals, err := secret.GenerateSecrets(secretsToCreate, app.StackName(), app.Server)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
secretVals, err := secret.GenerateSecrets(cl, secrets, app.Server)
if err != nil {
logrus.Fatal(err)
}
if internal.Pass {
for name, data := range secretVals {
if err := secret.PassInsertSecret(data, name, app.StackName(), app.Server); err != nil {
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
logrus.Fatal(err)
}
}
@ -83,41 +133,70 @@ var appSecretGenerateCommand = &cli.Command{
}
tableCol := []string{"name", "value"}
table := abraFormatter.CreateTable(tableCol)
table := formatter.CreateTable(tableCol)
for name, val := range secretVals {
table.Append([]string{name, val})
}
table.Render()
logrus.Warn("generated secrets are not shown again, please take note of them *now*")
if internal.MachineReadable {
table.JSONRender()
} else {
table.Render()
}
logrus.Warn("generated secrets are not shown again, please take note of them NOW")
return nil
},
}
var appSecretInsertCommand = &cli.Command{
Name: "insert",
Aliases: []string{"i"},
Usage: "Insert secret",
Flags: []cli.Flag{internal.PassFlag},
ArgsUsage: "<secret> <version> <data>",
var appSecretInsertCommand = cli.Command{
Name: "insert",
Aliases: []string{"i"},
Usage: "Insert secret",
Flags: []cli.Flag{
internal.DebugFlag,
internal.PassFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> <secret-name> <version> <data>",
BashComplete: autocomplete.AppNameComplete,
Description: `
This command inserts a secret into an app environment.
This can be useful when you want to manually generate secrets for an app
environment. Typically, you can let Abra generate them for you on app creation
(see "abra app new --secrets" for more).
Example:
abra app secret insert myapp db_pass v1 mySecretPassword
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if c.Args().Len() != 4 {
if len(c.Args()) != 4 {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments?"))
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
name := c.Args().Get(1)
version := c.Args().Get(2)
data := c.Args().Get(3)
secretName := fmt.Sprintf("%s_%s_%s", app.StackName(), name, version)
if err := client.StoreSecret(secretName, data, app.Server); err != nil {
if err := client.StoreSecret(cl, secretName, data, app.Server); err != nil {
logrus.Fatal(err)
}
logrus.Infof("%s successfully stored on server", secretName)
if internal.Pass {
if err := secret.PassInsertSecret(data, name, app.StackName(), app.Server); err != nil {
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
logrus.Fatal(err)
}
}
@ -126,20 +205,85 @@ var appSecretInsertCommand = &cli.Command{
},
}
var appSecretRmCommand = &cli.Command{
Name: "remove",
Usage: "Remove a secret",
Aliases: []string{"rm"},
Flags: []cli.Flag{allSecretsFlag, internal.PassFlag},
ArgsUsage: "<secret>",
// secretRm removes a secret.
func secretRm(cl *dockerClient.Client, app config.App, secretName, parsed string) error {
if err := cl.SecretRemove(context.Background(), secretName); err != nil {
return err
}
logrus.Infof("deleted %s successfully from server", secretName)
if internal.PassRemove {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
return err
}
logrus.Infof("deleted %s successfully from local pass store", secretName)
}
return nil
}
var appSecretRmCommand = cli.Command{
Name: "remove",
Aliases: []string{"rm"},
Usage: "Remove a secret",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
rmAllSecretsFlag,
internal.PassRemoveFlag,
internal.OfflineFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> [<secret-name>]",
BashComplete: autocomplete.AppNameComplete,
Description: `
This command removes app secrets.
Example:
abra app secret remove myapp db_pass
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if c.Args().Get(1) != "" && allSecrets {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<secret>' and '--all' together"))
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if c.Args().Get(1) == "" && !allSecrets {
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)
}
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
if err != nil {
logrus.Fatal(err)
}
if c.Args().Get(1) != "" && rmAllSecrets {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<secret-name>' and '--all' together"))
}
if c.Args().Get(1) == "" && !rmAllSecrets {
internal.ShowSubcommandHelpAndError(c, errors.New("no secret(s) specified?"))
}
@ -148,63 +292,12 @@ var appSecretRmCommand = &cli.Command{
logrus.Fatal(err)
}
filters := filters.NewArgs()
filters.Add("name", app.StackName())
secretList, err := cl.SecretList(c.Context, types.SecretListOptions{Filters: filters})
filters, err := app.Filters(false, false)
if err != nil {
logrus.Fatal(err)
}
secretToRm := c.Args().Get(1)
for _, cont := range secretList {
secretName := cont.Spec.Annotations.Name
parsed := secret.ParseGeneratedSecretName(secretName, app)
if allSecrets {
if err := cl.SecretRemove(c.Context, secretName); err != nil {
logrus.Fatal(err)
}
if internal.Pass {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
logrus.Fatal(err)
}
}
} else {
if parsed == secretToRm {
if err := cl.SecretRemove(c.Context, secretName); err != nil {
logrus.Fatal(err)
}
if internal.Pass {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
logrus.Fatal(err)
}
}
}
}
}
return nil
},
}
var appSecretLsCommand = &cli.Command{
Name: "list",
Usage: "List all secrets",
Aliases: []string{"ls"},
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
secrets := secret.ReadSecretEnvVars(app.Env)
tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"}
table := abraFormatter.CreateTable(tableCol)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
filters := filters.NewArgs()
filters.Add("name", app.StackName())
secretList, err := cl.SecretList(c.Context, types.SecretListOptions{Filters: filters})
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters})
if err != nil {
logrus.Fatal(err)
}
@ -214,44 +307,119 @@ var appSecretLsCommand = &cli.Command{
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)
match := false
secretToRm := c.Args().Get(1)
for secretName, val := range secrets {
secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, val.Version)
if _, ok := remoteSecretNames[secretRemoteName]; ok {
createdRemote = true
if secretToRm != "" {
if secretName == secretToRm {
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
logrus.Fatal(err)
}
return nil
}
} else {
match = true
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
logrus.Fatal(err)
}
}
}
tableRow := []string{secretName, secVal.Version, secretRemoteName, strconv.FormatBool(createdRemote)}
table.Append(tableRow)
}
table.Render()
if !match && secretToRm != "" {
logrus.Fatalf("%s doesn't exist on server?", secretToRm)
}
if !match {
logrus.Fatal("no secrets to remove?")
}
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}
var appSecretCommand = &cli.Command{
var appSecretLsCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
internal.ChaosFlag,
internal.MachineReadableFlag,
},
Before: internal.SubCommandBefore,
Usage: "List all secrets",
BashComplete: autocomplete.AppNameComplete,
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 := 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)
if err != nil {
logrus.Fatal(err)
}
tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"}
table := formatter.CreateTable(tableCol)
secStats, err := secret.PollSecretsStatus(cl, app)
if err != nil {
logrus.Fatal(err)
}
for _, secStat := range secStats {
tableRow := []string{
secStat.LocalName,
secStat.Version,
secStat.RemoteName,
strconv.FormatBool(secStat.CreatedOnRemote),
}
table.Append(tableRow)
}
if table.NumLines() > 0 {
if internal.MachineReadable {
table.JSONRender()
} else {
table.Render()
}
} else {
logrus.Warnf("no secrets stored for %s", app.Name)
}
return nil
},
}
var appSecretCommand = cli.Command{
Name: "secret",
Aliases: []string{"s"},
Usage: "Manage app secrets",
ArgsUsage: "<command>",
Subcommands: []*cli.Command{
ArgsUsage: "<domain>",
Subcommands: []cli.Command{
appSecretGenerateCommand,
appSecretInsertCommand,
appSecretRmCommand,

80
cli/app/services.go Normal file
View File

@ -0,0 +1,80 @@
package app
import (
"context"
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/service"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var appServicesCommand = cli.Command{
Name: "services",
Aliases: []string{"sr"},
Usage: "Display all services of an app",
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
cl, err := client.New(app.Server)
if err != nil {
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)
}
filters, err := app.Filters(true, true)
if err != nil {
logrus.Fatal(err)
}
containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters})
if err != nil {
logrus.Fatal(err)
}
tableCol := []string{"service name", "image"}
table := formatter.CreateTable(tableCol)
for _, container := range containers {
var containerNames []string
for _, containerName := range container.Names {
trimmed := strings.TrimPrefix(containerName, "/")
containerNames = append(containerNames, trimmed)
}
serviceShortName := service.ContainerToServiceName(container.Names, app.StackName())
serviceLongName := fmt.Sprintf("%s_%s", app.StackName(), serviceShortName)
tableRow := []string{
serviceLongName,
formatter.RemoveSha(container.Image),
}
table.Append(tableRow)
}
table.Render()
return nil
},
}

View File

@ -1,50 +1,138 @@
package app
import (
"context"
"fmt"
"time"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
stack "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var appUndeployCommand = &cli.Command{
Name: "undeploy",
Aliases: []string{"u"},
Usage: "Undeploy an app",
var prune bool
var pruneFlag = &cli.BoolFlag{
Name: "prune, p",
Destination: &prune,
Usage: "Prunes unused containers, networks, and dangling images for an app",
}
// pruneApp runs the equivalent of a "docker system prune" but only filtering
// against resources connected with the app deployment. It is not a system wide
// prune. Volumes are not pruned to avoid unwated data loss.
func pruneApp(c *cli.Context, cl *dockerClient.Client, app config.App) error {
stackName := app.StackName()
ctx := context.Background()
for {
logrus.Debugf("polling for %s stack, waiting to be undeployed...", stackName)
services, err := stack.GetStackServices(ctx, cl, stackName)
if err != nil {
return err
}
if len(services) == 0 {
logrus.Debugf("%s undeployed, moving on with pruning logic", stackName)
time.Sleep(time.Second) // give runtime more time to tear down related state
break
}
time.Sleep(time.Second)
}
pruneFilters := filters.NewArgs()
stackSearch := fmt.Sprintf("%s*", stackName)
pruneFilters.Add("label", stackSearch)
cr, err := cl.ContainersPrune(ctx, pruneFilters)
if err != nil {
return err
}
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
logrus.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed)
nr, err := cl.NetworksPrune(ctx, pruneFilters)
if err != nil {
return err
}
logrus.Infof("networks pruned: %d", len(nr.NetworksDeleted))
ir, err := cl.ImagesPrune(ctx, pruneFilters)
if err != nil {
return err
}
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
logrus.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed)
return nil
}
var appUndeployCommand = cli.Command{
Name: "undeploy",
Aliases: []string{"un"},
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
pruneFlag,
},
Before: internal.SubCommandBefore,
Usage: "Undeploy an app",
BashComplete: autocomplete.AppNameComplete,
Description: `
This does not destroy any of the application data. However, you should remain
vigilant, as your swarm installation will consider any previously attached
volumes as eligiblef or pruning once undeployed.
This does not destroy any of the application data.
However, you should remain vigilant, as your swarm installation will consider
any previously attached volumes as eligible for pruning once undeployed.
Passing "-p/--prune" does not remove those volumes.
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName()
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
rmOpts := stack.Remove{Namespaces: []string{app.StackName()}}
if err := stack.RunRemove(c.Context, cl, rmOpts); err != nil {
logrus.Debugf("checking whether %s is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
logrus.Fatal(err)
}
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
if err := internal.DeployOverview(app, deployedVersion, "continue with undeploy?"); err != nil {
logrus.Fatal(err)
}
rmOpts := stack.Remove{Namespaces: []string{app.StackName()}}
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
logrus.Fatal(err)
}
if prune {
if err := pruneApp(c, cl, app); err != nil {
logrus.Fatal(err)
}
}
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}

View File

@ -1,112 +1,235 @@
package app
import (
"context"
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
stack "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var appUpgradeCommand = &cli.Command{
var appUpgradeCommand = cli.Command{
Name: "upgrade",
Aliases: []string{"u"},
Aliases: []string{"up"},
Usage: "Upgrade an app",
ArgsUsage: "<app>",
ArgsUsage: "<domain> [<version>]",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
internal.ChaosFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `
This command supports upgrading an app. You can use it to choose and roll out a
new upgrade to an existing app.
Upgrade an app. You can use it to choose and roll out a new upgrade to an
existing app.
This command specifically supports changing the version of running apps, as
opposed to "abra app deploy <app>" which will not change the version of a
This command specifically supports incrementing the version of running apps, as
opposed to "abra app deploy <domain>" which will not change the version of a
deployed app.
You may pass "--force/-f" to upgrade to the same version again. This can be
useful if the container runtime has gotten into a weird state or your doing
some live hacking.
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
data beforehand - see "abra app backup <app>" for more.
data beforehand.
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
recipes.
`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
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)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("checking whether '%s' is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
logrus.Fatal(err)
}
if deployedVersion == "" {
logrus.Fatalf("failed to determine version of deployed '%s'", app.Name)
}
if !isDeployed {
logrus.Fatalf("'%s' is not deployed?", app.Name)
logrus.Fatalf("%s is not deployed?", app.Name)
}
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type)
catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
if err != nil {
logrus.Fatal(err)
}
versions, err := recipePkg.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil {
logrus.Fatal(err)
}
if len(versions) == 0 && !internal.Chaos {
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
for _, version := range versions {
if deployedVersion == "unknown" {
availableUpgrades = versions
logrus.Warnf("failed to determine deployed version of %s", app.Name)
}
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.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)
if err != nil {
logrus.Fatal(err)
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) && !(parsedVersion.Equals(parsedDeployedVersion)) {
availableUpgrades = append(availableUpgrades, version)
}
}
if len(availableUpgrades) == 0 && !internal.Force {
logrus.Infof("no available upgrades, you're on latest (%s) ✌️", deployedVersion)
return nil
}
}
var chosenUpgrade string
if len(availableUpgrades) > 0 && !internal.Chaos {
if internal.Force || internal.NoInput || specificVersion != "" {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
logrus.Debugf("choosing %s as version to upgrade to", chosenUpgrade)
} else {
prompt := &survey.Select{
Message: fmt.Sprintf("Please select an upgrade (current version: %s):", deployedVersion),
Options: internal.ReverseStringList(availableUpgrades),
}
if err := survey.AskOne(prompt, &chosenUpgrade); err != nil {
return err
}
}
}
if internal.Force && chosenUpgrade == "" {
logrus.Warnf("%s is already upgraded to latest but continuing (--force/--chaos)", app.Name)
chosenUpgrade = deployedVersion
}
// 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
// when we obviously will forget to write release notes before publishing
var releaseNotes string
for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
logrus.Fatal(err)
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) {
availableUpgrades = append(availableUpgrades, version)
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 {
return err
}
if note != "" {
releaseNotes += fmt.Sprintf("%s\n", note)
}
}
}
if len(availableUpgrades) == 0 {
logrus.Fatal("no available upgrades, you're on latest")
}
var chosenUpgrade string
if !internal.Force {
prompt := &survey.Select{
Message: fmt.Sprintf("Please select an upgrade (current version: '%s'):", deployedVersion),
Options: availableUpgrades,
}
if err := survey.AskOne(prompt, &chosenUpgrade); err != nil {
return err
if !internal.Chaos {
if err := recipePkg.EnsureVersion(app.Recipe, chosenUpgrade); err != nil {
logrus.Fatal(err)
}
}
if internal.Force {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
logrus.Debugf("choosing '%s' as version to upgrade to", chosenUpgrade)
if internal.Chaos {
logrus.Warn("chaos mode engaged")
var err error
chosenUpgrade, err = recipePkg.ChaosVersion(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureVersion(app.Type, chosenUpgrade); err != nil {
logrus.Fatal(err)
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh")
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
logrus.Fatal(err)
@ -115,7 +238,7 @@ data beforehand - see "abra app backup <app>" for more.
app.Env[k] = v
}
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil {
logrus.Fatal(err)
}
@ -129,29 +252,45 @@ data beforehand - see "abra app backup <app>" for more.
if err != nil {
logrus.Fatal(err)
}
config.ExposeAllEnv(stackName, compose, app.Env)
config.SetRecipeLabel(compose, stackName, app.Recipe)
config.SetChaosLabel(compose, stackName, internal.Chaos)
config.SetChaosVersionLabel(compose, stackName, chosenUpgrade)
config.SetUpdateLabel(compose, stackName, app.Env)
if !internal.Force {
if err := NewVersionOverview(app, deployedVersion, chosenUpgrade); err != nil {
logrus.Fatal(err)
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 := stack.RunDeploy(cl, deployOpts, compose); err != nil {
if err := internal.NewVersionOverview(app, deployedVersion, chosenUpgrade, releaseNotes); err != nil {
logrus.Fatal(err)
}
stack.WaitTimeout, err = config.GetTimeoutFromLabel(compose, stackName)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil {
logrus.Fatal(err)
}
postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"]
if ok && !internal.DontWaitConverge {
logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds)
if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
logrus.Fatalf("attempting to run post deploy commands, saw: %s", err)
}
}
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}

View File

@ -1,43 +1,59 @@
package app
import (
"fmt"
"strings"
"context"
"sort"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/distribution/reference"
"github.com/olekukonko/tablewriter"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"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
func getImagePath(image string) (string, error) {
img, err := reference.ParseNormalizedNamed(image)
if err != nil {
return "", err
}
path := reference.Path(img)
if strings.Contains(path, "library") {
path = strings.Split(path, "/")[1]
}
logrus.Debugf("parsed '%s' from '%s'", path, image)
path = formatter.StripTagMeta(path)
logrus.Debugf("parsed %s from %s", path, image)
return path, nil
}
var appVersionCommand = &cli.Command{
Name: "version",
Aliases: []string{"v"},
Usage: "Show app versions",
Description: `
This command shows 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.
`,
var appVersionCommand = cli.Command{
Name: "version",
Aliases: []string{"v"},
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Usage: "Show version info of a deployed app",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName()
@ -47,27 +63,27 @@ Cloud recipe version.
logrus.Fatal(err)
}
logrus.Debugf("checking whether '%s' is already deployed", stackName)
logrus.Debugf("checking whether %s is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
logrus.Fatal(err)
}
if deployedVersion == "" {
logrus.Fatalf("failed to determine version of deployed '%s'", app.Name)
}
if !isDeployed {
logrus.Fatalf("'%s' is not deployed?", app.Name)
logrus.Fatalf("%s is not deployed?", app.Name)
}
recipeMeta, err := catalogue.GetRecipeMeta(app.Type)
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 {
logrus.Fatal(err)
}
versionsMeta := make(map[string]catalogue.ServiceMeta)
versionsMeta := make(map[string]recipe.ServiceMeta)
for _, recipeVersion := range recipeMeta.Versions {
if currentVersion, exists := recipeVersion[deployedVersion]; exists {
versionsMeta = currentVersion
@ -75,30 +91,27 @@ Cloud recipe version.
}
if len(versionsMeta) == 0 {
logrus.Fatalf("PANIC: could not retrieve deployed version ('%s') from recipe catalogue?", deployedVersion)
logrus.Fatalf("could not retrieve deployed version (%s) from recipe catalogue?", deployedVersion)
}
tableCol := []string{"name", "image", "version", "tag", "digest"}
table := abraFormatter.CreateTable(tableCol)
tableCol := []string{"version", "service", "image", "tag"}
table := formatter.CreateTable(tableCol)
var versions [][]string
for serviceName, versionMeta := range versionsMeta {
table.Append([]string{serviceName, versionMeta.Image, deployedVersion, versionMeta.Tag, versionMeta.Digest})
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()
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}

View File

@ -1,105 +1,156 @@
package app
import (
"fmt"
"context"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var appVolumeListCommand = &cli.Command{
Name: "list",
Usage: "List volumes associated with an app",
Aliases: []string{"ls"},
var appVolumeListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
Usage: "List volumes associated with an app",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
volumeList, err := client.GetVolumes(c.Context, app.Server, app.Name)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
table := abraFormatter.CreateTable([]string{"driver", "volume name"})
filters, err := app.Filters(false, true)
if err != nil {
logrus.Fatal(err)
}
volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
if err != nil {
logrus.Fatal(err)
}
table := formatter.CreateTable([]string{"name", "created", "mounted"})
var volTable [][]string
for _, volume := range volumeList {
volRow := []string{
volume.Driver,
volume.Name,
}
volRow := []string{volume.Name, volume.CreatedAt, volume.Mountpoint}
volTable = append(volTable, volRow)
}
table.AppendBulk(volTable)
table.Render()
if table.NumLines() > 0 {
table.Render()
} else {
logrus.Warnf("no volumes created for %s", app.Name)
}
return nil
},
}
var appVolumeRemoveCommand = &cli.Command{
Name: "remove",
Usage: "Remove volume(s) associated with an app",
Aliases: []string{"rm"},
var appVolumeRemoveCommand = cli.Command{
Name: "remove",
Usage: "Remove volume(s) associated with an app",
Description: `
This command supports removing volumes associated with an app. The app in
question must be undeployed before you try to remove volumes. See "abra app
undeploy <domain>" for more.
The command is interactive and will show a multiple select input which allows
you to make a seclection. Use the "?" key to see more help on navigating this
interface.
Passing "--force/-f" will select all volumes for removal. Be careful.
`,
ArgsUsage: "<domain>",
Aliases: []string{"rm"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
volumeList, err := client.GetVolumes(c.Context, app.Server, app.Name)
cl, err := client.New(app.Server)
if err != nil {
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)
if err != nil {
logrus.Fatal(err)
}
volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
if err != nil {
logrus.Fatal(err)
}
volumeNames := client.GetVolumeNames(volumeList)
var volumesToRemove []string
if !internal.Force {
if !internal.Force && !internal.NoInput {
volumesPrompt := &survey.MultiSelect{
Message: "which volumes do you want to remove?",
Help: "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled",
VimMode: true,
Options: volumeNames,
Default: volumeNames,
}
if err := survey.AskOne(volumesPrompt, &volumesToRemove); err != nil {
logrus.Fatal(err)
}
} else {
}
if internal.Force || internal.NoInput {
volumesToRemove = volumeNames
}
err = client.RemoveVolumes(c.Context, app.Server, volumesToRemove, internal.Force)
if err != nil {
logrus.Fatal(err)
}
if len(volumesToRemove) > 0 {
err = client.RemoveVolumes(cl, context.Background(), app.Server, volumesToRemove, internal.Force)
if err != nil {
logrus.Fatal(err)
}
logrus.Info("volumes removed successfully")
logrus.Info("volumes removed successfully")
} else {
logrus.Info("no volumes removed")
}
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}
var appVolumeCommand = &cli.Command{
var appVolumeCommand = cli.Command{
Name: "volume",
Aliases: []string{"vl"},
Usage: "Manage app volumes",
ArgsUsage: "<command>",
Subcommands: []*cli.Command{
ArgsUsage: "<domain>",
Subcommands: []cli.Command{
appVolumeListCommand,
appVolumeRemoveCommand,
},

View File

@ -1,17 +1,223 @@
package catalogue
import (
"github.com/urfave/cli/v2"
"encoding/json"
"fmt"
"io/ioutil"
"path"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/recipe"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var catalogueGenerateCommand = cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate the recipe catalogue",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.PublishFlag,
internal.DryFlag,
internal.SkipUpdatesFlag,
internal.ChaosFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `
Generate a new copy of the recipe catalogue which can be found on:
https://recipes.coopcloud.tech (website that humans read)
https://recipes.coopcloud.tech/recipes.json (JSON that Abra reads)
It polls the entire git.coopcloud.tech/coop-cloud/... recipe repository
listing, parses README.md and git tags to produce recipe metadata which is
loaded into the catalogue JSON file.
It is possible to generate new metadata for a single recipe by passing
<recipe>. The existing local catalogue will be updated, not overwritten.
It is quite easy to get rate limited by Docker Hub when running this command.
If you have a Hub account you can have Abra log you in to avoid this. Pass
"--user" and "--pass".
Push your new release to git.coopcloud.tech with "-p/--publish". This requires
that you have permission to git push to these repositories and have your SSH
keys configured on your account.
`,
ArgsUsage: "[<recipe>]",
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
if recipeName != "" {
internal.ValidateRecipe(c)
}
if !internal.Chaos {
if err := catalogue.EnsureIsClean(); err != nil {
logrus.Fatal(err)
}
}
repos, err := recipe.ReadReposMetadata()
if err != nil {
logrus.Fatal(err)
}
var barLength int
var logMsg string
if recipeName != "" {
barLength = 1
logMsg = fmt.Sprintf("ensuring %v recipe is cloned & up-to-date", barLength)
} else {
barLength = len(repos)
logMsg = fmt.Sprintf("ensuring %v recipes are cloned & up-to-date, this could take some time...", barLength)
}
if !internal.SkipUpdates {
logrus.Warn(logMsg)
if err := recipe.UpdateRepositories(repos, recipeName); err != nil {
logrus.Fatal(err)
}
}
catl := make(recipe.RecipeCatalogue)
catlBar := formatter.CreateProgressbar(barLength, "generating catalogue metadata...")
for _, recipeMeta := range repos {
if recipeName != "" && recipeName != recipeMeta.Name {
catlBar.Add(1)
continue
}
versions, err := recipe.GetRecipeVersions(recipeMeta.Name, internal.Offline)
if err != nil {
logrus.Warn(err)
}
features, category, err := recipe.GetRecipeFeaturesAndCategory(recipeMeta.Name)
if err != nil {
logrus.Warn(err)
}
catl[recipeMeta.Name] = recipe.RecipeMeta{
Name: recipeMeta.Name,
Repository: recipeMeta.CloneURL,
SSHURL: recipeMeta.SSHURL,
Icon: recipeMeta.AvatarURL,
DefaultBranch: recipeMeta.DefaultBranch,
Description: recipeMeta.Description,
Website: recipeMeta.Website,
Versions: versions,
Category: category,
Features: features,
}
catlBar.Add(1)
}
recipesJSON, err := json.MarshalIndent(catl, "", " ")
if err != nil {
logrus.Fatal(err)
}
if recipeName == "" {
if err := ioutil.WriteFile(config.RECIPES_JSON, recipesJSON, 0764); err != nil {
logrus.Fatal(err)
}
} else {
catlFS, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
logrus.Fatal(err)
}
catlFS[recipeName] = catl[recipeName]
updatedRecipesJSON, err := json.MarshalIndent(catlFS, "", " ")
if err != nil {
logrus.Fatal(err)
}
if err := ioutil.WriteFile(config.RECIPES_JSON, updatedRecipesJSON, 0764); err != nil {
logrus.Fatal(err)
}
}
logrus.Infof("generated new recipe catalogue in %s", config.RECIPES_JSON)
cataloguePath := path.Join(config.ABRA_DIR, "catalogue")
if internal.Publish {
isClean, err := gitPkg.IsClean(cataloguePath)
if err != nil {
logrus.Fatal(err)
}
if isClean {
if !internal.Dry {
logrus.Fatalf("no changes discovered in %s, nothing to publish?", cataloguePath)
}
}
msg := "chore: publish new catalogue release changes"
if err := gitPkg.Commit(cataloguePath, msg, internal.Dry); err != nil {
logrus.Fatal(err)
}
repo, err := git.PlainOpen(cataloguePath)
if err != nil {
logrus.Fatal(err)
}
sshURL := fmt.Sprintf(config.SSH_URL_TEMPLATE, config.CATALOGUE_JSON_REPO_NAME)
if err := gitPkg.CreateRemote(repo, "origin-ssh", sshURL, internal.Dry); err != nil {
logrus.Fatal(err)
}
if err := gitPkg.Push(cataloguePath, "origin-ssh", false, internal.Dry); err != nil {
logrus.Fatal(err)
}
}
repo, err := git.PlainOpen(cataloguePath)
if err != nil {
logrus.Fatal(err)
}
head, err := repo.Head()
if err != nil {
logrus.Fatal(err)
}
if !internal.Dry && internal.Publish {
url := fmt.Sprintf("%s/%s/commit/%s", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME, head.Hash())
logrus.Infof("new changes published: %s", url)
}
if internal.Dry {
logrus.Info("dry run: no changes published")
}
return nil
},
}
// CatalogueCommand defines the `abra catalogue` command and sub-commands.
var CatalogueCommand = &cli.Command{
var CatalogueCommand = cli.Command{
Name: "catalogue",
Usage: "Manage the recipe catalogue",
Aliases: []string{"c"},
ArgsUsage: "<recipe>",
Description: "This command helps recipe packagers interact with the recipe catalogue",
Subcommands: []*cli.Command{
Subcommands: []cli.Command{
catalogueGenerateCommand,
},
}

View File

@ -1,173 +0,0 @@
package catalogue
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/git"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
// CatalogueSkipList is all the repos that are not recipes.
var CatalogueSkipList = map[string]bool{
"abra": true,
"abra-bash": true,
"abra-apps": true,
"abra-aur": true,
"abra-capsul": true,
"abra-gandi": true,
"abra-hetzner": true,
"apps": true,
"aur-abra-git": true,
"auto-apps-json": true,
"auto-mirror": true,
"backup-bot": true,
"coopcloud.tech": true,
"coturn": true,
"docker-cp-deploy": true,
"docker-dind-bats-kcov": true,
"docs.coopcloud.tech": true,
"example": true,
"gardening": true,
"go-abra": true,
"organising": true,
"pyabra": true,
"radicle-seed-node": true,
"stack-ssh-deploy": true,
"swarm-cronjob": true,
"tagcmp": true,
"tyop": true,
}
var catalogueGenerateCommand = &cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate a new copy of the catalogue",
ArgsUsage: "[<recipe>]",
BashComplete: func(c *cli.Context) {
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for name := range catl {
fmt.Println(name)
}
},
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
repos, err := catalogue.ReadReposMetadata()
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("ensuring '%v' recipe(s) are locally present and up-to-date", len(repos))
retrieveBar := formatter.CreateProgressbar(len(repos), "retrieving recipes...")
ch := make(chan string, len(repos))
for _, repoMeta := range repos {
go func(rm catalogue.RepoMeta) {
if recipeName != "" && recipeName != rm.Name {
ch <- rm.Name
retrieveBar.Add(1)
return
}
if _, exists := CatalogueSkipList[rm.Name]; exists {
ch <- rm.Name
retrieveBar.Add(1)
return
}
recipeDir := path.Join(config.ABRA_DIR, "apps", rm.Name)
if err := git.Clone(recipeDir, rm.SSHURL); err != nil {
logrus.Fatal(err)
}
if err := git.EnsureUpToDate(recipeDir); err != nil {
logrus.Fatal(err)
}
ch <- rm.Name
retrieveBar.Add(1)
}(repoMeta)
}
for range repos {
<-ch // wait for everything
}
catl := make(catalogue.RecipeCatalogue)
catlBar := formatter.CreateProgressbar(len(repos), "generating catalogue...")
for _, recipeMeta := range repos {
if recipeName != "" && recipeName != recipeMeta.Name {
catlBar.Add(1)
continue
}
if _, exists := CatalogueSkipList[recipeMeta.Name]; exists {
catlBar.Add(1)
continue
}
versions, err := catalogue.GetRecipeVersions(recipeMeta.Name)
if err != nil {
logrus.Fatal(err)
}
catl[recipeMeta.Name] = catalogue.RecipeMeta{
Name: recipeMeta.Name,
Repository: recipeMeta.CloneURL,
Icon: recipeMeta.AvatarURL,
DefaultBranch: recipeMeta.DefaultBranch,
Description: recipeMeta.Description,
Website: recipeMeta.Website,
Versions: versions,
// Category: ..., // FIXME: once we sort out the machine-readable catalogue interface
// Features: ..., // FIXME: once we figure out the machine-readable catalogue interface
}
catlBar.Add(1)
}
recipesJSON, err := json.MarshalIndent(catl, "", " ")
if err != nil {
logrus.Fatal(err)
}
if _, err := os.Stat(config.APPS_JSON); err != nil && os.IsNotExist(err) {
if err := ioutil.WriteFile(config.APPS_JSON, recipesJSON, 0644); err != nil {
logrus.Fatal(err)
}
} else {
if recipeName != "" {
catlFS, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
catlFS[recipeName] = catl[recipeName]
updatedRecipesJSON, err := json.MarshalIndent(catlFS, "", " ")
if err != nil {
logrus.Fatal(err)
}
if err := ioutil.WriteFile(config.APPS_JSON, updatedRecipesJSON, 0644); err != nil {
logrus.Fatal(err)
}
}
}
logrus.Infof("generated new recipe catalogue in '%s'", config.APPS_JSON)
return nil
},
}

View File

@ -2,111 +2,204 @@
package cli
import (
"errors"
"fmt"
"os"
"os/exec"
"path"
"coopcloud.tech/abra/cli/app"
"coopcloud.tech/abra/cli/catalogue"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/cli/recipe"
"coopcloud.tech/abra/cli/server"
"coopcloud.tech/abra/pkg/autocomplete"
cataloguePkg "coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config"
logrusStack "github.com/Gurpartap/logrus-stack"
"coopcloud.tech/abra/pkg/web"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
// Verbose stores the variable from VerboseFlag.
var Verbose bool
// AutoCompleteCommand helps people set up auto-complete in their shells
var AutoCompleteCommand = cli.Command{
Name: "autocomplete",
Aliases: []string{"ac"},
Usage: "Configure shell autocompletion (recommended)",
Description: `
Set up auto-completion in your shell by downloading the relevant files and
laying out what additional information must be loaded. Supported shells are as
follows: bash, fish, fizsh & zsh.
// VerboseFlag turns on/off verbose logging down to the INFO level.
var VerboseFlag = &cli.BoolFlag{
Name: "verbose",
Aliases: []string{"V"},
Value: false,
Destination: &Verbose,
Usage: "Show INFO messages",
Example:
abra autocomplete bash
`,
ArgsUsage: "<shell>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Action: func(c *cli.Context) error {
shellType := c.Args().First()
if shellType == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no shell provided"))
}
supportedShells := map[string]bool{
"bash": true,
"zsh": true,
"fizsh": true,
"fish": true,
}
if _, ok := supportedShells[shellType]; !ok {
logrus.Fatalf("%s is not a supported shell right now, sorry", shellType)
}
if shellType == "fizsh" {
shellType = "zsh" // handled the same on the autocompletion side
}
autocompletionDir := path.Join(config.ABRA_DIR, "autocompletion")
if err := os.Mkdir(autocompletionDir, 0764); err != nil {
if !os.IsExist(err) {
logrus.Fatal(err)
}
logrus.Debugf("%s already created", autocompletionDir)
}
autocompletionFile := path.Join(config.ABRA_DIR, "autocompletion", shellType)
if _, err := os.Stat(autocompletionFile); err != nil && os.IsNotExist(err) {
url := fmt.Sprintf("https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/autocomplete/%s", shellType)
logrus.Infof("fetching %s", url)
if err := web.GetFile(autocompletionFile, url); err != nil {
logrus.Fatal(err)
}
}
switch shellType {
case "bash":
fmt.Println(fmt.Sprintf(`
# Run the following commands to install auto-completion
sudo mkdir /etc/bash_completion.d/
sudo cp %s /etc/bash_completion.d/abra
echo "source /etc/bash_completion.d/abra" >> ~/.bashrc
# To test, run the following: "abra app <hit tab key>" - you should see command completion!
`, autocompletionFile))
case "zsh":
fmt.Println(fmt.Sprintf(`
# Run the following commands to install auto-completion
sudo mkdir /etc/zsh/completion.d/
sudo cp %s /etc/zsh/completion.d/abra
echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc
# To test, run the following: "abra app <hit tab key>" - you should see command completion!
`, autocompletionFile))
case "fish":
fmt.Println(fmt.Sprintf(`
# Run the following commands to install auto-completion
sudo mkdir -p /etc/fish/completions
sudo cp %s /etc/fish/completions/abra
echo "source /etc/fish/completions/abra" >> ~/.config/fish/config.fish
# To test, run the following: "abra app <hit tab key>" - you should see command completion!
`, autocompletionFile))
}
return nil
},
}
// Debug stores the variable from DebugFlag.
var Debug bool
// UpgradeCommand upgrades abra in-place.
var UpgradeCommand = cli.Command{
Name: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade Abra itself",
Description: `
Upgrade Abra in-place with the latest stable or release candidate.
// DebugFlag turns on/off verbose logging down to the DEBUG level.
var DebugFlag = &cli.BoolFlag{
Name: "debug",
Aliases: []string{"d"},
Value: false,
Destination: &Debug,
Usage: "Show DEBUG messages",
Pass "-r/--rc" to install the latest release candidate. Please bear in mind
that it may contain catastrophic bugs. Thank you very much for the testing
efforts!
`,
Flags: []cli.Flag{internal.RCFlag},
Action: func(c *cli.Context) error {
mainURL := "https://install.abra.coopcloud.tech"
cmd := exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash", mainURL))
if internal.RC {
releaseCandidateURL := "https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer"
cmd = exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash -s -- --rc", releaseCandidateURL))
}
logrus.Debugf("attempting to run %s", cmd)
if err := internal.RunCmd(cmd); err != nil {
logrus.Fatal(err)
}
return nil
},
}
// RunApp runs CLI abra app.
func RunApp(version, commit string) {
func newAbraApp(version, commit string) *cli.App {
app := &cli.App{
Name: "abra",
Usage: `The Co-op Cloud command-line utility belt 🎩🐇
____ ____ _ _
/ ___|___ ___ _ __ / ___| | ___ _ _ __| |
| | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |
| |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |
\____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|
|_|
`,
`,
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Commands: []*cli.Command{
Commands: []cli.Command{
app.AppCommand,
server.ServerCommand,
recipe.RecipeCommand,
catalogue.CatalogueCommand,
VersionCommand,
UpgradeCommand,
AutoCompleteCommand,
},
Flags: []cli.Flag{
VerboseFlag,
DebugFlag,
},
Authors: []*cli.Author{
{
Name: "Autonomic Co-op",
Email: "helo@autonomic.zone",
},
},
BashComplete: autocomplete.SubcommandComplete,
}
app.EnableBashCompletion = true
app.Before = func(c *cli.Context) error {
if Debug {
logrus.SetLevel(logrus.DebugLevel)
logrus.SetFormatter(&logrus.TextFormatter{})
logrus.SetOutput(os.Stderr)
logrus.AddHook(logrusStack.StandardHook())
}
paths := []string{
config.ABRA_DIR,
path.Join(config.ABRA_DIR, "servers"),
path.Join(config.ABRA_DIR, "apps"),
path.Join(config.ABRA_DIR, "vendor"),
config.SERVERS_DIR,
config.RECIPES_DIR,
config.VENDOR_DIR,
config.BACKUP_DIR,
}
for _, path := range paths {
if err := os.Mkdir(path, 0755); err != nil {
if err := os.Mkdir(path, 0764); err != nil {
if !os.IsExist(err) {
logrus.Fatal(err)
}
logrus.Debugf("'%s' already created, moving on...", path)
continue
}
logrus.Debugf("'%s' is missing, creating...", path)
}
logrus.Debugf("abra version '%s', commit '%s'", version, commit)
if err := cataloguePkg.EnsureCatalogue(); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("abra version %s, commit %s", version, commit)
return nil
}
return app
}
// RunApp runs CLI abra app.
func RunApp(version, commit string) {
app := newAbraApp(version, commit)
if err := app.Run(os.Args); err != nil {
logrus.Fatal(err)
}

View File

@ -1,52 +0,0 @@
package formatter
import (
"fmt"
"os"
"strings"
"time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/go-units"
"github.com/olekukonko/tablewriter"
"github.com/schollz/progressbar/v3"
)
func ShortenID(str string) string {
return str[:12]
}
func Truncate(str string) string {
return fmt.Sprintf(`"%s"`, formatter.Ellipsis(str, 19))
}
// RemoveSha remove image sha from a string that are added in some docker outputs
func RemoveSha(str string) string {
return strings.Split(str, "@")[0]
}
// HumanDuration from docker/cli RunningFor() to be accessible outside of the class
func HumanDuration(timestamp int64) string {
date := time.Unix(timestamp, 0)
now := time.Now().UTC()
return units.HumanDuration(now.Sub(date)) + " ago"
}
// CreateTable prepares a table layout for output.
func CreateTable(columns []string) *tablewriter.Table {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader(columns)
return table
}
// CreateProgressbar generates a progress bar
func CreateProgressbar(length int, title string) *progressbar.ProgressBar {
return progressbar.NewOptions(
length,
progressbar.OptionClearOnFinish(),
progressbar.OptionSetPredictTime(false),
progressbar.OptionShowCount(),
progressbar.OptionFullWidth(),
progressbar.OptionSetDescription(title),
)
}

35
cli/internal/backup.go Normal file
View File

@ -0,0 +1,35 @@
package internal
import (
"strings"
)
// SafeSplit splits up a string into a list of commands safely.
func SafeSplit(s string) []string {
split := strings.Split(s, " ")
var result []string
var inquote string
var block string
for _, i := range split {
if inquote == "" {
if strings.HasPrefix(i, "'") || strings.HasPrefix(i, "\"") {
inquote = string(i[0])
block = strings.TrimPrefix(i, inquote) + " "
} else {
result = append(result, i)
}
} else {
if !strings.HasSuffix(i, inquote) {
block += i + " "
} else {
block += strings.TrimSuffix(i, inquote)
inquote = ""
result = append(result, block)
block = ""
}
}
}
return result
}

251
cli/internal/cli.go Normal file
View File

@ -0,0 +1,251 @@
package internal
import (
"os"
logrusStack "github.com/Gurpartap/logrus-stack"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// Secrets stores the variable from SecretsFlag
var Secrets bool
// SecretsFlag turns on/off automatically generating secrets
var SecretsFlag = &cli.BoolFlag{
Name: "secrets, S",
Usage: "Automatically generate secrets",
Destination: &Secrets,
}
// Pass stores the variable from PassFlag
var Pass bool
// PassFlag turns on/off storing generated secrets in pass
var PassFlag = &cli.BoolFlag{
Name: "pass, p",
Usage: "Store the generated secrets in a local pass store",
Destination: &Pass,
}
// PassRemove stores the variable for PassRemoveFlag
var PassRemove bool
// PassRemoveFlag turns on/off removing generated secrets from pass
var PassRemoveFlag = &cli.BoolFlag{
Name: "pass, p",
Usage: "Remove generated secrets from a local pass store",
Destination: &PassRemove,
}
// Force force functionality without asking.
var Force bool
// ForceFlag turns on/off force functionality.
var ForceFlag = &cli.BoolFlag{
Name: "force, f",
Usage: "Perform action without further prompt. Use with care!",
Destination: &Force,
}
// Chaos engages chaos mode.
var Chaos bool
// ChaosFlag turns on/off chaos functionality.
var ChaosFlag = &cli.BoolFlag{
Name: "chaos, C",
Usage: "Proceed with uncommitted recipes changes. Use with care!",
Destination: &Chaos,
}
// Disable tty to run commands from script
var Tty bool
// TtyFlag turns on/off tty mode.
var TtyFlag = &cli.BoolFlag{
Name: "tty, T",
Usage: "Disables TTY mode to run this command from a script.",
Destination: &Tty,
}
var NoInput bool
var NoInputFlag = &cli.BoolFlag{
Name: "no-input, n",
Usage: "Toggle non-interactive mode",
Destination: &NoInput,
}
// Debug stores the variable from DebugFlag.
var Debug bool
// DebugFlag turns on/off verbose logging down to the DEBUG level.
var DebugFlag = &cli.BoolFlag{
Name: "debug, d",
Destination: &Debug,
Usage: "Show DEBUG messages",
}
// Offline stores the variable from OfflineFlag.
var Offline bool
// DebugFlag turns on/off offline mode.
var OfflineFlag = &cli.BoolFlag{
Name: "offline, o",
Destination: &Offline,
Usage: "Prefer offline & filesystem access when possible",
}
// MachineReadable stores the variable from MachineReadableFlag
var MachineReadable bool
// MachineReadableFlag turns on/off machine readable output where supported
var MachineReadableFlag = &cli.BoolFlag{
Name: "machine, m",
Destination: &MachineReadable,
Usage: "Output in a machine-readable format (where supported)",
}
// RC signifies the latest release candidate
var RC bool
// RCFlag chooses the latest release candidate for install
var RCFlag = &cli.BoolFlag{
Name: "rc, r",
Destination: &RC,
Usage: "Install the latest release candidate",
}
var Major bool
var MajorFlag = &cli.BoolFlag{
Name: "major, x",
Usage: "Increase the major part of the version",
Destination: &Major,
}
var Minor bool
var MinorFlag = &cli.BoolFlag{
Name: "minor, y",
Usage: "Increase the minor part of the version",
Destination: &Minor,
}
var Patch bool
var PatchFlag = &cli.BoolFlag{
Name: "patch, z",
Usage: "Increase the patch part of the version",
Destination: &Patch,
}
var Dry bool
var DryFlag = &cli.BoolFlag{
Name: "dry-run, r",
Usage: "Only reports changes that would be made",
Destination: &Dry,
}
var Publish bool
var PublishFlag = &cli.BoolFlag{
Name: "publish, p",
Usage: "Publish changes to git.coopcloud.tech",
Destination: &Publish,
}
var Domain string
var DomainFlag = &cli.StringFlag{
Name: "domain, D",
Value: "",
Usage: "Choose a domain name",
Destination: &Domain,
}
var NewAppServer string
var NewAppServerFlag = &cli.StringFlag{
Name: "server, s",
Value: "",
Usage: "Show apps of a specific server",
Destination: &NewAppServer,
}
var NoDomainChecks bool
var NoDomainChecksFlag = &cli.BoolFlag{
Name: "no-domain-checks, D",
Usage: "Disable app domain sanity checks",
Destination: &NoDomainChecks,
}
var StdErrOnly bool
var StdErrOnlyFlag = &cli.BoolFlag{
Name: "stderr, s",
Usage: "Only tail stderr",
Destination: &StdErrOnly,
}
var SinceLogs string
var SinceLogsFlag = &cli.StringFlag{
Name: "since, S",
Value: "",
Usage: "tail logs since YYYY-MM-DDTHH:MM:SSZ",
Destination: &SinceLogs,
}
var DontWaitConverge bool
var DontWaitConvergeFlag = &cli.BoolFlag{
Name: "no-converge-checks, c",
Usage: "Don't wait for converge logic checks",
Destination: &DontWaitConverge,
}
var Watch bool
var WatchFlag = &cli.BoolFlag{
Name: "watch, w",
Usage: "Watch status by polling repeatedly",
Destination: &Watch,
}
var OnlyErrors bool
var OnlyErrorFlag = &cli.BoolFlag{
Name: "errors, e",
Usage: "Only show errors",
Destination: &OnlyErrors,
}
var SkipUpdates bool
var SkipUpdatesFlag = &cli.BoolFlag{
Name: "skip-updates, s",
Usage: "Skip updating recipe repositories",
Destination: &SkipUpdates,
}
var AllTags bool
var AllTagsFlag = &cli.BoolFlag{
Name: "all-tags, a",
Usage: "List all tags, not just upgrades",
Destination: &AllTags,
}
var LocalCmd bool
var LocalCmdFlag = &cli.BoolFlag{
Name: "local, l",
Usage: "Run command locally",
Destination: &LocalCmd,
}
var RemoteUser string
var RemoteUserFlag = &cli.StringFlag{
Name: "user, u",
Value: "",
Usage: "User to run command within a service context",
Destination: &RemoteUser,
}
// SubCommandBefore wires up pre-action machinery (e.g. --debug handling).
func SubCommandBefore(c *cli.Context) error {
if Debug {
logrus.SetLevel(logrus.DebugLevel)
logrus.SetFormatter(&logrus.TextFormatter{})
logrus.SetOutput(os.Stderr)
logrus.AddHook(logrusStack.StandardHook())
}
return nil
}

View File

@ -2,10 +2,109 @@ package internal
import (
"bufio"
"context"
"fmt"
"io/ioutil"
"os/exec"
"strings"
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/archive"
"github.com/sirupsen/logrus"
)
// RunCmdRemote executes an abra.sh command in the target service
func RunCmdRemote(cl *dockerClient.Client, app config.App, abraSh, serviceName, cmdName, cmdArgs string) error {
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, false)
if err != nil {
return err
}
logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(targetContainer.ID), app.Server)
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
content, err := archive.TarWithOptions(abraSh, toTarOpts)
if err != nil {
return err
}
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(context.Background(), targetContainer.ID, "/tmp", content, copyOpts); err != nil {
return err
}
// FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli()
if err != nil {
return err
}
shell := "/bin/bash"
findShell := []string{"test", "-e", shell}
execCreateOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: findShell,
Detach: false,
Tty: false,
}
if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
logrus.Infof("%s does not exist for %s, use /bin/sh as fallback", shell, app.Name)
shell = "/bin/sh"
}
var cmd []string
if cmdArgs != "" {
cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s %s", serviceName, app.Name, app.StackName(), cmdName, cmdArgs)}
} else {
cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s", serviceName, app.Name, app.StackName(), cmdName)}
}
logrus.Debugf("running command: %s", strings.Join(cmd, " "))
if RemoteUser != "" {
logrus.Debugf("running command with user %s", RemoteUser)
execCreateOpts.User = RemoteUser
}
execCreateOpts.Cmd = cmd
execCreateOpts.Tty = true
if Tty {
execCreateOpts.Tty = false
}
if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
return err
}
return nil
}
func EnsureCommand(abraSh, recipeName, execCmd string) error {
bytes, err := ioutil.ReadFile(abraSh)
if err != nil {
return err
}
if !strings.Contains(string(bytes), execCmd) {
return fmt.Errorf("%s doesn't have a %s function", recipeName, execCmd)
}
return nil
}
// RunCmd runs a shell command and streams stdout/stderr in real-time.
func RunCmd(cmd *exec.Cmd) error {
r, err := cmd.StdoutPipe()

View File

@ -1,51 +0,0 @@
package internal
import (
"github.com/urfave/cli/v2"
)
// Secrets stores the variable from SecretsFlag
var Secrets bool
// SecretsFlag turns on/off automatically generating secrets
var SecretsFlag = &cli.BoolFlag{
Name: "secrets",
Aliases: []string{"S"},
Value: false,
Usage: "Automatically generate secrets",
Destination: &Secrets,
}
// Pass stores the variable from PassFlag
var Pass bool
// PassFlag turns on/off storing generated secrets in pass
var PassFlag = &cli.BoolFlag{
Name: "pass",
Aliases: []string{"P"},
Value: false,
Usage: "Store the generated secrets in a local pass store",
Destination: &Pass,
}
// Context is temp
var Context string
// ContextFlag is temp
var ContextFlag = &cli.StringFlag{
Name: "context",
Value: "",
Aliases: []string{"c"},
Destination: &Context,
}
// Force force functionality without asking.
var Force bool
// ForceFlag turns on/off force functionality.
var ForceFlag = &cli.BoolFlag{
Name: "force",
Value: false,
Aliases: []string{"f"},
Destination: &Force,
}

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

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

10
cli/internal/list.go Normal file
View File

@ -0,0 +1,10 @@
package internal
// ReverseStringList reverses a list of a strings. Roll on Go generics.
func ReverseStringList(strings []string) []string {
for i, j := 0, len(strings)-1; i < j; i, j = i+1, j-1 {
strings[i], strings[j] = strings[j], strings[i]
}
return strings
}

110
cli/internal/recipe.go Normal file
View File

@ -0,0 +1,110 @@
package internal
import (
"fmt"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
)
// PromptBumpType prompts for version bump type
func PromptBumpType(tagString, latestRelease string) error {
if (!Major && !Minor && !Patch) && tagString == "" {
fmt.Printf(`
You need to make a decision about what kind of an update this new recipe
version is. If someone else performs this upgrade, do they have to do some
migration work or take care of some breaking changes? This can be signaled in
the version you specify on the recipe deploy label and is called a semantic
version.
The latest published version is %s.
Here is a semver cheat sheet (more on https://semver.org):
major: new features/bug fixes, backwards incompatible (e.g 1.0.0 -> 2.0.0).
the upgrade won't work without some preparation work and others need
to take care when performing it. "it could go wrong".
minor: new features/bug fixes, backwards compatible (e.g. 0.1.0 -> 0.2.0).
the upgrade should Just Work and there are no breaking changes in
the app and the recipe config. "it should go fine".
patch: bug fixes, backwards compatible (e.g. 0.0.1 -> 0.0.2). this upgrade
should also Just Work and is mostly to do with minor bug fixes
and/or security patches. "nothing to worry about".
`, latestRelease)
var chosenBumpType string
prompt := &survey.Select{
Message: fmt.Sprintf("select recipe version increment type"),
Options: []string{"major", "minor", "patch"},
}
if err := survey.AskOne(prompt, &chosenBumpType); err != nil {
return err
}
SetBumpType(chosenBumpType)
}
return nil
}
// GetBumpType figures out which bump type is specified
func GetBumpType() string {
var bumpType string
if Major {
bumpType = "major"
} else if Minor {
bumpType = "minor"
} else if Patch {
bumpType = "patch"
} else {
logrus.Fatal("no version bump type specififed?")
}
return bumpType
}
// SetBumpType figures out which bump type is specified
func SetBumpType(bumpType string) {
if bumpType == "major" {
Major = true
} else if bumpType == "minor" {
Minor = true
} else if bumpType == "patch" {
Patch = true
} else {
logrus.Fatal("no version bump type specififed?")
}
}
// GetMainAppImage retrieves the main 'app' image name
func GetMainAppImage(recipe recipe.Recipe) (string, error) {
var path string
for _, service := range recipe.Config.Services {
if service.Name == "app" {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return "", err
}
path = reference.Path(img)
path = formatter.StripTagMeta(path)
return path, nil
}
}
if path == "" {
return path, fmt.Errorf("%s has no main 'app' service?", recipe.Name)
}
return path, nil
}

View File

@ -2,30 +2,79 @@ package internal
import (
"errors"
"strings"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
// ValidateRecipe ensures the recipe arg is valid.
func ValidateRecipe(c *cli.Context) recipe.Recipe {
recipeName := c.Args().First()
if recipeName == "" && !NoInput {
var recipes []string
catl, err := recipe.ReadRecipeCatalogue(Offline)
if err != nil {
logrus.Fatal(err)
}
knownRecipes := make(map[string]bool)
for name := range catl {
knownRecipes[name] = true
}
localRecipes, err := recipe.GetRecipesLocal()
if err != nil {
logrus.Fatal(err)
}
for _, recipeLocal := range localRecipes {
if _, ok := knownRecipes[recipeLocal]; !ok {
knownRecipes[recipeLocal] = true
}
}
for recipeName := range knownRecipes {
recipes = append(recipes, recipeName)
}
prompt := &survey.Select{
Message: "Select recipe",
Options: recipes,
}
if err := survey.AskOne(prompt, &recipeName); err != nil {
logrus.Fatal(err)
}
}
if recipeName == "" {
ShowSubcommandHelpAndError(c, errors.New("no recipe provided"))
ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
}
recipe, err := recipe.Get(recipeName)
chosenRecipe, err := recipe.Get(recipeName, Offline)
if err != nil {
logrus.Fatal(err)
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)
}
}
logrus.Debugf("validated '%s' as recipe argument", recipeName)
logrus.Debugf("validated %s as recipe argument", recipeName)
return recipe
return chosenRecipe
}
// ValidateApp ensures the app name arg is valid.
@ -41,11 +90,7 @@ func ValidateApp(c *cli.Context) config.App {
logrus.Fatal(err)
}
if err := recipe.EnsureExists(app.Type); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("validated '%s' as app argument", appName)
logrus.Debugf("validated %s as app argument", appName)
return app
}
@ -54,11 +99,74 @@ func ValidateApp(c *cli.Context) config.App {
func ValidateDomain(c *cli.Context) string {
domainName := c.Args().First()
if domainName == "" && !NoInput {
prompt := &survey.Input{
Message: "Specify a domain name",
Default: "example.com",
}
if err := survey.AskOne(prompt, &domainName); err != nil {
logrus.Fatal(err)
}
}
if domainName == "" {
ShowSubcommandHelpAndError(c, errors.New("no domain provided"))
}
logrus.Debugf("validated '%s' as domain argument", domainName)
logrus.Debugf("validated %s as domain argument", domainName)
return domainName
}
// ValidateSubCmdFlags ensures flag order conforms to correct order
func ValidateSubCmdFlags(c *cli.Context) bool {
for argIdx, arg := range c.Args() {
if !strings.HasPrefix(arg, "--") {
for _, flag := range c.Args()[argIdx:] {
if strings.HasPrefix(flag, "--") {
return false
}
}
}
}
return true
}
// ValidateServer ensures the server name arg is valid.
func ValidateServer(c *cli.Context) string {
serverName := c.Args().First()
serverNames, err := config.ReadServerNames()
if err != nil {
logrus.Fatal(err)
}
if serverName == "" && !NoInput {
prompt := &survey.Select{
Message: "Specify a server name",
Options: serverNames,
}
if err := survey.AskOne(prompt, &serverName); err != nil {
logrus.Fatal(err)
}
}
matched := false
for _, name := range serverNames {
if name == serverName {
matched = true
}
}
if serverName == "" {
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)
return serverName
}

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

50
cli/recipe/fetch.go Normal file
View File

@ -0,0 +1,50 @@
package recipe
import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var recipeFetchCommand = cli.Command{
Name: "fetch",
Usage: "Fetch recipe(s)",
Aliases: []string{"f"},
ArgsUsage: "[<recipe>]",
Description: "Retrieves all recipes if no <recipe> argument is passed",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
if recipeName != "" {
internal.ValidateRecipe(c)
if err := recipe.Ensure(recipeName); err != nil {
logrus.Fatal(err)
}
return nil
}
catalogue, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
logrus.Fatal(err)
}
catlBar := formatter.CreateProgressbar(len(catalogue), "fetching latest recipes...")
for recipeName := range catalogue {
if err := recipe.Ensure(recipeName); err != nil {
logrus.Error(err)
}
catlBar.Add(1)
}
return nil
},
}

View File

@ -2,105 +2,121 @@ package recipe
import (
"fmt"
"os"
"strconv"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/tagcmp"
"github.com/docker/distribution/reference"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var recipeLintCommand = &cli.Command{
var recipeLintCommand = cli.Command{
Name: "lint",
Usage: "Lint a recipe",
Aliases: []string{"l"},
ArgsUsage: "<recipe>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.OnlyErrorFlag,
internal.OfflineFlag,
internal.NoInputFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
expectedVersion := false
if recipe.Config.Version == "3.8" {
expectedVersion = true
}
envSampleProvided := false
envSample := fmt.Sprintf("%s/%s/.env.sample", config.APPS_DIR, recipe.Name)
if _, err := os.Stat(envSample); !os.IsNotExist(err) {
envSampleProvided = true
} else if err != nil {
if err := recipePkg.EnsureExists(recipe.Name); err != nil {
logrus.Fatal(err)
}
serviceNamedApp := false
traefikEnabled := false
healthChecksForAllServices := true
allImagesTagged := true
noUnstableTags := true
semverLikeTags := true
for _, service := range recipe.Config.Services {
if service.Name == "app" {
serviceNamedApp = true
if !internal.Chaos {
if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
logrus.Fatal(err)
}
for label := range service.Deploy.Labels {
if label == "traefik.enable" {
if service.Deploy.Labels[label] == "true" {
traefikEnabled = true
}
if !internal.Offline {
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err)
}
}
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
logrus.Fatal(err)
}
if reference.IsNameOnly(img) {
allImagesTagged = false
}
tag := img.(reference.NamedTagged).Tag()
if tag == "latest" {
noUnstableTags = false
}
if !tagcmp.IsParsable(tag) {
semverLikeTags = false
}
if service.HealthCheck == nil {
healthChecksForAllServices = false
}
}
tableCol := []string{"rule", "satisfied"}
tableCol := []string{"ref", "rule", "severity", "satisfied", "skipped", "resolve"}
table := formatter.CreateTable(tableCol)
table.Append([]string{"compose files have the expected version", strconv.FormatBool(expectedVersion)})
table.Append([]string{"environment configuration is provided", strconv.FormatBool(envSampleProvided)})
table.Append([]string{"recipe contains a service named 'app'", strconv.FormatBool(serviceNamedApp)})
table.Append([]string{"traefik routing enabled on at least one service", strconv.FormatBool(traefikEnabled)})
table.Append([]string{"all services have a healthcheck enabled", strconv.FormatBool(healthChecksForAllServices)})
table.Append([]string{"all images are using a tag", strconv.FormatBool(allImagesTagged)})
table.Append([]string{"no usage of unstable 'latest' tags", strconv.FormatBool(noUnstableTags)})
table.Append([]string{"all tags are using a semver-like format", strconv.FormatBool(semverLikeTags)})
table.Render()
hasError := false
bar := formatter.CreateProgressbar(-1, "running recipe lint rules...")
for level := range lint.LintRules {
for _, rule := range lint.LintRules[level] {
if internal.OnlyErrors && rule.Level != "error" {
logrus.Debugf("skipping %s, does not have level \"error\"", rule.Ref)
continue
}
skipped := false
if rule.Skip(recipe) {
skipped = true
}
skippedOutput := "-"
if skipped {
skippedOutput = "✅"
}
satisfied := false
if !skipped {
ok, err := rule.Function(recipe)
if err != nil {
logrus.Warn(err)
}
if !ok && rule.Level == "error" {
hasError = true
}
if ok {
satisfied = true
}
}
satisfiedOutput := "✅"
if !satisfied {
satisfiedOutput = "❌"
if skipped {
satisfiedOutput = "-"
}
}
table.Append([]string{
rule.Ref,
rule.Description,
rule.Level,
satisfiedOutput,
skippedOutput,
rule.HowToResolve,
})
bar.Add(1)
}
}
if table.NumLines() > 0 {
fmt.Println()
table.Render()
}
if hasError {
logrus.Warn("watch out, some critical errors are present in your recipe config")
}
return nil
},
BashComplete: func(c *cli.Context) {
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for name := range catl {
fmt.Println(name)
}
},
}

View File

@ -3,36 +3,80 @@ package recipe
import (
"fmt"
"sort"
"strconv"
"strings"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var recipeListCommand = &cli.Command{
var pattern string
var patternFlag = &cli.StringFlag{
Name: "pattern, p",
Value: "",
Usage: "Simple string to filter recipes",
Destination: &pattern,
}
var recipeListCommand = cli.Command{
Name: "list",
Usage: "List available recipes",
Aliases: []string{"ls"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.MachineReadableFlag,
patternFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
catl, err := catalogue.ReadRecipeCatalogue()
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
logrus.Fatal(err.Error())
}
recipes := catl.Flatten()
sort.Sort(catalogue.ByRecipeName(recipes))
sort.Sort(recipe.ByRecipeName(recipes))
tableCol := []string{"name", "category", "status"}
tableCol := []string{"name", "category", "status", "healthcheck", "backups", "email", "tests", "SSO"}
table := formatter.CreateTable(tableCol)
len := 0
for _, recipe := range recipes {
status := fmt.Sprintf("%v", recipe.Features.Status)
tableRow := []string{recipe.Name, recipe.Category, status}
table.Append(tableRow)
tableRow := []string{
recipe.Name,
recipe.Category,
strconv.Itoa(recipe.Features.Status),
recipe.Features.Healthcheck,
recipe.Features.Backups,
recipe.Features.Email,
recipe.Features.Tests,
recipe.Features.SSO,
}
if pattern != "" {
if strings.Contains(recipe.Name, pattern) {
table.Append(tableRow)
len++
}
} else {
table.Append(tableRow)
len++
}
}
table.Render()
if table.NumLines() > 0 {
if internal.MachineReadable {
table.SetCaption(false, "")
table.JSONRender()
} else {
table.SetCaption(true, fmt.Sprintf("total recipes: %v", len))
table.Render()
}
}
return nil
},

View File

@ -1,7 +1,10 @@
package recipe
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"text/template"
@ -10,70 +13,129 @@ import (
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/git"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var recipeNewCommand = &cli.Command{
Name: "new",
Usage: "Create a new recipe",
Aliases: []string{"n"},
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
// recipeMetadata is the recipe metadata for the README.md
type recipeMetadata struct {
Name string
Description string
Category string
Status string
Image string
Healthcheck string
Backups string
Email string
Tests string
SSO string
}
directory := path.Join(config.APPS_DIR, recipe.Name)
var recipeNewCommand = cli.Command{
Name: "new",
Aliases: []string{"n"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Usage: "Create a new recipe",
ArgsUsage: "<recipe>",
Description: `
Create a new recipe.
Abra uses the built-in example repository which is available here:
https://git.coopcloud.tech/coop-cloud/example
Files within the example repository make use of the Golang templating system
which Abra uses to inject values into the generated recipe folder (e.g. name of
recipe and domain in the sample environment config).
`,
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
if recipeName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
}
directory := path.Join(config.RECIPES_DIR, recipeName)
if _, err := os.Stat(directory); !os.IsNotExist(err) {
logrus.Fatalf("'%s' recipe directory already exists?", directory)
return nil
logrus.Fatalf("%s recipe directory already exists?", directory)
}
url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL)
if err := git.Clone(directory, url); err != nil {
return err
logrus.Fatal(err)
}
gitRepo := path.Join(config.APPS_DIR, recipe.Name, ".git")
gitRepo := path.Join(config.RECIPES_DIR, recipeName, ".git")
if err := os.RemoveAll(gitRepo); err != nil {
logrus.Fatal(err)
return nil
}
logrus.Debugf("removed git repo in '%s'", gitRepo)
logrus.Debugf("removed example git repo in %s", gitRepo)
meta := newRecipeMeta(recipeName)
toParse := []string{
path.Join(config.APPS_DIR, recipe.Name, "README.md"),
path.Join(config.APPS_DIR, recipe.Name, ".env.sample"),
path.Join(config.APPS_DIR, recipe.Name, ".drone.yml"),
path.Join(config.RECIPES_DIR, recipeName, "README.md"),
path.Join(config.RECIPES_DIR, recipeName, ".env.sample"),
}
for _, path := range toParse {
file, err := os.OpenFile(path, os.O_RDWR, 0755)
if err != nil {
logrus.Fatal(err)
return nil
}
tpl, err := template.ParseFiles(path)
if err != nil {
logrus.Fatal(err)
return nil
}
// TODO: ask for description and probably other things so that the
// template repository is more "ready" to go than the current best-guess
// mode of templating
if err := tpl.Execute(file, struct {
Name string
Description string
}{recipe.Name, "TODO"}); err != nil {
var templated bytes.Buffer
if err := tpl.Execute(&templated, meta); err != nil {
logrus.Fatal(err)
return nil
}
if err := ioutil.WriteFile(path, templated.Bytes(), 0644); err != nil {
logrus.Fatal(err)
}
}
logrus.Infof(
"new recipe '%s' created in %s, happy hacking!\n",
recipe.Name, path.Join(config.APPS_DIR, recipe.Name),
)
newGitRepo := path.Join(config.RECIPES_DIR, recipeName)
if err := git.Init(newGitRepo, true); err != nil {
logrus.Fatal(err)
}
fmt.Print(fmt.Sprintf(`
Your new %s recipe has been created in %s.
In order to share your recipe, you can upload it the git repository to:
https://git.coopcloud.tech/coop-cloud/%s
If you're not sure how to do that, come chat with us:
https://docs.coopcloud.tech/intro/contact
See "abra recipe -h" for additional recipe maintainer commands.
Happy Hacking!
`, recipeName, path.Join(config.RECIPES_DIR, recipeName), recipeName))
return nil
},
}
// newRecipeMeta creates a new recipeMetadata instance with defaults
func newRecipeMeta(recipeName string) recipeMetadata {
return recipeMetadata{
Name: recipeName,
Description: "> One line description of the recipe",
Category: "Apps",
Status: "0",
Image: fmt.Sprintf("[`%s`](https://hub.docker.com/r/%s), 4, upstream", recipeName, recipeName),
Healthcheck: "No",
Backups: "No",
Email: "No",
Tests: "No",
SSO: "No",
}
}

View File

@ -1,51 +1,36 @@
package recipe
import (
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var Major bool
var MajorFlag = &cli.BoolFlag{
Name: "major",
Value: false,
Aliases: []string{"ma", "x"},
Destination: &Major,
}
var Minor bool
var MinorFlag = &cli.BoolFlag{
Name: "minor",
Value: false,
Aliases: []string{"mi", "y"},
Destination: &Minor,
}
var Patch bool
var PatchFlag = &cli.BoolFlag{
Name: "patch",
Value: false,
Aliases: []string{"p", "z"},
Destination: &Patch,
}
// RecipeCommand defines all recipe related sub-commands.
var RecipeCommand = &cli.Command{
var RecipeCommand = cli.Command{
Name: "recipe",
Aliases: []string{"r"},
Usage: "Manage recipes",
ArgsUsage: "<recipe>",
Aliases: []string{"r"},
Description: `
A recipe is a blueprint for an app. It is a bunch of configuration files which
A recipe is a blueprint for an app. It is a bunch of config files which
describe how to deploy and maintain an app. Recipes are maintained by the Co-op
Cloud community and you can use Abra to read them and create apps for you.
Cloud community and you can use Abra to read them, deploy them and create apps
for you.
Anyone who uses a recipe can become a maintainer. Maintainers typically make
sure the recipe is in good working order and the config upgraded in a timely
manner. Abra supports convenient automation for recipe maintainenace, see the
"abra recipe upgrade", "abra recipe sync" and "abra recipe release" commands.
`,
Subcommands: []*cli.Command{
recipeListCommand,
recipeVersionCommand,
recipeReleaseCommand,
recipeNewCommand,
recipeUpgradeCommand,
recipeSyncCommand,
Subcommands: []cli.Command{
recipeFetchCommand,
recipeLintCommand,
recipeListCommand,
recipeNewCommand,
recipeReleaseCommand,
recipeSyncCommand,
recipeUpgradeCommand,
recipeVersionCommand,
recipeResetCommand,
recipeDiffCommand,
},
}

View File

@ -1,312 +1,552 @@
package recipe
import (
"errors"
"fmt"
"os"
"path"
"strconv"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var Push bool
var PushFlag = &cli.BoolFlag{
Name: "push",
Value: false,
Destination: &Push,
}
var Dry bool
var DryFlag = &cli.BoolFlag{
Name: "dry-run",
Value: false,
Aliases: []string{"d"},
Destination: &Dry,
}
var CommitMessage string
var CommitMessageFlag = &cli.StringFlag{
Name: "commit-message",
Usage: "commit message. Implies --commit",
Aliases: []string{"cm"},
Destination: &CommitMessage,
}
var Commit bool
var CommitFlag = &cli.BoolFlag{
Name: "commit",
Usage: "add compose.yml to staging area and commit changes",
Value: false,
Aliases: []string{"c"},
Destination: &Commit,
}
var TagMessage string
var TagMessageFlag = &cli.StringFlag{
Name: "tag-comment",
Usage: "tag comment. If not given, user will be asked for it",
Aliases: []string{"t", "tm"},
Destination: &TagMessage,
}
var recipeReleaseCommand = &cli.Command{
var recipeReleaseCommand = cli.Command{
Name: "release",
Usage: "tag a recipe",
Aliases: []string{"rl"},
ArgsUsage: "<recipe> [<tag>]",
Usage: "Release a new recipe version",
ArgsUsage: "<recipe> [<version>]",
Description: `
This command is used to specify a new tag for a recipe. These tags are used to
identify different versions of the recipe and are published on the Co-op Cloud
recipe catalogue.
These tags take the following form:
Create a new version of a recipe. These versions are then published on the
Co-op Cloud recipe catalogue. These versions take the following form:
a.b.c+x.y.z
Where the "a.b.c" part is maintained as a semantic version of the recipe by the
recipe maintainer. And the "x.y.z" part is the image tag of the recipe "app"
service (the main container which contains the software to be used).
Where the "a.b.c" part is a semantic version determined by the maintainer. The
"x.y.z" part is the image tag of the recipe "app" service (the main container
which contains the software to be used, by naming convention).
We maintain a semantic versioning scheme ("a.b.c") alongside the libre app
versioning scheme in order to maximise the chances that the nature of recipe
updates are properly communicated.
We maintain a semantic versioning scheme ("a.b.c") alongside the recipe
versioning scheme ("x.y.z") in order to maximise the chances that the nature of
recipe updates are properly communicated. I.e. developers of an app might
publish a minor version but that might lead to changes in the recipe which are
major and therefore require intervention while doing the upgrade work.
Abra does its best to read the "a.b.c" version scheme and communicate what
action needs to be taken when performing different operations such as an update
or a rollback of an app.
Publish your new release to git.coopcloud.tech with "-p/--publish". This
requires that you have permission to git push to these repositories and have
your SSH keys configured on your account.
`,
Flags: []cli.Flag{
DryFlag,
PatchFlag,
MinorFlag,
MajorFlag,
PushFlag,
CommitFlag,
CommitMessageFlag,
TagMessageFlag,
internal.DebugFlag,
internal.NoInputFlag,
internal.DryFlag,
internal.MajorFlag,
internal.MinorFlag,
internal.PatchFlag,
internal.PublishFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
directory := path.Join(config.APPS_DIR, recipe.Name)
tagstring := c.Args().Get(1)
imagesTmp := getImageVersions(recipe)
mainApp := getMainApp(recipe)
imagesTmp, err := getImageVersions(recipe)
if err != nil {
logrus.Fatal(err)
}
mainApp, err := internal.GetMainAppImage(recipe)
if err != nil {
logrus.Fatal(err)
}
mainAppVersion := imagesTmp[mainApp]
if err := recipePkg.EnsureExists(recipe.Name); err != nil {
logrus.Fatal(err)
}
if mainAppVersion == "" {
logrus.Fatal("main app version is empty?")
logrus.Fatalf("main app service version for %s is empty?", recipe.Name)
}
if tagstring != "" {
if _, err := tagcmp.Parse(tagstring); err != nil {
logrus.Fatal("invalid tag specified")
tagString := c.Args().Get(1)
if tagString != "" {
if _, err := tagcmp.Parse(tagString); err != nil {
logrus.Fatalf("cannot parse %s, invalid tag specified?", tagString)
}
}
if TagMessage == "" {
prompt := &survey.Input{
Message: "tag message",
}
survey.AskOne(prompt, &TagMessage)
if (internal.Major || internal.Minor || internal.Patch) && tagString != "" {
logrus.Fatal("cannot specify tag and bump type at the same time")
}
var createTagOptions git.CreateTagOptions
createTagOptions.Message = TagMessage
if Commit || (CommitMessage != "") {
commitRepo, err := git.PlainOpen(directory)
if err != nil {
if tagString != "" {
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
logrus.Fatal(err)
}
commitWorktree, err := commitRepo.Worktree()
if err != nil {
logrus.Fatal(err)
}
if CommitMessage == "" {
prompt := &survey.Input{
Message: "commit message",
}
survey.AskOne(prompt, &CommitMessage)
}
err = commitWorktree.AddGlob("compose.**yml")
if err != nil {
logrus.Fatal(err)
}
logrus.Debug("staged compose.**yml for commit")
_, err = commitWorktree.Commit(CommitMessage, &git.CommitOptions{})
if err != nil {
logrus.Fatal(err)
}
logrus.Info("changes commited")
}
repo, err := git.PlainOpen(directory)
if err != nil {
logrus.Fatal(err)
}
head, err := repo.Head()
tags, err := recipe.Tags()
if err != nil {
logrus.Fatal(err)
}
// bumpType is used to decide what part of the tag should be incremented
bumpType := btoi(Major)*4 + btoi(Minor)*2 + btoi(Patch)
if bumpType != 0 {
// a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 {
logrus.Fatal("you can only use one of: --major, --minor, --patch.")
}
}
if tagstring != "" {
if bumpType > 0 {
logrus.Warn("user specified a version number and --major/--minor/--patch at the same time! using version number...")
}
tag, err := tagcmp.Parse(tagstring)
if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
var err error
tagString, err = getLabelVersion(recipe, false)
if err != nil {
logrus.Fatal(err)
}
if tag.MissingMinor {
tag.Minor = "0"
tag.MissingMinor = false
}
if tag.MissingPatch {
tag.Patch = "0"
tag.MissingPatch = false
}
tagstring = fmt.Sprintf("%s+%s", tag.String(), mainAppVersion)
if Dry {
logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", tagstring, head.Hash()))
return nil
}
repo.CreateTag(tagstring, head.Hash(), &createTagOptions)
logrus.Info(fmt.Sprintf("created tag %s at %s", tagstring, head.Hash()))
if Push {
if err := repo.Push(&git.PushOptions{}); err != nil {
logrus.Fatal(err)
}
logrus.Info(fmt.Sprintf("pushed tag %s to remote", tagstring))
}
return nil
}
// get the latest tag with its hash, name etc
var lastGitTag tagcmp.Tag
iter, err := repo.Tags()
isClean, err := gitPkg.IsClean(recipe.Dir())
if err != nil {
logrus.Fatal(err)
}
if err := iter.ForEach(func(ref *plumbing.Reference) error {
obj, err := repo.TagObject(ref.Hash())
if err != nil {
return err
if !isClean {
logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name)
if err := gitPkg.DiffUnstaged(recipe.Dir()); err != nil {
logrus.Fatal(err)
}
tagcmpTag, err := tagcmp.Parse(obj.Name)
if err != nil {
return err
}
if (lastGitTag == tagcmp.Tag{}) {
lastGitTag = tagcmpTag
} else if tagcmpTag.IsGreaterThan(lastGitTag) {
lastGitTag = tagcmpTag
}
return nil
}); err != nil {
logrus.Fatal(err)
}
fmt.Println(lastGitTag)
newTag := lastGitTag
var newTagString string
if bumpType > 0 {
if Patch {
now, err := strconv.Atoi(newTag.Patch)
if err != nil {
logrus.Fatal(err)
}
newTag.Patch = strconv.Itoa(now + 1)
} else if Minor {
now, err := strconv.Atoi(newTag.Minor)
if err != nil {
logrus.Fatal(err)
}
newTag.Patch = "0"
newTag.Minor = strconv.Itoa(now + 1)
} else if Major {
now, err := strconv.Atoi(newTag.Major)
if err != nil {
logrus.Fatal(err)
}
newTag.Patch = "0"
newTag.Minor = "0"
newTag.Major = strconv.Itoa(now + 1)
if len(tags) > 0 {
logrus.Warnf("previous git tags detected, assuming this is a new semver release")
if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil {
logrus.Fatal(err)
}
} else {
logrus.Fatal("we don't support automatic tag generation yet - specify a version or use one of: --major --minor --patch")
}
newTag.Metadata = mainAppVersion
newTagString = newTag.String()
if Dry {
logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", newTagString, head.Hash()))
return nil
}
logrus.Warnf("no tag specified and no previous tag available for %s, assuming this is the initial release", recipe.Name)
repo.CreateTag(newTagString, head.Hash(), &createTagOptions)
logrus.Info(fmt.Sprintf("created tag %s at %s", newTagString, head.Hash()))
if Push {
if err := repo.Push(&git.PushOptions{}); err != nil {
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
if cleanUpErr := cleanUpTag(tagString, recipe.Name); err != nil {
logrus.Fatal(cleanUpErr)
}
logrus.Fatal(err)
}
logrus.Info(fmt.Sprintf("pushed tag %s to remote", newTagString))
}
return nil
},
}
func getImageVersions(recipe recipe.Recipe) map[string]string {
// getImageVersions retrieves image versions for a recipe
func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
services := make(map[string]string)
var services = make(map[string]string)
missingTag := false
for _, service := range recipe.Config.Services {
srv := strings.Split(service.Image, ":")
services[srv[0]] = srv[1]
}
return services
}
func getMainApp(recipe recipe.Recipe) string {
for _, service := range recipe.Config.Services {
name := service.Name
if name == "app" {
return strings.Split(service.Image, ":")[0]
if service.Image == "" {
continue
}
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return services, err
}
path := reference.Path(img)
path = formatter.StripTagMeta(path)
var tag string
switch img.(type) {
case reference.NamedTagged:
tag = img.(reference.NamedTagged).Tag()
case reference.Named:
if service.Name == "app" {
missingTag = true
}
continue
}
services[path] = tag
}
return ""
if missingTag {
return services, fmt.Errorf("app service is missing image tag?")
}
return services, nil
}
// createReleaseFromTag creates a new release based on a supplied recipe version string
func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error {
var err error
directory := path.Join(config.RECIPES_DIR, recipe.Name)
repo, err := git.PlainOpen(directory)
if err != nil {
return err
}
tag, err := tagcmp.Parse(tagString)
if err != nil {
return err
}
if tag.MissingMinor {
tag.Minor = "0"
tag.MissingMinor = false
}
if tag.MissingPatch {
tag.Patch = "0"
tag.MissingPatch = false
}
if tagString == "" {
tagString = fmt.Sprintf("%s+%s", tag.String(), mainAppVersion)
}
if err := addReleaseNotes(recipe, tagString); err != nil {
logrus.Fatal(err)
}
if err := commitRelease(recipe, tagString); err != nil {
logrus.Fatal(err)
}
if err := tagRelease(tagString, repo); err != nil {
logrus.Fatal(err)
}
if err := pushRelease(recipe, tagString); err != nil {
logrus.Fatal(err)
}
return nil
}
// btoi converts a boolean value into an integer
func btoi(b bool) int {
if b {
return 1
}
return 0
}
// getTagCreateOptions constructs git tag create options
func getTagCreateOptions(tag string) (git.CreateTagOptions, error) {
msg := fmt.Sprintf("chore: publish %s release", tag)
return git.CreateTagOptions{Message: msg}, nil
}
// addReleaseNotes checks if the release/next release note exists and moves the
// file to release/<tag>.
func addReleaseNotes(recipe recipe.Recipe, tag string) error {
repoPath := path.Join(config.RECIPES_DIR, recipe.Name)
tagReleaseNotePath := path.Join(repoPath, "release", tag)
if _, err := os.Stat(tagReleaseNotePath); err == nil {
// Release note for current tag already exist exists.
return nil
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
nextReleaseNotePath := path.Join(repoPath, "release", "next")
if _, err := os.Stat(nextReleaseNotePath); err == nil {
// release/next note exists. Move it to release/<tag>
if internal.Dry {
logrus.Debugf("dry run: move release note from 'next' to %s", tag)
return nil
}
if !internal.NoInput {
prompt := &survey.Input{
Message: "Use release note in release/next?",
}
var addReleaseNote bool
if err := survey.AskOne(prompt, &addReleaseNote); err != nil {
return err
}
if !addReleaseNote {
return nil
}
}
err := os.Rename(nextReleaseNotePath, tagReleaseNotePath)
if err != nil {
return err
}
err = gitPkg.Add(repoPath, path.Join("release", "next"), internal.Dry)
if err != nil {
return err
}
err = gitPkg.Add(repoPath, path.Join("release", tag), internal.Dry)
if err != nil {
return err
}
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
// No release note exists for the current release.
if internal.NoInput {
return nil
}
prompt := &survey.Input{
Message: "Release Note (leave empty for no release note)",
}
var releaseNote string
if err := survey.AskOne(prompt, &releaseNote); err != nil {
return err
}
if releaseNote == "" {
return nil
}
err := os.WriteFile(tagReleaseNotePath, []byte(releaseNote), 0o644)
if err != nil {
return err
}
err = gitPkg.Add(repoPath, path.Join("release", tag), internal.Dry)
if err != nil {
return err
}
return nil
}
func commitRelease(recipe recipe.Recipe, tag string) error {
if internal.Dry {
logrus.Debugf("dry run: no changes committed")
return nil
}
isClean, err := gitPkg.IsClean(recipe.Dir())
if err != nil {
return err
}
if isClean {
if !internal.Dry {
return fmt.Errorf("no changes discovered in %s, nothing to publish?", recipe.Dir())
}
}
msg := fmt.Sprintf("chore: publish %s release", tag)
repoPath := path.Join(config.RECIPES_DIR, recipe.Name)
if err := gitPkg.Commit(repoPath, msg, internal.Dry); err != nil {
return err
}
return nil
}
func tagRelease(tagString string, repo *git.Repository) error {
if internal.Dry {
logrus.Debugf("dry run: no git tag created (%s)", tagString)
return nil
}
head, err := repo.Head()
if err != nil {
return err
}
createTagOptions, err := getTagCreateOptions(tagString)
if err != nil {
return err
}
_, err = repo.CreateTag(tagString, head.Hash(), &createTagOptions)
if err != nil {
return err
}
hash := formatter.SmallSHA(head.Hash().String())
logrus.Debugf(fmt.Sprintf("created tag %s at %s", tagString, hash))
return nil
}
func pushRelease(recipe recipe.Recipe, tagString string) error {
if internal.Dry {
logrus.Info("dry run: no changes published")
return nil
}
if !internal.Publish && !internal.NoInput {
prompt := &survey.Confirm{
Message: "publish new release?",
}
if err := survey.AskOne(prompt, &internal.Publish); err != nil {
return err
}
}
if internal.Publish {
if err := recipe.Push(internal.Dry); err != nil {
return err
}
url := fmt.Sprintf("%s/%s/src/tag/%s", config.REPOS_BASE_URL, recipe.Name, tagString)
logrus.Infof("new release published: %s", url)
} else {
logrus.Info("no -p/--publish passed, not publishing")
}
return nil
}
func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error {
directory := path.Join(config.RECIPES_DIR, recipe.Name)
repo, err := git.PlainOpen(directory)
if err != nil {
return err
}
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 {
if (bumpType & (bumpType - 1)) != 0 {
return fmt.Errorf("you can only use one of: --major, --minor, --patch")
}
}
var lastGitTag tagcmp.Tag
for _, tag := range tags {
parsed, err := tagcmp.Parse(tag)
if err != nil {
return err
}
if (lastGitTag == tagcmp.Tag{}) {
lastGitTag = parsed
} else if parsed.IsGreaterThan(lastGitTag) {
lastGitTag = parsed
}
}
newTag := lastGitTag
if internal.Patch {
now, err := strconv.Atoi(newTag.Patch)
if err != nil {
return err
}
newTag.Patch = strconv.Itoa(now + 1)
} else if internal.Minor {
now, err := strconv.Atoi(newTag.Minor)
if err != nil {
return err
}
newTag.Patch = "0"
newTag.Minor = strconv.Itoa(now + 1)
} else if internal.Major {
now, err := strconv.Atoi(newTag.Major)
if err != nil {
return err
}
newTag.Patch = "0"
newTag.Minor = "0"
newTag.Major = strconv.Itoa(now + 1)
}
if tagString == "" {
if err := internal.PromptBumpType(tagString, lastGitTag.String()); err != nil {
return err
}
}
if internal.Major || internal.Minor || internal.Patch {
newTag.Metadata = mainAppVersion
tagString = newTag.String()
}
if lastGitTag.String() == tagString {
logrus.Fatalf("latest git tag (%s) and synced label (%s) are the same?", lastGitTag, tagString)
}
if !internal.NoInput {
prompt := &survey.Confirm{
Message: fmt.Sprintf("current: %s, new: %s, correct?", lastGitTag, tagString),
}
var ok bool
if err := survey.AskOne(prompt, &ok); err != nil {
logrus.Fatal(err)
}
if !ok {
logrus.Fatal("exiting as requested")
}
}
if err := addReleaseNotes(recipe, tagString); err != nil {
logrus.Fatal(err)
}
if err := commitRelease(recipe, tagString); err != nil {
logrus.Fatalf("failed to commit changes: %s", err.Error())
}
if err := tagRelease(tagString, repo); err != nil {
logrus.Fatalf("failed to tag release: %s", err.Error())
}
if err := pushRelease(recipe, tagString); err != nil {
logrus.Fatalf("failed to publish new release: %s", err.Error())
}
return nil
}
// cleanUpTag removes a freshly created tag
func cleanUpTag(tag, recipeName string) error {
directory := path.Join(config.RECIPES_DIR, recipeName)
repo, err := git.PlainOpen(directory)
if err != nil {
return err
}
if err := repo.DeleteTag(tag); err != nil {
if !strings.Contains(err.Error(), "not found") {
return err
}
}
logrus.Debugf("removed freshly created tag %s", tag)
return nil
}
func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
initTag, err := recipePkg.GetVersionLabelLocal(recipe)
if err != nil {
return "", err
}
if initTag == "" {
logrus.Fatalf("unable to read version for %s from synced label. Did you try running \"abra recipe sync %s\" already?", recipe.Name, recipe.Name)
}
logrus.Warnf("discovered %s as currently synced recipe label", initTag)
if prompt && !internal.NoInput {
var response bool
prompt := &survey.Confirm{Message: fmt.Sprintf("use %s as the new version?", initTag)}
if err := survey.AskOne(prompt, &response); err != nil {
return "", err
}
if !response {
return "", fmt.Errorf("please fix your synced label for %s and re-run this command", recipe.Name)
}
}
return initTag, nil
}

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

@ -1,87 +1,214 @@
package recipe
import (
"errors"
"fmt"
"path"
"strconv"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var recipeSyncCommand = &cli.Command{
var recipeSyncCommand = cli.Command{
Name: "sync",
Usage: "Ensure recipe version labels are up-to-date",
Aliases: []string{"s"},
ArgsUsage: "<recipe> <version>",
Usage: "Sync recipe version label",
ArgsUsage: "<recipe> [<version>]",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.DryFlag,
internal.MajorFlag,
internal.MinorFlag,
internal.PatchFlag,
},
Before: internal.SubCommandBefore,
Description: `
This command will generate labels for the main recipe service (i.e. by
convention, typically the service named "app") which corresponds to the
following format:
Generate labels for the main recipe service (i.e. by convention, the service
named "app") which corresponds to the following format:
coop-cloud.${STACK_NAME}.version=<version>
The <version> is determined by the recipe maintainer and is specified on the
command-line. The <recipe> configuration will be updated on the local file
system.
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
local file system.
`,
BashComplete: func(c *cli.Context) {
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for name := range catl {
fmt.Println(name)
}
},
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
if c.Args().Len() != 2 {
internal.ShowSubcommandHelpAndError(c, errors.New("missing <recipe>/<version> arguments?"))
}
recipe := internal.ValidateRecipe(c)
// TODO: validate with tagcmp when new commits come in
// See https://git.coopcloud.tech/coop-cloud/abra/pulls/109
nextTag := c.Args().Get(1)
mainService := "app"
var services []string
hasAppService := false
for _, service := range recipe.Config.Services {
services = append(services, service.Name)
if service.Name == "app" {
hasAppService = true
logrus.Debugf("detected app service in '%s'", recipe.Name)
}
}
if !hasAppService {
logrus.Warnf("no 'app' service defined in '%s'", recipe.Name)
var chosenService string
prompt := &survey.Select{
Message: fmt.Sprintf("what is the main service name for '%s'?", recipe.Name),
Options: services,
}
if err := survey.AskOne(prompt, &chosenService); err != nil {
logrus.Fatal(err)
}
mainService = chosenService
}
logrus.Debugf("selecting '%s' as the service to sync version labels", mainService)
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag)
if err := recipe.UpdateLabel(mainService, label); err != nil {
mainApp, err := internal.GetMainAppImage(recipe)
if err != nil {
logrus.Fatal(err)
}
logrus.Infof("synced label '%s' to service '%s'", label, mainService)
imagesTmp, err := getImageVersions(recipe)
if err != nil {
logrus.Fatal(err)
}
mainAppVersion := imagesTmp[mainApp]
tags, err := recipe.Tags()
if err != nil {
logrus.Fatal(err)
}
nextTag := c.Args().Get(1)
if len(tags) == 0 && nextTag == "" {
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(`
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
semver convention (more on https://semver.org), here is a short cheatsheet
0.1.0: development release, still hacking. when you make a major upgrade
you increment the "y" part (i.e. 0.1.0 -> 0.2.0) and only move to
using the "x" part when things are stable.
1.0.0: public release, assumed to be working. you already have a stable
and reliable deployment of this app and feel relatively confident
about it.
If you want people to be able alpha test your current config for %s but don't
think it is quite reliable, go with 0.1.0 and people will know that things are
likely to change.
`, recipe.Name, recipe.Name))
var chosenVersion string
edPrompt := &survey.Select{
Message: "which version do you want to begin with?",
Options: []string{"0.1.0", "1.0.0"},
}
if err := survey.AskOne(edPrompt, &chosenVersion); err != nil {
logrus.Fatal(err)
}
nextTag = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion)
}
if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
latestRelease := tags[len(tags)-1]
if err := internal.PromptBumpType("", latestRelease); err != nil {
logrus.Fatal(err)
}
}
if nextTag == "" {
recipeDir := path.Join(config.RECIPES_DIR, recipe.Name)
repo, err := git.PlainOpen(recipeDir)
if err != nil {
logrus.Fatal(err)
}
var lastGitTag tagcmp.Tag
iter, err := repo.Tags()
if err != nil {
logrus.Fatal(err)
}
if err := iter.ForEach(func(ref *plumbing.Reference) error {
obj, err := repo.TagObject(ref.Hash())
if err != nil {
logrus.Fatal("Tag at commit ", ref.Hash(), " is unannotated or otherwise broken. Please fix it.")
return err
}
tagcmpTag, err := tagcmp.Parse(obj.Name)
if err != nil {
return err
}
if (lastGitTag == tagcmp.Tag{}) {
lastGitTag = tagcmpTag
} else if tagcmpTag.IsGreaterThan(lastGitTag) {
lastGitTag = tagcmpTag
}
return nil
}); err != nil {
logrus.Fatal(err)
}
// bumpType is used to decide what part of the tag should be incremented
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 {
// a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 {
logrus.Fatal("you can only use one version flag: --major, --minor or --patch")
}
}
newTag := lastGitTag
if bumpType > 0 {
if internal.Patch {
now, err := strconv.Atoi(newTag.Patch)
if err != nil {
logrus.Fatal(err)
}
newTag.Patch = strconv.Itoa(now + 1)
} else if internal.Minor {
now, err := strconv.Atoi(newTag.Minor)
if err != nil {
logrus.Fatal(err)
}
newTag.Patch = "0"
newTag.Minor = strconv.Itoa(now + 1)
} else if internal.Major {
now, err := strconv.Atoi(newTag.Major)
if err != nil {
logrus.Fatal(err)
}
newTag.Patch = "0"
newTag.Minor = "0"
newTag.Major = strconv.Itoa(now + 1)
}
}
newTag.Metadata = mainAppVersion
logrus.Debugf("choosing %s as new version for %s", newTag.String(), recipe.Name)
nextTag = newTag.String()
}
if _, err := tagcmp.Parse(nextTag); err != nil {
logrus.Fatalf("invalid version %s specified", nextTag)
}
mainService := "app"
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag)
if !internal.Dry {
if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil {
logrus.Fatal(err)
}
} else {
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
},

View File

@ -1,43 +1,95 @@
package recipe
import (
"bufio"
"encoding/json"
"fmt"
"os"
"path"
"sort"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var recipeUpgradeCommand = &cli.Command{
type imgPin struct {
image string
version tagcmp.Tag
}
// anUpgrade represents a single service upgrade (as within a recipe), and the list of tags that it can be upgraded to,
// for serialization purposes.
type anUpgrade struct {
Service string `json:"service"`
Image string `json:"image"`
Tag string `json:"tag"`
UpgradeTags []string `json:"upgrades"`
}
var recipeUpgradeCommand = cli.Command{
Name: "upgrade",
Usage: "Upgrade recipe image tags",
Aliases: []string{"u"},
Usage: "Upgrade recipe image tags",
Description: `
This command reads and attempts to parse all image tags within the given
<recipe> configuration and prompt with more recent tags to upgrade to. It will
update the relevant compose file tags on the local file system.
Parse all image tags within the given <recipe> configuration and prompt with
more recent tags to upgrade to. It will update the relevant compose file tags
on the local file system.
Some image tags cannot be parsed because they do not follow some sort of
semver-like convention. In this case, all possible tags will be listed and it
is up to the end-user to decide.
The command is interactive and will show a select input which allows you to
make a seclection. Use the "?" key to see more help on navigating this
interface.
You may invoke this command in "wizard" mode and be prompted for input:
abra recipe upgrade
`,
ArgsUsage: "<recipe>",
Flags: []cli.Flag{
PatchFlag,
MinorFlag,
MajorFlag,
internal.DebugFlag,
internal.NoInputFlag,
internal.PatchFlag,
internal.MinorFlag,
internal.MajorFlag,
internal.MachineReadableFlag,
internal.AllTagsFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
bumpType := btoi(Major)*4 + btoi(Minor)*2 + btoi(Patch)
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)
}
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 {
// a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 {
@ -45,46 +97,86 @@ is up to the end-user to decide.
}
}
for _, service := range recipe.Config.Services {
catlVersions, err := catalogue.VersionsOfService(recipe.Name, service.Name)
if internal.MachineReadable {
// -m implies -n in this case
internal.NoInput = true
}
upgradeList := make(map[string]anUpgrade)
// check for versions file and load pinned versions
versionsPresent := false
recipeDir := path.Join(config.RECIPES_DIR, recipe.Name)
versionsPath := path.Join(recipeDir, "versions")
var servicePins = make(map[string]imgPin)
if _, err := os.Stat(versionsPath); err == nil {
logrus.Debugf("found versions file for %s", recipe.Name)
file, err := os.Open(versionsPath)
if err != nil {
logrus.Fatal(err)
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
splitLine := strings.Split(line, " ")
if splitLine[0] != "pin" || len(splitLine) != 3 {
logrus.Fatalf("malformed version pin specification: %s", line)
}
pinSlice := strings.Split(splitLine[2], ":")
pinTag, err := tagcmp.Parse(pinSlice[1])
if err != nil {
logrus.Fatal(err)
}
pin := imgPin{
image: pinSlice[0],
version: pinTag,
}
servicePins[splitLine[1]] = pin
}
if err := scanner.Err(); err != nil {
logrus.Error(err)
}
versionsPresent = true
} else {
logrus.Debugf("did not find versions file for %s", recipe.Name)
}
for _, service := range recipe.Config.Services {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
logrus.Fatal(err)
}
image := reference.Path(img)
regVersions, err := client.GetRegistryTags(image)
regVersions, err := client.GetRegistryTags(img)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("retrieved '%s' from remote registry for '%s'", regVersions, image)
if strings.Contains(image, "library") {
// ParseNormalizedNamed prepends 'library' to images like nginx:<tag>,
// postgres:<tag>, i.e. images which do not have a username in the
// first position of the string
image = strings.Split(image, "/")[1]
}
image := reference.Path(img)
logrus.Debugf("retrieved %s from remote registry for %s", regVersions, image)
image = formatter.StripTagMeta(image)
semverLikeTag := true
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
logrus.Debugf("'%s' not considered semver-like", img.(reference.NamedTagged).Tag())
semverLikeTag = false
switch img.(type) {
case reference.NamedTagged:
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
logrus.Debugf("%s not considered semver-like", img.(reference.NamedTagged).Tag())
}
default:
logrus.Warnf("unable to read tag for image %s, is it missing? skipping upgrade for %s", image, service.Name)
continue
}
tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag())
if err != nil && semverLikeTag {
logrus.Fatal(err)
if err != nil {
logrus.Warnf("unable to parse %s, error was: %s, skipping upgrade for %s", image, err.Error(), service.Name)
continue
}
logrus.Debugf("parsed '%s' for '%s'", tag, service.Name)
logrus.Debugf("parsed %s for %s", tag, service.Name)
var compatible []tagcmp.Tag
for _, regVersion := range regVersions {
other, err := tagcmp.Parse(regVersion.Name)
other, err := tagcmp.Parse(regVersion)
if err != nil {
continue // skip tags that cannot be parsed
}
@ -94,16 +186,21 @@ is up to the end-user to decide.
}
}
logrus.Debugf("detected potential upgradable tags '%s' for '%s'", compatible, service.Name)
logrus.Debugf("detected potential upgradable tags %s for %s", compatible, service.Name)
sort.Sort(tagcmp.ByTagDesc(compatible))
if len(compatible) == 0 && semverLikeTag {
logrus.Info(fmt.Sprintf("no new versions available for '%s', '%s' is the latest", image, tag))
if len(compatible) == 0 && !internal.AllTags {
logrus.Info(fmt.Sprintf("no new versions available for %s, assuming %s is the latest (use -a/--all-tags to see all anyway)", image, tag))
continue // skip on to the next tag and don't update any compose files
}
var compatibleStrings []string
catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name, internal.Offline)
if err != nil {
logrus.Fatal(err)
}
compatibleStrings := []string{"skip"}
for _, compat := range compatible {
skip := false
for _, catlVersion := range catlVersions {
@ -116,49 +213,141 @@ is up to the end-user to decide.
}
}
logrus.Debugf("detected compatible upgradable tags '%s' for '%s'", compatibleStrings, service.Name)
logrus.Debugf("detected compatible upgradable tags %s for %s", compatibleStrings, service.Name)
var upgradeTag string
if bumpType != 0 {
for _, upTag := range compatible {
upElement, err := tag.UpgradeDelta(upTag)
if err != nil {
return err
_, ok := servicePins[service.Name]
if versionsPresent && ok {
pinnedTag := servicePins[service.Name].version
if tag.IsLessThan(pinnedTag) {
pinnedTagString := pinnedTag.String()
contains := false
for _, v := range compatible {
if pinnedTag.IsUpgradeCompatible(v) {
contains = true
upgradeTag = v.String()
break
}
}
delta := upElement.UpgradeType()
if delta <= bumpType {
upgradeTag = upTag.String()
break
if contains {
logrus.Infof("upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString)
} else {
logrus.Infof("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString)
continue
}
}
if upgradeTag == "" {
logrus.Warnf("not upgrading from '%s' to '%s' for '%s', because the upgrade type is more serious than what user wants.", tag.String(), compatible[0].String(), image)
} else {
logrus.Fatalf("service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String())
continue
}
} else {
msg := fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
tag := img.(reference.NamedTagged).Tag()
logrus.Warning(fmt.Sprintf("unable to determine versioning semantics of '%s', listing all tags", tag))
msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
compatibleStrings = []string{}
for _, regVersion := range regVersions {
compatibleStrings = append(compatibleStrings, regVersion.Name)
if bumpType != 0 {
for _, upTag := range compatible {
upElement, err := tag.UpgradeDelta(upTag)
if err != nil {
return err
}
delta := upElement.UpgradeType()
if delta <= bumpType {
upgradeTag = upTag.String()
break
}
}
if upgradeTag == "" {
logrus.Warnf("not upgrading from %s to %s for %s, because the upgrade type is more serious than what user wants", tag.String(), compatible[0].String(), image)
continue
}
} else {
msg := fmt.Sprintf("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, tag)
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || internal.AllTags {
tag := img.(reference.NamedTagged).Tag()
if !internal.AllTags {
logrus.Warning(fmt.Sprintf("unable to determine versioning semantics of %s, listing all tags", tag))
}
msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
compatibleStrings = []string{"skip"}
for _, regVersion := range regVersions {
compatibleStrings = append(compatibleStrings, regVersion)
}
}
// there is always at least the item "skip" in compatibleStrings (a list of
// possible upgradable tags) and at least one other tag.
upgradableTags := compatibleStrings[1:]
upgrade := anUpgrade{
Service: service.Name,
Image: image,
Tag: tag.String(),
UpgradeTags: make([]string, len(upgradableTags)),
}
for n, s := range upgradableTags {
var sb strings.Builder
if _, err := sb.WriteString(s); err != nil {
}
upgrade.UpgradeTags[n] = sb.String()
}
upgradeList[upgrade.Service] = upgrade
if internal.NoInput {
upgradeTag = "skip"
} else {
prompt := &survey.Select{
Message: msg,
Help: "enter / return to confirm, choose 'skip' to not upgrade this tag, vim mode is enabled",
VimMode: true,
Options: compatibleStrings,
}
if err := survey.AskOne(prompt, &upgradeTag); err != nil {
logrus.Fatal(err)
}
}
}
prompt := &survey.Select{
Message: msg,
Options: compatibleStrings,
}
if err := survey.AskOne(prompt, &upgradeTag); err != nil {
}
if upgradeTag != "skip" {
ok, err := recipe.UpdateTag(image, upgradeTag)
if err != nil {
logrus.Fatal(err)
}
if ok {
logrus.Infof("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image)
}
} else {
if !internal.NoInput {
logrus.Warnf("not upgrading %s, skipping as requested", image)
}
}
}
if internal.NoInput {
if internal.MachineReadable {
jsonstring, err := json.Marshal(upgradeList)
if err != nil {
logrus.Fatal(err)
}
fmt.Println(string(jsonstring))
return nil
}
if err := recipe.UpdateTag(image, upgradeTag); err != nil {
for _, upgrade := range upgradeList {
logrus.Infof("can upgrade service: %s, image: %s, tag: %s ::\n", upgrade.Service, upgrade.Image, upgrade.Tag)
for _, utag := range upgrade.UpgradeTags {
logrus.Infof(" %s\n", utag)
}
}
}
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)
}
logrus.Infof("tag upgraded from '%s' to '%s' for '%s'", tag.String(), upgradeTag, image)
}
return nil

View File

@ -1,45 +1,85 @@
package recipe
import (
"coopcloud.tech/abra/cli/formatter"
"fmt"
"sort"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/olekukonko/tablewriter"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var recipeVersionCommand = &cli.Command{
Name: "versions",
Usage: "List recipe versions",
Aliases: []string{"v"},
ArgsUsage: "<recipe>",
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{
Name: "versions",
Aliases: []string{"v"},
Usage: "List recipe versions",
ArgsUsage: "<recipe>",
Description: "Versions are read from the recipe catalogue.",
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
internal.NoInputFlag,
internal.MachineReadableFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
catalogue, err := catalogue.ReadRecipeCatalogue()
catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
if err != nil {
logrus.Fatal(err)
}
recipeMeta, ok := catalogue[recipe.Name]
recipeMeta, ok := catl[recipe.Name]
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", "Digest"}
table := formatter.CreateTable(tableCol)
if len(recipeMeta.Versions) == 0 {
logrus.Fatalf("%s has no catalogue published versions?", recipe.Name)
}
for _, serviceVersion := range recipeMeta.Versions {
for tag, meta := range serviceVersion {
for i := len(recipeMeta.Versions) - 1; i >= 0; 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 {
table.Append([]string{tag, service, serviceMeta.Image, serviceMeta.Tag, serviceMeta.Digest})
versions = append(versions, []string{version, service, serviceMeta.Image, serviceMeta.Tag})
}
sort.Slice(versions, sortServiceByName(versions))
for _, version := range versions {
table.Append(version)
}
if internal.MachineReadable {
table.JSONRender()
} else {
table.SetAutoMergeCellsByColumnIndex([]int{0})
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.Render()
fmt.Println()
}
}
}
table.SetAutoMergeCells(true)
table.Render()
return nil
},
}

View File

@ -1,137 +1,180 @@
package server
import (
"context"
"errors"
"os/user"
"strings"
"os"
"path/filepath"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
contextPkg "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/dns"
"coopcloud.tech/abra/pkg/server"
sshPkg "coopcloud.tech/abra/pkg/ssh"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var local bool
var localFlag = &cli.BoolFlag{
Name: "local",
Aliases: []string{"L"},
Value: false,
Usage: "Set up the local server",
Name: "local, l",
Usage: "Use local server",
Destination: &local,
}
var serverAddCommand = &cli.Command{
Name: "add",
Usage: "Add a new server",
func cleanUp(domainName string) {
if domainName != "default" {
logrus.Infof("cleaning up context for %s", domainName)
if err := client.DeleteContext(domainName); err != nil {
logrus.Fatal(err)
}
}
logrus.Infof("attempting to clean up server directory for %s", domainName)
serverDir := filepath.Join(config.SERVERS_DIR, domainName)
files, err := config.GetAllFilesInDirectory(serverDir)
if err != nil {
logrus.Fatalf("unable to list files in %s: %s", serverDir, err)
}
if len(files) > 0 {
logrus.Warnf("aborting clean up of %s because it is not empty", serverDir)
return
}
if err := os.RemoveAll(serverDir); err != nil {
logrus.Fatal(err)
}
}
// newContext creates a new internal Docker context for a server. This is how
// Docker manages SSH connection details. These are stored to disk in
// ~/.docker. Abra can manage this completely for the user, so it's an
// implementation detail.
func newContext(c *cli.Context, domainName, username, port string) error {
store := contextPkg.NewDefaultDockerContextStore()
contexts, err := store.Store.List()
if err != nil {
return err
}
for _, context := range contexts {
if context.Name == domainName {
logrus.Debugf("context for %s already exists", domainName)
return nil
}
}
logrus.Debugf("creating context with domain %s, username %s and port %s", domainName, username, port)
if err := client.CreateContext(domainName, username, port); err != nil {
return err
}
return nil
}
// createServerDir creates the ~/.abra/servers/... directory for a new server.
func createServerDir(domainName string) error {
if err := server.CreateServerDir(domainName); err != nil {
if !os.IsExist(err) {
return err
}
logrus.Debugf("server dir for %s already created", domainName)
}
return nil
}
var serverAddCommand = cli.Command{
Name: "add",
Aliases: []string{"a"},
Usage: "Add a server to your configuration",
Description: `
This command adds a new server that abra will communicate with, to deploy apps.
Add a new server to your configuration so that it can be managed by Abra.
Abra uses the SSH command-line to discover connection details for your server.
It is advised to configure an entry per-host in your ~/.ssh/config for each
server. For example:
Host example.com
Hostname example.com
User exampleUser
Port 12345
IdentityFile ~/.ssh/example@somewhere
Abra can then load SSH connection details from this configuratiion with:
abra server add example.com
If "--local" is passed, then Abra assumes that the current local server is
intended as the target server.
Otherwise, you may specify a remote server. The <domain> argument must be a
publicy accessible domain name which points to your server. You should have SSH
access to this server, Abra will assume port 22 and will use your current
system username to make an initial connection. You can use the <user> and
<port> arguments to adjust this.
For example:
abra server add varia.zone glodemodem 12345
Abra will construct the following SSH connection string then:
ssh://globemodem@varia.zone:12345
All communication between Abra and the server will use this SSH connection.
intended as the target server. This is useful when you want to have your entire
Co-op Cloud config located on the server itself, and not on your local
developer machine.
`,
Aliases: []string{"a"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
localFlag,
},
ArgsUsage: "<domain> [<user>] [<port>]",
Before: internal.SubCommandBefore,
ArgsUsage: "<domain>",
Action: func(c *cli.Context) error {
if c.Args().Len() == 1 && !local {
err := errors.New("missing arguments <domain> or '--local'")
if len(c.Args()) > 0 && local || !internal.ValidateSubCmdFlags(c) {
err := errors.New("cannot use <domain> and --local together")
internal.ShowSubcommandHelpAndError(c, err)
}
if c.Args().Get(1) != "" && local {
err := errors.New("cannot use '<domain>' and '--local' together")
internal.ShowSubcommandHelpAndError(c, err)
var domainName string
if local {
domainName = "default"
} else {
domainName = internal.ValidateDomain(c)
}
domainName := "default"
if local {
if err := server.CreateServerDir(domainName); err != nil {
if err := createServerDir(domainName); err != nil {
logrus.Fatal(err)
}
logrus.Info("local server has been added")
logrus.Infof("attempting to create client for %s", domainName)
if _, err := client.New(domainName); err != nil {
cleanUp(domainName)
logrus.Fatal(err)
}
logrus.Info("local server added")
return nil
}
domainName = internal.ValidateDomain(c)
var username string
var port string
username = c.Args().Get(1)
if username == "" {
systemUser, err := user.Current()
if err != nil {
logrus.Fatal(err)
}
username = systemUser.Username
if _, err := dns.EnsureIPv4(domainName); err != nil {
logrus.Fatal(err)
}
port = c.Args().Get(2)
if port == "" {
port = "22"
if err := createServerDir(domainName); err != nil {
logrus.Fatal(err)
}
store := client.NewDefaultDockerContextStore()
contexts, err := store.Store.List()
hostConfig, err := sshPkg.GetHostConfig(domainName)
if err != nil {
logrus.Fatal(err)
}
for _, context := range contexts {
if context.Name == domainName {
logrus.Fatalf("server at '%s' already exists?", domainName)
}
}
logrus.Debugf("creating context with domain '%s', username '%s' and port '%s'", domainName, username, port)
if err := client.CreateContext(domainName, username, port); err != nil {
if err := newContext(c, domainName, hostConfig.User, hostConfig.Port); err != nil {
logrus.Fatal(err)
}
ctx := context.Background()
cl, err := client.New(domainName)
if err != nil {
logrus.Fatal(err)
logrus.Infof("attempting to create client for %s", domainName)
if _, err := client.New(domainName); err != nil {
cleanUp(domainName)
logrus.Debugf("failed to construct client for %s, saw %s", domainName, err.Error())
logrus.Fatal(sshPkg.Fatal(domainName, err))
}
if _, err := cl.Info(ctx); err != nil {
if strings.Contains(err.Error(), "command not found") {
logrus.Fatalf("docker is not installed on '%s'?", domainName)
} else {
logrus.Fatalf("unable to make a connection to '%s'?", domainName)
}
logrus.Debug(err)
}
logrus.Debugf("remote connection to '%s' is definitely up", domainName)
if err := server.CreateServerDir(domainName); err != nil {
logrus.Fatal(err)
}
logrus.Infof("server at '%s' has been added", domainName)
logrus.Infof("%s added", domainName)
return nil
},

View File

@ -1,81 +0,0 @@
package server
import (
"context"
"fmt"
"net"
"time"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var serverInitCommand = &cli.Command{
Name: "init",
Usage: "Initialise server for deploying apps",
Aliases: []string{"i"},
HideHelp: true,
ArgsUsage: "<domain>",
Description: `
Initialise swarm mode on the target <domain>.
This initialisation explicitly chooses the "single host swarm" mode which uses
the default IPv4 address as the advertising address. This can be re-configured
later for more advanced use cases.
`,
Action: func(c *cli.Context) error {
domainName := internal.ValidateDomain(c)
cl, err := client.New(domainName)
if err != nil {
return err
}
// https://www.privacy-handbuch.de/handbuch_93d.htm
freifunkDNS := "5.1.66.255:53"
resolver := &net.Resolver{
PreferGo: false,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{
Timeout: time.Millisecond * time.Duration(10000),
}
// comrade librehosters DNS resolver https://snopyta.org/service/dns/
return d.DialContext(ctx, "udp", freifunkDNS)
},
}
logrus.Debugf("created DNS resolver via '%s'", freifunkDNS)
ctx := context.Background()
ips, err := resolver.LookupIPAddr(ctx, domainName)
if err != nil {
logrus.Fatal(err)
}
if len(ips) == 0 {
return fmt.Errorf("unable to retrieve ipv4 address for %s", domainName)
}
ipv4 := ips[0].IP.To4().String()
initReq := swarm.InitRequest{
ListenAddr: "0.0.0.0:2377",
AdvertiseAddr: ipv4,
}
if _, err := cl.SwarmInit(ctx, initReq); err != nil {
return err
}
logrus.Debugf("initialised swarm on '%s'", domainName)
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
if _, err := cl.NetworkCreate(ctx, "proxy", netOpts); err != nil {
return err
}
logrus.Debug("swarm overlay network 'proxy' created")
return nil
},
}

View File

@ -3,29 +3,43 @@ package server
import (
"strings"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/formatter"
"github.com/docker/cli/cli/connhelper/ssh"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var serverListCommand = &cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List managed servers",
ArgsUsage: " ",
HideHelp: true,
var problemsFilter bool
var problemsFilterFlag = &cli.BoolFlag{
Name: "problems, p",
Usage: "Show only servers with potential connection problems",
Destination: &problemsFilter,
}
var serverListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List managed servers",
Flags: []cli.Flag{
problemsFilterFlag,
internal.DebugFlag,
internal.MachineReadableFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
dockerContextStore := client.NewDefaultDockerContextStore()
dockerContextStore := context.NewDefaultDockerContextStore()
contexts, err := dockerContextStore.Store.List()
if err != nil {
logrus.Fatal(err)
}
tableColumns := []string{"Name", "Connection"}
tableColumns := []string{"name", "host", "user", "port"}
table := formatter.CreateTable(tableColumns)
defer table.Render()
serverNames, err := config.ReadServerNames()
if err != nil {
@ -35,23 +49,60 @@ var serverListCommand = &cli.Command{
for _, serverName := range serverNames {
var row []string
for _, ctx := range contexts {
endpoint, err := client.GetContextEndpoint(ctx)
endpoint, err := context.GetContextEndpoint(ctx)
if err != nil && strings.Contains(err.Error(), "does not exist") {
// No local context found, we can continue safely
continue
}
if ctx.Name == serverName {
row = []string{serverName, endpoint}
sp, err := ssh.ParseURL(endpoint)
if err != nil {
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}
}
}
if len(row) == 0 {
if serverName == "default" {
row = []string{serverName, "local"}
row = []string{serverName, "local", "n/a", "n/a"}
} else {
row = []string{serverName, "unknown"}
row = []string{serverName, "unknown", "unknown", "unknown"}
}
}
table.Append(row)
if problemsFilter {
for _, val := range row {
if val == "unknown" {
table.Append(row)
break
}
}
} else {
table.Append(row)
}
}
if internal.MachineReadable {
table.JSONRender()
} else {
if problemsFilter && table.NumLines() == 0 {
logrus.Info("all servers wired up correctly 👏")
} else {
table.Render()
}
}
return nil

View File

@ -1,264 +0,0 @@
package server
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"time"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"github.com/hetznercloud/hcloud-go/hcloud"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var hetznerCloudType string
var hetznerCloudImage string
var hetznerCloudSSHKeys cli.StringSlice
var hetznerCloudLocation string
var hetznerCloudAPIToken string
var serverNewHetznerCloudCommand = &cli.Command{
Name: "hetzner",
Usage: "Create a new Hetzner virtual server",
ArgsUsage: "<name>",
Description: `
Create a new Hetzner virtual server.
This command uses the uses the Hetzner Cloud API bindings to send a server
creation request. You must already have a Hetzner Cloud account and an account
API token before using this command.
Your token can be loaded from the environment using the HCLOUD_TOKEN
environment variable or otherwise passing the "--env/-e" flag.
`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
Usage: "Server type",
Destination: &hetznerCloudType,
Value: "cx11",
},
&cli.StringFlag{
Name: "image",
Aliases: []string{"i"},
Usage: "Image type",
Value: "debian-10",
Destination: &hetznerCloudImage,
},
&cli.StringSliceFlag{
Name: "ssh-keys",
Aliases: []string{"s"},
Usage: "SSH keys",
Destination: &hetznerCloudSSHKeys,
},
&cli.StringFlag{
Name: "location",
Aliases: []string{"l"},
Usage: "Server location",
Value: "hel1",
Destination: &hetznerCloudLocation,
},
&cli.StringFlag{
Name: "token",
Aliases: []string{"T"},
Usage: "Hetzner Cloud API token",
EnvVars: []string{"HCLOUD_TOKEN"},
Destination: &hetznerCloudAPIToken,
},
},
Action: func(c *cli.Context) error {
name := c.Args().First()
if name == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no name provided"))
}
if hetznerCloudAPIToken == "" {
logrus.Fatal("Hetzner Cloud API token is missing")
}
ctx := context.Background()
client := hcloud.NewClient(hcloud.WithToken(hetznerCloudAPIToken))
logrus.Debugf("successfully created hetzner cloud API client")
var sshKeys []*hcloud.SSHKey
for _, sshKey := range c.StringSlice("ssh-keys") {
sshKey, _, err := client.SSHKey.GetByName(ctx, sshKey)
if err != nil {
logrus.Fatal(err)
}
sshKeys = append(sshKeys, sshKey)
}
serverOpts := hcloud.ServerCreateOpts{
Name: name,
ServerType: &hcloud.ServerType{Name: hetznerCloudType},
Image: &hcloud.Image{Name: hetznerCloudImage},
SSHKeys: sshKeys,
Location: &hcloud.Location{Name: hetznerCloudLocation},
}
res, _, err := client.Server.Create(ctx, serverOpts)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("new server '%s' created", name)
tableColumns := []string{"Name", "IPv4", "Root Password"}
table := formatter.CreateTable(tableColumns)
if len(sshKeys) > 0 {
table.Append([]string{name, res.Server.PublicNet.IPv4.IP.String(), "N/A (using SSH keys)"})
} else {
table.Append([]string{name, res.Server.PublicNet.IPv4.IP.String(), res.RootPassword})
}
table.Render()
return nil
},
}
var capsulInstance string
var capsulType string
var capsulImage string
var capsulSSHKey string
var capsulAPIToken string
var serverNewCapsulCommand = &cli.Command{
Name: "capsul",
Usage: "Create a new Capsul virtual server",
ArgsUsage: "<name>",
Description: `
Create a new Capsul virtual server.
This command uses the uses the Capsul API bindings of your chosen instance to
send a server creation request. You must already have an account on your chosen
Capsul instance before using this command.
Your token can be loaded from the environment using the CAPSUL_TOKEN
environment variable or otherwise passing the "--env/-e" flag.
`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "instance",
Aliases: []string{"I"},
Usage: "Capsul instance",
Destination: &capsulInstance,
Value: "yolo.servers.coop",
},
&cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
Usage: "Server type",
Value: "f1-xs",
Destination: &capsulType,
},
&cli.StringFlag{
Name: "image",
Aliases: []string{"i"},
Usage: "Image type",
Value: "debian10",
Destination: &capsulImage,
},
&cli.StringFlag{
Name: "ssh-key",
Aliases: []string{"s"},
Usage: "SSH key",
Value: "",
Destination: &capsulSSHKey,
},
&cli.StringFlag{
Name: "token",
Aliases: []string{"T"},
Usage: "Capsul instance API token",
EnvVars: []string{"CAPSUL_TOKEN"},
Destination: &capsulAPIToken,
},
},
Action: func(c *cli.Context) error {
name := c.Args().First()
if name == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no name provided"))
}
if capsulAPIToken == "" {
logrus.Fatal("Capsul API token is missing")
}
// yep, the response time is quite slow, something to fix on the Capsul side
client := &http.Client{Timeout: 20 * time.Second}
capsulCreateURL := fmt.Sprintf("https://%s/api/capsul/create", capsulInstance)
logrus.Debugf("using '%s' as capsul create url", capsulCreateURL)
values := map[string]string{
"name": name,
"size": capsulType,
"os": capsulImage,
"ssh_key_0": capsulSSHKey,
}
payload, err := json.Marshal(values)
if err != nil {
logrus.Fatal(err)
}
req, err := http.NewRequest("POST", capsulCreateURL, bytes.NewBuffer(payload))
if err != nil {
logrus.Fatal(err)
}
req.Header = http.Header{
"Content-Type": []string{"application/json"},
"Authorization": []string{capsulAPIToken},
}
res, err := client.Do(req)
if err != nil {
logrus.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
body, err := ioutil.ReadAll(res.Body)
if err != nil {
panic(err)
}
logrus.Fatal(string(body))
}
type capsulCreateResponse struct{ ID string }
var resp capsulCreateResponse
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("capsul created with ID: '%s'", resp.ID)
tableColumns := []string{"Name", "ID"}
table := formatter.CreateTable(tableColumns)
table.Append([]string{name, resp.ID})
table.Render()
return nil
},
}
var serverNewCommand = &cli.Command{
Name: "new",
Aliases: []string{"n"},
Usage: "Create a new server using a 3rd party provider",
Description: `
Use a provider plugin to create a new server which can then be used to house a
new Co-op Cloud installation.
`,
ArgsUsage: "<provider>",
Subcommands: []*cli.Command{
serverNewHetznerCloudCommand,
serverNewCapsulCommand,
},
}

103
cli/server/prune.go Normal file
View File

@ -0,0 +1,103 @@
package server
import (
"context"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var allFilter bool
var allFilterFlag = &cli.BoolFlag{
Name: "all, a",
Usage: "Remove all unused images not just dangling ones",
Destination: &allFilter,
}
var volumesFilter bool
var volumesFilterFlag = &cli.BoolFlag{
Name: "volumes, v",
Usage: "Prune volumes. This will remove app data, Be Careful!",
Destination: &volumesFilter,
}
var serverPruneCommand = cli.Command{
Name: "prune",
Aliases: []string{"p"},
Usage: "Prune a managed server; Runs a docker system prune",
Description: `
Prunes unused containers, networks, and dangling images.
If passing "-v/--volumes" then volumes not connected to a deployed app will
also be removed. This can result in unwanted data loss if not used carefully.
`,
ArgsUsage: "[<server>]",
Flags: []cli.Flag{
allFilterFlag,
volumesFilterFlag,
internal.DebugFlag,
internal.OfflineFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.ServerNameComplete,
Action: func(c *cli.Context) error {
serverName := internal.ValidateServer(c)
cl, err := client.New(serverName)
if err != nil {
logrus.Fatal(err)
}
var args filters.Args
ctx := context.Background()
cr, err := cl.ContainersPrune(ctx, args)
if err != nil {
logrus.Fatal(err)
}
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
logrus.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed)
nr, err := cl.NetworksPrune(ctx, args)
if err != nil {
logrus.Fatal(err)
}
logrus.Infof("networks pruned: %d", len(nr.NetworksDeleted))
pruneFilters := filters.NewArgs()
if allFilter {
logrus.Debugf("removing all images, not only dangling ones")
pruneFilters.Add("dangling", "false")
}
ir, err := cl.ImagesPrune(ctx, pruneFilters)
if err != nil {
logrus.Fatal(err)
}
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
logrus.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed)
if volumesFilter {
vr, err := cl.VolumesPrune(ctx, args)
if err != nil {
logrus.Fatal(err)
}
volSpaceReclaimed := formatter.ByteCountSI(vr.SpaceReclaimed)
logrus.Infof("volumes pruned: %d; space reclaimed: %s", len(vr.VolumesDeleted), volSpaceReclaimed)
}
return nil
},
}

View File

@ -1,29 +1,47 @@
package server
import (
"os"
"path/filepath"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
var serverRemoveCommand = &cli.Command{
Name: "remove",
Aliases: []string{"rm"},
Usage: "Remove a server",
Description: `
This does not destroy the actual server. It simply removes it from Abra
internal bookkeeping so that it is not managed any more.
`,
HideHelp: true,
Action: func(c *cli.Context) error {
domainName := internal.ValidateDomain(c)
var serverRemoveCommand = cli.Command{
Name: "remove",
Aliases: []string{"rm"},
ArgsUsage: "<server>",
Usage: "Remove a managed server",
Description: `Remove a managed server.
if err := client.DeleteContext(domainName); err != nil {
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{
internal.DebugFlag,
internal.NoInputFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.ServerNameComplete,
Action: func(c *cli.Context) error {
serverName := internal.ValidateServer(c)
if err := client.DeleteContext(serverName); err != nil {
logrus.Fatal(err)
}
logrus.Infof("server at '%s' has been forgotten", domainName)
if err := os.RemoveAll(filepath.Join(config.SERVERS_DIR, serverName)); err != nil {
logrus.Fatal(err)
}
logrus.Infof("server at %s has been lost in time, like tears in rain", serverName)
return nil
},

View File

@ -1,26 +1,18 @@
package server
import (
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
// ServerCommand defines the `abra server` command and its subcommands
var ServerCommand = &cli.Command{
var ServerCommand = cli.Command{
Name: "server",
Aliases: []string{"s"},
Usage: "Manage servers",
Description: `
Manage the lifecycle of a server.
These commands support creating new servers using 3rd party integrations,
initialising existing servers to support Co-op Cloud deployments and managing
the connections to those servers.
`,
Subcommands: []*cli.Command{
serverNewCommand,
serverInitCommand,
Subcommands: []cli.Command{
serverAddCommand,
serverListCommand,
serverRemoveCommand,
serverPruneCommand,
},
}

498
cli/updater/updater.go Normal file
View File

@ -0,0 +1,498 @@
package updater
import (
"context"
"fmt"
"os"
"strconv"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/convert"
"coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerclient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
const SERVER = "localhost"
var majorUpdate bool
var majorFlag = &cli.BoolFlag{
Name: "major, m",
Usage: "Also check for major updates",
Destination: &majorUpdate,
}
var updateAll bool
var allFlag = &cli.BoolFlag{
Name: "all, a",
Usage: "Update all deployed apps",
Destination: &updateAll,
}
// Notify checks for available upgrades
var Notify = cli.Command{
Name: "notify",
Aliases: []string{"n"},
Usage: "Check for available upgrades",
Flags: []cli.Flag{
internal.DebugFlag,
majorFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `
It reads the deployed app versions and looks for new versions in the recipe
catalogue. If a new patch/minor version is available, a notification is
printed. To include major versions use the --major flag.
`,
Action: func(c *cli.Context) error {
cl, err := client.New("default")
if err != nil {
logrus.Fatal(err)
}
stacks, err := stack.GetStacks(cl)
if err != nil {
logrus.Fatal(err)
}
for _, stackInfo := range stacks {
stackName := stackInfo.Name
recipeName, err := getLabel(cl, stackName, "recipe")
if err != nil {
logrus.Fatal(err)
}
if recipeName != "" {
_, err = getLatestUpgrade(cl, stackName, recipeName)
if err != nil {
logrus.Fatal(err)
}
}
}
return nil
},
}
// UpgradeApp upgrades apps.
var UpgradeApp = cli.Command{
Name: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade apps",
ArgsUsage: "<stack-name> <recipe>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.ChaosFlag,
majorFlag,
allFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `
Upgrade an app by specifying its stack name and recipe. By passing "--all"
instead, every deployed app is upgraded. For each apps with enabled auto
updates the deployed version is compared with the current recipe catalogue
version. If a new patch/minor version is available, the app is upgraded. To
include major versions use the "--major" flag. Don't do that, it will probably
break things. Only apps that are not deployed with "--chaos" are upgraded, to
update chaos deployments use the "--chaos" flag. Use it with care.
`,
Action: func(c *cli.Context) error {
cl, err := client.New("default")
if err != nil {
logrus.Fatal(err)
}
if !updateAll {
stackName := c.Args().Get(0)
recipeName := c.Args().Get(1)
err = tryUpgrade(cl, stackName, recipeName)
if err != nil {
logrus.Fatal(err)
}
return nil
}
stacks, err := stack.GetStacks(cl)
if err != nil {
logrus.Fatal(err)
}
for _, stackInfo := range stacks {
stackName := stackInfo.Name
recipeName, err := getLabel(cl, stackName, "recipe")
if err != nil {
logrus.Fatal(err)
}
err = tryUpgrade(cl, stackName, recipeName)
if err != nil {
logrus.Fatal(err)
}
}
return nil
},
}
// getLabel reads docker labels from running services in the format of "coop-cloud.${STACK_NAME}.${LABEL}".
func getLabel(cl *dockerclient.Client, stackName string, label string) (string, error) {
filter := filters.NewArgs()
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter})
if err != nil {
return "", err
}
for _, service := range services {
labelKey := fmt.Sprintf("coop-cloud.%s.%s", stackName, label)
if labelValue, ok := service.Spec.Labels[labelKey]; ok {
return labelValue, nil
}
}
logrus.Debugf("no %s label found for %s", label, stackName)
return "", nil
}
// getBoolLabel reads a boolean docker label from running services
func getBoolLabel(cl *dockerclient.Client, stackName string, label string) (bool, error) {
lableValue, err := getLabel(cl, stackName, label)
if err != nil {
return false, err
}
if lableValue != "" {
value, err := strconv.ParseBool(lableValue)
if err != nil {
return false, err
}
return value, nil
}
logrus.Debugf("Boolean label %s could not be found for %s, set default to false.", label, stackName)
return false, nil
}
// getEnv reads env variables from docker services.
func getEnv(cl *dockerclient.Client, stackName string) (config.AppEnv, error) {
envMap := make(map[string]string)
filter := filters.NewArgs()
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter})
if err != nil {
return nil, err
}
for _, service := range services {
envList := service.Spec.TaskTemplate.ContainerSpec.Env
for _, envString := range envList {
splitString := strings.SplitN(envString, "=", 2)
if len(splitString) != 2 {
logrus.Debugf("can't separate key from value: %s (this variable is probably unset)", envString)
continue
}
k := splitString[0]
v := splitString[1]
logrus.Debugf("For %s read env %s with value: %s from docker service", stackName, k, v)
envMap[k] = v
}
}
return envMap, nil
}
// getLatestUpgrade returns the latest available version for an app respecting
// the "--major" flag if it is newer than the currently deployed version.
func getLatestUpgrade(cl *dockerclient.Client, stackName string, recipeName string) (string, error) {
deployedVersion, err := getDeployedVersion(cl, stackName, recipeName)
if err != nil {
return "", err
}
availableUpgrades, err := getAvailableUpgrades(cl, stackName, recipeName, deployedVersion)
if err != nil {
return "", err
}
if len(availableUpgrades) == 0 {
logrus.Debugf("no available upgrades for %s", stackName)
return "", nil
}
var chosenUpgrade string
if len(availableUpgrades) > 0 {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
logrus.Infof("%s (%s) can be upgraded from version %s to %s", stackName, recipeName, deployedVersion, chosenUpgrade)
}
return chosenUpgrade, nil
}
// getDeployedVersion returns the currently deployed version of an app.
func getDeployedVersion(cl *dockerclient.Client, stackName string, recipeName string) (string, error) {
logrus.Debugf("Retrieve deployed version whether %s is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
return "", err
}
if !isDeployed {
return "", fmt.Errorf("%s is not deployed?", stackName)
}
if deployedVersion == "unknown" {
return "", fmt.Errorf("failed to determine deployed version of %s", stackName)
}
return deployedVersion, nil
}
// getAvailableUpgrades returns all available versions of an app that are newer
// than the deployed version. It only includes major upgrades if the "--major"
// flag is set.
func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName string,
deployedVersion string) ([]string, error) {
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
return nil, err
}
versions, err := recipe.GetRecipeCatalogueVersions(recipeName, catl)
if err != nil {
return nil, err
}
if len(versions) == 0 {
logrus.Warnf("no published releases for %s in the recipe catalogue?", recipeName)
return nil, nil
}
var availableUpgrades []string
for _, version := range versions {
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
if err != nil {
return nil, err
}
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
return nil, err
}
versionDelta, err := parsedDeployedVersion.UpgradeDelta(parsedVersion)
if err != nil {
return nil, err
}
if 0 < versionDelta.UpgradeType() && (versionDelta.UpgradeType() < 4 || majorUpdate) {
availableUpgrades = append(availableUpgrades, version)
}
}
logrus.Debugf("Available updates for %s: %s", stackName, availableUpgrades)
return availableUpgrades, nil
}
// processRecipeRepoVersion clones, pulls, checks out the version and lints the
// recipe repository.
func processRecipeRepoVersion(recipeName, version string) error {
if err := recipe.EnsureExists(recipeName); err != nil {
return err
}
if err := recipe.EnsureUpToDate(recipeName); err != nil {
return err
}
if err := recipe.EnsureVersion(recipeName, version); err != nil {
return err
}
if r, err := recipe.Get(recipeName, internal.Offline); err != nil {
return err
} else if err := lint.LintForErrors(r); err != nil {
return err
}
return nil
}
// mergeAbraShEnv merges abra.sh env vars into the app env vars.
func mergeAbraShEnv(recipeName string, env config.AppEnv) error {
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, recipeName, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
return err
}
for k, v := range abraShEnv {
logrus.Debugf("read v:%s k: %s", v, k)
env[k] = v
}
return nil
}
// createDeployConfig merges and enriches the compose config for the deployment.
func createDeployConfig(recipeName string, stackName string, env config.AppEnv) (*composetypes.Config, stack.Deploy, error) {
env["STACK_NAME"] = stackName
deployOpts := stack.Deploy{
Namespace: stackName,
Prune: false,
ResolveImage: stack.ResolveImageAlways,
}
composeFiles, err := config.GetComposeFiles(recipeName, env)
if err != nil {
return nil, deployOpts, err
}
deployOpts.Composefiles = composeFiles
compose, err := config.GetAppComposeConfig(stackName, deployOpts, env)
if err != nil {
return nil, deployOpts, err
}
config.ExposeAllEnv(stackName, compose, env)
// after the upgrade the deployment won't be in chaos state anymore
config.SetChaosLabel(compose, stackName, false)
config.SetRecipeLabel(compose, stackName, recipeName)
config.SetUpdateLabel(compose, stackName, env)
return compose, deployOpts, nil
}
// tryUpgrade performs the upgrade if all the requirements are fulfilled.
func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
if recipeName == "" {
logrus.Debugf("don't update %s due to missing recipe name", stackName)
return nil
}
chaos, err := getBoolLabel(cl, stackName, "chaos")
if err != nil {
return err
}
if chaos && !internal.Chaos {
logrus.Debugf("don't update %s due to chaos deployment", stackName)
return nil
}
updatesEnabled, err := getBoolLabel(cl, stackName, "autoupdate")
if err != nil {
return err
}
if !updatesEnabled {
logrus.Debugf("don't update %s due to disabled auto updates or missing ENABLE_AUTO_UPDATE env", stackName)
return nil
}
upgradeVersion, err := getLatestUpgrade(cl, stackName, recipeName)
if err != nil {
return err
}
if upgradeVersion == "" {
logrus.Debugf("don't update %s due to no new version", stackName)
return nil
}
err = upgrade(cl, stackName, recipeName, upgradeVersion)
return err
}
// upgrade performs all necessary steps to upgrade an app.
func upgrade(cl *dockerclient.Client, stackName, recipeName,
upgradeVersion string) error {
env, err := getEnv(cl, stackName)
if err != nil {
return err
}
app := config.App{
Name: stackName,
Recipe: recipeName,
Server: SERVER,
Env: env,
}
if err = processRecipeRepoVersion(recipeName, upgradeVersion); err != nil {
return err
}
if err = mergeAbraShEnv(recipeName, app.Env); err != nil {
return err
}
compose, deployOpts, err := createDeployConfig(recipeName, stackName, app.Env)
if err != nil {
return err
}
logrus.Infof("upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion)
err = stack.RunDeploy(cl, deployOpts, compose, stackName, true)
return err
}
func newAbraApp(version, commit string) *cli.App {
app := &cli.App{
Name: "kadabra",
Usage: `The Co-op Cloud auto-updater
____ ____ _ _
/ ___|___ ___ _ __ / ___| | ___ _ _ __| |
| | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |
| |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |
\____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|
|_|
`,
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Commands: []cli.Command{
Notify,
UpgradeApp,
},
}
app.Before = func(c *cli.Context) error {
logrus.Debugf("kadabra version %s, commit %s", version, commit)
return nil
}
return app
}
// RunApp runs CLI abra app.
func RunApp(version, commit string) {
app := newAbraApp(version, commit)
if err := app.Run(os.Args); err != nil {
logrus.Fatal(err)
}
}

View File

@ -1,23 +0,0 @@
package cli
import (
"os/exec"
"coopcloud.tech/abra/cli/internal"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
// UpgradeCommand upgrades abra in-place.
var UpgradeCommand = &cli.Command{
Name: "upgrade",
Usage: "Upgrade abra",
Action: func(c *cli.Context) error {
cmd := exec.Command("bash", "-c", "curl -s https://install.abra.coopcloud.tech | bash")
logrus.Debugf("attempting to run '%s'", cmd)
if err := internal.RunCmd(cmd); err != nil {
logrus.Fatal(err)
}
return nil
},
}

View File

@ -1,15 +0,0 @@
package cli
import (
"github.com/urfave/cli/v2"
)
// VersionCommand prints the version of abra.
var VersionCommand = &cli.Command{
Name: "version",
Usage: "Print version",
Action: func(c *cli.Context) error {
cli.VersionPrinter(c)
return nil
},
}

View File

@ -5,14 +5,13 @@ import (
"coopcloud.tech/abra/cli"
)
// Version is the current version of abra.
// Version is the current version of Abra.
var Version string
// Commit is the current commit of abra.
// Commit is the current git commit of Abra.
var Commit string
func main() {
// If not set in the ld-flags
if Version == "" {
Version = "dev"
}

23
cmd/kadabra/main.go Normal file
View File

@ -0,0 +1,23 @@
// Package main provides the command-line entrypoint.
package main
import (
"coopcloud.tech/abra/cli/updater"
)
// Version is the current version of Kadabra.
var Version string
// Commit is the current git commit of Kadabra.
var Commit string
func main() {
if Version == "" {
Version = "dev"
}
if Commit == "" {
Commit = " "
}
updater.RunApp(Version, Commit)
}

126
go.mod
View File

@ -1,40 +1,120 @@
module coopcloud.tech/abra
go 1.16
go 1.21
require (
coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb
github.com/AlecAivazis/survey/v2 v2.3.1
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
github.com/docker/cli v20.10.8+incompatible
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v20.10.8+incompatible
github.com/docker/go-units v0.4.0
github.com/go-git/go-git/v5 v5.4.2
github.com/hetznercloud/hcloud-go v1.32.0
github.com/moby/sys/signal v0.5.0
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
github.com/docker/cli v24.0.7+incompatible
github.com/docker/distribution v2.8.3+incompatible
github.com/docker/docker v24.0.7+incompatible
github.com/docker/go-units v0.5.0
github.com/go-git/go-git/v5 v5.10.0
github.com/google/go-cmp v0.5.9
github.com/moby/sys/signal v0.7.0
github.com/moby/term v0.5.0
github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.8.3
github.com/schultz-is/passgen v1.0.1
github.com/sirupsen/logrus v1.8.1
github.com/urfave/cli/v2 v2.3.0
gotest.tools/v3 v3.0.3
github.com/schollz/progressbar/v3 v3.14.1
github.com/sirupsen/logrus v1.9.3
gotest.tools/v3 v3.5.1
)
require (
github.com/Microsoft/hcsshim v0.8.21 // indirect
github.com/containerd/containerd v1.5.5 // indirect
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/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.20 // 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.4.4 // 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.14.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 (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect
github.com/buger/goterm v1.0.4
github.com/containerd/containerd v1.5.9 // indirect
github.com/containers/image v3.0.2+incompatible
github.com/containers/storage v1.38.2 // indirect
github.com/decentral1se/passgen v1.0.1
github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/fvbommel/sortorder v1.0.2 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/moby/sys/mount v0.2.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/runc v1.0.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5
github.com/klauspost/pgzip v1.2.6
github.com/moby/patternmatcher 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/prometheus/client_golang v1.16.0 // indirect
github.com/sergi/go-diff v1.2.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/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/urfave/cli v1.22.9
github.com/xeipuuv/gojsonpointer v0.0.0-20190809123943-df4f5c81cb3b // indirect
golang.org/x/sys v0.14.0
)

764
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,9 @@
package app
import (
"context"
"fmt"
"strings"
"coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
apiclient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
@ -23,7 +19,7 @@ func Get(appName string) (config.App, error) {
return config.App{}, err
}
logrus.Debugf("retrieved '%s' for '%s'", app, appName)
logrus.Debugf("retrieved %s for %s", app, appName)
return app, nil
}
@ -37,49 +33,10 @@ type deployedServiceSpec struct {
// VersionSpec represents a deployed app and associated metadata.
type VersionSpec map[string]deployedServiceSpec
// DeployedVersions lists metadata (e.g. versions) for deployed
func DeployedVersions(ctx context.Context, cl *apiclient.Client, app config.App) (VersionSpec, bool, error) {
services, err := stack.GetStackServices(ctx, cl, app.StackName())
if err != nil {
return VersionSpec{}, false, err
}
appSpec := make(VersionSpec)
for _, service := range services {
serviceName := ParseServiceName(service.Spec.Name)
label := fmt.Sprintf("coop-cloud.%s.%s.version", app.StackName(), serviceName)
if deployLabel, ok := service.Spec.Labels[label]; ok {
version, _ := ParseVersionLabel(deployLabel)
appSpec[serviceName] = deployedServiceSpec{Name: serviceName, Version: version}
}
}
deployed := len(services) > 0
if deployed {
logrus.Debugf("detected '%s' as deployed versions of '%s'", appSpec, app.Name)
} else {
logrus.Debugf("detected '%s' as not deployed", app.Name)
}
return appSpec, len(services) > 0, nil
}
// ParseVersionLabel parses a $VERSION-$DIGEST app service label.
func ParseVersionLabel(label string) (string, string) {
// versions may look like v4.2-abcd or v4.2-alpine-abcd
idx := strings.LastIndex(label, "-")
version := label[:idx]
digest := label[idx+1:]
logrus.Debugf("parsed '%s' as version from '%s'", version, label)
logrus.Debugf("parsed '%s' as digest from '%s'", digest, label)
return version, digest
}
// ParseVersionName parses a $STACK_NAME_$SERVICE_NAME service label.
// ParseServiceName parses a $STACK_NAME_$SERVICE_NAME service label.
func ParseServiceName(label string) string {
idx := strings.LastIndex(label, "_")
serviceName := label[idx+1:]
logrus.Debugf("parsed '%s' as service name from '%s'", serviceName, label)
logrus.Debugf("parsed %s as service name from %s", serviceName, label)
return serviceName
}

View File

@ -0,0 +1,102 @@
package autocomplete
import (
"fmt"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// AppNameComplete copletes app names.
func AppNameComplete(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
}
func ServiceNameComplete(appName string) {
serviceNames, err := config.GetAppServiceNames(appName)
if err != nil {
return
}
for _, s := range serviceNames {
fmt.Println(s)
}
}
// RecipeNameComplete completes recipe names.
func RecipeNameComplete(c *cli.Context) {
catl, err := recipe.ReadRecipeCatalogue(false)
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for name := range catl {
fmt.Println(name)
}
}
// RecipeVersionComplete completes versions for the recipe.
func RecipeVersionComplete(recipeName string) {
catl, err := recipe.ReadRecipeCatalogue(false)
if err != nil {
logrus.Warn(err)
}
for _, v := range catl[recipeName].Versions {
for v2 := range v {
fmt.Println(v2)
}
}
}
// ServerNameComplete completes server names.
func ServerNameComplete(c *cli.Context) {
files, err := config.LoadAppFiles("")
if err != nil {
logrus.Fatal(err)
}
if c.NArg() > 0 {
return
}
for _, appFile := range files {
fmt.Println(appFile.Server)
}
}
// SubcommandComplete completes sub-commands.
func SubcommandComplete(c *cli.Context) {
if c.NArg() > 0 {
return
}
subcmds := []string{
"app",
"autocomplete",
"catalogue",
"recipe",
"server",
"upgrade",
}
for _, cmd := range subcmds {
fmt.Println(cmd)
}
}

View File

@ -1,500 +1,88 @@
// Package catalogue provides ways of interacting with recipe catalogues which
// are JSON data structures which contain meta information about recipes (e.g.
// what versions of the Nextcloud recipe are available?).
package catalogue
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"strings"
"time"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/web"
"github.com/docker/distribution/reference"
gitPkg "coopcloud.tech/abra/pkg/git"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus"
)
// RecipeCatalogueURL is the only current recipe catalogue available.
const RecipeCatalogueURL = "https://apps.coopcloud.tech"
// ReposMetadataURL is the recipe repository metadata
const ReposMetadataURL = "https://git.coopcloud.tech/api/v1/orgs/coop-cloud/repos"
// image represents a recipe container image.
type image struct {
Image string `json:"image"`
Rating string `json:"rating"`
Source string `json:"source"`
URL string `json:"url"`
}
// features represent what top-level features a recipe supports (e.g. does this
// recipe support backups?).
type features struct {
Backups string `json:"backups"`
Email string `json:"email"`
Healthcheck string `json:"healthcheck"`
Image image `json:"image"`
Status int `json:"status"`
Tests string `json:"tests"`
}
// tag represents a git tag.
type tag = string
// service represents a service within a recipe.
type service = string
// ServiceMeta represents meta info associated with a service.
type ServiceMeta struct {
Digest string `json:"digest"`
Image string `json:"image"`
Tag string `json:"tag"`
}
// RecipeVersions are the versions associated with a recipe.
type RecipeVersions []map[tag]map[service]ServiceMeta
// RecipeMeta represents metadata for a recipe in the abra catalogue.
type RecipeMeta struct {
Category string `json:"category"`
DefaultBranch string `json:"default_branch"`
Description string `json:"description"`
Features features `json:"features"`
Icon string `json:"icon"`
Name string `json:"name"`
Repository string `json:"repository"`
Versions RecipeVersions `json:"versions"`
Website string `json:"website"`
}
// LatestVersion returns the latest version of a recipe.
func (r RecipeMeta) LatestVersion() string {
var version string
// apps.json versions are sorted so the last key is latest
latest := r.Versions[len(r.Versions)-1]
for tag := range latest {
version = tag
}
logrus.Debugf("choosing '%s' as latest version of '%s'", version, r.Name)
return version
}
// Name represents a recipe name.
type Name = string
// RecipeCatalogue represents the entire recipe catalogue.
type RecipeCatalogue map[Name]RecipeMeta
// Flatten converts AppCatalogue to slice
func (r RecipeCatalogue) Flatten() []RecipeMeta {
recipes := make([]RecipeMeta, 0, len(r))
for name := range r {
recipes = append(recipes, r[name])
}
return recipes
}
// ByRecipeName sorts recipes by name.
type ByRecipeName []RecipeMeta
func (r ByRecipeName) Len() int { return len(r) }
func (r ByRecipeName) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r ByRecipeName) Less(i, j int) bool {
return strings.ToLower(r[i].Name) < strings.ToLower(r[j].Name)
}
// recipeCatalogueFSIsLatest checks whether the recipe catalogue stored locally
// is up to date.
func recipeCatalogueFSIsLatest() (bool, error) {
httpClient := &http.Client{Timeout: web.Timeout}
res, err := httpClient.Head(RecipeCatalogueURL)
if err != nil {
return false, err
}
lastModified := res.Header["Last-Modified"][0]
parsed, err := time.Parse(time.RFC1123, lastModified)
if err != nil {
return false, err
}
info, err := os.Stat(config.APPS_JSON)
if err != nil {
if os.IsNotExist(err) {
logrus.Debugf("no recipe catalogue found in file system cache")
return false, nil
// EnsureCatalogue ensures that the catalogue is cloned locally & present.
func EnsureCatalogue() error {
catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) {
logrus.Warnf("local recipe catalogue is missing, retrieving now")
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME)
if err := gitPkg.Clone(catalogueDir, url); err != nil {
return err
}
return false, err
logrus.Debugf("cloned catalogue repository to %s", catalogueDir)
}
localModifiedTime := info.ModTime().Unix()
remoteModifiedTime := parsed.Unix()
if localModifiedTime < remoteModifiedTime {
logrus.Debug("file system cached recipe catalogue is out-of-date")
return false, nil
}
logrus.Debug("file system cached recipe catalogue is up-to-date")
return true, nil
}
// ReadRecipeCatalogue reads the recipe catalogue.
func ReadRecipeCatalogue() (RecipeCatalogue, error) {
recipes := make(RecipeCatalogue)
recipeFSIsLatest, err := recipeCatalogueFSIsLatest()
if err != nil {
return nil, err
}
if !recipeFSIsLatest {
logrus.Debugf("reading recipe catalogue from web to get latest")
if err := readRecipeCatalogueWeb(&recipes); err != nil {
return nil, err
}
return recipes, nil
}
logrus.Debugf("reading recipe catalogue from file system cache to get latest")
if err := readRecipeCatalogueFS(&recipes); err != nil {
return nil, err
}
return recipes, nil
}
// readRecipeCatalogueFS reads the catalogue from the file system.
func readRecipeCatalogueFS(target interface{}) error {
recipesJSONFS, err := ioutil.ReadFile(config.APPS_JSON)
if err != nil {
return err
}
if err := json.Unmarshal(recipesJSONFS, &target); err != nil {
return err
}
logrus.Debugf("read recipe catalogue from file system cache in '%s'", config.APPS_JSON)
return nil
}
// readRecipeCatalogueWeb reads the catalogue from the web.
func readRecipeCatalogueWeb(target interface{}) error {
if err := web.ReadJSON(RecipeCatalogueURL, &target); err != nil {
return err
}
recipesJSON, err := json.MarshalIndent(target, "", " ")
// EnsureIsClean makes sure that the catalogue has no unstaged changes.
func EnsureIsClean() error {
isClean, err := gitPkg.IsClean(config.CATALOGUE_DIR)
if err != nil {
return err
}
if err := ioutil.WriteFile(config.APPS_JSON, recipesJSON, 0644); err != nil {
return err
if !isClean {
msg := "%s has locally unstaged changes? please commit/remove your changes before proceeding"
return fmt.Errorf(msg, config.CATALOGUE_DIR)
}
logrus.Debugf("read recipe catalogue from web at '%s'", RecipeCatalogueURL)
return nil
}
// VersionsOfService lists the version of a service.
func VersionsOfService(recipe, serviceName string) ([]string, error) {
catalogue, err := ReadRecipeCatalogue()
// EnsureUpToDate ensures that the local catalogue is up to date.
func EnsureUpToDate() error {
repo, err := git.PlainOpen(config.CATALOGUE_DIR)
if err != nil {
return nil, err
return err
}
rec, ok := catalogue[recipe]
if !ok {
return nil, fmt.Errorf("recipe '%s' does not exist?", recipe)
}
versions := []string{}
alreadySeen := make(map[string]bool)
for _, serviceVersion := range rec.Versions {
for tag := range serviceVersion {
if _, ok := alreadySeen[tag]; !ok {
alreadySeen[tag] = true
versions = append(versions, tag)
}
}
}
logrus.Debugf("detected versions '%s' for '%s'", strings.Join(versions, ", "), recipe)
return versions, nil
}
// GetRecipeMeta retrieves the recipe metadata from the recipe catalogue.
func GetRecipeMeta(recipeName string) (RecipeMeta, error) {
catl, err := ReadRecipeCatalogue()
remotes, err := repo.Remotes()
if err != nil {
return RecipeMeta{}, err
return err
}
recipeMeta, ok := catl[recipeName]
if !ok {
err := fmt.Errorf("recipe '%s' does not exist?", recipeName)
return RecipeMeta{}, err
}
if err := recipe.EnsureExists(recipeName); err != nil {
return RecipeMeta{}, err
}
logrus.Debugf("recipe metadata retrieved for '%s'", recipeName)
return recipeMeta, nil
}
// RepoMeta is a single recipe repo metadata.
type RepoMeta struct {
ID int `json:"id"`
Owner Owner
Name string `json:"name"`
FullName string `json:"full_name"`
Description string `json:"description"`
Empty bool `json:"empty"`
Private bool `json:"private"`
Fork bool `json:"fork"`
Template bool `json:"template"`
Parent interface{} `json:"parent"`
Mirror bool `json:"mirror"`
Size int `json:"size"`
HTMLURL string `json:"html_url"`
SSHURL string `json:"ssh_url"`
CloneURL string `json:"clone_url"`
OriginalURL string `json:"original_url"`
Website string `json:"website"`
StarsCount int `json:"stars_count"`
ForksCount int `json:"forks_count"`
WatchersCount int `json:"watchers_count"`
OpenIssuesCount int `json:"open_issues_count"`
OpenPRCount int `json:"open_pr_counter"`
ReleaseCounter int `json:"release_counter"`
DefaultBranch string `json:"default_branch"`
Archived bool `json:"archived"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Permissions Permissions
HasIssues bool `json:"has_issues"`
InternalTracker InternalTracker
HasWiki bool `json:"has_wiki"`
HasPullRequests bool `json:"has_pull_requests"`
HasProjects bool `json:"has_projects"`
IgnoreWhitespaceConflicts bool `json:"ignore_whitespace_conflicts"`
AllowMergeCommits bool `json:"allow_merge_commits"`
AllowRebase bool `json:"allow_rebase"`
AllowRebaseExplicit bool `json:"allow_rebase_explicit"`
AllowSquashMerge bool `json:"allow_squash_merge"`
AvatarURL string `json:"avatar_url"`
Internal bool `json:"internal"`
MirrorInterval string `json:"mirror_interval"`
}
// Owner is the repo organisation owner metadata.
type Owner struct {
ID int `json:"id"`
Login string `json:"login"`
FullName string `json:"full_name"`
Email string `json:"email"`
AvatarURL string `json:"avatar_url"`
Language string `json:"language"`
IsAdmin bool `json:"is_admin"`
LastLogin string `json:"last_login"`
Created string `json:"created"`
Restricted bool `json:"restricted"`
Username string `json:"username"`
}
// Permissions is perms metadata for a repo.
type Permissions struct {
Admin bool `json:"admin"`
Push bool `json:"push"`
Pull bool `json:"pull"`
}
// InternalTracker is issue tracker metadata for a repo.
type InternalTracker struct {
EnableTimeTracker bool `json:"enable_time_tracker"`
AllowOnlyContributorsToTrackTime bool `json:"allow_only_contributors_to_track_time"`
EnableIssuesDependencies bool `json:"enable_issue_dependencies"`
}
// RepoCatalogue represents all the recipe repo metadata.
type RepoCatalogue map[string]RepoMeta
// ReadReposMetadata retrieves coop-cloud/... repo metadata from Gitea.
func ReadReposMetadata() (RepoCatalogue, error) {
reposMeta := make(RepoCatalogue)
pageIdx := 1
for {
var reposList []RepoMeta
pagedURL := fmt.Sprintf("%s?page=%v", ReposMetadataURL, pageIdx)
logrus.Debugf("fetching repo metadata from '%s'", pagedURL)
if err := web.ReadJSON(pagedURL, &reposList); err != nil {
return reposMeta, err
}
if len(reposList) == 0 {
break
}
for idx, repo := range reposList {
reposMeta[repo.Name] = reposList[idx]
}
pageIdx++
}
return reposMeta, nil
}
// GetRecipeVersions retrieves all recipe versions.
func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
versions := RecipeVersions{}
recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName)
logrus.Debugf("attempting to open git repository in '%s'", recipeDir)
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return versions, err
if len(remotes) == 0 {
msg := "cannot ensure %s is up-to-date, no git remotes configured"
logrus.Debugf(msg, config.CATALOGUE_DIR)
return nil
}
worktree, err := repo.Worktree()
if err != nil {
logrus.Fatal(err)
return err
}
gitTags, err := repo.Tags()
branch, err := gitPkg.CheckoutDefaultBranch(repo, config.CATALOGUE_DIR)
if err != nil {
return versions, err
return err
}
if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
tag := strings.TrimPrefix(string(ref.Name()), "refs/tags/")
opts := &git.PullOptions{
Force: true,
ReferenceName: branch,
}
logrus.Debugf("processing '%s' for '%s'", tag, recipeName)
checkOutOpts := &git.CheckoutOptions{
Create: false,
Force: true,
Branch: plumbing.ReferenceName(ref.Name()),
}
if err := worktree.Checkout(checkOutOpts); err != nil {
logrus.Debugf("failed to check out '%s' in '%s'", tag, recipeDir)
if err := worktree.Pull(opts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") {
return err
}
logrus.Debugf("successfully checked out '%s' in '%s'", ref.Name(), recipeDir)
recipe, err := recipe.Get(recipeName)
if err != nil {
return err
}
versionMeta := make(map[string]ServiceMeta)
for _, service := range recipe.Config.Services {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return err
}
path := reference.Path(img)
if strings.Contains(path, "library") {
path = strings.Split(path, "/")[1]
}
digest, err := client.GetTagDigest(img)
if err != nil {
return err
}
versionMeta[service.Name] = ServiceMeta{
Digest: digest,
Image: path,
Tag: img.(reference.NamedTagged).Tag(),
}
logrus.Debugf("collecting digest: '%s', image: '%s', tag: '%s'", digest, path, tag)
}
versions = append(versions, map[string]map[string]ServiceMeta{tag: versionMeta})
return nil
}); err != nil {
return versions, err
}
branch := "master"
if _, err := repo.Branch("master"); err != nil {
if _, err := repo.Branch("main"); err != nil {
logrus.Debugf("failed to select branch in '%s'", recipeDir)
logrus.Fatal(err)
}
branch = "main"
}
logrus.Debugf("fetched latest git changes for %s", config.CATALOGUE_DIR)
refName := fmt.Sprintf("refs/heads/%s", branch)
checkOutOpts := &git.CheckoutOptions{
Create: false,
Force: true,
Branch: plumbing.ReferenceName(refName),
}
if err := worktree.Checkout(checkOutOpts); err != nil {
logrus.Debugf("failed to check out '%s' in '%s'", branch, recipeDir)
logrus.Fatal(err)
}
logrus.Debugf("switched back to '%s' in '%s'", branch, recipeDir)
logrus.Debugf("collected '%s' for '%s'", versions, recipeName)
return versions, nil
}
// GetRecipeCatalogueVersions list the recipe versions listed in the recipe catalogue.
func GetRecipeCatalogueVersions(recipeName string) ([]string, error) {
var versions []string
catl, err := ReadRecipeCatalogue()
if err != nil {
return versions, err
}
if recipeMeta, exists := catl[recipeName]; exists {
for _, versionMeta := range recipeMeta.Versions {
for tag := range versionMeta {
versions = append(versions, tag)
}
}
}
return versions, nil
return nil
}

View File

@ -2,39 +2,56 @@
package client
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"time"
contextPkg "coopcloud.tech/abra/pkg/context"
sshPkg "coopcloud.tech/abra/pkg/ssh"
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
// New initiates a new Docker client.
func New(contextName string) (*client.Client, error) {
context, err := GetContext(contextName)
if err != nil {
return nil, err
}
ctxEndpoint, err := GetContextEndpoint(context)
if err != nil {
return nil, err
}
helper := newConnectionHelper(ctxEndpoint)
httpClient := &http.Client{
// No tls, no proxy
Transport: &http.Transport{
DialContext: helper.Dialer,
},
}
// New initiates a new Docker client. New client connections are validated so
// that we ensure connections via SSH to the daemon can succeed. It takes into
// account that you may only want the local client and not communicate via SSH.
// For this use-case, please pass "default" as the contextName.
func New(serverName string) (*client.Client, error) {
var clientOpts []client.Opt
clientOpts = append(clientOpts,
client.WithHTTPClient(httpClient),
client.WithHost(helper.Host),
client.WithDialContext(helper.Dialer),
)
if serverName != "default" {
context, err := GetContext(serverName)
if err != nil {
return nil, fmt.Errorf("unknown server, run \"abra server add %s\"?", serverName)
}
ctxEndpoint, err := contextPkg.GetContextEndpoint(context)
if err != nil {
return nil, err
}
helper, err := commandconnPkg.NewConnectionHelper(ctxEndpoint)
if err != nil {
return nil, err
}
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: helper.Dialer,
IdleConnTimeout: 30 * time.Second,
},
}
clientOpts = append(clientOpts,
client.WithHTTPClient(httpClient),
client.WithHost(helper.Host),
client.WithDialContext(helper.Dialer),
)
}
version := os.Getenv("DOCKER_API_VERSION")
if version != "" {
@ -45,10 +62,23 @@ func New(contextName string) (*client.Client, error) {
cl, err := client.NewClientWithOpts(clientOpts...)
if err != nil {
logrus.Fatalf("unable to create Docker client: %s", err)
return nil, err
}
logrus.Debugf("created client for '%s'", contextName)
logrus.Debugf("created client for %s", serverName)
info, err := cl.Info(context.Background())
if err != nil {
return cl, sshPkg.Fatal(serverName, err)
}
if info.Swarm.LocalNodeState == "inactive" {
if serverName != "default" {
return cl, fmt.Errorf("swarm mode not enabled on %s?", serverName)
} else {
return cl, errors.New("swarm mode not enabled on local server?")
}
}
return cl, nil
}

View File

@ -1,45 +0,0 @@
package client
import (
"github.com/docker/cli/cli/connhelper"
"github.com/docker/cli/cli/context/docker"
dCliContextStore "github.com/docker/cli/cli/context/store"
dClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
func newConnectionHelper(daemonURL string) *connhelper.ConnectionHelper {
helper, err := connhelper.GetConnectionHelper(daemonURL)
if err != nil {
logrus.Fatal(err)
}
return helper
}
func getDockerEndpoint(host string) (docker.Endpoint, error) {
skipTLSVerify := false
ep := docker.Endpoint{
EndpointMeta: docker.EndpointMeta{
Host: host,
SkipTLSVerify: skipTLSVerify,
},
}
// try to resolve a docker client, validating the configuration
opts, err := ep.ClientOpts()
if err != nil {
return docker.Endpoint{}, err
}
if _, err := dClient.NewClientWithOpts(opts...); err != nil {
return docker.Endpoint{}, err
}
return ep, nil
}
func getDockerEndpointMetadataAndTLS(host string) (docker.EndpointMeta, *dCliContextStore.EndpointTLSData, error) {
ep, err := getDockerEndpoint(host)
if err != nil {
return docker.EndpointMeta{}, nil, err
}
return ep.EndpointMeta, ep.TLSData.ToStoreTLSData(), nil
}

View File

@ -1,191 +0,0 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2013-2017 Docker, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,7 +0,0 @@
# github.com/docker/cli/cli/command/container
Due to this literally just being copy-pasted from the lib, the Apache license
will be posted in this folder. Small edits to the source code have been to
function names and parts we don't need deleted.
Same vibe as [../convert](../convert).

View File

@ -4,12 +4,11 @@ import (
"errors"
"fmt"
command "github.com/docker/cli/cli/command"
"coopcloud.tech/abra/pkg/context"
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
dConfig "github.com/docker/cli/cli/config"
context "github.com/docker/cli/cli/context"
"github.com/docker/cli/cli/context/docker"
contextStore "github.com/docker/cli/cli/context/store"
"github.com/moby/term"
"github.com/sirupsen/logrus"
)
@ -27,13 +26,13 @@ func CreateContext(contextName string, user string, port string) error {
if err := createContext(contextName, host); err != nil {
return err
}
logrus.Debugf("created the '%s' context", contextName)
logrus.Debugf("created the %s context", contextName)
return nil
}
// createContext interacts with Docker Context to create a Docker context config
func createContext(name string, host string) error {
s := NewDefaultDockerContextStore()
s := context.NewDefaultDockerContextStore()
contextMetadata := contextStore.Metadata{
Endpoints: make(map[string]interface{}),
Name: name,
@ -43,7 +42,7 @@ func createContext(name string, host string) error {
Endpoints: make(map[string]contextStore.EndpointTLSData),
}
dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(host)
dockerEP, dockerTLS, err := commandconnPkg.GetDockerEndpointMetadataAndTLS(host)
if err != nil {
return err
}
@ -73,56 +72,20 @@ func DeleteContext(name string) error {
return err
}
// remove any context that might be loaded
// TODO: Check if the context we are removing is the active one rather than doing it all the time
cfg := dConfig.LoadDefaultConfigFile(nil)
cfg.CurrentContext = ""
if err := cfg.Save(); err != nil {
return err
}
return NewDefaultDockerContextStore().Remove(name)
return context.NewDefaultDockerContextStore().Remove(name)
}
func GetContext(contextName string) (contextStore.Metadata, error) {
ctx, err := NewDefaultDockerContextStore().GetMetadata(contextName)
ctx, err := context.NewDefaultDockerContextStore().GetMetadata(contextName)
if err != nil {
return contextStore.Metadata{}, err
}
return ctx, nil
}
func GetContextEndpoint(ctx contextStore.Metadata) (string, error) {
// safe to use docker key hardcoded since abra doesn't use k8s... yet...
endpointmeta, ok := ctx.Endpoints["docker"].(context.EndpointMetaBase)
if !ok {
err := errors.New("context lacks Docker endpoint")
return "", err
}
return endpointmeta.Host, nil
}
func newContextStore(dir string, config contextStore.Config) contextStore.Store {
return contextStore.New(dir, config)
}
func NewDefaultDockerContextStore() *command.ContextStoreWithDefault {
// Grabbing the stderr from Docker commands
// Much easier to fit this into the code we are using to replicate docker cli commands
_, _, stderr := term.StdStreams()
// TODO: Look into custom docker configs in case users want that
dockerConfig := dConfig.LoadDefaultConfigFile(stderr)
contextDir := dConfig.ContextStoreDir()
storeConfig := command.DefaultContextStoreConfig()
store := newContextStore(contextDir, storeConfig)
dockerContextStore := &command.ContextStoreWithDefault{
Store: store,
Resolver: func() (*command.DefaultContext, error) {
// nil for the Opts because it works without it and its a cli thing
return command.ResolveDefaultContext(nil, dockerConfig, storeConfig, stderr)
},
}
return dockerContextStore
}

View File

@ -1,52 +0,0 @@
package client_test
import (
"testing"
"coopcloud.tech/abra/pkg/client"
dContext "github.com/docker/cli/cli/context"
dCliContextStore "github.com/docker/cli/cli/context/store"
)
type TestContext struct {
context dCliContextStore.Metadata
expected_endpoint string
}
func dockerContext(host, key string) TestContext {
dockerContext := dCliContextStore.Metadata{
Name: "foo",
Metadata: nil,
Endpoints: map[string]interface{}{
key: dContext.EndpointMetaBase{
Host: host,
SkipTLSVerify: false,
},
},
}
return TestContext{
context: dockerContext,
expected_endpoint: host,
}
}
func TestGetContextEndpoint(t *testing.T) {
var testDockerContexts = []TestContext{
dockerContext("ssh://foobar", "docker"),
dockerContext("ssh://foobar", "k8"),
}
for _, context := range testDockerContexts {
endpoint, err := client.GetContextEndpoint(context.context)
if err != nil {
if err.Error() != "context lacks Docker endpoint" {
t.Error(err)
}
} else {
if endpoint != context.expected_endpoint {
t.Errorf("did not get correct context endpoint. Expected: %s, received: %s", context.expected_endpoint, endpoint)
}
}
}
}

View File

@ -1,191 +0,0 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2013-2017 Docker, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,10 +0,0 @@
# github.com/docker/cli/cli/compose/convert
DISCLAIMER: This is like the entire `github.com/docker/cli/cli/compose/convert`
package. This should be an easy import but importing it creates DEPENDENCY
HELL. I tried for an hour to fix it but it would work. TRY TO FIX AT YOUR OWN
RISK!!!
Due to this literally just being copy-pasted from the lib, the Apache license
will be posted in this folder. Small edits to the source code have been to
function names and parts we don't need deleted.

View File

@ -1,170 +1,28 @@
package client
import (
"encoding/json"
"context"
"fmt"
"io/ioutil"
"net/http"
"strings"
"coopcloud.tech/abra/pkg/web"
"github.com/containers/image/docker"
"github.com/containers/image/types"
"github.com/docker/distribution/reference"
)
type RawTag struct {
Layer string
Name string
}
// GetRegistryTags retrieves all tags of an image from a container registry.
func GetRegistryTags(img reference.Named) ([]string, error) {
var tags []string
type RawTags []RawTag
ref, err := docker.ParseReference(fmt.Sprintf("//%s", img))
if err != nil {
return tags, fmt.Errorf("failed to parse image %s, saw: %s", img, err.Error())
}
var registryURL = "https://registry.hub.docker.com/v1/repositories/%s/tags"
func GetRegistryTags(image string) (RawTags, error) {
var tags RawTags
tagsUrl := fmt.Sprintf(registryURL, image)
if err := web.ReadJSON(tagsUrl, &tags); err != nil {
ctx := context.Background()
tags, err = docker.GetRepositoryTags(ctx, &types.SystemContext{}, ref)
if err != nil {
return tags, err
}
return tags, nil
}
// getRegv2Token retrieves a registry v2 authentication token.
func getRegv2Token(image reference.Named) (string, error) {
img := reference.Path(image)
authTokenURL := fmt.Sprintf("https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull", img)
req, err := http.NewRequest("GET", authTokenURL, nil)
if err != nil {
return "", err
}
client := &http.Client{Timeout: web.Timeout}
res, err := client.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
_, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", nil
}
tokenRes := struct {
Token string
Expiry string
Issued string
}{}
if err := json.Unmarshal(body, &tokenRes); err != nil {
return "", err
}
return tokenRes.Token, nil
}
// GetTagDigest retrieves an image digest from a v2 registry
func GetTagDigest(image reference.Named) (string, error) {
img := reference.Path(image)
tag := image.(reference.NamedTagged).Tag()
manifestURL := fmt.Sprintf("https://index.docker.io/v2/%s/manifests/%s", img, tag)
req, err := http.NewRequest("GET", manifestURL, nil)
if err != nil {
return "", err
}
token, err := getRegv2Token(image)
if err != nil {
return "", err
}
req.Header = http.Header{
"Accept": []string{
"application/vnd.docker.distribution.manifest.v2+json",
"application/vnd.docker.distribution.manifest.list.v2+json",
},
"Authorization": []string{fmt.Sprintf("Bearer %s", token)},
}
client := &http.Client{Timeout: web.Timeout}
res, err := client.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
_, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
registryResT1 := struct {
SchemaVersion int
MediaType string
Manifests []struct {
MediaType string
Size int
Digest string
Platform struct {
Architecture string
Os string
}
}
}{}
registryResT2 := struct {
SchemaVersion int
MediaType string
Config struct {
MediaType string
Size int
Digest string
}
Layers []struct {
MediaType string
Size int
Digest string
}
}{}
if err := json.Unmarshal(body, &registryResT1); err != nil {
return "", err
}
var digest string
for _, manifest := range registryResT1.Manifests {
if string(manifest.Platform.Architecture) == "amd64" {
digest = strings.Split(manifest.Digest, ":")[1][:7]
}
}
if digest == "" {
if err := json.Unmarshal(body, &registryResT2); err != nil {
return "", err
}
digest = strings.Split(registryResT2.Config.Digest, ":")[1][:7]
}
if digest == "" {
return "", fmt.Errorf("Unable to retrieve amd64 digest for '%s'", image)
}
return digest, nil
}

View File

@ -4,20 +4,14 @@ import (
"context"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
)
func StoreSecret(secretName, secretValue, server string) error {
cl, err := New(server)
if err != nil {
return err
}
ctx := context.Background()
func StoreSecret(cl *client.Client, secretName, secretValue, server string) error {
ann := swarm.Annotations{Name: secretName}
spec := swarm.SecretSpec{Annotations: ann, Data: []byte(secretValue)}
// We don't bother with the secret IDs for now
if _, err := cl.SecretCreate(ctx, spec); err != nil {
if _, err := cl.SecretCreate(context.Background(), spec); err != nil {
return err
}

View File

@ -3,49 +3,39 @@ package client
import (
"context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
)
func GetVolumes(ctx context.Context, server string, appName string) ([]*types.Volume, error) {
cl, err := New(server)
if err != nil {
return nil, err
}
fs := filters.NewArgs()
fs.Add("name", appName)
volumeListOKBody, err := cl.VolumeList(ctx, fs)
func GetVolumes(cl *client.Client, ctx context.Context, server string, fs filters.Args) ([]*volume.Volume, error) {
volumeListOptions := volume.ListOptions{fs}
volumeListOKBody, err := cl.VolumeList(ctx, volumeListOptions)
volumeList := volumeListOKBody.Volumes
if err != nil {
logrus.Fatal(err)
return volumeList, err
}
return volumeList, nil
}
func GetVolumeNames(volumes []*types.Volume) []string {
func GetVolumeNames(volumes []*volume.Volume) []string {
var volumeNames []string
for _, vol := range volumes {
volumeNames = append(volumeNames, vol.Name)
}
return volumeNames
}
func RemoveVolumes(ctx context.Context, server string, volumeNames []string, force bool) error {
cl, err := New(server)
if err != nil {
return err
}
func RemoveVolumes(cl *client.Client, ctx context.Context, server string, volumeNames []string, force bool) error {
for _, volName := range volumeNames {
err := cl.VolumeRemove(ctx, volName, force)
if err != nil {
return err
}
}
return nil
return nil
}

View File

@ -7,35 +7,36 @@ import (
"path/filepath"
"strings"
"coopcloud.tech/abra/pkg/client/stack"
loader "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/upstream/stack"
loader "coopcloud.tech/abra/pkg/upstream/stack"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
)
// UpdateTag updates an image tag in-place on file system local compose files.
func UpdateTag(pattern, image, tag, recipeName string) error {
func UpdateTag(pattern, image, tag, recipeName string) (bool, error) {
composeFiles, err := filepath.Glob(pattern)
if err != nil {
return err
return false, err
}
logrus.Debugf("considering '%s' config(s) for tag update", strings.Join(composeFiles, ", "))
logrus.Debugf("considering %s config(s) for tag update", strings.Join(composeFiles, ", "))
for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}}
envSamplePath := path.Join(config.ABRA_DIR, "apps", recipeName, ".env.sample")
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath)
if err != nil {
return err
return false, err
}
compose, err := loader.LoadComposefile(opts, sampleEnv)
if err != nil {
return err
return false, err
}
for _, service := range compose.Services {
@ -45,40 +46,42 @@ func UpdateTag(pattern, image, tag, recipeName string) error {
img, _ := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return err
return false, err
}
composeImage := reference.Path(img)
if strings.Contains(composeImage, "library") {
// ParseNormalizedNamed prepends 'library' to images like nginx:<tag>,
// postgres:<tag>, i.e. images which do not have a username in the
// first position of the string
composeImage = strings.Split(composeImage, "/")[1]
var composeTag string
switch img.(type) {
case reference.NamedTagged:
composeTag = img.(reference.NamedTagged).Tag()
default:
logrus.Debugf("unable to parse %s, skipping", img)
continue
}
composeTag := img.(reference.NamedTagged).Tag()
logrus.Debugf("parsed '%s' from '%s'", composeTag, service.Image)
composeImage := formatter.StripTagMeta(reference.Path(img))
logrus.Debugf("parsed %s from %s", composeTag, service.Image)
if image == composeImage {
bytes, err := ioutil.ReadFile(composeFile)
if err != nil {
return err
return false, err
}
old := fmt.Sprintf("%s:%s", composeImage, composeTag)
new := fmt.Sprintf("%s:%s", composeImage, tag)
replacedBytes := strings.Replace(string(bytes), old, new, -1)
logrus.Debugf("updating '%s' to '%s' in '%s'", old, new, compose.Filename)
logrus.Debugf("updating %s to %s in %s", old, new, compose.Filename)
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0644); err != nil {
return err
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil {
return false, err
}
}
}
}
return nil
return false, nil
}
// UpdateLabel updates a label in-place on file system local compose files.
@ -88,12 +91,12 @@ func UpdateLabel(pattern, serviceName, label, recipeName string) error {
return err
}
logrus.Debugf("considering '%s' config(s) for label update", strings.Join(composeFiles, ", "))
logrus.Debugf("considering %s config(s) for label update", strings.Join(composeFiles, ", "))
for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}}
envSamplePath := path.Join(config.ABRA_DIR, "apps", recipeName, ".env.sample")
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath)
if err != nil {
return err
@ -117,8 +120,11 @@ func UpdateLabel(pattern, serviceName, label, recipeName string) error {
continue
}
discovered := false
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
bytes, err := ioutil.ReadFile(composeFile)
if err != nil {
return err
@ -127,13 +133,25 @@ func UpdateLabel(pattern, serviceName, label, recipeName string) error {
old := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", value)
replacedBytes := strings.Replace(string(bytes), old, label, -1)
logrus.Debugf("updating '%s' to '%s' in '%s'", old, label, compose.Filename)
if old == label {
logrus.Warnf("%s is already set, nothing to do?", label)
return nil
}
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0644); err != nil {
logrus.Debugf("updating %s to %s in %s", old, label, compose.Filename)
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil {
return err
}
logrus.Infof("synced label %s to service %s", label, serviceName)
}
}
if !discovered {
logrus.Warn("no existing label found, automagic insertion not supported yet")
logrus.Fatalf("add '- \"%s\"' manually to the 'app' service in %s", label, composeFile)
}
}
return nil

View File

@ -1,18 +1,22 @@
package config
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"strconv"
"strings"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/client/convert"
loader "coopcloud.tech/abra/pkg/client/stack"
stack "coopcloud.tech/abra/pkg/client/stack"
"github.com/schollz/progressbar/v3"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/upstream/convert"
loader "coopcloud.tech/abra/pkg/upstream/stack"
stack "coopcloud.tech/abra/pkg/upstream/stack"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
)
@ -21,6 +25,9 @@ import (
// AppEnv is a map of the values in an apps env config
type AppEnv = map[string]string
// AppModifiers is a map of modifiers in an apps env config
type AppModifiers = map[string]map[string]string
// AppName is AppName
type AppName = string
@ -36,24 +43,97 @@ type AppFiles map[AppName]AppFile
// App reprents an app with its env file read into memory
type App struct {
Name AppName
Type string
Recipe string
Domain string
Env AppEnv
Server string
Path string
}
// StackName gets what the docker safe stack name is for the app
// See documentation of config.StackName
func (a App) StackName() string {
if _, exists := a.Env["STACK_NAME"]; exists {
return a.Env["STACK_NAME"]
}
stackName := SanitiseAppName(a.Name)
stackName := StackName(a.Name)
a.Env["STACK_NAME"] = stackName
return stackName
}
// SORTING TYPES
// StackName gets whatever the docker safe (uses the right delimiting
// character, e.g. "_") stack name is for the app. In general, you don't want
// to use this to show anything to end-users, you want use a.Name instead.
func StackName(appName string) string {
stackName := SanitiseAppName(appName)
if len(stackName) > 45 {
logrus.Debugf("trimming %s to %s to avoid runtime limits", stackName, stackName[:45])
stackName = stackName[:45]
}
return stackName
}
// Filters retrieves app filters for querying the container runtime. By default
// it filters on all services in the app. It is also possible to pass an
// otional list of service names, which get filtered instead.
//
// Due to upstream issues, filtering works different depending on what you're
// querying. So, for example, secrets don't work with regex! The caller needs
// to implement their own validation that the right secrets are matched. In
// order to handle these cases, we provide the `appendServiceNames` /
// `exactMatch` modifiers.
func (a App) Filters(appendServiceNames, exactMatch bool, services ...string) (filters.Args, error) {
filters := filters.NewArgs()
if len(services) > 0 {
for _, serviceName := range services {
filters.Add("name", ServiceFilter(a.StackName(), serviceName, exactMatch))
}
return filters, nil
}
// When not appending the service name, just add one filter for the whole
// stack.
if !appendServiceNames {
f := fmt.Sprintf("%s", a.StackName())
if exactMatch {
f = fmt.Sprintf("^%s", f)
}
filters.Add("name", f)
return filters, nil
}
composeFiles, err := GetComposeFiles(a.Recipe, a.Env)
if err != nil {
return filters, err
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := GetAppComposeConfig(a.Recipe, opts, a.Env)
if err != nil {
return filters, err
}
for _, service := range compose.Services {
f := ServiceFilter(a.StackName(), service.Name, exactMatch)
filters.Add("name", f)
}
return filters, nil
}
// ServiceFilter creates a filter string for filtering a service in the docker
// container runtime. When exact match is true, it uses regex to match the
// string exactly.
func ServiceFilter(stack, service string, exact bool) string {
if exact {
return fmt.Sprintf("^%s_%s", stack, service)
}
return fmt.Sprintf("%s_%s", stack, service)
}
// ByServer sort a slice of Apps
type ByServer []App
@ -64,25 +144,25 @@ func (a ByServer) Less(i, j int) bool {
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
}
// ByServerAndType sort a slice of Apps
type ByServerAndType []App
// ByServerAndRecipe sort a slice of Apps
type ByServerAndRecipe []App
func (a ByServerAndType) Len() int { return len(a) }
func (a ByServerAndType) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByServerAndType) Less(i, j int) bool {
func (a ByServerAndRecipe) Len() int { return len(a) }
func (a ByServerAndRecipe) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByServerAndRecipe) Less(i, j int) bool {
if a[i].Server == a[j].Server {
return strings.ToLower(a[i].Type) < strings.ToLower(a[j].Type)
return strings.ToLower(a[i].Recipe) < strings.ToLower(a[j].Recipe)
}
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
}
// ByType sort a slice of Apps
type ByType []App
// ByRecipe sort a slice of Apps
type ByRecipe []App
func (a ByType) Len() int { return len(a) }
func (a ByType) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByType) Less(i, j int) bool {
return strings.ToLower(a[i].Type) < strings.ToLower(a[j].Type)
func (a ByRecipe) Len() int { return len(a) }
func (a ByRecipe) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByRecipe) Less(i, j int) bool {
return strings.ToLower(a[i].Recipe) < strings.ToLower(a[j].Recipe)
}
// ByName sort a slice of Apps
@ -94,85 +174,90 @@ func (a ByName) Less(i, j int) bool {
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)
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 {
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())
}
return app, nil
}
// newApp creates new App object
func newApp(env AppEnv, name string, appFile AppFile) (App, error) {
// Checking for type as it is required - apps wont work without it
// NewApp creates new App object
func NewApp(env AppEnv, name string, appFile AppFile) (App, error) {
domain := env["DOMAIN"]
apptype, ok := env["TYPE"]
if !ok {
return App{}, errors.New("missing TYPE variable")
recipe, exists := env["RECIPE"]
if !exists {
recipe, exists = env["TYPE"]
if !exists {
return App{}, fmt.Errorf("%s is missing the TYPE env var?", name)
}
}
return App{
Name: name,
Domain: domain,
Type: apptype,
Recipe: recipe,
Env: env,
Server: appFile.Server,
Path: appFile.Path,
}, nil
}
// LoadAppFiles gets all app files for a given set of servers or all servers
// LoadAppFiles gets all app files for a given set of servers or all servers.
func LoadAppFiles(servers ...string) (AppFiles, error) {
appFiles := make(AppFiles)
if len(servers) == 1 {
if servers[0] == "" {
// Empty servers flag, one string will always be passed
var err error
servers, err = getAllFoldersInDirectory(ABRA_SERVER_FOLDER)
servers, err = GetAllFoldersInDirectory(SERVERS_DIR)
if err != nil {
return nil, err
return appFiles, err
}
}
}
logrus.Debugf("collecting metadata from '%v' servers: '%s'", len(servers), strings.Join(servers, ", "))
logrus.Debugf("collecting metadata from %v servers: %s", len(servers), strings.Join(servers, ", "))
for _, server := range servers {
serverDir := path.Join(ABRA_SERVER_FOLDER, server)
files, err := getAllFilesInDirectory(serverDir)
serverDir := path.Join(SERVERS_DIR, server)
files, err := GetAllFilesInDirectory(serverDir)
if err != nil {
return nil, err
return appFiles, fmt.Errorf("server %s doesn't exist? Run \"abra server ls\" to check", server)
}
for _, file := range files {
appName := strings.TrimSuffix(file.Name(), ".env")
appFilePath := path.Join(ABRA_SERVER_FOLDER, server, file.Name())
appFilePath := path.Join(SERVERS_DIR, server, file.Name())
appFiles[appName] = AppFile{
Path: appFilePath,
Server: server,
}
}
}
return appFiles, nil
}
// GetApp loads an apps settings, reading it from file, in preparation to use it
//
// ONLY use when ready to use the env file to keep IO down
// GetApp loads an apps settings, reading it from file, in preparation to use
// it. It should only be used when ready to use the env file to keep IO
// operations down.
func GetApp(apps AppFiles, name AppName) (App, error) {
appFile, exists := apps[name]
if !exists {
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 {
return App{}, err
}
@ -180,8 +265,9 @@ func GetApp(apps AppFiles, name AppName) (App, error) {
return app, nil
}
// GetApps returns a slice of Apps with their env files read from a given slice of AppFiles
func GetApps(appFiles AppFiles) ([]App, error) {
// GetApps returns a slice of Apps with their env files read from a given
// slice of AppFiles.
func GetApps(appFiles AppFiles, recipeFilter string) ([]App, error) {
var apps []App
for name := range appFiles {
@ -189,7 +275,14 @@ func GetApps(appFiles AppFiles) ([]App, error) {
if err != nil {
return nil, err
}
apps = append(apps, app)
if recipeFilter != "" {
if app.Recipe == recipeFilter {
apps = append(apps, app)
}
} else {
apps = append(apps, app)
}
}
return apps, nil
@ -209,13 +302,13 @@ func GetAppServiceNames(appName string) ([]string, error) {
return serviceNames, err
}
composeFiles, err := GetAppComposeFiles(app.Type, app.Env)
composeFiles, err := GetComposeFiles(app.Recipe, app.Env)
if err != nil {
return serviceNames, err
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := GetAppComposeConfig(app.Type, opts, app.Env)
compose, err := GetAppComposeConfig(app.Recipe, opts, app.Env)
if err != nil {
return serviceNames, err
}
@ -236,7 +329,7 @@ func GetAppNames() ([]string, error) {
return appNames, err
}
apps, err := GetApps(appFiles)
apps, err := GetApps(appFiles, "")
if err != nil {
return appNames, err
}
@ -248,61 +341,85 @@ func GetAppNames() ([]string, error) {
return appNames, nil
}
// TemplateAppEnvSample copies the example env file for the app into the users env files
func TemplateAppEnvSample(appType, appName, server, domain, recipe string) error {
envSamplePath := path.Join(ABRA_DIR, "apps", appType, ".env.sample")
// TemplateAppEnvSample copies the example env file for the app into the users
// env files.
func TemplateAppEnvSample(recipeName, appName, server, domain string) error {
envSamplePath := path.Join(RECIPES_DIR, recipeName, ".env.sample")
envSample, err := ioutil.ReadFile(envSamplePath)
if err != nil {
return err
}
appEnvPath := path.Join(ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName))
if _, err := os.Stat(appEnvPath); err == nil {
if _, err := os.Stat(appEnvPath); !os.IsNotExist(err) {
return fmt.Errorf("%s already exists?", appEnvPath)
}
envSample = []byte(strings.Replace(string(envSample), fmt.Sprintf("%s.example.com", recipe), domain, -1))
envSample = []byte(strings.Replace(string(envSample), "example.com", domain, -1))
err = ioutil.WriteFile(appEnvPath, envSample, 0755)
err = ioutil.WriteFile(appEnvPath, envSample, 0o664)
if err != nil {
return err
}
logrus.Debugf("copied '%s' to '%s'", envSamplePath, appEnvPath)
read, err := ioutil.ReadFile(appEnvPath)
if err != nil {
return err
}
newContents := strings.Replace(string(read), recipeName+".example.com", domain, -1)
err = ioutil.WriteFile(appEnvPath, []byte(newContents), 0)
if err != nil {
return err
}
logrus.Debugf("copied & templated %s to %s", envSamplePath, appEnvPath)
return nil
}
// SanitiseAppName makes a app name usable with Docker by replacing illegal characters
// SanitiseAppName makes a app name usable with Docker by replacing illegal
// characters.
func SanitiseAppName(name string) string {
return strings.ReplaceAll(name, ".", "_")
}
// GetAppStatuses queries servers to check the deployment status of given apps
func GetAppStatuses(appFiles AppFiles) (map[string]map[string]string, error) {
// GetAppStatuses queries servers to check the deployment status of given apps.
func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]string, error) {
statuses := make(map[string]map[string]string)
var unique []string
servers := make(map[string]struct{})
for _, appFile := range appFiles {
if _, ok := servers[appFile.Server]; !ok {
servers[appFile.Server] = struct{}{}
unique = append(unique, appFile.Server)
for _, app := range apps {
if _, ok := servers[app.Server]; !ok {
servers[app.Server] = struct{}{}
}
}
bar := formatter.CreateProgressbar(len(servers), "querying remote servers...")
var bar *progressbar.ProgressBar
if !MachineReadable {
bar = formatter.CreateProgressbar(len(servers), "querying remote servers...")
}
ch := make(chan stack.StackStatus, len(servers))
for server := range servers {
cl, err := client.New(server)
if err != nil {
return statuses, err
}
go func(s string) {
ch <- stack.GetAllDeployedServices(s)
bar.Add(1)
ch <- stack.GetAllDeployedServices(cl, s)
if !MachineReadable {
bar.Add(1)
}
}(server)
}
for range servers {
status := <-ch
if status.Err != nil {
return statuses, status.Err
}
for _, service := range status.Services {
result := make(map[string]string)
name := service.Spec.Labels[convert.LabelNamespace]
@ -311,13 +428,28 @@ func GetAppStatuses(appFiles AppFiles) (map[string]map[string]string, error) {
result["status"] = "deployed"
}
labelKey := fmt.Sprintf("coop-cloud.%s.version", name)
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", name)
chaos, ok := service.Spec.Labels[labelKey]
if ok {
result["chaos"] = chaos
}
labelKey = fmt.Sprintf("coop-cloud.%s.chaos-version", name)
if chaosVersion, ok := service.Spec.Labels[labelKey]; ok {
result["chaosVersion"] = chaosVersion
}
labelKey = fmt.Sprintf("coop-cloud.%s.autoupdate", name)
if autoUpdate, ok := service.Spec.Labels[labelKey]; ok {
result["autoUpdate"] = autoUpdate
} else {
result["autoUpdate"] = "false"
}
labelKey = fmt.Sprintf("coop-cloud.%s.version", name)
if version, ok := service.Spec.Labels[labelKey]; ok {
result["version"] = version
} else {
//FIXME: we only need to check containers with the version label not
// every single container and then skip when we see no label perf gains
// to be had here
continue
}
@ -325,32 +457,62 @@ func GetAppStatuses(appFiles AppFiles) (map[string]map[string]string, error) {
}
}
logrus.Debugf("retrieved app statuses: '%s'", statuses)
logrus.Debugf("retrieved app statuses: %s", statuses)
return statuses, nil
}
// GetAppComposeFiles gets the list of compose files for an app which should be
// merged into a composetypes.Config while respecting the COMPOSE_FILE env var.
func GetAppComposeFiles(recipe string, appEnv AppEnv) ([]string, error) {
// ensurePathExists ensures that a path exists.
func ensurePathExists(path 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
if _, ok := appEnv["COMPOSE_FILE"]; !ok {
logrus.Debug("no COMPOSE_FILE detected, loading compose.yml")
path := fmt.Sprintf("%s/%s/compose.yml", APPS_DIR, recipe)
composeFileEnvVar, ok := appEnv["COMPOSE_FILE"]
if !ok {
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)
return composeFiles, nil
}
composeFileEnvVar := appEnv["COMPOSE_FILE"]
envVars := strings.Split(composeFileEnvVar, ":")
logrus.Debugf("COMPOSE_FILE detected ('%s'), loading '%s'", composeFileEnvVar, strings.Join(envVars, ", "))
for _, file := range strings.Split(composeFileEnvVar, ":") {
path := fmt.Sprintf("%s/%s/%s", APPS_DIR, recipe, file)
if !strings.Contains(composeFileEnvVar, ":") {
path := fmt.Sprintf("%s/%s/%s", RECIPES_DIR, recipe, composeFileEnvVar)
if err := ensurePathExists(path); err != nil {
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)
if err := ensurePathExists(path); err != nil {
return composeFiles, err
}
composeFiles = append(composeFiles, path)
}
logrus.Debugf("retrieved '%s' configs for '%s'", strings.Join(composeFiles, ", "), recipe)
logrus.Debugf("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", "))
logrus.Debugf("retrieved %s configs for %s", strings.Join(composeFiles, ", "), recipe)
return composeFiles, nil
}
@ -364,7 +526,102 @@ func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv AppEnv) (*comp
return &composetypes.Config{}, err
}
logrus.Debugf("retrieved '%s' for '%s'", compose.Filename, recipe)
logrus.Debugf("retrieved %s for %s", compose.Filename, recipe)
return compose, nil
}
// ExposeAllEnv exposes all env variables to the app container
func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv AppEnv) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("Add the following environment to the app service config of %s:", stackName)
for k, v := range appEnv {
_, exists := service.Environment[k]
if !exists {
value := v
service.Environment[k] = &value
logrus.Debugf("Add Key: %s Value: %s to %s", k, value, stackName)
}
}
}
}
}
// SetRecipeLabel adds the label 'coop-cloud.${STACK_NAME}.recipe=${RECIPE}' to the app container
// to signal which recipe is connected to the deployed app
func SetRecipeLabel(compose *composetypes.Config, stackName string, recipe string) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("set recipe label 'coop-cloud.%s.recipe' to %s for %s", stackName, recipe, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.recipe", stackName)
service.Deploy.Labels[labelKey] = recipe
}
}
}
// SetChaosLabel adds the label 'coop-cloud.${STACK_NAME}.chaos=true/false' to the app container
// to signal if the app is deployed in chaos mode
func SetChaosLabel(compose *composetypes.Config, stackName string, chaos bool) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("set label 'coop-cloud.%s.chaos' to %v for %s", stackName, chaos, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", stackName)
service.Deploy.Labels[labelKey] = strconv.FormatBool(chaos)
}
}
}
// SetChaosVersionLabel adds the label 'coop-cloud.${STACK_NAME}.chaos-version=$(GIT_COMMIT)' to the app container
func SetChaosVersionLabel(compose *composetypes.Config, stackName string, chaosVersion string) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("set label 'coop-cloud.%s.chaos-version' to %v for %s", stackName, chaosVersion, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.chaos-version", stackName)
service.Deploy.Labels[labelKey] = chaosVersion
}
}
}
// SetUpdateLabel adds env ENABLE_AUTO_UPDATE as label to enable/disable the
// auto update process for this app. The default if this variable is not set is to disable
// the auto update process.
func SetUpdateLabel(compose *composetypes.Config, stackName string, appEnv AppEnv) {
for _, service := range compose.Services {
if service.Name == "app" {
enable_auto_update, exists := appEnv["ENABLE_AUTO_UPDATE"]
if !exists {
enable_auto_update = "false"
}
logrus.Debugf("set label 'coop-cloud.%s.autoupdate' to %s for %s", stackName, enable_auto_update, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.autoupdate", stackName)
service.Deploy.Labels[labelKey] = enable_auto_update
}
}
}
// GetLabel reads docker labels in the format of "coop-cloud.${STACK_NAME}.${LABEL}" from the local compose files
func GetLabel(compose *composetypes.Config, stackName string, label string) string {
for _, service := range compose.Services {
if service.Name == "app" {
labelKey := fmt.Sprintf("coop-cloud.%s.%s", stackName, label)
logrus.Debugf("get label '%s'", labelKey)
if labelValue, ok := service.Deploy.Labels[labelKey]; ok {
return labelValue
}
}
}
logrus.Debugf("no %s label found for %s", label, stackName)
return ""
}
// GetTimeoutFromLabel reads the timeout value from docker label "coop-cloud.${STACK_NAME}.TIMEOUT" and returns 50 as default value
func GetTimeoutFromLabel(compose *composetypes.Config, stackName string) (int, error) {
timeout := 50 // Default Timeout
var err error = nil
if timeoutLabel := GetLabel(compose, stackName, "timeout"); timeoutLabel != "" {
logrus.Debugf("timeout label: %s", timeoutLabel)
timeout, err = strconv.Atoi(timeoutLabel)
}
return timeout, err
}

View File

@ -1,37 +1,197 @@
package config
package config_test
import (
"encoding/json"
"fmt"
"reflect"
"testing"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"github.com/docker/docker/api/types/filters"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
)
func TestNewApp(t *testing.T) {
app, err := newApp(expectedAppEnv, appName, expectedAppFile)
app, err := config.NewApp(ExpectedAppEnv, AppName, ExpectedAppFile)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(app, expectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp)
if !reflect.DeepEqual(app, ExpectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, ExpectedApp)
}
}
func TestReadAppEnvFile(t *testing.T) {
app, err := readAppEnvFile(expectedAppFile, appName)
app, err := config.ReadAppEnvFile(ExpectedAppFile, AppName)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(app, expectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp)
if !reflect.DeepEqual(app, ExpectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, ExpectedApp)
}
}
func TestGetApp(t *testing.T) {
// TODO: Test failures as well as successes
app, err := GetApp(expectedAppFiles, appName)
app, err := config.GetApp(ExpectedAppFiles, AppName)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(app, expectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp)
if !reflect.DeepEqual(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)
}
}
}
func TestFilters(t *testing.T) {
oldDir := config.RECIPES_DIR
config.RECIPES_DIR = "./testdir"
defer func() {
config.RECIPES_DIR = oldDir
}()
app, err := config.NewApp(config.AppEnv{
"DOMAIN": "test.example.com",
"RECIPE": "test-recipe",
}, "test_example_com", config.AppFile{
Path: "./testdir/filtertest.end",
Server: "local",
})
if err != nil {
t.Fatal(err)
}
f, err := app.Filters(false, false)
if err != nil {
t.Error(err)
}
compareFilter(t, f, map[string]map[string]bool{
"name": {
"test_example_com": true,
},
})
f2, err := app.Filters(false, true)
if err != nil {
t.Error(err)
}
compareFilter(t, f2, map[string]map[string]bool{
"name": {
"^test_example_com": true,
},
})
f3, err := app.Filters(true, false)
if err != nil {
t.Error(err)
}
compareFilter(t, f3, map[string]map[string]bool{
"name": {
"test_example_com_bar": true,
"test_example_com_foo": true,
},
})
f4, err := app.Filters(true, true)
if err != nil {
t.Error(err)
}
compareFilter(t, f4, map[string]map[string]bool{
"name": {
"^test_example_com_bar": true,
"^test_example_com_foo": true,
},
})
f5, err := app.Filters(false, false, "foo")
if err != nil {
t.Error(err)
}
compareFilter(t, f5, map[string]map[string]bool{
"name": {
"test_example_com_foo": true,
},
})
}
func compareFilter(t *testing.T, f1 filters.Args, f2 map[string]map[string]bool) {
t.Helper()
j1, err := f1.MarshalJSON()
if err != nil {
t.Error(err)
}
j2, err := json.Marshal(f2)
if err != nil {
t.Error(err)
}
if diff := cmp.Diff(string(j2), string(j1)); diff != "" {
t.Errorf("filters mismatch (-want +got):\n%s", diff)
}
}

View File

@ -8,61 +8,96 @@ import (
"os"
"path"
"path/filepath"
"regexp"
"sort"
"strings"
"github.com/Autonomic-Cooperative/godotenv"
"git.coopcloud.tech/coop-cloud/godotenv"
"github.com/sirupsen/logrus"
)
var ABRA_DIR = os.ExpandEnv("$HOME/.abra")
var ABRA_SERVER_FOLDER = path.Join(ABRA_DIR, "servers")
var APPS_JSON = path.Join(ABRA_DIR, "apps.json")
var APPS_DIR = path.Join(ABRA_DIR, "apps")
// 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 RECIPES_DIR = path.Join(ABRA_DIR, "recipes")
var VENDOR_DIR = path.Join(ABRA_DIR, "vendor")
var BACKUP_DIR = path.Join(ABRA_DIR, "backups")
var CATALOGUE_DIR = path.Join(ABRA_DIR, "catalogue")
var RECIPES_JSON = path.Join(ABRA_DIR, "catalogue", "recipes.json")
var REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
var CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json"
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.
func GetServers() ([]string, error) {
var servers []string
servers, err := getAllFoldersInDirectory(ABRA_SERVER_FOLDER)
servers, err := GetAllFoldersInDirectory(SERVERS_DIR)
if err != nil {
return servers, err
}
logrus.Debugf("retrieved '%v' servers: '%s'", len(servers), servers)
logrus.Debugf("retrieved %v servers: %s", len(servers), servers)
return servers, nil
}
// ReadEnv loads an app envivornment into a map.
func ReadEnv(filePath string) (AppEnv, error) {
var envFile AppEnv
var envVars AppEnv
envFile, err := godotenv.Read(filePath)
envVars, _, err := godotenv.Read(filePath)
if err != nil {
return nil, err
}
logrus.Debugf("read '%s' from '%s'", envFile, filePath)
logrus.Debugf("read %s from %s", envVars, filePath)
return envFile, nil
return envVars, nil
}
// ReadEnv loads an app envivornment and their modifiers in two different maps.
func ReadEnvWithModifiers(filePath string) (AppEnv, AppModifiers, error) {
var envVars AppEnv
envVars, mods, err := godotenv.Read(filePath)
if err != nil {
return nil, mods, err
}
logrus.Debugf("read %s from %s", envVars, filePath)
return envVars, mods, nil
}
// ReadServerNames retrieves all server names.
func ReadServerNames() ([]string, error) {
serverNames, err := getAllFoldersInDirectory(ABRA_SERVER_FOLDER)
serverNames, err := GetAllFoldersInDirectory(SERVERS_DIR)
if err != nil {
return nil, err
}
logrus.Debugf("read '%s' from '%s'", strings.Join(serverNames, ","), ABRA_SERVER_FOLDER)
logrus.Debugf("read %s from %s", strings.Join(serverNames, ","), SERVERS_DIR)
return serverNames, nil
}
// getAllFilesInDirectory returns filenames of all files in directory
func getAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
// GetAllFilesInDirectory returns filenames of all files in directory
func GetAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
var realFiles []fs.FileInfo
files, err := ioutil.ReadDir(directory)
@ -80,7 +115,7 @@ func getAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
realPath, err := filepath.EvalSymlinks(filePath)
if err != nil {
logrus.Warningf("broken symlink in your abra config folders: '%s'", filePath)
logrus.Warningf("broken symlink in your abra config folders: %s", filePath)
} else {
realFile, err := os.Stat(realPath)
if err != nil {
@ -95,8 +130,8 @@ func getAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
return realFiles, nil
}
// getAllFoldersInDirectory returns both folder and symlink paths
func getAllFoldersInDirectory(directory string) ([]string, error) {
// GetAllFoldersInDirectory returns both folder and symlink paths
func GetAllFoldersInDirectory(directory string) ([]string, error) {
var folders []string
files, err := ioutil.ReadDir(directory)
@ -104,7 +139,7 @@ func getAllFoldersInDirectory(directory string) ([]string, error) {
return nil, err
}
if len(files) == 0 {
return nil, fmt.Errorf("directory is empty: '%s'", directory)
return nil, fmt.Errorf("directory is empty: %s", directory)
}
for _, file := range files {
@ -113,7 +148,7 @@ func getAllFoldersInDirectory(directory string) ([]string, error) {
filePath := path.Join(directory, file.Name())
realDir, err := filepath.EvalSymlinks(filePath)
if err != nil {
logrus.Warningf("broken symlink in your abra config folders: '%s'", filePath)
logrus.Warningf("broken symlink in your abra config folders: %s", filePath)
} else if stat, err := os.Stat(realDir); err == nil && stat.IsDir() {
// path is a directory
folders = append(folders, file.Name())
@ -124,17 +159,6 @@ func getAllFoldersInDirectory(directory string) ([]string, error) {
return folders, nil
}
// EnsureAbraDirExists checks for the abra config folder and throws error if not
func EnsureAbraDirExists() error {
if _, err := os.Stat(ABRA_DIR); os.IsNotExist(err) {
logrus.Debugf("'%s' does not exist, creating it", ABRA_DIR)
if err := os.Mkdir(ABRA_DIR, 0777); err != nil {
return err
}
}
return nil
}
// ReadAbraShEnvVars reads env vars from an abra.sh recipe file.
func ReadAbraShEnvVars(abraSh string) (map[string]string, error) {
envVars := make(map[string]string)
@ -146,22 +170,107 @@ func ReadAbraShEnvVars(abraSh string) (map[string]string, error) {
}
return envVars, err
}
defer file.Close()
exportRegex, err := regexp.Compile(`^export\s+(\w+=\w+)`)
if err != nil {
return envVars, err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "export") {
splitVals := strings.Split(line, "export ")
txt := scanner.Text()
if exportRegex.MatchString(txt) {
splitVals := strings.Split(txt, "export ")
envVarDef := splitVals[len(splitVals)-1]
keyVal := strings.Split(envVarDef, "=")
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]
}
}
logrus.Debugf("read '%s' from '%s'", envVars, abraSh)
if len(envVars) > 0 {
logrus.Debugf("read %s from %s", envVars, abraSh)
} else {
logrus.Debugf("read 0 env var exports from %s", abraSh)
}
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)
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,69 @@
package config
package config_test
import (
"fmt"
"os"
"path"
"reflect"
"slices"
"strings"
"testing"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
)
var testFolder = os.ExpandEnv("$PWD/../../tests/resources/test_folder")
var validAbraConf = os.ExpandEnv("$PWD/../../tests/resources/valid_abra_config")
var (
TestFolder = os.ExpandEnv("$PWD/../../tests/resources/test_folder")
ValidAbraConf = os.ExpandEnv("$PWD/../../tests/resources/valid_abra_config")
)
// make sure these are in alphabetical order
var tFolders = []string{"folder1", "folder2"}
var tFiles = []string{"bar.env", "foo.env"}
var (
TFolders = []string{"folder1", "folder2"}
TFiles = []string{"bar.env", "foo.env"}
)
var appName = "ecloud"
var serverName = "evil.corp"
var (
AppName = "ecloud"
ServerName = "evil.corp"
)
var expectedAppEnv = AppEnv{
var ExpectedAppEnv = config.AppEnv{
"DOMAIN": "ecloud.evil.corp",
"TYPE": "ecloud",
"RECIPE": "ecloud",
}
var expectedApp = App{
Name: appName,
Type: expectedAppEnv["TYPE"],
Domain: expectedAppEnv["DOMAIN"],
Env: expectedAppEnv,
Path: expectedAppFile.Path,
Server: expectedAppFile.Server,
var ExpectedApp = config.App{
Name: AppName,
Recipe: ExpectedAppEnv["RECIPE"],
Domain: ExpectedAppEnv["DOMAIN"],
Env: ExpectedAppEnv,
Path: ExpectedAppFile.Path,
Server: ExpectedAppFile.Server,
}
var expectedAppFile = AppFile{
Path: path.Join(validAbraConf, "servers", serverName, appName+".env"),
Server: serverName,
var ExpectedAppFile = config.AppFile{
Path: path.Join(ValidAbraConf, "servers", ServerName, AppName+".env"),
Server: ServerName,
}
var expectedAppFiles = map[string]AppFile{
appName: expectedAppFile,
var ExpectedAppFiles = map[string]config.AppFile{
AppName: ExpectedAppFile,
}
// var expectedServerNames = []string{"evil.corp"}
func TestGetAllFoldersInDirectory(t *testing.T) {
folders, err := getAllFoldersInDirectory(testFolder)
folders, err := config.GetAllFoldersInDirectory(TestFolder)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(folders, tFolders) {
t.Fatalf("did not get expected folders. Expected: (%s), Got: (%s)", strings.Join(tFolders, ","), strings.Join(folders, ","))
if !reflect.DeepEqual(folders, TFolders) {
t.Fatalf("did not get expected folders. Expected: (%s), Got: (%s)", strings.Join(TFolders, ","), strings.Join(folders, ","))
}
}
func TestGetAllFilesInDirectory(t *testing.T) {
files, err := getAllFilesInDirectory(testFolder)
files, err := config.GetAllFilesInDirectory(TestFolder)
if err != nil {
t.Fatal(err)
}
@ -62,23 +71,205 @@ func TestGetAllFilesInDirectory(t *testing.T) {
for _, file := range files {
fileNames = append(fileNames, file.Name())
}
if !reflect.DeepEqual(fileNames, tFiles) {
t.Fatalf("did not get expected files. Expected: (%s), Got: (%s)", strings.Join(tFiles, ","), strings.Join(fileNames, ","))
if !reflect.DeepEqual(fileNames, TFiles) {
t.Fatalf("did not get expected files. Expected: (%s), Got: (%s)", strings.Join(TFiles, ","), strings.Join(fileNames, ","))
}
}
func TestReadEnv(t *testing.T) {
env, err := ReadEnv(expectedAppFile.Path)
env, err := config.ReadEnv(ExpectedAppFile.Path)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(env, expectedAppEnv) {
if !reflect.DeepEqual(env, ExpectedAppEnv) {
t.Fatalf(
"did not get expected application settings. Expected: DOMAIN=%s TYPE=%s; Got: DOMAIN=%s TYPE=%s",
expectedAppEnv["DOMAIN"],
expectedAppEnv["TYPE"],
"did not get expected application settings. Expected: DOMAIN=%s RECIPE=%s; Got: DOMAIN=%s RECIPE=%s",
ExpectedAppEnv["DOMAIN"],
ExpectedAppEnv["RECIPE"],
env["DOMAIN"],
env["TYPE"],
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)
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)
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 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)
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, modifiers, err := config.ReadEnvWithModifiers(envSamplePath)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(envSample["SECRET_TEST_PASS_TWO_VERSION"], "v1") {
t.Errorf("value should be 'v1', got: '%s'", envSample["SECRET_TEST_PASS_TWO_VERSION"])
}
if modifiers == nil || modifiers["SECRET_TEST_PASS_TWO_VERSION"] == nil {
t.Errorf("no modifiers included")
} else {
if modifiers["SECRET_TEST_PASS_TWO_VERSION"]["length"] != "10" {
t.Errorf("length modifier should be '10', got: '%s'", modifiers["SECRET_TEST_PASS_TWO_VERSION"]["length"])
}
}
}

View File

@ -0,0 +1,2 @@
RECIPE=test-recipe
DOMAIN=test.example.com

View File

@ -0,0 +1,6 @@
version: "3.8"
services:
foo:
image: debian
bar:
image: debian

View File

@ -0,0 +1,82 @@
package container
import (
"context"
"fmt"
"strings"
"coopcloud.tech/abra/pkg/formatter"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
// GetContainer retrieves a container. If noInput is false and the retrievd
// count of containers does not match 1, then a prompt is presented to let the
// user choose. A count of 0 is handled gracefully.
func GetContainer(c context.Context, cl *client.Client, filters filters.Args, noInput bool) (types.Container, error) {
containerOpts := types.ContainerListOptions{Filters: filters}
containers, err := cl.ContainerList(c, containerOpts)
if err != nil {
return types.Container{}, err
}
if len(containers) == 0 {
filter := filters.Get("name")[0]
return types.Container{}, fmt.Errorf("no containers matching the %v filter found?", filter)
}
if len(containers) != 1 {
var containersRaw []string
for _, container := range containers {
containerName := strings.Join(container.Names, " ")
trimmed := strings.TrimPrefix(containerName, "/")
created := formatter.HumanDuration(container.Created)
containersRaw = append(containersRaw, fmt.Sprintf("%s (created %v)", trimmed, created))
}
if noInput {
err := fmt.Errorf("expected 1 container but found %v: %s", len(containers), strings.Join(containersRaw, " "))
return types.Container{}, err
}
logrus.Warnf("ambiguous container list received, prompting for input")
var response string
prompt := &survey.Select{
Message: "which container are you looking for?",
Options: containersRaw,
}
if err := survey.AskOne(prompt, &response); err != nil {
return types.Container{}, err
}
chosenContainer := strings.TrimSpace(strings.Split(response, " ")[0])
for _, container := range containers {
containerName := strings.TrimSpace(strings.Join(container.Names, " "))
trimmed := strings.TrimPrefix(containerName, "/")
if trimmed == chosenContainer {
return container, nil
}
}
logrus.Panic("failed to match chosen container")
}
return containers[0], nil
}
// GetContainerFromStackAndService retrieves the container for the given stack and service.
func GetContainerFromStackAndService(cl *client.Client, stack, service string) (types.Container, error) {
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", stack, service))
container, err := GetContainer(context.Background(), cl, filters, true)
if err != nil {
return types.Container{}, err
}
return container, nil
}

41
pkg/context/context.go Normal file
View File

@ -0,0 +1,41 @@
package context
import (
"errors"
"github.com/docker/cli/cli/command"
dConfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/context"
contextStore "github.com/docker/cli/cli/context/store"
cliflags "github.com/docker/cli/cli/flags"
)
func NewDefaultDockerContextStore() *command.ContextStoreWithDefault {
contextDir := dConfig.ContextStoreDir()
storeConfig := command.DefaultContextStoreConfig()
store := newContextStore(contextDir, storeConfig)
opts := &cliflags.ClientOptions{Context: "default"}
dockerContextStore := &command.ContextStoreWithDefault{
Store: store,
Resolver: func() (*command.DefaultContext, error) {
return command.ResolveDefaultContext(opts, storeConfig)
},
}
return dockerContextStore
}
func GetContextEndpoint(ctx contextStore.Metadata) (string, error) {
endpointmeta, ok := ctx.Endpoints["docker"].(context.EndpointMetaBase)
if !ok {
err := errors.New("context lacks Docker endpoint")
return "", err
}
return endpointmeta.Host, nil
}
func newContextStore(dir string, config contextStore.Config) contextStore.Store {
return contextStore.New(dir, config)
}

55
pkg/dns/dns.go Normal file
View File

@ -0,0 +1,55 @@
package dns
import (
"fmt"
"net"
)
// EnsureIPv4 ensures that an ipv4 address is set for a domain name
func EnsureIPv4(domainName string) (string, error) {
ipv4, err := net.ResolveIPAddr("ip4", domainName)
if err != nil {
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
}
// EnsureDomainsResolveSameIPv4 ensures that domains resolve to the same ipv4 address
func EnsureDomainsResolveSameIPv4(domainName, server string) (string, error) {
if server == "default" || server == "local" {
return "", nil
}
var ipv4 string
domainIPv4, err := EnsureIPv4(domainName)
if err != nil {
return ipv4, err
}
if domainIPv4 == "" {
return ipv4, fmt.Errorf("cannot resolve ipv4 for %s?", domainName)
}
serverIPv4, err := EnsureIPv4(server)
if err != nil {
return ipv4, err
}
if serverIPv4 == "" {
return ipv4, fmt.Errorf("cannot resolve ipv4 for %s?", server)
}
if domainIPv4 != serverIPv4 {
err := "app domain %s (%s) does not appear to resolve to app server %s (%s)?"
return ipv4, fmt.Errorf(err, domainName, domainIPv4, server, serverIPv4)
}
return ipv4, nil
}

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