Compare commits

...

242 Commits

Author SHA1 Message Date
3wc
a21d431541 fix: don't panic() 😅 2022-11-24 17:33:59 +00:00
3wc
8fad34e430 fix: switch back to replacing <recipe>.example.com
Fixes #355
2022-11-24 17:33:59 +00:00
a036de3c26
chore: go mod tidy 2022-11-17 13:23:19 +01:00
4c2109e8ce chore(deps): update module golang.org/x/crypto to v0.3.0 2022-11-17 08:01:01 +00:00
7f745ff19f
feat(cmd)!: run abra.sh commands with /bin/bash if available.
BREAKING CHANGE: abra.sh commands that depend on /bin/sh will break

Closes coop-cloud/organising#357.

See coop-cloud/abra#229.
2022-11-15 23:01:57 +01:00
521d3d1259 feat(autocomplete): add autocompletion for fish shell 2022-11-15 22:24:34 +01:00
14187449a5
fix: fork passgen
See coop-cloud/organising#358
2022-11-14 15:18:54 +01:00
2037f4cc19
chore: go mod tidy 2022-11-11 17:40:42 +01:00
05d492d30b chore(deps): update module github.com/hetznercloud/hcloud-go to v1.37.0 2022-11-11 08:01:11 +00:00
9591e91ed6 feat(cmd): make env variables accessible for local abra.sh commands 2022-11-10 11:12:35 +00:00
f6f587e506
chore: go mod tidy 2022-11-10 11:47:16 +01:00
4f28dbee87 chore(deps): update module golang.org/x/crypto to v0.2.0 2022-11-10 08:01:07 +00:00
ad1cc038e3 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.36.0 2022-11-09 08:01:00 +00:00
15dbd85d25 chore(deps): update module golang.org/x/sys to v0.2.0 2022-11-08 08:00:59 +00:00
2a97955586 chore(deps): update module github.com/schollz/progressbar/v3 to v3.12.1 2022-11-07 08:00:59 +00:00
9e44d1dfba
chore: go mod tidy 2022-11-04 14:52:56 +01:00
87ad8e2761 chore(deps): update module github.com/schollz/progressbar/v3 to v3.12.0 2022-11-03 08:01:03 +00:00
cfe703b15d chore(deps): update module github.com/docker/cli to v20.10.21 2022-10-27 08:44:38 +00:00
96503fa9e9 chore(deps): update module github.com/docker/docker to v20.10.21 2022-10-26 07:01:18 +00:00
07d49d8566
chore go mod tidy 2022-10-22 14:19:10 +02:00
5a7c25375a
Merge remote-tracking branch 'origin/renovate/main-gotest.tools-v3-3.x' 2022-10-22 14:18:35 +02:00
652143e76c
Merge remote-tracking branch 'origin/renovate/main-golang.org-x-sys-0.x' 2022-10-22 14:18:25 +02:00
8afce6eebf
Merge remote-tracking branch 'origin/renovate/main-golang.org-x-crypto-0.x' 2022-10-22 14:17:35 +02:00
d3e6c9dc94
Merge remote-tracking branch 'origin/renovate/main-github.com-docker-docker-20.x' 2022-10-22 14:17:26 +02:00
4fd0ca3dd1 chore(deps): update module golang.org/x/crypto to v0.1.0 2022-10-20 07:00:57 +00:00
dc0b6c2c8c chore(deps): update module github.com/docker/docker to v20.10.20 2022-10-19 07:00:57 +00:00
54f242baf7 chore(deps): update module github.com/docker/cli to v20.10.20 2022-10-19 07:00:51 +00:00
07620c7d89 chore(deps): update module golang.org/x/sys to v0.1.0 2022-10-18 07:01:15 +00:00
1cae4cce4e chore(deps): update module gotest.tools/v3 to v3.4.0 2022-10-10 07:01:50 +00:00
9347ade82c
chore: go mod tidy 2022-09-20 10:42:53 +02:00
3fa18a8050
Merge remote-tracking branch 'origin/renovate/main-github.com-schollz-progressbar-v3-3.x' 2022-09-20 10:42:25 +02:00
4ac67662a2
Merge remote-tracking branch 'origin/renovate/main-github.com-hetznercloud-hcloud-go-1.x' 2022-09-20 10:42:17 +02:00
d1be4077c5
Merge remote-tracking branch 'origin/renovate/main-github.com-gliderlabs-ssh-0.x' 2022-09-20 10:42:09 +02:00
5a88c34a7c
Merge remote-tracking branch 'origin/renovate/main-github.com-docker-go-units-0.x' 2022-09-20 10:41:57 +02:00
2e452e3213
Merge remote-tracking branch 'origin/renovate/main-github.com-docker-docker-20.x' 2022-09-20 10:41:30 +02:00
9d16a8e10c
Merge remote-tracking branch 'origin/renovate/main-github.com-docker-cli-20.x' 2022-09-20 10:41:22 +02:00
8755a6c3b4 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.35.3 2022-09-20 07:01:14 +00:00
8cee8ae33a chore(deps): update module github.com/schollz/progressbar/v3 to v3.11.0 2022-09-12 07:01:55 +00:00
15b138e026 chore(deps): update module github.com/docker/docker to v20.10.18 2022-09-12 07:01:39 +00:00
4a8ed36dea chore(deps): update module github.com/docker/cli to v20.10.18 2022-09-12 07:01:28 +00:00
7d0c3cc496 chore(deps): update module github.com/alecaivazis/survey/v2 to v2.3.6 2022-09-12 07:01:18 +00:00
3cf479ffd5 chore(deps): update module github.com/docker/go-units to v0.5.0 2022-09-01 07:01:24 +00:00
d402050a40 chore(deps): update module github.com/gliderlabs/ssh to v0.3.5 2022-09-01 07:01:15 +00:00
664edce09d
build: fix matching to ignore deps upgrade [ci skip] 2022-08-15 12:32:19 +02:00
e41caa891d
fix: dont check ip on server when it is local
Closes coop-cloud/organising#334.
2022-08-14 22:20:17 +02:00
42a6818ff4
fix: app cmd parsing, usage & tests
Note: the integration tests don't work due to ValidateApp still
attempting to validate the host key for the test app which doesn't
exist. This will be fixed in a future commit.
2022-08-14 16:18:58 +02:00
8f709c05bf
build: ignore merges, chores & sort 2022-08-12 01:11:25 +02:00
a4ebf7befc
docs: add frando & fix intro [ci skip] 2022-08-11 17:50:19 +02:00
Franz Heinzmann (Frando)
8458e61d17
fix: branch checking logic
See https://github.com/go-git/go-git/issues/518 for why this is needed.
2022-08-11 17:49:22 +02:00
b42d5bf113
fix: ignore until coop-cloud/organising#336 is fixed [ci skip]
See coop-cloud/organising#336
2022-08-04 12:39:04 +03:00
f684c6d6e4
fix: drop back to urfave@v1.22.5 for parsing fix
See coop-cloud/organising#336
2022-08-03 14:40:01 +03:00
6593baf9f4 chore(deps): update golang docker tag to v1.19 2022-08-03 07:01:11 +00:00
50123f3810
chore: go mod tidy 2022-08-02 11:25:13 +03:00
d132e87f14
Merge remote-tracking branch 'origin/renovate/main-github.com-schollz-progressbar-v3-3.x' 2022-08-02 11:24:03 +03:00
37a1c3fb85 chore(deps): update module github.com/schollz/progressbar/v3 to v3.9.0 2022-08-01 07:01:38 +00:00
c8183aa6d1 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.35.2 2022-08-01 07:01:21 +00:00
4711de29ae
chore: go mod tidy 2022-07-21 15:03:17 +03:00
b719aaba41
Merge remote-tracking branch 'origin/renovate/main-github.com-sirupsen-logrus-1.x' 2022-07-21 15:02:25 +03:00
074c51b672 chore(deps): update module github.com/sirupsen/logrus to v1.9.0 2022-07-20 07:01:14 +00:00
1aa6be704a chore(deps): update module github.com/schollz/progressbar/v3 to v3.8.7 2022-07-20 07:01:06 +00:00
e8e3cb8598
chore: go mod tidy 2022-07-14 11:53:22 +02:00
85fec6b107
Merge remote-tracking branch 'origin/renovate/main-gotest.tools-v3-3.x' 2022-07-14 11:51:41 +02:00
12dbb061a9 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.35.1 2022-07-05 07:01:24 +00:00
351bd7d4ba chore(deps): update module gotest.tools/v3 to v3.3.0 2022-06-20 07:01:21 +00:00
cdc7037c25
chore: go mod tidy [ci skip] 2022-06-15 13:56:43 +02:00
682237c98e chore(deps): update module github.com/alecaivazis/survey/v2 to v2.3.5 2022-06-08 07:01:45 +00:00
08d97be43a
chore: go mod tidy 2022-06-07 09:09:08 +02:00
786dfde27e
Merge commit 'c153c5d' into main 2022-06-07 09:08:55 +02:00
6e012b910e chore(deps): update module github.com/docker/docker to v20.10.17 2022-06-07 07:01:42 +00:00
c153c5da2e chore(deps): update module github.com/docker/cli to v20.10.17 2022-06-07 07:01:28 +00:00
0540e42168
alpha -> beta 2022-05-31 10:23:49 +02:00
4bc95a5b52
chore: go mod tidy [ci skip] 2022-05-16 16:22:21 +02:00
febc6e2874
Merge remote-tracking branch 'origin/renovate/main-github.com-docker-docker-20.x' into main 2022-05-16 16:22:12 +02:00
b2c990bf12
Merge remote-tracking branch 'origin/renovate/main-github.com-docker-cli-20.x' into main 2022-05-16 16:22:06 +02:00
3b8893502a
docs: re-word on docstrings [ci skip] 2022-05-13 16:44:49 +02:00
e0a0378f73 chore(deps): update module github.com/docker/docker to v20.10.16 2022-05-13 07:01:43 +00:00
0837045d44 chore(deps): update module github.com/docker/cli to v20.10.16 2022-05-13 07:01:33 +00:00
cd8137a7d8
chore: go mod tidy [ci skip] 2022-05-10 16:15:08 +02:00
ece4537a2d
Merge remote-tracking branch 'origin/renovate/main-github.com-gliderlabs-ssh-0.x' into main 2022-05-10 16:14:45 +02:00
16fe1b68c6
fix: thread app name & stack name correctly 2022-05-10 12:10:36 +02:00
e37f235fd4 chore(deps): update module github.com/gliderlabs/ssh to v0.3.4 2022-05-10 07:01:27 +00:00
0423ce7e84
fix: working link [ci skip] 2022-05-10 08:32:12 +02:00
d46ac22bd7
chore: go mod tidy [ci skip] 2022-05-09 14:09:14 +02:00
cef5cd8611
Merge remote-tracking branch 'origin/renovate/main-github.com-docker-docker-20.x' into main 2022-05-09 14:04:16 +02:00
8b38dac9ab chore(deps): update module github.com/docker/docker to v20.10.15 2022-05-06 07:01:51 +00:00
89fc875088 chore(deps): update module github.com/docker/cli to v20.10.15 2022-05-06 07:01:31 +00:00
026a9ba2d7
chore: go mod tidy [ci skip] 2022-05-05 15:13:20 +02:00
99f2b9c6dc chore(deps): update module github.com/urfave/cli to v1.22.9 2022-05-05 07:01:30 +00:00
578e91eeec
chore: publish next tag 0.5.0-alpha 2022-05-03 17:22:54 +02:00
49f79dbd45
fix!: new catalogue URL 2022-05-03 17:08:52 +02:00
574d556bb9
chore: go mod tidy 2022-04-30 18:28:42 +02:00
801aad64df
Merge remote-tracking branch 'origin/renovate/main-gotest.tools-v3-3.x' into main 2022-04-30 18:28:22 +02:00
b0a0829712
Merge remote-tracking branch 'origin/renovate/main-github.com-urfave-cli-1.x' into main 2022-04-30 18:28:15 +02:00
6aae06c3ec chore(deps): update module github.com/urfave/cli to v1.22.8 2022-04-29 07:01:30 +00:00
d0c6fa5b45 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.33.2 2022-04-27 07:02:33 +00:00
c947354ee3 chore(deps): update module gotest.tools/v3 to v3.2.0 2022-04-25 07:01:44 +00:00
9b7e5752fb
chore: go mod tidy [ci skip] 2022-04-22 11:25:08 +02:00
9bc51629d4 chore(deps): update module github.com/urfave/cli to v1.22.7 2022-04-22 07:01:22 +00:00
4ba15df9b7
chore: 0.4.1-alpha 2022-04-21 15:47:39 +02:00
5721b357a2
fix: per service logs 2022-04-21 15:40:23 +02:00
6140abbcac fix: sync to latest before commits come in
Follows from a4989e383402e5c1af0b9713a3ad4f50ab5581e5
2022-04-20 11:42:24 +00:00
996255188b Revert "fix: ensure we're on latest for recipe release dance"
This reverts commit 3c4bb6a55e28590c9bcfe0862ffbbfda75ea68cb.
2022-04-20 11:42:24 +00:00
11d78234b2
installer: add 32 bit arm support 2022-04-20 13:37:51 +02:00
c214937e4a
installer: download on aarch64 2022-04-20 13:13:50 +02:00
3a3f41988b
chore: publish 0.4.0-alpha 2022-04-19 14:36:56 +02:00
f6690a80bd
build: upx release script [ci skip] 2022-04-19 14:34:06 +02:00
2337c4648b
chore: remove unused command 2022-04-19 14:32:34 +02:00
a1190f1352
fix: show which service is getting backed up [ci skip] 2022-04-19 13:50:23 +02:00
e421922f5b
fix: restore uses absolute paths & better docs 2022-04-19 13:21:12 +02:00
10d5705d1a
docs: better backup docs 2022-04-19 13:20:48 +02:00
a4f1634b24
fix: backups get gzip, absolute paths, single archive file 2022-04-19 12:52:30 +02:00
cbd924060f
fix: better local changes message 2022-04-19 10:29:05 +02:00
3c4bb6a55e
fix: ensure we're on latest for recipe release dance
Closes coop-cloud/organising#313.
2022-04-19 10:28:49 +02:00
a0d7a76f9d
fix: better error messages for release failures
See coop-cloud/organising#313
2022-04-19 10:20:35 +02:00
c71efb46ba
feat: arm builds [ci skip]
See coop-cloud/organising#312
2022-04-19 10:06:14 +02:00
ce69967ec5
chore: go mod tidy 2022-04-18 10:42:39 +02:00
1a04439b1f chore(deps): update module github.com/hashicorp/go-retryablehttp to v0.7.1 2022-04-14 07:01:24 +00:00
979f417a63
chore: gpl this sucka [ci skip] 2022-04-05 12:18:34 +02:00
b27acb2f61
feat: backup/restore [ci skip]
See coop-cloud/organising#30.
2022-04-03 18:24:09 +02:00
622ecc4885
docs: drop slash [ci skip] 2022-04-01 23:18:22 +02:00
ed5bbda811
docs: wording & emoji [ci skip] 2022-04-01 23:14:57 +02:00
7b627ea518
docs: nice gopher [ci skip] 2022-04-01 23:12:24 +02:00
1ac66da83f
chore: go mod tidy 2022-04-01 10:21:16 +02:00
061de96b62 chore(deps): update module github.com/kevinburke/ssh_config to v1.2.0 2022-04-01 07:01:23 +00:00
6998298d32
chore: publish next tag 0.4.0-alpha-rc8 2022-03-30 16:28:55 +02:00
323f4467c8
fix: filtering requires case-by-case handling
See https://github.com/moby/moby/issues/32985.
2022-03-30 16:25:38 +02:00
e8e41850b5
fix: pass args to local function invocations too 2022-03-30 11:31:16 +02:00
0e23ec53d7
refactor!: simple validation only 2022-03-30 11:30:51 +02:00
b943a8b9b1
feat: allow choosing user on remote commands 2022-03-30 11:30:36 +02:00
acc665f054
chore: publish next tag 0.4.0-alpha-rc7 2022-03-27 21:33:30 +02:00
860f1d6376 feat: bring back scripts interface
See coop-cloud/organising#301.
2022-03-27 19:30:48 +00:00
2122f0e67c fix: avoid short command alias conflicts 2022-03-27 19:30:48 +00:00
6aa23a76a1 fix: more precise filtering
Closes coop-cloud/organising#305.
2022-03-27 19:30:36 +00:00
338360096c
feat: pass domain to new app envs
See coop-cloud/organising#304.
2022-03-27 21:06:48 +02:00
7a8c7cd50f
ci: drop static check 2022-03-27 13:51:40 +02:00
bafc8a8e34
chore: go mod tidy 2022-03-26 15:23:27 +01:00
3d44d8c9fd
Merge remote-tracking branch 'origin/renovate/main-github.com-docker-docker-20.x' into main 2022-03-26 15:22:31 +01:00
b8b4616498
Merge remote-tracking branch 'origin/renovate/main-github.com-docker-cli-20.x' into main 2022-03-26 15:22:18 +01:00
da97117929 chore(deps): update module github.com/docker/docker to v20.10.14 2022-03-24 08:01:35 +00:00
978297c464 chore(deps): update module github.com/docker/cli to v20.10.14 2022-03-24 08:01:27 +00:00
11da4808fc chore(deps): update module github.com/alecaivazis/survey/v2 to v2.3.4 2022-03-24 08:01:21 +00:00
4023e6a066
fix: wait until app created to check for secrets 2022-03-18 11:10:15 +01:00
f432bfdd23
fix: warn when no repo on git 2022-03-18 10:13:24 +01:00
848e17578d chore(deps): update golang docker tag to v1.18 2022-03-16 08:01:41 +00:00
1615130929
fix: skip prompt for no passwords 2022-03-15 10:54:05 +01:00
7f315315f0
fix: better prompts & matching for secret removal 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 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
Closes coop-cloud/organising#302.
2022-03-12 16:47:19 +01:00
f39e186b66
fix: match Force/NoInput where needed 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
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 2022-03-11 19:37:50 +01:00
e9cfb076c6
fix: strip length modifiers
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 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 2022-03-11 08:01:57 +00:00
99a6135f72 chore(deps): update module github.com/docker/cli to v20.10.13 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 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 2022-03-08 18:04:47 +01:00
3381b8936d
fix: better error handling & proper context deletion for server rm 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 2022-02-22 10:52:14 +01:00
91b4e021d0 chore(deps): update module github.com/containers/image to v5 2022-02-22 08:01:12 +00:00
598e87dca2 chore: skip new repositories 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
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
Also fix error handling to match function signatures.
2022-02-18 11:17:43 +01:00
5da4afa0ec
fix: only ensure latest after cloning 2022-02-18 09:55:07 +01:00
9d5e805748
chore: go mod tidy 2022-02-16 13:53:09 +01:00
770ae5ed9b chore(deps): update module github.com/moby/sys/signal to v0.7.0 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 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 2022-02-14 17:10:53 +01:00
fc5107865b
fix: typo 2022-02-10 10:59:19 +01:00
53ed1fc545
chore: go mod tidy 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 2022-02-09 08:58:51 +00:00
b5f23d3791 feat: show latest published version on sync 2022-02-09 08:58:20 +00:00
2b2dcc01b4
fix: dont checkout latest if we dont have a copy 2022-02-09 09:54:02 +01:00
0a208d049e
chore: go mod tidy + patch upgrades 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 2022-02-04 08:01:17 +00:00
6fa090352d chore(deps): update module github.com/buger/goterm to v1.0.4 2022-02-04 08:01:11 +00:00
227c02cd09
refactor!: make common flags single char again 2022-02-03 14:19:51 +01:00
bfeda40e34
fix: catch more ssh failure modes with help 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
Follows b2d17a1829c5415c4700cf5da85144af4de87162.
2022-01-29 14:06:25 +01:00
b2d17a1829
fix: ensure latest checked out for recipe upgrade 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 2022-01-27 12:06:32 +01:00
224b8865bf
test: newlines for output when Y'ing & N'ing 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 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 2022-01-25 11:39:38 +01:00
415df981ff
test: long flags, drop docker, use run_tests for all tests 2022-01-24 16:49:51 +01:00
57728e58e8
test: improve semi-manual testing 2022-01-21 16:48:42 +01:00
c7062e0494
fix: initial subcmd completion
Broken by migration to v1 API.
2022-01-20 11:42:04 +01:00
101 changed files with 2590 additions and 1075 deletions

