Compare commits

..

151 Commits

Author SHA1 Message Date
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
90 changed files with 1932 additions and 1829 deletions

View File

@ -3,12 +3,12 @@ kind: pipeline
name: coopcloud.tech/abra name: coopcloud.tech/abra
steps: steps:
- name: make check - name: make check
image: golang:1.17 image: golang:1.18
commands: commands:
- make check - make check
- name: make static - name: make static
image: golang:1.17 image: golang:1.18
ignore: true # until we decide we all want this check ignore: true # until we decide we all want this check
environment: environment:
STATIC_CHECK_URL: honnef.co/go/tools/cmd/staticcheck STATIC_CHECK_URL: honnef.co/go/tools/cmd/staticcheck
@ -18,12 +18,12 @@ steps:
- make static - make static
- name: make build - name: make build
image: golang:1.17 image: golang:1.18
commands: commands:
- make build - make build
- name: make test - name: make test
image: golang:1.17 image: golang:1.18
commands: commands:
- make test - make test
@ -55,7 +55,7 @@ steps:
event: tag event: tag
- name: release - name: release
image: golang:1.17 image: golang:1.18
environment: environment:
GITEA_TOKEN: GITEA_TOKEN:
from_secret: goreleaser_gitea_token from_secret: goreleaser_gitea_token

1
.gitignore vendored
View File

@ -4,4 +4,5 @@
.vscode/ .vscode/
abra abra
dist/ dist/
tests/integration/.abra/catalogue
vendor/ vendor/

10
AUTHORS.md Normal file
View File

@ -0,0 +1,10 @@
# 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
- 3wordchant
- decentral1se
- kawaiipunk
- knoflook
- roxxers

View File

@ -43,15 +43,3 @@ loc-author:
sort -f | \ sort -f | \
uniq -ic | \ uniq -ic | \
sort -n sort -n
int-core:
@docker run \
-v $$(pwd):/src \
--env-file .e2e.env \
debian:bullseye-slim \
sh -c "\
apt update && apt install -y wget curl git; echo ""; echo ""; \
git config --global user.email 'e2e@coopcloud.tech'; \
git config --global user.name 'e2e'; \
cd /src/tests/integration && bash core.sh -- --dev \
"

View File