View File

@ -3,27 +3,17 @@ 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.19
commands: commands:
- make check - make check
- name: make static
image: golang:1.17
ignore: true # until we decide we all want this check
environment:
STATIC_CHECK_URL: honnef.co/go/tools/cmd/staticcheck
STATIC_CHECK_VERSION: v0.2.0
commands:
- go install $STATIC_CHECK_URL@$STATIC_CHECK_VERSION
- make static
- name: make build - name: make build
image: golang:1.17 image: golang:1.19
commands: commands:
- make build - make build
- name: make test - name: make test
image: golang:1.17 image: golang:1.19
commands: commands:
- make test - make test
@ -55,7 +45,7 @@ steps:
event: tag event: tag
- name: release - name: release
image: golang:1.17 image: golang:1.19
environment: environment:
GITEA_TOKEN: GITEA_TOKEN:
from_secret: goreleaser_gitea_token from_secret: goreleaser_gitea_token

View File

@ -7,7 +7,6 @@ gitea_urls:
before: before:
hooks: hooks:
- go mod tidy - go mod tidy
- go generate ./...
builds: builds:
- env: - env:
- CGO_ENABLED=0 - CGO_ENABLED=0
@ -15,6 +14,15 @@ builds:
goos: goos:
- linux - linux
- darwin - darwin
goarch:
- 386
- amd64
- arm
- arm64
goarm:
- 5
- 6
- 7
ldflags: ldflags:
- "-X 'main.Commit={{ .Commit }}'" - "-X 'main.Commit={{ .Commit }}'"
- "-X 'main.Version={{ .Version }}'" - "-X 'main.Version={{ .Version }}'"
@ -31,8 +39,10 @@ changelog:
sort: desc sort: desc
filters: filters:
exclude: exclude:
- "^Merge"
- "^Revert"
- "^WIP:" - "^WIP:"
- "^chore(deps):"
- "^style:" - "^style:"
- "^test:" - "^test:"
- "^tests:" - "^tests:"
- "^Revert"

11
AUTHORS.md Normal file
View File

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

15
LICENSE Normal file
View File

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

View File

@ -5,7 +5,7 @@ LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
DIST_LDFLAGS := $(LDFLAGS)" -s -w" DIST_LDFLAGS := $(LDFLAGS)" -s -w"
export GOPRIVATE=coopcloud.tech export GOPRIVATE=coopcloud.tech
all: format check static build test all: format check build test
run: run:
@go run -ldflags=$(LDFLAGS) $(ABRA) @go run -ldflags=$(LDFLAGS) $(ABRA)
@ -28,9 +28,6 @@ format:
check: check:
@test -z $$(gofmt -l .) || (echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1) @test -z $$(gofmt -l .) || (echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1)
static:
@staticcheck $(ABRA)
test: test:
@go test ./... -cover -v @go test ./... -cover -v

View File

@ -1,73 +1,12 @@
# abra # `abra`
> https://coopcloud.tech
[![Build Status](https://build.coopcloud.tech/api/badges/coop-cloud/abra/status.svg?ref=refs/heads/main)](https://build.coopcloud.tech/coop-cloud/abra) [![Build Status](https://build.coopcloud.tech/api/badges/coop-cloud/abra/status.svg?ref=refs/heads/main)](https://build.coopcloud.tech/coop-cloud/abra)
[![Go Report Card](https://goreportcard.com/badge/git.coopcloud.tech/coop-cloud/abra)](https://goreportcard.com/report/git.coopcloud.tech/coop-cloud/abra) [![Go Report Card](https://goreportcard.com/badge/git.coopcloud.tech/coop-cloud/abra)](https://goreportcard.com/report/git.coopcloud.tech/coop-cloud/abra)
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 and a whole lot of other things. Please see [docs.coopcloud.tech](https://docs.coopcloud.tech) for more extensive documentation. <a href="https://github.com/egonelbre/gophers"><img align="right" width="150" src="https://github.com/egonelbre/gophers/raw/master/.thumb/sketch/adventure/poking-fire.png"/></a>
## Quick install `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 the day-to-day operations of [operators](https://docs.coopcloud.tech/operators/) and [maintainers](https://docs.coopcloud.tech/maintainers/) pleasant & convenient. It is libre software, written in [Go](https://go.dev) and maintained and extended by the community :heart:
```bash Please see [docs.coopcloud.tech/abra](https://docs.coopcloud.tech/abra) for help on install, upgrade, hacking, troubleshooting & more!
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. At time of writing, `go get github.com/Autonomic-Cooperative/godotenv@b031ea1211e7fd297af4c7747ffb562ebe00cd33` is the command you want to run to maintain the above functionality.
#### `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

@ -8,8 +8,8 @@ var AppCommand = cli.Command{
Name: "app", Name: "app",
Aliases: []string{"a"}, Aliases: []string{"a"},
Usage: "Manage apps", Usage: "Manage apps",
ArgsUsage: "<app>", ArgsUsage: "<domain>",
Description: "This command provides functionality for managing the life cycle of your apps", Description: "Functionality for managing the life cycle of your apps",
Subcommands: []cli.Command{ Subcommands: []cli.Command{
appNewCommand, appNewCommand,
appConfigCommand, appConfigCommand,
@ -29,5 +29,8 @@ var AppCommand = cli.Command{
appVolumeCommand, appVolumeCommand,
appVersionCommand, appVersionCommand,
appErrorsCommand, appErrorsCommand,
appCmdCommand,
appBackupCommand,
appRestoreCommand,
}, },
} }

389
cli/app/backup.go Normal file
View File

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

View File

@ -14,18 +14,17 @@ import (
var appCheckCommand = cli.Command{ var appCheckCommand = cli.Command{
Name: "check", Name: "check",
Aliases: []string{"c"}, Aliases: []string{"chk"},
Usage: "Check if app is configured correctly", Usage: "Check if app is configured correctly",
ArgsUsage: "<service>", ArgsUsage: "<domain>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag,
}, },
Before: internal.SubCommandBefore, 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)

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

@ -0,0 +1,244 @@
package app
import (
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/archive"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var appCmdCommand = cli.Command{
Name: "command",
Aliases: []string{"cmd"},
Usage: "Run app commands",
Description: `
Run an app specific command.
These commands are bash functions, defined in the abra.sh of the recipe itself.
They can be run within the context of a service (e.g. app) or locally on your
work station by passing "--local". Arguments can be passed into these functions
using the "-- <args>" syntax.
Example:
abra app cmd example.com app create_user -- me@example.com
`,
ArgsUsage: "<domain> [<service>] <command> [-- <args>]",
Flags: []cli.Flag{
internal.DebugFlag,
internal.LocalCmdFlag,
internal.RemoteUserFlag,
},
BashComplete: autocomplete.AppNameComplete,
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if internal.LocalCmd && internal.RemoteUser != "" {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use --local & --user together"))
}
hasCmdArgs, parsedCmdArgs := parseCmdArgs(c.Args(), internal.LocalCmd)
abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("%s does not exist for %s?", abraSh, app.Name)
}
logrus.Fatal(err)
}
if internal.LocalCmd {
cmdName := c.Args().Get(1)
if err := ensureCommand(abraSh, app.Recipe, cmdName); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("--local detected, running %s on local work station", cmdName)
var exportEnv string
for k, v := range app.Env {
exportEnv = exportEnv + fmt.Sprintf("%s='%s'; ", k, v)
}
var sourceAndExec string
if hasCmdArgs {
logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs)
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s %s", app.Name, app.StackName(), exportEnv, abraSh, cmdName, parsedCmdArgs)
} else {
logrus.Debug("did not detect any command arguments")
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s", app.Name, app.StackName(), exportEnv, abraSh, cmdName)
}
shell := "/bin/bash"
if _, err := os.Stat(shell); errors.Is(err, os.ErrNotExist) {
logrus.Debugf("%s does not exist locally, use /bin/sh as fallback", shell)
shell = "/bin/sh"
}
cmd := exec.Command(shell, "-c", sourceAndExec)
if err := internal.RunCmd(cmd); err != nil {
logrus.Fatal(err)
}
} else {
targetServiceName := c.Args().Get(1)
cmdName := c.Args().Get(2)
if err := ensureCommand(abraSh, app.Recipe, cmdName); err != nil {
logrus.Fatal(err)
}
serviceNames, err := config.GetAppServiceNames(app.Name)
if err != nil {
logrus.Fatal(err)
}
matchingServiceName := false
for _, serviceName := range serviceNames {
if serviceName == targetServiceName {
matchingServiceName = true
}
}
if !matchingServiceName {
logrus.Fatalf("no service %s for %s?", targetServiceName, app.Name)
}
logrus.Debugf("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName)
if hasCmdArgs {
logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs)
} else {
logrus.Debug("did not detect any command arguments")
}
if err := runCmdRemote(app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil {
logrus.Fatal(err)
}
}
return nil
},
}
func parseCmdArgs(args []string, isLocal bool) (bool, string) {
var (
parsedCmdArgs string
hasCmdArgs bool
)
if isLocal {
if len(args) > 2 {
return true, fmt.Sprintf("%s ", strings.Join(args[2:], " "))
}
} else {
if len(args) > 3 {
return true, fmt.Sprintf("%s ", strings.Join(args[3:], " "))
}
}
return hasCmdArgs, parsedCmdArgs
}
func ensureCommand(abraSh, recipeName, execCmd string) error {
bytes, err := ioutil.ReadFile(abraSh)
if err != nil {
return err
}
if !strings.Contains(string(bytes), execCmd) {
return fmt.Errorf("%s doesn't have a %s function", recipeName, execCmd)
}
return nil
}
func runCmdRemote(app config.App, abraSh, serviceName, cmdName, cmdArgs string) error {
cl, err := client.New(app.Server)
if err != nil {
return err
}
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, true)
if err != nil {
return err
}
logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(targetContainer.ID), app.Server)
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
content, err := archive.TarWithOptions(abraSh, toTarOpts)
if err != nil {
return err
}
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(context.Background(), targetContainer.ID, "/tmp", content, copyOpts); err != nil {
return err
}
// FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli()
if err != nil {
return err
}
shell := "/bin/bash"
findShell := []string{"test", "-e", shell}
execCreateOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: findShell,
Detach: false,
Tty: false,
}
if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
logrus.Infof("%s does not exist for %s, use /bin/sh as fallback", shell, app.Name)
shell = "/bin/sh"
}
var cmd []string
if cmdArgs != "" {
cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s %s", serviceName, app.Name, app.StackName(), cmdName, cmdArgs)}
} else {
cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s", serviceName, app.Name, app.StackName(), cmdName)}
}
logrus.Debugf("running command: %s", strings.Join(cmd, " "))
if internal.RemoteUser != "" {
logrus.Debugf("running command with user %s", internal.RemoteUser)
execCreateOpts.User = internal.RemoteUser
}
execCreateOpts.Cmd = cmd
execCreateOpts.Tty = true
if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
return err
}
return nil
}

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

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

View File

@ -14,12 +14,12 @@ import (
) )
var appConfigCommand = cli.Command{ var appConfigCommand = cli.Command{
Name: "config", Name: "config",
Aliases: []string{"c"}, Aliases: []string{"cfg"},
Usage: "Edit app config", Usage: "Edit app config",
ArgsUsage: "<domain>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {

View File

@ -22,7 +22,7 @@ import (
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>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
@ -30,16 +30,15 @@ var appCpCommand = cli.Command{
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Usage: "Copy files to/from a running app service", Usage: "Copy files to/from a running app service",
Description: ` Description: `
This command supports copying files to and from any app service file system. Copy files to and from any app service file system.
If you want to copy a myfile.txt to the root of the app service: 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)
@ -106,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(context.Background(), 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)
} }
@ -157,5 +146,6 @@ func configureAndCp(
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
return nil return nil
} }

View File

@ -7,9 +7,10 @@ import (
) )
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.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
@ -20,9 +21,8 @@ var appDeployCommand = cli.Command{
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Description: ` Description: `
This command deploys an app. It does not support incrementing the version of a Deploy an app. It does not support incrementing the version of a deployed app,
deployed app, for this you need to look at the "abra app upgrade <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
if the container runtime has gotten into a weird state. if the container runtime has gotten into a weird state.

View File

@ -2,6 +2,7 @@ package app
import ( import (
"context" "context"
"fmt"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -20,10 +21,11 @@ import (
) )
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. List errors for a deployed app.
This is a best-effort implementation and an attempt to gather a number of tips This is a best-effort implementation and an attempt to gather a number of tips
& tricks for finding errors together into one convenient command. When an app & tricks for finding errors together into one convenient command. When an app
@ -40,15 +42,13 @@ 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{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag,
internal.WatchFlag, internal.WatchFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
@ -89,14 +89,15 @@ 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
} }
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", fmt.Sprintf("^%s_%s", app.StackName(), service.Name))
containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters}) containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters})
if err != nil { if err != nil {
return err return err

View File

@ -22,12 +22,12 @@ var statusFlag = &cli.BoolFlag{
Destination: &status, Destination: &status,
} }
var appType string var appRecipe string
var typeFlag = &cli.StringFlag{ var recipeFlag = &cli.StringFlag{
Name: "type, t", Name: "recipe, r",
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
@ -62,19 +62,18 @@ var appListCommand = cli.Command{
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Usage: "List all managed apps", Usage: "List all managed apps",
Description: ` Description: `
This command looks at your local file system listing of apps and servers (e.g. Read the local file system listing of apps and servers (e.g. ~/.abra/) to
in ~/.abra/) to generate a report of all your apps. 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.
`, `,
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag,
statusFlag, statusFlag,
listAppServerFlag, listAppServerFlag,
typeFlag, recipeFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
@ -88,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
@ -123,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++
} }
@ -161,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)
} }
@ -198,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
@ -216,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"}...)
} }
@ -224,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