@ -7,69 +7,6 @@
The Co-op Cloud utility belt 🎩🐇 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 apps, deploy them, run backup and restore operations and a whole lot of other things. Please see [docs.coopcloud.tech](https://docs.coopcloud.tech) for more extensive documentation. `abra` is our flagship client & command-line tool which has been developed specifically in the context of the Co-op Cloud project for the purpose of making day-to-day operations for [operators](https://docs.coopcloud.tech/operators/) and [maintainers](https://docs.coopcloud.tech/maintainers/) as convenient as possible. It is libre software, written in [Go](https://go.dev) and maintained and extended by the community ❤
## Quick install Please see [docs.coopcloud.tech/abra/](https://docs.coopcloud.tech/abra/) for help on install, upgrade, hacking, troubleshooting & more!
```bash
curl https://install.abra.autonomic.zone | bash
```
Or using the latest release candidate (extra experimental!):
```bash
curl https://install.abra.autonomic.zone | bash -s -- --rc
```
Source for this script is in [scripts/installer/installer](./scripts/installer/installer).
## Hacking
### Getting started
Install [direnv](https://direnv.net), 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
- `make install` will install it to `$GOPATH/bin`
- `go get <package>` and `go mod tidy` to add a new dependency
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 x.y.z-alpha'`)
- Make a new tag (e.g. `git tag -a x.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 -v`)
### Fork maintenance
#### `godotenv`
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.
#### `docker/client`
A number of modules in [pkg/upstream](./pkg/upstream) are copy/pasta'd from the upstream [docker/docker/client](https://pkg.go.dev/github.com/docker/docker/client). We had to do this because upstream are not exposing their API as public.

View File

@ -1,29 +1,22 @@
package app package app
import ( import (
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// AppCommand defines the `abra app` command and ets subcommands var AppCommand = cli.Command{
var AppCommand = &cli.Command{ Name: "app",
Name: "app", Aliases: []string{"a"},
Usage: "Manage apps", Usage: "Manage apps",
Aliases: []string{"a"}, ArgsUsage: "<domain>",
ArgsUsage: "<app>", Description: "This command provides functionality for managing the life cycle of your apps",
Description: ` Subcommands: []cli.Command{
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, appNewCommand,
appConfigCommand, appConfigCommand,
appRestartCommand, appRestartCommand,
appDeployCommand, appDeployCommand,
appUpgradeCommand, appUpgradeCommand,
appUndeployCommand, appUndeployCommand,
appBackupCommand,
appRestoreCommand,
appRemoveCommand, appRemoveCommand,
appCheckCommand, appCheckCommand,
appListCommand, appListCommand,

View File

@ -1,77 +0,0 @@
package app
import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var backupAllServices bool
var backupAllServicesFlag = &cli.BoolFlag{
Name: "all",
Value: false,
Destination: &backupAllServices,
Aliases: []string{"a"},
Usage: "Backup all services",
}
var appBackupCommand = &cli.Command{
Name: "backup",
Usage: "Backup an app",
Aliases: []string{"b"},
Flags: []cli.Flag{backupAllServicesFlag},
ArgsUsage: "<service>",
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.RECIPES_DIR, 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)
if err != nil {
logrus.Fatal(err)
}
if !strings.Contains(string(bytes), execCmd) {
logrus.Fatalf("%s doesn't have a %s function", app.Type, execCmd)
}
sourceAndExec := fmt.Sprintf("%s; %s", sourceCmd, execCmd)
cmd := exec.Command("bash", "-c", sourceAndExec)
if err := internal.RunCmd(cmd); err != nil {
logrus.Fatal(err)
}
return nil
},
BashComplete: autocomplete.AppNameComplete,
}

View File

@ -9,18 +9,22 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appCheckCommand = &cli.Command{ var appCheckCommand = cli.Command{
Name: "check", Name: "check",
Usage: "Check if app is configured correctly",
Aliases: []string{"c"}, Aliases: []string{"c"},
ArgsUsage: "<service>", Usage: "Check if app is configured correctly",
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
envSamplePath := path.Join(config.RECIPES_DIR, app.Type, ".env.sample") envSamplePath := path.Join(config.RECIPES_DIR, app.Recipe, ".env.sample")
if _, err := os.Stat(envSamplePath); err != nil { if _, err := os.Stat(envSamplePath); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
logrus.Fatalf("%s does not exist?", envSamplePath) logrus.Fatalf("%s does not exist?", envSamplePath)

View File

@ -10,13 +10,18 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appConfigCommand = &cli.Command{ var appConfigCommand = cli.Command{
Name: "config", Name: "config",
Aliases: []string{"c"}, Aliases: []string{"c"},
Usage: "Edit app config", Usage: "Edit app config",
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
appName := c.Args().First() appName := c.Args().First()

View File

@ -1,6 +1,7 @@
package app package app
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"strings" "strings"
@ -15,25 +16,29 @@ import (
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appCpCommand = &cli.Command{ var appCpCommand = cli.Command{
Name: "cp", Name: "cp",
Aliases: []string{"c"}, Aliases: []string{"c"},
ArgsUsage: "<src> <dst>", ArgsUsage: "<domain> <src> <dst>",
Usage: "Copy files to/from a running app service", Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
Usage: "Copy files to/from a running app service",
Description: ` Description: `
This command supports copying files to and from any app service file system. This command supports copying files to and from any app service file system.
If you want to copy a myfile.txt to the root of the app service: If you want to copy a myfile.txt to the root of the app service:
abra app cp <app> myfile.txt app:/ abra app cp <domain> myfile.txt app:/
And if you want to copy that file back to your current working directory locally: And if you want to copy that file back to your current working directory locally:
abra app cp <app> app:/myfile.txt . abra app cp <domain> app:/myfile.txt .
`, `,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
@ -100,25 +105,15 @@ func configureAndCp(
dstPath string, dstPath string,
service string, service string,
isToContainer bool) error { isToContainer bool) error {
appFiles, err := config.LoadAppFiles("")
if err != nil {
logrus.Fatal(err)
}
appEnv, err := config.GetApp(appFiles, app.Name)
if err != nil {
logrus.Fatal(err)
}
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("%s_%s", appEnv.StackName(), service)) filters.Add("name", fmt.Sprintf("%s_%s", app.StackName(), service))
container, err := container.GetContainer(c.Context, cl, filters, true) container, err := container.GetContainer(context.Background(), cl, filters, internal.NoInput)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -137,11 +132,11 @@ func configureAndCp(
} }
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false} copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(c.Context, container.ID, dstPath, content, copyOpts); err != nil { if err := cl.CopyToContainer(context.Background(), container.ID, dstPath, content, copyOpts); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} else { } else {
content, _, err := cl.CopyFromContainer(c.Context, container.ID, srcPath) content, _, err := cl.CopyFromContainer(context.Background(), container.ID, srcPath)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -151,5 +146,6 @@ func configureAndCp(
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
return nil return nil
} }

View File

@ -3,22 +3,26 @@ package app
import ( import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appDeployCommand = &cli.Command{ var appDeployCommand = cli.Command{
Name: "deploy", Name: "deploy",
Aliases: []string{"d"}, Aliases: []string{"d"},
Usage: "Deploy an app", Usage: "Deploy an app",
ArgsUsage: "<domain>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag, internal.ForceFlag,
internal.ChaosFlag, internal.ChaosFlag,
internal.NoDomainChecksFlag, internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag, internal.DontWaitConvergeFlag,
}, },
Before: internal.SubCommandBefore,
Description: ` Description: `
This command deploys an app. It does not support incrementing the version of a This command deploys an app. It does not support incrementing the version of a
deployed app, for this you need to look at the "abra app upgrade <app>" deployed app, for this you need to look at the "abra app upgrade <domain>"
command. command.
You may pass "--force" to re-deploy the same version again. This can be useful You may pass "--force" to re-deploy the same version again. This can be useful

View File

@ -1,6 +1,7 @@
package app package app
import ( import (
"context"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -15,12 +16,13 @@ import (
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appErrorsCommand = &cli.Command{ var appErrorsCommand = cli.Command{
Name: "errors", Name: "errors",
Usage: "List errors for a deployed app", Usage: "List errors for a deployed app",
ArgsUsage: "<domain>",
Description: ` Description: `
This command lists errors for a deployed app. This command lists errors for a deployed app.
@ -30,22 +32,25 @@ is failing to deploy or having issues, it could be a lot of things.
This command currently takes into account: This command currently takes into account:
Is the service deployed? Is the service deployed?
Is the service killed by an OOM error? Is the service killed by an OOM error?
Is the service reporting an error (like in "ps --no-trunc" output) Is the service reporting an error (like in "ps --no-trunc" output)
Is the service healthcheck failing? what are the healthcheck logs? Is the service healthcheck failing? what are the healthcheck logs?
Got any more ideas? Please let us know: Got any more ideas? Please let us know:
https://git.coopcloud.tech/coop-cloud/organising/issues/new/choose https://git.coopcloud.tech/coop-cloud/organising/issues/new/choose
This command is best accompanied by "abra app logs <app>" which may reveal 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 further information which can help you debug the cause of an app failure via
the logs. the logs.
`, `,
Aliases: []string{"e"}, Aliases: []string{"e"},
Flags: []cli.Flag{internal.WatchFlag}, Flags: []cli.Flag{
internal.DebugFlag,
internal.WatchFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
@ -55,7 +60,7 @@ the logs.
logrus.Fatal(err) logrus.Fatal(err)
} }
isDeployed, _, err := stack.IsDeployed(c.Context, cl, app.StackName()) isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -83,7 +88,7 @@ the logs.
} }
func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App) error { func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App) error {
recipe, err := recipe.Get(app.Type) recipe, err := recipe.Get(app.Recipe)
if err != nil { if err != nil {
return err return err
} }
@ -91,7 +96,7 @@ func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App) error
for _, service := range recipe.Config.Services { for _, service := range recipe.Config.Services {
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", service.Name) filters.Add("name", service.Name)
containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters}) containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters})
if err != nil { if err != nil {
return err return err
} }
@ -102,7 +107,7 @@ func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App) error
} }
container := containers[0] container := containers[0]
containerState, err := cl.ContainerInspect(c.Context, container.ID) containerState, err := cl.ContainerInspect(context.Background(), container.ID)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -12,31 +12,27 @@ import (
"coopcloud.tech/abra/pkg/ssh" "coopcloud.tech/abra/pkg/ssh"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var status bool var status bool
var statusFlag = &cli.BoolFlag{ var statusFlag = &cli.BoolFlag{
Name: "status", Name: "status, S",
Aliases: []string{"S"},
Value: false,
Usage: "Show app deployment status", Usage: "Show app deployment status",
Destination: &status, Destination: &status,
} }
var appType string var appRecipe string
var typeFlag = &cli.StringFlag{ var recipeFlag = &cli.StringFlag{
Name: "type", Name: "recipe, r",
Aliases: []string{"t"},
Value: "", Value: "",
Usage: "Show apps of a specific type", Usage: "Show apps of a specific recipe",
Destination: &appType, Destination: &appRecipe,
} }
var listAppServer string var listAppServer string
var listAppServerFlag = &cli.StringFlag{ var listAppServerFlag = &cli.StringFlag{
Name: "server", Name: "server, s",
Aliases: []string{"s"},
Value: "", Value: "",
Usage: "Show apps of a specific server", Usage: "Show apps of a specific server",
Destination: &listAppServer, Destination: &listAppServer,
@ -61,9 +57,10 @@ type serverStatus struct {
upgradeCount int upgradeCount int
} }
var appListCommand = &cli.Command{ var appListCommand = cli.Command{
Name: "list", Name: "list",
Usage: "List all managed apps", Aliases: []string{"ls"},
Usage: "List all managed apps",
Description: ` Description: `
This command looks at your local file system listing of apps and servers (e.g. 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. in ~/.abra/) to generate a report of all your apps.
@ -71,13 +68,14 @@ in ~/.abra/) to generate a report of all your apps.
By passing the "--status/-S" flag, you can query all your servers for the 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 actual live deployment status. Depending on how many servers you manage, this
can take some time. can take some time.
`, `,
Aliases: []string{"ls"},
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
statusFlag, statusFlag,
listAppServerFlag, listAppServerFlag,
typeFlag, recipeFlag,
}, },
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
appFiles, err := config.LoadAppFiles(listAppServer) appFiles, err := config.LoadAppFiles(listAppServer)
if err != nil { if err != nil {
@ -89,7 +87,7 @@ can take some time.
logrus.Fatal(err) logrus.Fatal(err)
} }
sort.Sort(config.ByServerAndType(apps)) sort.Sort(config.ByServerAndRecipe(apps))
statuses := make(map[string]map[string]string) statuses := make(map[string]map[string]string)
var catl recipe.RecipeCatalogue var catl recipe.RecipeCatalogue
@ -124,14 +122,14 @@ can take some time.
var ok bool var ok bool
if stats, ok = allStats[app.Server]; !ok { if stats, ok = allStats[app.Server]; !ok {
stats = serverStatus{} stats = serverStatus{}
if appType == "" { if appRecipe == "" {
// count server, no filtering // count server, no filtering
totalServersCount++ totalServersCount++
} }
} }
if app.Type == appType || appType == "" { if app.Recipe == appRecipe || appRecipe == "" {
if appType != "" { if appRecipe != "" {
// only count server if matches filter // only count server if matches filter
totalServersCount++ totalServersCount++
} }
@ -145,7 +143,9 @@ can take some time.
version := "unknown" version := "unknown"
if statusMeta, ok := statuses[app.StackName()]; ok { if statusMeta, ok := statuses[app.StackName()]; ok {
if currentVersion, exists := statusMeta["version"]; exists { if currentVersion, exists := statusMeta["version"]; exists {
version = currentVersion if currentVersion != "" {
version = currentVersion
}
} }
if statusMeta["status"] != "" { if statusMeta["status"] != "" {
status = statusMeta["status"] status = statusMeta["status"]
@ -160,7 +160,7 @@ can take some time.
var newUpdates []string var newUpdates []string
if version != "unknown" { if version != "unknown" {
updates, err := recipe.GetRecipeCatalogueVersions(app.Type, catl) updates, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -197,7 +197,7 @@ can take some time.
} }
appStats.server = app.Server appStats.server = app.Server
appStats.recipe = app.Type appStats.recipe = app.Recipe
appStats.appName = app.Name appStats.appName = app.Name
appStats.domain = app.Domain appStats.domain = app.Domain
@ -215,7 +215,7 @@ can take some time.
serverStat := allStats[app.Server] serverStat := allStats[app.Server]
tableCol := []string{"recipe", "domain", "app name"} tableCol := []string{"recipe", "domain"}
if status { if status {
tableCol = append(tableCol, []string{"status", "version", "upgrade"}...) tableCol = append(tableCol, []string{"status", "version", "upgrade"}...)
} }
@ -223,7 +223,7 @@ can take some time.
table := formatter.CreateTable(tableCol) table := formatter.CreateTable(tableCol)
for _, appStat := range serverStat.apps { for _, appStat := range serverStat.apps {
tableRow := []string{appStat.recipe, appStat.domain, appStat.appName} tableRow := []string{appStat.recipe, appStat.domain}
if status { if status {
tableRow = append(tableRow, []string{appStat.status, appStat.version, appStat.upgrade}...) tableRow = append(tableRow, []string{appStat.status, appStat.version, appStat.upgrade}...)
} }

View File

@ -1,6 +1,7 @@
package app package app
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -15,7 +16,7 @@ import (
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var logOpts = types.ContainerLogsOptions{ var logOpts = types.ContainerLogsOptions{
@ -32,7 +33,7 @@ func stackLogs(c *cli.Context, stackName string, client *dockerClient.Client) {
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", stackName) filters.Add("name", stackName)
serviceOpts := types.ServiceListOptions{Filters: filters} serviceOpts := types.ServiceListOptions{Filters: filters}
services, err := client.ServiceList(c.Context, serviceOpts) services, err := client.ServiceList(context.Background(), serviceOpts)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -45,7 +46,7 @@ func stackLogs(c *cli.Context, stackName string, client *dockerClient.Client) {
logOpts.ShowStdout = false logOpts.ShowStdout = false
} }
logs, err := client.ServiceLogs(c.Context, s, logOpts) logs, err := client.ServiceLogs(context.Background(), s, logOpts)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -63,14 +64,16 @@ func stackLogs(c *cli.Context, stackName string, client *dockerClient.Client) {
os.Exit(0) os.Exit(0)
} }
var appLogsCommand = &cli.Command{ var appLogsCommand = cli.Command{
Name: "logs", Name: "logs",
Aliases: []string{"l"}, Aliases: []string{"l"},
ArgsUsage: "[<service>]", ArgsUsage: "<domain> [<service>]",
Usage: "Tail app logs", Usage: "Tail app logs",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.StdErrOnlyFlag, internal.StdErrOnlyFlag,
internal.DebugFlag,
}, },
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
@ -82,7 +85,7 @@ var appLogsCommand = &cli.Command{
serviceName := c.Args().Get(1) serviceName := c.Args().Get(1)
if serviceName == "" { if serviceName == "" {
logrus.Debugf("tailing logs for all %s services", app.Type) logrus.Debugf("tailing logs for all %s services", app.Recipe)
stackLogs(c, app.StackName(), cl) stackLogs(c, app.StackName(), cl)
} else { } else {
logrus.Debugf("tailing logs for %s", serviceName) logrus.Debugf("tailing logs for %s", serviceName)
@ -98,7 +101,7 @@ var appLogsCommand = &cli.Command{
func tailServiceLogs(c *cli.Context, cl *dockerClient.Client, app config.App, serviceName string) error { func tailServiceLogs(c *cli.Context, cl *dockerClient.Client, app config.App, serviceName string) error {
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("%s_%s", app.StackName(), serviceName)) filters.Add("name", fmt.Sprintf("%s_%s", app.StackName(), serviceName))
chosenService, err := service.GetService(c.Context, cl, filters, internal.NoInput) chosenService, err := service.GetService(context.Background(), cl, filters, internal.NoInput)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -107,7 +110,7 @@ func tailServiceLogs(c *cli.Context, cl *dockerClient.Client, app config.App, se
logOpts.ShowStdout = false logOpts.ShowStdout = false
} }
logs, err := cl.ServiceLogs(c.Context, chosenService.ID, logOpts) logs, err := cl.ServiceLogs(context.Background(), chosenService.ID, logOpts)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -3,7 +3,7 @@ package app
import ( import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appNewDescription = ` var appNewDescription = `
@ -11,7 +11,7 @@ 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. 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 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) You can see what recipes are available (i.e. values for the <recipe> argument)
by running "abra recipe ls". by running "abra recipe ls".
@ -26,19 +26,21 @@ pass store (see passwordstore.org for more). The pass command must be available
on your $PATH. on your $PATH.
` `
var appNewCommand = &cli.Command{ var appNewCommand = cli.Command{
Name: "new", Name: "new",
Usage: "Create a new app",
Aliases: []string{"n"}, Aliases: []string{"n"},
Usage: "Create a new app",
Description: appNewDescription, Description: appNewDescription,
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.NewAppServerFlag, internal.NewAppServerFlag,
internal.DomainFlag, internal.DomainFlag,
internal.NewAppNameFlag,
internal.PassFlag, internal.PassFlag,
internal.SecretsFlag, internal.SecretsFlag,
}, },
ArgsUsage: "<recipe>", Before: internal.SubCommandBefore,
ArgsUsage: "[<recipe>]",
Action: internal.NewAction, Action: internal.NewAction,
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
} }

View File

@ -1,6 +1,7 @@
package app package app
import ( import (
"context"
"strings" "strings"
"time" "time"
@ -17,17 +18,20 @@ import (
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appPsCommand = &cli.Command{ var appPsCommand = cli.Command{
Name: "ps", Name: "ps",
Usage: "Check app status",
Description: "This command shows a more detailed status output of a specific deployed app.",
Aliases: []string{"p"}, Aliases: []string{"p"},
Usage: "Check app status",
ArgsUsage: "<domain>",
Description: "This command shows a more detailed status output of a specific deployed app.",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.WatchFlag, internal.WatchFlag,
internal.DebugFlag,
}, },
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
@ -37,7 +41,7 @@ var appPsCommand = &cli.Command{
logrus.Fatal(err) logrus.Fatal(err)
} }
isDeployed, _, err := stack.IsDeployed(c.Context, cl, app.StackName()) isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -66,7 +70,7 @@ func showPSOutput(c *cli.Context, app config.App, cl *dockerClient.Client) {
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", app.StackName()) 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 { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -1,6 +1,7 @@
package app package app
import ( import (
"context"
"fmt" "fmt"
"os" "os"
@ -12,7 +13,7 @@ import (
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// Volumes stores the variable from VolumesFlag // Volumes stores the variable from VolumesFlag
@ -20,32 +21,35 @@ var Volumes bool
// VolumesFlag is used to specify if volumes should be deleted when deleting an app // VolumesFlag is used to specify if volumes should be deleted when deleting an app
var VolumesFlag = &cli.BoolFlag{ var VolumesFlag = &cli.BoolFlag{
Name: "volumes", Name: "volumes, V",
Value: false,
Destination: &Volumes, Destination: &Volumes,
} }
var appRemoveCommand = &cli.Command{ var appRemoveCommand = cli.Command{
Name: "remove", Name: "remove",
Usage: "Remove an already undeployed app", Aliases: []string{"rm"},
Aliases: []string{"rm"}, ArgsUsage: "<domain>",
Usage: "Remove an already undeployed app",
Flags: []cli.Flag{ Flags: []cli.Flag{
VolumesFlag, VolumesFlag,
internal.ForceFlag, internal.ForceFlag,
internal.DebugFlag,
internal.NoInputFlag,
}, },
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if !internal.Force { if !internal.Force && !internal.NoInput {
response := false response := false
prompt := &survey.Confirm{ prompt := &survey.Confirm{
Message: fmt.Sprintf("about to delete %s, are you sure?", app.Name), Message: fmt.Sprintf("about to remove %s, are you sure?", app.Name),
} }
if err := survey.AskOne(prompt, &response); err != nil { if err := survey.AskOne(prompt, &response); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !response { if !response {
logrus.Fatal("user aborted app removal") logrus.Fatal("aborting as requested")
} }
} }
@ -54,19 +58,17 @@ var appRemoveCommand = &cli.Command{
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Force { isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
isDeployed, _, err := stack.IsDeployed(c.Context, cl, app.StackName()) if err != nil {
if err != nil { logrus.Fatal(err)
logrus.Fatal(err) }
} if isDeployed {
if isDeployed { logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name)
logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s \" or pass --force", app.Name, app.Name)
}
} }
fs := filters.NewArgs() fs := filters.NewArgs()
fs.Add("name", app.Name) fs.Add("name", app.StackName())
secretList, err := cl.SecretList(c.Context, types.SecretListOptions{Filters: fs}) secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: fs})
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -81,7 +83,8 @@ var appRemoveCommand = &cli.Command{
if len(secrets) > 0 { if len(secrets) > 0 {
var secretNamesToRemove []string var secretNamesToRemove []string
if !internal.Force {
if !internal.Force && !internal.NoInput {
secretsPrompt := &survey.MultiSelect{ secretsPrompt := &survey.MultiSelect{
Message: "which secrets do you want to remove?", Message: "which secrets do you want to remove?",
Help: "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled", Help: "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled",
@ -94,8 +97,12 @@ var appRemoveCommand = &cli.Command{
} }
} }
if internal.Force || internal.NoInput {
secretNamesToRemove = secretNames
}
for _, name := range secretNamesToRemove { for _, name := range secretNamesToRemove {
err := cl.SecretRemove(c.Context, secrets[name]) err := cl.SecretRemove(context.Background(), secrets[name])
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -105,7 +112,7 @@ var appRemoveCommand = &cli.Command{
logrus.Info("no secrets to remove") logrus.Info("no secrets to remove")
} }
volumeListOKBody, err := cl.VolumeList(c.Context, fs) volumeListOKBody, err := cl.VolumeList(context.Background(), fs)
volumeList := volumeListOKBody.Volumes volumeList := volumeListOKBody.Volumes
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -119,7 +126,7 @@ var appRemoveCommand = &cli.Command{
if len(vols) > 0 { if len(vols) > 0 {
if Volumes { if Volumes {
var removeVols []string var removeVols []string
if !internal.Force { if !internal.Force && !internal.NoInput {
volumesPrompt := &survey.MultiSelect{ volumesPrompt := &survey.MultiSelect{
Message: "which volumes do you want to remove?", Message: "which volumes do you want to remove?",
Help: "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled", Help: "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled",
@ -131,8 +138,9 @@ var appRemoveCommand = &cli.Command{
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
for _, vol := range removeVols { for _, vol := range removeVols {
err := cl.VolumeRemove(c.Context, vol, internal.Force) // last argument is for force removing err := cl.VolumeRemove(context.Background(), vol, internal.Force) // last argument is for force removing
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -142,7 +150,9 @@ var appRemoveCommand = &cli.Command{
logrus.Info("no volumes were removed") logrus.Info("no volumes were removed")
} }
} else { } else {
logrus.Info("no volumes to remove") if Volumes {
logrus.Info("no volumes to remove")
}
} }
err = os.Remove(app.Path) err = os.Remove(app.Path)

View File

@ -1,6 +1,7 @@
package app package app
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
@ -10,14 +11,18 @@ import (
upstream "coopcloud.tech/abra/pkg/upstream/service" upstream "coopcloud.tech/abra/pkg/upstream/service"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appRestartCommand = &cli.Command{ var appRestartCommand = cli.Command{
Name: "restart", Name: "restart",
Usage: "Restart an app", Aliases: []string{"re"},
Aliases: []string{"re"}, Usage: "Restart an app",
ArgsUsage: "<service>", ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
Description: `This command restarts a service within a deployed app.`, Description: `This command restarts a service within a deployed app.`,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
@ -37,22 +42,22 @@ var appRestartCommand = &cli.Command{
serviceName := fmt.Sprintf("%s_%s", app.StackName(), serviceNameShort) serviceName := fmt.Sprintf("%s_%s", app.StackName(), serviceNameShort)
logrus.Debugf("attempting to scale %s to 0 (restart logic)", serviceName) logrus.Debugf("attempting to scale %s to 0 (restart logic)", serviceName)
if err := upstream.RunServiceScale(c.Context, cl, serviceName, 0); err != nil { if err := upstream.RunServiceScale(context.Background(), cl, serviceName, 0); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if err := stack.WaitOnService(c.Context, cl, serviceName, app.Name); err != nil { if err := stack.WaitOnService(context.Background(), cl, serviceName, app.Name); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("%s has been scaled to 0 (restart logic)", serviceName) logrus.Debugf("%s has been scaled to 0 (restart logic)", serviceName)
logrus.Debugf("attempting to scale %s to 1 (restart logic)", serviceName) logrus.Debugf("attempting to scale %s to 1 (restart logic)", serviceName)
if err := upstream.RunServiceScale(c.Context, cl, serviceName, 1); err != nil { if err := upstream.RunServiceScale(context.Background(), cl, serviceName, 1); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if err := stack.WaitOnService(c.Context, cl, serviceName, app.Name); err != nil { if err := stack.WaitOnService(context.Background(), cl, serviceName, app.Name); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -1,79 +0,0 @@
package app
import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var restoreAllServices bool
var restoreAllServicesFlag = &cli.BoolFlag{
Name: "all",
Value: false,
Destination: &restoreAllServices,
Aliases: []string{"a"},
Usage: "Restore all services",
}
var appRestoreCommand = &cli.Command{
Name: "restore",
Usage: "Restore an app from a backup",
Aliases: []string{"rs"},
Flags: []cli.Flag{restoreAllServicesFlag},
ArgsUsage: "<service> [<backup file>]",
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.RECIPES_DIR, 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)
if err != nil {
logrus.Fatal(err)
}
if !strings.Contains(string(bytes), execCmd) {
logrus.Fatalf("%s doesn't have a %s function", app.Type, execCmd)
}
backupFile := c.Args().Get(2)
if backupFile != "" {
execCmd = fmt.Sprintf("%s %s", execCmd, backupFile)
}
sourceAndExec := fmt.Sprintf("%s; %s", sourceCmd, execCmd)
cmd := exec.Command("bash", "-c", sourceAndExec)
if err := internal.RunCmd(cmd); err != nil {
logrus.Fatal(err)
}
return nil
},
}

View File

@ -1,6 +1,7 @@
package app package app
import ( import (
"context"
"fmt" "fmt"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
@ -14,19 +15,23 @@ import (
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appRollbackCommand = &cli.Command{ var appRollbackCommand = cli.Command{
Name: "rollback", Name: "rollback",
Usage: "Roll an app back to a previous version",
Aliases: []string{"rl"}, Aliases: []string{"rl"},
ArgsUsage: "<app>", Usage: "Roll an app back to a previous version",
ArgsUsage: "<domain>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag, internal.ForceFlag,
internal.ChaosFlag, internal.ChaosFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag, internal.DontWaitConvergeFlag,
}, },
Before: internal.SubCommandBefore,
Description: ` Description: `
This command rolls an app back to a previous version if one exists. This command rolls an app back to a previous version if one exists.
@ -34,7 +39,7 @@ 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. useful if the container runtime has gotten into a weird state.
This action could be destructive, please ensure you have a copy of your app This action could be destructive, please ensure you have a copy of your app
data beforehand - see "abra app backup <app>" for more. data beforehand.
Chas mode ("--chaos") will deploy your local checkout of a recipe as-is, Chas mode ("--chaos") will deploy your local checkout of a recipe as-is,
including unstaged changes and can be useful for live hacking and testing new including unstaged changes and can be useful for live hacking and testing new
@ -45,11 +50,13 @@ recipes.
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
stackName := app.StackName() stackName := app.StackName()
if err := recipe.EnsureUpToDate(app.Type); err != nil { if !internal.Chaos {
logrus.Fatal(err) if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
} }
r, err := recipe.Get(app.Type) r, err := recipe.Get(app.Recipe)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -65,7 +72,7 @@ recipes.
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 { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -79,13 +86,13 @@ recipes.
logrus.Fatal(err) logrus.Fatal(err)
} }
versions, err := recipe.GetRecipeCatalogueVersions(app.Type, catl) versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if len(versions) == 0 && !internal.Chaos { if len(versions) == 0 && !internal.Chaos {
logrus.Fatalf("no published releases for %s in the recipe catalogue?", app.Type) logrus.Fatalf("no published releases for %s in the recipe catalogue?", app.Recipe)
} }
var availableDowngrades []string var availableDowngrades []string
@ -119,7 +126,7 @@ recipes.
var chosenDowngrade string var chosenDowngrade string
if !internal.Chaos { if !internal.Chaos {
if internal.Force { if internal.Force || internal.NoInput {
chosenDowngrade = availableDowngrades[0] chosenDowngrade = availableDowngrades[0]
logrus.Debugf("choosing %s as version to downgrade to (--force)", chosenDowngrade) logrus.Debugf("choosing %s as version to downgrade to (--force)", chosenDowngrade)
} else { } else {
@ -134,7 +141,7 @@ recipes.
} }
if !internal.Chaos { if !internal.Chaos {
if err := recipe.EnsureVersion(app.Type, chosenDowngrade); err != nil { if err := recipe.EnsureVersion(app.Recipe, chosenDowngrade); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
@ -142,13 +149,13 @@ recipes.
if internal.Chaos { if internal.Chaos {
logrus.Warn("chaos mode engaged") logrus.Warn("chaos mode engaged")
var err error var err error
chosenDowngrade, err = recipe.ChaosVersion(app.Type) chosenDowngrade, err = recipe.ChaosVersion(app.Recipe)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Type, "abra.sh") abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath) abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -157,7 +164,7 @@ recipes.
app.Env[k] = v app.Env[k] = v
} }
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env) composeFiles, err := config.GetAppComposeFiles(app.Recipe, app.Env)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -1,6 +1,7 @@
package app package app
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
@ -13,41 +14,42 @@ import (
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var user string var user string
var userFlag = &cli.StringFlag{ var userFlag = &cli.StringFlag{
Name: "user", Name: "user, u",
Value: "", Value: "",
Destination: &user, Destination: &user,
} }
var noTTY bool var noTTY bool
var noTTYFlag = &cli.BoolFlag{ var noTTYFlag = &cli.BoolFlag{
Name: "no-tty", Name: "no-tty, t",
Value: false,
Destination: &noTTY, Destination: &noTTY,
} }
var appRunCommand = &cli.Command{ var appRunCommand = cli.Command{
Name: "run", Name: "run",
Aliases: []string{"r"},
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
noTTYFlag, noTTYFlag,
userFlag, userFlag,
}, },
Aliases: []string{"r"}, Before: internal.SubCommandBefore,
ArgsUsage: "<service> <args>...", ArgsUsage: "<domain> <service> <args>...",
Usage: "Run a command in a service container", Usage: "Run a command in a service container",
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if c.Args().Len() < 2 { if len(c.Args()) < 2 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <service> provided?")) internal.ShowSubcommandHelpAndError(c, errors.New("no <service> provided?"))
} }
if c.Args().Len() < 3 { if len(c.Args()) < 3 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <args> provided?")) internal.ShowSubcommandHelpAndError(c, errors.New("no <args> provided?"))
} }
@ -61,12 +63,12 @@ var appRunCommand = &cli.Command{
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", stackAndServiceName) filters.Add("name", stackAndServiceName)
targetContainer, err := containerPkg.GetContainer(c.Context, cl, filters, true) targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, false)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
cmd := c.Args().Slice()[2:] cmd := c.Args()[2:]
execCreateOpts := types.ExecConfig{ execCreateOpts := types.ExecConfig{
AttachStderr: true, AttachStderr: true,
AttachStdin: true, AttachStdin: true,

View File

@ -1,6 +1,7 @@
package app package app
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@ -9,33 +10,46 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var allSecrets bool var allSecrets bool
var allSecretsFlag = &cli.BoolFlag{ var allSecretsFlag = &cli.BoolFlag{
Name: "all", Name: "all, a",
Aliases: []string{"A"},
Value: false,
Destination: &allSecrets, Destination: &allSecrets,
Usage: "Generate all secrets", Usage: "Generate all secrets",
} }
var appSecretGenerateCommand = &cli.Command{ var rmAllSecrets bool
var rmAllSecretsFlag = &cli.BoolFlag{
Name: "all, a",
Destination: &rmAllSecrets,
Usage: "Remove all secrets",
}
var appSecretGenerateCommand = cli.Command{
Name: "generate", Name: "generate",
Aliases: []string{"g"}, Aliases: []string{"g"},
Usage: "Generate secrets", Usage: "Generate secrets",
ArgsUsage: "<secret> <version>", ArgsUsage: "<domain> <secret> <version>",
Flags: []cli.Flag{allSecretsFlag, internal.PassFlag}, Flags: []cli.Flag{
internal.DebugFlag,
allSecretsFlag,
internal.PassFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if c.Args().Len() == 1 && !allSecrets { if len(c.Args()) == 1 && !allSecrets {
err := errors.New("missing arguments <secret>/<version> or '--all'") err := errors.New("missing arguments <secret>/<version> or '--all'")
internal.ShowSubcommandHelpAndError(c, err) internal.ShowSubcommandHelpAndError(c, err)
} }
@ -57,8 +71,10 @@ var appSecretGenerateCommand = &cli.Command{
parsed := secret.ParseSecretEnvVarName(sec) parsed := secret.ParseSecretEnvVarName(sec)
if secretName == parsed { if secretName == parsed {
secretsToCreate[sec] = secretVersion secretsToCreate[sec] = secretVersion
matches = true
} }
} }
if !matches { if !matches {
logrus.Fatalf("%s doesn't exist in the env config?", secretName) logrus.Fatalf("%s doesn't exist in the env config?", secretName)
} }
@ -71,7 +87,7 @@ var appSecretGenerateCommand = &cli.Command{
if internal.Pass { if internal.Pass {
for name, data := range secretVals { 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) logrus.Fatal(err)
} }
} }
@ -94,12 +110,17 @@ var appSecretGenerateCommand = &cli.Command{
}, },
} }
var appSecretInsertCommand = &cli.Command{ var appSecretInsertCommand = cli.Command{
Name: "insert", Name: "insert",
Aliases: []string{"i"}, Aliases: []string{"i"},
Usage: "Insert secret", Usage: "Insert secret",
Flags: []cli.Flag{internal.PassFlag}, Flags: []cli.Flag{
ArgsUsage: "<app> <secret-name> <version> <data>", internal.DebugFlag,
internal.PassFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> <secret-name> <version> <data>",
BashComplete: autocomplete.AppNameComplete,
Description: ` Description: `
This command inserts a secret into an app environment. This command inserts a secret into an app environment.
@ -115,7 +136,7 @@ Example:
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if c.Args().Len() != 4 { if len(c.Args()) != 4 {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments?")) internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments?"))
} }
@ -128,8 +149,10 @@ Example:
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Infof("%s successfully stored on server", secretName)
if internal.Pass { 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) logrus.Fatal(err)
} }
} }
@ -138,28 +161,54 @@ Example:
}, },
} }
var appSecretRmCommand = &cli.Command{ // secretRm removes a secret.
Name: "remove", func secretRm(cl *dockerClient.Client, app config.App, secretName, parsed string) error {
Usage: "Remove a secret", if err := cl.SecretRemove(context.Background(), secretName); err != nil {
Aliases: []string{"rm"}, return err
Flags: []cli.Flag{allSecretsFlag, internal.PassFlag}, }
ArgsUsage: "<app> <secret-name>",
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,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> [<secret-name>]",
BashComplete: autocomplete.AppNameComplete,
Description: ` Description: `
This command removes a secret from an app environment. This command removes app secrets.
Example: Example:
abra app secret remove myapp db_pass abra app secret remove myapp db_pass
`, `,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
secrets := secret.ReadSecretEnvVars(app.Env)
if c.Args().Get(1) != "" && allSecrets { if c.Args().Get(1) != "" && rmAllSecrets {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<secret-name>' and '--all' together")) internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<secret-name>' and '--all' together"))
} }
if c.Args().Get(1) == "" && !allSecrets { if c.Args().Get(1) == "" && !rmAllSecrets {
internal.ShowSubcommandHelpAndError(c, errors.New("no secret(s) specified?")) internal.ShowSubcommandHelpAndError(c, errors.New("no secret(s) specified?"))
} }
@ -170,46 +219,66 @@ Example:
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", app.StackName()) 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 { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
secretToRm := c.Args().Get(1) remoteSecretNames := make(map[string]bool)
for _, cont := range secretList { for _, cont := range secretList {
secretName := cont.Spec.Annotations.Name remoteSecretNames[cont.Spec.Annotations.Name] = true
parsed := secret.ParseGeneratedSecretName(secretName, app) }
if allSecrets {
if err := cl.SecretRemove(c.Context, secretName); err != nil { match := false
logrus.Fatal(err) secretToRm := c.Args().Get(1)
} for sec := range secrets {
if internal.Pass { secretName := secret.ParseSecretEnvVarName(sec)
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
logrus.Fatal(err) secVal, err := secret.ParseSecretEnvVarValue(secrets[sec])
} if err != nil {
} logrus.Fatal(err)
} else { }
if parsed == secretToRm {
if err := cl.SecretRemove(c.Context, secretName); err != nil { secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, secVal.Version)
logrus.Fatal(err) if _, ok := remoteSecretNames[secretRemoteName]; ok {
} if secretToRm != "" {
if internal.Pass { if secretName == secretToRm {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil { if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
return nil
}
} else {
match = true
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
logrus.Fatal(err)
} }
} }
} }
} }
if !match && secretToRm != "" {
logrus.Fatalf("%s doesn't exist on server?", secretToRm)
}
if !match {
logrus.Fatal("no secrets to remove?")
}
return nil return nil
}, },
} }
var appSecretLsCommand = &cli.Command{ var appSecretLsCommand = cli.Command{
Name: "list", Name: "list",
Usage: "List all secrets",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
Usage: "List all secrets",
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
secrets := secret.ReadSecretEnvVars(app.Env) secrets := secret.ReadSecretEnvVars(app.Env)
@ -224,7 +293,7 @@ var appSecretLsCommand = &cli.Command{
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", app.StackName()) 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 { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -260,12 +329,12 @@ var appSecretLsCommand = &cli.Command{
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
} }
var appSecretCommand = &cli.Command{ var appSecretCommand = cli.Command{
Name: "secret", Name: "secret",
Aliases: []string{"s"}, Aliases: []string{"s"},
Usage: "Manage app secrets", Usage: "Manage app secrets",
ArgsUsage: "<command>", ArgsUsage: "<domain>",
Subcommands: []*cli.Command{ Subcommands: []cli.Command{
appSecretGenerateCommand, appSecretGenerateCommand,
appSecretInsertCommand, appSecretInsertCommand,
appSecretRmCommand, appSecretRmCommand,

View File

@ -1,18 +1,26 @@
package app package app
import ( import (
"context"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appUndeployCommand = &cli.Command{ var appUndeployCommand = cli.Command{
Name: "undeploy", Name: "undeploy",
Aliases: []string{"un"}, Aliases: []string{"un"},
Usage: "Undeploy an app", ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
Usage: "Undeploy an app",
Description: ` Description: `
This does not destroy any of the application data. However, you should remain This does not destroy any of the application data. However, you should remain
vigilant, as your swarm installation will consider any previously attached vigilant, as your swarm installation will consider any previously attached
@ -29,7 +37,7 @@ volumes as eligiblef or pruning once undeployed.
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 { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -43,7 +51,7 @@ volumes as eligiblef or pruning once undeployed.
} }
rmOpts := stack.Remove{Namespaces: []string{app.StackName()}} rmOpts := stack.Remove{Namespaces: []string{app.StackName()}}
if err := stack.RunRemove(c.Context, cl, rmOpts); err != nil { if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -1,6 +1,7 @@
package app package app
import ( import (
"context"
"fmt" "fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
@ -13,32 +14,36 @@ import (
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var appUpgradeCommand = &cli.Command{ var appUpgradeCommand = cli.Command{
Name: "upgrade", Name: "upgrade",
Aliases: []string{"up"}, Aliases: []string{"up"},
Usage: "Upgrade an app", Usage: "Upgrade an app",
ArgsUsage: "<app>", ArgsUsage: "<domain>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag, internal.ForceFlag,
internal.ChaosFlag, internal.ChaosFlag,
internal.NoDomainChecksFlag, internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
}, },
Before: internal.SubCommandBefore,
Description: ` Description: `
This command supports upgrading an app. You can use it to choose and roll out a This command supports upgrading an app. You can use it to choose and roll out a
new upgrade to an existing app. new upgrade to an existing app.
This command specifically supports incrementing the version of running apps, as This command specifically supports incrementing the version of running apps, as
opposed to "abra app deploy <app>" which will not change the version of a opposed to "abra app deploy <domain>" which will not change the version of a
deployed app. deployed app.
You may pass "--force/-f" to upgrade to the same version again. This can be 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. useful if the container runtime has gotten into a weird state.
This action could be destructive, please ensure you have a copy of your app This action could be destructive, please ensure you have a copy of your app
data beforehand - see "abra app backup <app>" for more. data beforehand.
Chas mode ("--chaos") will deploy your local checkout of a recipe as-is, Chas mode ("--chaos") will deploy your local checkout of a recipe as-is,
including unstaged changes and can be useful for live hacking and testing new including unstaged changes and can be useful for live hacking and testing new
@ -48,11 +53,13 @@ recipes.
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
stackName := app.StackName() stackName := app.StackName()
if err := recipe.EnsureUpToDate(app.Type); err != nil { if !internal.Chaos {
logrus.Fatal(err) if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
} }
r, err := recipe.Get(app.Type) r, err := recipe.Get(app.Recipe)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -68,7 +75,7 @@ recipes.
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 { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -82,17 +89,17 @@ recipes.
logrus.Fatal(err) logrus.Fatal(err)
} }
versions, err := recipe.GetRecipeCatalogueVersions(app.Type, catl) versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if len(versions) == 0 && !internal.Chaos { if len(versions) == 0 && !internal.Chaos {
logrus.Fatalf("no published releases for %s in the recipe catalogue?", app.Type) logrus.Fatalf("no published releases for %s in the recipe catalogue?", app.Recipe)
} }
var availableUpgrades []string var availableUpgrades []string
if deployedVersion == "uknown" { if deployedVersion == "unknown" {
availableUpgrades = versions availableUpgrades = versions
logrus.Warnf("failed to determine version of deployed %s", app.Name) logrus.Warnf("failed to determine version of deployed %s", app.Name)
} }
@ -122,7 +129,7 @@ recipes.
var chosenUpgrade string var chosenUpgrade string
if len(availableUpgrades) > 0 && !internal.Chaos { if len(availableUpgrades) > 0 && !internal.Chaos {
if internal.Force { if internal.Force || internal.NoInput {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1] chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
logrus.Debugf("choosing %s as version to upgrade to", chosenUpgrade) logrus.Debugf("choosing %s as version to upgrade to", chosenUpgrade)
} else { } else {
@ -139,13 +146,13 @@ recipes.
// if release notes written after git tag published, read them before we // if release notes written after git tag published, read them before we
// check out the tag and then they'll appear to be missing. this covers // check out the tag and then they'll appear to be missing. this covers
// when we obviously will forget to write release notes before publishing // when we obviously will forget to write release notes before publishing
releaseNotes, err := internal.GetReleaseNotes(app.Type, chosenUpgrade) releaseNotes, err := internal.GetReleaseNotes(app.Recipe, chosenUpgrade)
if err != nil { if err != nil {
return err return err
} }
if !internal.Chaos { if !internal.Chaos {
if err := recipe.EnsureVersion(app.Type, chosenUpgrade); err != nil { if err := recipe.EnsureVersion(app.Recipe, chosenUpgrade); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
@ -153,13 +160,13 @@ recipes.
if internal.Chaos { if internal.Chaos {
logrus.Warn("chaos mode engaged") logrus.Warn("chaos mode engaged")
var err error var err error
chosenUpgrade, err = recipe.ChaosVersion(app.Type) chosenUpgrade, err = recipe.ChaosVersion(app.Recipe)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Type, "abra.sh") abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath) abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -168,7 +175,7 @@ recipes.
app.Env[k] = v app.Env[k] = v
} }
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env) composeFiles, err := config.GetAppComposeFiles(app.Recipe, app.Env)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -1,7 +1,7 @@
package app package app
import ( import (
"strings" "context"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
@ -11,7 +11,7 @@ import (
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// getImagePath returns the image name // getImagePath returns the image name
@ -22,19 +22,24 @@ func getImagePath(image string) (string, error) {
} }
path := reference.Path(img) path := reference.Path(img)
if strings.Contains(path, "library") {
path = strings.Split(path, "/")[1] path = formatter.StripTagMeta(path)
}
logrus.Debugf("parsed %s from %s", path, image) logrus.Debugf("parsed %s from %s", path, image)
return path, nil return path, nil
} }
var appVersionCommand = &cli.Command{ var appVersionCommand = cli.Command{
Name: "version", Name: "version",
Aliases: []string{"v"}, Aliases: []string{"v"},
Usage: "Show app versions", ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
Usage: "Show app versions",
Description: ` Description: `
This command shows all information about versioning related to a deployed app. 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 This includes the individual image names, tags and digests. But also the Co-op
@ -51,7 +56,7 @@ Cloud recipe version.
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 { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -64,7 +69,7 @@ Cloud recipe version.
logrus.Fatalf("%s is not deployed?", app.Name) logrus.Fatalf("%s is not deployed?", app.Name)
} }
recipeMeta, err := recipe.GetRecipeMeta(app.Type) recipeMeta, err := recipe.GetRecipeMeta(app.Recipe)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

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

View File

@ -15,57 +15,61 @@ import (
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// CatalogueSkipList is all the repos that are not recipes. // CatalogueSkipList is all the repos that are not recipes.
var CatalogueSkipList = map[string]bool{ var CatalogueSkipList = map[string]bool{
"abra": true, "abra": true,
"abra-apps": true, "abra-apps": true,
"abra-aur": true, "abra-aur": true,
"abra-bash": true, "abra-bash": true,
"abra-capsul": true, "abra-capsul": true,
"abra-gandi": true, "abra-gandi": true,
"abra-hetzner": true, "abra-hetzner": true,
"apps": true, "apps": true,
"aur-abra-git": true, "aur-abra-git": true,
"auto-apps-json": true, "auto-apps-json": true,
"auto-mirror": true, "auto-mirror": true,
"backup-bot": true, "backup-bot": true,
"backup-bot-two": true, "backup-bot-two": true,
"comrade-renovate-bot": true, "beta.coopcloud.tech": true,
"coopcloud.tech": true, "comrade-renovate-bot": true,
"coturn": true, "coopcloud.tech": true,
"docker-cp-deploy": true, "coturn": true,
"docker-dind-bats-kcov": true, "docker-cp-deploy": true,
"docs.coopcloud.tech": true, "docker-dind-bats-kcov": true,
"drone-abra": true, "docs.coopcloud.tech": true,
"example": true, "drone-abra": true,
"gardening": true, "example": true,
"go-abra": true, "gardening": true,
"organising": true, "go-abra": true,
"outline-with-patch": true, "organising": true,
"pyabra": true, "outline-with-patch": true,
"radicle-seed-node": true, "pyabra": true,
"recipes": true, "radicle-seed-node": true,
"stack-ssh-deploy": true, "recipes-catalogue-json": true,
"swarm-cronjob": true, "recipes-wishlist": true,
"tagcmp": true, "recipes.coopcloud.tech": true,
"traefik-cert-dumper": true, "stack-ssh-deploy": true,
"tyop": true, "swarm-cronjob": true,
"tagcmp": true,
"traefik-cert-dumper": true,
"tyop": true,
} }
var catalogueGenerateCommand = &cli.Command{ var catalogueGenerateCommand = cli.Command{
Name: "generate", Name: "generate",
Aliases: []string{"g"}, Aliases: []string{"g"},
Usage: "Generate the recipe catalogue", Usage: "Generate the recipe catalogue",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.PublishFlag, internal.PublishFlag,
internal.DryFlag, internal.DryFlag,
internal.SkipUpdatesFlag, internal.SkipUpdatesFlag,
internal.RegistryUsernameFlag,
internal.RegistryPasswordFlag,
}, },
Before: internal.SubCommandBefore,
Description: ` Description: `
This command generates a new copy of the recipe catalogue which can be found on: This command generates a new copy of the recipe catalogue which can be found on:
@ -90,13 +94,7 @@ keys configured on your account.
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipeName := c.Args().First() recipeName := c.Args().First()
if recipeName != "" { if recipeName != "" {
internal.ValidateRecipe(c) internal.ValidateRecipe(c, true)
}
catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "recipes")
if err := gitPkg.Clone(catalogueDir, url); err != nil {
return err
} }
repos, err := recipe.ReadReposMetadata() repos, err := recipe.ReadReposMetadata()
@ -134,13 +132,9 @@ keys configured on your account.
continue continue
} }
versions, err := recipe.GetRecipeVersions( versions, err := recipe.GetRecipeVersions(recipeMeta.Name)
recipeMeta.Name,
internal.RegistryUsername,
internal.RegistryPassword,
)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Warn(err)
} }
features, category, err := recipe.GetRecipeFeaturesAndCategory(recipeMeta.Name) features, category, err := recipe.GetRecipeFeaturesAndCategory(recipeMeta.Name)
@ -217,7 +211,7 @@ keys configured on your account.
logrus.Fatal(err) logrus.Fatal(err)
} }
sshURL := fmt.Sprintf(config.SSH_URL_TEMPLATE, "recipes") sshURL := fmt.Sprintf(config.SSH_URL_TEMPLATE, config.CATALOGUE_JSON_REPO_NAME)
if err := gitPkg.CreateRemote(repo, "origin-ssh", sshURL, internal.Dry); err != nil { if err := gitPkg.CreateRemote(repo, "origin-ssh", sshURL, internal.Dry); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -238,7 +232,7 @@ keys configured on your account.
} }
if !internal.Dry && internal.Publish { if !internal.Dry && internal.Publish {
url := fmt.Sprintf("%s/recipes/commit/%s", config.REPOS_BASE_URL, head.Hash()) 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) logrus.Infof("new changes published: %s", url)
} }
@ -252,13 +246,13 @@ keys configured on your account.
} }
// CatalogueCommand defines the `abra catalogue` command and sub-commands. // CatalogueCommand defines the `abra catalogue` command and sub-commands.
var CatalogueCommand = &cli.Command{ var CatalogueCommand = cli.Command{
Name: "catalogue", Name: "catalogue",
Usage: "Manage the recipe catalogue", Usage: "Manage the recipe catalogue",
Aliases: []string{"c"}, Aliases: []string{"c"},
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Description: "This command helps recipe packagers interact with the recipe catalogue", Description: "This command helps recipe packagers interact with the recipe catalogue",
Subcommands: []*cli.Command{ Subcommands: []cli.Command{
catalogueGenerateCommand, catalogueGenerateCommand,
}, },
} }

View File

@ -14,18 +14,18 @@ import (
"coopcloud.tech/abra/cli/recipe" "coopcloud.tech/abra/cli/recipe"
"coopcloud.tech/abra/cli/record" "coopcloud.tech/abra/cli/record"
"coopcloud.tech/abra/cli/server" "coopcloud.tech/abra/cli/server"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/web" "coopcloud.tech/abra/pkg/web"
logrusStack "github.com/Gurpartap/logrus-stack"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// AutoCompleteCommand helps people set up auto-complete in their shells // AutoCompleteCommand helps people set up auto-complete in their shells
var AutoCompleteCommand = &cli.Command{ var AutoCompleteCommand = cli.Command{
Name: "autocomplete", Name: "autocomplete",
Usage: "Configure shell autocompletion (recommended)",
Aliases: []string{"ac"}, Aliases: []string{"ac"},
Usage: "Configure shell autocompletion (recommended)",
Description: ` Description: `
This command helps set up autocompletion in your shell by downloading the This command helps set up autocompletion in your shell by downloading the
relevant autocompletion files and laying out what additional information must relevant autocompletion files and laying out what additional information must
@ -40,9 +40,11 @@ Supported shells are as follows:
fizsh fizsh
zsh zsh
bash bash
`, `,
ArgsUsage: "<shell>", ArgsUsage: "<shell>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
shellType := c.Args().First() shellType := c.Args().First()
@ -88,7 +90,7 @@ Supported shells are as follows:
sudo mkdir /etc/bash_completion.d/ sudo mkdir /etc/bash_completion.d/
sudo cp %s /etc/bash_completion.d/abra sudo cp %s /etc/bash_completion.d/abra
echo "source /etc/bash_completion.d/abra" >> ~/.bashrc echo "source /etc/bash_completion.d/abra" >> ~/.bashrc
# And finally run "abra app ps <hit tab key>" to test things are working, you should see app names listed! # And finally run "abra app ps <hit tab key>" to test things are working, you should see app domains listed!
`, autocompletionFile)) `, autocompletionFile))
case "zsh": case "zsh":
fmt.Println(fmt.Sprintf(` fmt.Println(fmt.Sprintf(`
@ -96,7 +98,7 @@ echo "source /etc/bash_completion.d/abra" >> ~/.bashrc
sudo mkdir /etc/zsh/completion.d/ sudo mkdir /etc/zsh/completion.d/
sudo cp %s /etc/zsh/completion.d/abra sudo cp %s /etc/zsh/completion.d/abra
echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc
# And finally run "abra app ps <hit tab key>" to test things are working, you should see app names listed! # And finally run "abra app ps <hit tab key>" to test things are working, you should see app domains listed!
`, autocompletionFile)) `, autocompletionFile))
} }
@ -105,18 +107,18 @@ echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra
} }
// UpgradeCommand upgrades abra in-place. // UpgradeCommand upgrades abra in-place.
var UpgradeCommand = &cli.Command{ var UpgradeCommand = cli.Command{
Name: "upgrade", Name: "upgrade",
Usage: "Upgrade Abra itself",
Aliases: []string{"u"}, Aliases: []string{"u"},
Usage: "Upgrade Abra itself",
Description: ` Description: `
This command allows you to upgrade Abra in-place with the latest stable or This command allows you to upgrade Abra in-place with the latest stable or
release candidate. release candidate.
If you would like to install the latest release candidate, please pass the If you would like to install the latest release candidate, please pass the
"--rc" option. Please bear in mind that the latest release candidate may have "-r/--rc" option. Please bear in mind that the latest release candidate may
some catastrophic bugs contained in it. In any case, thank you very much for have some catastrophic bugs contained in it. In any case, thank you very much
the testing efforts! for the testing efforts!
`, `,
Flags: []cli.Flag{internal.RCFlag}, Flags: []cli.Flag{internal.RCFlag},
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
@ -150,7 +152,7 @@ func newAbraApp(version, commit string) *cli.App {
|_| |_|
`, `,
Version: fmt.Sprintf("%s-%s", version, commit[:7]), Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Commands: []*cli.Command{ Commands: []cli.Command{
app.AppCommand, app.AppCommand,
server.ServerCommand, server.ServerCommand,
recipe.RecipeCommand, recipe.RecipeCommand,
@ -159,32 +161,12 @@ func newAbraApp(version, commit string) *cli.App {
UpgradeCommand, UpgradeCommand,
AutoCompleteCommand, AutoCompleteCommand,
}, },
Flags: []cli.Flag{ BashComplete: autocomplete.SubcommandComplete,
internal.DebugFlag,
internal.NoInputFlag,
},
Authors: []*cli.Author{
// 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 love
{Name: "3wordchant"},
{Name: "decentral1se"},
{Name: "kawaiipunk"},
{Name: "knoflook"},
{Name: "roxxers"},
},
} }
app.EnableBashCompletion = true app.EnableBashCompletion = true
app.Before = func(c *cli.Context) error { app.Before = func(c *cli.Context) error {
if internal.Debug {
logrus.SetLevel(logrus.DebugLevel)
logrus.SetFormatter(&logrus.TextFormatter{})
logrus.SetOutput(os.Stderr)
logrus.AddHook(logrusStack.StandardHook())
}
paths := []string{ paths := []string{
config.ABRA_DIR, config.ABRA_DIR,
path.Join(config.SERVERS_DIR), path.Join(config.SERVERS_DIR),

View File

@ -1,7 +1,11 @@
package internal package internal
import ( import (
"github.com/urfave/cli/v2" "os"
logrusStack "github.com/Gurpartap/logrus-stack"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
// Secrets stores the variable from SecretsFlag // Secrets stores the variable from SecretsFlag
@ -9,9 +13,7 @@ var Secrets bool
// SecretsFlag turns on/off automatically generating secrets // SecretsFlag turns on/off automatically generating secrets
var SecretsFlag = &cli.BoolFlag{ var SecretsFlag = &cli.BoolFlag{
Name: "secrets", Name: "secrets, S",
Aliases: []string{"ss"},
Value: false,
Usage: "Automatically generate secrets", Usage: "Automatically generate secrets",
Destination: &Secrets, Destination: &Secrets,
} }
@ -21,22 +23,19 @@ var Pass bool
// PassFlag turns on/off storing generated secrets in pass // PassFlag turns on/off storing generated secrets in pass
var PassFlag = &cli.BoolFlag{ var PassFlag = &cli.BoolFlag{
Name: "pass", Name: "pass, p",
Aliases: []string{"p"},
Value: false,
Usage: "Store the generated secrets in a local pass store", Usage: "Store the generated secrets in a local pass store",
Destination: &Pass, Destination: &Pass,
} }
// Context is temp // PassRemove stores the variable for PassRemoveFlag
var Context string var PassRemove bool
// ContextFlag is temp // PassRemoveFlag turns on/off removing generated secrets from pass
var ContextFlag = &cli.StringFlag{ var PassRemoveFlag = &cli.BoolFlag{
Name: "context", Name: "pass, p",
Value: "", Usage: "Remove generated secrets from a local pass store",
Aliases: []string{"c"}, Destination: &PassRemove,
Destination: &Context,
} }
// Force force functionality without asking. // Force force functionality without asking.
@ -44,9 +43,7 @@ var Force bool
// ForceFlag turns on/off force functionality. // ForceFlag turns on/off force functionality.
var ForceFlag = &cli.BoolFlag{ var ForceFlag = &cli.BoolFlag{
Name: "force", Name: "force, f",
Value: false,
Aliases: []string{"f"},
Usage: "Perform action without further prompt. Use with care!", Usage: "Perform action without further prompt. Use with care!",
Destination: &Force, Destination: &Force,
} }
@ -56,9 +53,7 @@ var Chaos bool
// ChaosFlag turns on/off chaos functionality. // ChaosFlag turns on/off chaos functionality.
var ChaosFlag = &cli.BoolFlag{ var ChaosFlag = &cli.BoolFlag{
Name: "chaos", Name: "chaos, C",
Value: false,
Aliases: []string{"ch"},
Usage: "Deploy uncommitted recipes changes. Use with care!", Usage: "Deploy uncommitted recipes changes. Use with care!",
Destination: &Chaos, Destination: &Chaos,
} }
@ -68,18 +63,15 @@ var DNSProvider string
// DNSProviderFlag selects a DNS provider. // DNSProviderFlag selects a DNS provider.
var DNSProviderFlag = &cli.StringFlag{ var DNSProviderFlag = &cli.StringFlag{
Name: "provider", Name: "provider, p",
Value: "", Value: "",
Aliases: []string{"p"},
Usage: "DNS provider", Usage: "DNS provider",
Destination: &DNSProvider, Destination: &DNSProvider,
} }
var NoInput bool var NoInput bool
var NoInputFlag = &cli.BoolFlag{ var NoInputFlag = &cli.BoolFlag{
Name: "no-input", Name: "no-input, n",
Value: false,
Aliases: []string{"n"},
Usage: "Toggle non-interactive mode", Usage: "Toggle non-interactive mode",
Destination: &NoInput, Destination: &NoInput,
} }
@ -87,9 +79,8 @@ var NoInputFlag = &cli.BoolFlag{
var DNSType string var DNSType string
var DNSTypeFlag = &cli.StringFlag{ var DNSTypeFlag = &cli.StringFlag{
Name: "type", Name: "record-type, rt",
Value: "", Value: "",
Aliases: []string{"t"},
Usage: "Domain name record type (e.g. A)", Usage: "Domain name record type (e.g. A)",
Destination: &DNSType, Destination: &DNSType,
} }
@ -97,9 +88,8 @@ var DNSTypeFlag = &cli.StringFlag{
var DNSName string var DNSName string
var DNSNameFlag = &cli.StringFlag{ var DNSNameFlag = &cli.StringFlag{
Name: "name", Name: "record-name, rn",
Value: "", Value: "",
Aliases: []string{"n"},
Usage: "Domain name record name (e.g. mysubdomain)", Usage: "Domain name record name (e.g. mysubdomain)",
Destination: &DNSName, Destination: &DNSName,
} }
@ -107,18 +97,16 @@ var DNSNameFlag = &cli.StringFlag{
var DNSValue string var DNSValue string
var DNSValueFlag = &cli.StringFlag{ var DNSValueFlag = &cli.StringFlag{
Name: "value", Name: "record-value, rv",
Value: "", Value: "",
Aliases: []string{"v"},
Usage: "Domain name record value (e.g. 192.168.1.1)", Usage: "Domain name record value (e.g. 192.168.1.1)",
Destination: &DNSValue, Destination: &DNSValue,
} }
var DNSTTL string var DNSTTL string
var DNSTTLFlag = &cli.StringFlag{ var DNSTTLFlag = &cli.StringFlag{
Name: "ttl", Name: "record-ttl, rl",
Value: "600s", Value: "600s",
Aliases: []string{"T"},
Usage: "Domain name TTL value (seconds)", Usage: "Domain name TTL value (seconds)",
Destination: &DNSTTL, Destination: &DNSTTL,
} }
@ -126,9 +114,8 @@ var DNSTTLFlag = &cli.StringFlag{
var DNSPriority int var DNSPriority int
var DNSPriorityFlag = &cli.IntFlag{ var DNSPriorityFlag = &cli.IntFlag{
Name: "priority", Name: "record-priority, rp",
Value: 10, Value: 10,
Aliases: []string{"P"},
Usage: "Domain name priority value", Usage: "Domain name priority value",
Destination: &DNSPriority, Destination: &DNSPriority,
} }
@ -136,8 +123,7 @@ var DNSPriorityFlag = &cli.IntFlag{
var ServerProvider string var ServerProvider string
var ServerProviderFlag = &cli.StringFlag{ var ServerProviderFlag = &cli.StringFlag{
Name: "provider", Name: "provider, p",
Aliases: []string{"p"},
Usage: "3rd party server provider", Usage: "3rd party server provider",
Destination: &ServerProvider, Destination: &ServerProvider,
} }
@ -145,9 +131,8 @@ var ServerProviderFlag = &cli.StringFlag{
var CapsulInstanceURL string var CapsulInstanceURL string
var CapsulInstanceURLFlag = &cli.StringFlag{ var CapsulInstanceURLFlag = &cli.StringFlag{
Name: "capsul-url", Name: "capsul-url, cu",
Value: "yolo.servers.coop", Value: "yolo.servers.coop",
Aliases: []string{"cu"},
Usage: "capsul instance URL", Usage: "capsul instance URL",
Destination: &CapsulInstanceURL, Destination: &CapsulInstanceURL,
} }
@ -155,9 +140,8 @@ var CapsulInstanceURLFlag = &cli.StringFlag{
var CapsulName string var CapsulName string
var CapsulNameFlag = &cli.StringFlag{ var CapsulNameFlag = &cli.StringFlag{
Name: "capsul-name", Name: "capsul-name, cn",
Value: "", Value: "",
Aliases: []string{"cn"},
Usage: "capsul name", Usage: "capsul name",
Destination: &CapsulName, Destination: &CapsulName,
} }
@ -165,9 +149,8 @@ var CapsulNameFlag = &cli.StringFlag{
var CapsulType string var CapsulType string
var CapsulTypeFlag = &cli.StringFlag{ var CapsulTypeFlag = &cli.StringFlag{
Name: "capsul-type", Name: "capsul-type, ct",
Value: "f1-xs", Value: "f1-xs",
Aliases: []string{"ct"},
Usage: "capsul type", Usage: "capsul type",
Destination: &CapsulType, Destination: &CapsulType,
} }
@ -175,38 +158,33 @@ var CapsulTypeFlag = &cli.StringFlag{
var CapsulImage string var CapsulImage string
var CapsulImageFlag = &cli.StringFlag{ var CapsulImageFlag = &cli.StringFlag{
Name: "capsul-image", Name: "capsul-image, ci",
Value: "debian10", Value: "debian10",
Aliases: []string{"ci"},
Usage: "capsul image", Usage: "capsul image",
Destination: &CapsulImage, Destination: &CapsulImage,
} }
var CapsulSSHKeys cli.StringSlice var CapsulSSHKeys cli.StringSlice
var CapsulSSHKeysFlag = &cli.StringSliceFlag{ var CapsulSSHKeysFlag = &cli.StringSliceFlag{
Name: "capsul-ssh-keys", Name: "capsul-ssh-keys, cs",
Aliases: []string{"cs"}, Usage: "capsul SSH key",
Usage: "capsul SSH key", Value: &CapsulSSHKeys,
Destination: &CapsulSSHKeys,
} }
var CapsulAPIToken string var CapsulAPIToken string
var CapsulAPITokenFlag = &cli.StringFlag{ var CapsulAPITokenFlag = &cli.StringFlag{
Name: "capsul-token", Name: "capsul-token, ca",
Aliases: []string{"ca"},
Usage: "capsul API token", Usage: "capsul API token",
EnvVars: []string{"CAPSUL_TOKEN"}, EnvVar: "CAPSUL_TOKEN",
Destination: &CapsulAPIToken, Destination: &CapsulAPIToken,
} }
var HetznerCloudName string var HetznerCloudName string
var HetznerCloudNameFlag = &cli.StringFlag{ var HetznerCloudNameFlag = &cli.StringFlag{
Name: "hetzner-name", Name: "hetzner-name, hn",
Value: "", Value: "",
Aliases: []string{"hn"},
Usage: "hetzner cloud name", Usage: "hetzner cloud name",
Destination: &HetznerCloudName, Destination: &HetznerCloudName,
} }
@ -214,8 +192,7 @@ var HetznerCloudNameFlag = &cli.StringFlag{
var HetznerCloudType string var HetznerCloudType string
var HetznerCloudTypeFlag = &cli.StringFlag{ var HetznerCloudTypeFlag = &cli.StringFlag{
Name: "hetzner-type", Name: "hetzner-type, ht",
Aliases: []string{"ht"},
Usage: "hetzner cloud type", Usage: "hetzner cloud type",
Destination: &HetznerCloudType, Destination: &HetznerCloudType,
Value: "cx11", Value: "cx11",
@ -224,8 +201,7 @@ var HetznerCloudTypeFlag = &cli.StringFlag{
var HetznerCloudImage string var HetznerCloudImage string
var HetznerCloudImageFlag = &cli.StringFlag{ var HetznerCloudImageFlag = &cli.StringFlag{
Name: "hetzner-image", Name: "hetzner-image, hi",
Aliases: []string{"hi"},
Usage: "hetzner cloud image", Usage: "hetzner cloud image",
Value: "debian-10", Value: "debian-10",
Destination: &HetznerCloudImage, Destination: &HetznerCloudImage,
@ -234,17 +210,15 @@ var HetznerCloudImageFlag = &cli.StringFlag{
var HetznerCloudSSHKeys cli.StringSlice var HetznerCloudSSHKeys cli.StringSlice
var HetznerCloudSSHKeysFlag = &cli.StringSliceFlag{ var HetznerCloudSSHKeysFlag = &cli.StringSliceFlag{
Name: "hetzner-ssh-keys", Name: "hetzner-ssh-keys, hs",
Aliases: []string{"hs"}, Usage: "hetzner cloud SSH keys (e.g. me@foo.com)",
Usage: "hetzner cloud SSH keys (e.g. me@foo.com)", Value: &HetznerCloudSSHKeys,
Destination: &HetznerCloudSSHKeys,
} }
var HetznerCloudLocation string var HetznerCloudLocation string
var HetznerCloudLocationFlag = &cli.StringFlag{ var HetznerCloudLocationFlag = &cli.StringFlag{
Name: "hetzner-location", Name: "hetzner-location, hl",
Aliases: []string{"hl"},
Usage: "hetzner cloud server location", Usage: "hetzner cloud server location",
Value: "hel1", Value: "hel1",
Destination: &HetznerCloudLocation, Destination: &HetznerCloudLocation,
@ -253,10 +227,9 @@ var HetznerCloudLocationFlag = &cli.StringFlag{
var HetznerCloudAPIToken string var HetznerCloudAPIToken string
var HetznerCloudAPITokenFlag = &cli.StringFlag{ var HetznerCloudAPITokenFlag = &cli.StringFlag{
Name: "hetzner-token", Name: "hetzner-token, ha",
Aliases: []string{"ha"},
Usage: "hetzner cloud API token", Usage: "hetzner cloud API token",
EnvVars: []string{"HCLOUD_TOKEN"}, EnvVar: "HCLOUD_TOKEN",
Destination: &HetznerCloudAPIToken, Destination: &HetznerCloudAPIToken,
} }
@ -265,9 +238,7 @@ var Debug bool
// DebugFlag turns on/off verbose logging down to the DEBUG level. // DebugFlag turns on/off verbose logging down to the DEBUG level.
var DebugFlag = &cli.BoolFlag{ var DebugFlag = &cli.BoolFlag{
Name: "debug", Name: "debug, d",
Aliases: []string{"d"},
Value: false,
Destination: &Debug, Destination: &Debug,
Usage: "Show DEBUG messages", Usage: "Show DEBUG messages",
} }
@ -277,61 +248,49 @@ var RC bool
// RCFlag chooses the latest release candidate for install // RCFlag chooses the latest release candidate for install
var RCFlag = &cli.BoolFlag{ var RCFlag = &cli.BoolFlag{
Name: "rc", Name: "rc, r",
Value: false,
Destination: &RC, Destination: &RC,
Usage: "Insatll the latest release candidate", Usage: "Insatll the latest release candidate",
} }
var Major bool var Major bool
var MajorFlag = &cli.BoolFlag{ var MajorFlag = &cli.BoolFlag{
Name: "major", Name: "major, x",
Usage: "Increase the major part of the version", Usage: "Increase the major part of the version",
Value: false,
Aliases: []string{"ma", "x"},
Destination: &Major, Destination: &Major,
} }
var Minor bool var Minor bool
var MinorFlag = &cli.BoolFlag{ var MinorFlag = &cli.BoolFlag{
Name: "minor", Name: "minor, y",
Usage: "Increase the minor part of the version", Usage: "Increase the minor part of the version",
Value: false,
Aliases: []string{"mi", "y"},
Destination: &Minor, Destination: &Minor,
} }
var Patch bool var Patch bool
var PatchFlag = &cli.BoolFlag{ var PatchFlag = &cli.BoolFlag{
Name: "patch", Name: "patch, z",
Usage: "Increase the patch part of the version", Usage: "Increase the patch part of the version",
Value: false,
Aliases: []string{"pa", "z"},
Destination: &Patch, Destination: &Patch,
} }
var Dry bool var Dry bool
var DryFlag = &cli.BoolFlag{ var DryFlag = &cli.BoolFlag{
Name: "dry-run", Name: "dry-run, r",
Usage: "Only reports changes that would be made", Usage: "Only reports changes that would be made",
Value: false,
Aliases: []string{"d"},
Destination: &Dry, Destination: &Dry,
} }
var Publish bool var Publish bool
var PublishFlag = &cli.BoolFlag{ var PublishFlag = &cli.BoolFlag{
Name: "publish", Name: "publish, p",
Usage: "Publish changes to git.coopcloud.tech", Usage: "Publish changes to git.coopcloud.tech",
Value: false,
Aliases: []string{"p"},
Destination: &Publish, Destination: &Publish,
} }
var Domain string var Domain string
var DomainFlag = &cli.StringFlag{ var DomainFlag = &cli.StringFlag{
Name: "domain", Name: "domain, D",
Aliases: []string{"d"},
Value: "", Value: "",
Usage: "Choose a domain name", Usage: "Choose a domain name",
Destination: &Domain, Destination: &Domain,
@ -339,103 +298,59 @@ var DomainFlag = &cli.StringFlag{
var NewAppServer string var NewAppServer string
var NewAppServerFlag = &cli.StringFlag{ var NewAppServerFlag = &cli.StringFlag{
Name: "server", Name: "server, s",
Aliases: []string{"s"},
Value: "", Value: "",
Usage: "Show apps of a specific server", Usage: "Show apps of a specific server",
Destination: &NewAppServer, Destination: &NewAppServer,
} }
var NewAppName string
var NewAppNameFlag = &cli.StringFlag{
Name: "app-name",
Aliases: []string{"a"},
Value: "",
Usage: "Choose an app name",
Destination: &NewAppName,
}
var NoDomainChecks bool var NoDomainChecks bool
var NoDomainChecksFlag = &cli.BoolFlag{ var NoDomainChecksFlag = &cli.BoolFlag{
Name: "no-domain-checks", Name: "no-domain-checks, D",
Aliases: []string{"nd"},
Value: false,
Usage: "Disable app domain sanity checks", Usage: "Disable app domain sanity checks",
Destination: &NoDomainChecks, Destination: &NoDomainChecks,
} }
var StdErrOnly bool var StdErrOnly bool
var StdErrOnlyFlag = &cli.BoolFlag{ var StdErrOnlyFlag = &cli.BoolFlag{
Name: "stderr", Name: "stderr, s",
Aliases: []string{"s"},
Value: false,
Usage: "Only tail stderr", Usage: "Only tail stderr",
Destination: &StdErrOnly, Destination: &StdErrOnly,
} }
var AutoDNSRecord bool
var AutoDNSRecordFlag = &cli.BoolFlag{
Name: "auto",
Aliases: []string{"a"},
Value: false,
Usage: "Automatically configure DNS records",
Destination: &AutoDNSRecord,
}
var DontWaitConverge bool var DontWaitConverge bool
var DontWaitConvergeFlag = &cli.BoolFlag{ var DontWaitConvergeFlag = &cli.BoolFlag{
Name: "no-converge-checks", Name: "no-converge-checks, c",
Aliases: []string{"nc"},
Value: false,
Usage: "Don't wait for converge logic checks", Usage: "Don't wait for converge logic checks",
Destination: &DontWaitConverge, Destination: &DontWaitConverge,
} }
var Watch bool var Watch bool
var WatchFlag = &cli.BoolFlag{ var WatchFlag = &cli.BoolFlag{
Name: "watch", Name: "watch, w",
Aliases: []string{"w"},
Value: false,
Usage: "Watch status by polling repeatedly", Usage: "Watch status by polling repeatedly",
Destination: &Watch, Destination: &Watch,
} }
var OnlyErrors bool var OnlyErrors bool
var OnlyErrorFlag = &cli.BoolFlag{ var OnlyErrorFlag = &cli.BoolFlag{
Name: "errors", Name: "errors, e",
Aliases: []string{"e"},
Value: false,
Usage: "Only show errors", Usage: "Only show errors",
Destination: &OnlyErrors, Destination: &OnlyErrors,
} }
var SkipUpdates bool var SkipUpdates bool
var SkipUpdatesFlag = &cli.BoolFlag{ var SkipUpdatesFlag = &cli.BoolFlag{
Name: "skip-updates", Name: "skip-updates, s",
Aliases: []string{"s"},
Value: false,
Usage: "Skip updating recipe repositories", Usage: "Skip updating recipe repositories",
Destination: &SkipUpdates, Destination: &SkipUpdates,
} }
var RegistryUsername string var AllTags bool
var RegistryUsernameFlag = &cli.StringFlag{ var AllTagsFlag = &cli.BoolFlag{
Name: "username", Name: "all-tags, a",
Aliases: []string{"user"}, Usage: "List all tags, not just upgrades",
Value: "", Destination: &AllTags,
Usage: "Registry username",
EnvVars: []string{"REGISTRY_USERNAME"},
Destination: &RegistryUsername,
}
var RegistryPassword string
var RegistryPasswordFlag = &cli.StringFlag{
Name: "password",
Aliases: []string{"pass"},
Value: "",
Usage: "Registry password",
EnvVars: []string{"REGISTRY_PASSWORD"},
Destination: &RegistryUsername,
} }
// SSHFailMsg is a hopefully helpful SSH failure message // SSHFailMsg is a hopefully helpful SSH failure message
@ -486,3 +401,33 @@ Host foo.coopcloud.tech
Good luck! Good luck!
` `
var ServerAddFailMsg = `
Failed to add server %s.
This could be caused by two things.
Abra isn't picking up your SSH configuration or you need to specify it on the
command-line (e.g you use a non-standard port or username to connect). Run
"server add" with "-d/--debug" to learn more about what Abra is doing under the
hood.
Docker is not installed on your server. You can pass "-p/--provision" to
install Docker and initialise Docker Swarm mode. See help output for "server
add"
See "abra server add -h" for more.
`
// 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

@ -1,6 +1,7 @@
package internal package internal
import ( import (
"context"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@ -17,18 +18,20 @@ import (
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// DeployAction is the main command-line action for this package // DeployAction is the main command-line action for this package
func DeployAction(c *cli.Context) error { func DeployAction(c *cli.Context) error {
app := ValidateApp(c) app := ValidateApp(c)
if err := recipe.EnsureUpToDate(app.Type); err != nil { if !Chaos {
logrus.Fatal(err) if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
} }
r, err := recipe.Get(app.Type) r, err := recipe.Get(app.Recipe)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -44,7 +47,7 @@ func DeployAction(c *cli.Context) error {
logrus.Debugf("checking whether %s is already deployed", app.StackName()) logrus.Debugf("checking whether %s is already deployed", app.StackName())
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, app.StackName()) isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -63,24 +66,24 @@ func DeployAction(c *cli.Context) error {
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
versions, err := recipe.GetRecipeCatalogueVersions(app.Type, catl) versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if len(versions) > 0 { if len(versions) > 0 {
version = versions[len(versions)-1] version = versions[len(versions)-1]
logrus.Debugf("choosing %s as version to deploy", version) logrus.Debugf("choosing %s as version to deploy", version)
if err := recipe.EnsureVersion(app.Type, version); err != nil { if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} else { } else {
head, err := git.GetRecipeHead(app.Type) head, err := git.GetRecipeHead(app.Recipe)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
version = formatter.SmallSHA(head.String()) version = formatter.SmallSHA(head.String())
logrus.Warn("no versions detected, using latest commit") logrus.Warn("no versions detected, using latest commit")
if err := recipe.EnsureLatest(app.Type); err != nil { if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
@ -88,13 +91,13 @@ func DeployAction(c *cli.Context) error {
if version == "unknown" && !Chaos { if version == "unknown" && !Chaos {
logrus.Debugf("choosing %s as version to deploy", version) logrus.Debugf("choosing %s as version to deploy", version)
if err := recipe.EnsureVersion(app.Type, version); err != nil { if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
if version != "unknown" && !Chaos { if version != "unknown" && !Chaos {
if err := recipe.EnsureVersion(app.Type, version); err != nil { if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
@ -102,13 +105,13 @@ func DeployAction(c *cli.Context) error {
if Chaos { if Chaos {
logrus.Warnf("chaos mode engaged") logrus.Warnf("chaos mode engaged")
var err error var err error
version, err = recipe.ChaosVersion(app.Type) version, err = recipe.ChaosVersion(app.Recipe)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Type, "abra.sh") abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath) abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -117,7 +120,7 @@ func DeployAction(c *cli.Context) error {
app.Env[k] = v app.Env[k] = v
} }
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env) composeFiles, err := config.GetAppComposeFiles(app.Recipe, app.Env)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -138,11 +141,6 @@ func DeployAction(c *cli.Context) error {
if !NoDomainChecks { if !NoDomainChecks {
domainName := app.Env["DOMAIN"] domainName := app.Env["DOMAIN"]
ipv4, err := dns.EnsureIPv4(domainName)
if err != nil || ipv4 == "" {
logrus.Fatalf("could not find an IP address assigned to %s?", domainName)
}
if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil { if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -159,7 +157,7 @@ func DeployAction(c *cli.Context) error {
// DeployOverview shows a deployment overview // DeployOverview shows a deployment overview
func DeployOverview(app config.App, version, message string) error { func DeployOverview(app config.App, version, message string) error {
tableCol := []string{"server", "compose", "domain", "app name", "version"} tableCol := []string{"server", "recipe", "config", "domain", "version"}
table := formatter.CreateTable(tableCol) table := formatter.CreateTable(tableCol)
deployConfig := "compose.yml" deployConfig := "compose.yml"
@ -172,7 +170,7 @@ func DeployOverview(app config.App, version, message string) error {
server = "local" server = "local"
} }
table.Append([]string{server, deployConfig, app.Domain, app.Name, version}) table.Append([]string{server, app.Recipe, deployConfig, app.Domain, version})
table.Render() table.Render()
if NoInput { if NoInput {
@ -197,7 +195,7 @@ func DeployOverview(app config.App, version, message string) error {
// NewVersionOverview shows an upgrade or downgrade overview // NewVersionOverview shows an upgrade or downgrade overview
func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes string) error { func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes string) error {
tableCol := []string{"server", "compose", "domain", "app name", "current version", "to be deployed"} tableCol := []string{"server", "recipe", "config", "domain", "current version", "to be deployed"}
table := formatter.CreateTable(tableCol) table := formatter.CreateTable(tableCol)
deployConfig := "compose.yml" deployConfig := "compose.yml"
@ -210,12 +208,12 @@ func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes
server = "local" server = "local"
} }
table.Append([]string{server, deployConfig, app.Domain, app.Name, currentVersion, newVersion}) table.Append([]string{server, app.Recipe, deployConfig, app.Domain, currentVersion, newVersion})
table.Render() table.Render()
if releaseNotes == "" { if releaseNotes == "" {
var err error var err error
releaseNotes, err = GetReleaseNotes(app.Type, newVersion) releaseNotes, err = GetReleaseNotes(app.Recipe, newVersion)
if err != nil { if err != nil {
return err return err
} }

View File

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

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"path" "path"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
@ -11,8 +12,9 @@ import (
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
"coopcloud.tech/abra/pkg/ssh" "coopcloud.tech/abra/pkg/ssh"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/olekukonko/tablewriter"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// AppSecrets represents all app secrest // AppSecrets represents all app secrest
@ -23,7 +25,7 @@ var RecipeName string
// createSecrets creates all secrets for a new app. // createSecrets creates all secrets for a new app.
func createSecrets(sanitisedAppName string) (AppSecrets, error) { func createSecrets(sanitisedAppName string) (AppSecrets, error) {
appEnvPath := path.Join(config.ABRA_DIR, "servers", NewAppServer, fmt.Sprintf("%s.env", sanitisedAppName)) appEnvPath := path.Join(config.ABRA_DIR, "servers", NewAppServer, fmt.Sprintf("%s.env", Domain))
appEnv, err := config.ReadEnv(appEnvPath) appEnv, err := config.ReadEnv(appEnvPath)
if err != nil { if err != nil {
return nil, err return nil, err
@ -38,7 +40,7 @@ func createSecrets(sanitisedAppName string) (AppSecrets, error) {
if Pass { if Pass {
for secretName := range secrets { for secretName := range secrets {
secretValue := secrets[secretName] secretValue := secrets[secretName]
if err := secret.PassInsertSecret(secretValue, secretName, sanitisedAppName, NewAppServer); err != nil { if err := secret.PassInsertSecret(secretValue, secretName, Domain, NewAppServer); err != nil {
return nil, err return nil, err
} }
} }
@ -65,6 +67,31 @@ func ensureDomainFlag(recipe recipe.Recipe, server string) error {
return nil return nil
} }
// promptForSecrets asks if we should generate secrets for a new app.
func promptForSecrets(appName string) error {
app, err := app.Get(appName)
if err != nil {
return err
}
secretEnvVars := secret.ReadSecretEnvVars(app.Env)
if len(secretEnvVars) == 0 {
logrus.Debugf("%s has no secrets to generate, skipping...", app.Recipe)
return nil
}
if !Secrets && !NoInput {
prompt := &survey.Confirm{
Message: "Generate app secrets?",
}
if err := survey.AskOne(prompt, &Secrets); err != nil {
return err
}
}
return nil
}
// ensureServerFlag checks if the server flag was used. if not, asks the user for it. // ensureServerFlag checks if the server flag was used. if not, asks the user for it.
func ensureServerFlag() error { func ensureServerFlag() error {
servers, err := config.GetServers() servers, err := config.GetServers()
@ -89,28 +116,9 @@ func ensureServerFlag() error {
return nil return nil
} }
// ensureServerFlag checks if the AppName flag was used. if not, asks the user for it.
func ensureAppNameFlag() error {
if NewAppName == "" && !NoInput {
prompt := &survey.Input{
Message: "Specify app name:",
Default: Domain,
}
if err := survey.AskOne(prompt, &NewAppName); err != nil {
return err
}
}
if NewAppName == "" {
return fmt.Errorf("no app name provided")
}
return nil
}
// NewAction is the new app creation logic // NewAction is the new app creation logic
func NewAction(c *cli.Context) error { func NewAction(c *cli.Context) error {
recipe := ValidateRecipeWithPrompt(c) recipe := ValidateRecipeWithPrompt(c, false)
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil { if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -124,48 +132,45 @@ func NewAction(c *cli.Context) error {
logrus.Fatal(err) logrus.Fatal(err)
} }
if err := ensureAppNameFlag(); err != nil { if err := promptForSecrets(Domain); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
sanitisedAppName := config.SanitiseAppName(NewAppName) sanitisedAppName := config.SanitiseAppName(Domain)
if len(sanitisedAppName) > 45 { logrus.Debugf("%s sanitised as %s for new app", Domain, sanitisedAppName)
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); err != nil { if err := config.TemplateAppEnvSample(recipe.Name, Domain, NewAppServer, Domain); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
var secrets AppSecrets
var secretTable *tablewriter.Table
if Secrets { if Secrets {
if err := ssh.EnsureHostKey(NewAppServer); err != nil { if err := ssh.EnsureHostKey(NewAppServer); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
secrets, err := createSecrets(sanitisedAppName) var err error
secrets, err = createSecrets(sanitisedAppName)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
secretCols := []string{"Name", "Value"} secretCols := []string{"Name", "Value"}
secretTable := formatter.CreateTable(secretCols) secretTable = formatter.CreateTable(secretCols)
for secret := range secrets { for secret := range secrets {
secretTable.Append([]string{secret, secrets[secret]}) secretTable.Append([]string{secret, secrets[secret]})
} }
if len(secrets) > 0 {
defer secretTable.Render()
}
} }
if NewAppServer == "default" { if NewAppServer == "default" {
NewAppServer = "local" NewAppServer = "local"
} }
tableCol := []string{"Name", "Domain", "Type", "Server"} tableCol := []string{"server", "recipe", "domain"}
table := formatter.CreateTable(tableCol) table := formatter.CreateTable(tableCol)
table.Append([]string{sanitisedAppName, Domain, recipe.Name, NewAppServer}) table.Append([]string{NewAppServer, recipe.Name, Domain})
fmt.Println("") fmt.Println("")
fmt.Println(fmt.Sprintf("A new %s app has been created! Here is an overview:", recipe.Name)) fmt.Println(fmt.Sprintf("A new %s app has been created! Here is an overview:", recipe.Name))
@ -173,11 +178,19 @@ func NewAction(c *cli.Context) error {
table.Render() table.Render()
fmt.Println("") fmt.Println("")
fmt.Println("You can configure this app by running the following:") fmt.Println("You can configure this app by running the following:")
fmt.Println(fmt.Sprintf("\n abra app config %s", sanitisedAppName)) fmt.Println(fmt.Sprintf("\n abra app config %s", Domain))
fmt.Println("") fmt.Println("")
fmt.Println("You can deploy this app by running the following:") fmt.Println("You can deploy this app by running the following:")
fmt.Println(fmt.Sprintf("\n abra app deploy %s", sanitisedAppName)) fmt.Println(fmt.Sprintf("\n abra app deploy %s", Domain))
fmt.Println("") fmt.Println("")
if len(secrets) > 0 {
fmt.Println("Here are your generated secrets:")
fmt.Println("")
secretTable.Render()
fmt.Println("")
logrus.Warn("generated secrets are not shown again, please take note of them *now*")
}
return nil return nil
} }

View File

@ -2,8 +2,8 @@ package internal
import ( import (
"fmt" "fmt"
"strings"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
@ -11,7 +11,7 @@ import (
) )
// PromptBumpType prompts for version bump type // PromptBumpType prompts for version bump type
func PromptBumpType(tagString string) error { func PromptBumpType(tagString, latestRelease string) error {
if (!Major && !Minor && !Patch) && tagString == "" { if (!Major && !Minor && !Patch) && tagString == "" {
fmt.Printf(` fmt.Printf(`
You need to make a decision about what kind of an update this new recipe You need to make a decision about what kind of an update this new recipe
@ -20,6 +20,8 @@ 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 the version you specify on the recipe deploy label and is called a semantic
version. version.
The latest published version is %s.
Here is a semver cheat sheet (more on https://semver.org): 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). major: new features/bug fixes, backwards incompatible (e.g 1.0.0 -> 2.0.0).
@ -34,7 +36,7 @@ Here is a semver cheat sheet (more on https://semver.org):
should also Just Work and is mostly to do with minor bug fixes should also Just Work and is mostly to do with minor bug fixes
and/or security patches. "nothing to worry about". and/or security patches. "nothing to worry about".
`) `, latestRelease)
var chosenBumpType string var chosenBumpType string
prompt := &survey.Select{ prompt := &survey.Select{
@ -94,9 +96,7 @@ func GetMainAppImage(recipe recipe.Recipe) (string, error) {
} }
path = reference.Path(img) path = reference.Path(img)
if strings.Contains(path, "library") { path = formatter.StripTagMeta(path)
path = strings.Split(path, "/")[1]
}
return path, nil return path, nil
} }

View File

@ -12,14 +12,14 @@ import (
"coopcloud.tech/abra/pkg/ssh" "coopcloud.tech/abra/pkg/ssh"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// AppName is used for configuring app name programmatically // AppName is used for configuring app name programmatically
var AppName string var AppName string
// ValidateRecipe ensures the recipe arg is valid. // ValidateRecipe ensures the recipe arg is valid.
func ValidateRecipe(c *cli.Context) recipe.Recipe { func ValidateRecipe(c *cli.Context, ensureLatest bool) recipe.Recipe {
recipeName := c.Args().First() recipeName := c.Args().First()
if recipeName == "" { if recipeName == "" {
@ -38,6 +38,12 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
} }
} }
if ensureLatest {
if err := recipe.EnsureLatest(recipeName); err != nil {
logrus.Fatal(err)
}
}
logrus.Debugf("validated %s as recipe argument", recipeName) logrus.Debugf("validated %s as recipe argument", recipeName)
return chosenRecipe return chosenRecipe
@ -45,7 +51,7 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
// ValidateRecipeWithPrompt ensures a recipe argument is present before // ValidateRecipeWithPrompt ensures a recipe argument is present before
// validating, asking for input if required. // validating, asking for input if required.
func ValidateRecipeWithPrompt(c *cli.Context) recipe.Recipe { func ValidateRecipeWithPrompt(c *cli.Context, ensureLatest bool) recipe.Recipe {
recipeName := c.Args().First() recipeName := c.Args().First()
if recipeName == "" && !NoInput { if recipeName == "" && !NoInput {
@ -99,6 +105,12 @@ func ValidateRecipeWithPrompt(c *cli.Context) recipe.Recipe {
logrus.Fatal(err) logrus.Fatal(err)
} }
if ensureLatest {
if err := recipe.EnsureLatest(recipeName); err != nil {
logrus.Fatal(err)
}
}
logrus.Debugf("validated %s as recipe argument", recipeName) logrus.Debugf("validated %s as recipe argument", recipeName)
return chosenRecipe return chosenRecipe
@ -122,7 +134,7 @@ func ValidateApp(c *cli.Context) config.App {
logrus.Fatal(err) logrus.Fatal(err)
} }
if err := recipe.EnsureExists(app.Type); err != nil { if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -136,7 +148,7 @@ func ValidateApp(c *cli.Context) config.App {
} }
// ValidateDomain ensures the domain name arg is valid. // ValidateDomain ensures the domain name arg is valid.
func ValidateDomain(c *cli.Context) (string, error) { func ValidateDomain(c *cli.Context) string {
domainName := c.Args().First() domainName := c.Args().First()
if domainName == "" && !NoInput { if domainName == "" && !NoInput {
@ -145,7 +157,7 @@ func ValidateDomain(c *cli.Context) (string, error) {
Default: "example.com", Default: "example.com",
} }
if err := survey.AskOne(prompt, &domainName); err != nil { if err := survey.AskOne(prompt, &domainName); err != nil {
return domainName, err logrus.Fatal(err)
} }
} }
@ -155,14 +167,14 @@ func ValidateDomain(c *cli.Context) (string, error) {
logrus.Debugf("validated %s as domain argument", domainName) logrus.Debugf("validated %s as domain argument", domainName)
return domainName, nil return domainName
} }
// ValidateSubCmdFlags ensures flag order conforms to correct order // ValidateSubCmdFlags ensures flag order conforms to correct order
func ValidateSubCmdFlags(c *cli.Context) bool { func ValidateSubCmdFlags(c *cli.Context) bool {
for argIdx, arg := range c.Args().Slice() { for argIdx, arg := range c.Args() {
if !strings.HasPrefix(arg, "--") { if !strings.HasPrefix(arg, "--") {
for _, flag := range c.Args().Slice()[argIdx:] { for _, flag := range c.Args()[argIdx:] {
if strings.HasPrefix(flag, "--") { if strings.HasPrefix(flag, "--") {
return false return false
} }
@ -173,12 +185,12 @@ func ValidateSubCmdFlags(c *cli.Context) bool {
} }
// ValidateServer ensures the server name arg is valid. // ValidateServer ensures the server name arg is valid.
func ValidateServer(c *cli.Context) (string, error) { func ValidateServer(c *cli.Context) string {
serverName := c.Args().First() serverName := c.Args().First()
serverNames, err := config.ReadServerNames() serverNames, err := config.ReadServerNames()
if err != nil { if err != nil {
return serverName, err logrus.Fatal(err)
} }
if serverName == "" && !NoInput { if serverName == "" && !NoInput {
@ -187,17 +199,28 @@ func ValidateServer(c *cli.Context) (string, error) {
Options: serverNames, Options: serverNames,
} }
if err := survey.AskOne(prompt, &serverName); err != nil { if err := survey.AskOne(prompt, &serverName); err != nil {
return serverName, err logrus.Fatal(err)
} }
} }
matched := false
for _, name := range serverNames {
if name == serverName {
matched = true
}
}
if !matched {
ShowSubcommandHelpAndError(c, errors.New("server doesn't exist?"))
}
if serverName == "" { if serverName == "" {
ShowSubcommandHelpAndError(c, errors.New("no server provided")) ShowSubcommandHelpAndError(c, errors.New("no server provided"))
} }
logrus.Debugf("validated %s as server argument", serverName) logrus.Debugf("validated %s as server argument", serverName)
return serverName, nil return serverName
} }
// EnsureDNSProvider ensures a DNS provider is chosen. // EnsureDNSProvider ensures a DNS provider is chosen.
@ -369,7 +392,7 @@ func EnsureNewCapsulVPSFlags(c *cli.Context) error {
if err := survey.AskOne(prompt, &CapsulSSHKeys); err != nil { if err := survey.AskOne(prompt, &CapsulSSHKeys); err != nil {
return err return err
} }
CapsulSSHKeys = *cli.NewStringSlice(strings.Split(sshKeys, ",")...) CapsulSSHKeys = cli.StringSlice(strings.Split(sshKeys, ","))
} }
if CapsulAPIToken == "" && !NoInput { if CapsulAPIToken == "" && !NoInput {
@ -448,7 +471,7 @@ func EnsureNewHetznerCloudVPSFlags(c *cli.Context) error {
if err := survey.AskOne(prompt, &sshKeys); err != nil { if err := survey.AskOne(prompt, &sshKeys); err != nil {
return err return err
} }
HetznerCloudSSHKeys = *cli.NewStringSlice(strings.Split(sshKeys, ",")...) HetznerCloudSSHKeys = cli.StringSlice(strings.Split(sshKeys, ","))
} }
if !NoInput { if !NoInput {

View File

@ -9,18 +9,22 @@ import (
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var recipeLintCommand = &cli.Command{ var recipeLintCommand = cli.Command{
Name: "lint", Name: "lint",
Usage: "Lint a recipe", Usage: "Lint a recipe",
Aliases: []string{"l"}, Aliases: []string{"l"},
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Flags: []cli.Flag{internal.OnlyErrorFlag}, Flags: []cli.Flag{
internal.DebugFlag,
internal.OnlyErrorFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c) recipe := internal.ValidateRecipe(c, true)
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil { if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err) logrus.Fatal(err)

View File

@ -2,42 +2,35 @@ package recipe
import ( import (
"fmt" "fmt"
"path"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var pattern string var pattern string
var patternFlag = &cli.StringFlag{ var patternFlag = &cli.StringFlag{
Name: "pattern", Name: "pattern, p",
Value: "", Value: "",
Aliases: []string{"p"},
Usage: "Simple string to filter recipes", Usage: "Simple string to filter recipes",
Destination: &pattern, Destination: &pattern,
} }
var recipeListCommand = &cli.Command{ var recipeListCommand = cli.Command{
Name: "list", Name: "list",
Usage: "List available recipes", Usage: "List available recipes",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
patternFlag, patternFlag,
}, },
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "recipes")
if err := gitPkg.Clone(catalogueDir, url); err != nil {
return err
}
catl, err := recipe.ReadRecipeCatalogue() catl, err := recipe.ReadRecipeCatalogue()
if err != nil { if err != nil {
logrus.Fatal(err.Error()) logrus.Fatal(err.Error())

View File

@ -13,7 +13,7 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/git"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// recipeMetadata is the recipe metadata for the README.md // recipeMetadata is the recipe metadata for the README.md
@ -30,10 +30,15 @@ type recipeMetadata struct {
SSO string SSO string
} }
var recipeNewCommand = &cli.Command{ var recipeNewCommand = cli.Command{
Name: "new", Name: "new",
Aliases: []string{"n"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
Usage: "Create a new recipe", Usage: "Create a new recipe",
Aliases: []string{"n"},
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Description: ` Description: `
This command creates a new recipe. This command creates a new recipe.

View File

@ -1,15 +1,15 @@
package recipe package recipe
import ( import (
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// RecipeCommand defines all recipe related sub-commands. // RecipeCommand defines all recipe related sub-commands.
var RecipeCommand = &cli.Command{ var RecipeCommand = cli.Command{
Name: "recipe", Name: "recipe",
Aliases: []string{"r"},
Usage: "Manage recipes", Usage: "Manage recipes",
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Aliases: []string{"r"},
Description: ` Description: `
A recipe is a blueprint for an app. It is a bunch of config 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 describe how to deploy and maintain an app. Recipes are maintained by the Co-op
@ -20,7 +20,7 @@ sure the recipe is in good working order and the config upgraded in a timely
manner. Abra supports convenient automation for recipe maintainenace, see the manner. Abra supports convenient automation for recipe maintainenace, see the
"abra recipe upgrade", "abra recipe sync" and "abra recipe release" commands. "abra recipe upgrade", "abra recipe sync" and "abra recipe release" commands.
`, `,
Subcommands: []*cli.Command{ Subcommands: []cli.Command{
recipeListCommand, recipeListCommand,
recipeVersionCommand, recipeVersionCommand,
recipeReleaseCommand, recipeReleaseCommand,

View File

@ -18,13 +18,13 @@ import (
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var recipeReleaseCommand = &cli.Command{ var recipeReleaseCommand = cli.Command{
Name: "release", Name: "release",
Usage: "Release a new recipe version",
Aliases: []string{"rl"}, Aliases: []string{"rl"},
Usage: "Release a new recipe version",
ArgsUsage: "<recipe> [<version>]", ArgsUsage: "<recipe> [<version>]",
Description: ` Description: `
This command is used to specify a new version of a recipe. These versions are This command is used to specify a new version of a recipe. These versions are
@ -48,15 +48,18 @@ requires that you have permission to git push to these repositories and have
your SSH keys configured on your account. your SSH keys configured on your account.
`, `,
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.DryFlag, internal.DryFlag,
internal.MajorFlag, internal.MajorFlag,
internal.MinorFlag, internal.MinorFlag,
internal.PatchFlag, internal.PatchFlag,
internal.PublishFlag, internal.PublishFlag,
}, },
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeWithPrompt(c) recipe := internal.ValidateRecipeWithPrompt(c, false)
imagesTmp, err := getImageVersions(recipe) imagesTmp, err := getImageVersions(recipe)
if err != nil { if err != nil {
@ -127,6 +130,7 @@ your SSH keys configured on your account.
func getImageVersions(recipe recipe.Recipe) (map[string]string, error) { func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
var services = make(map[string]string) var services = make(map[string]string)
missingTag := false
for _, service := range recipe.Config.Services { for _, service := range recipe.Config.Services {
if service.Image == "" { if service.Image == "" {
continue continue
@ -138,21 +142,27 @@ func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
} }
path := reference.Path(img) path := reference.Path(img)
if strings.Contains(path, "library") {
path = strings.Split(path, "/")[1] path = formatter.StripTagMeta(path)
}
var tag string var tag string
switch img.(type) { switch img.(type) {
case reference.NamedTagged: case reference.NamedTagged:
tag = img.(reference.NamedTagged).Tag() tag = img.(reference.NamedTagged).Tag()
case reference.Named: case reference.Named:
return services, fmt.Errorf("%s service is missing image tag?", path) if service.Name == "app" {
missingTag = true
}
continue
} }
services[path] = tag services[path] = tag
} }
if missingTag {
return services, fmt.Errorf("app service is missing image tag?")
}
return services, nil return services, nil
} }
@ -232,12 +242,10 @@ func commitRelease(recipe recipe.Recipe, tag string) error {
} }
} }
if internal.Publish { msg := fmt.Sprintf("chore: publish %s release", tag)
msg := fmt.Sprintf("chore: publish %s release", tag) repoPath := path.Join(config.RECIPES_DIR, recipe.Name)
repoPath := path.Join(config.RECIPES_DIR, recipe.Name) if err := gitPkg.Commit(repoPath, ".", msg, internal.Dry); err != nil {
if err := gitPkg.Commit(repoPath, "compose.**yml", msg, internal.Dry); err != nil { return err
return err
}
} }
return nil return nil
@ -290,13 +298,10 @@ func pushRelease(recipe recipe.Recipe, tagString string) error {
if err := recipe.Push(internal.Dry); err != nil { if err := recipe.Push(internal.Dry); err != nil {
return err return err
} }
url := fmt.Sprintf("%s/%s/src/tag/%s", config.REPOS_BASE_URL, recipe.Name, tagString)
if !internal.Dry { logrus.Infof("new release published: %s", url)
url := fmt.Sprintf("%s/%s/src/tag/%s", config.REPOS_BASE_URL, recipe.Name, tagString) } else {
logrus.Infof("new release published: %s", url) logrus.Info("no -p/--publish passed, not publishing")
} else {
logrus.Info("dry run: no changes published")
}
} }
return nil return nil
@ -317,12 +322,6 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
} }
var lastGitTag tagcmp.Tag var lastGitTag tagcmp.Tag
if tagString == "" {
if err := internal.PromptBumpType(tagString); err != nil {
return err
}
}
for _, tag := range tags { for _, tag := range tags {
parsed, err := tagcmp.Parse(tag) parsed, err := tagcmp.Parse(tag)
if err != nil { if err != nil {
@ -363,6 +362,12 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
newTag.Major = strconv.Itoa(now + 1) 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 { if internal.Major || internal.Minor || internal.Patch {
newTag.Metadata = mainAppVersion newTag.Metadata = mainAppVersion
tagString = newTag.String() tagString = newTag.String()

View File

@ -13,20 +13,23 @@ import (
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var recipeSyncCommand = &cli.Command{ var recipeSyncCommand = cli.Command{
Name: "sync", Name: "sync",
Usage: "Sync recipe version label",
Aliases: []string{"s"}, Aliases: []string{"s"},
Usage: "Sync recipe version label",
ArgsUsage: "<recipe> [<version>]", ArgsUsage: "<recipe> [<version>]",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.DryFlag, internal.DryFlag,
internal.MajorFlag, internal.MajorFlag,
internal.MinorFlag, internal.MinorFlag,
internal.PatchFlag, internal.PatchFlag,
}, },
Before: internal.SubCommandBefore,
Description: ` Description: `
This command will generate labels for the main recipe service (i.e. by This command will generate labels for the main recipe service (i.e. by
convention, the service named 'app') which corresponds to the following format: convention, the service named 'app') which corresponds to the following format:
@ -38,7 +41,7 @@ auto-generate it for you. The <recipe> configuration will be updated on the
local file system. local file system.
`, `,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeWithPrompt(c) recipe := internal.ValidateRecipeWithPrompt(c, false)
mainApp, err := internal.GetMainAppImage(recipe) mainApp, err := internal.GetMainAppImage(recipe)
if err != nil { if err != nil {
@ -92,7 +95,8 @@ likely to change.
} }
if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) { if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
if err := internal.PromptBumpType(""); err != nil { latestRelease := tags[len(tags)-1]
if err := internal.PromptBumpType("", latestRelease); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }

View File

@ -12,12 +12,13 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
type imgPin struct { type imgPin struct {
@ -25,10 +26,10 @@ type imgPin struct {
version tagcmp.Tag version tagcmp.Tag
} }
var recipeUpgradeCommand = &cli.Command{ var recipeUpgradeCommand = cli.Command{
Name: "upgrade", Name: "upgrade",
Usage: "Upgrade recipe image tags",
Aliases: []string{"u"}, Aliases: []string{"u"},
Usage: "Upgrade recipe image tags",
Description: ` Description: `
This command reads and attempts to parse all image tags within the given 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 <recipe> configuration and prompt with more recent tags to upgrade to. It will
@ -45,17 +46,20 @@ interface.
You may invoke this command in "wizard" mode and be prompted for input: You may invoke this command in "wizard" mode and be prompted for input:
abra recipe upgrade abra recipe upgrade
`, `,
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.PatchFlag, internal.PatchFlag,
internal.MinorFlag, internal.MinorFlag,
internal.MajorFlag, internal.MajorFlag,
internal.AllTagsFlag,
}, },
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeWithPrompt(c) recipe := internal.ValidateRecipeWithPrompt(c, true)
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch) bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 { if bumpType != 0 {
@ -108,33 +112,36 @@ You may invoke this command in "wizard" mode and be prompted for input:
logrus.Fatal(err) logrus.Fatal(err)
} }
image := reference.Path(img) regVersions, err := client.GetRegistryTags(img)
regVersions, err := client.GetRegistryTags(image)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("retrieved %s from remote registry for %s", regVersions, image)
if strings.Contains(image, "library") { image := reference.Path(img)
// ParseNormalizedNamed prepends 'library' to images like nginx:<tag>, logrus.Debugf("retrieved %s from remote registry for %s", regVersions, image)
// postgres:<tag>, i.e. images which do not have a username in the image = formatter.StripTagMeta(image)
// first position of the string
image = strings.Split(image, "/")[1] switch img.(type) {
} case reference.NamedTagged:
semverLikeTag := true if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) { logrus.Debugf("%s not considered semver-like", img.(reference.NamedTagged).Tag())
logrus.Debugf("%s not considered semver-like", img.(reference.NamedTagged).Tag()) }
semverLikeTag = false 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()) tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag())
if err != nil && semverLikeTag { if err != nil {
logrus.Fatal(err) 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 var compatible []tagcmp.Tag
for _, regVersion := range regVersions { for _, regVersion := range regVersions {
other, err := tagcmp.Parse(regVersion.Name) other, err := tagcmp.Parse(regVersion)
if err != nil { if err != nil {
continue // skip tags that cannot be parsed continue // skip tags that cannot be parsed
} }
@ -148,8 +155,8 @@ You may invoke this command in "wizard" mode and be prompted for input:
sort.Sort(tagcmp.ByTagDesc(compatible)) sort.Sort(tagcmp.ByTagDesc(compatible))
if len(compatible) == 0 && semverLikeTag { if len(compatible) == 0 && !internal.AllTags {
logrus.Info(fmt.Sprintf("no new versions available for %s, %s is the latest", image, tag)) 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 continue // skip on to the next tag and don't update any compose files
} }
@ -188,13 +195,13 @@ You may invoke this command in "wizard" mode and be prompted for input:
} }
} }
if contains { if contains {
logrus.Infof("Upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString) logrus.Infof("upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString)
} else { } else {
logrus.Infof("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString) logrus.Infof("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString)
continue continue
} }
} else { } 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()) 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 continue
} }
} else { } else {
@ -211,18 +218,20 @@ You may invoke this command in "wizard" mode and be prompted for input:
} }
} }
if upgradeTag == "" { 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) 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 continue
} }
} else { } else {
msg := fmt.Sprintf("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, tag) msg := fmt.Sprintf("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, tag)
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) { if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || internal.AllTags {
tag := img.(reference.NamedTagged).Tag() tag := img.(reference.NamedTagged).Tag()
logrus.Warning(fmt.Sprintf("unable to determine versioning semantics of %s, listing all tags", 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) msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
compatibleStrings = []string{} compatibleStrings = []string{"skip"}
for _, regVersion := range regVersions { for _, regVersion := range regVersions {
compatibleStrings = append(compatibleStrings, regVersion.Name) compatibleStrings = append(compatibleStrings, regVersion)
} }
} }
@ -238,10 +247,13 @@ You may invoke this command in "wizard" mode and be prompted for input:
} }
} }
if upgradeTag != "skip" { if upgradeTag != "skip" {
if err := recipe.UpdateTag(image, upgradeTag); err != nil { ok, err := recipe.UpdateTag(image, upgradeTag)
if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Infof("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image) if ok {
logrus.Infof("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image)
}
} else { } else {
logrus.Warnf("not upgrading %s, skipping as requested", image) logrus.Warnf("not upgrading %s, skipping as requested", image)
} }

View File

@ -1,33 +1,26 @@
package recipe package recipe
import ( import (
"fmt"
"path"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var recipeVersionCommand = &cli.Command{ var recipeVersionCommand = cli.Command{
Name: "versions", Name: "versions",
Usage: "List recipe versions", Aliases: []string{"v"},
Aliases: []string{"v"}, Usage: "List recipe versions",
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c) recipe := internal.ValidateRecipe(c, false)
catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "recipes")
if err := gitPkg.Clone(catalogueDir, url); err != nil {
return err
}
catalogue, err := recipePkg.ReadRecipeCatalogue() catalogue, err := recipePkg.ReadRecipeCatalogue()
if err != nil { if err != nil {

View File

@ -1,6 +1,7 @@
package record package record
import ( import (
"context"
"fmt" "fmt"
"strconv" "strconv"
@ -9,18 +10,20 @@ import (
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"github.com/libdns/gandi" "github.com/libdns/gandi"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// RecordListCommand lists domains. // RecordListCommand lists domains.
var RecordListCommand = &cli.Command{ var RecordListCommand = cli.Command{
Name: "list", Name: "list",
Usage: "List domain name records", Usage: "List domain name records",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
ArgsUsage: "<zone>", ArgsUsage: "<zone>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
internal.DNSProviderFlag, internal.DNSProviderFlag,
}, },
Before: internal.SubCommandBefore,
Description: ` Description: `
This command lists all domain name records managed by a 3rd party provider for This command lists all domain name records managed by a 3rd party provider for
a specific zone. a specific zone.
@ -49,7 +52,7 @@ are listed. This zone must already be created on your provider account.
logrus.Fatalf("%s is not a supported DNS provider", internal.DNSProvider) logrus.Fatalf("%s is not a supported DNS provider", internal.DNSProvider)
} }
records, err := provider.GetRecords(c.Context, zone) records, err := provider.GetRecords(context.Background(), zone)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -1,6 +1,7 @@
package record package record
import ( import (
"context"
"fmt" "fmt"
"strconv" "strconv"
@ -11,24 +12,26 @@ import (
"github.com/libdns/gandi" "github.com/libdns/gandi"
"github.com/libdns/libdns" "github.com/libdns/libdns"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// RecordNewCommand creates a new domain name record. // RecordNewCommand creates a new domain name record.
var RecordNewCommand = &cli.Command{ var RecordNewCommand = cli.Command{
Name: "new", Name: "new",
Usage: "Create a new domain record", Usage: "Create a new domain record",
Aliases: []string{"n"}, Aliases: []string{"n"},
ArgsUsage: "<zone>", ArgsUsage: "<zone>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.DNSProviderFlag, internal.DNSProviderFlag,
internal.DNSTypeFlag, internal.DNSTypeFlag,
internal.DNSNameFlag, internal.DNSNameFlag,
internal.DNSValueFlag, internal.DNSValueFlag,
internal.DNSTTLFlag, internal.DNSTTLFlag,
internal.DNSPriorityFlag, internal.DNSPriorityFlag,
internal.AutoDNSRecordFlag,
}, },
Before: internal.SubCommandBefore,
Description: ` Description: `
This command creates a new domain name record for a specific zone. This command creates a new domain name record for a specific zone.
@ -39,16 +42,9 @@ Example:
abra record new foo.com -p gandi -t A -n myapp -v 192.168.178.44 abra record new foo.com -p gandi -t A -n myapp -v 192.168.178.44
Typically, you need two records, an A record which points at the zone (@.) and You may also invoke this command in "wizard" mode and be prompted for input:
a wildcard record for your apps (*.). Pass "--auto" to have Abra automatically
set this up.
abra record new --auto foo.com -p gandi -v 192.168.178.44
You may also invoke this command in "wizard" mode and be prompted for input
abra record new abra record new
`, `,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
zone, err := internal.EnsureZoneArgument(c) zone, err := internal.EnsureZoneArgument(c)
@ -71,25 +67,6 @@ You may also invoke this command in "wizard" mode and be prompted for input
logrus.Fatalf("%s is not a supported DNS provider", internal.DNSProvider) logrus.Fatalf("%s is not a supported DNS provider", internal.DNSProvider)
} }
if internal.AutoDNSRecord {
ipv4, err := dns.EnsureIPv4(zone)
if err != nil {
logrus.Debugf("no ipv4 associated with %s, prompting for input", zone)
if err := internal.EnsureDNSValueFlag(c); err != nil {
logrus.Fatal(err)
}
ipv4 = internal.DNSValue
}
logrus.Infof("automatically configuring @./*. A records for %s for %s (--auto)", zone, ipv4)
if err := autoConfigure(c, &provider, zone, ipv4); err != nil {
logrus.Fatal(err)
}
return nil
}
if err := internal.EnsureDNSTypeFlag(c); err != nil { if err := internal.EnsureDNSTypeFlag(c); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -118,7 +95,7 @@ You may also invoke this command in "wizard" mode and be prompted for input
record.Priority = internal.DNSPriority record.Priority = internal.DNSPriority
} }
records, err := provider.GetRecords(c.Context, zone) records, err := provider.GetRecords(context.Background(), zone)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -132,7 +109,7 @@ You may also invoke this command in "wizard" mode and be prompted for input
} }
createdRecords, err := provider.SetRecords( createdRecords, err := provider.SetRecords(
c.Context, context.Background(),
zone, zone,
[]libdns.Record{record}, []libdns.Record{record},
) )
@ -169,84 +146,3 @@ You may also invoke this command in "wizard" mode and be prompted for input
return nil return nil
}, },
} }
func autoConfigure(c *cli.Context, provider *gandi.Provider, zone, ipv4 string) error {
ttl, err := dns.GetTTL(internal.DNSTTL)
if err != nil {
return err
}
atRecord := libdns.Record{
Type: "A",
Name: "@",
Value: ipv4,
TTL: ttl,
}
wildcardRecord := libdns.Record{
Type: "A",
Name: "*",
Value: ipv4,
TTL: ttl,
}
records := []libdns.Record{atRecord, wildcardRecord}
tableCol := []string{"type", "name", "value", "TTL", "priority"}
table := formatter.CreateTable(tableCol)
for _, record := range records {
existingRecords, err := provider.GetRecords(c.Context, zone)
if err != nil {
return err
}
discovered := false
for _, existingRecord := range existingRecords {
if existingRecord.Type == record.Type &&
existingRecord.Name == record.Name &&
existingRecord.Value == record.Value {
logrus.Warnf("%s record: %s %s for %s already exists?", record.Type, record.Name, record.Value, zone)
discovered = true
}
}
if discovered {
continue
}
createdRecords, err := provider.SetRecords(
c.Context,
zone,
[]libdns.Record{record},
)
if err != nil {
return err
}
if len(createdRecords) == 0 {
return fmt.Errorf("provider library reports that no record was created?")
}
createdRecord := createdRecords[0]
value := createdRecord.Value
if len(createdRecord.Value) > 30 {
value = fmt.Sprintf("%s...", createdRecord.Value[:30])
}
table.Append([]string{
createdRecord.Type,
createdRecord.Name,
value,
createdRecord.TTL.String(),
strconv.Itoa(createdRecord.Priority),
})
}
if table.NumLines() > 0 {
table.Render()
}
return nil
}

View File

@ -1,11 +1,11 @@
package record package record
import ( import (
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// RecordCommand supports managing DNS entries. // RecordCommand supports managing DNS entries.
var RecordCommand = &cli.Command{ var RecordCommand = cli.Command{
Name: "record", Name: "record",
Usage: "Manage domain name records", Usage: "Manage domain name records",
Aliases: []string{"rc"}, Aliases: []string{"rc"},
@ -28,9 +28,8 @@ library documentation for more. It supports many existing providers and allows
to implement new provider support easily. to implement new provider support easily.
https://pkg.go.dev/github.com/libdns/libdns https://pkg.go.dev/github.com/libdns/libdns
`, `,
Subcommands: []*cli.Command{ Subcommands: []cli.Command{
RecordListCommand, RecordListCommand,
RecordNewCommand, RecordNewCommand,
RecordRemoveCommand, RecordRemoveCommand,

View File

@ -1,6 +1,7 @@
package record package record
import ( import (
"context"
"fmt" "fmt"
"strconv" "strconv"
@ -11,20 +12,23 @@ import (
"github.com/libdns/gandi" "github.com/libdns/gandi"
"github.com/libdns/libdns" "github.com/libdns/libdns"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// RecordRemoveCommand lists domains. // RecordRemoveCommand lists domains.
var RecordRemoveCommand = &cli.Command{ var RecordRemoveCommand = cli.Command{
Name: "remove", Name: "remove",
Usage: "Remove a domain name record", Usage: "Remove a domain name record",
Aliases: []string{"rm"}, Aliases: []string{"rm"},
ArgsUsage: "<zone>", ArgsUsage: "<zone>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.DNSProviderFlag, internal.DNSProviderFlag,
internal.DNSTypeFlag, internal.DNSTypeFlag,
internal.DNSNameFlag, internal.DNSNameFlag,
}, },
Before: internal.SubCommandBefore,
Description: ` Description: `
This command removes a domain name record for a specific zone. This command removes a domain name record for a specific zone.
@ -37,7 +41,7 @@ Example:
abra record remove foo.com -p gandi -t A -n myapp abra record remove foo.com -p gandi -t A -n myapp
You may also invoke this command in "wizard" mode and be prompted for input You may also invoke this command in "wizard" mode and be prompted for input:
abra record rm abra record rm
`, `,
@ -70,7 +74,7 @@ You may also invoke this command in "wizard" mode and be prompted for input
logrus.Fatal(err) logrus.Fatal(err)
} }
records, err := provider.GetRecords(c.Context, zone) records, err := provider.GetRecords(context.Background(), zone)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -120,7 +124,7 @@ You may also invoke this command in "wizard" mode and be prompted for input
} }
} }
_, err = provider.DeleteRecords(c.Context, zone, []libdns.Record{toDelete}) _, err = provider.DeleteRecords(context.Background(), zone, []libdns.Record{toDelete})
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -1,12 +1,12 @@
package server package server
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"os/user" "os/user"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
@ -22,7 +22,7 @@ import (
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var ( var (
@ -41,32 +41,26 @@ such purposes. Docker stable is now installed by default by this script. The
source for this script can be seen here: source for this script can be seen here:
https://github.com/docker/docker-install https://github.com/docker/docker-install
` `
) )
var local bool var local bool
var localFlag = &cli.BoolFlag{ var localFlag = &cli.BoolFlag{
Name: "local", Name: "local, l",
Aliases: []string{"l"},
Value: false,
Usage: "Use local server", Usage: "Use local server",
Destination: &local, Destination: &local,
} }
var provision bool var provision bool
var provisionFlag = &cli.BoolFlag{ var provisionFlag = &cli.BoolFlag{
Name: "provision", Name: "provision, p",
Aliases: []string{"p"},
Value: false,
Usage: "Provision server so it can deploy apps", Usage: "Provision server so it can deploy apps",
Destination: &provision, Destination: &provision,
} }
var sshAuth string var sshAuth string
var sshAuthFlag = &cli.StringFlag{ var sshAuthFlag = &cli.StringFlag{
Name: "ssh-auth", Name: "ssh-auth, s",
Aliases: []string{"sh"},
Value: "identity-file", Value: "identity-file",
Usage: "Select SSH authentication method (identity-file, password)", Usage: "Select SSH authentication method (identity-file, password)",
Destination: &sshAuth, Destination: &sshAuth,
@ -74,22 +68,11 @@ var sshAuthFlag = &cli.StringFlag{
var askSudoPass bool var askSudoPass bool
var askSudoPassFlag = &cli.BoolFlag{ var askSudoPassFlag = &cli.BoolFlag{
Name: "ask-sudo-pass", Name: "ask-sudo-pass, a",
Aliases: []string{"as"},
Value: false,
Usage: "Ask for sudo password", Usage: "Ask for sudo password",
Destination: &askSudoPass, Destination: &askSudoPass,
} }
var traefik bool
var traefikFlag = &cli.BoolFlag{
Name: "traefik",
Aliases: []string{"t"},
Value: false,
Usage: "Deploy traefik",
Destination: &traefik,
}
func cleanUp(domainName string) { func cleanUp(domainName string) {
logrus.Warnf("cleaning up context for %s", domainName) logrus.Warnf("cleaning up context for %s", domainName)
if err := client.DeleteContext(domainName); err != nil { if err := client.DeleteContext(domainName); err != nil {
@ -163,12 +146,6 @@ func newLocalServer(c *cli.Context, domainName string) error {
} }
} }
if traefik {
if err := deployTraefik(c, cl, domainName); err != nil {
return err
}
}
logrus.Info("local server has been added") logrus.Info("local server has been added")
return nil return nil
@ -320,7 +297,7 @@ If nothing works, you try running the Docker install script manually on your ser
func initSwarmLocal(c *cli.Context, cl *dockerClient.Client, domainName string) error { func initSwarmLocal(c *cli.Context, cl *dockerClient.Client, domainName string) error {
initReq := swarm.InitRequest{ListenAddr: "0.0.0.0:2377"} initReq := swarm.InitRequest{ListenAddr: "0.0.0.0:2377"}
if _, err := cl.SwarmInit(c.Context, initReq); err != nil { if _, err := cl.SwarmInit(context.Background(), initReq); err != nil {
if strings.Contains(err.Error(), "is already part of a swarm") || if strings.Contains(err.Error(), "is already part of a swarm") ||
strings.Contains(err.Error(), "must specify a listening address") { strings.Contains(err.Error(), "must specify a listening address") {
logrus.Infof("swarm mode already initialised on %s", domainName) logrus.Infof("swarm mode already initialised on %s", domainName)
@ -332,7 +309,7 @@ func initSwarmLocal(c *cli.Context, cl *dockerClient.Client, domainName string)
} }
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"} netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
if _, err := cl.NetworkCreate(c.Context, "proxy", netOpts); err != nil { if _, err := cl.NetworkCreate(context.Background(), "proxy", netOpts); err != nil {
if !strings.Contains(err.Error(), "proxy already exists") { if !strings.Contains(err.Error(), "proxy already exists") {
return err return err
} }
@ -354,7 +331,7 @@ func initSwarm(c *cli.Context, cl *dockerClient.Client, domainName string) error
ListenAddr: "0.0.0.0:2377", ListenAddr: "0.0.0.0:2377",
AdvertiseAddr: ipv4, AdvertiseAddr: ipv4,
} }
if _, err := cl.SwarmInit(c.Context, initReq); err != nil { if _, err := cl.SwarmInit(context.Background(), initReq); err != nil {
if strings.Contains(err.Error(), "is already part of a swarm") || if strings.Contains(err.Error(), "is already part of a swarm") ||
strings.Contains(err.Error(), "must specify a listening address") { strings.Contains(err.Error(), "must specify a listening address") {
logrus.Infof("swarm mode already initialised on %s", domainName) logrus.Infof("swarm mode already initialised on %s", domainName)
@ -366,7 +343,7 @@ func initSwarm(c *cli.Context, cl *dockerClient.Client, domainName string) error
} }
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"} netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
if _, err := cl.NetworkCreate(c.Context, "proxy", netOpts); err != nil { if _, err := cl.NetworkCreate(context.Background(), "proxy", netOpts); err != nil {
if !strings.Contains(err.Error(), "proxy already exists") { if !strings.Contains(err.Error(), "proxy already exists") {
return err return err
} }
@ -388,70 +365,33 @@ func createServerDir(domainName string) error {
return nil return nil
} }
func deployTraefik(c *cli.Context, cl *dockerClient.Client, domainName string) error { var serverAddCommand = cli.Command{
internal.NoInput = true Name: "add",
Aliases: []string{"a"},
internal.RecipeName = "traefik" Usage: "Add a server to your configuration",
internal.NewAppServer = domainName
internal.Domain = fmt.Sprintf("%s.%s", "traefik", domainName)
internal.NewAppName = fmt.Sprintf("%s_%s", "traefik", config.SanitiseAppName(domainName))
appEnvPath := path.Join(config.ABRA_DIR, "servers", internal.Domain, fmt.Sprintf("%s.env", internal.NewAppName))
if _, err := os.Stat(appEnvPath); os.IsNotExist(err) {
logrus.Info(fmt.Sprintf("-t/--traefik specified, automatically deploying traefik to %s", internal.NewAppServer))
if err := internal.NewAction(c); err != nil {
logrus.Fatal(err)
}
} else {
logrus.Infof("%s already exists, not creating again", appEnvPath)
}
internal.AppName = internal.NewAppName
if err := internal.DeployAction(c); err != nil {
logrus.Fatal(err)
}
return nil
}
var serverAddCommand = &cli.Command{
Name: "add",
Usage: "Add a server to your configuration",
Description: ` Description: `
This command adds a new server to your configuration so that it can be managed This command adds a new server to your configuration so that it can be managed
by Abra. This can be useful when you already have a server provisioned and want by Abra. This command can also provision your server ("--provision/-p") with a
to start running Abra commands against it. Docker installation so that it is capable of hosting Co-op Cloud apps.
This command can also provision your server ("--provision/-p") so that it is Abra will default to expecting that you have a running ssh-agent and are using
capable of hosting Co-op Cloud apps. Abra will default to expecting that you SSH keys to connect to your new server. Abra will also read your SSH config
have a running ssh-agent and are using SSH keys to connect to your new server. (matching "Host" as <domain>). SSH connection details precedence follows as
Abra will also read your SSH config (matching "Host" as <domain>). SSH such: command-line > SSH config > guessed defaults.
connection details precedence follows as such: command-line > SSH config >
guessed defaults.
If you have no SSH key configured for this host and are instead using password If you have no SSH key configured for this host and are instead using password
authentication, you may pass "--ssh-auth password" to have Abra ask you for the authentication, you may pass "--ssh-auth password" to have Abra ask you for the
password. "--ask-sudo-pass" may be passed if you run your provisioning commands password. "--ask-sudo-pass" may be passed if you run your provisioning commands
via sudo privilege escalation. via sudo privilege escalation.
If "--local" is passed, then Abra assumes that the current local server is The <domain> argument must be a publicy accessible domain name which points to
intended as the target server. This is useful when you want to have your entire your server. You should working SSH access to this server already, Abra will
Co-op Cloud config located on the server itself, and not on your local assume port 22 and will use your current system username to make an initial
developer machine. connection. You can use the <user> and <port> arguments to adjust this.
Example: Example:
abra server add --local abra server add varia.zone glodemodem 12345 -p
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.
Example:
abra server add --provision --traefik varia.zone glodemodem 12345
Abra will construct the following SSH connection and Docker context: Abra will construct the following SSH connection and Docker context:
@ -459,25 +399,23 @@ Abra will construct the following SSH connection and Docker context:
All communication between Abra and the server will use this SSH connection. All communication between Abra and the server will use this SSH connection.
In this example, Abra will run the following operations: If "--local" is passed, then Abra assumes that the current local server is
intended as the target server. This is useful when you want to have your entire
1. Install Docker Co-op Cloud config located on the server itself, and not on your local
2. Initialise Swarm mode developer machine.
3. Deploy Traefik (core web proxy)
You may omit flags to avoid performing this provisioning logic.
`, `,
Aliases: []string{"a"},
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
localFlag, localFlag,
provisionFlag, provisionFlag,
sshAuthFlag, sshAuthFlag,
askSudoPassFlag, askSudoPassFlag,
traefikFlag,
}, },
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> [<user>] [<port>]", ArgsUsage: "<domain> [<user>] [<port>]",
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if c.Args().Len() > 0 && local || !internal.ValidateSubCmdFlags(c) { if len(c.Args()) > 0 && local || !internal.ValidateSubCmdFlags(c) {
err := errors.New("cannot use <domain> and --local together") err := errors.New("cannot use <domain> and --local together")
internal.ShowSubcommandHelpAndError(c, err) internal.ShowSubcommandHelpAndError(c, err)
} }
@ -487,6 +425,8 @@ You may omit flags to avoid performing this provisioning logic.
internal.ShowSubcommandHelpAndError(c, err) internal.ShowSubcommandHelpAndError(c, err)
} }
domainName := internal.ValidateDomain(c)
if local { if local {
if err := newLocalServer(c, "default"); err != nil { if err := newLocalServer(c, "default"); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -494,11 +434,6 @@ You may omit flags to avoid performing this provisioning logic.
return nil return nil
} }
domainName, err := internal.ValidateDomain(c)
if err != nil {
logrus.Fatal(err)
}
username := c.Args().Get(1) username := c.Args().Get(1)
if username == "" { if username == "" {
systemUser, err := user.Current() systemUser, err := user.Current()
@ -523,14 +458,17 @@ You may omit flags to avoid performing this provisioning logic.
cl, err := newClient(c, domainName) cl, err := newClient(c, domainName)
if err != nil { if err != nil {
logrus.Fatal(err) cleanUp(domainName)
logrus.Debugf("failed to construct client for %s, saw %s", domainName, err.Error())
logrus.Fatalf(fmt.Sprintf(internal.ServerAddFailMsg, domainName))
} }
if provision { if provision {
logrus.Debugf("attempting to construct SSH client for %s", domainName) logrus.Debugf("attempting to construct SSH client for %s", domainName)
sshCl, err := ssh.New(domainName, sshAuth, username, port) sshCl, err := ssh.New(domainName, sshAuth, username, port)
if err != nil { if err != nil {
logrus.Fatal(err) cleanUp(domainName)
logrus.Fatalf(fmt.Sprintf(internal.ServerAddFailMsg, domainName))
} }
defer sshCl.Close() defer sshCl.Close()
logrus.Debugf("successfully created SSH client for %s", domainName) logrus.Debugf("successfully created SSH client for %s", domainName)
@ -543,15 +481,9 @@ You may omit flags to avoid performing this provisioning logic.
} }
} }
if _, err := cl.Info(c.Context); err != nil { if _, err := cl.Info(context.Background()); err != nil {
cleanUp(domainName) cleanUp(domainName)
logrus.Fatalf("couldn't make a remote docker connection to %s? use --provision/-p to attempt to install", domainName) logrus.Fatalf(fmt.Sprintf(internal.ServerAddFailMsg, domainName))
}
if traefik {
if err := deployTraefik(c, cl, domainName); err != nil {
logrus.Fatal(err)
}
} }
return nil return nil

View File

@ -3,20 +3,23 @@ package server
import ( import (
"strings" "strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/context" "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"github.com/docker/cli/cli/connhelper/ssh" "github.com/docker/cli/cli/connhelper/ssh"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var serverListCommand = &cli.Command{ var serverListCommand = cli.Command{
Name: "list", Name: "list",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Usage: "List managed servers", Usage: "List managed servers",
ArgsUsage: " ", Flags: []cli.Flag{
HideHelp: true, internal.DebugFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
dockerContextStore := context.NewDefaultDockerContextStore() dockerContextStore := context.NewDefaultDockerContextStore()
contexts, err := dockerContextStore.Store.List() contexts, err := dockerContextStore.Store.List()

View File

@ -1,6 +1,7 @@
package server package server
import ( import (
"context"
"fmt" "fmt"
"strings" "strings"
@ -10,7 +11,7 @@ import (
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/hetznercloud/hcloud-go/hcloud" "github.com/hetznercloud/hcloud-go/hcloud"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
func newHetznerCloudVPS(c *cli.Context) error { func newHetznerCloudVPS(c *cli.Context) error {
@ -27,7 +28,7 @@ func newHetznerCloudVPS(c *cli.Context) error {
continue continue
} }
sshKey, _, err := client.SSHKey.GetByName(c.Context, sshKey) sshKey, _, err := client.SSHKey.GetByName(context.Background(), sshKey)
if err != nil { if err != nil {
return err return err
} }
@ -72,7 +73,7 @@ func newHetznerCloudVPS(c *cli.Context) error {
logrus.Fatal("exiting as requested") logrus.Fatal("exiting as requested")
} }
res, _, err := client.Server.Create(c.Context, serverOpts) res, _, err := client.Server.Create(context.Background(), serverOpts)
if err != nil { if err != nil {
return err return err
} }
@ -110,9 +111,6 @@ bar.example.com).
@ 1800 IN A %s @ 1800 IN A %s
* 1800 IN A %s * 1800 IN A %s
"abra record new --auto" can help you do this quickly if you use a supported
DNS provider.
`, `,
internal.HetznerCloudName, ip, rootPassword, internal.HetznerCloudName, ip, rootPassword,
ip, ip, ip, ip, ip, ip,
@ -200,7 +198,7 @@ bar.example.com).
return nil return nil
} }
var serverNewCommand = &cli.Command{ var serverNewCommand = cli.Command{
Name: "new", Name: "new",
Aliases: []string{"n"}, Aliases: []string{"n"},
Usage: "Create a new server using a 3rd party provider", Usage: "Create a new server using a 3rd party provider",
@ -223,6 +221,8 @@ API tokens are read from the environment if specified, e.g.
Where "$provider_TOKEN" is the expected env var format. Where "$provider_TOKEN" is the expected env var format.
`, `,
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ServerProviderFlag, internal.ServerProviderFlag,
// Capsul // Capsul
@ -240,6 +240,7 @@ Where "$provider_TOKEN" is the expected env var format.
internal.HetznerCloudLocationFlag, internal.HetznerCloudLocationFlag,
internal.HetznerCloudAPITokenFlag, internal.HetznerCloudAPITokenFlag,
}, },
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if err := internal.EnsureServerProvider(); err != nil { if err := internal.EnsureServerProvider(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)

View File

@ -1,6 +1,7 @@
package server package server
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -12,14 +13,12 @@ import (
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/hetznercloud/hcloud-go/hcloud" "github.com/hetznercloud/hcloud-go/hcloud"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
var rmServer bool var rmServer bool
var rmServerFlag = &cli.BoolFlag{ var rmServerFlag = &cli.BoolFlag{
Name: "server", Name: "server, s",
Aliases: []string{"s"},
Value: false,
Usage: "remove the actual server also", Usage: "remove the actual server also",
Destination: &rmServer, Destination: &rmServer,
} }
@ -50,7 +49,7 @@ func rmHetznerCloudVPS(c *cli.Context) error {
client := hcloud.NewClient(hcloud.WithToken(internal.HetznerCloudAPIToken)) client := hcloud.NewClient(hcloud.WithToken(internal.HetznerCloudAPIToken))
server, _, err := client.Server.Get(c.Context, internal.HetznerCloudName) server, _, err := client.Server.Get(context.Background(), internal.HetznerCloudName)
if err != nil { if err != nil {
return err return err
} }
@ -89,7 +88,7 @@ destroyed.
logrus.Fatal("exiting as requested") logrus.Fatal("exiting as requested")
} }
_, err = client.Server.Delete(c.Context, server) _, err = client.Server.Delete(context.Background(), server)
if err != nil { if err != nil {
return err return err
} }
@ -99,7 +98,7 @@ destroyed.
return nil return nil
} }
var serverRemoveCommand = &cli.Command{ var serverRemoveCommand = cli.Command{
Name: "remove", Name: "remove",
Aliases: []string{"rm"}, Aliases: []string{"rm"},
ArgsUsage: "[<server>]", ArgsUsage: "[<server>]",
@ -116,6 +115,8 @@ underlying client connection context. This server will then be lost in time,
like tears in rain. like tears in rain.
`, `,
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
rmServerFlag, rmServerFlag,
internal.ServerProviderFlag, internal.ServerProviderFlag,
@ -123,22 +124,26 @@ like tears in rain.
internal.HetznerCloudNameFlag, internal.HetznerCloudNameFlag,
internal.HetznerCloudAPITokenFlag, internal.HetznerCloudAPITokenFlag,
}, },
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
serverName := c.Args().Get(1) serverName := internal.ValidateServer(c)
if serverName != "" {
var err error warnMsg := `Did not pass -s/--server for actual server deletion, prompting!
serverName, err = internal.ValidateServer(c)
if err != nil { Abra doesn't currently know if it helped you create this server with one of the
logrus.Fatal(err) 3rd party integrations (e.g. Capsul). You have a choice here to actually,
} really and finally destroy this server using those integrations. If you want to
} do this, choose Yes.
If you just want to remove the server config files & context, choose No.
`
if !rmServer { if !rmServer {
logrus.Warn("did not pass -s/--server for actual server deletion, prompting") logrus.Warn(fmt.Sprintf(warnMsg))
response := false response := false
prompt := &survey.Confirm{ prompt := &survey.Confirm{
Message: "prompt to actual server deletion?", Message: "delete actual live server?",
} }
if err := survey.AskOne(prompt, &response); err != nil { if err := survey.AskOne(prompt, &response); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -162,21 +167,18 @@ like tears in rain.
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
} }
if serverName != "" { if err := client.DeleteContext(serverName); err != nil {
if err := client.DeleteContext(serverName); err != nil { logrus.Fatal(err)
logrus.Fatal(err)
}
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)
} }
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 return nil
}, },
} }

View File

@ -1,11 +1,11 @@
package server package server
import ( import (
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// ServerCommand defines the `abra server` command and its subcommands // ServerCommand defines the `abra server` command and its subcommands
var ServerCommand = &cli.Command{ var ServerCommand = cli.Command{
Name: "server", Name: "server",
Aliases: []string{"s"}, Aliases: []string{"s"},
Usage: "Manage servers", Usage: "Manage servers",
@ -18,7 +18,7 @@ already have a server, you can add it to your configuration using "abra server
add". Abra can provision servers so that they are ready to deploy Co-op Cloud add". Abra can provision servers so that they are ready to deploy Co-op Cloud
apps, see available flags on "server add" for more. apps, see available flags on "server add" for more.
`, `,
Subcommands: []*cli.Command{ Subcommands: []cli.Command{
serverNewCommand, serverNewCommand,
serverAddCommand, serverAddCommand,
serverListCommand, serverListCommand,

36
go.mod
View File

@ -5,30 +5,31 @@ go 1.16
require ( require (
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52 coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52
github.com/AlecAivazis/survey/v2 v2.3.2 github.com/AlecAivazis/survey/v2 v2.3.2
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4 github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731094149-b031ea1211e7
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
github.com/docker/cli v20.10.12+incompatible github.com/docker/cli v20.10.13+incompatible
github.com/docker/distribution v2.7.1+incompatible github.com/docker/distribution v2.8.1+incompatible
github.com/docker/docker v20.10.12+incompatible github.com/docker/docker v20.10.13+incompatible
github.com/docker/go-units v0.4.0 github.com/docker/go-units v0.4.0
github.com/go-git/go-git/v5 v5.4.2 github.com/go-git/go-git/v5 v5.4.2
github.com/hetznercloud/hcloud-go v1.33.1 github.com/hetznercloud/hcloud-go v1.33.1
github.com/moby/sys/signal v0.6.0 github.com/moby/sys/signal v0.7.0
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
github.com/olekukonko/tablewriter v0.0.5 github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.8.5 github.com/schollz/progressbar/v3 v3.8.6
github.com/schultz-is/passgen v1.0.1 github.com/schultz-is/passgen v1.0.1
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/urfave/cli/v2 v2.3.0 gotest.tools/v3 v3.1.0
gotest.tools/v3 v3.0.3
) )
require ( require (
coopcloud.tech/libcapsul v0.0.0-20211022074848-c35e78fe3f3e coopcloud.tech/libcapsul v0.0.0-20211022074848-c35e78fe3f3e
github.com/Microsoft/hcsshim v0.8.21 // indirect github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 // indirect
github.com/buger/goterm v1.0.3 github.com/buger/goterm v1.0.4
github.com/containerd/containerd v1.5.5 // indirect 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/docker/docker-credential-helpers v0.6.4 // indirect github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/fvbommel/sortorder v1.0.2 // indirect github.com/fvbommel/sortorder v1.0.2 // indirect
@ -40,10 +41,13 @@ require (
github.com/libdns/gandi v1.0.2 github.com/libdns/gandi v1.0.2
github.com/libdns/libdns v0.2.1 github.com/libdns/libdns v0.2.1
github.com/moby/sys/mount v0.2.0 // indirect github.com/moby/sys/mount v0.2.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.3-0.20211202193544-a5463b7f9c84 // indirect
github.com/opencontainers/runc v1.0.2 // indirect github.com/sergi/go-diff v1.2.0 // indirect
github.com/spf13/cobra v1.3.0 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect github.com/theupdateframework/notary v0.7.0 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/urfave/cli v1.22.5
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 github.com/xanzy/ssh-agent v0.3.1 // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e github.com/xeipuuv/gojsonpointer v0.0.0-20190809123943-df4f5c81cb3b // indirect
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27
) )

550
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli"
) )
// AppNameComplete copletes app names // AppNameComplete copletes app names
@ -40,3 +40,24 @@ func RecipeNameComplete(c *cli.Context) {
fmt.Println(name) fmt.Println(name)
} }
} }
// SubcommandComplete completes subcommands.
func SubcommandComplete(c *cli.Context) {
if c.NArg() > 0 {
return
}
subcmds := []string{
"app",
"autocomplete",
"catalogue",
"recipe",
"record",
"server",
"upgrade",
}
for _, cmd := range subcmds {
fmt.Println(cmd)
}
}

View File

@ -27,7 +27,11 @@ func New(contextName string) (*client.Client, error) {
return nil, err return nil, err
} }
helper := commandconnPkg.NewConnectionHelper(ctxEndpoint) helper, err := commandconnPkg.NewConnectionHelper(ctxEndpoint)
if err != nil {
return nil, err
}
httpClient := &http.Client{ httpClient := &http.Client{
// No tls, no proxy // No tls, no proxy
Transport: &http.Transport{ Transport: &http.Transport{

View File

@ -1,193 +1,57 @@
package client package client
import ( import (
"encoding/base64" "context"
"encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http"
"strings" "strings"
"coopcloud.tech/abra/pkg/web" "github.com/containers/image/docker"
"github.com/containers/image/types"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/hashicorp/go-retryablehttp"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
type RawTag struct { // GetRegistryTags retrieves all tags of an image from a container registry.
Layer string func GetRegistryTags(img reference.Named) ([]string, error) {
Name string 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" ctx := context.Background()
tags, err = docker.GetRepositoryTags(ctx, &types.SystemContext{}, ref)
func GetRegistryTags(image string) (RawTags, error) { if err != nil {
var tags RawTags
tagsUrl := fmt.Sprintf(registryURL, image)
if err := web.ReadJSON(tagsUrl, &tags); err != nil {
return tags, err return tags, err
} }
return tags, nil return tags, nil
} }
func basicAuth(username, password string) string { // GetTagDigest retrieves an image digest from a container registry.
auth := username + ":" + password func GetTagDigest(cl *client.Client, image reference.Named) (string, error) {
return base64.StdEncoding.EncodeToString([]byte(auth)) target := fmt.Sprintf("//%s", reference.Path(image))
}
// getRegv2Token retrieves a registry v2 authentication token. ref, err := docker.ParseReference(target)
func getRegv2Token(cl *client.Client, image reference.Named, registryUsername, registryPassword string) (string, error) {
img := reference.Path(image)
tokenURL := "https://auth.docker.io/token"
values := fmt.Sprintf("service=registry.docker.io&scope=repository:%s:pull", img)
fullURL := fmt.Sprintf("%s?%s", tokenURL, values)
req, err := retryablehttp.NewRequest("GET", fullURL, nil)
if err != nil { if err != nil {
return "", err return "", fmt.Errorf("failed to parse image %s, saw: %s", image, err.Error())
} }
if registryUsername != "" && registryPassword != "" { ctx := context.Background()
logrus.Debugf("using registry log in credentials for token request") img, err := ref.NewImage(ctx, nil)
auth := basicAuth(registryUsername, registryPassword)
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", auth))
}
client := web.NewHTTPRetryClient()
res, err := client.Do(req)
if err != nil { if err != nil {
return "", err logrus.Debugf("failed to query remote registry for %s, saw: %s", image, err.Error())
return "", fmt.Errorf("unable to read digest for %s", image)
} }
defer res.Body.Close() defer img.Close()
if res.StatusCode != http.StatusOK { digest := img.ConfigInfo().Digest.String()
_, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", nil
}
tokenRes := struct {
AccessToken string `json:"access_token"`
Expiry int `json:"expires_in"`
Issued string `json:"issued_at"`
Token string `json:"token"`
}{}
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(cl *client.Client, image reference.Named, registryUsername, registryPassword string) (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 := retryablehttp.NewRequest("GET", manifestURL, nil)
if err != nil {
return "", err
}
token, err := getRegv2Token(cl, image, registryUsername, registryPassword)
if err != nil {
return "", err
}
if token == "" {
return "", fmt.Errorf("unable to retrieve registry token?")
}
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 := web.NewHTTPRetryClient()
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 digest == "" {
if err := json.Unmarshal(body, &registryResT2); err != nil { return digest, fmt.Errorf("unable to read digest for %s", image)
return "", err
}
digest = strings.Split(registryResT2.Config.Digest, ":")[1][:7]
} }
if digest == "" { return strings.Split(digest, ":")[1][:7], nil
return "", fmt.Errorf("Unable to retrieve amd64 digest for %s", image)
}
return digest, nil
} }

View File

@ -5,11 +5,9 @@ import (
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
) )
func GetVolumes(ctx context.Context, server string, appName string) ([]*types.Volume, error) { func GetVolumes(ctx context.Context, server string, appName string) ([]*types.Volume, error) {
cl, err := New(server) cl, err := New(server)
if err != nil { if err != nil {
return nil, err return nil, err
@ -21,7 +19,7 @@ func GetVolumes(ctx context.Context, server string, appName string) ([]*types.Vo
volumeListOKBody, err := cl.VolumeList(ctx, fs) volumeListOKBody, err := cl.VolumeList(ctx, fs)
volumeList := volumeListOKBody.Volumes volumeList := volumeListOKBody.Volumes
if err != nil { if err != nil {
logrus.Fatal(err) return volumeList, err
} }
return volumeList, nil return volumeList, nil
@ -29,9 +27,11 @@ func GetVolumes(ctx context.Context, server string, appName string) ([]*types.Vo
func GetVolumeNames(volumes []*types.Volume) []string { func GetVolumeNames(volumes []*types.Volume) []string {
var volumeNames []string var volumeNames []string
for _, vol := range volumes { for _, vol := range volumes {
volumeNames = append(volumeNames, vol.Name) volumeNames = append(volumeNames, vol.Name)
} }
return volumeNames return volumeNames
} }
@ -40,12 +40,13 @@ func RemoveVolumes(ctx context.Context, server string, volumeNames []string, for
if err != nil { if err != nil {
return err return err
} }
for _, volName := range volumeNames { for _, volName := range volumeNames {
err := cl.VolumeRemove(ctx, volName, force) err := cl.VolumeRemove(ctx, volName, force)
if err != nil { if err != nil {
return err return err
} }
} }
return nil
return nil
} }

View File

@ -8,6 +8,7 @@ import (
"strings" "strings"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
loader "coopcloud.tech/abra/pkg/upstream/stack" loader "coopcloud.tech/abra/pkg/upstream/stack"
composetypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types"
@ -16,10 +17,10 @@ import (
) )
// UpdateTag updates an image tag in-place on file system local compose files. // 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) composeFiles, err := filepath.Glob(pattern)
if err != nil { 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, ", "))
@ -30,12 +31,12 @@ func UpdateTag(pattern, image, tag, recipeName string) error {
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample") envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath) sampleEnv, err := config.ReadEnv(envSamplePath)
if err != nil { if err != nil {
return err return false, err
} }
compose, err := loader.LoadComposefile(opts, sampleEnv) compose, err := loader.LoadComposefile(opts, sampleEnv)
if err != nil { if err != nil {
return err return false, err
} }
for _, service := range compose.Services { for _, service := range compose.Services {
@ -45,24 +46,26 @@ func UpdateTag(pattern, image, tag, recipeName string) error {
img, _ := reference.ParseNormalizedNamed(service.Image) img, _ := reference.ParseNormalizedNamed(service.Image)
if err != nil { if err != nil {
return err return false, err
} }
composeImage := reference.Path(img) var composeTag string
if strings.Contains(composeImage, "library") { switch img.(type) {
// ParseNormalizedNamed prepends 'library' to images like nginx:<tag>, case reference.NamedTagged:
// postgres:<tag>, i.e. images which do not have a username in the composeTag = img.(reference.NamedTagged).Tag()
// first position of the string default:
composeImage = strings.Split(composeImage, "/")[1] logrus.Debugf("unable to parse %s, skipping", img)
continue
} }
composeTag := img.(reference.NamedTagged).Tag()
composeImage := formatter.StripTagMeta(reference.Path(img))
logrus.Debugf("parsed %s from %s", composeTag, service.Image) logrus.Debugf("parsed %s from %s", composeTag, service.Image)
if image == composeImage { if image == composeImage {
bytes, err := ioutil.ReadFile(composeFile) bytes, err := ioutil.ReadFile(composeFile)
if err != nil { if err != nil {
return err return false, err
} }
old := fmt.Sprintf("%s:%s", composeImage, composeTag) old := fmt.Sprintf("%s:%s", composeImage, composeTag)
@ -72,13 +75,13 @@ func UpdateTag(pattern, image, tag, recipeName string) error {
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), 0764); err != nil { if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil {
return err return false, err
} }
} }
} }
} }
return nil return false, nil
} }
// UpdateLabel updates a label in-place on file system local compose files. // UpdateLabel updates a label in-place on file system local compose files.

View File

@ -36,7 +36,7 @@ type AppFiles map[AppName]AppFile
// App reprents an app with its env file read into memory // App reprents an app with its env file read into memory
type App struct { type App struct {
Name AppName Name AppName
Type string Recipe string
Domain string Domain string
Env AppEnv Env AppEnv
Server string Server string
@ -52,13 +52,17 @@ func (a App) StackName() string {
} }
stackName := SanitiseAppName(a.Name) stackName := SanitiseAppName(a.Name)
if len(stackName) > 45 {
logrus.Debugf("trimming %s to %s to avoid runtime limits", stackName, stackName[:45])
stackName = stackName[:45]
}
a.Env["STACK_NAME"] = stackName a.Env["STACK_NAME"] = stackName
return stackName return stackName
} }
// SORTING TYPES
// ByServer sort a slice of Apps // ByServer sort a slice of Apps
type ByServer []App type ByServer []App
@ -68,25 +72,25 @@ func (a ByServer) Less(i, j int) bool {
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server) return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
} }
// ByServerAndType sort a slice of Apps // ByServerAndRecipe sort a slice of Apps
type ByServerAndType []App type ByServerAndRecipe []App
func (a ByServerAndType) Len() int { return len(a) } func (a ByServerAndRecipe) Len() int { return len(a) }
func (a ByServerAndType) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByServerAndRecipe) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByServerAndType) Less(i, j int) bool { func (a ByServerAndRecipe) Less(i, j int) bool {
if a[i].Server == a[j].Server { 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) return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
} }
// ByType sort a slice of Apps // ByRecipe sort a slice of Apps
type ByType []App type ByRecipe []App
func (a ByType) Len() int { return len(a) } func (a ByRecipe) Len() int { return len(a) }
func (a ByType) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByRecipe) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByType) Less(i, j int) bool { func (a ByRecipe) Less(i, j int) bool {
return strings.ToLower(a[i].Type) < strings.ToLower(a[j].Type) return strings.ToLower(a[i].Recipe) < strings.ToLower(a[j].Recipe)
} }
// ByName sort a slice of Apps // ByName sort a slice of Apps
@ -118,15 +122,18 @@ func readAppEnvFile(appFile AppFile, name AppName) (App, error) {
func newApp(env AppEnv, name string, appFile AppFile) (App, error) { func newApp(env AppEnv, name string, appFile AppFile) (App, error) {
domain := env["DOMAIN"] domain := env["DOMAIN"]
appType, exists := env["TYPE"] recipe, exists := env["RECIPE"]
if !exists { if !exists {
return App{}, fmt.Errorf("%s is missing the TYPE env var", name) recipe, exists = env["TYPE"]
if !exists {
return App{}, fmt.Errorf("%s is missing the RECIPE env var", name)
}
} }
return App{ return App{
Name: name, Name: name,
Domain: domain, Domain: domain,
Type: appType, Recipe: recipe,
Env: env, Env: env,
Server: appFile.Server, Server: appFile.Server,
Path: appFile.Path, Path: appFile.Path,
@ -153,7 +160,7 @@ func LoadAppFiles(servers ...string) (AppFiles, error) {
serverDir := path.Join(SERVERS_DIR, server) serverDir := path.Join(SERVERS_DIR, server)
files, err := getAllFilesInDirectory(serverDir) files, err := getAllFilesInDirectory(serverDir)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("server %s doesn't exist? Run \"abra server ls\" to check", server)
} }
for _, file := range files { for _, file := range files {
appName := strings.TrimSuffix(file.Name(), ".env") appName := strings.TrimSuffix(file.Name(), ".env")
@ -213,13 +220,13 @@ func GetAppServiceNames(appName string) ([]string, error) {
return serviceNames, err return serviceNames, err
} }
composeFiles, err := GetAppComposeFiles(app.Type, app.Env) composeFiles, err := GetAppComposeFiles(app.Recipe, app.Env)
if err != nil { if err != nil {
return serviceNames, err return serviceNames, err
} }
opts := stack.Deploy{Composefiles: composeFiles} opts := stack.Deploy{Composefiles: composeFiles}
compose, err := GetAppComposeConfig(app.Type, opts, app.Env) compose, err := GetAppComposeConfig(app.Recipe, opts, app.Env)
if err != nil { if err != nil {
return serviceNames, err return serviceNames, err
} }

View File

@ -16,10 +16,11 @@ import (
var ABRA_DIR = os.ExpandEnv("$HOME/.abra") var ABRA_DIR = os.ExpandEnv("$HOME/.abra")
var SERVERS_DIR = path.Join(ABRA_DIR, "servers") var SERVERS_DIR = path.Join(ABRA_DIR, "servers")
var RECIPES_DIR = path.Join(ABRA_DIR, "apps") var RECIPES_DIR = path.Join(ABRA_DIR, "recipes")
var VENDOR_DIR = path.Join(ABRA_DIR, "vendor") var VENDOR_DIR = path.Join(ABRA_DIR, "vendor")
var RECIPES_JSON = path.Join(ABRA_DIR, "catalogue", "recipes.json") var RECIPES_JSON = path.Join(ABRA_DIR, "catalogue", "recipes.json")
var REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud" 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" var SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git"
// GetServers retrieves all servers. // GetServers retrieves all servers.

View File

@ -20,12 +20,12 @@ var serverName = "evil.corp"
var expectedAppEnv = AppEnv{ var expectedAppEnv = AppEnv{
"DOMAIN": "ecloud.evil.corp", "DOMAIN": "ecloud.evil.corp",
"TYPE": "ecloud", "RECIPE": "ecloud",
} }
var expectedApp = App{ var expectedApp = App{
Name: appName, Name: appName,
Type: expectedAppEnv["TYPE"], Recipe: expectedAppEnv["RECIPE"],
Domain: expectedAppEnv["DOMAIN"], Domain: expectedAppEnv["DOMAIN"],
Env: expectedAppEnv, Env: expectedAppEnv,
Path: expectedAppFile.Path, Path: expectedAppFile.Path,
@ -74,11 +74,11 @@ func TestReadEnv(t *testing.T) {
} }
if !reflect.DeepEqual(env, expectedAppEnv) { if !reflect.DeepEqual(env, expectedAppEnv) {
t.Fatalf( t.Fatalf(
"did not get expected application settings. Expected: DOMAIN=%s TYPE=%s; Got: DOMAIN=%s TYPE=%s", "did not get expected application settings. Expected: DOMAIN=%s RECIPE=%s; Got: DOMAIN=%s RECIPE=%s",
expectedAppEnv["DOMAIN"], expectedAppEnv["DOMAIN"],
expectedAppEnv["TYPE"], expectedAppEnv["RECIPE"],
env["DOMAIN"], env["DOMAIN"],
env["TYPE"], env["RECIPE"],
) )
} }
} }

View File

@ -13,10 +13,10 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// GetContainer retrieves a container. If prompt is true and the retrievd count // GetContainer retrieves a container. If noInput is false and the retrievd
// of containers does not match 1, then a prompt is presented to let the user // count of containers does not match 1, then a prompt is presented to let the
// choose. A count of 0 is handled gracefully. // user choose. A count of 0 is handled gracefully.
func GetContainer(c context.Context, cl *client.Client, filters filters.Args, prompt bool) (types.Container, error) { func GetContainer(c context.Context, cl *client.Client, filters filters.Args, noInput bool) (types.Container, error) {
containerOpts := types.ContainerListOptions{Filters: filters} containerOpts := types.ContainerListOptions{Filters: filters}
containers, err := cl.ContainerList(c, containerOpts) containers, err := cl.ContainerList(c, containerOpts)
if err != nil { if err != nil {
@ -37,7 +37,7 @@ func GetContainer(c context.Context, cl *client.Client, filters filters.Args, pr
containersRaw = append(containersRaw, fmt.Sprintf("%s (created %v)", trimmed, created)) containersRaw = append(containersRaw, fmt.Sprintf("%s (created %v)", trimmed, created))
} }
if !prompt { if noInput {
err := fmt.Errorf("expected 1 container but found %v: %s", len(containers), strings.Join(containersRaw, " ")) err := fmt.Errorf("expected 1 container but found %v: %s", len(containers), strings.Join(containersRaw, " "))
return types.Container{}, err return types.Container{}, err
} }

View File

@ -47,8 +47,6 @@ func EnsureIPv4(domainName string) (string, error) {
}, },
} }
logrus.Debugf("created DNS resolver via %s", freifunkDNS)
ctx := context.Background() ctx := context.Background()
ips, err := resolver.LookupIPAddr(ctx, domainName) ips, err := resolver.LookupIPAddr(ctx, domainName)
if err != nil { if err != nil {
@ -60,7 +58,7 @@ func EnsureIPv4(domainName string) (string, error) {
} }
ipv4 = ips[0].IP.To4().String() ipv4 = ips[0].IP.To4().String()
logrus.Debugf("discovered the following ipv4 addr: %s", ipv4) logrus.Debugf("%s points to %s (resolver: %s)", domainName, ipv4, freifunkDNS)
return ipv4, nil return ipv4, nil
} }

View File

@ -8,6 +8,7 @@ import (
"github.com/docker/go-units" "github.com/docker/go-units"
"github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter"
"github.com/schollz/progressbar/v3" "github.com/schollz/progressbar/v3"
"github.com/sirupsen/logrus"
) )
func ShortenID(str string) string { func ShortenID(str string) string {
@ -49,3 +50,22 @@ func CreateProgressbar(length int, title string) *progressbar.ProgressBar {
progressbar.OptionSetDescription(title), progressbar.OptionSetDescription(title),
) )
} }
// StripTagMeta strips front-matter image tag data that we don't need for parsing.
func StripTagMeta(image string) string {
originalImage := image
if strings.Contains(image, "docker.io") {
image = strings.Split(image, "/")[1]
}
if strings.Contains(image, "library") {
image = strings.Split(image, "/")[1]
}
if originalImage != image {
logrus.Debugf("stripped %s to %s for parsing", originalImage, image)
}
return image
}

View File

@ -78,6 +78,13 @@ var LintRules = map[string][]LintRule{
HowToResolve: "fill out all the metadata", HowToResolve: "fill out all the metadata",
Function: LintMetadataFilledIn, Function: LintMetadataFilledIn,
}, },
{
Ref: "R013",
Level: "warn",
Description: "git.coopcloud.tech repo exists",
HowToResolve: "upload your recipe to git.coopcloud.tech/coop-cloud/...",
Function: LintHasRecipeRepo,
},
}, },
"error": { "error": {
{ {
@ -115,13 +122,6 @@ var LintRules = map[string][]LintRule{
HowToResolve: "vendor config versions in an abra.sh", HowToResolve: "vendor config versions in an abra.sh",
Function: LintAbraShVendors, Function: LintAbraShVendors,
}, },
{
Ref: "R013",
Level: "error",
Description: "git.coopcloud.tech repo exists",
HowToResolve: "upload your recipe to git.coopcloud.tech/coop-cloud/...",
Function: LintHasRecipeRepo,
},
}, },
} }

View File

@ -26,7 +26,7 @@ import (
) )
// RecipeCatalogueURL is the only current recipe catalogue available. // RecipeCatalogueURL is the only current recipe catalogue available.
const RecipeCatalogueURL = "https://apps.coopcloud.tech" const RecipeCatalogueURL = "https://recipes.coopcloud.tech"
// ReposMetadataURL is the recipe repository metadata // ReposMetadataURL is the recipe repository metadata
const ReposMetadataURL = "https://git.coopcloud.tech/api/v1/orgs/coop-cloud/repos" const ReposMetadataURL = "https://git.coopcloud.tech/api/v1/orgs/coop-cloud/repos"
@ -163,12 +163,17 @@ func (r Recipe) UpdateLabel(pattern, serviceName, label string) error {
} }
// UpdateTag updates a recipe tag // UpdateTag updates a recipe tag
func (r Recipe) UpdateTag(image, tag string) error { func (r Recipe) UpdateTag(image, tag string) (bool, error) {
pattern := fmt.Sprintf("%s/%s/compose**yml", config.RECIPES_DIR, r.Name) pattern := fmt.Sprintf("%s/%s/compose**yml", config.RECIPES_DIR, r.Name)
if err := compose.UpdateTag(pattern, image, tag, r.Name); err != nil {
return err image = formatter.StripTagMeta(image)
ok, err := compose.UpdateTag(pattern, image, tag, r.Name)
if err != nil {
return false, err
} }
return nil
return ok, nil
} }
// Tags list the recipe tags // Tags list the recipe tags
@ -227,7 +232,11 @@ func Get(recipeName string) (Recipe, error) {
meta, err := GetRecipeMeta(recipeName) meta, err := GetRecipeMeta(recipeName)
if err != nil { if err != nil {
return Recipe{}, err if strings.Contains(err.Error(), "does not exist") {
meta = RecipeMeta{}
} else {
return Recipe{}, err
}
} }
return Recipe{ return Recipe{
@ -350,7 +359,7 @@ func EnsureLatest(recipeName string) error {
return err return err
} }
branch, err := gitPkg.GetCurrentBranch(repo) branch, err := GetDefaultBranch(repo, recipeName)
if err != nil { if err != nil {
return err return err
} }
@ -610,11 +619,15 @@ func EnsureUpToDate(recipeName string) error {
func GetDefaultBranch(repo *git.Repository, recipeName string) (plumbing.ReferenceName, error) { func GetDefaultBranch(repo *git.Repository, recipeName string) (plumbing.ReferenceName, error) {
recipeDir := path.Join(config.RECIPES_DIR, recipeName) recipeDir := path.Join(config.RECIPES_DIR, recipeName)
meta, _ := GetRecipeMeta(recipeName)
if meta.DefaultBranch != "" {
return plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", meta.DefaultBranch)), nil
}
branch := "master" branch := "master"
if _, err := repo.Branch("master"); err != nil { if _, err := repo.Branch("master"); err != nil {
if _, err := repo.Branch("main"); err != nil { if _, err := repo.Branch("main"); err != nil {
logrus.Debugf("failed to select branch in %s", recipeDir) return "", fmt.Errorf("failed to select default branch in %s", recipeDir)
return "", err
} }
branch = "main" branch = "main"
} }
@ -684,7 +697,7 @@ func recipeCatalogueFSIsLatest() (bool, error) {
return false, nil return false, nil
} }
logrus.Debug("file system cached recipe catalogue is now up-to-date") logrus.Debug("file system cached recipe catalogue is up-to-date")
return true, nil return true, nil
} }
@ -693,20 +706,22 @@ func recipeCatalogueFSIsLatest() (bool, error) {
func ReadRecipeCatalogue() (RecipeCatalogue, error) { func ReadRecipeCatalogue() (RecipeCatalogue, error) {
recipes := make(RecipeCatalogue) recipes := make(RecipeCatalogue)
if err := EnsureCatalogue(); err != nil {
return nil, err
}
recipeFSIsLatest, err := recipeCatalogueFSIsLatest() recipeFSIsLatest, err := recipeCatalogueFSIsLatest()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !recipeFSIsLatest { if !recipeFSIsLatest {
logrus.Debugf("reading recipe catalogue from web to get latest")
if err := readRecipeCatalogueWeb(&recipes); err != nil { if err := readRecipeCatalogueWeb(&recipes); err != nil {
return nil, err return nil, err
} }
return recipes, nil return recipes, nil
} }
logrus.Debugf("reading recipe catalogue from file system cache to get latest")
if err := readRecipeCatalogueFS(&recipes); err != nil { if err := readRecipeCatalogueFS(&recipes); err != nil {
return nil, err return nil, err
} }
@ -788,8 +803,7 @@ func GetRecipeMeta(recipeName string) (RecipeMeta, error) {
recipeMeta, ok := catl[recipeName] recipeMeta, ok := catl[recipeName]
if !ok { if !ok {
err := fmt.Errorf("recipe %s does not exist?", recipeName) return RecipeMeta{}, fmt.Errorf("recipe %s does not exist?", recipeName)
return RecipeMeta{}, err
} }
if err := EnsureExists(recipeName); err != nil { if err := EnsureExists(recipeName); err != nil {
@ -914,7 +928,7 @@ func ReadReposMetadata() (RepoCatalogue, error) {
} }
// GetRecipeVersions retrieves all recipe versions. // GetRecipeVersions retrieves all recipe versions.
func GetRecipeVersions(recipeName, registryUsername, registryPassword string) (RecipeVersions, error) { func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
versions := RecipeVersions{} versions := RecipeVersions{}
recipeDir := path.Join(config.RECIPES_DIR, recipeName) recipeDir := path.Join(config.RECIPES_DIR, recipeName)
@ -928,7 +942,7 @@ func GetRecipeVersions(recipeName, registryUsername, registryPassword string) (R
worktree, err := repo.Worktree() worktree, err := repo.Worktree()
if err != nil { if err != nil {
logrus.Fatal(err) return versions, err
} }
gitTags, err := repo.Tags() gitTags, err := repo.Tags()
@ -958,9 +972,9 @@ func GetRecipeVersions(recipeName, registryUsername, registryPassword string) (R
return err return err
} }
cl, err := client.New("default") // only required for docker.io registry calls cl, err := client.New("default") // only required for container registry calls
if err != nil { if err != nil {
logrus.Fatal(err) return err
} }
queryCache := make(map[reference.Named]string) queryCache := make(map[reference.Named]string)
@ -973,9 +987,8 @@ func GetRecipeVersions(recipeName, registryUsername, registryPassword string) (R
} }
path := reference.Path(img) path := reference.Path(img)
if strings.Contains(path, "library") {
path = strings.Split(path, "/")[1] path = formatter.StripTagMeta(path)
}
var tag string var tag string
switch img.(type) { switch img.(type) {
@ -989,18 +1002,19 @@ func GetRecipeVersions(recipeName, registryUsername, registryPassword string) (R
var exists bool var exists bool
var digest string var digest string
if digest, exists = queryCache[img]; !exists { if digest, exists = queryCache[img]; !exists {
logrus.Debugf("looking up image: %s from %s", img, path) logrus.Debugf("cache miss: querying for image: %s, tag: %s", path, tag)
var err error var err error
digest, err = client.GetTagDigest(cl, img, registryUsername, registryPassword) digest, err = client.GetTagDigest(cl, img)
if err != nil { if err != nil {
logrus.Warn(err) logrus.Warn(err)
continue digest = "unknown"
} }
logrus.Debugf("queried for image: %s, tag: %s, digest: %s", path, tag, digest)
queryCache[img] = digest queryCache[img] = digest
logrus.Debugf("cached image: %s, tag: %s, digest: %s", path, tag, digest) logrus.Debugf("cached insert: %s, tag: %s, digest: %s", path, tag, digest)
} else { } else {
logrus.Debugf("reading image: %s, tag: %s, digest: %s from cache", path, tag, digest) logrus.Debugf("cache hit: image: %s, tag: %s, digest: %s", path, tag, digest)
} }
versionMeta[service.Name] = ServiceMeta{ versionMeta[service.Name] = ServiceMeta{
@ -1041,3 +1055,18 @@ func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]stri
return versions, nil return versions, 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) {
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
}
logrus.Debugf("cloned catalogue repository to %s", catalogueDir)
}
return nil
}

View File

@ -8,6 +8,7 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"sync"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
@ -119,23 +120,32 @@ func ParseSecretEnvVarValue(secret string) (secretValue, error) {
func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (map[string]string, error) { func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (map[string]string, error) {
secrets := make(map[string]string) secrets := make(map[string]string)
var mutex sync.Mutex
var wg sync.WaitGroup
ch := make(chan error, len(secretEnvVars)) ch := make(chan error, len(secretEnvVars))
for secretEnvVar := range secretEnvVars { for secretEnvVar := range secretEnvVars {
wg.Add(1)
go func(s string) { go func(s string) {
defer wg.Done()
secretName := ParseSecretEnvVarName(s) secretName := ParseSecretEnvVarName(s)
secretValue, err := ParseSecretEnvVarValue(secretEnvVars[s]) secretValue, err := ParseSecretEnvVarValue(secretEnvVars[s])
if err != nil { if err != nil {
ch <- err ch <- err
return return
} }
secretRemoteName := fmt.Sprintf("%s_%s_%s", appName, secretName, secretValue.Version) secretRemoteName := fmt.Sprintf("%s_%s_%s", appName, secretName, secretValue.Version)
logrus.Debugf("attempting to generate and store %s on %s", secretRemoteName, server) logrus.Debugf("attempting to generate and store %s on %s", secretRemoteName, server)
if secretValue.Length > 0 { if secretValue.Length > 0 {
passwords, err := GeneratePasswords(1, uint(secretValue.Length)) passwords, err := GeneratePasswords(1, uint(secretValue.Length))
if err != nil { if err != nil {
ch <- err ch <- err
return return
} }
if err := client.StoreSecret(secretRemoteName, passwords[0], server); err != nil { if err := client.StoreSecret(secretRemoteName, passwords[0], server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") { if strings.Contains(err.Error(), "AlreadyExists") {
logrus.Warnf("%s already exists, moving on...", secretRemoteName) logrus.Warnf("%s already exists, moving on...", secretRemoteName)
@ -145,6 +155,9 @@ func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (m
} }
return return
} }
mutex.Lock()
defer mutex.Unlock()
secrets[secretName] = passwords[0] secrets[secretName] = passwords[0]
} else { } else {
passphrases, err := GeneratePassphrases(1) passphrases, err := GeneratePassphrases(1)
@ -152,6 +165,7 @@ func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (m
ch <- err ch <- err
return return
} }
if err := client.StoreSecret(secretRemoteName, passphrases[0], server); err != nil { if err := client.StoreSecret(secretRemoteName, passphrases[0], server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") { if strings.Contains(err.Error(), "AlreadyExists") {
logrus.Warnf("%s already exists, moving on...", secretRemoteName) logrus.Warnf("%s already exists, moving on...", secretRemoteName)
@ -161,12 +175,17 @@ func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (m
} }
return return
} }
mutex.Lock()
defer mutex.Unlock()
secrets[secretName] = passphrases[0] secrets[secretName] = passphrases[0]
} }
ch <- nil ch <- nil
}(secretEnvVar) }(secretEnvVar)
} }
wg.Wait()
for range secretEnvVars { for range secretEnvVars {
err := <-ch err := <-ch
if err != nil { if err != nil {

View File

@ -325,7 +325,7 @@ func connect(username, host, port string, authMethod ssh.AuthMethod, timeout tim
conn, err = net.DialTimeout("tcp", hostnameAndPort, timeout) conn, err = net.DialTimeout("tcp", hostnameAndPort, timeout)
if err != nil { if err != nil {
logrus.Debugf("tcp dialing %s failed, trying via ~/.ssh/config", hostnameAndPort) logrus.Debugf("tcp dialing %s failed, trying via ~/.ssh/config", hostnameAndPort)
hostConfig, err := GetHostConfig(host, username, port) hostConfig, err := GetHostConfig(host, username, port, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -452,7 +452,7 @@ func GetContextConnDetails(serverName string) (*dockerSSHPkg.Spec, error) {
} }
} }
hostConfig, err := GetHostConfig(serverName, "", "") hostConfig, err := GetHostConfig(serverName, "", "", false)
if err != nil { if err != nil {
return &dockerSSHPkg.Spec{}, err return &dockerSSHPkg.Spec{}, err
} }
@ -472,30 +472,36 @@ func GetContextConnDetails(serverName string) (*dockerSSHPkg.Spec, error) {
} }
// GetHostConfig retrieves a ~/.ssh/config config for a host. // GetHostConfig retrieves a ~/.ssh/config config for a host.
func GetHostConfig(hostname, username, port string) (HostConfig, error) { func GetHostConfig(hostname, username, port string, override bool) (HostConfig, error) {
var hostConfig HostConfig var hostConfig HostConfig
if hostname == "" { if hostname == "" || override {
if hostname = ssh_config.Get(hostname, "Hostname"); hostname == "" { if sshHost := ssh_config.Get(hostname, "Hostname"); sshHost != "" {
logrus.Debugf("no hostname found in SSH config, assuming %s", hostname) hostname = sshHost
} }
} }
if username == "" { if username == "" || override {
if username = ssh_config.Get(hostname, "User"); username == "" { if sshUser := ssh_config.Get(hostname, "User"); sshUser != "" {
username = sshUser
} else {
systemUser, err := user.Current() systemUser, err := user.Current()
if err != nil { if err != nil {
return hostConfig, err return hostConfig, err
} }
logrus.Debugf("no username found in SSH config or passed on command-line, assuming %s", username)
username = systemUser.Username username = systemUser.Username
} }
} }
if port == "" { if port == "" || override {
if port = ssh_config.Get(hostname, "Port"); port == "" { if sshPort := ssh_config.Get(hostname, "Port"); sshPort != "" {
logrus.Debugf("no port found in SSH config or passed on command-line, assuming 22") // skip override probably correct port with dummy default value from
port = "22" // ssh_config which is 22. only when the original port number is empty
// should we try this default. this might not cover all cases
// unfortunately.
if port != "" && sshPort != "22" {
port = sshPort
}
} }
} }
@ -507,7 +513,6 @@ func GetHostConfig(hostname, username, port string) (HostConfig, error) {
} }
hostConfig.IdentityFile = idf hostConfig.IdentityFile = idf
} else { } else {
logrus.Debugf("no identity file found in SSH config for %s", hostname)
hostConfig.IdentityFile = "" hostConfig.IdentityFile = ""
} }

View File

@ -44,6 +44,7 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.Conne
ctxConnDetails.Host, ctxConnDetails.Host,
ctxConnDetails.User, ctxConnDetails.User,
ctxConnDetails.Port, ctxConnDetails.Port,
false,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -66,13 +67,13 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.Conne
return nil, err return nil, err
} }
func NewConnectionHelper(daemonURL string) *connhelper.ConnectionHelper { func NewConnectionHelper(daemonURL string) (*connhelper.ConnectionHelper, error) {
helper, err := GetConnectionHelper(daemonURL) helper, err := GetConnectionHelper(daemonURL)
if err != nil { if err != nil {
logrus.Fatal(err) return nil, err
} }
return helper return helper, nil
} }
func getDockerEndpoint(host string) (docker.Endpoint, error) { func getDockerEndpoint(host string) (docker.Endpoint, error) {

View File

@ -420,6 +420,12 @@ func convertServiceSecrets(
return nil, err return nil, err
} }
// NOTE(d1): strip # length=... modifiers
if strings.Contains(obj.Name, "#") {
vals := strings.Split(obj.Name, "#")
obj.Name = strings.TrimSpace(vals[0])
}
file := swarm.SecretReferenceFileTarget(obj.File) file := swarm.SecretReferenceFileTarget(obj.File)
refs = append(refs, &swarm.SecretReference{ refs = append(refs, &swarm.SecretReference{
File: &file, File: &file,

View File

@ -35,16 +35,21 @@ func LoadComposefile(opts Deploy, appEnv map[string]string) (*composetypes.Confi
return nil, err return nil, err
} }
recipeName, exists := appEnv["RECIPE"]
if !exists {
recipeName, _ = appEnv["TYPE"]
}
unsupportedProperties := loader.GetUnsupportedProperties(dicts...) unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
if len(unsupportedProperties) > 0 { if len(unsupportedProperties) > 0 {
logrus.Warnf("%s: ignoring unsupported options: %s", logrus.Warnf("%s: ignoring unsupported options: %s",
appEnv["TYPE"], strings.Join(unsupportedProperties, ", ")) recipeName, strings.Join(unsupportedProperties, ", "))
} }
deprecatedProperties := loader.GetDeprecatedProperties(dicts...) deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
if len(deprecatedProperties) > 0 { if len(deprecatedProperties) > 0 {
logrus.Warnf("%s: ignoring deprecated options: %s", logrus.Warnf("%s: ignoring deprecated options: %s",
appEnv["TYPE"], propertyWarnings(deprecatedProperties)) recipeName, propertyWarnings(deprecatedProperties))
} }
return config, nil return config, nil
} }

View File

@ -2,7 +2,7 @@
ABRA_VERSION="0.3.0-alpha" ABRA_VERSION="0.3.0-alpha"
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION" ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION"
RC_VERSION="0.4.0-alpha-rc2" RC_VERSION="0.4.0-alpha-rc6"
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION" RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION"
for arg in "$@"; do for arg in "$@"; do

View File

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

View File

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

14
tests/integration/app.sh Executable file
View File

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

View File

@ -0,0 +1,10 @@
#!/bin/bash
source ./testfunctions.sh
source ./common.sh
run_test '$ABRA autocomplete bash'
run_test '$ABRA autocomplete fizsh'
run_test '$ABRA autocomplete zsh'

8
tests/integration/catalogue.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
source ./testfunctions.sh
source ./common.sh
run_test '$ABRA catalogue generate'
run_test '$ABRA catalogue generate gitea'

15
tests/integration/common.sh Executable file
View File

@ -0,0 +1,15 @@
#!/bin/bash
set -e
function init() {
ABRA="$(pwd)/../../abra"
INSTALLER_URL="https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer"
export PATH=$PATH:$HOME/.local/bin
echo "choosing $ABRA as abra command"
echo "choosing $INSTALLER_URL as abra installer url"
}
init "$@"

View File

@ -1,101 +0,0 @@
#!/bin/bash
# the goal of this script is to ensure basic core functionality is working
# before we make new releases. we try to make a balance between manual testing
# and automated testing, i.e. we don't invest too much time in a fragile
# automation that costs us more time to maintain and instead just do the test
# manually (see `../manual/manual.md` for more). it is a balance which we
# figure out together.
set -ex
ABRA="$HOME/.local/bin/abra -d"
INSTALLER_URL="https://install.abra.coopcloud.tech"
for arg in "$@"; do
if [ "$arg" == "--dev" ]; then
ABRA="/src/abra -d"
INSTALLER_URL="https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer"
fi
done
export PATH=$PATH:$HOME/.local/bin
# ========================================================================
# choosing abra executable for test run
# ========================================================================
echo "choosing $ABRA as abra executable"
echo "choosing $INSTALLER_URL as abra installer url"
# ========================================================================
# latest stable release
# ========================================================================
wget -O- https://install.abra.autonomic.zone | bash
~/.local/bin/abra -v
# ========================================================================
# latest rc release
# ========================================================================
wget -O- https://install.abra.autonomic.zone | bash -s -- --rc
~/.local/bin/abra -v
# ========================================================================
# upgrade to stable in-place
# ========================================================================
$ABRA upgrade
~/.local/bin/abra -v
# ========================================================================
# upgrade to rc in-place
# ========================================================================
$ABRA upgrade --rc
~/.local/bin/abra -v
# ========================================================================
# autocomplete
# ========================================================================
$ABRA autocomplete bash
$ABRA autocomplete fizsh
$ABRA autocomplete zsh
# ========================================================================
# record command
# ========================================================================
$ABRA record new -p gandi -t A -n int-core -v 192.157.2.21 coopcloud.tech
$ABRA record list -p gandi coopcloud.tech | grep -q int-core
$ABRA -n record rm -p gandi -t A -n int-core coopcloud.tech
# ========================================================================
# catalogue command
# ========================================================================
$ABRA catalogue generate
$ABRA catalogue generate -s gitea
# ========================================================================
# recipe command
# ========================================================================
$ABRA recipe new testrecipe
$ABRA recipe list
$ABRA recipe list -p cloud
$ABRA recipe versions peertube
$ABRA recipe lint gitea
# ========================================================================
# server command
# ========================================================================
$ABRA -n server new -p hetzner-cloud --hn int-core
$ABRA server ls | grep -q int-core
$ABRA -n server rm -s -p hetzner-cloud --hn int-core
# ========================================================================
# app command
# ========================================================================
$ABRA app ls
$ABRA app ls -S

12
tests/integration/install.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
source ./testfunctions.sh
source ./common.sh
run_test 'wget -O- https://install.abra.autonomic.zone | bash; ~/.local/bin/abra -v'
run_test 'wget -O- https://install.abra.autonomic.zone | bash -s -- --rc; ~/.local/bin/abra -v'
run_test '$ABRA upgrade; ~/.local/bin/abra -v'
run_test '$ABRA upgrade --rc; ~/.local/bin/abra -v'

14
tests/integration/recipe.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
source ./testfunctions.sh
source ./common.sh
run_test '$ABRA recipe new testrecipe'
run_test '$ABRA recipe list'
run_test '$ABRA recipe list --pattern cloud'
run_test '$ABRA recipe versions peertube'
run_test '$ABRA recipe lint gitea'

21
tests/integration/records.sh Executable file
View File

@ -0,0 +1,21 @@
#!/bin/bash
source ./testfunctions.sh
source ./common.sh
run_test "$ABRA record new \
--provider gandi \
--record-type A \
--record-name integration-tests \
--record-value 192.157.2.21 \
--no-input coopcloud.tech \
"
run_test '$ABRA record list --provider gandi coopcloud.tech'
run_test "$ABRA record rm \
--provider gandi \
--record-type A \
--record-name integration-tests \
--no-input coopcloud.tech
"

10
tests/integration/server.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
source ./testfunctions.sh
source ./common.sh
run_test '$ABRA server new --provider hetzner-cloud --hetzner-name integration-tests --no-input'
run_test '$ABRA server ls'
run_test '$ABRA server rm --provider hetzner-cloud --hetzner-name int-core --server --no-input'

24
tests/integration/test_all.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/bash
if [ -z $1 ]; then
echo "usage: ./test_all.sh logdir"
exit
fi
res_dir=$1/
if [[ ! -d "$res_dir" ]]; then
mkdir "$res_dir"
fi
# Usage: run_test [number] [name] [command]
run_test () {
logfile="$res_dir/$1-$2.log"
echo $logfile
}
testScripts=("app.sh" "autocomplete.sh" "catalogue.sh" "install.sh" "recipe.sh" "records.sh" "server.sh")
for i in "${testScripts[@]}"; do
cmd="./$i $res_dir${i/sh/log}"
eval $cmd
done

View File

@ -0,0 +1,35 @@
#!/bin/bash
if [ -z $1 ]; then
logfile=/dev/null
else
logfile=$1
fi
if [ -z $logall ]; then
logall=no
fi
run_test () {
if [ -z "$@" ]; then
echo "run_test needs a command to run"
else
tempLogfile=$(mktemp)
cmd=$(eval echo "$@")
echo -e "\\n------------ INPUT -------------------" | tee -a $tempLogfile
echo "$" "$cmd" | tee -a $tempLogfile
echo "------------ OUTPUT ------------------" | tee -a $tempLogfile
eval $cmd 2>&1 | tee -a $tempLogfile
if [ $logall = "yes" ]; then
cat $tempLogfile >> $logfile
echo -e "\\n\\n" >> $logfile
else
read -N 1 -p "Did the test pass? [y/n]: " pass
if [ $pass = 'n' ]; then
cat $tempLogfile >> $logfile
echo -e "\\n\\n" >> $logfile
fi
fi
rm $tempLogfile
fi
}

View File

@ -1 +0,0 @@
TYPE=works

View File

@ -1,84 +0,0 @@
---
# The goal of this compose file is to have a testing ground for understanding
# what cases we need to handle to get stable deployments. For that, we need to
# work with healthchecks and deploy configurations quite closely. If you run
# the `make symlink` target then this will be loaded into a "fake" app on your
# local machine which you can deploy with `abra`.
version: "3.8"
services:
r1_should_work:
image: redis:alpine
deploy:
update_config:
failure_action: rollback
order: start-first
rollback_config:
order: start-first
restart_policy:
max_attempts: 1
healthcheck:
test: redis-cli ping
interval: 2s
retries: 3
start_period: 1s
timeout: 3s
r2_broken_health_check:
image: redis:alpine
deploy:
update_config:
failure_action: rollback
order: start-first
rollback_config:
order: start-first
restart_policy:
max_attempts: 3
healthcheck:
test: foobar
interval: 2s
retries: 3
start_period: 1s
timeout: 3s
r3_no_health_check:
image: redis:alpine
deploy:
update_config:
failure_action: rollback
order: start-first
rollback_config:
order: start-first
restart_policy:
max_attempts: 3
r4_disabled_health_check:
image: redis:alpine
deploy:
update_config:
failure_action: rollback
order: start-first
rollback_config:
order: start-first
restart_policy:
max_attempts: 3
healthcheck:
disable: true
r5_should_also_work:
image: redis:alpine
deploy:
update_config:
failure_action: rollback
order: start-first
rollback_config:
order: start-first
restart_policy:
max_attempts: 1
healthcheck:
test: redis-cli ping
interval: 2s
retries: 3
start_period: 1s
timeout: 3s

View File

@ -1 +0,0 @@
TYPE=works

View File

@ -1,57 +0,0 @@
# manual test plan
Best served after running `make int-core` which assures most core functionality
is still working. These manual tests are for testing things that are hard to
wire up for testing in an automated way.
## recipe publish
- `abra recipe upgrade <recipe>`
- `abra recipe sync <recipe>`
- `abra recipe release --publish <recipe>`
## automagic traefik deploy
- `abra server add -p -t <server>`
## deploy, upgrade, rollback
- `abra app deploy <app>`
- `abra app upgrade <app>`
- `abra app rollback <app>`
## backup & restore
- `abra app deploy <app>`
- `abra app backup <app>`
- `abra app undeploy <app>`
- `abra app volume remove --force <app>`
- `abra app deploy <app>`
- `abra app restore <app>`
## app day-to-day ops
### easy mode
- `abra app ls -t <recipe>`
- `abra app ls -s <server>`
- `abra app ls -s <server> -t <recipe>`
- `abra app ls -s <server> -t <recipe> -S`
- `abra app config <app>`
- `abra app check <app>`
- `abra app ps <app>`
- `abra app logs <app>`
- `abra app cp <app>`
- `abra app run <app>`
- `abra app secret ls <app>`
- `abra app volume ls <app>`
### hard mode
- `abra app restart <app>`
- `abra app remove <app>`
- `abra app secret insert <app> foo v1 bar`
- `abra app secret remove <app> foo`
- `abra app secret generate --all`
- `abra app volume remove --force <app>`
- `abra app errors -w <app>`

View File

@ -1,2 +1,2 @@
TYPE=ecloud RECIPE=ecloud
DOMAIN=ecloud.evil.corp DOMAIN=ecloud.evil.corp