@ -29,9 +29,12 @@ var logOpts = types.ContainerLogsOptions{
} }
// stackLogs lists logs for all stack services // stackLogs lists logs for all stack services
func stackLogs(c *cli.Context, stackName string, client *dockerClient.Client) { func stackLogs(c *cli.Context, app config.App, client *dockerClient.Client) {
filters := filters.NewArgs() filters, err := app.Filters(true, false)
filters.Add("name", stackName) if err != nil {
logrus.Fatal(err)
}
serviceOpts := types.ServiceListOptions{Filters: filters} serviceOpts := types.ServiceListOptions{Filters: filters}
services, err := client.ServiceList(context.Background(), serviceOpts) services, err := client.ServiceList(context.Background(), serviceOpts)
if err != nil { if err != nil {
@ -67,12 +70,11 @@ func stackLogs(c *cli.Context, stackName string, client *dockerClient.Client) {
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, internal.DebugFlag,
internal.NoInputFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
@ -86,8 +88,8 @@ 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, cl)
} else { } else {
logrus.Debugf("tailing logs for %s", serviceName) logrus.Debugf("tailing logs for %s", serviceName)
if err := tailServiceLogs(c, cl, app, serviceName); err != nil { if err := tailServiceLogs(c, cl, app, serviceName); err != nil {
@ -102,6 +104,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(context.Background(), 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)

View File

@ -7,11 +7,11 @@ import (
) )
var appNewDescription = ` var appNewDescription = `
This command takes a recipe and uses it to create a new app. This new app Take a recipe and uses it to create a new app. This new app configuration is
configuration is stored in your ~/.abra directory under the appropriate server. 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".
@ -36,12 +36,11 @@ var appNewCommand = cli.Command{
internal.NoInputFlag, internal.NoInputFlag,
internal.NewAppServerFlag, internal.NewAppServerFlag,
internal.DomainFlag, internal.DomainFlag,
internal.NewAppNameFlag,
internal.PassFlag, internal.PassFlag,
internal.SecretsFlag, internal.SecretsFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
ArgsUsage: "<recipe>", ArgsUsage: "[<recipe>]",
Action: internal.NewAction, Action: internal.NewAction,
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
} }

View File

@ -15,7 +15,6 @@ import (
"github.com/buger/goterm" "github.com/buger/goterm"
dockerFormatter "github.com/docker/cli/cli/command/formatter" dockerFormatter "github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
@ -25,11 +24,11 @@ var appPsCommand = cli.Command{
Name: "ps", Name: "ps",
Aliases: []string{"p"}, Aliases: []string{"p"},
Usage: "Check app status", Usage: "Check app status",
Description: "This command shows a more detailed status output of a specific deployed app.", ArgsUsage: "<domain>",
Description: "Show a more detailed status output of a specific deployed app",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.WatchFlag, internal.WatchFlag,
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
@ -67,8 +66,10 @@ var appPsCommand = cli.Command{
// showPSOutput renders ps output. // showPSOutput renders ps output.
func showPSOutput(c *cli.Context, app config.App, cl *dockerClient.Client) { func showPSOutput(c *cli.Context, app config.App, cl *dockerClient.Client) {
filters := filters.NewArgs() filters, err := app.Filters(true, true)
filters.Add("name", app.StackName()) if err != nil {
logrus.Fatal(err)
}
containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters}) containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters})
if err != nil { if err != nil {

View File

@ -11,7 +11,6 @@ import (
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -21,14 +20,15 @@ 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",
Destination: &Volumes, Destination: &Volumes,
} }
var appRemoveCommand = cli.Command{ var appRemoveCommand = cli.Command{
Name: "remove", Name: "remove",
Aliases: []string{"rm"}, Aliases: []string{"rm"},
Usage: "Remove an already undeployed app", ArgsUsage: "<domain>",
Usage: "Remove an already undeployed app",
Flags: []cli.Flag{ Flags: []cli.Flag{
VolumesFlag, VolumesFlag,
internal.ForceFlag, internal.ForceFlag,
@ -39,7 +39,7 @@ var appRemoveCommand = cli.Command{
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 remove %s, are you sure?", app.Name), Message: fmt.Sprintf("about to remove %s, are you sure?", app.Name),
@ -62,11 +62,14 @@ var appRemoveCommand = cli.Command{
logrus.Fatal(err) logrus.Fatal(err)
} }
if isDeployed { if isDeployed {
logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s \" or pass --force", app.Name, app.Name) logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name)
}
fs, err := app.Filters(false, false)
if err != nil {
logrus.Fatal(err)
} }
fs := filters.NewArgs()
fs.Add("name", app.StackName())
secretList, err := cl.SecretList(context.Background(), 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)
@ -83,7 +86,7 @@ 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",
@ -96,6 +99,10 @@ var appRemoveCommand = cli.Command{
} }
} }
if internal.Force || internal.NoInput {
secretNamesToRemove = secretNames
}
for _, name := range secretNamesToRemove { for _, name := range secretNamesToRemove {
err := cl.SecretRemove(context.Background(), secrets[name]) err := cl.SecretRemove(context.Background(), secrets[name])
if err != nil { if err != nil {
@ -107,6 +114,11 @@ var appRemoveCommand = cli.Command{
logrus.Info("no secrets to remove") logrus.Info("no secrets to remove")
} }
fs, err = app.Filters(false, true)
if err != nil {
logrus.Fatal(err)
}
volumeListOKBody, err := cl.VolumeList(context.Background(), fs) volumeListOKBody, err := cl.VolumeList(context.Background(), fs)
volumeList := volumeListOKBody.Volumes volumeList := volumeListOKBody.Volumes
if err != nil { if err != nil {
@ -121,7 +133,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",
@ -133,6 +145,7 @@ var appRemoveCommand = cli.Command{
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
for _, vol := range removeVols { for _, vol := range removeVols {
err := cl.VolumeRemove(context.Background(), 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 {

View File

@ -18,10 +18,9 @@ var appRestartCommand = cli.Command{
Name: "restart", Name: "restart",
Aliases: []string{"re"}, Aliases: []string{"re"},
Usage: "Restart an app", Usage: "Restart an app",
ArgsUsage: "<service>", ArgsUsage: "<domain>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Description: `This command restarts a service within a deployed app.`, Description: `This command restarts a service within a deployed app.`,

201
cli/app/restore.go Normal file
View File

@ -0,0 +1,201 @@
package app
import (
"context"
"errors"
"fmt"
"os"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/archive"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
type restoreConfig struct {
preHookCmd string
postHookCmd string
}
var appRestoreCommand = cli.Command{
Name: "restore",
Aliases: []string{"rs"},
Usage: "Run app restore",
ArgsUsage: "<domain> <service> <file>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Description: `
Run an app restore.
Pre/post hook commands are defined in the recipe configuration. Abra reads this
configuration and run the comands in the context of the service before
restoring the backup.
Unlike "abra app backup", restore must be run on a per-service basis. You can
not restore all services in one go. Backup files produced by Abra are
compressed archives which use absolute paths. This allows Abra to restore
according to standard tar command logic.
Example:
abra app restore example.com app ~/.abra/backups/example_com_app_609341138.tar.gz
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
serviceName := c.Args().Get(1)
if serviceName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("missing <service>?"))
}
backupPath := c.Args().Get(2)
if backupPath == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("missing <file>?"))
}
if _, err := os.Stat(backupPath); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("%s doesn't exist?", backupPath)
}
}
recipe, err := recipe.Get(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
restoreConfigs := make(map[string]restoreConfig)
for _, service := range recipe.Config.Services {
if restoreEnabled, ok := service.Deploy.Labels["backupbot.restore"]; ok {
if restoreEnabled == "true" {
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), service.Name)
rsConfig := restoreConfig{}
logrus.Debugf("restore config detected for %s", fullServiceName)
if preHookCmd, ok := service.Deploy.Labels["backupbot.restore.pre-hook"]; ok {
logrus.Debugf("detected pre-hook command for %s: %s", fullServiceName, preHookCmd)
rsConfig.preHookCmd = preHookCmd
}
if postHookCmd, ok := service.Deploy.Labels["backupbot.restore.post-hook"]; ok {
logrus.Debugf("detected post-hook command for %s: %s", fullServiceName, postHookCmd)
rsConfig.postHookCmd = postHookCmd
}
restoreConfigs[service.Name] = rsConfig
}
}
}
rsConfig, ok := restoreConfigs[serviceName]
if !ok {
rsConfig = restoreConfig{}
}
if err := runRestore(app, backupPath, serviceName, rsConfig); err != nil {
logrus.Fatal(err)
}
return nil
},
}
// runRestore does the actual restore logic.
func runRestore(app config.App, backupPath, serviceName string, rsConfig restoreConfig) error {
cl, err := client.New(app.Server)
if err != nil {
return err
}
// FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli()
if err != nil {
return err
}
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, true)
if err != nil {
return err
}
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
if rsConfig.preHookCmd != "" {
splitCmd := internal.SafeSplit(rsConfig.preHookCmd)
logrus.Debugf("split pre-hook command for %s into %s", fullServiceName, splitCmd)
preHookExecOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: splitCmd,
Detach: false,
Tty: true,
}
if err := container.RunExec(dcli, cl, targetContainer.ID, &preHookExecOpts); err != nil {
return err
}
logrus.Infof("succesfully ran %s pre-hook command: %s", fullServiceName, rsConfig.preHookCmd)
}
backupReader, err := os.Open(backupPath)
if err != nil {
return err
}
content, err := archive.DecompressStream(backupReader)
if err != nil {
return err
}
// we use absolute paths so tar knows what to do. it will restore files
// according to the paths set in the compresed archive
restorePath := "/"
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(context.Background(), targetContainer.ID, restorePath, content, copyOpts); err != nil {
return err
}
logrus.Infof("restored %s to %s", backupPath, fullServiceName)
if rsConfig.postHookCmd != "" {
splitCmd := internal.SafeSplit(rsConfig.postHookCmd)
logrus.Debugf("split post-hook command for %s into %s", fullServiceName, splitCmd)
postHookExecOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: splitCmd,
Detach: false,
Tty: true,
}
if err := container.RunExec(dcli, cl, targetContainer.ID, &postHookExecOpts); err != nil {
return err
}
logrus.Infof("succesfully ran %s post-hook command: %s", fullServiceName, rsConfig.postHookCmd)
}
return nil
}

View File

@ -22,12 +22,13 @@ var appRollbackCommand = cli.Command{
Name: "rollback", Name: "rollback",
Aliases: []string{"rl"}, Aliases: []string{"rl"},
Usage: "Roll an app back to a previous version", Usage: "Roll an app back to a previous version",
ArgsUsage: "<app>", ArgsUsage: "<domain>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
internal.ForceFlag, internal.ForceFlag,
internal.ChaosFlag, internal.ChaosFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag, internal.DontWaitConvergeFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
@ -50,12 +51,12 @@ recipes.
stackName := app.StackName() stackName := app.StackName()
if !internal.Chaos { if !internal.Chaos {
if err := recipe.EnsureUpToDate(app.Type); err != nil { if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err) 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)
} }
@ -85,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
@ -125,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 {
@ -140,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)
} }
} }
@ -148,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)
@ -163,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

@ -19,14 +19,14 @@ import (
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",
Destination: &noTTY, Destination: &noTTY,
} }
@ -35,12 +35,11 @@ var appRunCommand = cli.Command{
Aliases: []string{"r"}, Aliases: []string{"r"},
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag,
noTTYFlag, noTTYFlag,
userFlag, userFlag,
}, },
Before: internal.SubCommandBefore, 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 {
@ -60,11 +59,11 @@ var appRunCommand = cli.Command{
} }
serviceName := c.Args().Get(1) serviceName := c.Args().Get(1)
stackAndServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName) stackAndServiceName := fmt.Sprintf("^%s_%s", app.StackName(), serviceName)
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", stackAndServiceName) filters.Add("name", stackAndServiceName)
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, true) targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, false)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -10,10 +10,11 @@ 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" dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -25,15 +26,22 @@ var allSecretsFlag = &cli.BoolFlag{
Usage: "Generate all secrets", Usage: "Generate all secrets",
} }
var rmAllSecrets bool
var rmAllSecretsFlag = &cli.BoolFlag{
Name: "all, a",
Destination: &rmAllSecrets,
Usage: "Remove all secrets",
}
var appSecretGenerateCommand = cli.Command{ 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{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, allSecretsFlag,
allSecretsFlag, internal.PassFlag, internal.PassFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
@ -62,8 +70,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)
} }
@ -76,7 +86,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)
} }
} }
@ -105,11 +115,10 @@ var appSecretInsertCommand = cli.Command{
Usage: "Insert secret", Usage: "Insert secret",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag,
internal.PassFlag, internal.PassFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
ArgsUsage: "<app> <secret-name> <version> <data>", ArgsUsage: "<domain> <secret-name> <version> <data>",
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Description: ` Description: `
This command inserts a secret into an app environment. This command inserts a secret into an app environment.
@ -139,8 +148,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)
} }
} }
@ -149,6 +160,25 @@ Example:
}, },
} }
// secretRm removes a secret.
func secretRm(cl *dockerClient.Client, app config.App, secretName, parsed string) error {
if err := cl.SecretRemove(context.Background(), secretName); err != nil {
return err
}
logrus.Infof("deleted %s successfully from server", secretName)
if internal.PassRemove {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
return err
}
logrus.Infof("deleted %s successfully from local pass store", secretName)
}
return nil
}
var appSecretRmCommand = cli.Command{ var appSecretRmCommand = cli.Command{
Name: "remove", Name: "remove",
Aliases: []string{"rm"}, Aliases: []string{"rm"},
@ -156,27 +186,28 @@ var appSecretRmCommand = cli.Command{
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
allSecretsFlag, internal.PassFlag, rmAllSecretsFlag,
internal.PassRemoveFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
ArgsUsage: "<app> <secret-name>", ArgsUsage: "<domain> [<secret-name>]",
BashComplete: autocomplete.AppNameComplete, 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?"))
} }
@ -185,49 +216,59 @@ Example:
logrus.Fatal(err) logrus.Fatal(err)
} }
filters := filters.NewArgs() filters, err := app.Filters(false, false)
filters.Add("name", app.StackName()) if err != nil {
logrus.Fatal(err)
}
secretList, err := cl.SecretList(context.Background(), 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(context.Background(), secretName); err != nil {
logrus.Fatal(err)
}
logrus.Infof("deleted %s successfully from server", secretName)
if internal.Pass { match := false
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil { secretToRm := c.Args().Get(1)
logrus.Fatal(err) for sec := range secrets {
} secretName := secret.ParseSecretEnvVarName(sec)
logrus.Infof("deleted %s successfully from local pass store", secretName) secVal, err := secret.ParseSecretEnvVarValue(secrets[sec])
} if err != nil {
} else { logrus.Fatal(err)
if parsed == secretToRm { }
if err := cl.SecretRemove(context.Background(), secretName); err != nil {
logrus.Fatal(err)
}
logrus.Infof("deleted %s successfully from server", secretName) secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, secVal.Version)
if _, ok := remoteSecretNames[secretRemoteName]; ok {
if internal.Pass { if secretToRm != "" {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil { if secretName == secretToRm {
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Infof("deleted %s successfully from local pass store", secretName) 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
}, },
} }
@ -237,7 +278,6 @@ var appSecretLsCommand = cli.Command{
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Usage: "List all secrets", Usage: "List all secrets",
@ -253,8 +293,11 @@ var appSecretLsCommand = cli.Command{
logrus.Fatal(err) logrus.Fatal(err)
} }
filters := filters.NewArgs() filters, err := app.Filters(false, false)
filters.Add("name", app.StackName()) if err != nil {
logrus.Fatal(err)
}
secretList, err := cl.SecretList(context.Background(), 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)
@ -295,7 +338,7 @@ 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,

View File

@ -12,8 +12,9 @@ import (
) )
var appUndeployCommand = cli.Command{ var appUndeployCommand = cli.Command{
Name: "undeploy", Name: "undeploy",
Aliases: []string{"un"}, Aliases: []string{"un"},
ArgsUsage: "<domain>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,

View File

@ -21,21 +21,22 @@ 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.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
internal.ForceFlag, internal.ForceFlag,
internal.ChaosFlag, internal.ChaosFlag,
internal.NoDomainChecksFlag, internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Description: ` Description: `
This command supports upgrading an app. You can use it to choose and roll out a Upgrade an app. You can use it to choose and roll out a new upgrade to an
new upgrade to an existing app. 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
@ -53,12 +54,12 @@ recipes.
stackName := app.StackName() stackName := app.StackName()
if !internal.Chaos { if !internal.Chaos {
if err := recipe.EnsureUpToDate(app.Type); err != nil { if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err) 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)
} }
@ -88,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)
} }
@ -128,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 {
@ -145,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)
} }
} }
@ -159,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)
@ -174,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

@ -31,8 +31,9 @@ func getImagePath(image string) (string, error) {
} }
var appVersionCommand = cli.Command{ var appVersionCommand = cli.Command{
Name: "version", Name: "version",
Aliases: []string{"v"}, Aliases: []string{"v"},
ArgsUsage: "<domain>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
@ -40,9 +41,9 @@ var appVersionCommand = cli.Command{
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Usage: "Show app versions", Usage: "Show app versions",
Description: ` Description: `
This command shows all information about versioning related to a deployed app. Show all information about versioning related to a deployed app. This includes
This includes the individual image names, tags and digests. But also the Co-op the individual image names, tags and digests. But also the Co-op Cloud recipe
Cloud recipe version. version.
`, `,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
@ -68,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

@ -13,8 +13,9 @@ import (
) )
var appVolumeListCommand = cli.Command{ var appVolumeListCommand = cli.Command{
Name: "list", Name: "list",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
ArgsUsage: "<domain>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
@ -25,18 +26,20 @@ var appVolumeListCommand = cli.Command{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
volumeList, err := client.GetVolumes(context.Background(), app.Server, app.Name) filters, err := app.Filters(false, true)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
table := formatter.CreateTable([]string{"driver", "volume name"}) volumeList, err := client.GetVolumes(context.Background(), app.Server, filters)
if err != nil {
logrus.Fatal(err)
}
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)
} }
@ -58,15 +61,15 @@ var appVolumeRemoveCommand = cli.Command{
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.DebugFlag,
@ -77,14 +80,19 @@ Passing "--force" will select all volumes for removal. Be careful.
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
volumeList, err := client.GetVolumes(context.Background(), app.Server, app.Name) filters, err := app.Filters(false, true)
if err != nil {
logrus.Fatal(err)
}
volumeList, err := client.GetVolumes(context.Background(), app.Server, filters)
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",
@ -95,7 +103,9 @@ 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
} }
@ -115,7 +125,7 @@ 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

@ -20,40 +20,42 @@ import (
// 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,
"beta.coopcloud.tech": true, "beta.coopcloud.tech": true,
"comrade-renovate-bot": true, "comrade-renovate-bot": true,
"coopcloud.tech": true, "coopcloud.tech": true,
"coturn": true, "coturn": true,
"docker-cp-deploy": true, "docker-cp-deploy": true,
"docker-dind-bats-kcov": true, "docker-dind-bats-kcov": true,
"docs.coopcloud.tech": true, "docs.coopcloud.tech": true,
"drone-abra": true, "drone-abra": true,
"example": true, "example": true,
"gardening": true, "gardening": true,
"go-abra": true, "go-abra": true,
"organising": true, "organising": true,
"outline-with-patch": true, "outline-with-patch": true,
"pyabra": true, "pyabra": true,
"radicle-seed-node": true, "radicle-seed-node": true,
"recipes": true, "recipes-catalogue-json": true,
"stack-ssh-deploy": true, "recipes-wishlist": true,
"swarm-cronjob": true, "recipes.coopcloud.tech": true,
"tagcmp": true, "stack-ssh-deploy": true,
"traefik-cert-dumper": true, "swarm-cronjob": true,
"tyop": true, "tagcmp": true,
"traefik-cert-dumper": true,
"tyop": true,
} }
var catalogueGenerateCommand = cli.Command{ var catalogueGenerateCommand = cli.Command{
@ -66,18 +68,17 @@ var catalogueGenerateCommand = cli.Command{
internal.PublishFlag, internal.PublishFlag,
internal.DryFlag, internal.DryFlag,
internal.SkipUpdatesFlag, internal.SkipUpdatesFlag,
internal.RegistryUsernameFlag,
internal.RegistryPasswordFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Description: ` Description: `
This command generates a new copy of the recipe catalogue which can be found on: Generate a new copy of the recipe catalogue which can be found on:
https://recipes.coopcloud.tech https://recipes.coopcloud.tech (website that humans read)
https://recipes.coopcloud.tech/recipes.json (JSON that Abra reads)
It polls the entire git.coopcloud.tech/coop-cloud/... recipe repository It polls the entire git.coopcloud.tech/coop-cloud/... recipe repository
listing, parses README.md and git tags of those repositories to produce recipe listing, parses README.md and git tags to produce recipe metadata which is
metadata and produces a recipes JSON file. loaded into the catalogue JSON file.
It is possible to generate new metadata for a single recipe by passing It is possible to generate new metadata for a single recipe by passing
<recipe>. The existing local catalogue will be updated, not overwritten. <recipe>. The existing local catalogue will be updated, not overwritten.
@ -94,7 +95,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)
} }
repos, err := recipe.ReadReposMetadata() repos, err := recipe.ReadReposMetadata()
@ -132,13 +133,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)
@ -215,7 +212,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)
} }
@ -236,7 +233,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)
} }

View File

@ -14,6 +14,7 @@ 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"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -26,25 +27,17 @@ var AutoCompleteCommand = cli.Command{
Aliases: []string{"ac"}, Aliases: []string{"ac"},
Usage: "Configure shell autocompletion (recommended)", Usage: "Configure shell autocompletion (recommended)",
Description: ` Description: `
This command helps set up autocompletion in your shell by downloading the Set up auto-completion in your shell by downloading the relevant files and
relevant autocompletion files and laying out what additional information must laying out what additional information must be loaded. Supported shells are as
be loaded. follows: bash, fish, fizsh & zsh.
Example: Example:
abra autocomplete bash abra autocomplete bash
Supported shells are as follows:
fizsh
zsh
bash
`, `,
ArgsUsage: "<shell>", ArgsUsage: "<shell>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag,
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
shellType := c.Args().First() shellType := c.Args().First()
@ -57,6 +50,7 @@ Supported shells are as follows:
"bash": true, "bash": true,
"zsh": true, "zsh": true,
"fizsh": true, "fizsh": true,
"fish": true,
} }
if _, ok := supportedShells[shellType]; !ok { if _, ok := supportedShells[shellType]; !ok {
@ -87,19 +81,27 @@ Supported shells are as follows:
switch shellType { switch shellType {
case "bash": case "bash":
fmt.Println(fmt.Sprintf(` fmt.Println(fmt.Sprintf(`
# Run the following commands to install autocompletion # Run the following commands to install auto-completion
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(`
# Run the following commands to install autocompletion # Run the following commands to install auto-completion
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))
case "fish":
fmt.Println(fmt.Sprintf(`
# Run the following commands to install auto-completion
sudo mkdir -p /etc/fish/completions
sudo cp %s /etc/fish/completions/abra
echo "source /etc/fish/completions/abra" >> ~/.config/fish/config.fish
# And finally run "abra app ps <hit tab key>" to test things are working, you should see app domains listed!
`, autocompletionFile)) `, autocompletionFile))
} }
@ -113,13 +115,11 @@ var UpgradeCommand = cli.Command{
Aliases: []string{"u"}, Aliases: []string{"u"},
Usage: "Upgrade Abra itself", Usage: "Upgrade Abra itself",
Description: ` Description: `
This command allows you to upgrade Abra in-place with the latest stable or 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 Pass "-r/--rc" to install the latest release candidate. Please bear in mind
"--rc" option. Please bear in mind that the latest release candidate may have that it may contain catastrophic bugs. Thank you very much for the testing
some catastrophic bugs contained in it. In any case, thank you very much for efforts!
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 {
@ -162,16 +162,7 @@ func newAbraApp(version, commit string) *cli.App {
UpgradeCommand, UpgradeCommand,
AutoCompleteCommand, AutoCompleteCommand,
}, },
Authors: []cli.Author{ BashComplete: autocomplete.SubcommandComplete,
// 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
@ -182,6 +173,7 @@ func newAbraApp(version, commit string) *cli.App {
path.Join(config.SERVERS_DIR), path.Join(config.SERVERS_DIR),
path.Join(config.RECIPES_DIR), path.Join(config.RECIPES_DIR),
path.Join(config.VENDOR_DIR), path.Join(config.VENDOR_DIR),
path.Join(config.BACKUP_DIR),
} }
for _, path := range paths { for _, path := range paths {

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

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

View File

@ -13,7 +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, ss", Name: "secrets, S",
Usage: "Automatically generate secrets", Usage: "Automatically generate secrets",
Destination: &Secrets, Destination: &Secrets,
} }
@ -28,14 +28,14 @@ var PassFlag = &cli.BoolFlag{
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, c", Name: "pass, p",
Value: "", Usage: "Remove generated secrets from a local pass store",
Destination: &Context, Destination: &PassRemove,
} }
// Force force functionality without asking. // Force force functionality without asking.
@ -53,7 +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, ch", Name: "chaos, C",
Usage: "Deploy uncommitted recipes changes. Use with care!", Usage: "Deploy uncommitted recipes changes. Use with care!",
Destination: &Chaos, Destination: &Chaos,
} }
@ -79,7 +79,7 @@ var NoInputFlag = &cli.BoolFlag{
var DNSType string var DNSType string
var DNSTypeFlag = &cli.StringFlag{ var DNSTypeFlag = &cli.StringFlag{
Name: "type, t", Name: "record-type, rt",
Value: "", Value: "",
Usage: "Domain name record type (e.g. A)", Usage: "Domain name record type (e.g. A)",
Destination: &DNSType, Destination: &DNSType,
@ -88,7 +88,7 @@ var DNSTypeFlag = &cli.StringFlag{
var DNSName string var DNSName string
var DNSNameFlag = &cli.StringFlag{ var DNSNameFlag = &cli.StringFlag{
Name: "name, n", Name: "record-name, rn",
Value: "", Value: "",
Usage: "Domain name record name (e.g. mysubdomain)", Usage: "Domain name record name (e.g. mysubdomain)",
Destination: &DNSName, Destination: &DNSName,
@ -97,7 +97,7 @@ var DNSNameFlag = &cli.StringFlag{
var DNSValue string var DNSValue string
var DNSValueFlag = &cli.StringFlag{ var DNSValueFlag = &cli.StringFlag{
Name: "value, v", Name: "record-value, rv",
Value: "", Value: "",
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,
@ -105,7 +105,7 @@ var DNSValueFlag = &cli.StringFlag{
var DNSTTL string var DNSTTL string
var DNSTTLFlag = &cli.StringFlag{ var DNSTTLFlag = &cli.StringFlag{
Name: "ttl, T", Name: "record-ttl, rl",
Value: "600s", Value: "600s",
Usage: "Domain name TTL value (seconds)", Usage: "Domain name TTL value (seconds)",
Destination: &DNSTTL, Destination: &DNSTTL,
@ -114,7 +114,7 @@ var DNSTTLFlag = &cli.StringFlag{
var DNSPriority int var DNSPriority int
var DNSPriorityFlag = &cli.IntFlag{ var DNSPriorityFlag = &cli.IntFlag{
Name: "priority, P", Name: "record-priority, rp",
Value: 10, Value: 10,
Usage: "Domain name priority value", Usage: "Domain name priority value",
Destination: &DNSPriority, Destination: &DNSPriority,
@ -248,35 +248,35 @@ 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",
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, ma, x", Name: "major, x",
Usage: "Increase the major part of the version", Usage: "Increase the major part of the version",
Destination: &Major, Destination: &Major,
} }
var Minor bool var Minor bool
var MinorFlag = &cli.BoolFlag{ var MinorFlag = &cli.BoolFlag{
Name: "minor, mi, y", Name: "minor, y",
Usage: "Increase the minor part of the version", Usage: "Increase the minor part of the version",
Destination: &Minor, Destination: &Minor,
} }
var Patch bool var Patch bool
var PatchFlag = &cli.BoolFlag{ var PatchFlag = &cli.BoolFlag{
Name: "patch, pa, z", Name: "patch, z",
Usage: "Increase the patch part of the version", Usage: "Increase the patch part of the version",
Destination: &Patch, Destination: &Patch,
} }
var Dry bool var Dry bool
var DryFlag = &cli.BoolFlag{ var DryFlag = &cli.BoolFlag{
Name: "dry-run, dr", Name: "dry-run, r",
Usage: "Only reports changes that would be made", Usage: "Only reports changes that would be made",
Destination: &Dry, Destination: &Dry,
} }
@ -290,7 +290,7 @@ var PublishFlag = &cli.BoolFlag{
var Domain string var Domain string
var DomainFlag = &cli.StringFlag{ var DomainFlag = &cli.StringFlag{
Name: "domain, dn", Name: "domain, D",
Value: "", Value: "",
Usage: "Choose a domain name", Usage: "Choose a domain name",
Destination: &Domain, Destination: &Domain,
@ -304,17 +304,9 @@ var NewAppServerFlag = &cli.StringFlag{
Destination: &NewAppServer, Destination: &NewAppServer,
} }
var NewAppName string
var NewAppNameFlag = &cli.StringFlag{
Name: "app-name, 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, nd", Name: "no-domain-checks, D",
Usage: "Disable app domain sanity checks", Usage: "Disable app domain sanity checks",
Destination: &NoDomainChecks, Destination: &NoDomainChecks,
} }
@ -328,7 +320,7 @@ var StdErrOnlyFlag = &cli.BoolFlag{
var DontWaitConverge bool var DontWaitConverge bool
var DontWaitConvergeFlag = &cli.BoolFlag{ var DontWaitConvergeFlag = &cli.BoolFlag{
Name: "no-converge-checks, nc", Name: "no-converge-checks, c",
Usage: "Don't wait for converge logic checks", Usage: "Don't wait for converge logic checks",
Destination: &DontWaitConverge, Destination: &DontWaitConverge,
} }
@ -354,24 +346,6 @@ var SkipUpdatesFlag = &cli.BoolFlag{
Destination: &SkipUpdates, Destination: &SkipUpdates,
} }
var RegistryUsername string
var RegistryUsernameFlag = &cli.StringFlag{
Name: "username, user",
Value: "",
Usage: "Registry username",
EnvVar: "REGISTRY_USERNAME",
Destination: &RegistryUsername,
}
var RegistryPassword string
var RegistryPasswordFlag = &cli.StringFlag{
Name: "password, pass",
Value: "",
Usage: "Registry password",
EnvVar: "REGISTRY_PASSWORD",
Destination: &RegistryUsername,
}
var AllTags bool var AllTags bool
var AllTagsFlag = &cli.BoolFlag{ var AllTagsFlag = &cli.BoolFlag{
Name: "all-tags, a", Name: "all-tags, a",
@ -379,6 +353,21 @@ var AllTagsFlag = &cli.BoolFlag{
Destination: &AllTags, Destination: &AllTags,
} }
var LocalCmd bool
var LocalCmdFlag = &cli.BoolFlag{
Name: "local, l",
Usage: "Run command locally",
Destination: &LocalCmd,
}
var RemoteUser string
var RemoteUserFlag = &cli.StringFlag{
Name: "user, u",
Value: "",
Usage: "User to run command within a service context",
Destination: &RemoteUser,
}
// SSHFailMsg is a hopefully helpful SSH failure message // SSHFailMsg is a hopefully helpful SSH failure message
var SSHFailMsg = ` var SSHFailMsg = `
Woops, Abra is unable to connect to connect to %s. Woops, Abra is unable to connect to connect to %s.
@ -428,6 +417,24 @@ 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). // SubCommandBefore wires up pre-action machinery (e.g. --debug handling).
func SubCommandBefore(c *cli.Context) error { func SubCommandBefore(c *cli.Context) error {
if Debug { if Debug {

View File

@ -26,12 +26,12 @@ func DeployAction(c *cli.Context) error {
app := ValidateApp(c) app := ValidateApp(c)
if !Chaos { if !Chaos {
if err := recipe.EnsureUpToDate(app.Type); err != nil { if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err) 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)
} }
@ -66,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)
} }
} }
@ -91,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)
} }
} }
@ -105,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)
@ -120,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)
} }
@ -141,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)
} }
@ -162,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"
@ -175,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 {
@ -200,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"
@ -213,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,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,6 +12,7 @@ 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" "github.com/urfave/cli"
) )
@ -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", NewAppName)) 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 { sanitisedAppName := config.SanitiseAppName(Domain)
logrus.Debugf("%s sanitised as %s for new app", Domain, sanitisedAppName)
if err := config.TemplateAppEnvSample(recipe.Name, Domain, NewAppServer, Domain); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
sanitisedAppName := config.SanitiseAppName(NewAppName) if err := promptForSecrets(Domain); err != nil {
if len(sanitisedAppName) > 45 {
logrus.Fatalf("%s cannot be longer than 45 characters", sanitisedAppName)
}
logrus.Debugf("%s sanitised as %s for new app", NewAppName, sanitisedAppName)
if err := config.TemplateAppEnvSample(recipe.Name, NewAppName, NewAppServer, Domain); 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{"server", "type", "domain", "app name"} tableCol := []string{"server", "recipe", "domain"}
table := formatter.CreateTable(tableCol) table := formatter.CreateTable(tableCol)
table.Append([]string{NewAppServer, recipe.Name, Domain, NewAppName}) 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", NewAppName)) 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", NewAppName)) 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

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

View File

@ -19,7 +19,7 @@ import (
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,7 +167,7 @@ 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
@ -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.

View File

@ -19,13 +19,12 @@ var recipeLintCommand = cli.Command{
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag,
internal.OnlyErrorFlag, internal.OnlyErrorFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c) 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

@ -27,7 +27,6 @@ var recipeListCommand = cli.Command{
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag,
patternFlag, patternFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,

View File

@ -41,9 +41,9 @@ var recipeNewCommand = cli.Command{
Usage: "Create a new recipe", Usage: "Create a new recipe",
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Description: ` Description: `
This command creates a new recipe. Create a new recipe.
Abra uses our built-in example repository which is available here: Abra uses the built-in example repository which is available here:
https://git.coopcloud.tech/coop-cloud/example https://git.coopcloud.tech/coop-cloud/example
@ -111,7 +111,7 @@ In order to share your recipe, you can upload it the git repository to:
If you're not sure how to do that, come chat with us: If you're not sure how to do that, come chat with us:
https://docs.coopcloud.tech/contact https://docs.coopcloud.tech/intro/contact
See "abra recipe -h" for additional recipe maintainer commands. See "abra recipe -h" for additional recipe maintainer commands.

View File

@ -13,7 +13,8 @@ var RecipeCommand = cli.Command{
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
Cloud community and you can use Abra to read them and create apps for you. Cloud community and you can use Abra to read them, deploy them and create apps
for you.
Anyone who uses a recipe can become a maintainer. Maintainers typically make Anyone who uses a recipe can become a maintainer. Maintainers typically make
sure the recipe is in good working order and the config upgraded in a timely sure the recipe is in good working order and the config upgraded in a timely

View File

@ -27,17 +27,16 @@ var recipeReleaseCommand = cli.Command{
Usage: "Release a new recipe version", 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 Create a new version of a recipe. These versions are then published on the
then published on the Co-op Cloud recipe catalogue. These versions take the Co-op Cloud recipe catalogue. These versions take the following form:
following form:
a.b.c+x.y.z a.b.c+x.y.z
Where the "a.b.c" part is a semantic version determined by the maintainer. And Where the "a.b.c" part is a semantic version determined by the maintainer. The
the "x.y.z" part is the image tag of the recipe "app" service (the main "x.y.z" part is the image tag of the recipe "app" service (the main container
container which contains the software to be used). which contains the software to be used, by naming convention).
We maintain a semantic versioning scheme ("a.b.c") alongside the libre app We maintain a semantic versioning scheme ("a.b.c") alongside the recipe
versioning scheme ("x.y.z") in order to maximise the chances that the nature of versioning scheme ("x.y.z") in order to maximise the chances that the nature of
recipe updates are properly communicated. I.e. developers of an app might recipe updates are properly communicated. I.e. developers of an app might
publish a minor version but that might lead to changes in the recipe which are publish a minor version but that might lead to changes in the recipe which are
@ -59,7 +58,7 @@ your SSH keys configured on your account.
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeWithPrompt(c) recipe := internal.ValidateRecipeWithPrompt(c, false)
imagesTmp, err := getImageVersions(recipe) imagesTmp, err := getImageVersions(recipe)
if err != nil { if err != nil {
@ -322,12 +321,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 {
@ -368,6 +361,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()
@ -393,15 +392,15 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
} }
if err := commitRelease(recipe, tagString); err != nil { if err := commitRelease(recipe, tagString); err != nil {
logrus.Fatal(err) logrus.Fatalf("failed to commit changes: %s", err.Error())
} }
if err := tagRelease(tagString, repo); err != nil { if err := tagRelease(tagString, repo); err != nil {
logrus.Fatal(err) logrus.Fatalf("failed to tag release: %s", err.Error())
} }
if err := pushRelease(recipe, tagString); err != nil { if err := pushRelease(recipe, tagString); err != nil {
logrus.Fatal(err) logrus.Fatalf("failed to publish new release: %s", err.Error())
} }
return nil return nil

View File

@ -31,8 +31,8 @@ var recipeSyncCommand = cli.Command{
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Description: ` Description: `
This command will generate labels for the main recipe service (i.e. by Generate labels for the main recipe service (i.e. by convention, the service
convention, the service named 'app') which corresponds to the following format: named "app") which corresponds to the following format:
coop-cloud.${STACK_NAME}.version=<version> coop-cloud.${STACK_NAME}.version=<version>
@ -41,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 {
@ -95,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

@ -31,9 +31,9 @@ var recipeUpgradeCommand = cli.Command{
Aliases: []string{"u"}, Aliases: []string{"u"},
Usage: "Upgrade recipe image tags", Usage: "Upgrade recipe image tags",
Description: ` Description: `
This command reads and attempts to parse all image tags within the given Parse all image tags within the given <recipe> configuration and prompt with
<recipe> configuration and prompt with more recent tags to upgrade to. It will more recent tags to upgrade to. It will update the relevant compose file tags
update the relevant compose file tags on the local file system. on the local file system.
Some image tags cannot be parsed because they do not follow some sort of Some image tags cannot be parsed because they do not follow some sort of
semver-like convention. In this case, all possible tags will be listed and it semver-like convention. In this case, all possible tags will be listed and it
@ -46,7 +46,6 @@ 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>",
@ -60,7 +59,11 @@ You may invoke this command in "wizard" mode and be prompted for input:
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeWithPrompt(c) recipe := internal.ValidateRecipeWithPrompt(c, true)
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err)
}
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 {
@ -113,13 +116,13 @@ 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)
image := reference.Path(img)
logrus.Debugf("retrieved %s from remote registry for %s", regVersions, image)
image = formatter.StripTagMeta(image) image = formatter.StripTagMeta(image)
switch img.(type) { switch img.(type) {
@ -142,7 +145,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
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
} }
@ -232,7 +235,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
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{"skip"} compatibleStrings = []string{"skip"}
for _, regVersion := range regVersions { for _, regVersion := range regVersions {
compatibleStrings = append(compatibleStrings, regVersion.Name) compatibleStrings = append(compatibleStrings, regVersion)
} }
} }

View File

@ -16,12 +16,11 @@ var recipeVersionCommand = cli.Command{
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c) recipe := internal.ValidateRecipe(c, false)
catalogue, err := recipePkg.ReadRecipeCatalogue() catalogue, err := recipePkg.ReadRecipeCatalogue()
if err != nil { if err != nil {

View File

@ -21,13 +21,12 @@ var RecordListCommand = cli.Command{
ArgsUsage: "<zone>", ArgsUsage: "<zone>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag,
internal.DNSProviderFlag, internal.DNSProviderFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Description: ` Description: `
This command lists all domain name records managed by a 3rd party provider for List all domain name records managed by a 3rd party provider for a specific
a specific zone. zone.
You must specify a zone (e.g. example.com) under which your domain name records You must specify a zone (e.g. example.com) under which your domain name records
are listed. This zone must already be created on your provider account. are listed. This zone must already be created on your provider account.

View File

@ -33,7 +33,7 @@ var RecordNewCommand = cli.Command{
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Description: ` Description: `
This command creates a new domain name record for a specific zone. Create a new domain name record for a specific zone.
You must specify a zone (e.g. example.com) under which your domain name records You must specify a zone (e.g. example.com) under which your domain name records
are listed. This zone must already be created on your provider account. are listed. This zone must already be created on your provider account.
@ -45,7 +45,6 @@ Example:
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 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)

View File

@ -11,9 +11,9 @@ var RecordCommand = cli.Command{
Aliases: []string{"rc"}, Aliases: []string{"rc"},
ArgsUsage: "<record>", ArgsUsage: "<record>",
Description: ` Description: `
This command supports managing domain name records via 3rd party providers such Manage domain name records via 3rd party providers such as Gandi DNS. It
as Gandi DNS. It supports listing, creating and removing all types of records supports listing, creating and removing all types of records that you might
that you might need for managing Co-op Cloud apps. need for managing Co-op Cloud apps.
The following providers are supported: The following providers are supported:
@ -28,7 +28,6 @@ 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,

View File

@ -30,7 +30,7 @@ var RecordRemoveCommand = cli.Command{
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Description: ` Description: `
This command removes a domain name record for a specific zone. Remove a domain name record for a specific zone.
It uses the type of record and name to match existing records and choose one It uses the type of record and name to match existing records and choose one
for deletion. You must specify a zone (e.g. example.com) under which your for deletion. You must specify a zone (e.g. example.com) under which your

View File

@ -28,8 +28,8 @@ import (
var ( var (
dockerInstallMsg = ` dockerInstallMsg = `
A docker installation cannot be found on %s. This is a required system A docker installation cannot be found on %s. This is a required system
dependency for running Co-op Cloud on your server. If you would like, Abra can dependency for running Co-op Cloud apps on your server. If you would like, Abra
attempt to install Docker for you using the upstream non-interactive can attempt to install Docker for you using the upstream non-interactive
installation script. installation script.
See the following documentation for more: See the following documentation for more:
@ -41,7 +41,6 @@ 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
` `
) )
@ -61,7 +60,7 @@ var provisionFlag = &cli.BoolFlag{
var sshAuth string var sshAuth string
var sshAuthFlag = &cli.StringFlag{ var sshAuthFlag = &cli.StringFlag{
Name: "ssh-auth, sh", Name: "ssh-auth, s",
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,
@ -69,7 +68,7 @@ var sshAuthFlag = &cli.StringFlag{
var askSudoPass bool var askSudoPass bool
var askSudoPassFlag = &cli.BoolFlag{ var askSudoPassFlag = &cli.BoolFlag{
Name: "ask-sudo-pass, as", Name: "ask-sudo-pass, a",
Usage: "Ask for sudo password", Usage: "Ask for sudo password",
Destination: &askSudoPass, Destination: &askSudoPass,
} }
@ -247,7 +246,7 @@ Abra was unable to bootstrap Docker, see below for logs:
%s %s
If nothing works, you try running the Docker install script manually on your server: If nothing works, you can try running the Docker install script manually on your server:
wget -O- https://get.docker.com | bash wget -O- https://get.docker.com | bash
@ -277,7 +276,7 @@ Abra was unable to bootstrap Docker, see below for logs:
%s %s
This could be due to a number of things but one of the most common is that your This could be due to several reasons. One of the most common is that your
server user account does not have sudo access, and if it does, you need to pass server user account does not have sudo access, and if it does, you need to pass
"--ask-sudo-pass" in order to supply Abra with your password. "--ask-sudo-pass" in order to supply Abra with your password.
@ -371,40 +370,28 @@ var serverAddCommand = cli.Command{
Aliases: []string{"a"}, Aliases: []string{"a"},
Usage: "Add a server to your configuration", Usage: "Add a server to your configuration",
Description: ` Description: `
This command adds a new server to your configuration so that it can be managed Add a new server to your configuration so that it can be managed by Abra. This
by Abra. This can be useful when you already have a server provisioned and want command can also provision your server ("--provision/-p") with a Docker
to start running Abra commands against it. 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 have working SSH access to this server already, Abra
Co-op Cloud config located on the server itself, and not on your local will assume port 22 and will use your current system username to make an
developer machine. initial 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 varia.zone glodemodem 12345
Abra will construct the following SSH connection and Docker context: Abra will construct the following SSH connection and Docker context:
@ -412,9 +399,10 @@ 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 install Docker and initialise swarm mode. 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
You may omit flags to avoid performing this provisioning logic. Co-op Cloud config located on the server itself, and not on your local
developer machine.
`, `,
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
@ -437,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)
@ -444,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()
@ -473,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)
@ -495,7 +483,7 @@ You may omit flags to avoid performing this provisioning logic.
if _, err := cl.Info(context.Background()); 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))
} }
return nil return nil

View File

@ -18,7 +18,6 @@ var serverListCommand = cli.Command{
Usage: "List managed servers", Usage: "List managed servers",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {

View File

@ -99,9 +99,10 @@ You can access this new VPS via SSH using the following command:
ssh root@%s ssh root@%s
Please note, this server is not managed by Abra yet (i.e. "abra server ls" will Please note, this server is not managed by Abra yet (i.e. "abra server ls" will
not list this server)! You will need to assign a domain name record ("abra not list this server)! You will need to assign a domain name record (manually
record new") and add the server to your Abra configuration ("abra server add") or by using "abra record new") and add the server to your Abra configuration
to have a working server that you can deploy Co-op Cloud apps to. ("abra server add") to have a working server that you can deploy Co-op Cloud
apps to.
When setting up domain name records, you probably want to set up the following When setting up domain name records, you probably want to set up the following
2 A records. This supports deploying apps to your root domain (e.g. 2 A records. This supports deploying apps to your root domain (e.g.
@ -110,7 +111,6 @@ bar.example.com).
@ 1800 IN A %s @ 1800 IN A %s
* 1800 IN A %s * 1800 IN A %s
`, `,
internal.HetznerCloudName, ip, rootPassword, internal.HetznerCloudName, ip, rootPassword,
ip, ip, ip, ip, ip, ip,
@ -181,9 +181,10 @@ address. You can learn all about how to get SSH access to your new Capsul on:
%s/about-ssh %s/about-ssh
Please note, this server is not managed by Abra yet (i.e. "abra server ls" will Please note, this server is not managed by Abra yet (i.e. "abra server ls" will
not list this server)! You will need to assign a domain name record ("abra not list this server)! You will need to assign a domain name record (manually
record new") and add the server to your Abra configuration ("abra server add") or by using "abra record new") and add the server to your Abra configuration
to have a working server that you can deploy Co-op Cloud apps to. ("abra server add") to have a working server that you can deploy Co-op Cloud
apps to.
When setting up domain name records, you probably want to set up the following When setting up domain name records, you probably want to set up the following
2 A records. This supports deploying apps to your root domain (e.g. 2 A records. This supports deploying apps to your root domain (e.g.
@ -192,7 +193,6 @@ bar.example.com).
@ 1800 IN A <your-capsul-ip> @ 1800 IN A <your-capsul-ip>
* 1800 IN A <your-capsul-ip> * 1800 IN A <your-capsul-ip>
`, internal.CapsulName, resp.ID, internal.CapsulInstanceURL)) `, internal.CapsulName, resp.ID, internal.CapsulInstanceURL))
return nil return nil
@ -203,7 +203,7 @@ var serverNewCommand = cli.Command{
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",
Description: ` Description: `
This command creates a new server via a 3rd party provider. Create a new server via a 3rd party provider.
The following providers are supported: The following providers are supported:
@ -217,16 +217,11 @@ You may invoke this command in "wizard" mode and be prompted for input:
API tokens are read from the environment if specified, e.g. API tokens are read from the environment if specified, e.g.
export HCLOUD_TOKEN=... export HCLOUD_TOKEN=...
Where "$provider_TOKEN" is the expected env var format.
`, `,
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
internal.ServerProviderFlag, internal.ServerProviderFlag,
internal.DebugFlag,
internal.NoInputFlag,
// Capsul // Capsul
internal.CapsulInstanceURLFlag, internal.CapsulInstanceURLFlag,

View File

@ -104,7 +104,7 @@ var serverRemoveCommand = cli.Command{
ArgsUsage: "[<server>]", ArgsUsage: "[<server>]",
Usage: "Remove a managed server", Usage: "Remove a managed server",
Description: ` Description: `
This command removes a server from Abra management. Remova a server from Abra management.
Depending on whether you used a 3rd party provider to create this server ("abra Depending on whether you used a 3rd party provider to create this server ("abra
server new"), you can also destroy the virtual server as well. Pass server new"), you can also destroy the virtual server as well. Pass
@ -126,21 +126,24 @@ like tears in rain.
}, },
Before: internal.SubCommandBefore, 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)
@ -164,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

@ -10,13 +10,12 @@ var ServerCommand = cli.Command{
Aliases: []string{"s"}, Aliases: []string{"s"},
Usage: "Manage servers", Usage: "Manage servers",
Description: ` Description: `
These commands support creating, managing and removing servers using 3rd party Create, manage and remove servers using 3rd party integrations.
integrations.
Servers can be created from scratch using the "abra server new" command. If you Servers can be created from scratch using the "abra server new" command. If you
already have a server, you can add it to your configuration using "abra server 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. recipes, see available flags on "abra server add" for more.
`, `,
Subcommands: []cli.Command{ Subcommands: []cli.Command{
serverNewCommand, serverNewCommand,

51
go.mod
View File

@ -4,46 +4,51 @@ 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.6
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731094149-b031ea1211e7 github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731094149-b031ea1211e7
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
github.com/docker/cli v20.10.12+incompatible github.com/docker/cli v20.10.21+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.21+incompatible
github.com/docker/go-units v0.4.0 github.com/docker/go-units v0.5.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.37.0
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.12.1
github.com/schultz-is/passgen v1.0.1 github.com/sirupsen/logrus v1.9.0
github.com/sirupsen/logrus v1.8.1 gotest.tools/v3 v3.4.0
gotest.tools/v3 v3.1.0
) )
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/decentral1se/passgen v1.0.1
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
github.com/gliderlabs/ssh v0.3.3 github.com/gliderlabs/ssh v0.3.5
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/mux v1.8.0 // indirect
github.com/hashicorp/go-retryablehttp v0.7.0 github.com/hashicorp/go-retryablehttp v0.7.1
github.com/kevinburke/ssh_config v1.1.0 github.com/kevinburke/ssh_config v1.2.0
github.com/klauspost/pgzip v1.2.5
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/urfave/cli v1.22.5 github.com/urfave/cli v1.22.9
github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xanzy/ssh-agent v0.3.1 // indirect
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 github.com/xeipuuv/gojsonpointer v0.0.0-20190809123943-df4f5c81cb3b // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e golang.org/x/crypto v0.3.0
golang.org/x/sys v0.2.0
) )

627
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -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,23 +5,18 @@ 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, fs filters.Args) ([]*types.Volume, error) {
cl, err := New(server) cl, err := New(server)
if err != nil { if err != nil {
return nil, err return nil, err
} }
fs := filters.NewArgs()
fs.Add("name", appName)
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 +24,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 +37,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

@ -2,7 +2,6 @@ package config
import ( import (
"fmt" "fmt"
"html/template"
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
@ -13,6 +12,7 @@ import (
loader "coopcloud.tech/abra/pkg/upstream/stack" loader "coopcloud.tech/abra/pkg/upstream/stack"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
composetypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -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,12 +52,59 @@ 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 // Filters retrieves exact app filters for querying the container runtime. Due
// to upstream issues, filtering works different depending on what you're
// querying. So, for example, secrets don't work with regex! The caller needs
// to implement their own validation that the right secrets are matched. In
// order to handle these cases, we provide the `appendServiceNames` /
// `exactMatch` modifiers.
func (a App) Filters(appendServiceNames, exactMatch bool) (filters.Args, error) {
filters := filters.NewArgs()
composeFiles, err := GetAppComposeFiles(a.Recipe, a.Env)
if err != nil {
return filters, err
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := GetAppComposeConfig(a.Recipe, opts, a.Env)
if err != nil {
return filters, err
}
for _, service := range compose.Services {
var filter string
if appendServiceNames {
if exactMatch {
filter = fmt.Sprintf("^%s_%s", a.StackName(), service.Name)
} else {
filter = fmt.Sprintf("%s_%s", a.StackName(), service.Name)
}
} else {
if exactMatch {
filter = fmt.Sprintf("^%s", a.StackName())
} else {
filter = fmt.Sprintf("%s", a.StackName())
}
}
filters.Add("name", filter)
}
return filters, nil
}
// ByServer sort a slice of Apps // ByServer sort a slice of Apps
type ByServer []App type ByServer []App
@ -68,25 +115,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 +165,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,
@ -213,13 +263,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
} }
@ -270,18 +320,15 @@ func TemplateAppEnvSample(recipeName, appName, server, domain string) error {
return err return err
} }
file, err := os.OpenFile(appEnvPath, os.O_RDWR, 0664) read, err := ioutil.ReadFile(appEnvPath)
if err != nil {
return err
}
defer file.Close()
tpl, err := template.ParseFiles(appEnvPath)
if err != nil { if err != nil {
return err return err
} }
if err := tpl.Execute(file, struct{ Name string }{recipeName}); err != nil { newContents := strings.Replace(string(read), recipeName+".example.com", domain, -1)
err = ioutil.WriteFile(appEnvPath, []byte(newContents), 0)
if err != nil {
return err return err
} }

View File

@ -16,10 +16,12 @@ 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 BACKUP_DIR = path.Join(ABRA_DIR, "backups")
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,13 +58,17 @@ 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
} }
// EnsureDomainsResolveSameIPv4 ensures that domains resolve to the same ipv4 address // EnsureDomainsResolveSameIPv4 ensures that domains resolve to the same ipv4 address
func EnsureDomainsResolveSameIPv4(domainName, server string) (string, error) { func EnsureDomainsResolveSameIPv4(domainName, server string) (string, error) {
if server == "default" || server == "local" {
return "", nil
}
var ipv4 string var ipv4 string
domainIPv4, err := EnsureIPv4(domainName) domainIPv4, err := EnsureIPv4(domainName)

View File

@ -5,6 +5,25 @@ import (
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
) )
// Check if a branch exists in a repo.
// Use this and not repository.Branch(), because the latter does not
// actually check for existing branches.
// See https://github.com/go-git/go-git/issues/518
func HasBranch(repository *git.Repository, name string) bool {
var exist bool
if iter, err := repository.Branches(); err == nil {
iterFunc := func(reference *plumbing.Reference) error {
if name == reference.Name().Short() {
exist = true
return nil
}
return nil
}
_ = iter.ForEach(iterFunc)
}
return exist
}
// GetCurrentBranch retrieves the current branch of a repository // GetCurrentBranch retrieves the current branch of a repository
func GetCurrentBranch(repository *git.Repository) (string, error) { func GetCurrentBranch(repository *git.Repository) (string, error) {
branchRefs, err := repository.Branches() branchRefs, err := repository.Branches()

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/recipes.json"
// ReposMetadataURL is the recipe repository metadata // ReposMetadataURL is the recipe repository metadata
const ReposMetadataURL = "https://git.coopcloud.tech/api/v1/orgs/coop-cloud/repos" const ReposMetadataURL = "https://git.coopcloud.tech/api/v1/orgs/coop-cloud/repos"
@ -232,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{
@ -355,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
} }
@ -568,7 +572,7 @@ func EnsureUpToDate(recipeName string) error {
} }
if !isClean { if !isClean {
return fmt.Errorf("%s has locally unstaged changes", recipeName) return fmt.Errorf("%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding", recipeName, recipeDir)
} }
repo, err := git.PlainOpen(recipeDir) repo, err := git.PlainOpen(recipeDir)
@ -615,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 !gitPkg.HasBranch(repo, "master") {
if _, err := repo.Branch("main"); err != nil { if !gitPkg.HasBranch(repo, "main") {
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"
} }
@ -689,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
} }
@ -708,14 +716,12 @@ func ReadRecipeCatalogue() (RecipeCatalogue, error) {
} }
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
} }
@ -797,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 {
@ -923,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)
@ -937,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()
@ -967,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)
@ -997,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{
@ -1054,7 +1060,7 @@ func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]stri
func EnsureCatalogue() error { func EnsureCatalogue() error {
catalogueDir := path.Join(config.ABRA_DIR, "catalogue") catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) { if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) {
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "recipes") url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME)
if err := gitPkg.Clone(catalogueDir, url); err != nil { if err := gitPkg.Clone(catalogueDir, url); err != nil {
return err return err
} }

View File

@ -8,10 +8,11 @@ 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"
"github.com/schultz-is/passgen" "github.com/decentral1se/passgen"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -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 {

38
pkg/test/test.go Normal file
View File

@ -0,0 +1,38 @@
package test
import (
"log"
"os"
"github.com/sirupsen/logrus"
)
// RmServerAppRecipe deletes the test server / app / recipe.
func RmServerAppRecipe() {
testAppLink := os.ExpandEnv("$HOME/.abra/servers/foo.com")
if err := os.Remove(testAppLink); err != nil {
logrus.Fatal(err)
}
testRecipeLink := os.ExpandEnv("$HOME/.abra/recipes/test")
if err := os.Remove(testRecipeLink); err != nil {
logrus.Fatal(err)
}
}
// MkServerAppRecipe symlinks the test server / app / recipe.
func MkServerAppRecipe() {
RmServerAppRecipe()
testAppDir := os.ExpandEnv("$PWD/../../tests/resources/testapp")
testAppLink := os.ExpandEnv("$HOME/.abra/servers/foo.com")
if err := os.Symlink(testAppDir, testAppLink); err != nil {
log.Fatal(err)
}
testRecipeDir := os.ExpandEnv("$PWD/../../tests/resources/testrecipe")
testRecipeLink := os.ExpandEnv("$HOME/.abra/recipes/test")
if err := os.Symlink(testRecipeDir, testRecipeLink); err != nil {
log.Fatal(err)
}
}

View File

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

@ -1,3 +1,6 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json" "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"ignoreDeps": [
"github.com/urfave/cli"
]
} }

View File

@ -0,0 +1,7 @@
function complete_abra_args
set -l cmd (commandline -poc) --generate-bash-completion
$cmd
end
complete -c abra -f -n "not __fish_seen_subcommand_from -h --help -v --version complete_abra_args" -a "(complete_abra_args)"
complete -c abra -f -s h -l help -d 'show help'
complete -c abra -f -s v -l version -d 'print the version'

View File

@ -1,8 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
ABRA_VERSION="0.3.0-alpha" ABRA_VERSION="0.5.1-beta"
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION" ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION"
RC_VERSION="0.4.0-alpha-rc6" RC_VERSION="0.5.1-beta"
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION" RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION"
for arg in "$@"; do for arg in "$@"; do
@ -44,8 +44,17 @@ function install_abra_release {
exit 1 exit 1
fi fi
ARCH=$(uname -m)
PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m) if [[ $ARCH =~ "aarch64" ]]; then
ARCH="arm64"
elif [[ $ARCH =~ "armv5l" ]]; then
ARCH="armv5"
elif [[ $ARCH =~ "armv6l" ]]; then
ARCH="armv6"
elif [[ $ARCH =~ "armv7l" ]]; then
ARCH="armv7"
fi
PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')_$ARCH
FILENAME="abra_"$ABRA_VERSION"_"$PLATFORM"" FILENAME="abra_"$ABRA_VERSION"_"$PLATFORM""
sed_command_rel='s/.*"assets":\[\{[^]]*"name":"'$FILENAME'"[^}]*"browser_download_url":"([^"]*)".*\].*/\1/p' sed_command_rel='s/.*"assets":\[\{[^]]*"name":"'$FILENAME'"[^}]*"browser_download_url":"([^"]*)".*\].*/\1/p'
sed_command_checksums='s/.*"assets":\[\{[^\]*"name":"checksums.txt"[^}]*"browser_download_url":"([^"]*)".*\].*/\1/p' sed_command_checksums='s/.*"assets":\[\{[^\]*"name":"checksums.txt"[^}]*"browser_download_url":"([^"]*)".*\].*/\1/p'

5
scripts/release/upx.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
set -ex
upx ./dist/abra_*/abra

View File

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

View File

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

View File

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

1
tests/integration/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
logs

View File

@ -1,7 +0,0 @@
FROM debian:bullseye-slim
RUN apt update && apt install -y wget curl git;
RUN git config --global user.email "integration-tests@coopcloud.tech";
RUN git config --global user.name "integration-tests";

View File

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

View File

@ -1,15 +1,14 @@
#!/bin/bash #!/bin/bash
source ./testfunctions.sh
source ./common.sh source ./common.sh
echo "all apps, all servers" run_test '$ABRA app ls'
$ABRA app ls
printf "\\n\\n\\n"
echo "all wordpress apps, all servers" run_test '$ABRA app ls --status'
$ABRA app ls --type wordpress
printf "\\n\\n\\n"
echo "all wordpress apps, only server2" run_test '$ABRA app ls --type wordpress'
$ABRA app ls --type wordpress --server server2
printf "\\n\\n\\n" 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

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

View File

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

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

@ -0,0 +1,14 @@
#!/bin/bash
source ./testfunctions.sh
source ./common.sh
create_server_app_recipe
run_test '$ABRA app cmd foo.com test --local'
run_test '$ABRA app cmd foo.com test --local -- foo'
run_test '$ABRA app cmd foo.com test --local -- foo bar baz'
clean_server_app_recipe

View File

@ -2,16 +2,19 @@
set -e set -e
function init() { create_server_app_recipe() {
ABRA="$HOME/.local/bin/abra" ln -srf ../resources/testapp ~/.abra/servers/foo.com
INSTALLER_URL="https://install.abra.coopcloud.tech" ln -srf ../resources/testrecipe ~/.abra/recipes
}
for arg in "$@"; do clean_server_app_recipe() {
if [ "$arg" == "--dev" ]; then unlink ~/.abra/servers/foo.com
ABRA="/src/abra" unlink ~/.abra/recipes/testrecipe
INSTALLER_URL="https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer" }
fi
done 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 export PATH=$PATH:$HOME/.local/bin

View File

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

View File

@ -1,11 +0,0 @@
default:
@docker run \
-v $$(pwd)/../../:/src \
-v $$(pwd)/.abra:/root/.abra \
--env-file .envrc \
decentral1se/abra-int:latest \
sh -c '\
echo "Running $(TARGET)..."; \
cd /src/tests/integration; \
bash "$(TARGET)" -- --dev \
'

View File

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

View File

@ -1,9 +1,21 @@
#!/bin/bash #!/bin/bash
source ./testfunctions.sh
source ./common.sh source ./common.sh
$ABRA record new -p gandi -t A -n int-core -v 192.157.2.21 coopcloud.tech run_test "$ABRA record new \
--provider gandi \
--record-type A \
--record-name integration-tests \
--record-value 192.157.2.21 \
--no-input coopcloud.tech \
"
$ABRA record list -p gandi coopcloud.tech | grep -q int-core run_test '$ABRA record list --provider gandi coopcloud.tech'
$ABRA -n record rm -p gandi -t A -n int-core coopcloud.tech run_test "$ABRA record rm \
--provider gandi \
--record-type A \
--record-name integration-tests \
--no-input coopcloud.tech
"

View File

@ -1,9 +1,10 @@
#!/bin/bash #!/bin/bash
source ./testfunctions.sh
source ./common.sh source ./common.sh
$ABRA -n server new -p hetzner-cloud --hn int-core run_test '$ABRA server new --provider hetzner-cloud --hetzner-name integration-tests --no-input'
$ABRA server ls | grep -q int-core run_test '$ABRA server ls'
$ABRA -n server rm -s -p hetzner-cloud --hn int-core 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", "cmd.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,40 +0,0 @@
# manual test plan
## recipe publish
- `abra recipe upgrade <recipe>`
- `cd ~/.abra/apps/<recipe>/ && git diff` to ensure changes made
- `abra recipe sync <recipe>`
- `cd ~/.abra/apps/<recipe>/ && git diff` to ensure changes made
- `abra recipe release <recipe> --dry-run`
- prompts should be correct, read what `abra` asks you carefully
## deploy, upgrade, rollback
- `abra app deploy --chaos <app>`
- `abra app deploy --force <app>`
- `abra app deploy <app>`
- `abra app rollback <app>`
- `abra app upgrade <app>`
## app day-to-day ops
- `abra app check <app>`
- `abra app config <app>`
- `abra app cp <app>`
- `abra app errors -w <app>`
- `abra app logs <app>`
- `abra app ls --status <app>`
- `abra app new --secrets <recipe>`
- `abra app ps <app>`
- `abra app remove <app>`
- `abra app restart <app>`
- `abra app run <app>`
- `abra app secret generate --all`
- `abra app secret insert <app> foo v1 bar`
- `abra app secret ls <app>`
- `abra app secret remove <app> foo`
- `abra app volume ls <app>`
- `abra app volume remove --force <app>`

View File

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

View File

@ -0,0 +1 @@
.

View File

@ -0,0 +1,5 @@
test(){
echo "1: $1"
echo "2: $2"
echo "all: $@"
}

View File

@ -0,0 +1,5 @@
---
version: "3.8"
services:
app: []

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