Compare commits

...

1973 Commits

Author SHA1 Message Date
renovate-bot 9859ff46f5 chore(deps): update otel/weaver docker tag to v0.24.1 2026-06-21 21:01:00 +00:00
renovate-bot 3109a0da82 chore(deps): update otel/weaver docker tag to v0.24.0 2026-06-20 00:01:07 +00:00
renovate-bot b1146a707e chore(deps): update nginx docker tag to v1.31.2 2026-06-17 23:01:19 +00:00
Weblate 6d13d14d75 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2026-06-16 17:41:06 +00:00
Linus Gasser 427ec22cab Running i18n 2026-06-16 17:40:58 +00:00
Linus Gasser 1111b69f12 Use docker login credentials from host
I had a lot of failures for pulling the docker images lately,
so I was looking for a way to connect using docker login.
This PR sends the docker login credentials from the host to
the swarm server.
2026-06-16 17:40:58 +00:00
Linus Gasser 1541e6aa6a Also use xgettext-go@latest for .drone.yml 2026-06-14 18:37:34 +02:00
Linus Gasser 14ee80582a Use go run for xgettext-go
Before: on a mac, the Makefile downloaded a linux file, which did not work
for updating the i18n.

Now: use 'go run' to run the xgettext-go file. go caches it, so it compiles only
once.
2026-06-14 18:28:45 +02:00
renovate-bot e5c65b8fa0 chore(deps): update alpine docker tag to v3.24 2026-06-09 21:00:57 +00:00
Weblate e1b10f6020 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2026-06-09 14:36:32 +00:00
moritz 8e4ed7b689 chore: make i18n 2026-06-09 16:30:08 +02:00
moritz a44fde2df2 fix(new): checkout given recipeVersion before generating env closes #862 2026-06-09 16:09:58 +02:00
renovate-bot e623f55852 chore(deps): update golang docker tag 2026-06-08 17:01:04 +00:00
renovate-bot 11e6f28a60 chore(deps): update module golang.org/x/term to v0.44.0 2026-06-08 16:01:46 +00:00
renovate-bot acec067d76 chore(deps): update golang docker tag 2026-06-08 14:01:12 +00:00
renovate-bot f50a57af7a chore(deps): update module golang.org/x/sys to v0.46.0 2026-06-08 13:01:32 +00:00
renovate-bot 776693acc0 chore(deps): update golang docker tag 2026-05-22 23:01:07 +00:00
renovate-bot 5dea5f7746 chore(deps): update module golang.org/x/sys to v0.45.0 2026-05-22 22:01:49 +00:00
renovate-bot 1d9a289888 chore(deps): update nginx docker tag to v1.31.1 2026-05-22 21:00:50 +00:00
devydave c7bd55e371 feat: adds nix flake 2026-05-20 22:29:47 +00:00
renovate-bot 4276337b0f chore(deps): update golang docker tag 2026-05-18 22:01:06 +00:00
renovate-bot 90ca856b64 chore(deps): update module github.com/go-git/go-git/v5 to v5.19.1 2026-05-18 21:01:33 +00:00
renovate-bot f2dd65491d chore(deps): update golang docker tag 2026-05-15 16:01:27 +00:00
renovate-bot 0e902ed897 chore(deps): update coopcloud.tech/tagcmp digest to c26951b 2026-05-15 14:01:42 +00:00
renovate-bot db001c1ba4 chore(deps): update tonistiigi/xx docker tag to v1.9.0 2026-05-15 00:00:59 +00:00
renovate-bot e4215c09aa chore(deps): update nginx docker tag to v1.31.0 2026-05-14 23:04:17 +00:00
renovate-bot e0e6dcb710 chore(deps): update otel/weaver docker tag to v0.23.0 2026-05-14 23:00:58 +00:00
renovate-bot e7ddb74a08 chore(deps): update module golang.org/x/term to v0.43.0 2026-05-14 22:04:37 +00:00
renovate-bot 24a5e6334f chore(deps): update golang docker tag 2026-05-14 22:00:58 +00:00
renovate-bot 9d8eb2317e chore(deps): update module golang.org/x/sys to v0.44.0 2026-05-14 21:01:45 +00:00
renovate-bot 5945ea8e1b chore(deps): update module github.com/go-git/go-git/v5 to v5.19.0 2026-05-14 20:01:30 +00:00
renovate-bot e170d1c971 chore(deps): update alpine docker tag to v3.23 2026-05-14 19:04:12 +00:00
renovate-bot 5eba3abb1b chore(deps): update golang docker tag 2026-05-14 19:01:12 +00:00
renovate-bot df5a38e887 chore(deps): update module github.com/decentral1se/cobra to v1.10.2 2026-05-14 18:01:50 +00:00
decentral1se 1e80d111e6 test: less flaky test 2026-05-02 09:56:40 +02:00
Weblate 7a079b78de chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2026-04-11 13:54:52 +00:00
iexos 7524a785ce chore: make i18n 2026-04-11 11:36:14 +02:00
iexos 1013f669bb fix: i18n & clarify no release notes msg 2026-04-10 23:22:40 +02:00
decentral1se aae20f07cc fix: general golang path [ci skip] 2026-04-03 22:59:21 +02:00
decentral1se 6ef8e1ff52 test: temporarily skip flaky test
See toolshed/abra#814
2026-04-01 11:37:07 +02:00
decentral1se 7fb9675b1e chore: golang 1.26 && make deps 2026-04-01 11:25:57 +02:00
decentral1se d88b478503 test: unit tests clean up themselves
See toolshed/abra#792
2026-04-01 10:47:32 +02:00
decentral1se 7a735043cd fix: unbork json [ci skip] 2026-03-30 09:40:13 +02:00
decentral1se e610f32c35 fix: ignore new forked deps [ci skip] 2026-03-29 18:15:47 +02:00
p4u1 e04a1e15c4 feat: add filename to error message when yaml file is invalid
Before:
FATA <internal/validate.go:84> unable to validate recipe: yaml: line 3: did not find expected key

After:
FATA <internal/validate.go:84> unable to validate recipe: <redacted>/recipes/monitoring-ng/compose.grafana-oidc.yml: yaml: line 3: did not find expected key
2026-03-20 14:42:14 +01:00
decentral1se 9d401202b4 test: ensure "release, fail, release" works
See toolshed/abra#794
2026-03-05 11:28:34 +01:00
decentral1se 6504be6403 test: ensure reproducible version 2026-03-05 11:28:33 +01:00
Weblate d4944dbf35 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2026-03-03 00:03:00 +00:00
decentral1se 8d8d4f799d chore: i18n 2026-03-03 01:02:49 +01:00
iexos 0633f24d1b feat: recipe release reverts completely on failure 2026-03-03 00:02:15 +00:00
decentral1se 2e062899c7 feat: release make target 2026-03-01 09:42:59 +01:00
decentral1se fbd7275f03 build: remove broken goreleaser automagic
See toolshed/abra#780
2026-03-01 09:40:29 +01:00
ChasquiLabo cedf185e97 chore: translation using Weblate (Spanish)
Currently translated at 91.1% (1048 of 1150 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2026-02-28 13:52:56 +00:00
decentral1se 06a57ded02 chore: 0.13.0-beta 2026-02-23 17:26:42 +01:00
ChasquiLabo 6f92ba0deb chore: translation using Weblate (Spanish)
Currently translated at 91.1% (1048 of 1150 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2026-02-21 20:37:47 +00:00
ChasquiLabo dcd830e3f8 chore: translation using Weblate (Spanish)
Currently translated at 91.1% (1048 of 1150 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2026-02-21 01:44:54 +00:00
Weblate 8056703d59 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2026-02-20 16:25:39 +00:00
decentral1se 566bdf2bd8 Merge remote-tracking branch 'weblate/main' 2026-02-20 17:25:15 +01:00
decentral1se 24288c81d3 fix: missing i18n 2026-02-20 14:49:02 +01:00
decentral1se 2ef2a7ed2c chore: 0.13.0-rc2-beta 2026-02-20 11:05:39 +01:00
decentral1se cf8cd7423d build: trimpath 2026-02-20 11:03:16 +01:00
ChasquiLabo a18f57488f chore: translation using Weblate (Spanish)
Currently translated at 91.2% (1043 of 1143 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2026-02-20 09:50:31 +00:00
decentral1se b2e691265a fix: consistent i18n usage on rootCmd 2026-02-20 10:49:04 +01:00
decentral1se bff23f0ae6 fix: translated help
Follows toolshed/abra#785
2026-02-20 10:46:52 +01:00
Weblate 403c7a3e5b chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2026-02-19 13:36:14 +00:00
3wordchant 66b932a553 Merge remote-tracking branch 'weblate/main' 2026-02-19 08:35:59 -05:00
3wordchant f64e4b62cf chore: Re-run make i18n 2026-02-19 08:28:14 -05:00
ChasquiLabo e80ecbc332 chore: translation using Weblate (Spanish)
Currently translated at 90.9% (1041 of 1144 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2026-02-19 08:27:33 -05:00
iexos abcbdf57f1 test: remove recipe sync dependency 2026-02-19 11:04:47 +00:00
decentral1se 78899f173c fix: remove double help flag 2026-02-19 00:42:56 +01:00
ChasquiLabo 90142cb783 chore: translation using Weblate (Spanish)
Currently translated at 90.9% (1041 of 1144 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2026-02-18 23:08:37 +00:00
decentral1se 8dbde3d158 fix: wrap string (i18n) 2026-02-19 00:07:18 +01:00
Apfelwurm 8f42e36302 feat: bytes/base64 secret generation 2026-02-18 20:41:45 +01:00
ChasquiLabo c2552ec2f6 chore: translation using Weblate (Spanish)
Currently translated at 90.9% (1041 of 1144 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2026-02-18 13:52:56 +00:00
decentral1se 8c64a8049d chore: 0.13.0-rc1-beta x 3 [ci skip]
That didn't work. Doing it manually *again*.
2026-02-18 09:28:30 +01:00
decentral1se b073072489 chore: 0.13.0-rc1-beta x 2 2026-02-18 09:14:11 +01:00
decentral1se 9daa4fee48 chore: 0.13.0-rc1-beta 2026-02-18 09:04:29 +01:00
decentral1se ea48917e6c test: ensure ssh-agent is configured
See https://build.coopcloud.tech/toolshed/abra/3594/1/7
2026-02-17 12:54:40 +00:00
ChasquiLabo 728f873a3e chore: translation using Weblate (Spanish)
Currently translated at 90.0% (1030 of 1144 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2026-02-17 10:40:21 +00:00
decentral1se ddb90dd44d docs: woops, remove that [ci skip] 2026-02-16 17:35:27 +01:00
decentral1se 7a8485492e test: give more space to CI machine for these tests [ci skip] 2026-02-16 17:28:44 +01:00
Weblate 32bb05abba chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2026-02-16 11:06:33 +00:00
decentral1se 3d2006a696 chore: i18n 2026-02-16 12:06:24 +01:00
iexos 521f5c1647 feat: optionally commit changes with recipe upgrade 2026-02-16 09:18:41 +00:00
iexos 5eb41bc803 feat!: require clean working copy for recipe release cmd 2026-02-16 09:18:41 +00:00
iexos fc39721501 feat!: merge recipe sync into recipe release 2026-02-16 09:18:41 +00:00
iexos 44bacc582b feat!: always publish on recipe release 2026-02-16 09:18:41 +00:00
decentral1se 53e8b52717 docs: more authors [ci skip] 2026-02-15 19:40:24 +01:00
decentral1se 0aba922dda test: fix error [ci skip] 2026-02-15 18:58:23 +01:00
decentral1se 4e0eb739b4 feat: ls requires --chaos due to Ensure logic
Follows toolshed/abra#771
2026-02-15 17:57:31 +00:00
Weblate 6b661dd7a7 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2026-02-15 17:57:28 +00:00
decentral1se 39102752c0 fix: more graceful bailing if borked .env.sample 2026-02-15 18:26:05 +01:00
decentral1se 5cfc1c076c fix: remove timeout 2026-02-15 16:34:18 +00:00
Weblate 10f7ed74b0 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2026-02-15 16:34:05 +00:00
decentral1se 227d37dc26 chore: i18n 2026-02-15 17:08:14 +01:00
decentral1se 0ccf3d2b12 fix: ensure and fail for updated recipes 2026-02-15 17:07:46 +01:00
decentral1se f87ce74027 fix: ensure borked tag handled for deploy/ls 2026-02-15 17:07:45 +01:00
Weblate 4349ee82bc chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2026-02-15 13:47:09 +00:00
decentral1se f9ea7506d0 test: toolshed/abra#761 2026-02-15 14:46:54 +01:00
decentral1se 1fe2d0421b chore: i18n 2026-02-15 14:41:03 +01:00
ammaratef45 59c0d1f4c5 ensure recipe is up to date before creating new app 2026-02-15 13:40:34 +00:00
decentral1se 7c3364f87a test: do not wait for converge checks
See toolshed/abra#750 (comment)
2026-02-15 14:39:07 +01:00
Weblate 2c5a273fa7 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2026-02-15 13:08:19 +00:00
decentral1se c54fe3ef85 chore: i18n 2026-02-15 14:08:08 +01:00
ChasquiLabo bda0d23d39 chore: translation using Weblate (Spanish)
Currently translated at 87.8% (1002 of 1141 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2026-02-15 09:40:56 +00:00
ChasquiLabo b9dc7b8437 chore: translation using Weblate (Spanish)
Currently translated at 87.7% (1001 of 1141 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2026-02-14 13:52:56 +00:00
Apfelwurm 8f7dbfedbc add integration test that does not use old recipe version when recipe is broken 2026-02-13 21:51:46 +00:00
Apfelwurm 4b863f1e15 change usage of tags to recipe versions in deploy.go 2026-02-13 21:51:46 +00:00
Apfelwurm 064c9f5d65 fix: breaking GetRecipeVersions when an invalid recipe version exists 2026-02-13 21:51:46 +00:00
iexos 98e48c95c7 fix: duplicate R015 rule number 2026-02-13 15:57:21 +01:00
iexos c85d8ee6d1 small fixes 2026-02-13 12:13:50 +01:00
p4u1 23268a0e92 fix: Does not crash when an image has no tag 2026-02-13 11:24:22 +01:00
p4u1 d60d426752 moved fork 2026-02-13 09:29:01 +00:00
p4u1 0e273de8f6 fix: Allows multiple protocols on one port 2026-02-13 09:29:01 +00:00
ChasquiLabo ab32118bfe chore: translation using Weblate (Spanish)
Currently translated at 75.0% (856 of 1141 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2026-02-13 05:52:56 +00:00
ChasquiLabo 683396d75a chore: translation using Weblate (Spanish)
Currently translated at 58.7% (670 of 1141 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2026-02-12 05:13:22 +00:00
ChasquiLabo 4db6755f0d chore: translation using Weblate (Spanish)
Currently translated at 30.2% (345 of 1141 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2026-02-05 14:52:56 +00:00
ChasquiLabo 4c132e30f6 chore: translation using Weblate (Spanish)
Currently translated at 29.8% (341 of 1141 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2026-02-01 03:52:59 +00:00
ChasquiLabo f5aeae30c7 chore: translation using Weblate (Spanish)
Currently translated at 23.3% (266 of 1141 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2026-01-30 16:52:56 +00:00
ammaratef45 1d24107956 fix regexp for _remove_tags bats output 2026-01-26 07:05:51 +00:00
Weblate f835b87255 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2026-01-16 23:45:14 +00:00
decentral1se dba21d6a29 chore: lowercase, i18n 2026-01-17 00:44:55 +01:00
ammaratef45 182fc41c58 added integration test 2026-01-16 14:27:27 -08:00
ammaratef45 304ac87cec ensure repo is up to date before printing status 2026-01-14 16:58:21 -08:00
namnatulco 5b3929d885 modified install script to output a safe PATH-update (fixes #735) 2026-01-02 10:19:10 +01:00
Weblate c41df874d1 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-12-21 15:39:13 +00:00
decentral1se b721adbf9c chore: i18n 2025-12-21 16:39:02 +01:00
ammaratef45 42f9e6d458 return error instead of handling it inside getLatestVersion 2025-12-19 06:50:35 -08:00
ammaratef45 9e7bc31d4d avoiding #732 by checking for empty versions list for recipe sync 2025-12-18 14:41:58 -08:00
Weblate b79c4f33b6 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-12-14 15:02:41 +00:00
jade cc87d5b3da chore: remove reference to wizard mode from recipe upgrade cli docs 2025-12-13 15:26:12 +11:00
ChasquiLabo 8b5e3f3c78 chore: translation using Weblate (Spanish)
Currently translated at 9.5% (109 of 1140 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2025-12-10 01:43:58 +00:00
decentral1se db7c4042d0 chore: go mod vendor 2025-11-09 11:52:00 +01:00
decentral1se ed1a66dc5f chore: publish next tag 0.12.0-beta 2025-11-09 11:46:38 +01:00
decentral1se bb93e4266a fix: show domain with https (clickable)
See toolshed/abra#643
2025-11-09 10:56:21 +01:00
decentral1se a2cc70b2f5 test: reinstate debug 2025-11-09 10:43:09 +01:00
decentral1se ce1aa3d870 test: ensure recipe sync is robust 2025-11-09 10:41:32 +01:00
decentral1se d75700c8a9 test: ensure env vars updated
See toolshed/abra#723
2025-11-09 09:32:52 +01:00
decentral1se 0ccc4aae72 chore: remove --debug 2025-11-09 09:32:42 +01:00
decentral1se ec22d5d51d test: remove old tests
See toolshed/abra#716
2025-11-05 09:52:28 +01:00
decentral1se ab42584d05 docs: update var name 2025-11-05 09:52:28 +01:00
fauno 40eb6e9a18 fix: shorter hyphen 2025-11-04 11:49:44 -03:00
Weblate 35eb9d4a89 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-11-04 14:34:39 +00:00
decentral1se 08cc63d523 chore: make i18n 2025-11-04 15:34:27 +01:00
Weblate 797b8d899b chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-11-04 13:53:26 +00:00
decentral1se fb786306b5 chore: make i18n 2025-11-04 14:53:15 +01:00
decentral1se c3a2048eba fix: throw away unknown version
See toolshed/abra#715
2025-11-04 13:52:49 +00:00
Apfelwurm 1bdc11ba62 fix no-input app deployment when no tty is present 2025-11-04 13:52:28 +00:00
Weblate cc8703310c chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-11-04 07:35:14 +00:00
decentral1se fcd5bd863d chore: make i18n 2025-11-04 08:35:04 +01:00
decentral1se e6af2da9dd refactor: named note, merge if clause 2025-11-04 08:34:49 +01:00
p4u1 4b688825e0 feat: create docker context when server folder does exist 2025-11-03 17:29:04 +01:00
decentral1se b0cf2a1f8e chore: make i18n 2025-11-02 10:44:34 +00:00
decentral1se 6b7020d457 test: env version to .config.env 2025-11-02 10:44:34 +00:00
decentral1se efdac610bd fix: skip local server on it's own 2025-11-02 10:44:34 +00:00
decentral1se cd6021f116 fix: expose new version
See toolshed/abra#713
2025-11-02 10:44:34 +00:00
Weblate ee8de8ef5c chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-10-31 20:38:18 +00:00
decentral1se e5a653c002 chore: make i18n 2025-10-31 21:38:01 +01:00
decentral1se 2cca04de90 fix(move): does not error when secret already exists on new server
See toolshed/abra#709
2025-10-31 21:37:55 +01:00
Weblate f2f79e2df8 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-10-31 20:33:36 +00:00
decentral1se dd83741a9f chore: i18n 2025-10-31 21:31:49 +01:00
decentral1se dc2cd85d91 feat!: abra app env pull
`abra app env` -> `abra app env list`.

See toolshed/abra#497
2025-10-31 21:31:43 +01:00
decentral1se 96e59cf196 test: adjust to match new reality [ci skip] 2025-10-31 14:35:57 +01:00
decentral1se 11656c009d test: don't wat to converge [ci skip] 2025-10-26 11:49:57 +01:00
decentral1se e4e1b58501 test: update matches in old tests [ci skip] 2025-10-26 11:40:25 +01:00
decentral1se 3b8f12643c test: use new target 2025-10-25 20:01:23 +02:00
decentral1se e5f5154197 test: kadabra is gone 2025-10-23 20:58:08 +02:00
decentral1se 6c1c0a8a8a refactor: use xgettext-go from makefile variable
Easier to hack when customising xgettext-go.
2025-10-23 09:26:51 +02:00
Weblate 662f45008c chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-10-23 07:14:28 +00:00
decentral1se 708c5f5223 chore: make i18n
See toolshed/abra#688
2025-10-23 09:14:15 +02:00
Weblate d58552b748 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-10-23 07:10:59 +00:00
decentral1se 51fe809851 chore: make i18n
See toolshed/abra#688
2025-10-23 09:10:36 +02:00
Weblate 3f6a22747f chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-10-23 07:09:11 +00:00
decentral1se 4e75b96914 chore: make i18n
See toolshed/abra#688
2025-10-23 09:08:50 +02:00
Weblate fd4ee75ab7 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-10-19 13:47:17 +00:00
decentral1se 964ed834ee refactor!: remove autoupdate (kadabra) 2025-10-19 15:46:18 +02:00
Weblate fcb3167394 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-10-19 13:36:39 +00:00
decentral1se 3845b40aa3 refactor!: archive kadabra 2025-10-19 15:32:38 +02:00
3wordchant 0dc5c307af chore: update i18n 2025-10-18 16:03:11 -04:00
3wordchant fc5855ff28 feat: Add hexadecimal secret generation
Closes #695
2025-10-18 15:03:02 -04:00
decentral1se 5b504a1550 Revert "feat: cctuip lands in main"
See toolshed/abra#691 (comment)
2025-10-17 19:27:23 +02:00
Weblate fc16a21f1c chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-10-03 18:39:54 +00:00
decentral1se 7b4d2d7230 chore: make i18n 2025-10-03 20:35:47 +02:00
decentral1se d0ccb805c6 refactor: isolate expensive IsDirty() call
See toolshed/abra#689
2025-10-03 20:35:09 +02:00
decentral1se 2460dd9438 fix: pagination with multiline(true)
See toolshed/abra#689
2025-10-03 20:13:35 +02:00
Weblate 9c648a2566 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-10-02 09:05:45 +00:00
decentral1se 22ecfb9c4c test: remove old non-tui tests 2025-10-02 10:58:53 +02:00
decentral1se 9f3cf718be chore: make i18n 2025-10-02 10:54:07 +02:00
decentral1se b737ce2107 feat: cctuip lands in main
See toolshed/organising#657
2025-10-02 10:53:44 +02:00
decentral1se a3d0ece7cb refactor: single missing value 2025-10-02 10:53:31 +02:00
decentral1se d63a1c28ea chore: go mod tidy / vendor / make deps 2025-10-02 10:35:46 +02:00
Weblate 1c10e64c58 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-10-01 19:16:08 +00:00
decentral1se 21826ec555 chore: make i18n 2025-10-01 21:13:41 +02:00
decentral1se 4b4c56d406 fix: skip borked tags on app list
See toolshed/abra#656
2025-10-01 21:13:18 +02:00
decentral1se 4314195dd7 test: dont run xgettext-go on release
See toolshed/abra#663
2025-10-01 12:20:16 +02:00
Weblate df4447b038 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-10-01 10:12:54 +00:00
decentral1se 3fa660e579 chore: make i18n 2025-10-01 12:12:07 +02:00
decentral1se a430b1e4fd fix: dont show unchanged images/tags
See toolshed/abra#677
2025-10-01 12:11:29 +02:00
Weblate 896c434f38 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-10-01 09:50:52 +00:00
decentral1se 847b7238c5 chore: make i18n 2025-10-01 11:49:19 +02:00
decentral1se 89d5fc91b0 fix: gracefully explode of missing context
See toolshed/abra#675
2025-10-01 11:48:51 +02:00
Weblate 5af3c5f56e chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-10-01 09:20:19 +00:00
decentral1se beb3864b2d chore: make i18n 2025-10-01 11:18:51 +02:00
decentral1se 581e6ef538 chore: lowercase 2025-10-01 11:18:42 +02:00
decentral1se fd642ddb84 fix: fail if release conflicts
See toolshed/organising#638
2025-10-01 11:18:30 +02:00
decentral1se 1ad8c127d9 fix: point to the catalogue
See toolshed/abra#669
2025-10-01 09:09:40 +02:00
Weblate 40aab6a6c1 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-10-01 07:01:05 +00:00
decentral1se 4d33a24a07 refactor: less EN specific value 2025-10-01 08:54:09 +02:00
decentral1se ee59eb350b chore: make i18n 2025-10-01 08:54:08 +02:00
decentral1se 5da13ff15a test: adjust integration suite 2025-10-01 08:54:05 +02:00
decentral1se 491c594ad3 fix: better message for redeploying chaos version
See toolshed/abra#668
2025-10-01 08:19:47 +02:00
decentral1se c794d533be fix: avoid hanging when tasks randomly surge
See toolshed/abra#557
2025-10-01 08:19:46 +02:00
decentral1se a6daf7030e fix: show chaos version on upgrade 2025-10-01 08:19:45 +02:00
decentral1se fe3b7ffa9c fix: write correct undeploy version 2025-10-01 08:19:44 +02:00
decentral1se 4c066a92d8 fix: show chaos version on rollback overview 2025-10-01 08:19:43 +02:00
decentral1se 7899b57781 fix: show chaos version on deploy overview 2025-10-01 08:19:42 +02:00
decentral1se 6e0a901887 chore: spacing for readability 2025-10-01 08:19:41 +02:00
decentral1se 713fdebc90 fix: show chaos version on undeploy 2025-10-01 08:19:40 +02:00
decentral1se 6944d138c6 refactor: chaos-y handling
See toolshed/abra#659
2025-10-01 08:19:39 +02:00
decentral1se fbb1f16470 fix: dont overwrite label when chaos
See toolshed/abra#668
2025-10-01 08:19:38 +02:00
decentral1se 2473cafdf5 test: use new CI option for xgettext-go 2025-09-30 19:23:23 +02:00
Weblate 0ccfbd253e chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-09-30 17:20:47 +00:00
decentral1se 6c4bee0ac7 chore: make i18n 2025-09-30 19:20:35 +02:00
Weblate 4fa9f536eb chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-09-30 17:10:26 +00:00
decentral1se 033c9bfc13 feat: msgctxt support
See toolshed/abra#647
See toolshed/xgettext-go#1
2025-09-30 19:08:52 +02:00
Weblate 0db1ee87fc chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-09-30 17:03:14 +00:00
decentral1se d180bb924f chore: make i18n 2025-09-29 10:33:56 +02:00
decentral1se d50d68d95a test: further generate=false secre tests 2025-09-29 10:33:40 +02:00
decentral1se f468bc7443 fix: collect local name also 2025-09-29 10:33:26 +02:00
decentral1se dee2d9d104 fix: nuance of generate=false for app deploy 2025-09-29 10:32:29 +02:00
decentral1se 5c892b1d6a fix: nuance of generate=false for app new 2025-09-29 10:32:04 +02:00
decentral1se 81b96fc7b1 docs: better wording 2025-09-29 10:31:46 +02:00
cyrnel c92a0d0703 feat: Add cloud-init file 2025-09-14 13:02:21 -04:00
decentral1se 1c4abcf12f chore: bump installer 2025-09-10 22:06:12 +02:00
decentral1se f590870672 chore: make i18n 2025-09-10 21:46:43 +02:00
decentral1se a31a25cfa1 fix: show no images if no diff required 2025-09-10 21:46:42 +02:00
Weblate 870dcfb342 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-09-09 22:07:29 +00:00
3wordchant f53ba48efa docs: update comment 2025-09-09 17:37:36 -04:00
3wordchant 26c920e570 chore: i18n.G, update POT 2025-09-09 17:37:12 -04:00
3wordchant c67fc57902 feat: show proposed secret version changes during deploy 2025-09-09 17:37:12 -04:00
3wordchant 07cafd371c fix: top-align table cells in horizontal() 2025-09-09 16:55:23 -04:00
3wordchant 5bb6241172 test: fix "bail if env has a hash but no --chaos" test 2025-09-09 16:03:22 -04:00
3wordchant 66e6a2c47e chore: update POT 2025-09-09 15:59:45 -04:00
3wordchant d866527138 fix: set "chaos" if a specified* version is "chaos-y"
*either from env file or command-line
2025-09-09 13:25:18 -04:00
3wordchant 39d1997edf chore: update POT 2025-09-09 13:19:22 -04:00
3wordchant d5f5d96944 style: formatting 2025-09-09 13:18:49 -04:00
3wordchant 076d522b1a refactor: PR feedback 2025-09-09 13:18:26 -04:00
3wordchant 34934cf62d refactor: move strings.Join to DeployOverview 2025-09-08 22:12:22 -04:00
3wordchant 241dffb8cd chore: POT 2025-09-08 22:12:21 -04:00
3wordchant e42b42e882 chore: 4matting 2025-09-08 22:12:20 -04:00
3wordchant 0a45424658 test: check new deploy overview stuff 2025-09-08 22:12:17 -04:00
3wordchant e73b0cc2fc refactor: don't reinvent the wheel 2025-09-08 22:12:12 -04:00
3wordchant 33aca42181 feat: add --show-unchanged/-U option 2025-09-08 22:12:08 -04:00
3wordchant 5c659bae5f chore: update POT 2025-09-08 22:12:04 -04:00
3wordchant d9f1f82923 style: 4matting 2025-09-08 22:12:00 -04:00
3wordchant 117f64a9d6 test: add some basic unit tests for new utility methods 2025-09-08 22:11:55 -04:00
3wordchant 90e9e9b5aa refactor: move MergeAbraShEnv to shared method
Re #638
2025-09-08 22:11:50 -04:00
3wordchant 7e217f8892 chore: regen POT 2025-09-08 22:11:39 -04:00
3wordchant bf68ec56a3 style: 4matting 2025-09-08 22:11:35 -04:00
3wordchant 40b5c5cd63 feat: roll out pre-deploy changes to rollback and upgrade 2025-09-08 22:11:30 -04:00
3wordchant 14d3f1f669 feat: show image differences in pre-deploy overview 2025-09-08 22:11:15 -04:00
3wordchant 8e8f7715a2 refactor: move secret- and config-gathering to separate file 2025-09-08 22:11:10 -04:00
3wordchant 745651e962 refactor: resolve circular import 2025-09-08 22:11:04 -04:00
3wordchant c2848cb3ec feat: add GetSecretNamesForStack, tidy up GetConfigNamesForStack 2025-09-08 22:10:58 -04:00
3wordchant f3edfea744 feat: warn instead of error on missing config version 2025-09-08 22:10:52 -04:00
3wordchant 719722a25b feat: working config version comparison 2025-09-08 22:10:48 -04:00
3wordchant 7f9f8f9d6a feat: skip empty sections in deploy overview 2025-09-08 22:10:43 -04:00
3wordchant 155df518dd feat: only show remote configs used in deployment 2025-09-08 22:10:39 -04:00
3wordchant 984bdd8792 feat: add some spacing, might delete 2025-09-08 22:10:29 -04:00
3wordchant 0b7c38c213 refactor: tidy up a little 2025-09-08 22:10:24 -04:00
3wordchant 1df0de2e65 WIP: Working secret and config versions during deploy overview 2025-09-08 22:06:27 -04:00
3wordchant 6d634ea4e2 WIP: Initial stab at secrets/configs/images 2025-09-08 22:06:27 -04:00
decentral1se dc207a0138 test: lang parsing
See toolshed/abra#652
2025-09-06 08:38:38 +02:00
decentral1se 02add8c3ef chore: make i18n 2025-09-06 08:28:53 +02:00
decentral1se 560d609013 test: ensure autocomplete output works
See toolshed/abra#649
2025-09-06 08:26:50 +02:00
decentral1se b4c9fbfe6d fix: use local flag
See toolshed/abra#648
2025-09-06 08:24:37 +02:00
3wordchant 7f456a3f24 chore: 4matting 2025-09-05 21:12:21 +00:00
3wordchant 709a9ad659 fix: use LANG prefix instead of full value 2025-09-05 21:12:21 +00:00
Weblate a468245413 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-09-05 21:09:51 +00:00
3wordchant e895b852bc chore: update POT 2025-09-05 17:09:39 -04:00
3wordchant bef92d53a8 fix: don't translate blank string 2025-09-05 13:33:51 -04:00
3wordchant a4b47b431b fix: temporary fix to #648 2025-09-05 13:29:31 -04:00
decentral1se bddf8039af test: ensure previous versions not deleted
See toolshed/abra#615
2025-09-04 08:26:21 +00:00
Weblate d74e760940 Update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-09-04 08:14:12 +00:00
ChasquiLabo 7f75d25d56 Translated using Weblate (Spanish)
Currently translated at 10.4% (116 of 1105 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2025-09-04 08:14:12 +00:00
3wordchant 0bb6d9609c Add test to make sure non-chaos hash deploy fails..
..and make sure _ensure_env_version passes through variable correctly.
2025-09-03 14:25:42 -04:00
3wordchant e858dcdd14 Whoops, gettext 2025-09-03 14:25:41 -04:00
3wordchant 3606349a4a Error out if env version is chaos-y and --chaos not provided
Re #554
2025-09-03 14:25:22 -04:00
decentral1se 4547cf2579 fix: help/version override for translation
See toolshed/abra#628
2025-09-03 08:58:31 +02:00
decentral1se e1f029d2db chore: make i18n 2025-09-03 00:25:23 +02:00
decentral1se cf2952dc65 chore: add missing i18n on --latest [ci skip] 2025-09-03 00:24:55 +02:00
decentral1se 2291712661 fix: abra app move docs/patches 2025-09-01 13:48:10 +02:00
decentral1se f0e2b012c6 chore: make i18n 2025-09-01 11:17:44 +02:00
decentral1se 9c37b9b748 test: app move (basics) 2025-09-01 11:17:39 +02:00
decentral1se 824f314472 refactor: app move review pass 2025-09-01 11:17:22 +02:00
p4u1 61849a358c feat(app): Adds abra app move command 2025-09-01 06:50:11 +02:00
decentral1se 8c7b06a7bb chore: add missing authors [ci skip] 2025-08-30 13:40:39 +02:00
decentral1se 4c9abbf925 feat: template example domain in release notes
See toolshed/organising#521
2025-08-30 12:45:48 +02:00
decentral1se 09176801e1 feat: warn for secret generation 2025-08-30 12:27:29 +02:00
decentral1se 36d4648114 feat: add config dir help
See toolshed/organising#470
2025-08-30 12:12:40 +02:00
decentral1se 83ca2a63d1 fix: support ValidArgs translation
See toolshed/abra#632
2025-08-30 12:02:13 +02:00
decentral1se e25ce5d1a0 chore: make i18n 2025-08-30 11:46:48 +02:00
decentral1se 4cb5091d50 Merge remote-tracking branch 'weblate/main' 2025-08-30 11:45:59 +02:00
decentral1se 4bfbc53b94 feat: support alias translation
See toolshed/abra#627
2025-08-30 11:39:49 +02:00
decentral1se 52f02ad9b9 translate: support usage translations
See toolshed/abra#628
2025-08-30 10:46:09 +02:00
ChasquiLabo c0acc3663b Translated using Weblate (Spanish)
Currently translated at 10.8% (114 of 1048 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2025-08-29 21:45:43 +00:00
decentral1se d5c66020ad refactor!: --ignore-env-version is --latest *only* on deploy
See toolshed/abra#617
2025-08-29 16:38:49 +02:00
decentral1se 86ba006e17 chore: fix merge conflict from weblate 2025-08-29 13:58:10 +02:00
decentral1se cb4355e61e docs: add chasqui [ci skip] 2025-08-29 10:20:51 +02:00
decentral1se 069f8fec54 chore: sort AUTHORS 2025-08-29 10:20:05 +02:00
ChasquiLabo c2819b9366 Translated using Weblate (Spanish)
Currently translated at 10.8% (114 of 1048 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2025-08-29 08:00:42 +00:00
decentral1se 850264d085 chore: make i18n 2025-08-29 09:58:42 +02:00
decentral1se e019142c9e fix: show when logs dont get collected
See toolshed/abra#575
2025-08-29 09:58:26 +02:00
ChasquiLabo e23c6197b5 Translated using Weblate (Spanish)
Currently translated at 10.7% (113 of 1048 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2025-08-28 22:30:49 +00:00
3wordchant 6539b1be7e Appease formatter (kinda weird?) 2025-08-28 11:44:21 -04:00
3wordchant 02b520200e Mark command short descriptions with translators: tag 2025-08-28 11:33:22 -04:00
3wordchant acb6170768 Add make find-tests 2025-08-28 14:26:27 +00:00
decentral1se e04af4e582 chore: make i18n 2025-08-28 16:18:10 +02:00
decentral1se 8bf0d7addc fix: wait for containers to go away
See toolshed/abra#564
2025-08-28 16:17:37 +02:00
3wordchant 20909695e0 Translated using Weblate (Spanish)
Currently translated at 5.2% (55 of 1043 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2025-08-28 01:00:51 +00:00
ChasquiLabo baf7631105 Translated using Weblate (Spanish)
Currently translated at 5.2% (55 of 1043 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2025-08-28 01:00:51 +00:00
Weblate 57fbc4c061 Update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-08-27 19:32:09 +00:00
3wordchant c43e68ea6a Fix Makefile & .drone.yml 2025-08-27 15:29:45 -04:00
3wordchant d3d3358a79 Sort files for make update-pot 2025-08-27 13:00:40 -04:00
3wordchant cb310c56b5 Add comments_tag 2025-08-27 12:58:06 -04:00
3wordchant 67c0a64f60 Run xgettext-go 2025-08-27 12:54:05 -04:00
3wordchant db5da1656a Semi-automated mass string commenting 2025-08-27 12:54:05 -04:00
3wordchant 5b6254a243 Update check-pot-changes 2025-08-27 16:50:27 +00:00
3wordchant 2a0857c388 Fix golang image tag 2025-08-27 16:50:27 +00:00
3wordchant 92a0294f2f Tweak Drone order, switch image for xgettext-go 2025-08-27 16:50:27 +00:00
3wordchant f79775d726 Tiny tweak to test Drone explosion 2025-08-27 16:50:27 +00:00
3wordchant e095bbe9d6 Fix check-pot-changes makefile command 2025-08-27 16:50:27 +00:00
3wordchant eb12127578 Fix makefile command for xgettext-go status build step 2025-08-27 16:50:27 +00:00
3wordchant 82779b233b Move check-pot-changes to Makefile 2025-08-27 16:50:27 +00:00
3wordchant ac4ac1d40f Adventurous xgettext-go automation 2025-08-27 16:50:27 +00:00
p4u1 7c31e4dc45 feat(secrets): Reading from stdin and reproducible secret list(#614)
Reviewed-on: toolshed/abra#614
Reviewed-by: decentral1se <decentral1se@noreply.git.coopcloud.tech>
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2025-08-27 15:37:43 +00:00
3wordchant b86cd2e85f Update localez 2025-08-26 13:37:15 -04:00
3wordchant 7dd7f763f4 Moar PR feedback 2025-08-26 13:15:06 -04:00
3wordchant 7b7477062f Implement PR feedback 2025-08-25 18:12:15 -04:00
3wordchant 238647a987 Prompt for secrets if not provided on CLI
Re #646
2025-08-25 17:16:50 -04:00
decentral1se f39eab8f1e test: pass ABRA_DIR to unit test on CI 2025-08-25 11:55:33 +02:00
decentral1se 6a52575ae0 refactor!: do not set default timeout
See toolshed/abra#596

Quite some `i18n.G` additions along the way!
2025-08-25 11:49:11 +02:00
decentral1se 44a7d288af test(unit): ensure timeout handling 2025-08-25 11:48:45 +02:00
decentral1se 97377dea39 test(integration): ensure timeout handling 2025-08-25 11:48:44 +02:00
decentral1se a2940a84b3 chore: restructure unit testing resources 2025-08-25 11:48:43 +02:00
decentral1se 0c708f6592 feat: recipe sync shows changes
See toolshed/abra#579
2025-08-24 15:56:26 +02:00
decentral1se 4cb660c348 fix: more robust -p failure handling
See toolshed/abra#576
2025-08-24 13:05:03 +02:00
decentral1se 42dde0930d fix: handle translations 2025-08-24 09:23:15 +02:00
decentral1se 7ccbbe8916 feat: include local recipes on auto-complete 2025-08-24 09:23:14 +02:00
decentral1se e421e00631 fix: don't ensure latest on auto-complete
See toolshed/abra#567
2025-08-24 09:23:13 +02:00
decentral1se a5104336a2 feat: catalogue sync command 2025-08-24 09:23:12 +02:00
decentral1se 4e205cf13e feat: translation support
See toolshed/abra#483
2025-08-23 17:55:56 +02:00
decentral1se 5cf6048ecb test: also wipe env version 2025-08-19 09:49:02 +02:00
decentral1se 3e2797c433 test: fixups for latest nightly failures 2025-08-19 09:41:37 +02:00
decentral1se df89e8143a chore: clean cruft / formatting / add whitespace 2025-08-19 07:30:55 +00:00
decentral1se b4ddd3e77c feat: handle generate=false env var mod
See toolshed/organising#461
2025-08-19 07:30:55 +00:00
decentral1se 81c28e3006 test: appease the tester 2025-08-18 20:50:50 +02:00
decentral1se 34d2e3b092 chore: go mod vendor 2025-08-18 20:50:49 +02:00
decentral1se 1894c2f5fc chore: appease formatter 2025-08-18 20:50:48 +02:00
decentral1se e0bd03bec3 chore: bump deps 2025-08-18 20:50:47 +02:00
decentral1se 77ff146991 fix: better parsing errors
See toolshed/organising#608
See toolshed/organising#531
2025-08-18 20:50:46 +02:00
decentral1se 6fad1a1dcc test: check app list doesn't explode if missing .env
See toolshed/abra#560
2025-08-18 09:47:10 +02:00
decentral1se a90e239547 refactor!: ensure insert/remove not arbitrary 2025-08-18 09:25:31 +02:00
decentral1se 9ee094fcd7 docs: secret removal examples 2025-08-18 09:25:20 +02:00
decentral1se 1aa7016789 fix: skip name validation for remote recipes
See toolshed/abra#601
2025-08-18 08:56:52 +02:00
decentral1se 60b3af1fa4 test: retrieve abra-test-recipe by hand now 2025-08-18 06:32:41 +00:00
decentral1se 5f4b5e0fad fix: return error, not log.Fatal
See toolshed/abra#582
2025-08-18 06:32:41 +00:00
decentral1se feadfca0d6 refactor: move ensureCtx closer to usage 2025-08-18 06:32:41 +00:00
decentral1se 73d4ee1c98 refactor: always validate recipe
This can slow things significantly down by requiring the catalogue and
if you don't have that, cause a slow `git clone`. However, the current
behvaiour is very confusing because it never actually checks if what the
user passes is actually a recipe. `abra recipe fetch DOESNTEXIST` gives
a better error to the user now. I'm hoping we can speed up the catalogue
handling at some point.
2025-08-18 06:32:41 +00:00
decentral1se f46c18c8d7 fix: warn on unknown server
See toolshed/abra#581
2025-08-18 08:29:14 +02:00
decentral1se f5a843bd90 feat: remove old app configs
See toolshed/abra#577
2025-08-17 13:27:35 +00:00
decentral1se fac372dc73 test: use latest release for check 2025-08-17 15:18:05 +02:00
decentral1se 8a3be01c3e fix: $ABRA_DIR/servers subdirs also 0700
See 38f308910a
See toolshed/abra#580
2025-08-17 14:13:47 +02:00
decentral1se 4193d63d23 test: advertise locally to avoid multiple ip error 2025-08-17 14:04:00 +02:00
decentral1se 38f308910a fix: $ABRA_DIR/servers=0700, $ABRA_DIR/servers/foo=0600
See toolshed/abra#580
2025-08-17 14:03:15 +02:00
decentral1se 4aaa7400b8 fix: also ensure server is created with 0600
See 6849e3554d
2025-08-17 13:32:37 +02:00
decentral1se 091611b984 feat: add volume arg to volume rm
See toolshed/abra#574
2025-08-17 11:16:30 +00:00
decentral1se 2cfc40dc28 fix: ensure recipe with undeploy
See toolshed/abra#573
2025-08-17 09:36:48 +00:00
decentral1se 6849e3554d fix: ensure $ABRA_DIR/servers is 0600
Also remove deprecated folders while I'm here: `vendor` / `backup`

See toolshed/abra#580
2025-08-17 09:17:00 +00:00
decentral1se 452de7fdc2 docs: show HOWTO generate in abra man help
See toolshed/abra#568
2025-08-13 13:08:48 +02:00
decentral1se 952d768ab0 docs: show app secret rm example
Closes toolshed/abra#558
2025-08-12 21:34:57 +02:00
iexos 2c91d2040e fix: app ls -S didn't show updates sometimes (#561) 2025-08-12 13:10:16 +00:00
3wordchant eff4435971 ABRA_TEST_DOMAIN → TEST_SERVER 2025-08-12 12:18:49 +00:00
3wordchant 032fe99086 Appease formatter 2025-08-12 12:18:29 +00:00
3wordchant 7add56df00 Add integration tests for new "--chaos to blast past lint errors"
Re toolshed/organising#497
2025-08-12 12:18:29 +00:00
3wordchant 0ab05cece2 Reformat linting errors in LintForErrors
See toolshed/organising#497 (comment)
2025-08-12 12:18:29 +00:00
3wordchant c63f6db61e WARN on recipe linting errors during --chaos..
..and show multiple linting errors instead of bailing on the first one.

Re #497
2025-08-12 12:18:29 +00:00
decentral1se 56a68dfa91 chore: bump deps 2025-08-12 05:17:15 +00:00
p4u1 157d131b37 feat: Retrieves auth token from image
This allows using a private registry for an image.
To use it, you have to run docker login on your local machine before
running abra deploy.
2025-08-12 05:01:42 +00:00
p4u1 3fae036db2 change to debug log 2025-08-11 12:47:43 +00:00
p4u1 ce9d0934b6 fix: Does not error when recipes folder does not exist in app new 2025-08-11 12:47:43 +00:00
3wordchant a32e30374f Translated using Weblate (Spanish)
Currently translated at 100.0% (1 of 1 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2025-08-04 16:51:35 +02:00
3wordchant cf46569f04 Add stub es catalogue 2025-08-04 16:51:34 +02:00
3wordchant 022606c13c Add default POT catalogue, don't alias gotext.Get 2025-08-04 16:51:33 +02:00
decentral1se 8cfda5229f feat: weblate 2025-08-04 16:51:26 +02:00
decentral1se 855a4c37c4 chore: bump installer script 2025-08-04 15:26:24 +02:00
3wordchant 7c3b740e14 Update the server used to deploy the installer script 2025-06-07 15:01:31 +01:00
decentral1se 2fbef41a3a test: clean up properly 2025-04-24 14:29:54 +02:00
decentral1se 6fb41e5300 fix: dont parse chaos version
See toolshed/abra#547
2025-04-24 11:24:14 +00:00
decentral1se 1432f480c7 fix: -T/--tty disables TTY remote request
See toolshed/abra#499
2025-04-24 08:57:53 +00:00
decentral1se 83af39771b test: drop unused tagHash 2025-04-24 10:57:34 +02:00
decentral1se 4d1333202e test: flaky test when no RC is available
Fixes https://build.coopcloud.tech/toolshed/abra/2760/1/5
2025-04-24 10:33:49 +02:00
decentral1se 55c24f070c feat: cancel git clone ops gracefully
See toolshed/abra#528
2025-04-22 22:56:10 +02:00
decentral1se 229e8eb9da feat: --ssh/--force for recipe fetch
See toolshed/organising#546
2025-04-22 09:35:36 +00:00
decentral1se b3ab95750e fix: trim final newline on release note
Follow-up toolshed/abra#544
2025-04-22 09:23:28 +02:00
decentral1se de009921a2 fix: show release notes once
See toolshed/abra#543
2025-04-22 05:47:03 +00:00
decentral1se d081bbaefa feat: auto select single server
See toolshed/organising#513
2025-04-21 21:06:29 +02:00
decentral1se 515b5466ca docs: add missing arg
See toolshed/abra#540
2025-04-21 20:17:39 +02:00
decentral1se 6965799bdc chore: publish 0.10.0-beta 2025-04-21 19:11:31 +02:00
decentral1se f75c9a6259 test: clean up test server correctly
Fixes https://build.coopcloud.tech/toolshed/abra/2723/1/5
2025-04-21 19:03:49 +02:00
decentral1se a43a092ba7 fix: fetch recipe for "app list -S" 2025-04-19 07:28:15 +00:00
p4u1 fa084a61d2 fix(lint): Improves error message if a lint rule errors
This was detected while debugging toolshed/abra#534
2025-04-16 05:12:19 +00:00
decentral1se 895a7fe7d6 fix: don't overwrite recipeVersion
Fixes https://build.coopcloud.tech/toolshed/abra/2709/1/5
2025-04-15 10:51:53 +02:00
decentral1se 742a726778 fix: latest commit for new recipe version
See toolshed/abra#527
2025-04-14 23:55:19 +02:00
decentral1se 2b9a185aff build: go mod tidy 2025-03-23 11:10:05 +01:00
decentral1se b7c1e87c0b build: go mod vendor 2025-03-23 11:08:17 +01:00
decentral1se cdfb8a08bb chore: publish 0.10.0-rc2-beta 2025-03-23 11:05:03 +01:00
decentral1se 8943cea13f test: get latest via helper 2025-03-23 11:00:04 +01:00
decentral1se 6d64e0edd3 fix: sshPkg.Fatal has more nuance
See toolshed/abra#507
2025-03-23 10:27:58 +01:00
decentral1se 47045ca8f1 feat: improved deploy progress reporting
See toolshed/abra#478
2025-03-23 09:13:36 +00:00
Apfelwurm d0f982456e add charset modifier to secret generation (#521)
since we need special chars in passwords for a recipe we are working on, i have added the option to specify a charset in the same way as the length can be setted.
i did not change anything in the behaviour, so if length is not specified, the charset gets ignored whether it is there or not.

you can specify the following:
`charset=default` - Results in passgen.AlphabetDefault being used
`charset=special` - Results in passgen.AlphabetSpecial being used
`charset=safespecial` - Results in `!@#%^&*_-+=` being used (so it is AlphabetSpecial without the dollar sign)
`charset=default,special` or `charset=special,default` - Results in passgen.AlphabetDefault + passgen.AlphabetSpecial being used
`charset=default,safespecial` or `charset=safespecial,default` - Results in passgen.AlphabetDefault + `!@#%^&*_-+=` being used ((so it is AlphabetSpecial without the dollar sign)

PR for the docs: toolshed/docs.coopcloud.tech#271

Co-authored-by: p4u1 <p4u1@noreply.git.coopcloud.tech>
Reviewed-on: toolshed/abra#521
Reviewed-by: p4u1 <p4u1@noreply.git.coopcloud.tech>
Co-authored-by: Apfelwurm <Alexander@volzit.de>
Co-committed-by: Apfelwurm <Alexander@volzit.de>
2025-03-21 10:29:21 +00:00
p4u1 80ad6c6681 fix(app): Properly detects release notes added after a release when upgrading an app (#523)
Fixes toolshed/abra#488

Reviewed-on: toolshed/abra#523
Reviewed-by: decentral1se <decentral1se@noreply.git.coopcloud.tech>
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2025-03-21 10:25:46 +00:00
decentral1se cb63cfe9c2 refactor: chaos redundant, shorter message 2025-03-16 13:54:07 +01:00
decentral1se d1e49d17ce test: on-demand integration tests 2025-03-16 13:22:50 +01:00
decentral1se 1574aa0631 refactor!: status between service/image
See toolshed/abra#487 (comment)
2025-03-16 12:42:09 +01:00
decentral1se 1723025fbf build: go 1.24
We were running behind and there were quite some deprecations to update.
This was mostly in the upstream copy/pasta package but seems quite
minimal.
2025-03-16 12:31:45 +01:00
decentral1se a2b678caf6 test: reset after undeploy for a clean env version
Follows toolshed/abra#510.
See https://build.coopcloud.tech/toolshed/abra/2620/1/5.
2025-03-16 11:49:38 +01:00
p4u1 0a371ec360 fix: integration tests 2025-03-13 08:31:11 +01:00
p4u1 e58a716fe1 feat(deploy): Simplifies deploy overview (#508)
This simplifies the deploy overview, to only show 3 version fields:
- CURRENT DEPLOYMENT
- CURRENT ENV
- NEW DEPLOYMENT

It also fixes a few errors around version detection

Reviewed-on: toolshed/abra#508
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2025-03-12 16:13:24 +00:00
p4u1 d09a19a385 fix: Adds chaos flag to restart command 2025-02-11 10:01:44 +00:00
p4u1 cee808ff06 fix: Changes how the deploy version is detected in app deploy command 2025-02-11 10:01:44 +00:00
p4u1 4326d1d259 fix: Sorts git tags with tagcmp 2025-02-11 10:01:44 +00:00
p4u1 b976872f77 fix(overview): Adds linebreak after compose file in deploy overview 2025-02-11 09:57:09 +00:00
p4u1 7b6ea76437 fix(secret): Checks for enough arguments 2025-02-11 09:55:03 +00:00
p4u1 9069758969 fix(cmd): Uses uppercase t for tty shorthand flag 2025-02-10 15:13:26 +01:00
p4u1 15d6b1a2a5 fix: app new with chaos should just take the local repo as it is (#495)
Fixes toolshed/abra#494

Reviewed-on: toolshed/abra#495
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2025-02-10 14:00:42 +00:00
decentral1se 8a7fe4ca07 fix: prompt, skip adding if next present
toolshed/abra#486
2025-01-17 17:46:41 +01:00
decentral1se 64ad60663f test: adjust for new abra-test-recipe version
See toolshed/abra#470
2025-01-09 13:14:47 +00:00
decentral1se cb3f46b46e fix: redirect to stderr for machine output
See toolshed/abra#477
2025-01-09 11:23:36 +00:00
decentral1se 41e514ae9a test: reset after deploy 2025-01-09 11:54:39 +01:00
decentral1se 086b4828ff docs: better comments, remove redundant output check 2025-01-09 11:54:38 +01:00
decentral1se ed263854d4 fix: show N/A if env version unknown
See toolshed/abra#478
2025-01-09 11:54:37 +01:00
decentral1se eb6fe4ba6e fix: dont set chaos label if no chaos
See toolshed/abra#478
2025-01-09 11:54:36 +01:00
decentral1se 993172d31b test: ensure .env version written 2025-01-08 13:42:35 +00:00
decentral1se c70b6e72a7 test: ensure unstaged changes preserved 2025-01-08 13:42:35 +00:00
decentral1se 22e4dd7fca fix: app new from chaos changes
See toolshed/abra#471
2025-01-08 13:42:35 +00:00
decentral1se b6009057a8 docs: note temp autocomplete, less whitespace 2025-01-08 12:10:17 +01:00
decentral1se b978f04910 fix: use "sudo tee" to avoid permissions error
See toolshed/abra#474
2025-01-08 12:09:51 +01:00
decentral1se 3ac29d54d9 chore: go update des/vendor 2025-01-07 16:59:56 +01:00
decentral1se 877c17fab5 test: re-enable this one 2025-01-05 16:46:48 +01:00
decentral1se f01fd26ce3 test: git status output 2025-01-05 16:46:38 +01:00
decentral1se 273c165a41 docs: --chaos/-C handling for catalogue generate 2025-01-05 16:46:20 +01:00
decentral1se c88fc66c99 test: moar chaos stability 😌 [ci skip] 2025-01-05 16:12:06 +01:00
decentral1se 9b271a6963 docs: moar authors [ci skip] 2025-01-05 15:53:17 +01:00
decentral1se 8af87aa382 chore: upgrade goreleaser 2025-01-05 12:47:46 +01:00
decentral1se ac0b9cd052 chore: new RC 2025-01-05 12:42:42 +01:00
decentral1se 4923984e84 fix: not flaky catalogue generate
See toolshed/abra#464
2025-01-05 12:08:10 +01:00
decentral1se 2bc77de751 test: ensure main branch on new recipe 2025-01-05 10:38:34 +01:00
decentral1se b3a2402cec chore: remove redundant logging 2025-01-05 10:38:24 +01:00
decentral1se a773fd4256 chore: spacing 2025-01-05 10:38:13 +01:00
decentral1se b1a0d54bd3 fix: default to main then master 2025-01-05 10:37:30 +01:00
decentral1se 3869d6bce9 Revert "test: try uppercase naming (following UI)"
This reverts commit 0ff07ab224.

Wrong UI, trying again via Drone.
2025-01-04 11:55:13 +01:00
decentral1se 0ff07ab224 test: try uppercase naming (following UI) 2025-01-04 11:47:33 +01:00
decentral1se 936c1b0626 fix: use new syntax 2025-01-04 11:20:17 +01:00
decentral1se b576cba227 fix: use abra-bot 2025-01-04 11:09:14 +01:00
decentral1se d087f3debf chore: go mod tidy 2025-01-03 21:25:11 +01:00
decentral1se e57a6d87a3 test: use recipes url 2025-01-03 20:35:19 +01:00
decentral1se 74b64099de fix: skip example && fix generate 2025-01-03 20:24:49 +01:00
decentral1se 354712ca46 fix: remove old docstring 2025-01-03 20:23:48 +01:00
decentral1se 81cdc843ec fix: coop-cloud -> toolshed 2025-01-03 20:23:27 +01:00
decentral1se d2931e3af0 fix: drop warning, can use this now 2025-01-03 20:21:20 +01:00
decentral1se b9f2d1f568 chore: go mod vendor / tidy 2025-01-03 20:21:06 +01:00
decentral1se a379b31a19 refactor: dont use topics
See coop-cloud/organising#377
See coop-cloud/organising#569
2025-01-03 17:01:37 +00:00
decentral1se 17e15dba77 chore: spacing / wording on log message [ci skip] 2025-01-03 17:53:22 +01:00
decentral1se 1194f3b228 refactor!: maintain "domain"
See toolshed/organising#636
2025-01-03 08:24:03 +01:00
decentral1se 2dc8034c16 fix: no dot dirs for server selection 2025-01-03 08:16:30 +01:00
decentral1se c5ddeb2d8a fix: dont update catalogue on autocomplete 2025-01-03 08:10:57 +01:00
decentral1se 0a63f9ce27 fix: undeploy handles chaos/unstaged in overview
Follows 3a71dc47f8
2025-01-02 21:50:23 +01:00
decentral1se 3a71dc47f8 fix: more env version write tests
See toolshed/organising#661
2025-01-02 21:20:40 +01:00
decentral1se f07c64f7b8 fix: sort abra app env output 2025-01-02 16:40:23 +01:00
decentral1se dd03c40e10 feat: abra app env 2025-01-02 16:32:32 +01:00
decentral1se 48198d55bd chore: rename [ci skip] 2025-01-02 11:31:15 +01:00
decentral1se c0931b96d8 fix: use same wording 2025-01-02 11:31:04 +01:00
decentral1se 64ea0f9684 test: drop, version is written on app new [ci skip] 2025-01-02 11:26:27 +01:00
decentral1se b0cd8ccbb9 refactor/fix: deploy/upgrade/rollback
See coop-cloud/abra#461
2025-01-02 11:12:38 +01:00
decentral1se 5975be6870 fix: unstaged changes handling
See toolshed/organising#651
2024-12-31 16:37:02 +01:00
decentral1se bfed51a69c fix: no newline on status in logs 2024-12-31 08:26:35 +01:00
decentral1se 5d0faf5e13 fix: only log once for the loaded app 2024-12-31 08:26:19 +01:00
decentral1se cd6af9708c docs: <> -> [] 2024-12-31 08:26:01 +01:00
decentral1se ef95bce1e4 fix: use default styles 2024-12-30 18:10:01 +01:00
decentral1se a159583874 chore: make format 2024-12-30 18:07:58 +01:00
decentral1se e3b0500875 fix: dont output error twice 2024-12-30 18:05:26 +01:00
decentral1se 994310a4ff refactor!: use charm defaults 2024-12-30 18:05:04 +01:00
philippr 74108b0dd9 fix: create release dir in recipe if not exists #660 2024-12-29 18:12:53 +00:00
decentral1se 3727c7fa78 test: ensure catalogue 2024-12-29 00:44:47 +01:00
decentral1se 9a4414fd13 test: fix failing upgrade test 2024-12-29 00:14:16 +01:00
decentral1se 9f189680f3 fix: less newline 2024-12-28 23:47:50 +01:00
decentral1se 356e527f1f refactor!: upgrade/rollback vertical render / ui fixes
See toolshed/organising#658
2024-12-28 23:35:47 +01:00
decentral1se 7ec61c6d03 fix: sort versions upgrade/rollback/list
See toolshed/organising#649
2024-12-28 23:10:22 +01:00
decentral1se fab93a559a fix: more robust <app> autocomplete + error handling
See toolshed/organising#652
2024-12-28 22:22:13 +01:00
decentral1se 8ac31330be fix: error out if missing "deploy.labels"
See toolshed/organising#643
2024-12-28 21:55:02 +01:00
decentral1se 03000c25e0 refactor: parametrize default value 2024-12-28 21:54:14 +01:00
decentral1se 3f32dbb1a3 fix: better "server add" failure
See toolshed/organising#570
2024-12-28 21:17:51 +01:00
decentral1se 27f68b1dc5 refactor!: recipe fetch [recipe | --all]
See toolshed/organising#639
2024-12-28 20:55:25 +01:00
decentral1se a0da5299fe feat: write undeploy version
See toolshed/organising#633
2024-12-28 19:42:01 +00:00
decentral1se 866c5c4536 test: even moar integration suite patches 2024-12-28 17:16:53 +01:00
decentral1se dc4c6784cb test: integration test patches 2024-12-28 16:39:58 +01:00
decentral1se 97959ef5da refactor!: vertical render & UI/UX fixes
See coop-cloud/abra#454
2024-12-28 15:00:31 +00:00
p4u1 b6573720ec fix: Adds chaos flag to app/cp command 2024-12-28 13:56:43 +01:00
decentral1se 4e8995cc0e fix: moar integration test patches
See toolshed/organising#650
2024-12-27 21:55:01 +01:00
decentral1se efb3fd8759 test: moar fixes
See toolshed/organising#650
2024-12-27 21:16:15 +01:00
decentral1se 008582c3d9 test: fixes for test suite post-cobra migrate
See toolshed/organising#650
2024-12-27 20:44:07 +01:00
decentral1se 8fa20e2c7f feat: new backup/restore 2024-12-27 19:27:56 +01:00
decentral1se aa1dc795ef fix: disable default complete func 2024-12-27 13:55:45 +01:00
decentral1se 18df498295 chore: deps and vendor 2024-12-27 13:47:45 +01:00
decentral1se 671e1ca276 refactor!: cobra migrate 2024-12-27 13:32:29 +01:00
decentral1se 0df2b15c33 fix: reinstate no-input as a global flag 2024-12-23 11:27:11 +01:00
decentral1se 3f29084664 chore: refactor / docstrings 2024-12-21 19:22:26 +01:00
decentral1se 0bb25a00ec test: migrated server 2024-12-21 19:21:50 +01:00
Ammar Hussein 28c7676413 replace code-descriptive comments with method level comments 2024-12-15 09:53:28 -08:00
Ammar Hussein 730fef09a3 add test for SwitchToMain 2024-12-14 18:41:34 -08:00
Ammar Hussein 8d076a308a bubble up errors on branch switch 2024-12-14 18:26:22 -08:00
Ammar Hussein 9510c04aeb new recipe default branch main instead of master 2024-12-12 19:08:18 -08:00
decentral1se d9e60afd71 chore: upgrade go version 2024-12-02 01:47:45 +01:00
decentral1se 31fa9b1a7a chore: make deps, go mod vendor 2024-12-02 01:45:06 +01:00
Ammar Hussein f664599836 [fix] chaos mode always fails deploy 2024-11-30 20:10:04 -08:00
Ammar Hussein bba1640913 Merge branch 'ammaratef45-removeDomainCheck' 2024-11-27 11:48:12 -08:00
Ammar Hussein 7b54c2b5b9 remove whitespace 2024-11-27 11:38:49 -08:00
Ammar Hussein 8ee1947fe9 remove -D on server add 2024-11-25 17:23:00 -08:00
decentral1se b313b0a145 fix: use old auto-completion for 0.9.x compat
See toolshed/organising#644

Partial revert of 1f8662cd95
2024-10-27 08:54:43 +01:00
decentral1se 1f9b863be0 fix: appease formatter, ignore vendor 2024-10-21 16:46:39 +02:00
decentral1se 3b3ce85ef9 fix: rebase coop-cloud/organising#533 2024-10-21 16:39:36 +02:00
decentral1se 1f8662cd95 refactor: urfave v3 2024-10-21 16:39:27 +02:00
decentral1se 375e17a4a0 refactor: urfave v2 2024-10-21 11:00:35 +02:00
decentral1se 04aec8232f chore: vendor 2024-08-04 11:06:58 +02:00
decentral1se 2a5985e44e build: drop 2MB with GCFLAGS [ci skip] 2024-07-27 12:56:43 +02:00
decentral1se c65be64e7d fix: dont checkout version for abra app undeploy
See coop-cloud/organising#628
2024-07-24 16:09:27 +02:00
decentral1se fd8652e26d fix: --chaos/--offline for abra app ps
See coop-cloud/organising#628
See coop-cloud/organising#629
2024-07-24 16:09:03 +02:00
decentral1se 518c5795f4 fix: avoid overwriting non version env vars
See coop-cloud/organising#630
2024-07-24 16:07:08 +02:00
decentral1se 827edcb0da test: full width for CI testing [ci skip]
Also clean up the .env.sample.
2024-07-18 11:03:02 +02:00
decentral1se 05489a129c test: re-create serer for setup [ci skip] 2024-07-17 14:32:53 +02:00
decentral1se c02e11eb0a test: fix order of teardown [ci skip] 2024-07-17 14:15:03 +02:00
decentral1se 8b8e158664 test: int suite fixes 2024-07-17 14:05:46 +02:00
decentral1se e5a6dea10c fix: catch ctrl-c again; less cryptic logging 2024-07-17 10:09:09 +02:00
decentral1se 1132b09b5b fix: error out for invalid env versions 2024-07-17 10:08:51 +02:00
decentral1se b2436174b0 chore: more logging for env versions 2024-07-17 10:08:32 +02:00
decentral1se ea10019068 fix: "secret insert" respects env version 2024-07-17 10:08:13 +02:00
decentral1se 9b0b3c2e4c fix: override version from CLI
See coop-cloud/organising#541
2024-07-17 10:07:47 +02:00
decentral1se 8084bff104 test: env version tests
See coop-cloud/organising#541
2024-07-17 10:06:46 +02:00
decentral1se d22e2c38ce test: just build main 2024-07-17 08:29:58 +02:00
decentral1se e945087f79 test: env version writing tests 2024-07-17 08:27:12 +02:00
decentral1se 7734dd555d fix: spacer between multiple versions 2024-07-17 02:12:26 +02:00
decentral1se aedf5e5ff7 fix: dont write commented out versions
See coop-cloud/organising#626
2024-07-17 01:56:28 +02:00
decentral1se 95c598d030 feat: "app new" supports writing env files
And, automagically, chaos commit hashes.
2024-07-17 01:45:19 +02:00
decentral1se 56068362e8 fix: write versions on deploy/upgrade/rollback
See coop-cloud/organising#625
2024-07-17 01:29:49 +02:00
decentral1se cf14731b46 refactor: "false" -> CHAOS_DEFAULT 2024-07-17 01:23:12 +02:00
decentral1se 486cfa68d8 test: explode on failures
Closes coop-cloud/organising#623
2024-07-17 00:16:47 +02:00
decentral1se 1718903834 test: reset recipe before undeploying [ci skip] 2024-07-17 00:00:06 +02:00
decentral1se eb9894e5bb test: dont clone if exists [ci skip] 2024-07-16 23:51:28 +02:00
decentral1se a2116774e8 test: ensure catalogue in place [ci skip] 2024-07-16 23:46:02 +02:00
decentral1se d2efdf8bf5 test: adjust output checking [ci skip] 2024-07-16 23:39:10 +02:00
decentral1se b15c05929c test: adjust output checking [ci skip] 2024-07-16 23:32:12 +02:00
decentral1se f167a91868 test: skip for now, it's flaking again [ci skip]
We need to solve coop-cloud/organising#541
2024-07-16 23:28:18 +02:00
decentral1se 8cded8752a test: ensure correct server for diffing [ci skip] 2024-07-16 23:25:17 +02:00
decentral1se d1876e2fae test: do exact diff of JSON for integration
See coop-cloud/organising#627
2024-07-16 23:19:36 +02:00
decentral1se e42a1bca29 fix: add chaos/deploy versiosn back to ps output
Fix to support alakazam parsing
2024-07-16 22:47:47 +02:00
decentral1se b5493ba059 refactor: CreateTable2 -> CreateTable [ci skip] 2024-07-16 22:45:03 +02:00
decentral1se a41a36b8fd fix: dont lock existing version on rollback
Otherwise, we can't select previous versions.
2024-07-16 17:35:15 +02:00
decentral1se de006782b6 refactor: tablewriter -> lipgloss
Also the jsontable impl. is dropped also. Output is unchanged.
2024-07-16 16:22:47 +02:00
decentral1se f28cffe6d8 refactor: vertical deploy overview 2024-07-16 09:37:10 +02:00
decentral1se d3ede0f0f6 refactor: logging with background/padding 2024-07-15 22:55:02 +02:00
decentral1se ae4653f5e3 build: add full install target [ci skip] 2024-07-13 15:30:38 +02:00
fauno 7f0a74d3c3 fix: source autocompletion on the current terminal 2024-07-11 12:02:38 -03:00
fauno e99114e695 fix: setup should be run once 2024-07-11 12:02:22 -03:00
fauno b1208f9db5 fix: sometimes the completion directories already exist 2024-07-11 12:01:21 -03:00
decentral1se b8e1a3b75f test: remote recipe tests
See coop-cloud/abra#432
2024-07-10 16:03:28 +02:00
decentral1se ff90b43929 fix: use struct data for HEAD retrieval
See ce7dda1eae
2024-07-10 15:51:11 +02:00
p4u1 c5724d56f8 fix(config): Removes config file name from abra dir 2024-07-10 13:42:24 +00:00
decentral1se ce7dda1eae fix: use recipe struct data
Follow up for coop-cloud/abra#432
2024-07-10 15:40:45 +02:00
decentral1se d38f3ab7f5 test: speed up test 2024-07-10 13:27:58 +02:00
decentral1se 4be8c8daed test: fix outputs [ci skip]
See https://build.coopcloud.tech/coop-cloud/abra/2035/1/5
2024-07-10 13:20:39 +02:00
decentral1se 3c9405a4ed refactor!: --problems/p goes away
Follow up for coop-cloud/abra#413
2024-07-10 13:06:46 +02:00
p4u1 f6b7510da6 feat: introduce remote recipes
Reviewed-on: coop-cloud/abra#432
2024-07-10 10:25:06 +00:00
p4u1 7596982282 feat: update new version in env file 2024-07-10 12:12:43 +02:00
p4u1 4085eb6654 feat: define recipe version inside app env file 2024-07-10 12:11:46 +02:00
p4u1 790dbca362 feat!: remove all catalogue reads from app commands 2024-07-10 12:06:57 +02:00
p4u1 d7a870b887 feat: remote recipes 2024-07-10 12:06:44 +02:00
decentral1se 1a3ec7a107 fix: pass recipe name for listing cmds 2024-07-09 17:23:06 +02:00
decentral1se 7f910b4e5b test: recipe test fixups 2024-07-09 11:34:20 +02:00
decentral1se b82ac3bd63 refactor: make IsChaos an actual bool 2024-07-09 11:34:01 +02:00
decentral1se 00d60f7114 fix: ensure force upgrade/rollback works 2024-07-09 11:33:33 +02:00
decentral1se 71d93cbbea refactor: debug logging and errors for version issues 2024-07-09 11:33:07 +02:00
decentral1se 2fb5493ab5 feat: support chaos commits on deploy
See coop-cloud/organising#517
2024-07-09 11:31:52 +02:00
decentral1se 0ff8e49cfd docs: pass on sub-command help 2024-07-09 09:43:18 +02:00
decentral1se addbda9145 test: fixups for the changepocalypse 2024-07-09 09:41:49 +02:00
decentral1se c33ca1c6bc fix!: chaos consistency (deploy/undeploy/rollback/upgrade)
See coop-cloud/organising#559

--chaos for rollback/upgrade goes away.
2024-07-08 17:23:49 +02:00
decentral1se 4580df72cb fix: use recipe name 2024-07-08 14:58:57 +02:00
decentral1se f003430a8d fix: use recipe name, not app name 2024-07-08 14:54:15 +02:00
decentral1se 5426464092 refactor!: drop version, show versions in ps
See coop-cloud/organising#526
See coop-cloud/organising#502
2024-07-08 14:41:46 +02:00
decentral1se 72c021c727 fix: remove old commands from deploy fail help 2024-07-08 14:29:51 +02:00
decentral1se f2e076b35f fix: set default logger on kadabra 2024-07-08 14:26:27 +02:00
decentral1se 4ccb4198d6 fix: "recipe version" handles non-catalogue recipes 2024-07-08 14:26:26 +02:00
decentral1se a9f7579ca9 fix: remove old logrus calls 2024-07-08 14:21:17 +02:00
p4u1 9cd1fe658b refactor(recipe): create a recipe struct that gets used everywhere #430
Reviewed-on: coop-cloud/abra#430
2024-07-08 12:18:58 +00:00
p4u1 41c16db670 test: fix test failure 2024-07-08 14:10:17 +02:00
p4u1 87ecc05962 refactor(recipe): remove direct usage of config.RECIPE_DIR 2024-07-08 13:48:02 +02:00
p4u1 f14d49cc64 refactor(recipe): rename Recipe2 -> Recipe 2024-07-08 13:19:40 +02:00
p4u1 f638b6a16b refator(recipe): remove old struct 2024-07-08 13:16:47 +02:00
p4u1 5617a9ba07 refactor(recipe): remove remaining usage of old recipe struct 2024-07-08 13:15:20 +02:00
p4u1 c1b03bcbd7 refactor(recipe): load load compoes config where its used 2024-07-08 12:31:39 +02:00
p4u1 99da8d4e57 refactor(recipe): move GetComposeFiles to new struct 2024-07-08 12:06:58 +02:00
p4u1 ca1db33e97 refactor(recipe): remove Dir method on old struct 2024-07-08 11:48:53 +02:00
p4u1 eb62e0ecc3 refactor(recipe): move Tags method to new struct 2024-07-08 11:45:47 +02:00
p4u1 6f90fc3025 refactor(recipe): don't use README.md path directly 2024-07-08 11:43:18 +02:00
p4u1 c861c09cce refactor(recipe): use method or variable for .env.sample 2024-07-08 11:41:26 +02:00
p4u1 2f41b6d8b4 refactor(recipe): store sample env path in new struct 2024-07-08 11:31:55 +02:00
p4u1 73e9b818b4 refactor(recipe): move SampleEnv method to new struct 2024-07-08 11:02:43 +02:00
p4u1 f268e5893b refactor(recipe): move functions that operate on the git repo to new file 2024-07-08 11:00:50 +02:00
p4u1 47013c63d6 refactor(recipe): use template for ssh url 2024-07-08 10:56:08 +02:00
p4u1 4cf6155fb8 refactor(recipe): introduce Dir var 2024-07-08 10:56:08 +02:00
p4u1 01f3f4be17 refactor(recipe): use new recipe.Ensure method 2024-07-08 10:55:55 +02:00
p4u1 eee2ecda06 refactor(recipe): add offline and chaos options to Ensure method 2024-07-08 10:55:55 +02:00
p4u1 950f85e2b4 refactor(recipe): introduce new recipe struct and move some methods 2024-07-08 10:55:43 +02:00
decentral1se 9ef64778f5 chore: go deps update 2024-07-08 01:52:17 +02:00
decentral1se 735f521bc0 refactor(errors)!: remove WIP/broken command 2024-07-08 01:33:06 +02:00
decentral1se 96a25425a4 refactor(ps)!: remove -w, "watch ..." does it better 2024-07-08 01:10:58 +02:00
decentral1se 1a8dca9804 fix(deploy): only output when actually waiting 2024-07-08 01:01:14 +02:00
decentral1se 465827d5ee log: no additional newlines 2024-07-08 01:01:14 +02:00
decentral1se cde06f4f00 log: output caller on debug, use stdout as default 2024-07-08 01:01:13 +02:00
decentral1se 050a479df7 refactor: "service name" -> "service" 2024-07-08 00:38:54 +02:00
decentral1se ef108d63e1 refactor: use central logger 2024-07-08 00:01:28 +02:00
decentral1se cf8ff410cc feat: central log config
See coop-cloud/organising#422
2024-07-08 00:01:27 +02:00
decentral1se 6ec678208f chore: formatting 2024-07-07 22:40:06 +02:00
decentral1se a001be3021 docs: better "app ps" description 2024-07-07 22:39:57 +02:00
decentral1se 6712bd446f chore: add upstream link 2024-07-07 21:52:45 +02:00
decentral1se 1097daa69f fix: "abra app restart" docs + --all-services
See coop-cloud/organising#605
2024-07-07 21:52:24 +02:00
decentral1se beaa233421 test: only publish image on main merge 2024-07-07 12:21:51 +02:00
decentral1se f871f9beee test: reduce duplication 2024-07-07 12:13:07 +02:00
decentral1se 0f8f0f908f test: ensure catalogue 2024-07-07 12:03:43 +02:00
decentral1se c5211fbd7e test: fix imports 2024-07-07 12:03:37 +02:00
p4u1 0076b31253 new package envfile and move GetComposeFiles to recipe package 2024-07-06 16:37:16 +02:00
p4u1 37aff723c0 move GetComposeFiles 2024-07-06 16:37:16 +02:00
p4u1 f18c642226 refactor: move app files from config to app package 2024-07-06 16:37:16 +02:00
p4u1 ac695ae28e feat: introduce abra config file and load abra dir from it (!419)
This is the first step to introduce a configuration file for abra. The config file must be named `abra.yaml` or àbra.yml`. abra look for the config file in the current directory and when not found traverses the directory tree up until it is found or the home/root directory is reached.

For now there is only one setting that is made configurable: `abraDir`. The new logic for setting the abra dir is the following:
1. lookup `$ABRA_DIR` env
2. look for config file and take value from there
3. `$HOME/.abra` as fallback

See coop-cloud/organising#303.

Reviewed-on: coop-cloud/abra#419
Reviewed-by: decentral1se <decentral1se@noreply.git.coopcloud.tech>
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2024-07-06 14:36:31 +00:00
decentral1se ac87898005 test: run versioned script [ci skip] 2024-07-03 10:02:04 +02:00
decentral1se 32ae2499b6 test: add CI integration script [ci skip] 2024-07-03 09:57:22 +02:00
decentral1se 1136ec5dcd build: remove old release scripts 2024-07-03 09:57:06 +02:00
decentral1se 6a2db1abaa test: run int suite on remote server via cron 2024-07-02 17:18:05 +02:00
decentral1se 9554ad40c8 refactor: use adapted upstream detach=false logic [ci skip]
See coop-cloud/organising#607.
2024-07-02 14:52:12 +02:00
decentral1se 2014cd6622 test: less fragile integration suite [ci skip]
See coop-cloud/organising#584
See coop-cloud/organising#595
2024-07-02 12:16:58 +02:00
decentral1se a9ce2106c6 test: skip test for now
Also, don't build image if tests fail.
2024-06-28 06:12:32 +02:00
decentral1se 34de38928a test: include catalogue 2024-06-26 23:46:35 +02:00
decentral1se f58522d822 fix: dont always download the catalogue
See coop-cloud/organising#592
2024-06-25 16:48:41 +02:00
decentral1se 712ebfb701 test: update and fix cleanup for "server add" 2024-06-25 16:24:44 +02:00
decentral1se 1fe601cd16 fix: custom timeout only for "server add" 2024-06-25 16:13:57 +02:00
decentral1se 7b7e1bfa97 refactor!: server add/rm has better UI/UX
Less confusing logging messages, clear "is created" / "already exists"
output. Move the majority of logging to debug output to not confuse the
situation. Some code cleanups also in there.
2024-06-25 09:48:53 +02:00
decentral1se 1a12bef53e docs: better "server add" help output 2024-06-25 09:24:01 +02:00
decentral1se d787f71215 fix: more accurate dns errors 2024-06-25 00:27:48 +02:00
decentral1se 9bf44c15ed fix: clean up if failed to create context 2024-06-25 00:27:34 +02:00
decentral1se 349cacc1f2 docs: explain -D for "server add" 2024-06-25 00:27:16 +02:00
decentral1se 938534f5ac feat: support non-TLD resolving server domains
See coop-cloud/organising#566
2024-06-24 22:07:16 +00:00
p4u1 6cd331ebd6 secret: allow inserting secret from file and add trim flag 2024-06-22 16:49:59 +00:00
decentral1se 40517171f7 test: separate test for git name/email
See coop-cloud/abra#405
2024-06-22 18:46:28 +02:00
p4u1 b2485cc122 feat: add git-user and git-email flags to recipe new 2024-06-22 16:38:32 +00:00
p4u1 9ec99c7712 test: return/echo from git helper functions 2024-06-22 17:04:33 +02:00
decentral1se aa3910f8df refactor!: drop all SSH opts / config handling
See coop-cloud/organising#601
See coop-cloud/organising#482
2024-06-21 17:16:41 +02:00
decentral1se 43990b6fae test: use more plumbung for git output 2024-06-21 17:10:12 +02:00
decentral1se 91ea2c01a5 fix: fix old app version deploy wrt. compose files
See coop-cloud/organising#617
2024-06-21 16:14:40 +02:00
decentral1se 316fdd3643 fix: abra app new checks out latest version
See coop-cloud/organising#618
2024-06-21 15:51:34 +02:00
decentral1se e07ae8cccd chore: make format/check 2024-06-19 19:17:22 +02:00
decentral1se 300a4ead01 fix: stop using deprecated APIs 2024-06-19 19:14:52 +02:00
decentral1se f209b6f564 chore: go get -u -t 2024-06-19 19:14:44 +02:00
decentral1se 791183adfe build: new deps target 2024-06-19 19:14:31 +02:00
moritz e6b35e8524 fix(upgrade): make upgrade --chaos working again 2024-05-22 10:21:31 +02:00
moritz 8a0274cac0 fix(recipe): output correct formatted json for recipe version 2024-05-21 16:59:59 +02:00
moritz e609924af0 feat(upgrade): add --releasenotes: show release notes and skip upgrading 2024-05-21 13:49:36 +02:00
moritz 70e2943301 fix(upgrade): only show release notes relevant for the upgrade 2024-05-21 13:49:11 +02:00
moritz 0590c1824d checkout deployed version 2024-05-14 00:07:58 +02:00
moritz 459abecfa5 only show container that should be deployed 2024-05-13 23:26:02 +02:00
moritz 183ad8f576 machine readable ps output 2024-05-13 22:08:03 +02:00
decentral1se 03f94da2d8 docs: add fauno [ci skip] 2024-05-01 01:20:25 +02:00
fauno 766f69b0fd feat: strip debug symbols
to produce smaller binaries
2024-04-30 14:05:03 -03:00
decentral1se 004cd70aed fix: use unique rule number & wording [ci skip] 2024-04-06 23:52:56 +02:00
decentral1se a4de446f58 test: more verbose failure msg, use contains [ci skip] 2024-04-06 23:48:22 +02:00
rix d21c35965d fix: add warning for long secret names (!359)
A start of a fix for coop-cloud/organising#463
Putting some code out to start a discussion.  I've added a linting rule for recipes to establish a general principal but I want to put some validation into cli/app/new.go as that's the point we have both the recipe and the domain and can say for sure whether or not the secret names lengths cause a problem but that will have to wait for a bit.  Let me know if I've missed the mark somewhere

Reviewed-on: coop-cloud/abra#359
Reviewed-by: decentral1se <decentral1se@noreply.git.coopcloud.tech>
Co-authored-by: Rich M <r.p.makepeace@gmail.com>
Co-committed-by: Rich M <r.p.makepeace@gmail.com>
2024-04-06 21:41:37 +00:00
mayel 63ea58ffaa add relevant command to error message 2024-04-01 18:51:53 +01:00
decentral1se 2ecace3e90 fix: add missing packages on final layer
Closes coop-cloud/organising#598
2024-04-01 13:57:51 +02:00
p4u1 d5ac3958a4 feat: add retries to app volume remove 2024-03-27 05:38:24 +00:00
3wordchant 72c20e0039 fix: make installer work again 2024-03-26 21:07:38 -03:00
decentral1se 575f9905f1 Revert "Revert "feat: backup revolution""
This reverts commit 2c515ce70a.
2024-03-12 10:34:40 +01:00
decentral1se e3a0af5840 build: upgrade goreleaser
Closes coop-cloud/organising#474
2024-03-12 10:11:14 +01:00
decentral1se 9a3a39a185 chore: new 0.9.x series 2024-03-12 10:05:31 +01:00
decentral1se cea56dddde fix: drop deprecated stanza (goreleaser) 2024-03-12 10:04:50 +01:00
decentral1se 2c515ce70a Revert "feat: backup revolution"
This reverts commit c5687dfbd7.

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

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

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

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

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

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

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

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

Closes coop-cloud/organising#518

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

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

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

Thanks @cas.
2023-11-27 11:15:59 +01:00
p4u1 b1d3fcbb0b add integration test 2023-11-27 10:01:33 +00:00
p4u1 7b6134f35e add bash completion for abra cmd 2023-11-27 10:01:33 +00:00
decentral1se 316b59b465 test: support local-first testing
Cherry-picked from coop-cloud/abra#389

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

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

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

See 8fa9419c99.
2023-08-01 19:31:51 +02:00
renovate-bot 1de45a6508 chore(deps): update goreleaser/goreleaser docker tag to v1.19.2 2023-07-31 07:02:04 +00:00
decentral1se 55c7aca3c0 chore: publish 0.8.0-rc2-beta 2023-07-29 00:31:49 +02:00
decentral1se 8fa9419c99 build: pin to goreleaser v18 [ci skip]
See coop-cloud/organising#474
2023-07-29 00:22:01 +02:00
decentral1se 73ad0a802e Revert "build: replacements is deprecated"
This reverts commit 473cae0146.

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

See coop-cloud/organising#471.

Closes coop-cloud/organising#432.
2023-07-25 15:05:01 +00:00
decentral1se 9f478dac1d fix: list downgrades/upgrades in correct order
Now that we have correct sorting of versions:

  coop-cloud/organising#427

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

Related:

  coop-cloud/organising#444

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes coop-cloud/organising#319.
2023-01-23 08:31:00 +00:00
decentral1se 903aac9d7a feat: recipe fetch command
Also may have rooted out another go-git cloning bug 🙄

Closes coop-cloud/organising#365
2023-01-23 09:26:53 +01:00
decentral1se 49865c6a97 feat: app services command
Closes coop-cloud/organising#372
2023-01-23 08:25:17 +00:00
decentral1se a694c8c20e feat: filter server by recipe
Closes coop-cloud/organising#363
2023-01-23 00:54:22 +01:00
decentral1se d4a42d8378 fix: error out if no backup configs found 2023-01-22 18:50:45 +01:00
decentral1se e16ca45fa7 fix!: better backup file names
Closes coop-cloud/organising#366
2023-01-22 18:50:27 +01:00
decentral1se 32de2ee5de fix: ensure catalogue is clean/up-to-date
Closes coop-cloud/organising#367
2023-01-22 17:52:36 +01:00
decentral1se 834d41ef50 docs: wording [ci skip] 2023-01-22 10:07:58 +01:00
decentral1se 6fe5aed408 fix!: remove digest handling
Closes coop-cloud/organising#379
2023-01-22 08:54:13 +01:00
decentral1se 03041b88d0 chore: gofmt 2023-01-21 23:26:23 +01:00
decentral1se 9338afb492 chore: go mod tidy 2023-01-20 10:16:14 +01:00
renovate-bot 313ae0dbe2 chore(deps): update module github.com/docker/cli to v20.10.23 2023-01-20 09:12:52 +00:00
renovate-bot 0dc7ec8570 chore(deps): update module github.com/docker/docker to v20.10.23 2023-01-20 08:02:52 +00:00
decentral1se 8a1a3aeb12 ci: automerge & run tidy [ci skip] 2023-01-18 17:28:36 +01:00
3wordchant 910469cfa0 chore: switch to dev image by default 2023-01-15 19:48:07 -08:00
3wordchant 4f055096e9 chore: fix Drone build, ignore auto-recipes-catalogue-json 2023-01-15 18:16:53 -08:00
3wordchant 6c93f980dc chore: tweak docker build 2023-01-15 18:08:22 -08:00
3wordchant 57f52bbf33 chore: disable go cache for now, parallelise build 2023-01-15 17:16:32 -08:00
3wordchant 9f5620d881 chore: attempt to fix drone build 2023-01-15 17:11:50 -08:00
3wordchant 44c4555aae chore: attempt to enable go caching for docker image build 2023-01-15 17:10:57 -08:00
3wordchant 025d1e0a8c chore: tweak drone image building 2023-01-15 17:10:52 -08:00
3wordchant f484021148 feat: add docker image, auto-built using CI 2023-01-15 17:10:45 -08:00
3wordchant 1403eac72c fix: parse "Status" field during catalogue generate 2023-01-15 17:10:45 -08:00
cas a6e23938eb Add tests to jsontable.
- Test major functionality of jsontable
- Fix bug discovered in testing.
2023-01-15 17:10:36 -08:00
cas cae0d9ef79 Introduce a JSON output table mechanic
- Create JSONTable as a proxy/extension to tablewriter which can also output JSON.
- Implement machine readable output for `server list` and `recipe list`
2023-01-12 21:15:14 +00:00
decentral1se 89fcb5b216 chore: go mod tidy 2023-01-06 10:05:20 +01:00
renovate-bot 56b3e9bb19 chore(deps): update module github.com/go-git/go-git/v5 to v5.5.2 2023-01-06 08:02:14 +00:00
decentral1se 9aa4a98b0b chore: go mod tidy 2023-01-05 17:45:33 +01:00
decentral1se 5fbba0c934 Merge remote-tracking branch 'origin/renovate/main-golang.org-x-sys-0.x' 2023-01-05 17:44:53 +01:00
decentral1se d772f4b2c6 Merge remote-tracking branch 'origin/renovate/main-golang.org-x-crypto-0.x' 2023-01-05 17:44:31 +01:00
renovate-bot 7513fbd57d chore(deps): update module golang.org/x/sys to v0.4.0 2023-01-05 08:02:28 +00:00
renovate-bot 9082761f86 chore(deps): update module golang.org/x/crypto to v0.5.0 2023-01-05 08:02:09 +00:00
renovate-bot a3bd6e14d0 chore(deps): update module github.com/schollz/progressbar/v3 to v3.13.0 2023-01-05 08:01:57 +00:00
decentral1se 49dd702d98 chore: go mod tidy 2023-01-04 09:36:35 +01:00
decentral1se e4cd5e3efe Merge remote-tracking branch 'origin/renovate/main-github.com-hetznercloud-hcloud-go-1.x' 2023-01-04 09:36:13 +01:00
renovate-bot 1db4602020 chore(deps): update module github.com/hashicorp/go-retryablehttp to v0.7.2 2023-01-04 08:01:58 +00:00
renovate-bot b50718050b chore(deps): update module github.com/hetznercloud/hcloud-go to v1.39.0 2022-12-30 08:01:53 +00:00
3wordchant 9e39e1dc88 docs: fix typo in error message 2022-12-22 19:27:42 -08:00
decentral1se 1a3a53cfc2 chore: go mod tidy 2022-12-19 09:06:28 +01:00
renovate-bot 5f53d591f8 chore(deps): update module github.com/docker/docker to v20.10.22 2022-12-19 09:06:27 +01:00
renovate-bot d7013518cc chore(deps): update module github.com/docker/cli to v20.10.22 2022-12-19 08:01:53 +00:00
3wordchant b204b289d1 fix: disable progress bar with machine-readable output 2022-12-16 10:20:51 -08:00
decentral1se 3a0d9f7ed7 chore: 0.6.0-beta release 2022-12-13 16:09:36 +01:00
decentral1se e794c17fb4 chore: authors add & sort [ci skip] 2022-12-13 16:07:05 +01:00
moritz e788ac21f6 fix: keep abra working if recipe catalogue is offline (!235)
Co-authored-by: Moritz <moritz.m@local-it.org>
Reviewed-on: coop-cloud/abra#235
2022-12-13 14:42:45 +00:00
decentral1se 4e78b060e0 chore: go mod tidy 2022-12-12 10:50:38 +01:00
decentral1se 4fada9c1b7 Merge remote-tracking branch 'origin/renovate/main-github.com-hetznercloud-hcloud-go-1.x' 2022-12-12 10:50:18 +01:00
decentral1se 08d26e1a39 Merge remote-tracking branch 'origin/renovate/main-github.com-schollz-progressbar-v3-3.x' 2022-12-12 10:49:42 +01:00
decentral1se 581b28a2b1 Merge remote-tracking branch 'origin/renovate/main-golang.org-x-crypto-0.x' 2022-12-12 10:49:32 +01:00
renovate-bot e4d58849ce chore(deps): update module github.com/go-git/go-git/v5 to v5.5.1 2022-12-12 08:01:31 +00:00
renovate-bot 5e8b9d9bf7 chore(deps): update module golang.org/x/crypto to v0.4.0 2022-12-08 08:01:33 +00:00
renovate-bot 11dd665794 chore(deps): update module github.com/schollz/progressbar/v3 to v3.12.2 2022-12-07 08:01:51 +00:00
renovate-bot ba163e9bf3 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.38.0 2022-12-06 08:01:41 +00:00
cas 09048ee223 Done did make format 2022-12-05 18:03:13 +00:00
cas 19a055b59b Add myself to the AUTHORS.md 2022-12-05 18:03:13 +00:00
cas 1b28a07e17 Minor stylistic improvements to MR output in list. 2022-12-05 18:03:13 +00:00
cas 82866cd213 Partial implementation of machine readable output.
- Implement global flag for machine readable output.
- Add machine readable output (as JSON) to list command.
2022-12-05 18:03:13 +00:00
decentral1se 47f3d2638b chore: go mod tidy 2022-12-05 09:30:46 +01:00
decentral1se a3b894320a Merge remote-tracking branch 'origin/renovate/main-golang.org-x-sys-0.x' 2022-12-05 09:29:33 +01:00
renovate-bot 9424a58c52 chore(deps): update module golang.org/x/sys to v0.3.0 2022-12-05 08:01:48 +00:00
renovate-bot 1751ba534e chore(deps): update module github.com/go-git/go-git/v5 to v5.5.0 2022-12-05 08:01:25 +00:00
3wordchant a21d431541 fix: don't panic() 😅 2022-11-24 17:33:59 +00:00
3wordchant 8fad34e430 fix: switch back to replacing <recipe>.example.com
Fixes #355
2022-11-24 17:33:59 +00:00
decentral1se a036de3c26 chore: go mod tidy 2022-11-17 13:23:19 +01:00
renovate-bot 4c2109e8ce chore(deps): update module golang.org/x/crypto to v0.3.0 2022-11-17 08:01:01 +00:00
moritz 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
moritz 521d3d1259 feat(autocomplete): add autocompletion for fish shell 2022-11-15 22:24:34 +01:00
decentral1se 14187449a5 fix: fork passgen
See coop-cloud/organising#358
2022-11-14 15:18:54 +01:00
decentral1se 2037f4cc19 chore: go mod tidy 2022-11-11 17:40:42 +01:00
renovate-bot 05d492d30b chore(deps): update module github.com/hetznercloud/hcloud-go to v1.37.0 2022-11-11 08:01:11 +00:00
moritz 9591e91ed6 feat(cmd): make env variables accessible for local abra.sh commands 2022-11-10 11:12:35 +00:00
decentral1se f6f587e506 chore: go mod tidy 2022-11-10 11:47:16 +01:00
renovate-bot 4f28dbee87 chore(deps): update module golang.org/x/crypto to v0.2.0 2022-11-10 08:01:07 +00:00
renovate-bot ad1cc038e3 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.36.0 2022-11-09 08:01:00 +00:00
renovate-bot 15dbd85d25 chore(deps): update module golang.org/x/sys to v0.2.0 2022-11-08 08:00:59 +00:00
renovate-bot 2a97955586 chore(deps): update module github.com/schollz/progressbar/v3 to v3.12.1 2022-11-07 08:00:59 +00:00
decentral1se 9e44d1dfba chore: go mod tidy 2022-11-04 14:52:56 +01:00
renovate-bot 87ad8e2761 chore(deps): update module github.com/schollz/progressbar/v3 to v3.12.0 2022-11-03 08:01:03 +00:00
renovate-bot cfe703b15d chore(deps): update module github.com/docker/cli to v20.10.21 2022-10-27 08:44:38 +00:00
renovate-bot 96503fa9e9 chore(deps): update module github.com/docker/docker to v20.10.21 2022-10-26 07:01:18 +00:00
decentral1se 07d49d8566 chore go mod tidy 2022-10-22 14:19:10 +02:00
decentral1se 5a7c25375a Merge remote-tracking branch 'origin/renovate/main-gotest.tools-v3-3.x' 2022-10-22 14:18:35 +02:00
decentral1se 652143e76c Merge remote-tracking branch 'origin/renovate/main-golang.org-x-sys-0.x' 2022-10-22 14:18:25 +02:00
decentral1se 8afce6eebf Merge remote-tracking branch 'origin/renovate/main-golang.org-x-crypto-0.x' 2022-10-22 14:17:35 +02:00
decentral1se d3e6c9dc94 Merge remote-tracking branch 'origin/renovate/main-github.com-docker-docker-20.x' 2022-10-22 14:17:26 +02:00
renovate-bot 4fd0ca3dd1 chore(deps): update module golang.org/x/crypto to v0.1.0 2022-10-20 07:00:57 +00:00
renovate-bot dc0b6c2c8c chore(deps): update module github.com/docker/docker to v20.10.20 2022-10-19 07:00:57 +00:00
renovate-bot 54f242baf7 chore(deps): update module github.com/docker/cli to v20.10.20 2022-10-19 07:00:51 +00:00
renovate-bot 07620c7d89 chore(deps): update module golang.org/x/sys to v0.1.0 2022-10-18 07:01:15 +00:00
renovate-bot 1cae4cce4e chore(deps): update module gotest.tools/v3 to v3.4.0 2022-10-10 07:01:50 +00:00
decentral1se 9347ade82c chore: go mod tidy 2022-09-20 10:42:53 +02:00
decentral1se 3fa18a8050 Merge remote-tracking branch 'origin/renovate/main-github.com-schollz-progressbar-v3-3.x' 2022-09-20 10:42:25 +02:00
decentral1se 4ac67662a2 Merge remote-tracking branch 'origin/renovate/main-github.com-hetznercloud-hcloud-go-1.x' 2022-09-20 10:42:17 +02:00
decentral1se d1be4077c5 Merge remote-tracking branch 'origin/renovate/main-github.com-gliderlabs-ssh-0.x' 2022-09-20 10:42:09 +02:00
decentral1se 5a88c34a7c Merge remote-tracking branch 'origin/renovate/main-github.com-docker-go-units-0.x' 2022-09-20 10:41:57 +02:00
decentral1se 2e452e3213 Merge remote-tracking branch 'origin/renovate/main-github.com-docker-docker-20.x' 2022-09-20 10:41:30 +02:00
decentral1se 9d16a8e10c Merge remote-tracking branch 'origin/renovate/main-github.com-docker-cli-20.x' 2022-09-20 10:41:22 +02:00
renovate-bot 8755a6c3b4 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.35.3 2022-09-20 07:01:14 +00:00
renovate-bot 8cee8ae33a chore(deps): update module github.com/schollz/progressbar/v3 to v3.11.0 2022-09-12 07:01:55 +00:00
renovate-bot 15b138e026 chore(deps): update module github.com/docker/docker to v20.10.18 2022-09-12 07:01:39 +00:00
renovate-bot 4a8ed36dea chore(deps): update module github.com/docker/cli to v20.10.18 2022-09-12 07:01:28 +00:00
renovate-bot 7d0c3cc496 chore(deps): update module github.com/alecaivazis/survey/v2 to v2.3.6 2022-09-12 07:01:18 +00:00
renovate-bot 3cf479ffd5 chore(deps): update module github.com/docker/go-units to v0.5.0 2022-09-01 07:01:24 +00:00
renovate-bot d402050a40 chore(deps): update module github.com/gliderlabs/ssh to v0.3.5 2022-09-01 07:01:15 +00:00
decentral1se 664edce09d build: fix matching to ignore deps upgrade [ci skip] 2022-08-15 12:32:19 +02:00
decentral1se e41caa891d fix: dont check ip on server when it is local
Closes coop-cloud/organising#334.
2022-08-14 22:20:17 +02:00
decentral1se 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
decentral1se 8f709c05bf build: ignore merges, chores & sort 2022-08-12 01:11:25 +02:00
decentral1se 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
decentral1se 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
decentral1se 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
renovate-bot 6593baf9f4 chore(deps): update golang docker tag to v1.19 2022-08-03 07:01:11 +00:00
decentral1se 50123f3810 chore: go mod tidy 2022-08-02 11:25:13 +03:00
decentral1se d132e87f14 Merge remote-tracking branch 'origin/renovate/main-github.com-schollz-progressbar-v3-3.x' 2022-08-02 11:24:03 +03:00
renovate-bot 37a1c3fb85 chore(deps): update module github.com/schollz/progressbar/v3 to v3.9.0 2022-08-01 07:01:38 +00:00
renovate-bot c8183aa6d1 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.35.2 2022-08-01 07:01:21 +00:00
decentral1se 4711de29ae chore: go mod tidy 2022-07-21 15:03:17 +03:00
decentral1se b719aaba41 Merge remote-tracking branch 'origin/renovate/main-github.com-sirupsen-logrus-1.x' 2022-07-21 15:02:25 +03:00
renovate-bot 074c51b672 chore(deps): update module github.com/sirupsen/logrus to v1.9.0 2022-07-20 07:01:14 +00:00
renovate-bot 1aa6be704a chore(deps): update module github.com/schollz/progressbar/v3 to v3.8.7 2022-07-20 07:01:06 +00:00
decentral1se e8e3cb8598 chore: go mod tidy 2022-07-14 11:53:22 +02:00
decentral1se 85fec6b107 Merge remote-tracking branch 'origin/renovate/main-gotest.tools-v3-3.x' 2022-07-14 11:51:41 +02:00
renovate-bot 12dbb061a9 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.35.1 2022-07-05 07:01:24 +00:00
renovate-bot 351bd7d4ba chore(deps): update module gotest.tools/v3 to v3.3.0 2022-06-20 07:01:21 +00:00
decentral1se cdc7037c25 chore: go mod tidy [ci skip] 2022-06-15 13:56:43 +02:00
renovate-bot 682237c98e chore(deps): update module github.com/alecaivazis/survey/v2 to v2.3.5 2022-06-08 07:01:45 +00:00
decentral1se 08d97be43a chore: go mod tidy 2022-06-07 09:09:08 +02:00
decentral1se 786dfde27e Merge commit 'c153c5d' into main 2022-06-07 09:08:55 +02:00
renovate-bot 6e012b910e chore(deps): update module github.com/docker/docker to v20.10.17 2022-06-07 07:01:42 +00:00
renovate-bot c153c5da2e chore(deps): update module github.com/docker/cli to v20.10.17 2022-06-07 07:01:28 +00:00
decentral1se 0540e42168 alpha -> beta 2022-05-31 10:23:49 +02:00
decentral1se 4bc95a5b52 chore: go mod tidy [ci skip] 2022-05-16 16:22:21 +02:00
decentral1se febc6e2874 Merge remote-tracking branch 'origin/renovate/main-github.com-docker-docker-20.x' into main 2022-05-16 16:22:12 +02:00
decentral1se b2c990bf12 Merge remote-tracking branch 'origin/renovate/main-github.com-docker-cli-20.x' into main 2022-05-16 16:22:06 +02:00
decentral1se 3b8893502a docs: re-word on docstrings [ci skip] 2022-05-13 16:44:49 +02:00
renovate-bot e0a0378f73 chore(deps): update module github.com/docker/docker to v20.10.16 2022-05-13 07:01:43 +00:00
renovate-bot 0837045d44 chore(deps): update module github.com/docker/cli to v20.10.16 2022-05-13 07:01:33 +00:00
decentral1se cd8137a7d8 chore: go mod tidy [ci skip] 2022-05-10 16:15:08 +02:00
decentral1se ece4537a2d Merge remote-tracking branch 'origin/renovate/main-github.com-gliderlabs-ssh-0.x' into main 2022-05-10 16:14:45 +02:00
decentral1se 16fe1b68c6 fix: thread app name & stack name correctly 2022-05-10 12:10:36 +02:00
renovate-bot e37f235fd4 chore(deps): update module github.com/gliderlabs/ssh to v0.3.4 2022-05-10 07:01:27 +00:00
decentral1se 0423ce7e84 fix: working link [ci skip] 2022-05-10 08:32:12 +02:00
decentral1se d46ac22bd7 chore: go mod tidy [ci skip] 2022-05-09 14:09:14 +02:00
decentral1se cef5cd8611 Merge remote-tracking branch 'origin/renovate/main-github.com-docker-docker-20.x' into main 2022-05-09 14:04:16 +02:00
renovate-bot 8b38dac9ab chore(deps): update module github.com/docker/docker to v20.10.15 2022-05-06 07:01:51 +00:00
renovate-bot 89fc875088 chore(deps): update module github.com/docker/cli to v20.10.15 2022-05-06 07:01:31 +00:00
decentral1se 026a9ba2d7 chore: go mod tidy [ci skip] 2022-05-05 15:13:20 +02:00
renovate-bot 99f2b9c6dc chore(deps): update module github.com/urfave/cli to v1.22.9 2022-05-05 07:01:30 +00:00
decentral1se 578e91eeec chore: publish next tag 0.5.0-alpha 2022-05-03 17:22:54 +02:00
decentral1se 49f79dbd45 fix!: new catalogue URL 2022-05-03 17:08:52 +02:00
decentral1se 574d556bb9 chore: go mod tidy 2022-04-30 18:28:42 +02:00
decentral1se 801aad64df Merge remote-tracking branch 'origin/renovate/main-gotest.tools-v3-3.x' into main 2022-04-30 18:28:22 +02:00
decentral1se b0a0829712 Merge remote-tracking branch 'origin/renovate/main-github.com-urfave-cli-1.x' into main 2022-04-30 18:28:15 +02:00
renovate-bot 6aae06c3ec chore(deps): update module github.com/urfave/cli to v1.22.8 2022-04-29 07:01:30 +00:00
renovate-bot d0c6fa5b45 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.33.2 2022-04-27 07:02:33 +00:00
renovate-bot c947354ee3 chore(deps): update module gotest.tools/v3 to v3.2.0 2022-04-25 07:01:44 +00:00
decentral1se 9b7e5752fb chore: go mod tidy [ci skip] 2022-04-22 11:25:08 +02:00
renovate-bot 9bc51629d4 chore(deps): update module github.com/urfave/cli to v1.22.7 2022-04-22 07:01:22 +00:00
decentral1se 4ba15df9b7 chore: 0.4.1-alpha 2022-04-21 15:47:39 +02:00
knoflook 5721b357a2 fix: per service logs 2022-04-21 15:40:23 +02:00
decentral1se 6140abbcac fix: sync to latest before commits come in
Follows from a4989e383402e5c1af0b9713a3ad4f50ab5581e5
2022-04-20 11:42:24 +00:00
decentral1se 996255188b Revert "fix: ensure we're on latest for recipe release dance"
This reverts commit 3c4bb6a55e.
2022-04-20 11:42:24 +00:00
knoflook 11d78234b2 installer: add 32 bit arm support 2022-04-20 13:37:51 +02:00
knoflook c214937e4a installer: download on aarch64 2022-04-20 13:13:50 +02:00
decentral1se 3a3f41988b chore: publish 0.4.0-alpha 2022-04-19 14:36:56 +02:00
decentral1se f6690a80bd build: upx release script [ci skip] 2022-04-19 14:34:06 +02:00
decentral1se 2337c4648b chore: remove unused command 2022-04-19 14:32:34 +02:00
decentral1se a1190f1352 fix: show which service is getting backed up [ci skip] 2022-04-19 13:50:23 +02:00
decentral1se e421922f5b fix: restore uses absolute paths & better docs 2022-04-19 13:21:12 +02:00
decentral1se 10d5705d1a docs: better backup docs 2022-04-19 13:20:48 +02:00
decentral1se a4f1634b24 fix: backups get gzip, absolute paths, single archive file 2022-04-19 12:52:30 +02:00
decentral1se cbd924060f fix: better local changes message 2022-04-19 10:29:05 +02:00
decentral1se 3c4bb6a55e fix: ensure we're on latest for recipe release dance
Closes coop-cloud/organising#313.
2022-04-19 10:28:49 +02:00
decentral1se a0d7a76f9d fix: better error messages for release failures
See coop-cloud/organising#313
2022-04-19 10:20:35 +02:00
decentral1se c71efb46ba feat: arm builds [ci skip]
See coop-cloud/organising#312
2022-04-19 10:06:14 +02:00
decentral1se ce69967ec5 chore: go mod tidy 2022-04-18 10:42:39 +02:00
renovate-bot 1a04439b1f chore(deps): update module github.com/hashicorp/go-retryablehttp to v0.7.1 2022-04-14 07:01:24 +00:00
decentral1se 979f417a63 chore: gpl this sucka [ci skip] 2022-04-05 12:18:34 +02:00
decentral1se b27acb2f61 feat: backup/restore [ci skip]
See coop-cloud/organising#30.
2022-04-03 18:24:09 +02:00
decentral1se 622ecc4885 docs: drop slash [ci skip] 2022-04-01 23:18:22 +02:00
decentral1se ed5bbda811 docs: wording & emoji [ci skip] 2022-04-01 23:14:57 +02:00
decentral1se 7b627ea518 docs: nice gopher [ci skip] 2022-04-01 23:12:24 +02:00
decentral1se 1ac66da83f chore: go mod tidy 2022-04-01 10:21:16 +02:00
renovate-bot 061de96b62 chore(deps): update module github.com/kevinburke/ssh_config to v1.2.0 2022-04-01 07:01:23 +00:00
decentral1se 6998298d32 chore: publish next tag 0.4.0-alpha-rc8 2022-03-30 16:28:55 +02:00
decentral1se 323f4467c8 fix: filtering requires case-by-case handling
See https://github.com/moby/moby/issues/32985.
2022-03-30 16:25:38 +02:00
decentral1se e8e41850b5 fix: pass args to local function invocations too 2022-03-30 11:31:16 +02:00
decentral1se 0e23ec53d7 refactor!: simple validation only 2022-03-30 11:30:51 +02:00
decentral1se b943a8b9b1 feat: allow choosing user on remote commands 2022-03-30 11:30:36 +02:00
decentral1se acc665f054 chore: publish next tag 0.4.0-alpha-rc7 2022-03-27 21:33:30 +02:00
decentral1se 860f1d6376 feat: bring back scripts interface
See coop-cloud/organising#301.
2022-03-27 19:30:48 +00:00
decentral1se 2122f0e67c fix: avoid short command alias conflicts 2022-03-27 19:30:48 +00:00
decentral1se 6aa23a76a1 fix: more precise filtering
Closes coop-cloud/organising#305.
2022-03-27 19:30:36 +00:00
decentral1se 338360096c feat: pass domain to new app envs
See coop-cloud/organising#304.
2022-03-27 21:06:48 +02:00
decentral1se 7a8c7cd50f ci: drop static check 2022-03-27 13:51:40 +02:00
decentral1se bafc8a8e34 chore: go mod tidy 2022-03-26 15:23:27 +01:00
decentral1se 3d44d8c9fd Merge remote-tracking branch 'origin/renovate/main-github.com-docker-docker-20.x' into main 2022-03-26 15:22:31 +01:00
decentral1se b8b4616498 Merge remote-tracking branch 'origin/renovate/main-github.com-docker-cli-20.x' into main 2022-03-26 15:22:18 +01:00
renovate-bot da97117929 chore(deps): update module github.com/docker/docker to v20.10.14 2022-03-24 08:01:35 +00:00
renovate-bot 978297c464 chore(deps): update module github.com/docker/cli to v20.10.14 2022-03-24 08:01:27 +00:00
renovate-bot 11da4808fc chore(deps): update module github.com/alecaivazis/survey/v2 to v2.3.4 2022-03-24 08:01:21 +00:00
decentral1se 4023e6a066 fix: wait until app created to check for secrets 2022-03-18 11:10:15 +01:00
knoflook f432bfdd23 fix: warn when no repo on git 2022-03-18 10:13:24 +01:00
renovate-bot 848e17578d chore(deps): update golang docker tag to v1.18 2022-03-16 08:01:41 +00:00
decentral1se 1615130929 fix: skip prompt for no passwords 2022-03-15 10:54:05 +01:00
decentral1se 7f315315f0 fix: better prompts & matching for secret removal 2022-03-13 10:59:19 +01:00
decentral1se 6a50981120 fix: match on generation of single secret 2022-03-13 10:50:35 +01:00
decentral1se c67471e6ca fix: show which secret was generated 2022-03-13 10:45:08 +01:00
decentral1se f0fc1027e5 feat: more info on volumes. skip driver info 2022-03-12 17:11:05 +01:00
decentral1se c66695d55e fix: return err not logrus + new lines 2022-03-12 17:02:04 +01:00
decentral1se 262009701e fix: guard against concurrent write errors 2022-03-12 16:59:45 +01:00
decentral1se b31cb6b866 feat: prompt for secret generation
Closes coop-cloud/organising#302.
2022-03-12 16:47:19 +01:00
decentral1se f39e186b66 fix: match Force/NoInput where needed 2022-03-12 16:15:20 +01:00
decentral1se a8f35bdf2f fix: handle NoInput for volume removal 2022-03-12 16:09:05 +01:00
decentral1se 6e1e02ac28 chore: use same flag docs style 2022-03-12 16:08:44 +01:00
decentral1se 16fc5ee54b fix: can't force remove if it is already deployed 2022-03-12 16:08:26 +01:00
decentral1se 37a1fcc4af fix: delete all secrets if force/noinput 2022-03-12 16:01:42 +01:00
decentral1se a9b522719f fix: use name not stack name for pass storage 2022-03-12 16:01:31 +01:00
decentral1se ce70932a1c feat: single char short flag for volumes removal 2022-03-12 16:01:14 +01:00
decentral1se d61e104536 fix: look at removal flag for pass logic 2022-03-12 15:48:43 +01:00
decentral1se d5f30a3ae4 fix: use removal flag with correct help 2022-03-12 15:48:26 +01:00
decentral1se 2555096510 feat: short flags for run command 2022-03-12 15:42:29 +01:00
decentral1se 3797292b20 fix: no domain/converge check for deploy/upgrade/rollback 2022-03-12 15:36:43 +01:00
decentral1se 6333815b71 fix: remove unused flag 2022-03-12 15:32:23 +01:00
decentral1se 793a850fd5 refactor!: short flags for server add 2022-03-12 15:30:43 +01:00
decentral1se 42c1450384 refactor!: prefer short flags on release 2022-03-12 15:28:33 +01:00
decentral1se a2377882f6 refacator!: use single char short flags 2022-03-12 15:27:19 +01:00
decentral1se e78b395662 feat: new short flag for RC upgrading 2022-03-12 15:24:19 +01:00
decentral1se cdec834ca9 reformat: remove extra line in CLI help 2022-03-12 10:20:37 +01:00
decentral1se b4b0b464bd fix: only delete secrets from specific app
See coop-cloud/organising#300.
2022-03-12 09:39:30 +01:00
decentral1se d8a1b0ccc1 doc: indicate storage location of secret in logs 2022-03-12 09:39:15 +01:00
decentral1se 3fbd381f55 fix: add pass remove flag & show name is optional 2022-03-12 09:17:24 +01:00
decentral1se d3e127e5c8 fix: retain backwards compat with TYPE/RECIPE change 2022-03-11 19:37:50 +01:00
decentral1se e9cfb076c6 fix: strip length modifiers
See coop-cloud/organising#297.
2022-03-11 16:40:10 +01:00
decentral1se 8ccf856110 fix: lay out generated secrets with warning/clarification 2022-03-11 16:39:34 +01:00
decentral1se d0945aa09d fix: handle NoInput for app removal 2022-03-11 16:39:20 +01:00
decentral1se 123619219e chore: go mod tidy 2022-03-11 09:17:37 +01:00
decentral1se a27410952e Merge remote-tracking branch 'origin/renovate/main-github.com-docker-docker-20.x' into main 2022-03-11 09:17:15 +01:00
renovate-bot 13e0392af6 chore(deps): update module github.com/docker/docker to v20.10.13 2022-03-11 08:01:57 +00:00
renovate-bot 99a6135f72 chore(deps): update module github.com/docker/cli to v20.10.13 2022-03-11 08:01:45 +00:00
decentral1se a6b52c1354 chore: go mod tidy [ci skip] 2022-03-09 12:28:26 +01:00
renovate-bot fa51459191 chore(deps): update module github.com/docker/distribution to v2.8.1 2022-03-09 08:01:26 +00:00
decentral1se c529988427 feat: output success for secret insert [ci skip] 2022-03-08 18:10:37 +01:00
decentral1se 231cc3c718 fix: use StackName to filter volumes 2022-03-08 18:04:47 +01:00
decentral1se 3381b8936d fix: better error handling & proper context deletion for server rm 2022-02-24 15:57:52 +01:00
decentral1se 823f869f1d fix: error out correctly from ValidateDomain 2022-02-24 15:57:40 +01:00
decentral1se ecbeacf10f fix: prompt for container choice correctly on run [ci skip] 2022-02-22 11:47:36 +01:00
decentral1se 3f838038d5 chore: go mod tidy 2022-02-22 10:52:14 +01:00
renovate-bot 91b4e021d0 chore(deps): update module github.com/containers/image to v5 2022-02-22 08:01:12 +00:00
decentral1se 598e87dca2 chore: skip new repositories 2022-02-21 08:46:30 +00:00
decentral1se 001511876d chore: go mod tidy 2022-02-21 08:46:30 +00:00
decentral1se 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
decentral1se 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
decentral1se 09ac74d205 fix: check out default branch from tags
Also fix error handling to match function signatures.
2022-02-18 11:17:43 +01:00
decentral1se 5da4afa0ec fix: only ensure latest after cloning 2022-02-18 09:55:07 +01:00
decentral1se 9d5e805748 chore: go mod tidy 2022-02-16 13:53:09 +01:00
renovate-bot 770ae5ed9b chore(deps): update module github.com/moby/sys/signal to v0.7.0 2022-02-16 08:01:33 +00:00
decentral1se e056d8dc44 fix: de-dupe dns resolver logging, more concise [ci skip] 2022-02-14 18:06:06 +01:00
decentral1se c3442354e7 fix: skip dupe ipv4 check, done in EnsureDomainsResolveSameIPv4 2022-02-14 17:44:15 +01:00
decentral1se 6b2a0011af fix: remove dupe logging on catalogue reading [ci skip] 2022-02-14 17:37:25 +01:00
decentral1se 46fca7cfa7 docs: less ambig wording [ci skip] 2022-02-14 17:35:42 +01:00
decentral1se 82d560a946 fix: prompt for input on app cp 2022-02-14 17:10:53 +01:00
decentral1se fc5107865b fix: typo 2022-02-10 10:59:19 +01:00
decentral1se 53ed1fc545 chore: go mod tidy 2022-02-09 09:59:23 +01:00
renovate-bot cc9e3d4e60 chore(deps): update module github.com/docker/distribution to v2.8.0 2022-02-09 09:59:23 +01:00
decentral1se 0557284461 fix: use new repo name 2022-02-09 08:58:51 +00:00
decentral1se b5f23d3791 feat: show latest published version on sync 2022-02-09 08:58:20 +00:00
decentral1se 2b2dcc01b4 fix: dont checkout latest if we dont have a copy 2022-02-09 09:54:02 +01:00
decentral1se 0a208d049e chore: go mod tidy + patch upgrades 2022-02-04 10:50:55 +01:00
decentral1se 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
renovate-bot cd46d71ce4 chore(deps): update module github.com/schollz/progressbar/v3 to v3.8.6 2022-02-04 08:01:17 +00:00
renovate-bot 6fa090352d chore(deps): update module github.com/buger/goterm to v1.0.4 2022-02-04 08:01:11 +00:00
decentral1se 227c02cd09 refactor!: make common flags single char again 2022-02-03 14:19:51 +01:00
decentral1se bfeda40e34 fix: catch more ssh failure modes with help 2022-02-03 13:43:11 +01:00
decentral1se 5237c7ed50 docs: focus more on straight ssh docs for server add 2022-02-03 13:42:49 +01:00
decentral1se 4e09f3b9a8 refactor: migrate authors to dedicated file [ci skip] 2022-02-02 21:00:00 +01:00
decentral1se dfb32cbb68 fix: type -> recipe [ci skip] 2022-02-02 20:48:12 +01:00
decentral1se bdd9b0a1aa fix: ensure recipes on latest for lint/generate
Follows b2d17a1829.
2022-01-29 14:06:25 +01:00
decentral1se b2d17a1829 fix: ensure latest checked out for recipe upgrade 2022-01-29 13:35:42 +01:00
decentral1se c905376472 refactor!: use "config" instead of "compose" [ci skip] 2022-01-27 12:24:33 +01:00
decentral1se d316de218c feat: include recipe in deploy & friends overview 2022-01-27 12:23:02 +01:00
decentral1se 123475bd36 chore: remove old files [ci skip] 2022-01-27 12:14:01 +01:00
decentral1se 58e98f490d refactor!: type -> recipes 2022-01-27 12:06:32 +01:00
decentral1se 224b8865bf test: newlines for output when Y'ing & N'ing 2022-01-27 12:05:22 +01:00
decentral1se 8fb9f42f13 test: add remaining scripts 2022-01-27 12:05:21 +01:00
decentral1se dc5e2a5b24 test: fix pwd usage, PWD doesn't exist 2022-01-27 12:05:21 +01:00
decentral1se 40b4ef5ab2 test: disable debug, its too much noise 2022-01-27 12:05:21 +01:00
decentral1se 4a912ae3bc test: show how to run all tests 2022-01-27 12:05:21 +01:00
decentral1se 1150fcc595 test: remove manual test guide, using semi-automated now 2022-01-27 12:05:20 +01:00
decentral1se 45224d1349 test: use new flags + order for record/server 2022-01-27 12:05:20 +01:00
decentral1se 7a40e2d616 fix: remove duplicate flags on "server new" 2022-01-27 12:05:20 +01:00
decentral1se 2277e4ef72 refactor!: remove no-input flag where not needed 2022-01-27 12:05:19 +01:00
decentral1se c0c3d9fe76 refactor!: make dry-run flag more convenient 2022-01-27 12:05:19 +01:00
decentral1se 2493921ade refactor!: de-duplicate record flags 2022-01-27 12:05:19 +01:00
decentral1se 22f9cf2be4 refactor: remove unused flag 2022-01-27 12:05:18 +01:00
decentral1se a23124aede feat: auto strip domain names to avoid runtime limits 2022-01-27 10:33:21 +00:00
decentral1se e670844b56 refactor!: app name -> domain 2022-01-27 10:33:21 +00:00
decentral1se bc1729c5ca trim docs, point to new docs [ci skip] 2022-01-27 10:30:28 +01:00
decentral1se fa8611b115 fix: respect NoInput on "app cp" & use app to get StackName 2022-01-25 11:39:38 +01:00
decentral1se 415df981ff test: long flags, drop docker, use run_tests for all tests 2022-01-24 16:49:51 +01:00
knoflook 57728e58e8 test: improve semi-manual testing 2022-01-21 16:48:42 +01:00
decentral1se c7062e0494 fix: initial subcmd completion
Broken by migration to v1 API.
2022-01-20 11:42:04 +01:00
decentral1se cff7534bf9 chore: publish 0.4.0-alpha-rc6 2022-01-19 13:33:32 +01:00
decentral1se 13e582349c fix: correctly override with ~/.ssh/config if failing to connect 2022-01-19 13:28:57 +01:00
decentral1se b1b9612e01 fix: dont try to parse empty values on status lookup 2022-01-19 12:38:41 +01:00
decentral1se afeee1270e test: break up integration, rejig manual 2022-01-19 12:17:09 +01:00
decentral1se cb210d0c81 docs: pass on flag/help strings 2022-01-19 11:21:06 +01:00
decentral1se 9f2bb3f74f refactor!: remove auto dns, too magic, too broken 2022-01-19 11:20:51 +01:00
decentral1se a33767f848 refactor!: drop auto traefik deploy, rarely works 2022-01-19 11:08:43 +01:00
decentral1se a1abe5c6be refactor!: drop backup/restore for now
This will be done with the bot from now on.
2022-01-19 11:06:54 +01:00
decentral1se 672b44f965 test: remove since we're not supporting that in abra now 2022-01-19 11:04:28 +01:00
decentral1se 6d9573ec7e test: more help for how to do this 2022-01-19 11:04:15 +01:00
decentral1se 53cd3b8b71 fix: drop duplicate flags 2022-01-19 10:58:09 +01:00
decentral1se b9ec41647b fix: when upgrading, skip over bad tags, don't error out 2022-01-19 10:40:55 +01:00
decentral1se f4b563528f docs: point to new option for better assurance on tag listing 2022-01-19 10:40:37 +01:00
decentral1se f9a2c1d58f refactor: put StripTagMeta into formatter package
Avoid circular import.
2022-01-19 10:40:14 +01:00
decentral1se 7a66a90ecb fix!: change dry-run alias to not conflict with debug 2022-01-18 17:13:28 +01:00
decentral1se 0e688f1407 refactor!: migrate to urfave/cli v1
Better flexible flags handling.
2022-01-18 14:38:20 +01:00
decentral1se c6db9ee355 chore: publish 0.4.0-alpha-rc5 2022-01-18 11:39:02 +01:00
decentral1se 7733637767 fix: ensure catalogue cloned for catalogue reliant commands 2022-01-18 11:19:33 +01:00
decentral1se 88f9796aaf fix: let us know if not pushing changes without dry-run (recipe release) 2022-01-18 10:55:07 +01:00
decentral1se 6cdba0f9de fix: commit changes if dry-run not present (recipe release) 2022-01-18 10:54:54 +01:00
decentral1se 199aa5f4e3 fix: read password length from env files 2022-01-17 22:34:32 +01:00
decentral1se 9b26c24a5f docs: drop that, not happening 2022-01-17 22:27:25 +01:00
decentral1se ca75654769 fix: read correct app file name for secret generation
Stack name is only an internal docker concept now.
2022-01-17 22:17:59 +01:00
decentral1se fc2d83d203 fix: better error message for missing server 2022-01-17 22:04:11 +01:00
decentral1se 2f4f288a46 feat: -a/--all-tags for listing all tags on recipe upgrade 2022-01-17 21:59:31 +01:00
decentral1se e98f00d354 chore: go mod tidy 2022-01-17 21:50:25 +01:00
renovate-bot b4c2773b87 chore(deps): update module gotest.tools/v3 to v3.1.0 2022-01-17 08:01:18 +00:00
decentral1se 3aec5d1d7e fix: ignore new test repo 2022-01-12 16:11:18 +01:00
decentral1se e0fa1b6995 fix: let users know what was deleted 2022-01-06 11:47:10 +01:00
decentral1se b69ab0df65 fix: chaos mode fixed for upgrade/rollback
Follows 4b7ec6384c.
2022-01-06 10:32:24 +01:00
decentral1se 69a7d37fb7 chore: release 0.4.0-alpha-rc4 2022-01-06 10:04:43 +01:00
decentral1se 87649cbbd0 docs: more manual test cases [ci skip] 2022-01-05 19:37:41 +01:00
decentral1se 4b7ec6384c fix: fix chaos mode for deployment 2022-01-05 19:21:41 +01:00
decentral1se b22b63c2ba fix: only output if volumes selected for removal 2022-01-05 19:00:09 +01:00
decentral1se d9f3a11265 fix: gracefully handle missing tag for syncing 2022-01-05 18:04:46 +01:00
decentral1se d7cf11b876 fix: further fixes for gracefully handling missing tag
Follows 1b37d2d5f5.
2022-01-05 17:58:15 +01:00
decentral1se d7e1b2947a fix: skip failed image parse for upgrade and move on 2022-01-05 17:57:11 +01:00
decentral1se 1b37d2d5f5 fix: handle tags without images gracefully 2022-01-05 17:32:58 +01:00
decentral1se 74dfb12fd6 refactor: centralise tag meta stripping 2022-01-05 17:32:33 +01:00
decentral1se 49ccf2d204 fix: also show skip for non semver tags 2022-01-04 22:49:36 +01:00
decentral1se 76adc45431 docs: match typically log message style 2022-01-04 22:49:23 +01:00
decentral1se e38a0078f3 chore: publish 0.4.0-alpha-rc3 2022-01-04 15:34:10 +01:00
decentral1se 25b44dc54e refactor!: use lowercase option to match others 2022-01-04 12:25:45 +01:00
decentral1se 0c2f6fb676 fix: app autocomplete for secret commands 2022-01-04 12:24:37 +01:00
decentral1se 10e4a8b97f fix: handle StackName/AppName correctly for new app creation 2022-01-04 11:56:29 +01:00
decentral1se eed2756784 fix: new app table colume matches usual order now 2022-01-04 11:56:17 +01:00
decentral1se b61b8f0d2a fix: always check for deployed status when removing
You can't delete regardless of -f if an app is deployed, the runtime
will error out. Best just deal with this for all cases then on our side.
2022-01-04 11:38:07 +01:00
decentral1se 763e7b5bff fix: use StackName for querying via Docker 2022-01-04 11:37:45 +01:00
decentral1se d5ab9aedbf docs: match other abort command outputs 2022-01-04 11:37:35 +01:00
decentral1se 2ebb00c9d4 docs: confirm prompt matches language of command 2022-01-04 11:37:04 +01:00
decentral1se 6d76b3646a fix: use spaces like the rest [ci skip] 2022-01-03 18:41:11 +01:00
decentral1se 636dc82258 chore: 0.4.x rc2 2022-01-03 16:37:19 +01:00
decentral1se 66d5453248 docs: recommend more helper commands for deploy timeout 2022-01-03 16:33:28 +01:00
decentral1se ba9abcb0d7 fix: increase converge timeout 2022-01-03 16:33:18 +01:00
decentral1se a1cbf21f61 fix: handle "uknown" version on deployment
Fixes pre-deploy overview version listing.
2022-01-03 16:32:03 +01:00
decentral1se bd1da39374 fix: show latest version when up-to-date 2022-01-03 16:31:30 +01:00
decentral1se 8b90519bc9 test: more manual test examples 2022-01-03 16:31:16 +01:00
decentral1se 65feda7f1d fix: dont lookup release notes if no version passed 2022-01-03 16:14:56 +01:00
decentral1se 64e223a810 fix: dont display non-existant release notes if no version 2022-01-03 16:14:44 +01:00
decentral1se 379e01d855 fix: use installer without progress bar [ci skip]
Doesn't look well when invoked from "bash -c '...'" when we run "abra
upgrade". The progress bar shoots down the page and you miss the intro
banner.
2022-01-02 20:39:11 +01:00
decentral1se a421c0dca5 test: use new name [ci skip] 2022-01-02 20:18:37 +01:00
decentral1se abf56f9054 chore: publish 0.4.0-alpha-rc1 2022-01-02 20:05:53 +01:00
decentral1se 4dec3c4646 fix: show order as in other tables 2022-01-02 16:25:18 +01:00
decentral1se c900cebc30 fix: fix filtering by type for output 2022-01-02 16:21:22 +01:00
decentral1se 30209de3e2 fix: correct url for commit [ci skip] 2022-01-02 16:01:03 +01:00
decentral1se 625747d048 fix: get right url 2022-01-02 15:54:46 +01:00
decentral1se a71b070921 feat: support skipping upgrades 2022-01-02 15:46:35 +01:00
decentral1se 33ff04c686 fix: dont list if no volumes 2022-01-02 15:20:17 +01:00
decentral1se c69a3c23c5 fix: show app arg 2022-01-02 15:19:40 +01:00
decentral1se 0b46909961 fix: dont output if no secrets 2022-01-02 15:19:30 +01:00
decentral1se 832e8e5a96 test: finish first draft of manual test plan 2022-01-02 15:19:12 +01:00
decentral1se abf83aa641 test: finish first pass on core integration script 2022-01-02 15:04:49 +01:00
decentral1se 1df69aa259 refactor: more shuffling test infra around [ci skip] 2022-01-02 14:59:46 +01:00
decentral1se 7596a67ad5 refactor: refocus the script purpose 2022-01-02 14:05:02 +01:00
decentral1se 93c7612efc feat: allow to only destroy remote server 2022-01-02 01:52:49 +01:00
decentral1se 2c78ac22e0 fix: handle missing ssh keys (pass auth) 2022-01-02 01:52:33 +01:00
decentral1se 13661c72ce test: more example env vars 2022-01-02 01:52:09 +01:00
decentral1se 454092644a test: debug + catalogue/recipe commands [ci skip] 2022-01-01 22:04:04 +01:00
decentral1se 224c0c38db fix: setup git for e2e testing 2022-01-01 22:03:53 +01:00
decentral1se 560e0eab86 fix: ensure catalogue is present 2022-01-01 22:01:16 +01:00
decentral1se b92fdbbd52 fix: use right arg 2022-01-01 21:46:48 +01:00
decentral1se 0a550363b8 fix: correctly count recipes 2022-01-01 21:46:38 +01:00
decentral1se 3119220c21 fix: better error 2022-01-01 21:46:24 +01:00
decentral1se 49f565e5db test: start on integration script 2022-01-01 21:36:00 +01:00
decentral1se 94522178b1 fix: handle noinput case 2022-01-01 21:34:58 +01:00
decentral1se 810bc27967 fix: dont assume ipv4 exists 2022-01-01 21:34:49 +01:00
decentral1se 35d95fb9fb docs: better example 2022-01-01 21:34:33 +01:00
decentral1se d26fabe8ef fix: handle zone argument correctly 2022-01-01 21:34:21 +01:00
decentral1se 84bf3ffa50 fix: use right variable 2022-01-01 21:34:07 +01:00
decentral1se 575485ec7a refactor: more portable wget usage 2022-01-01 21:33:50 +01:00
decentral1se 0b17292219 fix: revert to existing tags for testing purposes [ci skip] 2022-01-01 20:52:17 +01:00
decentral1se fffd8b2647 docs: add missing 'the' 2022-01-01 19:56:32 +01:00
decentral1se c07128b308 refactor: drop integration tests [ci skip]
Will use script instead.
2022-01-01 19:56:24 +01:00
decentral1se 929ff88013 fix: handle missing versions 2022-01-01 17:37:34 +01:00
decentral1se 0353427c71 fix: adapt to new unkown version marker
Follows 7a0d18ceb6.
2022-01-01 17:37:10 +01:00
decentral1se 7a0d18ceb6 fix: show unknown insteaf of empty for missing version 2022-01-01 17:23:21 +01:00
decentral1se 8992050409 docs: dont metion git explicitly in user messages 2022-01-01 17:23:04 +01:00
decentral1se abd094387f fix: use scale for restarting
The other approach wasn't working. Duplicating containers on restart.
You'd end up with 2 containers per restart...
2022-01-01 17:22:35 +01:00
decentral1se a556ca625b fix: handle StackName / Name correctly 2022-01-01 17:22:19 +01:00
decentral1se 1b7836009f test: spec out check tests [ci skip] 2021-12-31 17:19:30 +01:00
decentral1se eb3509ab3f refactor: drop uneccessary structs 2021-12-31 17:12:09 +01:00
decentral1se 87851d26f7 chore: makefile default runs more common tasks 2021-12-31 17:11:54 +01:00
decentral1se c4f344b50a refactor: move to manual dir [ci skip] 2021-12-31 16:56:18 +01:00
decentral1se 60e4dfd9cb refactor!: use lowercase like the rest style 2021-12-31 16:53:58 +01:00
decentral1se d957adb675 docs: update the release description 2021-12-31 16:48:03 +01:00
decentral1se 5254af0fe4 fix: handle no changes edge case for recipe release 2021-12-31 13:45:01 +01:00
decentral1se ce96269be0 fix: more fixed for dry mode, this time tested :)
Follows 299276c383.
2021-12-31 13:37:03 +01:00
decentral1se 299276c383 fix: handle dry run output result correctly 2021-12-31 13:17:50 +01:00
decentral1se 866cdd1f29 feat: service name in ps output 2021-12-31 12:59:31 +01:00
decentral1se 95d385c420 fix: GetService & handling missing services 2021-12-31 12:49:31 +01:00
decentral1se 605e2553b8 docs: expand errors docs 2021-12-31 12:10:11 +01:00
decentral1se 1245827dff fix: handle %s correctly 2021-12-31 12:05:40 +01:00
decentral1se 9bdb07463c fix: handle filtered server list with sort 2021-12-30 02:06:04 +01:00
decentral1se be26f80f03 fix: maintain sorted output 2021-12-30 01:07:21 +01:00
decentral1se 930ff68bb2 refactor: drop unused function 2021-12-30 00:42:37 +01:00
decentral1se 62441acf03 refactor: use SmallSHA 2021-12-30 00:41:21 +01:00
decentral1se 7460668ef4 fix: explain for single repo case too 2021-12-28 03:42:44 +01:00
decentral1se 047d0e6fbc fix: working url 2021-12-28 03:42:02 +01:00
decentral1se 8785f66391 feat: link direct to tag 2021-12-28 03:40:18 +01:00
decentral1se 24882e95b4 fix: take version from sync when releasing 2021-12-28 03:40:02 +01:00
decentral1se 1fd0941239 refactor: improved version choice flow 2021-12-28 03:19:32 +01:00
decentral1se 26a11533b4 feat: link directly to new commit 2021-12-28 02:37:35 +01:00
decentral1se b4f48c3c59 feat: show release notes on upgrade 2021-12-28 02:31:21 +01:00
decentral1se 43e68a99b0 refactor: reverse list function finally 2021-12-28 02:31:06 +01:00
decentral1se bac6fb0fa8 docs: better wording 2021-12-28 02:01:50 +01:00
decentral1se dc9c9715ce fix: remove duplication 2021-12-28 02:01:43 +01:00
decentral1se 1f91b3bb03 fix: add prompt before publishing 2021-12-28 01:51:39 +01:00
decentral1se a700aca23d fix: add autocomplete for app run 2021-12-28 01:37:41 +01:00
decentral1se 5cacd09a04 refactor: remove old/non-urgen/resolved FIXMEs 2021-12-28 01:35:40 +01:00
decentral1se 6a98024a2b refactor: drop old/upstream TODOs 2021-12-28 01:31:50 +01:00
decentral1se e85117be22 docs: capitalistion, style 2021-12-28 01:27:58 +01:00
decentral1se fb24357d38 refactor: merge top-level into one file 2021-12-28 01:26:40 +01:00
decentral1se f5d2d3adf6 refactor: formatter gets own package 2021-12-28 01:24:23 +01:00
decentral1se 07119b0575 refactor: less files, they werent used generally 2021-12-28 01:08:44 +01:00
decentral1se d2a6e35986 refactor: rename to flags 2021-12-28 01:04:51 +01:00
decentral1se 0aa37fcee8 refactor!: simplifying publish logic 2021-12-27 19:56:27 +01:00
decentral1se eb1b6be4c5 fix: auto-config ssh urls and push to them 2021-12-27 18:06:56 +01:00
decentral1se b98397144a fix: wording 2021-12-27 18:06:46 +01:00
decentral1se 4c186678b8 fix: clone https url by default
Catalogue package had to be merged into the recipe package due to too
many circular import errors. Also, use https url for cloning, assume
folks don't have ssh setup by default (the whole reason for the
refactor).
2021-12-27 16:45:56 +01:00
decentral1se b1d9d9d858 refactor: wording & short options 2021-12-27 16:12:29 +01:00
decentral1se a06043375d refactor: remove unused flag 2021-12-27 16:07:57 +01:00
decentral1se 3eef1e8587 feat: filter recipes list 2021-12-27 11:00:04 +01:00
decentral1se 37e48f262b fix: better wording 2021-12-27 04:17:30 +01:00
decentral1se 06cc5d1cc3 fix: only update when really needed 2021-12-27 04:10:12 +01:00
decentral1se c13f438580 refactor: remove old code 2021-12-27 04:03:53 +01:00
decentral1se 5cd4317580 fix: more performant ps'in 2021-12-27 04:00:37 +01:00
decentral1se 2ba1ec3df0 fix: x-platform loop output
See coop-cloud/organising#178.
2021-12-27 03:55:42 +01:00
decentral1se 34cdb9c9d8 fix: check for deployment when ps'in 2021-12-27 03:53:45 +01:00
decentral1se 9c281d8608 fix: flags for logging in 2021-12-27 03:27:05 +01:00
decentral1se 321ba1e0ec fix: template without weird breakages 2021-12-27 03:14:48 +01:00
decentral1se c5a74e9f6b fix: template env files too 2021-12-26 04:38:34 +01:00
decentral1se f8191ac248 refactor: go with domains as default 2021-12-26 04:24:12 +01:00
decentral1se 027c8a1420 fix: better recipe meta defaults 2021-12-26 04:10:50 +01:00
decentral1se cdc08ae95a fix: much hacking, maybe fixed catalogue generation 2021-12-26 04:02:40 +01:00
decentral1se 3f35510507 fix: runtime caching for catalogue generation 2021-12-26 04:01:02 +01:00
decentral1se 9f70a69bbf feat: skip git syncing on catalogue generation 2021-12-26 03:46:26 +01:00
decentral1se b0834925a3 fix: log in correctly
See coop-cloud/abra#139.
2021-12-26 03:44:29 +01:00
decentral1se 86d87253c5 fix: pass name correctly
Follows from 9cc2554846
2021-12-26 00:15:03 +01:00
decentral1se 17340a79da refactor: more local var 2021-12-26 00:14:48 +01:00
decentral1se 779c810521 refactor: less quotes, less verbose 2021-12-26 00:14:32 +01:00
decentral1se 9cc2554846 fix: don't run twice 2021-12-26 00:02:46 +01:00
decentral1se 9a1cf258a5 fix: check published version properly
Resulted in a refactor to a new lint package.
2021-12-26 00:00:19 +01:00
decentral1se ba8138079f fix: use one function for up-to-date checks 2021-12-25 23:45:52 +01:00
decentral1se 8735a8f0ea feat: lint before deploy/upgrade/rollback
See coop-cloud/organising#254.
2021-12-25 23:35:45 +01:00
decentral1se a84a5bc320 feat: more robust linting
See coop-cloud/organising#254.
2021-12-25 23:22:50 +01:00
decentral1se ae0e7b8e4c fix: dont wrap for table output 2021-12-25 17:22:40 +01:00
decentral1se c0caf14d74 fix: more meta for listing recipes 2021-12-25 17:17:41 +01:00
decentral1se d66c558b5c fix: dont render if no versions 2021-12-25 17:12:41 +01:00
decentral1se c8541e1b9d fix: show latest first 2021-12-25 17:12:34 +01:00
decentral1se 653b6c6d49 fix: autocomplete for recipe versions 2021-12-25 17:12:22 +01:00
decentral1se e2c3bc35c3 fix: handle missing label 2021-12-25 17:02:47 +01:00
decentral1se 6937bfbb0d fix: if no remotes, skip on 2021-12-25 16:56:21 +01:00
decentral1se decfe095fe feat: improved recipe creation 2021-12-25 16:56:20 +01:00
decentral1se 4283f130a2 refactor: apps -> recipes 2021-12-25 14:04:07 +01:00
decentral1se 3b5354b2a5 refactor: less quotes 2021-12-25 02:03:09 +01:00
decentral1se 14400d4ed8 fix: sync recipes from remotes 2021-12-24 16:06:29 +01:00
decentral1se dddf84d92b fix: avoid default value for idf
We could default to ~/.ssh/id_rsa but if that doesn't exist, then we'll
just be confusing people in the logs. Best is to just rely on the
ssh-agent which overrides this anyway. We will document this.

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

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

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

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

    coop-cloud/organising#250

And also part of:

    coop-cloud/docs.coopcloud.tech#27

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

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

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

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

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

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

The UX of record creation/deletion now mirrors the UX of new apps. All
the things are prompted for.
2021-10-22 08:58:18 +02:00
d1admin 9f9248b987 feat: select prompt for recipes on app new 2021-10-22 08:21:46 +02:00
d1admin 2bb4a9c063 docs: fix flag name [ci skip] 2021-10-21 20:58:01 +02:00
d1admin 0c8dba0681 docs: try handles directly [ci skip] 2021-10-21 20:53:04 +02:00
d1admin a491332c1c feat: support no-input mode for deploy ops 2021-10-21 20:48:45 +02:00
d1admin 6a75ffc051 docs: shape up release docs [ci skip] 2021-10-21 20:37:04 +02:00
d1admin 5261d1a033 chore: drop unused dep [ci skip] 2021-10-21 20:17:48 +02:00
d1admin a458a5d9f7 docs: mark upstreams for all upstreams 2021-10-21 19:54:43 +02:00
d1admin 5ce2419354 docs: mark new pkg for upstream [ci skip] 2021-10-21 19:41:20 +02:00
d1admin 963f8dcc73 fix: recover tests from overzealous cleanup 2021-10-21 19:40:26 +02:00
d1admin dc04cf5ff7 chore: migrate all upstream code to own dir 2021-10-21 19:35:13 +02:00
d1admin 80921c9f55 fix: remove cruft + readme pass + document forks 2021-10-21 18:35:24 +02:00
d1admin 8b15f2de5b chore: publish new release 2021-10-21 16:03:19 +02:00
d1admin cdb76e7276 fix: catch multiple containers correctly 2021-10-21 16:01:54 +02:00
d1admin a170e26e27 fix: drop copy/pasta, keep timeouts 2021-10-21 15:42:50 +02:00
d1admin 03b1882b81 chore: publish new tag 2021-10-21 15:17:34 +02:00
d1admin 2fcdaca75f fix: dont duplicate info output 2021-10-21 15:13:24 +02:00
d1admin c5f44cf340 feat: show undploy overview 2021-10-21 15:10:43 +02:00
d1admin 7a5ad65178 fix: load timeout before other opts 2021-10-21 15:06:03 +02:00
d1admin 6d4ee3de0d fix: force flag works for upgrade 2021-10-21 11:44:47 +02:00
d1admin 63318fb6ff fix: handle chaos mode correctly for deploy
Closes coop-cloud/organising#210.
2021-10-21 10:19:30 +02:00
d1admin 07ffa08a07 chore: remove unused files 2021-10-20 21:04:09 +02:00
d1admin 0e5e7490b3 docs: some rewording and clarifying 2021-10-20 17:52:54 +02:00
d1admin 640032b8fe fix: remove duplicate version command
We can use --version/-v instead.
2021-10-20 17:48:50 +02:00
d1admin 39babea963 docs: remove that missing feature [ci skip] 2021-10-20 17:36:41 +02:00
d1admin 07613f5163 fix: devendor capsul code
Closes coop-cloud/organising#155.
2021-10-20 17:34:01 +02:00
d1admin 7f1d9eeaec fix: check if record already exists 2021-10-20 16:56:34 +02:00
d1admin 02d24104e1 feat: domain CRUD complete with Gandi provider 2021-10-20 16:52:19 +02:00
roxxers da8d72620a test: warning not to test cli [ci skip] 2021-10-20 10:15:55 +01:00
roxxers 96ccadc70f refactor: move making app struct to construct func
makes the code cleaner and easier to grab the app struct for testing
2021-10-20 09:45:38 +01:00
d1admin 8703370785 WIP: domain create 2021-10-20 00:05:57 +02:00
d1admin 7d8c53299d docs: more domain command docs hacking 2021-10-20 00:05:49 +02:00
d1admin 0110aceb1f docs: rewording 2021-10-19 23:03:12 +02:00
d1admin aec1e4520d fix: handle missing containers
Closes coop-cloud/organising#198.
2021-10-19 22:50:43 +02:00
d1admin 74bcb99c70 fix: use this weird default
Closes coop-cloud/organising#207.
2021-10-19 22:43:43 +02:00
d1admin dd4f2b48ec fix: explode when wrong provider chosen 2021-10-19 10:19:31 +02:00
d1admin 7f3f41ede4 docs: dns list docs 2021-10-18 22:20:11 +02:00
d1admin 597b4b586e WIP: domain listing with Gandi
Rethinking the interface already.
2021-10-18 22:16:29 +02:00
d1admin 7ea3df45d4 WIP: dns support via libdns 2021-10-18 20:35:43 +02:00
d1admin 5941ed9728 fix: handle exceptions 2021-10-18 20:35:32 +02:00
d1admin d1e42752e2 fix: set connection timeouts + clean up bad contexts
Closes coop-cloud/organising#205.
2021-10-18 10:48:43 +02:00
d1admin 9dfbd21c61 fix: parse args correctly for validation 2021-10-18 09:43:32 +02:00
d1admin 9526d1fde6 fix: ensure we have version checked out on deploy 2021-10-18 09:30:43 +02:00
d1admin 62cc7ef92d feat: upgrade/downgrade support chaos mode 2021-10-18 08:57:25 +02:00
d1admin c5a7a831d2 docs: chaos mode flag docs 2021-10-18 08:35:59 +02:00
d1admin 4aae186f5f chore: squash formatting issue 2021-10-18 08:27:39 +02:00
d1admin 2f9b11f389 feat: support deploying with chaos mode 2021-10-18 08:14:06 +02:00
d1admin 6d42e72f16 fix: allow for client creation on default context
See coop-cloud/organising#206.
2021-10-17 23:50:44 +02:00
d1admin 5be190e110 fix: check that docker is installed on local add 2021-10-17 23:50:28 +02:00
d1admin c1390f232e fix: show "local" instead of "default" 2021-10-17 23:50:12 +02:00
3wordchant 95e19f03c4 fix: make release not crash on missing images 2021-10-16 18:57:21 +02:00
knoflook dc040a0b38 chore: change test context names 2021-10-16 13:26:03 +02:00
knoflook e6e2e5214f test: add tests for pkg/client/client.go 2021-10-16 13:04:57 +02:00
knoflook 61452b5f32 docs: add README.md to document testing 2021-10-16 12:26:43 +02:00
knoflook 78460ac0ba test: increatse client/context.go coverage to 90% 2021-10-16 11:41:41 +02:00
d1admin 0615c3f745 fix: support downgrade/upgrade for unknown versions 2021-10-15 09:58:45 +02:00
3wordchant e820e0219d docs: how to enable bash autocomplete from source 2021-10-14 22:37:32 +02:00
d1admin 75fb9a2774 chore: publish new version 2021-10-14 13:31:18 +02:00
d1admin 0d500b636d feat: more info on version changing deployments 2021-10-14 13:30:33 +02:00
d1admin 5dd97cace0 docs: expand deploy/upgrade/downgrade docs 2021-10-14 12:26:07 +02:00
d1admin ae32b1eed2 fix: standardise checkout options 2021-10-14 12:17:58 +02:00
d1admin 113bdf9e86 feat: add stats to app list 2021-10-14 12:02:12 +02:00
d1admin d4d4da19b7 feat: first steps towards watchable ps output
See coop-cloud/organising#178.
2021-10-14 11:51:40 +02:00
d1admin 454ee696d6 fix: make ps a bit more useful and less verbose 2021-10-14 11:36:03 +02:00
d1admin ca16c002ba docs: add more description for versions command 2021-10-14 11:32:32 +02:00
d1admin 91cc8b00b3 fix: avoid alias conflict 2021-10-14 11:32:25 +02:00
d1admin d0828c4d8d fix: teach app version command to read new versions 2021-10-14 11:29:57 +02:00
d1admin b69aed3bcf feat: add rollback command
Closes coop-cloud/organising#127.
2021-10-14 01:52:55 +02:00
d1admin 875255fd8c feat: add upgrade command 2021-10-14 01:23:04 +02:00
d1admin 2dca602c0b fix: error handling in deploy 2021-10-14 01:22:54 +02:00
d1admin 1dca8a1067 chore: set 1.16 as requirement now
Closes coop-cloud/organising#201.
2021-10-13 16:55:58 +02:00
d1admin 37022bf0c8 feat: make deploy only deploy
See coop-cloud/organising#127.
2021-10-13 16:51:04 +02:00
knoflook eb5b35d47f build: change sed flags in installer for mac os compatibility 2021-10-13 16:36:07 +02:00
knoflook ece1130797 build: add automatic os and architecture detection to installer script 2021-10-13 15:51:19 +02:00
knoflook c266316f7e build: remove python3 dependency from installer 2021-10-13 15:08:00 +02:00
d1admin d804276cf2 feat: add pre-deploy overview 2021-10-12 13:25:23 +02:00
d1admin 4235e06943 chore: new 0.1.8-alpha release 2021-10-12 11:19:26 +02:00
d1admin a9af0b3627 fix: let gofmt do its magic 2021-10-12 10:34:10 +02:00
3wordchant a0b4886eba WIP: default to compose.yml instead of all of 'em 2021-10-12 10:25:37 +02:00
d1admin 84489495dc fix: load STACK_NAME if not present 2021-10-12 09:03:48 +02:00
d1admin a8683dc38a refactor: better formatting 2021-10-12 08:59:14 +02:00
d1admin e2128ea5b6 fix: check key existance correctly 2021-10-12 08:55:42 +02:00
d1admin ca3c5fef0f refactor: better wording [ci skip] 2021-10-12 08:49:38 +02:00
d1admin 4a01e411be refactor: handle STACK_NAME override in one place 2021-10-12 01:14:14 +02:00
d1admin 777d49ac1d fix: handle STACK_NAME for the ps command 2021-10-12 01:11:34 +02:00
d1admin deb7d21158 fix: dont loop over dead tags
Closes coop-cloud/organising#195.
2021-10-12 00:56:52 +02:00
knoflook 6db1fdcfba refactor!: recipe upgrade: use new tagcmp version 2021-10-11 14:43:06 +00:00
d1admin 44dc0edf7b refactor: use ; trick for inline checking [ci skip] 2021-10-11 13:48:25 +02:00
knoflook 36ff50312c fix!: use annotated tags with recipe release 2021-10-11 10:45:00 +02:00
d1admin ff4b978876 fix: only list new versions
Closes coop-cloud/organising#192.
2021-10-11 01:17:52 +02:00
d1admin b68547b2c2 fix: dont overwrite generated catalogue
Closes coop-cloud/organising#190.
2021-10-11 01:06:51 +02:00
d1admin 0140f96ca1 fix: make sure to clone recipe
Closes coop-cloud/organising#193.
2021-10-11 00:34:23 +02:00
3wordchant 1cb45113db fix: default linux binary in installer, add context
Closes coop-cloud/organising#184
2021-10-09 21:45:28 +02:00
d1admin c764243f3a fix: manage multiple version showing edge cases 2021-10-08 10:50:48 +02:00
d1admin dde8afcd43 feat: support version/upgrade listing
Closes coop-cloud/organising#130.
2021-10-08 09:51:47 +02:00
d1admin 98ffc210e1 fix: show descending orders on releases [ci skip] 2021-10-06 09:13:07 +02:00
d1admin 7c0d883135 chore: new release 2021-10-06 08:48:23 +02:00
d1admin e78ced41fb fix: use freifunk DNS resolver
Closes coop-cloud/organising#180.
2021-10-06 08:47:01 +02:00
d1admin e9113500d8 feat: allow to override STACK_NAME 2021-10-05 20:40:16 +02:00
d1admin 7368cabc49 fix: format output correctly 2021-10-05 20:24:52 +02:00
d1admin f75e264811 fix: ensure dirs are created
Also use debug logging for help.

Closes coop-cloud/organising#183.
Closes coop-cloud/organising#183.
2021-10-05 20:24:41 +02:00
d1admin 8bfd76fd04 feat: generate versions for catalogue also
Closes coop-cloud/organising#179.
2021-10-05 20:14:00 +02:00
knoflook 1cb5e3509d fix: add compose.yml before commiting with recipe release; reset parts of tag according to semver when releasing 2021-10-05 16:36:15 +02:00
d1admin 3cd2399cca fix: ignore WIP stuff and sort [ci skip] 2021-10-05 11:56:26 +02:00
knoflook 11c4651a3b fix: don't crash when there is a more serious upgrade available 2021-10-05 09:55:25 +00:00
knoflook 49f90674f2 fix: --major/minor/patch is the most serious upgrade you want to do 2021-10-05 09:55:25 +00:00
knoflook 74a70edb03 feat: upgrade an app with no user input with --minor/major/patch flag 2021-10-05 09:55:25 +00:00
knoflook 6fc5c31347 WIP: #172 upgrade --major/minor/patch placeholder 2021-10-05 09:55:25 +00:00
d1admin c616907b71 feat: teach recipe sync to understand new versions
Closes coop-cloud/organising#177.
2021-10-05 10:28:09 +02:00
d1admin a58cea3e0a docs: dont assume that yet [ci skip] 2021-10-02 23:30:18 +02:00
d1admin 700f89425a chore: publish new release 2021-10-02 23:01:25 +02:00
d1admin 8cc0a350e6 fix: pass sample env when loading recipe
Closes coop-cloud/organising#176.
2021-10-02 23:00:09 +02:00
d1admin 46e67fa420 feat: support darwin builds 2021-10-02 22:53:07 +02:00
d1admin cacbb5a0f1 docs: remove extra change log items 2021-10-02 22:51:27 +02:00
d1admin e7046a15aa docs: keep it all lowercase 2021-10-02 22:51:12 +02:00
d1admin c1fd97c427 fix: handle new local server is listing 2021-10-02 22:40:08 +02:00
d1admin 2f218bd99f fix: ensure ~/.abra is created
Also make that debug message less cringe.
2021-10-02 22:37:30 +02:00
d1admin 48290aa316 fix: make server path creation more robust 2021-10-02 22:30:08 +02:00
d1admin db5cbfa992 docs: reword this local flag usage 2021-10-02 22:14:01 +02:00
d1admin 4c11e813e8 test: ensure .env reading tests work 2021-10-02 22:10:00 +02:00
knoflook 6ae75e013a refactor: move Major, Minor and Patch to recipe.go 2021-10-01 19:49:18 +02:00
d1admin 09f49cdc76 chore: fix tests 2021-10-01 12:57:34 +02:00
d1admin 22118b88e4 chore: appease formatter 2021-10-01 12:56:04 +02:00
d1admin e6db064149 chore: publish next tag 0.1.5-alpha 2021-10-01 12:32:41 +02:00
3wordchant 3688ea9d69 feat: support local server with --local 2021-10-01 11:59:17 +02:00
3wordchant 7c4cdc530c fix: don't crash if no abra.sh 2021-10-01 11:40:19 +02:00
3wordchant 49781c7e3f fix: ignore "env" files which don't end in .env 2021-10-01 11:40:19 +02:00
d1admin 10b15d65b4 docs: use same style log messages [ci skip] 2021-09-29 22:37:16 +02:00
d1admin 1c5d6d6357 docs: attempt some cmd docs 2021-09-29 22:36:43 +02:00
decentral1se 75bdd59585 Merge pull request 'feat: add a flag to commit your changes before creating a tag' (#102) from knoflook/abra:recipe-release into main
Reviewed-on: coop-cloud/abra#102
2021-09-29 20:24:55 +00:00
knoflook 96bb145981 feat: check and sanitize user-specified tag 2021-09-29 16:25:39 +02:00
knoflook c4c76f4848 feat: add a flag to commit your changes before creating a tag 2021-09-29 16:08:02 +02:00
decentral1se 2076c566bb Merge pull request 'feat: tag recipes with abra' (#99) from knoflook/abra:recipe-release into main
Reviewed-on: coop-cloud/abra#99
2021-09-29 12:39:35 +00:00
d1admin 62f6327b66 refactor: use usual naming style [ci skip] 2021-09-28 21:28:46 +02:00
d1admin 6f9120b59c chore: run mod tidy 2021-09-28 21:27:31 +02:00
decentral1se 8c617a9f12 Merge pull request 'feat: print stack traces for errors when debugging' (#101) from knoflook/abra:main into main
Reviewed-on: coop-cloud/abra#101
2021-09-28 19:26:56 +00:00
knoflook 857d12d23c feat: print stack traces for errors when debugging 2021-09-27 12:24:02 +02:00
knoflook 22c4d0d864 style: remove doubled debug message 2021-09-24 11:05:49 +02:00
knoflook e700e44363 feat: add main apps version as a semver build metadata when releasing 2021-09-24 10:48:09 +02:00
knoflook 9faefd2592 feat: push the new tag with --push 2021-09-23 18:52:21 +02:00
knoflook cd179175f5 refactor: dont' create the same objects twice 2021-09-23 18:32:58 +02:00
knoflook c0f92ca13d feat: support --major/-x --minor/-y --patch/-z for tag calculation 2021-09-23 18:27:19 +02:00
knoflook 48d28c8dd1 feat: tag recipes with abra 2021-09-22 16:03:56 +02:00
d1admin e840328e44 chore: publish next release 2021-09-22 09:04:19 +02:00
d1admin 6f43778691 fix: better UI/UX for app creation
Closes coop-cloud/organising#145.
2021-09-22 08:59:00 +02:00
d1admin 9783563fa6 fix: drop version checking while churning 2021-09-22 08:47:49 +02:00
d1admin 1392afc015 fix: give better error message on server create 2021-09-22 08:19:28 +02:00
d1admin 886009975d fix: order args correctly 2021-09-22 08:19:14 +02:00
d1admin b1147cd136 feat: add x-platform progress bars for long loads
Closes coop-cloud/organising#150.
2021-09-22 07:48:17 +02:00
d1admin 95a9013658 fix: use appFiles to determine server list 2021-09-20 22:43:30 +02:00
d1admin bd1bf3b0d6 chore: remove new line [ci skip] 2021-09-20 19:18:49 +02:00
d1admin 7b349732ac fix: fix name and doc exceptions for catalogue generation 2021-09-20 16:53:49 +02:00
d1admin a8ce64a9db fix: ignore abra-bash for catalogue generation 2021-09-20 16:53:38 +02:00
d1admin 96aa74a977 WIP: gather more meta for catalogue generation 2021-09-20 16:48:27 +02:00
d1admin 700f022790 WIP: use repo metadata not existing catalogue 2021-09-20 09:38:51 +02:00
d1admin d188327b17 WIP: generating new apps.json 2021-09-17 08:04:16 +02:00
d1admin fdd46a4d98 chore: run formatter 2021-09-17 07:38:38 +02:00
d1admin e00920643e WIP: implement async recipe cloning
See coop-cloud/organising#159.
2021-09-16 16:28:11 +02:00
3wordchant 754fe81e01 feat: add templating during .. app new
Closes coop-cloud/organising#168
2021-09-16 15:09:35 +02:00
d1admin bece2e8351 fix: recovering debug logging [ci skip]
Follows 31edbbd32e.
2021-09-16 13:10:17 +02:00
roxxers e47d7029d7 refactor: S1005 gosimple 2021-09-16 12:01:47 +01:00
roxxers 31edbbd32e fix: git metadata not removed in merge 2021-09-16 11:35:18 +01:00
roxxers 0a1c73bf00 refactor: use cli context vs creating new one 2021-09-16 11:21:38 +01:00
d1admin a74a8bc21b docs: finish release docs off [ci skip] 2021-09-16 09:52:03 +02:00
3998 changed files with 1040844 additions and 5263 deletions
-40
View File
@@ -1,40 +0,0 @@
{{ range .Versions }}
<a name="{{ .Tag.Name }}"></a>
## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }}
> {{ datetime "2006-01-02" .Tag.Date }}
{{ range .CommitGroups -}}
### {{ .Title }}
{{ range .Commits -}}
* {{ .Subject }}
{{ end }}
{{ end -}}
{{- if .RevertCommits -}}
### Reverts
{{ range .RevertCommits -}}
* {{ .Revert.Header }}
{{ end }}
{{ end -}}
{{- if .MergeCommits -}}
### Pull Requests
{{ range .MergeCommits -}}
* {{ .Header }}
{{ end }}
{{ end -}}
{{- if .NoteGroups -}}
{{ range .NoteGroups -}}
### {{ .Title }}
{{ range .Notes }}
{{ .Body }}
{{ end }}
{{ end -}}
{{ end -}}
{{ end -}}
-27
View File
@@ -1,27 +0,0 @@
style: github
template: CHANGELOG.tpl.md
info:
title: CHANGELOG
repository_url: https://git.autonomic.zone:2222/coop-cloud/go-abra
options:
commits:
# filters:
# Type:
# - feat
# - fix
# - perf
# - refactor
commit_groups:
# title_maps:
# feat: Features
# fix: Bug Fixes
# perf: Performance Improvements
# refactor: Code Refactoring
header:
pattern: "^(\\w*)\\:\\s(.*)$"
pattern_maps:
- Type
- Subject
notes:
keywords:
- BREAKING CHANGE
+7
View File
@@ -0,0 +1,7 @@
*.swo
*.swp
.dockerignore
Dockerfile
abra
dist
tags
+104 -52
View File
@@ -3,72 +3,124 @@ kind: pipeline
name: coopcloud.tech/abra
steps:
- name: make check
image: golang:1.17
image: golang:1.26
commands:
- make check
- name: make static
image: golang:1.17
ignore: true # until we decide we all want this check
- name: xgettext-go
image: golang:1.26
environment:
STATIC_CHECK_URL: honnef.co/go/tools/cmd/staticcheck
STATIC_CHECK_VERSION: v0.2.0
GOPRIVATE: coopcloud.tech
commands:
- go install $STATIC_CHECK_URL@$STATIC_CHECK_VERSION
- make static
- go run git.coopcloud.tech/toolshed/xgettext-go@latest
-o pkg/i18n/locales/abra.pot
--keyword=i18n.G
--keyword-ctx=i18n.GC
--sort-output
--add-comments-tag="translators"
$(find . -name "*.go" -not -path "*vendor*" | sort)
depends_on:
- make check
when:
event:
exclude:
- tag
- name: make build
image: golang:1.17
- name: xgettext-go status
image: golang:1.26-alpine3.22
commands:
- make build
- apk add patchutils git make
- cd /drone/src
- sed -i "s/charset=CHARSET/charset=UTF-8/g" pkg/i18n/locales/*.pot
- git diff pkg/i18n/locales/abra.pot | grepdiff --output-matching=hunk POT-Creation-Date | git apply --reverse --allow-empty
- git diff
- git diff-files --exit-code
depends_on:
- xgettext-go
when:
event:
exclude:
- tag
- name: make test
image: golang:1.17
commands:
- make test
- name: notify on failure
image: plugins/matrix
settings:
homeserver: https://matrix.autonomic.zone
roomid: "IFazIpLtxiScqbHqoa:autonomic.zone"
userid: "@autono-bot:autonomic.zone"
accesstoken:
from_secret: autono_bot_access_token
depends_on:
- make check
- make build
- make test
when:
status:
- failure
- name: fetch
image: docker:git
commands:
- git fetch --tags
depends_on:
- make check
- make build
- make test
when:
event: tag
- name: release
image: golang:1.17
image: golang:1.26
environment:
GITEA_TOKEN:
from_secret: goreleaser_gitea_token
volumes:
- name: deps
path: /go
ABRA_DIR: /root/.abra_test
commands:
- curl -sL https://git.io/goreleaser | bash
- make test
depends_on:
- fetch
- make check
- name: publish image
image: plugins/docker
settings:
auto_tag: true
username: abra-bot
password:
from_secret: git_coopcloud_tech_token_abra_bot
repo: git.coopcloud.tech/toolshed/abra
tags: dev
registry: git.coopcloud.tech
when:
event: tag
branch:
- main
depends_on:
- make check
- make test
- name: on-demand integration test
image: appleboy/drone-ssh
settings:
host:
- int.coopcloud.tech
username: abra
key:
from_secret: abra_int_private_key
port: 22
command_timeout: 60m
script_stop: true
request_pty: true
script:
- |
wget https://git.coopcloud.tech/toolshed/abra/raw/branch/main/scripts/tests/run-ci-int -O run-ci-int
chmod +x run-ci-int
sh run-ci-int
when:
ref:
- refs/heads/int-*
depends_on:
- make check
- make test
- name: nightly integration test
image: appleboy/drone-ssh
settings:
host:
- int.coopcloud.tech
username: abra
key:
from_secret: abra_int_private_key
port: 22
command_timeout: 60m
script_stop: true
request_pty: true
script:
- |
wget https://git.coopcloud.tech/toolshed/abra/raw/branch/main/scripts/tests/run-ci-int -O run-ci-int
chmod +x run-ci-int
sh run-ci-int
when:
event:
- cron:
cron:
# @daily https://docs.drone.io/cron/
- integration
volumes:
- name: deps
temp: {}
trigger:
action:
exclude:
- synchronized
+6 -5
View File
@@ -1,6 +1,7 @@
go env -w GOPRIVATE=coopcloud.tech
# integration test suite
# export ABRA_DIR="$HOME/.abra_test"
# export TEST_SERVER=test.example.com
# export ABRA_CI=1
# export PASSWORD_STORE_DIR=$(pwd)/../../autonomic/passwords/passwords/
# export HCLOUD_TOKEN=$(pass show logins/hetzner/cicd/api_key)
# export CAPSUL_TOKEN=...
# export GITEA_TOKEN=...
# release automation
# export GITEA_TOKEN=
-8
View File
@@ -1,8 +0,0 @@
---
name: "Do not use this issue tracker"
about: "Do not use this issue tracker"
title: "Do not use this issue tracker"
labels: []
---
Please report your issue on [`coop-cloud/organising`](https://git.coopcloud.tech/coop-cloud/organising)
+7 -3
View File
@@ -1,5 +1,9 @@
abra
.vscode/
vendor/
*.tar.gz
*fmtcoverage.html
.e2e.env
.envrc
.vscode/
/abra
/bin
dist/
tests/integration/.bats
+33 -12
View File
@@ -1,35 +1,56 @@
---
project_name: abra
version: 2
gitea_urls:
api: https://git.coopcloud.tech/api/v1
download: https://git.coopcloud.tech/
skip_tls_verify: false
before:
hooks:
- go mod tidy
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
- id: abra
binary: abra
dir: cmd/abra
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- 386
- amd64
- arm
- arm64
goarm:
- 5
- 6
- 7
flags:
- -v
- -trimpath
ldflags:
- "-X 'main.Commit={{ .Commit }}'"
- "-X 'main.Version={{ .Version }}'"
archives:
- replacements:
linux: Linux
386: i386
amd64: x86_64
format: binary
- "-s"
- "-w"
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
sort: desc
filters:
exclude:
- "^docs:"
- "^Merge"
- "^Revert"
- "^WIP:"
- "^chore(deps):"
- "^style:"
- "^test:"
- "^tests:"
+28
View File
@@ -0,0 +1,28 @@
# authors
> If you're looking at this and you hack on `abra` and you're not listed here,
> please do add yourself! This is a community project, let's show some 💞
- 3wordchant
- ammaratef45
- apfelwurm
- basebuilder
- cassowary
- chasqui
- codegod100
- cyrnel
- decentral1se
- fauno
- frando
- iexos
- jade
- kawaiipunk
- knoflook
- mayel
- moritz
- namnatulco
- p4u1
- rix
- roxxers
- vera
- yksflip
+29
View File
@@ -0,0 +1,29 @@
# Build image
FROM golang:1.26-alpine AS build
ENV GOPRIVATE=coopcloud.tech
RUN apk add --no-cache \
gcc \
git \
make \
musl-dev
COPY . /app
WORKDIR /app
RUN CGO_ENABLED=0 make build
FROM alpine:3.24
RUN apk add --no-cache \
ca-certificates \
git \
openssh
RUN update-ca-certificates
COPY --from=build /app/abra /abra
ENTRYPOINT ["/abra"]
+15
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/>.
+65 -17
View File
@@ -1,38 +1,86 @@
ABRA := ./cmd/abra
COMMIT := $(shell git rev-list -1 HEAD)
GOPATH := $(shell go env GOPATH)
LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
ABRA := ./cmd/abra
XGETTEXT := go run git.coopcloud.tech/toolshed/xgettext-go@latest
COMMIT := $(shell git rev-list -1 HEAD)
GOPATH := $(shell go env GOPATH)
GOVERSION := 1.26
LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
DIST_LDFLAGS := $(LDFLAGS)" -s -w"
BFLAGS := -v -trimpath
GCFLAGS := "all=-l -B"
DOMAIN := abra
POFILES := $(wildcard pkg/i18n/locales/*.po)
MOFILES := $(patsubst %.po,%.mo,$(POFILES))
LINGUAS := $(basename $(POFILES))
export GOPRIVATE=coopcloud.tech
all: run test install build clean format check static
all: format check build
run:
@go run -ldflags=$(LDFLAGS) $(ABRA)
@go run -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(ABRA)
install:
@go install -ldflags=$(LDFLAGS) $(ABRA)
build-dev:
@go build -ldflags=$(LDFLAGS) $(ABRA)
@go install -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(ABRA)
build:
@go build -ldflags=$(DIST_LDFLAGS) $(ABRA)
@go build $(BFLAGS) -gcflags=$(GCFLAGS) -ldflags=$(DIST_LDFLAGS) $(ABRA)
build-docker:
@docker run -it -v $(PWD):/abra golang:$(GOVERSION) \
bash -c 'cd /abra; ./scripts/docker/build.sh'
clean:
@rm '$(GOPATH)/bin/abra'
format:
@gofmt -s -w .
@gofmt -s -w $$(find . -type f -name '*.go' | grep -v "/vendor/")
check:
@test -z $$(gofmt -l .) || (echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1)
static:
@staticcheck $(ABRA)
@test -z $$(gofmt -l $$(find . -type f -name '*.go' | grep -v "/vendor/")) || \
(echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1)
test:
@go test ./... -cover
@go test ./... -cover -v -p 1
find-tests:
@find . -name "*_test.go"
loc:
@find . -name "*.go" | xargs wc -l
deps:
@go get -t -u ./...
.PHONY: i18n
i18n: update-pot update-pot-po-metadata update-po build-mo
.PHONY: update-po
update-po:
@set -eu; \
for lang in $(LINGUAS); do \
msgmerge --backup=none -U $$lang.po pkg/i18n/locales/$(DOMAIN).pot; \
done
.PHONY: update-pot
update-pot:
@$(XGETTEXT) \
-o pkg/i18n/locales/$(DOMAIN).pot \
--keyword=i18n.G \
--keyword-ctx=i18n.GC \
--sort-output \
--add-comments-tag="translators" \
$$(find . -name "*.go" -not -path "*vendor*" | sort)
.PHONY: update-pot-po-metadata
update-pot-po-metadata:
@sed -i "s/charset=CHARSET/charset=UTF-8/g" pkg/i18n/locales/*.po pkg/i18n/locales/*.pot
.PHONY: build-mo
build-mo:
@set -eu; \
for lang in $(POFILES); do \
msgfmt $$lang -o $$(echo $$lang | sed 's/.po/.mo/g') --statistics; \
done
release:
@goreleaser release --clean
+8 -97
View File
@@ -1,103 +1,14 @@
# 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)
[![Go Report Card](https://goreportcard.com/badge/git.coopcloud.tech/coop-cloud/abra)](https://goreportcard.com/report/git.coopcloud.tech/coop-cloud/abra)
[![Build Status](https://build.coopcloud.tech/api/badges/toolshed/abra/status.svg?ref=refs/heads/main)](https://build.coopcloud.tech/toolshed/abra)
[![Go Report Card](https://goreportcard.com/badge/git.coopcloud.tech/toolshed/abra)](https://goreportcard.com/report/git.coopcloud.tech/toolshed/abra)
[![Go Reference](https://pkg.go.dev/badge/coopcloud.tech/abra.svg)](https://pkg.go.dev/coopcloud.tech/abra)
[![Translation status](https://translate.coopcloud.tech/widget/co-op-cloud/svg-badge.svg)](https://translate.coopcloud.tech/engage/co-op-cloud/)
The Co-op Cloud utility belt 🎩🐇
`abra` is a command-line tool for managing your own [Co-op Cloud](https://coopcloud.tech). It can provision new servers, create applications, deploy them, run backup and restore operations and a whole lot of other things. It is the go-to tool for day-to-day operations when managing a Co-op Cloud instance.
<a href="https://github.com/egonelbre/gophers"><img align="right" width="150" src="https://github.com/egonelbre/gophers/raw/master/.thumb/sketch/adventure/poking-fire.png"/></a>
## Install
`abra` is the flagship client & command-line tool for Co-op Cloud. It has been developed specifically for the purpose of making the day-to-day operations of [operators](https://docs.coopcloud.tech/operators/) and [maintainers](https://docs.coopcloud.tech/maintainers/) pleasant & convenient. It is libre software, written in [Go](https://go.dev) and maintained and extended by the community 💖
### Arch-based Linux Distros
[abra (coming-soon)](https://aur.archlinux.org/packages/abra/) or for the latest version on git [abra-git](https://aur.archlinux.org/packages/abra-git/)
```sh
yay -S abra-git # or abra
```
### Debian-based Linux Distros
**Coming Soon**
### Homebrew
**Coming Soon**
### Build from source
```sh
git clone https://git.coopcloud.tech/coop-cloud/abra
cd abra
go env -w GOPRIVATE=coopcloud.tech
make install
```
The abra binary will be in `$GOPATH/bin`.
## Autocompletion
**bash**
Copy `scripts/autocomplete/bash` into `/etc/bash_completion.d/` and rename
it to abra.
```
sudo cp scripts/autocomplete/bash /etc/bash_completion.d/abra
source /etc/bash_completion.d/abra
```
**(fi)zsh**
(fi)zsh doesn't have an autocompletion folder by default but you can create one, then copy `scripts/autocomplete/zsh` into it and add a couple lines to your `~/.zshrc` or `~/.fizsh/.fizshrc`
```
sudo mkdir /etc/zsh/completion.d/
sudo cp scripts/autocomplete/zsh /etc/zsh/completion.d/abra
echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc
```
(replace .zshrc with ~/.fizsh/.fizshrc if you use fizsh)
## Hacking
Install direnv, run `cp .envrc.sample .envrc`, then run `direnv allow` in this directory. This will set coopcloud repos as private due to [this bug.](https://git.coopcloud.tech/coop-cloud/coopcloud.tech/issues/20#issuecomment-8201). Or you can run `go env -w GOPRIVATE=coopcloud.tech` but I'm not sure how persistent this is.
Install [Go >= 1.16](https://golang.org/doc/install) and then:
- `make build` to build
- `./abra` to run commands
- `make test` will run tests
Our [Drone CI configuration](.drone.yml) runs a number of sanity on each pushed commit. See the [Makefile](./Makefile) for more handy targets.
Please use the [conventional commit format](https://www.conventionalcommits.org/en/v1.0.0/) for your commits so we can automate our change log.
## Versioning
We use [goreleaser](https://goreleaser.com) to help us automate releases. We use [semver](https://semver.org) for versioning all releases of the tool. While we are still in the public alpha release phase, we will maintain a `0.y.z-alpha` format. Change logs are generated from our commit logs. We are still working this out and aim to refine our release praxis as we go.
For developers, while using this `-alpha` format, the `y` part is the "major" version part. So, if you make breaking changes, you increment that and _not_ the `x` part. So, if you're on `0.1.0-alpha`, then you'd go to `0.1.1-alpha` for a backwards compatible change and `0.2.0-alpha` for a backwards incompatible change.
## Making a new release
- Change `ABRA_VERSION` to match the new tag in [`scripts`](./scripts/installer/installer) (use [semver](https://semver.org))
- Commit that change (e.g. `git commit -m 'chore: publish next tag 0.3.1-alpha'`)
- Make a new tag (e.g. `git tag 0.y.z-alpha`)
- Push the new tag (e.g. `git push && git push --tags`)
- Wait until the build finishes on [build.coopcloud.tech](https://build.coopcloud.tech/coop-cloud/abra)
- Check the release worked, (e.g. `abra upgrade; abra version`)
## Fork maintenance
We maintain a fork of [godotenv](https://github.com/Autonomic-Cooperative/godotenv) for two features:
1. multi-line env var support
2. inline comment parsing
You can upgrade the version here by running `go get github.com/Autonomic-Cooperative/godotenv@<commit>` where `<commit>` is the
latest commit you want to pin to. We are aiming to migrate to YAML format for the environment configuration, so this should only
be a temporary thing.
Please see [docs.coopcloud.tech/abra](https://docs.coopcloud.tech/abra) for help on install, upgrade, hacking, troubleshooting & more!
+14 -31
View File
@@ -1,37 +1,20 @@
package app
import (
"github.com/urfave/cli/v2"
"strings"
"coopcloud.tech/abra/pkg/i18n"
"github.com/spf13/cobra"
)
// AppCommand defines the `abra app` command and ets subcommands
var AppCommand = &cli.Command{
Name: "app",
Usage: "Manage apps",
Aliases: []string{"a"},
ArgsUsage: "<app>",
Description: `
This command provides all the functionality you need to manage the life cycle
of your apps. From initial deployment, day-2 operations (e.g. backup/restore)
to scaling apps up and spinning them down.
`,
Subcommands: []*cli.Command{
appNewCommand,
appConfigCommand,
appDeployCommand,
appUndeployCommand,
appBackupCommand,
appRestoreCommand,
appRemoveCommand,
appCheckCommand,
appListCommand,
appPsCommand,
appLogsCommand,
appCpCommand,
appRunCommand,
appRollbackCommand,
appSecretCommand,
appVolumeCommand,
appVersionCommand,
},
// translators: `abra app` aliases. use a comma separated list of aliases with
// no spaces in between
var appAliases = i18n.GC("a", "abra app")
var AppCommand = &cobra.Command{
// translators: `app` command group
Use: i18n.G("app [cmd] [args] [flags]"),
Aliases: strings.Split(appAliases, ","),
// translators: Short description for `app` command group
Short: i18n.G("Manage apps"),
}
+322 -70
View File
@@ -1,87 +1,339 @@
package app
import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra"
)
var backupAllServices bool
var backupAllServicesFlag = &cli.BoolFlag{
Name: "all",
Value: false,
Destination: &backupAllServices,
Aliases: []string{"a"},
Usage: "Backup all services",
}
// translators: `abra app backup list` aliases. use a comma separated list of aliases with
// no spaces in between
var appBackupListAliases = i18n.G("ls")
var appBackupCommand = &cli.Command{
Name: "backup",
Usage: "Backup an app",
Aliases: []string{"b"},
Flags: []cli.Flag{backupAllServicesFlag},
ArgsUsage: "<service>",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if c.Args().Get(1) != "" && backupAllServices {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<service>' and '--all' together"))
}
abraSh := path.Join(config.ABRA_DIR, "apps", app.Type, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("'%s' does not exist?", abraSh)
}
logrus.Fatal(err)
}
sourceCmd := fmt.Sprintf("source %s", abraSh)
execCmd := "abra_backup"
if !backupAllServices {
serviceName := c.Args().Get(1)
if serviceName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no service(s) target provided"))
}
execCmd = fmt.Sprintf("abra_backup_%s", serviceName)
}
bytes, err := ioutil.ReadFile(abraSh)
if err != nil {
logrus.Fatal(err)
}
if !strings.Contains(string(bytes), execCmd) {
logrus.Fatalf("%s doesn't have a '%s' function", app.Type, execCmd)
}
sourceAndExec := fmt.Sprintf("%s; %s", sourceCmd, execCmd)
cmd := exec.Command("bash", "-c", sourceAndExec)
if err := internal.RunCmd(cmd); err != nil {
logrus.Fatal(err)
}
return nil
var AppBackupListCommand = &cobra.Command{
// translators: `app backup list` command
Use: i18n.G("list <domain> [flags]"),
Aliases: strings.Split(appBackupListAliases, ","),
// translators: Short description for `app backup list` command
Short: i18n.G("List the contents of a snapshot"),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
cl, err := client.New(app.Server)
if err != nil {
logrus.Warn(err)
log.Fatal(err)
}
if c.NArg() > 0 {
return
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
}
for _, a := range appNames {
fmt.Println(a)
execEnv := []string{
fmt.Sprintf("SERVICE=%s", app.Domain),
"MACHINE_LOGS=true",
}
if snapshot != "" {
log.Debug(i18n.G("including SNAPSHOT=%s in backupbot exec invocation", snapshot))
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
}
if showAllPaths {
log.Debug(i18n.G("including SHOW_ALL=%v in backupbot exec invocation", showAllPaths))
execEnv = append(execEnv, fmt.Sprintf("SHOW_ALL=%v", showAllPaths))
}
if timestamps {
log.Debug(i18n.G("including TIMESTAMPS=%v in backupbot exec invocation", timestamps))
execEnv = append(execEnv, fmt.Sprintf("TIMESTAMPS=%v", timestamps))
}
if _, err = internal.RunBackupCmdRemote(cl, "ls", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
}
},
}
// translators: `abra app backup download` aliases. use a comma separated list of aliases with
// no spaces in between
var appBackupDownloadAliases = i18n.G("d")
var AppBackupDownloadCommand = &cobra.Command{
// translators: `app backup download` command
Use: i18n.G("download <domain> [flags]"),
Aliases: strings.Split(appBackupDownloadAliases, ","),
// translators: Short description for `app backup download` command
Short: i18n.G("Download a snapshot"),
Long: i18n.G(`Downloads a backup.tar.gz to the current working directory.
"--volumes/-v" includes data contained in volumes alongide paths specified in
"backupbot.backup.path" labels.`),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
}
execEnv := []string{
fmt.Sprintf("SERVICE=%s", app.Domain),
"MACHINE_LOGS=true",
}
if snapshot != "" {
log.Debug(i18n.G("including SNAPSHOT=%s in backupbot exec invocation", snapshot))
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
}
if includePath != "" {
log.Debug(i18n.G("including INCLUDE_PATH=%s in backupbot exec invocation", includePath))
execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath))
}
if includeSecrets {
log.Debug(i18n.G("including SECRETS=%v in backupbot exec invocation", includeSecrets))
execEnv = append(execEnv, fmt.Sprintf("SECRETS=%v", includeSecrets))
}
if includeVolumes {
log.Debug(i18n.G("including VOLUMES=%v in backupbot exec invocation", includeVolumes))
execEnv = append(execEnv, fmt.Sprintf("VOLUMES=%v", includeVolumes))
}
if _, err := internal.RunBackupCmdRemote(cl, "download", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
}
remoteBackupDir := "/tmp/backup.tar.gz"
currentWorkingDir := "."
if err = CopyFromContainer(cl, targetContainer.ID, remoteBackupDir, currentWorkingDir); err != nil {
log.Fatal(err)
}
},
}
// translators: `abra app backup create` aliases. use a comma separated list of aliases with
// no spaces in between
var appBackupCreateAliases = i18n.G("c")
var AppBackupCreateCommand = &cobra.Command{
// translators: `app backup create` command
Use: i18n.G("create <domain> [flags]"),
Aliases: strings.Split(appBackupCreateAliases, ","),
// translators: Short description for `app backup create` command
Short: i18n.G("Create a new snapshot"),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
}
execEnv := []string{
fmt.Sprintf("SERVICE=%s", app.Domain),
"MACHINE_LOGS=true",
}
if retries != "" {
log.Debug(i18n.G("including RETRIES=%s in backupbot exec invocation", retries))
execEnv = append(execEnv, fmt.Sprintf("RETRIES=%s", retries))
}
if _, err := internal.RunBackupCmdRemote(cl, "create", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
}
},
}
// translators: `abra app backup snapshots` aliases. use a comma separated list of aliases with
// no spaces in between
var appBackupSnapshotsAliases = i18n.G("s")
var AppBackupSnapshotsCommand = &cobra.Command{
// translators: `app backup snapshots` command
Use: i18n.G("snapshots <domain> [flags]"),
Aliases: strings.Split(appBackupSnapshotsAliases, ","),
// translators: Short description for `app backup snapshots` command
Short: i18n.G("List all snapshots"),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
}
execEnv := []string{
fmt.Sprintf("SERVICE=%s", app.Domain),
"MACHINE_LOGS=true",
}
if _, err = internal.RunBackupCmdRemote(cl, "snapshots", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
}
},
}
// translators: `abra app backup` aliases. use a comma separated list of aliases with
// no spaces in between
var appBackupAliases = i18n.G("b")
var AppBackupCommand = &cobra.Command{
// translators: `app backup` command group
Use: i18n.G("backup [cmd] [args] [flags]"),
Aliases: strings.Split(appBackupAliases, ","),
// translators: Short description for `app backup` command group
Short: i18n.G("Manage app backups"),
}
var (
snapshot string
retries string
includePath string
showAllPaths bool
timestamps bool
includeSecrets bool
includeVolumes bool
)
func init() {
AppBackupListCommand.Flags().StringVarP(
&snapshot,
i18n.G("snapshot"),
i18n.G("s"),
"",
i18n.G("list specific snapshot"),
)
AppBackupListCommand.Flags().BoolVarP(
&showAllPaths,
i18n.G("all"),
i18n.GC("a", "app backup list"),
false,
i18n.G("show all paths"),
)
AppBackupListCommand.Flags().BoolVarP(
&timestamps,
i18n.G("timestamps"),
i18n.G("t"),
false,
i18n.G("include timestamps"),
)
AppBackupDownloadCommand.Flags().StringVarP(
&snapshot,
i18n.G("snapshot"),
i18n.G("s"),
"",
i18n.G("list specific snapshot"),
)
AppBackupDownloadCommand.Flags().StringVarP(
&includePath,
i18n.G("path"),
i18n.G("p"),
"",
i18n.G("volumes path"),
)
AppBackupDownloadCommand.Flags().BoolVarP(
&includeSecrets,
i18n.G("secrets"),
i18n.G("S"),
false,
i18n.G("include secrets"),
)
AppBackupDownloadCommand.Flags().BoolVarP(
&includeVolumes,
i18n.G("volumes"),
i18n.G("v"),
false,
i18n.G("include volumes"),
)
AppBackupDownloadCommand.Flags().BoolVarP(
&internal.Chaos,
i18n.G("chaos"),
i18n.G("C"),
false,
i18n.G("ignore uncommitted recipes changes"),
)
AppBackupCreateCommand.Flags().StringVarP(
&retries,
i18n.G("retries"),
i18n.G("r"),
"1",
i18n.G("number of retry attempts"),
)
AppBackupCreateCommand.Flags().BoolVarP(
&internal.Chaos,
i18n.G("chaos"),
i18n.G("C"),
false,
i18n.G("ignore uncommitted recipes changes"),
)
}
+80 -45
View File
@@ -2,63 +2,98 @@ package app
import (
"fmt"
"os"
"path"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
)
var appCheckCommand = &cli.Command{
Name: "check",
Usage: "Check if app is configured correctly",
Aliases: []string{"c"},
ArgsUsage: "<service>",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
// translators: `abra app check` aliases. use a comma separated list of aliases with
// no spaces in between
var appCheckAliases = i18n.G("chk")
envSamplePath := path.Join(config.ABRA_DIR, "apps", app.Type, ".env.sample")
if _, err := os.Stat(envSamplePath); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("'%s' does not exist?", envSamplePath)
}
logrus.Fatal(err)
}
var AppCheckCommand = &cobra.Command{
// translators: `app check` command
Use: i18n.G("check <domain> [flags]"),
Aliases: strings.Split(appCheckAliases, ","),
// translators: Short description for `app check` command
Short: i18n.G("Ensure an app is well configured"),
Long: i18n.G(`Compare env vars in both the app ".env" and recipe ".env.sample" file.
envSample, err := config.ReadEnv(envSamplePath)
if err != nil {
logrus.Fatal(err)
}
The goal is to ensure that recipe ".env.sample" env vars are defined in your
app ".env" file. Only env var definitions in the ".env.sample" which are
uncommented, e.g. "FOO=bar" are checked. If an app ".env" file does not include
these env vars, then "check" will complain.
var missing []string
for k := range envSample {
if _, ok := app.Env[k]; !ok {
missing = append(missing, k)
}
}
if len(missing) > 0 {
missingEnvVars := strings.Join(missing, ", ")
logrus.Fatalf("%s is missing %s", app.Path, missingEnvVars)
}
logrus.Infof("all necessary environment variables defined for '%s'", app.Name)
return nil
Recipe maintainers may or may not provide defaults for env vars within their
recipes regardless of commenting or not (e.g. through the use of
${FOO:<default>} syntax). "check" does not confirm or deny this for you.`),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
table, err := formatter.CreateTable()
if err != nil {
logrus.Warn(err)
log.Fatal(err)
}
if c.NArg() > 0 {
return
table.
Headers(
fmt.Sprintf("%s .env.sample", app.Recipe.Name),
fmt.Sprintf("%s.env", app.Name),
).
StyleFunc(func(row, col int) lipgloss.Style {
switch {
case col == 1:
return lipgloss.NewStyle().Padding(0, 1, 0, 1).Align(lipgloss.Center)
default:
return lipgloss.NewStyle().Padding(0, 1, 0, 1)
}
})
envVars, err := appPkg.CheckEnv(app)
if err != nil {
log.Fatal(err)
}
for _, a := range appNames {
fmt.Println(a)
for _, envVar := range envVars {
if envVar.Present {
val := []string{envVar.Name, "✅"}
table.Row(val...)
} else {
val := []string{envVar.Name, "❌"}
table.Row(val...)
}
}
if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
},
}
func init() {
AppCheckCommand.Flags().BoolVarP(
&internal.Chaos,
i18n.G("chaos"),
i18n.G("C"),
false,
i18n.G("ignore uncommitted recipes changes"),
)
}
+289
View File
@@ -0,0 +1,289 @@
package app
import (
"errors"
"fmt"
"os"
"os/exec"
"slices"
"sort"
"strings"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra"
)
// translators: `abra app cmd` aliases. use a comma separated list of aliases with
// no spaces in between
var appCmdAliases = i18n.G("cmd")
var AppCmdCommand = &cobra.Command{
// translators: `app command` command
Use: i18n.G("command <domain> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]"),
Aliases: strings.Split(appCmdAliases, ","),
// translators: Short description for `app cmd` command
Short: i18n.G("Run app commands"),
Long: i18n.G(`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/-l".
N.B. If using the "--" style to pass arguments, flags (e.g. "--local/-l") must
be passed *before* the "--". It is possible to pass arguments without the "--"
as long as no dashes are present (i.e. "foo" works without "--", "-foo"
does not).`),
Example: i18n.G(` # pass <cmd> args/flags without "--"
abra app cmd 1312.net app my_cmd_arg foo --user bar
# pass <cmd> args/flags with "--"
abra app cmd 1312.net app my_cmd_args --user bar -- foo -vvv
# drop the [service] arg if using "--local/-l"
abra app cmd 1312.net my_cmd --local`),
Args: func(cmd *cobra.Command, args []string) error {
if local {
if !(len(args) >= 2) {
return errors.New(i18n.G("requires at least 2 arguments with --local/-l"))
}
if slices.Contains(os.Args, "--") {
if cmd.ArgsLenAtDash() > 2 {
return errors.New(i18n.G("accepts at most 2 args with --local/-l"))
}
}
// NOTE(d1): it is unclear how to correctly validate this case
//
// abra app cmd 1312.net app test_cmd_args foo --local
// FATAL <recipe> doesn't have a app function
//
// "app" should not be there, but there is no reliable way to detect arg
// count when the user can pass an arbitrary amount of recipe command
// arguments
return nil
}
if !(len(args) >= 3) {
return errors.New(i18n.G("requires at least 3 arguments"))
}
return nil
},
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
if !local {
return autocomplete.ServiceNameComplete(args[0])
}
return autocomplete.CommandNameComplete(args[0])
case 2:
if !local {
return autocomplete.CommandNameComplete(args[0])
}
return nil, cobra.ShellCompDirectiveDefault
default:
return nil, cobra.ShellCompDirectiveError
}
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
if local && remoteUser != "" {
log.Fatal(i18n.G("cannot use --local & --user together"))
}
hasCmdArgs, parsedCmdArgs := parseCmdArgs(args, local)
if _, err := os.Stat(app.Recipe.AbraShPath); err != nil {
if os.IsNotExist(err) {
log.Fatal(i18n.G("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name))
}
log.Fatal(err)
}
if local {
cmdName := args[1]
if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
log.Fatal(err)
}
log.Debug(i18n.G("--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 {
log.Debug(i18n.G("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, app.Recipe.AbraShPath, cmdName, parsedCmdArgs)
} else {
log.Debug(i18n.G("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, app.Recipe.AbraShPath, cmdName)
}
shell := "/bin/bash"
if _, err := os.Stat(shell); errors.Is(err, os.ErrNotExist) {
log.Debug(i18n.G("%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 {
log.Fatal(err)
}
return
}
cmdName := args[2]
if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
log.Fatal(err)
}
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
if err != nil {
log.Fatal(err)
}
matchingServiceName := false
targetServiceName := args[1]
for _, serviceName := range serviceNames {
if serviceName == targetServiceName {
matchingServiceName = true
}
}
if !matchingServiceName {
log.Fatal(i18n.G("no service %s for %s?", targetServiceName, app.Name))
}
log.Debug(i18n.G("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName))
if hasCmdArgs {
log.Debug(i18n.G("parsed following command arguments: %s", parsedCmdArgs))
} else {
log.Debug(i18n.G("did not detect any command arguments"))
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
if err := internal.RunCmdRemote(
cl,
app,
disableTTY,
app.Recipe.AbraShPath,
targetServiceName, cmdName, parsedCmdArgs, remoteUser); err != nil {
log.Fatal(err)
}
},
}
// translators: `abra app command list` aliases. use a comma separated list of
// aliases with no spaces in between
var appCmdListAliases = i18n.G("ls")
var AppCmdListCommand = &cobra.Command{
// translators: `app cmd list` command
Use: i18n.G("list <domain> [flags]"),
Aliases: strings.Split(appCmdListAliases, ","),
// translators: Short description for `app cmd list` command
Short: i18n.G("List all available commands"),
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath)
if err != nil {
log.Fatal(err)
}
sort.Strings(cmdNames)
for _, cmdName := range cmdNames {
fmt.Println(cmdName)
}
},
}
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
}
var (
local bool
remoteUser string
disableTTY bool
)
func init() {
AppCmdCommand.Flags().BoolVarP(
&local,
i18n.G("local"),
i18n.G("l"),
false,
i18n.G("run command locally"),
)
AppCmdCommand.Flags().StringVarP(
&remoteUser,
i18n.G("user"),
i18n.G("u"),
"",
i18n.G("request remote user"),
)
AppCmdCommand.Flags().BoolVarP(
&disableTTY,
i18n.G("tty"),
i18n.G("T"),
false,
i18n.G("disable remote TTY"),
)
AppCmdCommand.Flags().BoolVarP(
&internal.Chaos,
i18n.G("chaos"),
i18n.G("C"),
false,
i18n.G("ignore uncommitted recipes changes"),
)
}
+31
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/toolshed/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)
}
}
}
+43 -33
View File
@@ -1,55 +1,65 @@
package app
import (
"fmt"
"os"
"os/exec"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/spf13/cobra"
)
var appConfigCommand = &cli.Command{
Name: "config",
Aliases: []string{"c"},
Usage: "Edit app config",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
// translators: `abra app config` aliases. use a comma separated list of
// aliases with no spaces in between
var appConfigAliases = i18n.G("cfg")
var AppConfigCommand = &cobra.Command{
// translators: `app config` command
Use: i18n.G("config <domain> [flags]"),
Aliases: strings.Split(appConfigAliases, ","),
// translators: Short description for `app config` command
Short: i18n.G("Edit app config"),
Example: i18n.G(" abra config 1312.net"),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
files, err := appPkg.LoadAppFiles("")
if err != nil {
log.Fatal(err)
}
appName := args[0]
appFile, exists := files[appName]
if !exists {
log.Fatal(i18n.G("cannot find app with name %s", appName))
}
ed, ok := os.LookupEnv("EDITOR")
if !ok {
edPrompt := &survey.Select{
Message: "Which editor do you wish to use?",
Message: i18n.G("which editor do you wish to use?"),
Options: []string{"vi", "vim", "nvim", "nano", "pico", "emacs"},
}
if err := survey.AskOne(edPrompt, &ed); err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
}
cmd := exec.Command(ed, app.Path)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
logrus.Fatal(err)
}
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
c := exec.Command(ed, appFile.Path)
c.Stdin = os.Stdin
c.Stdout = os.Stdout
c.Stderr = os.Stderr
if err := c.Run(); err != nil {
log.Fatal(err)
}
},
}
+360 -97
View File
@@ -2,125 +2,388 @@ package app
import (
"context"
"fmt"
"errors"
"io"
"os"
"path"
"path/filepath"
"strings"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command"
containertypes "github.com/docker/docker/api/types/container"
dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/archive"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/spf13/cobra"
)
var appCpCommand = &cli.Command{
Name: "cp",
Aliases: []string{"c"},
ArgsUsage: "<src> <dst>",
Usage: "Copy files to/from a running app service",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
// translators: `abra app cp` aliases. use a comma separated list of aliases with
// no spaces in between
var appCpAliases = i18n.G("c")
src := c.Args().Get(1)
dst := c.Args().Get(2)
if src == "" {
logrus.Fatal("missing <src> argument")
} else if dst == "" {
logrus.Fatal("missing <dest> argument")
var AppCpCommand = &cobra.Command{
// translators: `app cp` command
Use: i18n.G("cp <domain> <src> <dst> [flags]"),
Aliases: strings.Split(appCpAliases, ","),
// translators: Short description for `app cp` command
Short: i18n.G("Copy files to/from a deployed app service"),
Example: i18n.G(` # copy myfile.txt to the root of the app service
abra app cp 1312.net myfile.txt app:/
# copy that file back to your current working directory locally
abra app cp 1312.net app:/myfile.txt ./`),
Args: cobra.ExactArgs(3),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
default:
return nil, cobra.ShellCompDirectiveDefault
}
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
parsedSrc := strings.SplitN(src, ":", 2)
parsedDst := strings.SplitN(dst, ":", 2)
errorMsg := "one of <src>/<dest> arguments must take $SERVICE:$PATH form"
if len(parsedSrc) == 2 && len(parsedDst) == 2 {
logrus.Fatal(errorMsg)
} else if len(parsedSrc) != 2 {
if len(parsedDst) != 2 {
logrus.Fatal(errorMsg)
}
} else if len(parsedDst) != 2 {
if len(parsedSrc) != 2 {
logrus.Fatal(errorMsg)
}
}
var service string
var srcPath string
var dstPath string
isToContainer := false // <container:src> <dst>
if len(parsedSrc) == 2 {
service = parsedSrc[0]
srcPath = parsedSrc[1]
dstPath = dst
logrus.Debugf("assuming transfer is coming FROM the container")
} else if len(parsedDst) == 2 {
service = parsedDst[0]
dstPath = parsedDst[1]
srcPath = src
isToContainer = true // <src> <container:dst>
logrus.Debugf("assuming transfer is going TO the container")
}
appFiles, err := config.LoadAppFiles("")
src := args[1]
dst := args[2]
srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
appEnv, err := config.GetApp(appFiles, app.Name)
if err != nil {
logrus.Fatal(err)
}
ctx := context.Background()
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("%s_%s", appEnv.StackName(), service))
containers, err := cl.ContainerList(ctx, types.ContainerListOptions{Filters: filters})
container, err := containerPkg.GetContainerFromStackAndService(cl, app.StackName(), service)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
log.Debug(i18n.G("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server))
if len(containers) != 1 {
logrus.Fatalf("expected 1 container but got %v", len(containers))
}
container := containers[0]
logrus.Debugf("retrieved '%s' as target container on '%s'", formatter.ShortenID(container.ID), app.Server)
if isToContainer {
if _, err := os.Stat(srcPath); err != nil {
logrus.Fatalf("'%s' does not exist?", srcPath)
}
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
content, err := archive.TarWithOptions(srcPath, toTarOpts)
if err != nil {
logrus.Fatal(err)
}
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(ctx, container.ID, dstPath, content, copyOpts); err != nil {
logrus.Fatal(err)
}
if toContainer {
err = CopyToContainer(cl, container.ID, srcPath, dstPath)
} else {
content, _, err := cl.CopyFromContainer(ctx, container.ID, srcPath)
if err != nil {
logrus.Fatal(err)
}
defer content.Close()
fromTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
if err := archive.Untar(content, dstPath, fromTarOpts); err != nil {
logrus.Fatal(err)
}
err = CopyFromContainer(cl, container.ID, srcPath, dstPath)
}
if err != nil {
log.Fatal(err)
}
return nil
},
}
var errServiceMissing = errors.New(i18n.G("one of <src>/<dest> arguments must take $SERVICE:$PATH form"))
// parseSrcAndDst parses src and dest string. One of src or dst must be of the form $SERVICE:$PATH
func parseSrcAndDst(src, dst string) (srcPath string, dstPath string, service string, toContainer bool, err error) {
parsedSrc := strings.SplitN(src, ":", 2)
parsedDst := strings.SplitN(dst, ":", 2)
if len(parsedSrc)+len(parsedDst) != 3 {
return "", "", "", false, errServiceMissing
}
if len(parsedSrc) == 2 {
return parsedSrc[1], dst, parsedSrc[0], false, nil
}
if len(parsedDst) == 2 {
return src, parsedDst[1], parsedDst[0], true, nil
}
return "", "", "", false, errServiceMissing
}
// CopyToContainer copies a file or directory from the local file system to the container.
// See the possible copy modes and their documentation.
func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath string) error {
srcStat, err := os.Stat(srcPath)
if err != nil {
return errors.New(i18n.G("local %s ", err))
}
dstStat, err := cl.ContainerStatPath(context.Background(), containerID, dstPath)
dstExists := true
if err != nil {
if errdefs.IsNotFound(err) {
dstExists = false
} else {
return errors.New(i18n.G("remote path: %s", err))
}
}
mode, err := copyMode(srcPath, dstPath, srcStat.Mode(), dstStat.Mode, dstExists)
if err != nil {
return err
}
movePath := ""
switch mode {
case CopyModeDirToDir:
// Add the src directory to the destination path
_, srcDir := path.Split(srcPath)
dstPath = path.Join(dstPath, srcDir)
// Make sure the dst directory exits.
dcli, err := command.NewDockerCli()
if err != nil {
return err
}
if _, err := container.RunExec(dcli, cl, containerID, &containertypes.ExecOptions{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: []string{"mkdir", "-p", dstPath},
Detach: false,
Tty: true,
}); err != nil {
return errors.New(i18n.G("create remote directory: %s", err))
}
case CopyModeFileToFile:
// Remove the file component from the path, since docker can only copy
// to a directory.
dstPath, _ = path.Split(dstPath)
case CopyModeFileToFileRename:
// Copy the file to the temp directory and move it to its dstPath
// afterwards.
movePath = dstPath
dstPath = "/tmp"
}
toTarOpts := &archive.TarOptions{IncludeSourceDir: true, NoOverwriteDirNonDir: true, Compression: archive.Gzip}
content, err := archive.TarWithOptions(srcPath, toTarOpts)
if err != nil {
return err
}
log.Debug(i18n.G("copy %s from local to %s on container", srcPath, dstPath))
copyOpts := containertypes.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(context.Background(), containerID, dstPath, content, copyOpts); err != nil {
return err
}
if movePath != "" {
_, srcFile := path.Split(srcPath)
dcli, err := command.NewDockerCli()
if err != nil {
return err
}
if _, err := container.RunExec(dcli, cl, containerID, &containertypes.ExecOptions{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: []string{"mv", path.Join("/tmp", srcFile), movePath},
Detach: false,
Tty: true,
}); err != nil {
return errors.New(i18n.G("create remote directory: %s", err))
}
}
return nil
}
// CopyFromContainer copies a file or directory from the given container to the local file system.
// See the possible copy modes and their documentation.
func CopyFromContainer(cl *dockerClient.Client, containerID, srcPath, dstPath string) error {
srcStat, err := cl.ContainerStatPath(context.Background(), containerID, srcPath)
if err != nil {
if errdefs.IsNotFound(err) {
return errors.New(i18n.G("remote: %s does not exist", srcPath))
} else {
return errors.New(i18n.G("remote path: %s", err))
}
}
dstStat, err := os.Stat(dstPath)
dstExists := true
var dstMode os.FileMode
if err != nil {
if os.IsNotExist(err) {
dstExists = false
} else {
return errors.New(i18n.G("remote path: %s", err))
}
} else {
dstMode = dstStat.Mode()
}
mode, err := copyMode(srcPath, dstPath, srcStat.Mode, dstMode, dstExists)
if err != nil {
return err
}
moveDstDir := ""
moveDstFile := ""
switch mode {
case CopyModeFileToFile:
// Remove the file component from the path, since docker can only copy
// to a directory.
dstPath, _ = path.Split(dstPath)
case CopyModeFileToFileRename:
// Copy the file to the temp directory and move it to its dstPath
// afterwards.
moveDstFile = dstPath
dstPath = "/tmp"
case CopyModeFilesToDir:
// Copy the directory to the temp directory and move it to its
// dstPath afterwards.
moveDstDir = path.Join(dstPath, "/")
dstPath = "/tmp"
// Make sure the temp directory always gets removed
defer os.Remove(path.Join("/tmp"))
}
content, _, err := cl.CopyFromContainer(context.Background(), containerID, srcPath)
if err != nil {
return errors.New(i18n.G("copy: %s", err))
}
defer content.Close()
if err := archive.Untar(content, dstPath, &archive.TarOptions{
NoOverwriteDirNonDir: true,
Compression: archive.Gzip,
NoLchown: true,
}); err != nil {
return errors.New(i18n.G("untar: %s", err))
}
if moveDstFile != "" {
_, srcFile := path.Split(strings.TrimSuffix(srcPath, "/"))
if err := moveFile(path.Join("/tmp", srcFile), moveDstFile); err != nil {
return err
}
}
if moveDstDir != "" {
_, srcDir := path.Split(strings.TrimSuffix(srcPath, "/"))
if err := moveDir(path.Join("/tmp", srcDir), moveDstDir); err != nil {
return err
}
}
return nil
}
var (
ErrCopyDirToFile = errors.New(i18n.G("can't copy dir to file"))
ErrDstDirNotExist = errors.New(i18n.G("destination directory does not exist"))
)
type CopyMode int
const (
// Copy a src file to a dest file. The src and dest file names are the same.
// <dir_src>/<file> + <dir_dst>/<file> -> <dir_dst>/<file>
CopyModeFileToFile = CopyMode(iota)
// Copy a src file to a dest file. The src and dest file names are not the same.
// <dir_src>/<file_src> + <dir_dst>/<file_dst> -> <dir_dst>/<file_dst>
CopyModeFileToFileRename
// Copy a src file to dest directory. The dest file gets created in the dest
// folder with the src filename.
// <dir_src>/<file> + <dir_dst> -> <dir_dst>/<file>
CopyModeFileToDir
// Copy a src directory to dest directory.
// <dir_src> + <dir_dst> -> <dir_dst>/<dir_src>
CopyModeDirToDir
// Copy all files in the src directory to the dest directory. This works recursively.
// <dir_src>/ + <dir_dst> -> <dir_dst>/<files_from_dir_src>
CopyModeFilesToDir
)
// copyMode takes a src and dest path and file mode to determine the copy mode.
// See the possible copy modes and their documentation.
func copyMode(srcPath, dstPath string, srcMode os.FileMode, dstMode os.FileMode, dstExists bool) (CopyMode, error) {
_, srcFile := path.Split(srcPath)
_, dstFile := path.Split(dstPath)
if srcMode.IsDir() {
if !dstExists {
return -1, ErrDstDirNotExist
}
if dstMode.IsDir() {
if strings.HasSuffix(srcPath, "/") {
return CopyModeFilesToDir, nil
}
return CopyModeDirToDir, nil
}
return -1, ErrCopyDirToFile
}
if dstMode.IsDir() {
return CopyModeFileToDir, nil
}
if srcFile != dstFile {
return CopyModeFileToFileRename, nil
}
return CopyModeFileToFile, nil
}
// moveDir moves all files from a source path to the destination path recursively.
func moveDir(sourcePath, destPath string) error {
return filepath.Walk(sourcePath, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
newPath := path.Join(destPath, strings.TrimPrefix(p, sourcePath))
if info.IsDir() {
err := os.Mkdir(newPath, info.Mode())
if err != nil {
if os.IsExist(err) {
return nil
}
return err
}
}
if info.Mode().IsRegular() {
return moveFile(p, newPath)
}
return nil
})
}
// moveFile moves a file from a source path to a destination path.
func moveFile(sourcePath, destPath string) error {
inputFile, err := os.Open(sourcePath)
if err != nil {
return err
}
outputFile, err := os.Create(destPath)
if err != nil {
inputFile.Close()
return err
}
defer outputFile.Close()
_, err = io.Copy(outputFile, inputFile)
inputFile.Close()
if err != nil {
return err
}
// Remove file after succesfull copy.
err = os.Remove(sourcePath)
if err != nil {
return err
}
return nil
}
func init() {
AppCpCommand.Flags().BoolVarP(
&internal.Chaos,
i18n.G("chaos"),
i18n.G("C"),
false,
i18n.G("ignore uncommitted recipes changes"),
)
}
+113
View File
@@ -0,0 +1,113 @@
package app
import (
"os"
"testing"
)
func TestParse(t *testing.T) {
tests := []struct {
src string
dst string
srcPath string
dstPath string
service string
toContainer bool
err error
}{
{src: "foo", dst: "bar", err: errServiceMissing},
{src: "app:foo", dst: "app:bar", err: errServiceMissing},
{src: "app:foo", dst: "bar", srcPath: "foo", dstPath: "bar", service: "app", toContainer: false},
{src: "foo", dst: "app:bar", srcPath: "foo", dstPath: "bar", service: "app", toContainer: true},
}
for i, tc := range tests {
srcPath, dstPath, service, toContainer, err := parseSrcAndDst(tc.src, tc.dst)
if srcPath != tc.srcPath {
t.Errorf("[%d] srcPath: want (%s), got(%s)", i, tc.srcPath, srcPath)
}
if dstPath != tc.dstPath {
t.Errorf("[%d] dstPath: want (%s), got(%s)", i, tc.dstPath, dstPath)
}
if service != tc.service {
t.Errorf("[%d] service: want (%s), got(%s)", i, tc.service, service)
}
if toContainer != tc.toContainer {
t.Errorf("[%d] toConainer: want (%t), got(%t)", i, tc.toContainer, toContainer)
}
if err == nil && tc.err != nil && err.Error() != tc.err.Error() {
t.Errorf("[%d] err: want (%s), got(%s)", i, tc.err, err)
}
}
}
func TestCopyMode(t *testing.T) {
tests := []struct {
srcPath string
dstPath string
srcMode os.FileMode
dstMode os.FileMode
dstExists bool
mode CopyMode
err error
}{
{
srcPath: "foo.txt",
dstPath: "foo.txt",
srcMode: os.ModePerm,
dstMode: os.ModePerm,
dstExists: true,
mode: CopyModeFileToFile,
},
{
srcPath: "foo.txt",
dstPath: "bar.txt",
srcMode: os.ModePerm,
dstExists: true,
mode: CopyModeFileToFileRename,
},
{
srcPath: "foo",
dstPath: "foo",
srcMode: os.ModeDir,
dstMode: os.ModeDir,
dstExists: true,
mode: CopyModeDirToDir,
},
{
srcPath: "foo/",
dstPath: "foo",
srcMode: os.ModeDir,
dstMode: os.ModeDir,
dstExists: true,
mode: CopyModeFilesToDir,
},
{
srcPath: "foo",
dstPath: "foo",
srcMode: os.ModeDir,
dstExists: false,
mode: -1,
err: ErrDstDirNotExist,
},
{
srcPath: "foo",
dstPath: "foo",
srcMode: os.ModeDir,
dstMode: os.ModePerm,
dstExists: true,
mode: -1,
err: ErrCopyDirToFile,
},
}
for i, tc := range tests {
mode, err := copyMode(tc.srcPath, tc.dstPath, tc.srcMode, tc.dstMode, tc.dstExists)
if mode != tc.mode {
t.Errorf("[%d] mode: want (%d), got(%d)", i, tc.mode, mode)
}
if err != tc.err {
t.Errorf("[%d] err: want (%s), got(%s)", i, tc.err, err)
}
}
}
+429 -39
View File
@@ -1,69 +1,459 @@
package app
import (
"context"
"errors"
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
stack "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"coopcloud.tech/abra/pkg/secret"
"coopcloud.tech/tagcmp"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/deploy"
"coopcloud.tech/abra/pkg/dns"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack"
dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra"
)
var appDeployCommand = &cli.Command{
Name: "deploy",
Aliases: []string{"d"},
Usage: "Deploy an app",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
// translators: `abra app deploy` aliases. use a comma separated list of aliases with
// no spaces in between
var appDeployAliases = i18n.G("d")
var AppDeployCommand = &cobra.Command{
// translators: `app deploy` command
Use: i18n.G("deploy <domain> [version] [flags]"),
Aliases: strings.Split(appDeployAliases, ","),
// translators: Short description for `app deploy` command
Short: i18n.G("Deploy an app"),
Long: i18n.G(`Deploy an app.
This command supports chaos operations. Use "--chaos/-C" to deploy your recipe
checkout as-is. Recipe commit hashes are also supported as values for
"[version]". Please note, "upgrade"/"rollback" do not support chaos operations.`),
Example: i18n.G(` # standard deployment
abra app deploy 1312.net
# chaos deployment
abra app deploy 1312.net --chaos
# deploy specific version
abra app deploy 1312.net 2.0.0+1.2.3
# deploy a specific git hash
abra app deploy 1312.net 886db76d`),
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string,
) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
app, err := appPkg.Get(args[0])
if err != nil {
errMsg := i18n.G("autocomplete failed: %s", err)
return []string{errMsg}, cobra.ShellCompDirectiveError
}
return autocomplete.RecipeVersionComplete(app.Recipe.Name)
default:
return nil, cobra.ShellCompDirectiveDefault
}
},
Run: func(cmd *cobra.Command, args []string) {
var (
deployWarnMessages []string
toDeployVersion string
)
app := internal.ValidateApp(args)
if err := validateArgsAndFlags(args); err != nil {
log.Fatal(err)
}
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
logrus.Fatal(err)
}
for k, v := range abraShEnv {
app.Env[k] = v
}
app.Env["STACK_NAME"] = app.StackName()
log.Debug(i18n.G("checking whether %s is already deployed", app.StackName()))
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
if deployMeta.IsDeployed && !(internal.Force || internal.Chaos) {
log.Fatal(i18n.G("%s is already deployed", app.Name))
}
toDeployVersion, err = getDeployVersion(args, deployMeta, app)
if err != nil {
log.Fatal(err)
}
isChaosCommit, err := app.Recipe.IsChaosCommit(toDeployVersion)
if err != nil {
log.Fatal(i18n.G("unable to determine if %s is a chaos commit: %s", toDeployVersion, err))
}
if !isChaosCommit && !tagcmp.IsParsable(toDeployVersion) {
log.Fatal(i18n.G("unable to parse deploy version: %s", toDeployVersion))
}
if !internal.Chaos {
isChaosCommit, err := app.Recipe.EnsureVersion(toDeployVersion)
if err != nil {
log.Fatal(i18n.G("ensure recipe: %s", err))
}
if isChaosCommit {
log.Warnf(i18n.G("version '%s' appears to be a chaos commit, but --chaos/-C was not provided", toDeployVersion))
internal.Chaos = true
}
}
if err := lint.LintForErrors(app.Recipe); err != nil {
if internal.Chaos {
log.Warn(err)
} else {
log.Fatal(err)
}
}
if err := validateSecrets(cl, app); err != nil {
log.Fatal(err)
}
if err := deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
log.Fatal(err)
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil {
log.Fatal(err)
}
stackName := app.StackName()
deployOpts := stack.Deploy{
Composefiles: composeFiles,
Namespace: app.StackName(),
Prune: false,
ResolveImage: stack.ResolveImageAlways,
Composefiles: composeFiles,
Namespace: stackName,
Prune: false,
ResolveImage: stack.ResolveImageAlways,
Detach: false,
SendRegistryAuth: true,
}
compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
logrus.Fatal(err)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
if internal.Chaos {
appPkg.SetChaosVersionLabel(compose, stackName, toDeployVersion)
}
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
versionLabel := toDeployVersion
if internal.Chaos {
for _, service := range compose.Services {
if service.Name == "app" {
labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName)
// NOTE(d1): keep non-chaos version labbeling when doing chaos ops
versionLabel = service.Deploy.Labels[labelKey]
}
}
}
appPkg.SetVersionLabel(compose, stackName, versionLabel)
newRecipeWithDeployVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, toDeployVersion)
appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithDeployVersion)
envVars, err := appPkg.CheckEnv(app)
if err != nil {
logrus.Warn(err)
log.Fatal(err)
}
if c.NArg() > 0 {
return
for _, envVar := range envVars {
if !envVar.Present {
deployWarnMessages = append(deployWarnMessages,
i18n.G("%s missing from %s.env", envVar.Name, app.Domain),
)
}
}
for _, a := range appNames {
fmt.Println(a)
if !internal.NoDomainChecks {
if domainName, ok := app.Env["DOMAIN"]; ok {
if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
log.Fatal(err)
}
} else {
log.Debug(i18n.G("skipping domain checks, no DOMAIN=... configured"))
}
} else {
log.Debug(i18n.G("skipping domain checks"))
}
deployedVersion := config.MISSING_DEFAULT
if deployMeta.IsDeployed {
deployedVersion = deployMeta.Version
if deployMeta.IsChaos {
deployedVersion = deployMeta.ChaosVersion
}
}
// Gather secrets
secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, internal.ShowUnchanged)
if err != nil {
log.Fatal(err)
}
// Gather configs
configInfo, err := deploy.GatherConfigsForDeploy(cl, app, compose, app.Env, internal.ShowUnchanged)
if err != nil {
log.Fatal(err)
}
// Gather images
imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose, internal.ShowUnchanged)
if err != nil {
log.Fatal(err)
}
// Show deploy overview
if err := internal.DeployOverview(
app,
deployedVersion,
toDeployVersion,
"",
deployWarnMessages,
secretInfo,
configInfo,
imageInfo,
); err != nil {
log.Fatal(err)
}
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
if err != nil {
log.Fatal(err)
}
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
if err != nil {
log.Fatal(err)
}
f, err := app.Filters(true, false, serviceNames...)
if err != nil {
log.Fatal(err)
}
if err := stack.RunDeploy(
cl,
deployOpts,
compose,
app.Name,
app.Server,
internal.DontWaitConverge,
internal.NoInput,
f,
); err != nil {
log.Fatal(err)
}
postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"]
if ok && !internal.DontWaitConverge {
log.Debug(i18n.G("run the following post-deploy commands: %s", postDeployCmds))
if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
log.Fatal(i18n.G("attempting to run post deploy commands, saw: %s", err))
}
}
if err := app.WriteRecipeVersion(toDeployVersion, false); err != nil {
log.Fatal(i18n.G("writing recipe version failed: %s", err))
}
},
}
func getLatestVersionOrCommit(app appPkg.App) (string, error) {
recipeVersions, warnings, err := app.Recipe.GetRecipeVersions()
if err != nil {
return "", err
}
for _, warning := range warnings {
log.Warn(warning)
}
if len(recipeVersions) > 0 && !internal.Chaos {
latest := recipeVersions[len(recipeVersions)-1]
for tag := range latest {
log.Debug(i18n.G("selected latest recipe version: %s (from %d available versions)", tag, len(recipeVersions)))
return tag, nil
}
}
head, err := app.Recipe.Head()
if err != nil {
return "", err
}
return formatter.SmallSHA(head.String()), nil
}
// validateArgsAndFlags ensures compatible args/flags.
func validateArgsAndFlags(args []string) error {
if len(args) == 2 && args[1] != "" && internal.Chaos {
return errors.New(i18n.G("cannot use [version] and --chaos together"))
}
if len(args) == 2 && args[1] != "" && internal.DeployLatest {
return errors.New(i18n.G("cannot use [version] and --latest together"))
}
if internal.DeployLatest && internal.Chaos {
return errors.New(i18n.G("cannot use --chaos and --latest together"))
}
return nil
}
func validateSecrets(cl *dockerClient.Client, app appPkg.App) error {
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil {
return err
}
secretsConfig, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
if err != nil {
return err
}
secStats, err := secret.PollSecretsStatus(cl, app)
if err != nil {
return err
}
for _, secStat := range secStats {
if !secStat.CreatedOnRemote {
secretConfig := secretsConfig[secStat.LocalName]
if secretConfig.SkipGenerate {
return errors.New(i18n.G("secret not inserted (#generate=false): %s", secStat.LocalName))
}
return errors.New(i18n.G("secret not generated: %s", secStat.LocalName))
}
}
return nil
}
func getDeployVersion(cliArgs []string, deployMeta stack.DeployMeta, app appPkg.App) (string, error) {
// Chaos mode overrides everything
if internal.Chaos {
v, err := app.Recipe.ChaosVersion()
if err != nil {
return "", err
}
log.Debug(i18n.G("version: taking chaos version: %s", v))
return v, nil
}
// Check if the deploy version is set with a cli argument
if len(cliArgs) == 2 && cliArgs[1] != "" {
log.Debug(i18n.G("version: taking version from cli arg: %s", cliArgs[1]))
return cliArgs[1], nil
}
// Check if the recipe has a version in the .env file
if app.Recipe.EnvVersion != "" && !internal.DeployLatest {
if strings.HasSuffix(app.Recipe.EnvVersionRaw, "+U") {
// NOTE(d1): use double-line 5 spaces ("FATA ") trick to make a more
// informative error message. it's ugly but that's our logging situation
// atm
return "", errors.New(i18n.G(`cannot redeploy previous chaos version (%s), did you mean to use "--chaos"?
to return to a regular release, specify a release tag, commit SHA or use "--latest"`,
formatter.BoldDirtyDefault(app.Recipe.EnvVersionRaw)))
}
log.Debug(i18n.G("version: taking version from .env file: %s", app.Recipe.EnvVersion))
return app.Recipe.EnvVersion, nil
}
// Take deployed version
if deployMeta.IsDeployed && !internal.DeployLatest {
log.Debug(i18n.G("version: taking deployed version: %s", deployMeta.Version))
return deployMeta.Version, nil
}
v, err := getLatestVersionOrCommit(app)
log.Debug(i18n.G("version: taking new recipe version: %s", v))
if err != nil {
return "", err
}
return v, nil
}
func init() {
AppDeployCommand.Flags().BoolVarP(
&internal.Chaos,
i18n.G("chaos"),
i18n.G("C"),
false,
i18n.G("ignore uncommitted recipes changes"),
)
AppDeployCommand.Flags().BoolVarP(
&internal.Force,
i18n.G("force"),
i18n.G("f"),
false,
i18n.G("perform action without further prompt"),
)
AppDeployCommand.Flags().BoolVarP(
&internal.NoDomainChecks,
i18n.G("no-domain-checks"),
i18n.G("D"),
false,
i18n.G("disable public DNS checks"),
)
AppDeployCommand.Flags().BoolVarP(
&internal.DontWaitConverge,
i18n.G("no-converge-checks"),
i18n.G("c"),
false,
i18n.G("disable converge logic checks"),
)
AppDeployCommand.PersistentFlags().BoolVarP(
&internal.DeployLatest,
i18n.G("latest"),
i18n.G("l"),
false,
i18n.G("deploy latest recipe version"),
)
AppDeployCommand.Flags().BoolVarP(
&internal.ShowUnchanged,
i18n.G("show-unchanged"),
i18n.G("U"),
false,
i18n.G("show all configs & images, including unchanged ones"),
)
}
+344
View File
@@ -0,0 +1,344 @@
package app
import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"sort"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
contextPkg "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types/filters"
"github.com/spf13/cobra"
)
// translators: `abra app env` aliases. use a comma separated list of aliases
// with no spaces in between
var appEnvAliases = i18n.G("e")
// translators: `abra app env list` aliases. use a comma separated list of
// aliases with no spaces in between
var appEnvListAliases = i18n.G("l,ls")
// translators: `abra app env pull` aliases. use a comma separated list of
// aliases with no spaces in between
var appEnvPullAliases = i18n.G("pl,p")
var AppEnvListCommand = &cobra.Command{
// translators: `app env list` command
Use: i18n.G("list <domain> [flags]"),
Aliases: strings.Split(appEnvListAliases, ","),
// translators: Short description for `app env list` command
Short: i18n.G("List all app environment values"),
Example: i18n.G(" abra app env list 1312.net"),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
var envKeys []string
for k := range app.Env {
envKeys = append(envKeys, k)
}
sort.Strings(envKeys)
var rows [][]string
for _, k := range envKeys {
rows = append(rows, []string{k, app.Env[k]})
}
overview := formatter.CreateOverview(i18n.G("ENV OVERVIEW"), rows)
fmt.Println(overview)
},
}
var AppEnvPullCommand = &cobra.Command{
// translators: `app pull` command
Use: i18n.G("pull <domain> [flags]"),
Aliases: strings.Split(appEnvPullAliases, ","),
// translators: Short description for `app env pull` command
Short: i18n.G("Pull app environment values from a deployed app"),
Long: i18n.G(`Pull app environment values from a deploymed app.
A convenient command for when you've lost your app environment file or want to
synchronize your local app environment values with what is deployed live.`),
Example: i18n.G(` # pull existing .env file and overwrite local values
abra app env pull 1312.net --force
# pull lost app .env file
abra app env pull my.gitea.net --server 1312.net`),
Args: cobra.MaximumNArgs(2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
appName := args[0]
appEnvPath := path.Join(config.ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName))
if _, err := os.Stat(appEnvPath); !os.IsNotExist(err) {
log.Fatal(i18n.G("%s already exists?", appEnvPath))
}
if server == "" {
log.Fatal(i18n.G("unable to determine server of app %s, please pass --server/-s", appName))
}
serverDir := filepath.Join(config.SERVERS_DIR, server)
if _, err := os.Stat(serverDir); os.IsNotExist(err) {
log.Fatal(i18n.G("unknown server %s, run \"abra server add %s\"?", server, server))
}
store := contextPkg.NewDefaultDockerContextStore()
contexts, err := store.Store.List()
if err != nil {
log.Fatal(i18n.G("unable to look up server context for %s: %s", server, err))
}
var contextCreated bool
if server == "default" {
contextCreated = true
}
for _, context := range contexts {
if context.Name == server {
contextCreated = true
}
}
if !contextCreated {
log.Fatal(i18n.G("%s missing context, run \"abra server add %s\"?", server, server))
}
cl, err := client.New(server)
if err != nil {
log.Fatal(err)
}
deployMeta, err := stack.IsDeployed(context.Background(), cl, appPkg.StackName(appName))
if err != nil {
log.Fatal(err)
}
if !deployMeta.IsDeployed {
log.Fatal(i18n.G("%s is not deployed?", appName))
}
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(appName), "app"))
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, internal.NoInput)
if err != nil {
log.Fatal(i18n.G("unable to retrieve container for %s: %s", appName, err))
}
inspectResult, err := cl.ContainerInspect(context.Background(), targetContainer.ID)
if err != nil {
log.Fatal(i18n.G("unable to inspect container for %s: %s", appName, err))
}
deploymentEnv := make(map[string]string)
for _, envVar := range inspectResult.Config.Env {
split := strings.SplitN(envVar, "=", 2)
if len(split) != 2 {
log.Debug(i18n.G("no value attached to %s", envVar))
continue
}
key, val := split[0], split[1]
deploymentEnv[key] = val
}
log.Debug(i18n.G("pulled env values from %s deployment: %s", appName, deploymentEnv))
var (
recipeEnvVar string
recipeKey string
)
if r, ok := deploymentEnv["TYPE"]; ok {
recipeKey = "TYPE"
recipeEnvVar = r
}
if r, ok := deploymentEnv["RECIPE"]; ok {
recipeKey = "RECIPE"
recipeEnvVar = r
}
if recipeEnvVar == "" {
log.Fatal(i18n.G("unable to determine recipe type from %s, env: %v", appName, inspectResult.Config.Env))
}
var recipeName = recipeEnvVar
if strings.Contains(recipeEnvVar, ":") {
split := strings.Split(recipeEnvVar, ":")
recipeName = split[0]
}
recipe := internal.ValidateRecipe(
[]string{recipeName},
cmd.Name(),
)
version := deployMeta.Version
if deployMeta.IsChaos {
version = deployMeta.ChaosVersion
}
if _, err := recipe.EnsureVersion(version); err != nil {
log.Fatal(err)
}
mergedEnv, err := recipe.SampleEnv()
if err != nil {
log.Fatal(err)
}
log.Debug(i18n.G("retrieved env values from .env.sample of %s: %s", recipe.Name, mergedEnv))
for k, v := range deploymentEnv {
mergedEnv[k] = v
}
if !strings.Contains(recipeEnvVar, ":") {
mergedEnv[recipeKey] = fmt.Sprintf("%s:%s", mergedEnv[recipeKey], version)
}
log.Debug(i18n.G("final merged env values for %s are: %s", appName, mergedEnv))
envSample, err := os.ReadFile(recipe.SampleEnvPath)
if err != nil {
log.Fatal(err)
}
err = os.WriteFile(appEnvPath, envSample, 0o664)
if err != nil {
log.Fatal(i18n.G("unable to write new env %s: %s", appEnvPath, err))
}
read, err := os.ReadFile(appEnvPath)
if err != nil {
log.Fatal(i18n.G("unable to read new env %s: %s", appEnvPath, err))
}
sampleEnv, err := recipe.SampleEnv()
if err != nil {
log.Fatal(err)
}
var composeFileUpdated bool
newContents := string(read)
for key, val := range mergedEnv {
if sampleEnv[key] == val {
continue
}
if key == "COMPOSE_FILE" {
composeFileUpdated = true
continue
}
if m, _ := regexp.MatchString(fmt.Sprintf(`#%s=`, key), newContents); m {
log.Debug(i18n.G("uncommenting %s", key))
re := regexp.MustCompile(fmt.Sprintf(`#%s=`, key))
newContents = re.ReplaceAllString(newContents, fmt.Sprintf("%s=", key))
}
if m, _ := regexp.MatchString(fmt.Sprintf(`# %s=`, key), newContents); m {
log.Debug(i18n.G("uncommenting %s", key))
re := regexp.MustCompile(fmt.Sprintf(`# %s=`, key))
newContents = re.ReplaceAllString(newContents, fmt.Sprintf("%s=", key))
}
if m, _ := regexp.MatchString(fmt.Sprintf(`%s=".*"`, key), newContents); m {
log.Debug(i18n.G(`inserting %s="%s" (double quotes)`, key, val))
re := regexp.MustCompile(fmt.Sprintf(`%s=".*"`, key))
newContents = re.ReplaceAllString(newContents, fmt.Sprintf(`%s="%s"`, key, val))
continue
}
if m, _ := regexp.MatchString(fmt.Sprintf(`%s='.*'`, key), newContents); m {
log.Debug(i18n.G(`inserting %s='%s' (single quotes)`, key, val))
re := regexp.MustCompile(fmt.Sprintf(`%s='.*'`, key))
newContents = re.ReplaceAllString(newContents, fmt.Sprintf(`%s='%s'`, key, val))
continue
}
if m, _ := regexp.MatchString(fmt.Sprintf("%s=.*", key), newContents); m {
log.Debug(i18n.G("inserting %s=%s (no quotes)", key, val))
re := regexp.MustCompile(fmt.Sprintf("%s=.*", key))
newContents = re.ReplaceAllString(newContents, fmt.Sprintf("%s=%s", key, val))
}
}
err = os.WriteFile(appEnvPath, []byte(newContents), 0)
if err != nil {
log.Fatal(i18n.G("unable to write new env %s: %s", appEnvPath, err))
}
log.Info(i18n.G("%s successfully created", appEnvPath))
if composeFileUpdated {
log.Warn(i18n.G("manual update required: COMPOSE_FILE=\"%s\"", mergedEnv["COMPOSE_FILE"]))
}
},
}
var AppEnvCommand = &cobra.Command{
// translators: `app env` command group
Use: i18n.G("env [cmd] [args] [flags]"),
Aliases: strings.Split(appEnvAliases, ","),
// translators: Short description for `app env` command group
Short: i18n.G("Manage app environment values"),
}
var (
server string
)
func init() {
AppEnvPullCommand.Flags().BoolVarP(
&internal.Force,
i18n.G("force"),
i18n.G("f"),
false,
i18n.G("perform action without further prompt"),
)
AppEnvPullCommand.Flags().StringVarP(
&server,
i18n.G("server"),
i18n.G("s"),
"",
i18n.G("server associated with deployed app"),
)
AppEnvPullCommand.RegisterFlagCompletionFunc(
i18n.G("server"),
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.ServerNameComplete()
},
)
}
+147
View File
@@ -0,0 +1,147 @@
package app
import (
"context"
"fmt"
"sort"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/convert"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra"
)
// translators: `abra app labels` aliases. use a comma separated list of
// aliases with no spaces in between
var appLabelsAliases = i18n.G("lb")
var AppLabelsCommand = &cobra.Command{
// translators: `app labels` command
Use: i18n.G("labels <domain> [flags]"),
Aliases: strings.Split(appLabelsAliases, ","),
// translators: Short description for `app labels` command
Short: i18n.G("Show deployment labels"),
Long: i18n.G("Both local recipe and live deployment labels are shown."),
Example: " " + i18n.G("abra app labels 1312.net"),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
remoteLabels, err := getLabels(cl, app.StackName())
if err != nil {
log.Fatal(err)
}
rows := [][]string{
{i18n.G("DEPLOYED LABELS"), "---"},
}
remoteLabelKeys := make([]string, 0, len(remoteLabels))
for k := range remoteLabels {
remoteLabelKeys = append(remoteLabelKeys, k)
}
sort.Strings(remoteLabelKeys)
for _, k := range remoteLabelKeys {
rows = append(rows, []string{
k,
remoteLabels[k],
})
}
if len(remoteLabelKeys) == 0 {
rows = append(rows, []string{i18n.G("unknown")})
}
rows = append(rows, []string{i18n.G("RECIPE LABELS"), "---"})
config, err := app.Recipe.GetComposeConfig(app.Env)
if err != nil {
log.Fatal(err)
}
var localLabelKeys []string
var appServiceConfig composetypes.ServiceConfig
for _, service := range config.Services {
if service.Name == "app" {
appServiceConfig = service
for k := range service.Deploy.Labels {
localLabelKeys = append(localLabelKeys, k)
}
}
}
sort.Strings(localLabelKeys)
for _, k := range localLabelKeys {
rows = append(rows, []string{
k,
appServiceConfig.Deploy.Labels[k],
})
}
overview := formatter.CreateOverview(i18n.G("LABELS OVERVIEW"), rows)
fmt.Println(overview)
},
}
// getLabels reads docker labels from running services in the format of "coop-cloud.${STACK_NAME}.${LABEL}".
func getLabels(cl *dockerClient.Client, stackName string) (map[string]string, error) {
labels := make(map[string]string)
filter := filters.NewArgs()
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter})
if err != nil {
return labels, err
}
for _, service := range services {
if service.Spec.Name != fmt.Sprintf("%s_app", stackName) {
continue
}
for k, v := range service.Spec.Labels {
labels[k] = v
}
}
return labels, nil
}
func init() {
AppLabelsCommand.Flags().BoolVarP(
&internal.Chaos,
i18n.G("chaos"),
i18n.G("C"),
false,
i18n.G("ignore uncommitted recipes changes"),
)
}
+319 -74
View File
@@ -1,101 +1,346 @@
package app
import (
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/tagcmp"
"github.com/spf13/cobra"
)
var status bool
var statusFlag = &cli.BoolFlag{
Name: "status",
Aliases: []string{"S"},
Value: false,
Usage: "Show app deployment status",
Destination: &status,
type appStatus struct {
Server string `json:"server"`
Recipe string `json:"recipe"`
AppName string `json:"appName"`
Domain string `json:"domain"`
Status string `json:"status"`
Chaos string `json:"chaos"`
ChaosVersion string `json:"chaosVersion"`
Version string `json:"version"`
Upgrade string `json:"upgrade"`
}
var appType string
var typeFlag = &cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
Value: "",
Usage: "Show apps of a specific type",
Destination: &appType,
type serverStatus struct {
Apps []appStatus `json:"apps"`
AppCount int `json:"appCount"`
VersionCount int `json:"versionCount"`
UnversionedCount int `json:"unversionedCount"`
LatestCount int `json:"latestCount"`
UpgradeCount int `json:"upgradeCount"`
}
var listAppServer string
var listAppServerFlag = &cli.StringFlag{
Name: "server",
Aliases: []string{"s"},
Value: "",
Usage: "Show apps of a specific server",
Destination: &listAppServer,
}
// translators: `abra app list` aliases. use a comma separated list of aliases with
// no spaces in between
var appListAliases = i18n.G("ls")
var appListCommand = &cli.Command{
Name: "list",
Usage: "List all managed apps",
Description: `
This command looks at your local file system listing of apps and servers (e.g.
in ~/.abra/) to generate a report of all your apps.
var AppListCommand = &cobra.Command{
// translators: `app list` command
Use: i18n.G("list [flags]"),
Aliases: strings.Split(appListAliases, ","),
// translators: Short description for `app list` command
Short: i18n.G("List all managed apps"),
Long: i18n.G(`Generate a report of all managed apps.
By passing the "--status/-S" flag, you can query all your servers for the
actual live deployment status. Depending on how many servers you manage, this
can take some time.
`,
Aliases: []string{"ls"},
Flags: []cli.Flag{
statusFlag,
listAppServerFlag,
typeFlag,
},
Action: func(c *cli.Context) error {
appFiles, err := config.LoadAppFiles(listAppServer)
Use "--status/-S" flag to query all servers for the live deployment status.`),
Example: i18n.G(` # list apps of all servers without live status
abra app ls
# list apps of a specific server with live status
abra app ls -s 1312.net -S
# list apps of all servers which match a specific recipe
abra app ls -r gitea`),
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
appFiles, err := appPkg.LoadAppFiles(listAppServer)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
apps, err := config.GetApps(appFiles)
apps, err := appPkg.GetApps(appFiles, recipeFilter)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
sort.Sort(config.ByServerAndType(apps))
statuses := map[string]string{}
tableCol := []string{"Server", "Type", "Domain"}
sort.Sort(appPkg.ByServerAndRecipe(apps))
statuses := make(map[string]map[string]string)
if status {
tableCol = append(tableCol, "Status")
statuses, err = config.GetAppStatuses(appFiles)
if err != nil {
logrus.Fatal(err)
}
}
table := abraFormatter.CreateTable(tableCol)
table.SetAutoMergeCellsByColumnIndex([]int{0})
for _, app := range apps {
var tableRow []string
if app.Type == appType || appType == "" {
// If type flag is set, check for it, if not, Type == ""
tableRow = []string{app.Server, app.Type, app.Domain}
if status {
if status, ok := statuses[app.StackName()]; ok {
tableRow = append(tableRow, status)
} else {
tableRow = append(tableRow, "unknown")
}
alreadySeen := make(map[string]bool)
for _, app := range apps {
if _, ok := alreadySeen[app.Server]; !ok {
alreadySeen[app.Server] = true
}
}
table.Append(tableRow)
statuses, err = appPkg.GetAppStatuses(apps, internal.MachineReadable)
if err != nil {
log.Fatal(err)
}
}
table.Render()
var totalServersCount int
var totalAppsCount int
allStats := make(map[string]serverStatus)
for _, app := range apps {
var stats serverStatus
var ok bool
if stats, ok = allStats[app.Server]; !ok {
stats = serverStatus{}
if recipeFilter == "" {
// count server, no filtering
totalServersCount++
}
}
return nil
if app.Recipe.Name == recipeFilter || recipeFilter == "" {
if recipeFilter != "" {
// only count server if matches filter
totalServersCount++
}
appStats := appStatus{}
stats.AppCount++
totalAppsCount++
if status {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
status := i18n.G("unknown")
version := i18n.G("unknown")
chaos := i18n.G("unknown")
chaosVersion := i18n.G("unknown")
if statusMeta, ok := statuses[app.StackName()]; ok {
if currentVersion, exists := statusMeta["version"]; exists {
if currentVersion != "" {
version = currentVersion
}
}
if chaosDeploy, exists := statusMeta["chaos"]; exists {
chaos = chaosDeploy
}
if chaosDeployVersion, exists := statusMeta["chaosVersion"]; exists {
chaosVersion = chaosDeployVersion
}
if statusMeta["status"] != "" {
status = statusMeta["status"]
}
stats.VersionCount++
} else {
stats.UnversionedCount++
}
appStats.Status = status
appStats.Chaos = chaos
appStats.ChaosVersion = chaosVersion
appStats.Version = version
var newUpdates []string
if version != "unknown" && chaos == "false" {
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(i18n.G("unable to clone %s: %s", app.Name, err))
}
updates, err := app.Recipe.Tags()
if err != nil {
log.Fatal(i18n.G("unable to retrieve tags for %s: %s", app.Name, err))
}
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
log.Fatal(err)
}
for _, update := range updates {
if ok := tagcmp.IsParsable(update); !ok {
log.Debug(i18n.G("unable to parse %s, skipping as upgrade option", update))
continue
}
parsedUpdate, err := tagcmp.Parse(update)
if err != nil {
log.Fatal(err)
}
if update != version && parsedUpdate.IsGreaterThan(parsedVersion) {
newUpdates = append(newUpdates, update)
}
}
}
if len(newUpdates) == 0 {
if version == "unknown" {
appStats.Upgrade = i18n.G("unknown")
} else {
appStats.Upgrade = i18n.G("latest")
stats.LatestCount++
}
} else {
newUpdates = internal.SortVersionsDesc(newUpdates)
appStats.Upgrade = strings.Join(newUpdates, "\n")
stats.UpgradeCount++
}
}
appStats.Server = app.Server
appStats.Recipe = app.Recipe.Name
appStats.AppName = app.Name
appStats.Domain = app.Domain
stats.Apps = append(stats.Apps, appStats)
}
allStats[app.Server] = stats
}
if internal.MachineReadable {
jsonstring, err := json.Marshal(allStats)
if err != nil {
log.Fatal(err)
} else {
fmt.Println(string(jsonstring))
}
return
}
alreadySeen := make(map[string]bool)
for _, app := range apps {
if _, ok := alreadySeen[app.Server]; ok {
continue
}
serverStat := allStats[app.Server]
headers := []string{i18n.G("RECIPE"), i18n.G("DOMAIN"), i18n.G("SERVER")}
if status {
headers = append(headers, []string{
i18n.G("STATUS"),
i18n.G("CHAOS"),
i18n.G("VERSION"),
i18n.G("UPGRADE"),
}...,
)
}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table.Headers(headers...)
var rows [][]string
for _, appStat := range serverStat.Apps {
row := []string{appStat.Recipe, appStat.Domain, appStat.Server}
if status {
chaosStatus := appStat.Chaos
if chaosStatus != "unknown" {
chaosEnabled, err := strconv.ParseBool(chaosStatus)
if err != nil {
log.Fatal(err)
}
if chaosEnabled && appStat.ChaosVersion != "unknown" {
chaosStatus = appStat.ChaosVersion
}
}
row = append(row, []string{
appStat.Status,
chaosStatus,
appStat.Version,
appStat.Upgrade}...,
)
}
rows = append(rows, row)
}
table.Rows(rows...)
if len(rows) > 0 {
if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
if len(allStats) > 1 && len(rows) > 0 {
fmt.Println() // newline separator for multiple servers
}
}
alreadySeen[app.Server] = true
}
},
}
var (
status bool
recipeFilter string
listAppServer string
)
func init() {
AppListCommand.Flags().BoolVarP(
&status,
i18n.G("status"),
i18n.G("S"),
false,
i18n.G("show app deployment status"),
)
AppListCommand.Flags().StringVarP(
&recipeFilter,
i18n.G("recipe"),
i18n.G("r"),
"",
i18n.G("show apps of a specific recipe"),
)
AppListCommand.RegisterFlagCompletionFunc(
i18n.G("recipe"),
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
)
AppListCommand.Flags().BoolVarP(
&internal.MachineReadable,
i18n.G("machine"),
i18n.G("m"),
false,
i18n.G("print machine-readable output"),
)
AppListCommand.Flags().StringVarP(
&listAppServer,
i18n.G("server"),
i18n.G("s"),
"",
i18n.G("show apps of a specific server"),
)
AppListCommand.Flags().BoolVarP(
&internal.Chaos,
i18n.G("chaos"),
i18n.G("C"),
false,
i18n.G("ignore uncommitted recipes changes"),
)
AppListCommand.RegisterFlagCompletionFunc(
i18n.G("server"),
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.ServerNameComplete()
},
)
}
+84 -98
View File
@@ -2,126 +2,112 @@ package app
import (
"context"
"fmt"
"io"
"os"
"sync"
"strings"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/logs"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/spf13/cobra"
)
// stackLogs lists logs for all stack services
func stackLogs(stackName string, client *dockerClient.Client) {
ctx := context.Background()
filters := filters.NewArgs()
filters.Add("name", stackName)
serviceOpts := types.ServiceListOptions{Filters: filters}
services, err := client.ServiceList(ctx, serviceOpts)
if err != nil {
logrus.Fatal(err)
}
// translators: `abra app logs` aliases. use a comma separated list of aliases with
// no spaces in between
var appLogsAliases = i18n.G("l")
var wg sync.WaitGroup
for _, service := range services {
wg.Add(1)
go func(s string) {
logOpts := types.ContainerLogsOptions{
Details: true,
Follow: true,
ShowStderr: true,
ShowStdout: true,
Tail: "20",
Timestamps: true,
}
logs, err := client.ServiceLogs(ctx, s, logOpts)
var AppLogsCommand = &cobra.Command{
// translators: `app logs` command
Use: i18n.G("logs <domain> [service] [flags]"),
Aliases: strings.Split(appLogsAliases, ","),
// translators: Short description for `app logs` command
Short: i18n.G("Tail app logs"),
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
app, err := appPkg.Get(args[0])
if err != nil {
logrus.Fatal(err)
return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
}
// defer after err check as any err returns a nil io.ReadCloser
defer logs.Close()
return autocomplete.ServiceNameComplete(app.Name)
default:
return nil, cobra.ShellCompDirectiveDefault
}
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
stackName := app.StackName()
_, err = io.Copy(os.Stdout, logs)
if err != nil && err != io.EOF {
logrus.Fatal(err)
}
}(service.ID)
}
wg.Wait()
os.Exit(0)
}
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)
}
var appLogsCommand = &cli.Command{
Name: "logs",
Aliases: []string{"l"},
ArgsUsage: "[<service>]",
Usage: "Tail app logs",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
ctx := context.Background()
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
serviceName := c.Args().Get(1)
if serviceName == "" {
logrus.Debug("tailing logs for all app services")
stackLogs(app.StackName(), cl)
}
logrus.Debugf("tailing logs for '%s'", serviceName)
service := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
filters := filters.NewArgs()
filters.Add("name", service)
serviceOpts := types.ServiceListOptions{Filters: filters}
services, err := cl.ServiceList(ctx, serviceOpts)
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
logrus.Fatal(err)
}
if len(services) != 1 {
logrus.Fatalf("expected 1 service but got %v", len(services))
log.Fatal(err)
}
logOpts := types.ContainerLogsOptions{
Details: true,
Follow: true,
ShowStderr: true,
ShowStdout: true,
Tail: "20",
Timestamps: true,
if !deployMeta.IsDeployed {
log.Fatal(i18n.G("%s is not deployed?", app.Name))
}
logs, err := cl.ServiceLogs(ctx, services[0].ID, logOpts)
var serviceNames []string
if len(args) == 2 {
serviceNames = []string{args[1]}
}
f, err := app.Filters(true, false, serviceNames...)
if err != nil {
logrus.Fatal(err)
}
// defer after err check as any err returns a nil io.ReadCloser
defer logs.Close()
_, err = io.Copy(os.Stdout, logs)
if err != nil && err != io.EOF {
logrus.Fatal(err)
log.Fatal(err)
}
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
opts := logs.TailOpts{
AppName: app.Name,
Services: serviceNames,
StdErr: stdErr,
Since: sinceLogs,
Filters: f,
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
if err := logs.TailLogs(cl, opts); err != nil {
log.Fatal(err)
}
},
}
var (
stdErr bool
sinceLogs string
)
func init() {
AppLogsCommand.Flags().BoolVarP(
&stdErr,
i18n.G("stderr"),
i18n.G("s"),
false,
i18n.G("only tail stderr"),
)
AppLogsCommand.Flags().StringVarP(
&sinceLogs,
i18n.G("since"),
i18n.G("S"),
"",
i18n.G("tail logs since YYYY-MM-DDTHH:MM:SSZ"),
)
}
+354
View File
@@ -0,0 +1,354 @@
package app
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app"
"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/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/secret"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/volume"
dockerclient "github.com/docker/docker/client"
"github.com/spf13/cobra"
)
// translators: `abra app move` aliases. use a comma separated list of aliases
// with no spaces in between
var appMoveAliases = i18n.G("m")
var AppMoveCommand = &cobra.Command{
// translators: `app move` command
Use: i18n.G("move <domain> <server> [flags]"),
Aliases: strings.Split(appMoveAliases, ","),
// translators: Short description for `app move` command
Short: i18n.G("Moves an app to a different server"),
Long: i18n.G(`Move an app to a differnt server.
This command will migrate an app config and copy secrets and volumes from the
old server to the new one. The app MUST be deployed on the old server before
doing the move. The app will be undeployed from the current server but not
deployed on the new server.
The "tar" command is required on both the old and new server as well as "sudo"
permissions. The "rsync" command is required on your local machine for
transferring volumes.
Do not forget to update your DNS records. Don't panic, it might take a while
for the dust to settle after you move an app. If anything goes wrong, you can
always move the app config file to the original server and deploy it there
again. No data is removed from the old server.
Use "--dry-run/-r" to see which secrets and volumes will be moved.`),
Example: i18n.G(` # move an app
abra app move nextcloud.1312.net myserver.com`),
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string,
) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
return autocomplete.ServerNameComplete()
default:
return nil, cobra.ShellCompDirectiveDefault
}
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if len(args) <= 1 {
log.Fatal(i18n.G("no server provided?"))
}
newServer := internal.ValidateServer([]string{args[1]})
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
currentServerClient, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
deployMeta, err := stack.IsDeployed(context.Background(), currentServerClient, app.StackName())
if err != nil {
log.Fatal(err)
}
if !deployMeta.IsDeployed {
log.Fatal(i18n.G("%s must first be deployed on %s before moving", app.Name, app.Server))
}
resources, err := getAppResources(currentServerClient, app)
if err != nil {
log.Fatal(i18n.G("unable to retrieve %s resources on %s: %s", app.Name, app.Server, err))
}
internal.MoveOverview(app, newServer, resources.SecretNames(), resources.VolumeNames())
if err := internal.PromptProcced(); err != nil {
log.Fatal(i18n.G("bailing out: %s", err))
}
log.Info(i18n.G("undeploying %s on %s", app.Name, app.Server))
rmOpts := stack.Remove{
Namespaces: []string{app.StackName()},
Detach: false,
}
if err := stack.RunRemove(context.Background(), currentServerClient, rmOpts); err != nil {
log.Fatal(i18n.G("failed to remove app from %s: %s", err, app.Server))
}
newServerClient, err := client.New(newServer)
if err != nil {
log.Fatal(err)
}
for _, s := range resources.SecretList {
sname := strings.Split(strings.TrimPrefix(s.Spec.Name, app.StackName()+"_"), "_")
secretName := strings.Join(sname[:len(sname)-1], "_")
data := resources.Secrets[secretName]
if err := client.StoreSecret(newServerClient, s.Spec.Name, data); err != nil {
if strings.Contains(err.Error(), "already exists") {
log.Info(i18n.G("skipping secret (because it already exists) on %s: %s", s.Spec.Name, newServer))
continue
}
log.Fatal(i18n.G("failed to store secret on %s: %s", err, newServer))
}
log.Info(i18n.G("created secret on %s: %s", s.Spec.Name, newServer))
}
for _, v := range resources.Volumes {
log.Info(i18n.G("moving volume %s from %s to %s", v.Name, app.Server, newServer))
// NOTE(p4u1): Need to create the volume before copying the data, because
// when docker creates a new volume it set the folder permissions to
// root, which might be wrong. This ensures we always have the correct
// folder permissions inside the volume.
log.Debug(i18n.G("creating volume %s on %s", v.Name, newServer))
_, err := newServerClient.VolumeCreate(context.Background(), volume.CreateOptions{
Name: v.Name,
Driver: v.Driver,
})
if err != nil {
log.Fatal(i18n.G("failed to create volume %s on %s: %s", v.Name, newServer, err))
}
filename := fmt.Sprintf("%s_outgoing.tar.gz", v.Name)
log.Debug(i18n.G("creating %s on %s", filename, app.Server))
tarCmd := fmt.Sprintf("sudo tar --same-owner -czhpf %s -C /var/lib/docker/volumes %s", filename, v.Name)
cmd := exec.Command("ssh", app.Server, "-tt", tarCmd)
if out, err := cmd.CombinedOutput(); err != nil {
log.Fatal(i18n.G("%s failed on %s: output:%s err:%s", tarCmd, app.Server, string(out), err))
}
log.Debug(i18n.G("rsyncing %s from %s to local machine", filename, app.Server))
cmd = exec.Command("rsync", "-a", "-v", fmt.Sprintf("%s:%s", app.Server, filename), filename)
if out, err := cmd.CombinedOutput(); err != nil {
log.Fatal(i18n.G("failed to copy %s from %s to local machine: output:%s err:%s", filename, app.Server, string(out), err))
}
log.Debug(i18n.G("rsyncing %s to %s from local machine", filename, filename, newServer))
cmd = exec.Command("rsync", "-a", "-v", filename, fmt.Sprintf("%s:%s", newServer, filename))
if out, err := cmd.CombinedOutput(); err != nil {
log.Fatal(i18n.G("failed to copy %s from local machine to %s: output:%s err:%s", filename, newServer, string(out), err))
}
log.Debug(i18n.G("extracting %s on %s", filename, newServer))
tarExtractCmd := fmt.Sprintf("sudo tar --same-owner -xzpf %s -C /var/lib/docker/volumes", filename)
cmd = exec.Command("ssh", newServer, "-tt", tarExtractCmd)
if out, err := cmd.CombinedOutput(); err != nil {
log.Fatal(i18n.G("%s failed to extract %s on %s: output:%s err:%s", tarExtractCmd, filename, newServer, string(out), err))
}
// Remove tar files
log.Debug(i18n.G("removing %s from %s", filename, newServer))
cmd = exec.Command("ssh", newServer, "-tt", fmt.Sprintf("sudo rm -rf %s", filename))
if out, err := cmd.CombinedOutput(); err != nil {
log.Fatal(i18n.G("failed to remove %s from %s: output:%s err:%s", filename, newServer, string(out), err))
}
log.Debug(i18n.G("removing %s from %s", filename, app.Server))
cmd = exec.Command("ssh", app.Server, "-tt", fmt.Sprintf("sudo rm -rf %s", filename))
if out, err := cmd.CombinedOutput(); err != nil {
log.Fatal(i18n.G("failed to remove %s from %s: output:%s err:%s", filename, app.Server, string(out), err))
}
log.Debug(i18n.G("removing %s from local machine", filename))
cmd = exec.Command("rm", "-r", "-f", filename)
if out, err := cmd.CombinedOutput(); err != nil {
log.Fatal(i18n.G("failed to remove %s on local machine: output:%s err:%s", filename, string(out), err))
}
}
newServerPath := fmt.Sprintf("%s/servers/%s/%s.env", config.ABRA_DIR, newServer, app.Name)
log.Info(i18n.G("migrating app config from %s to %s", app.Server, newServerPath))
if err := copyFile(app.Path, newServerPath); err != nil {
log.Fatal(i18n.G("failed to migrate app config: %s", err))
}
if err := os.Remove(app.Path); err != nil {
log.Fatal(i18n.G("unable to remove %s: %s", app.Path, err))
}
log.Info(i18n.G("%s was successfully moved from %s to %s 🎉", app.Name, app.Server, newServer))
},
}
type AppResources struct {
Secrets map[string]string
SecretList []swarm.Secret
Volumes map[string]containertypes.MountPoint
}
func (a *AppResources) SecretNames() []string {
secrets := []string{}
for name := range a.Secrets {
secrets = append(secrets, name)
}
return secrets
}
func (a *AppResources) VolumeNames() []string {
volumes := []string{}
for name := range a.Volumes {
volumes = append(volumes, name)
}
return volumes
}
func getAppResources(cl *dockerclient.Client, app app.App) (*AppResources, error) {
filter, err := app.Filters(false, false)
if err != nil {
return nil, err
}
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter})
if err != nil {
return nil, err
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil {
return nil, err
}
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filter})
if err != nil {
return nil, err
}
secretConfigs, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
if err != nil {
return nil, err
}
opts := stack.Deploy{Composefiles: composeFiles, Namespace: app.StackName()}
compose, err := appPkg.GetAppComposeConfig(app.Name, opts, app.Env)
if err != nil {
return nil, err
}
resources := &AppResources{
Secrets: make(map[string]string),
SecretList: secretList,
Volumes: make(map[string]containertypes.MountPoint),
}
for _, s := range services {
secretNames := map[string]string{}
for _, serviceCompose := range compose.Services {
stackService := fmt.Sprintf("%s_%s", app.StackName(), serviceCompose.Name)
if stackService != s.Spec.Name {
log.Debug(i18n.G("skipping %s as it does not match %s", stackService, s.Spec.Name))
continue
}
for _, secret := range serviceCompose.Secrets {
for _, s := range secretList {
stackSecret := fmt.Sprintf("%s_%s_%s", app.StackName(), secret.Source, secretConfigs[secret.Source].Version)
if s.Spec.Name == stackSecret {
secretNames[secret.Source] = s.ID
break
}
}
}
}
f := filters.NewArgs()
f.Add("name", s.Spec.Name)
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, f, true)
if err != nil {
return nil, errors.New(i18n.G("unable to get container matching %s: %s", s.Spec.Name, err))
}
for _, m := range targetContainer.Mounts {
if m.Type == mount.TypeVolume {
resources.Volumes[m.Name] = m
}
}
for secretName, secretID := range secretNames {
if _, ok := resources.Secrets[secretName]; ok {
continue
}
log.Debug(i18n.G("extracting secret %s on %s", secretName, app.Server))
cmd := fmt.Sprintf("sudo cat /var/lib/docker/containers/%s/mounts/secrets/%s", targetContainer.ID, secretID)
out, err := exec.Command("ssh", app.Server, "-tt", cmd).Output()
if err != nil {
return nil, errors.New(i18n.G("%s failed on %s: output:%s err:%s", cmd, app.Server, string(out), err))
}
resources.Secrets[secretName] = string(out)
}
}
return resources, nil
}
func copyFile(src string, dst string) error {
// Read all content of src to data, may cause OOM for a large file.
data, err := os.ReadFile(src)
if err != nil {
return err
}
// Write data to dst
err = os.WriteFile(dst, data, 0o644)
if err != nil {
return err
}
return nil
}
func init() {
AppMoveCommand.Flags().BoolVarP(
&internal.Dry,
i18n.G("dry-run"),
i18n.G("r"),
false,
i18n.G("report changes that would be made"),
)
}
+356 -170
View File
@@ -1,59 +1,39 @@
package app
import (
"errors"
"fmt"
"path"
"strings"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra"
)
type secrets map[string]string
var appNewDescription = i18n.G(`Creates a new app from a default recipe.
var domain string
var domainFlag = &cli.StringFlag{
Name: "domain",
Aliases: []string{"d"},
Value: "",
Usage: "Choose a domain name",
Destination: &domain,
}
var newAppServer string
var newAppServerFlag = &cli.StringFlag{
Name: "server",
Aliases: []string{"s"},
Value: "",
Usage: "Show apps of a specific server",
Destination: &newAppServer,
}
var newAppName string
var newAppNameFlag = &cli.StringFlag{
Name: "app-name",
Aliases: []string{"a"},
Value: "",
Usage: "Choose an app name",
Destination: &newAppName,
}
var appNewDescription = `
This command takes a recipe and uses it to create a new app. This new app
configuration is stored in your ~/.abra directory under the appropriate server.
This new app configuration is stored in your $ABRA_DIR directory under the
appropriate server.
This command does not deploy your app for you. You will need to run "abra app
deploy <app>" to do so.
deploy <domain>" to do so.
You can see what recipes are available (i.e. values for the <recipe> argument)
You can see what recipes are available (i.e. values for the [recipe] argument)
by running "abra recipe ls".
Recipe commit hashes are supported values for "[version]".
Passing the "--secrets/-S" flag will automatically generate secrets for your
app and store them encrypted at rest on the chosen target server. These
generated secrets are only visible at generation time, so please take care to
@@ -61,47 +41,290 @@ store them somewhere safe.
You can use the "--pass/-P" to store these generated passwords locally in a
pass store (see passwordstore.org for more). The pass command must be available
on your $PATH.
`
on your $PATH.`)
var appNewCommand = &cli.Command{
Name: "new",
Usage: "Create a new app",
Aliases: []string{"n"},
Description: appNewDescription,
Flags: []cli.Flag{
newAppServerFlag,
domainFlag,
newAppNameFlag,
internal.PassFlag,
internal.SecretsFlag,
// translators: `abra app new` aliases. use a comma separated list of aliases with
// no spaces in between
var appNewAliases = i18n.G("n")
var AppNewCommand = &cobra.Command{
// translators: `app new` command
Use: i18n.G("new [recipe] [version] [flags]"),
Aliases: strings.Split(appNewAliases, ","),
// translators: Short description for `app new` command
Short: i18n.G("Create a new app"),
Long: appNewDescription,
Args: cobra.RangeArgs(0, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.RecipeNameComplete()
case 1:
recipe := internal.ValidateRecipe(args, cmd.Name())
return autocomplete.RecipeVersionComplete(recipe.Name)
default:
return nil, cobra.ShellCompDirectiveDefault
}
},
ArgsUsage: "<recipe>",
Action: action,
BashComplete: func(c *cli.Context) {
catl, err := catalogue.ReadRecipeCatalogue()
Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name())
if err := recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
if len(args) == 2 && internal.Chaos {
log.Fatal(i18n.G("cannot use [version] and --chaos together"))
}
var recipeVersion string
if len(args) == 2 {
recipeVersion = args[1]
}
chaosVersion := config.CHAOS_DEFAULT
if internal.Chaos {
var err error
chaosVersion, err = recipe.ChaosVersion()
if err != nil {
log.Fatal(err)
}
recipeVersion = chaosVersion
} else {
if err := recipe.EnsureIsClean(); err != nil {
log.Fatal(err)
}
var recipeVersions recipePkg.RecipeVersions
if recipeVersion == "" {
var err error
var warnings []string
recipeVersions, warnings, err = recipe.GetRecipeVersions()
if err != nil {
log.Fatal(err)
}
for _, warning := range warnings {
log.Warn(warning)
}
}
if recipeVersion != "" {
if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
log.Fatal(err)
}
} else if len(recipeVersions) > 0 {
latest := recipeVersions[len(recipeVersions)-1]
for tag := range latest {
recipeVersion = tag
}
log.Debug(i18n.G("selected recipe version: %s (from %d available versions)", recipeVersion, len(recipeVersions)))
if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
log.Fatal(err)
}
} else {
if err := recipe.EnsureLatest(); err != nil {
log.Fatal(err)
}
if recipeVersion == "" {
head, err := recipe.Head()
if err != nil {
log.Fatal(i18n.G("failed to retrieve latest commit for %s: %s", recipe.Name, err))
}
recipeVersion = formatter.SmallSHA(head.String())
}
}
}
if err := ensureServerFlag(); err != nil {
log.Fatal(err)
}
if err := ensureDomainFlag(recipe, newAppServer); err != nil {
log.Fatal(err)
}
sanitisedAppName := appPkg.SanitiseAppName(appDomain)
log.Debug(i18n.G("%s sanitised as %s for new app", appDomain, sanitisedAppName))
if err := appPkg.TemplateAppEnvSample(
recipe,
appDomain,
newAppServer,
appDomain,
); err != nil {
log.Fatal(err)
}
sampleEnv, err := recipe.SampleEnv()
if err != nil {
logrus.Warn(err)
log.Fatal(err)
}
if c.NArg() > 0 {
return
composeFiles, err := recipe.GetComposeFiles(sampleEnv)
if err != nil {
log.Fatal(err)
}
for name, _ := range catl {
fmt.Println(name)
secretsConfig, err := secret.ReadSecretsConfig(
recipe.SampleEnvPath,
composeFiles,
appPkg.StackName(appDomain),
)
if err != nil {
log.Fatal(err)
}
var appSecrets AppSecrets
if generateSecrets {
if err := promptForSecrets(recipe.Name, secretsConfig); err != nil {
log.Fatal(err)
}
cl, err := client.New(newAppServer)
if err != nil {
log.Fatal(err)
}
appSecrets, err = createSecrets(cl, secretsConfig, sanitisedAppName)
if err != nil {
log.Fatal(err)
}
}
if newAppServer == "default" {
newAppServer = "local"
}
log.Info(i18n.G("%s created (version: %s)", appDomain, recipeVersion))
if len(secretsConfig) > 0 {
var (
hasSecretToGenerate bool
hasSecretToSkip bool
)
for _, secretConfig := range secretsConfig {
if secretConfig.SkipGenerate {
hasSecretToSkip = true
continue
}
hasSecretToGenerate = true
}
if hasSecretToGenerate && !generateSecrets {
log.Warn(i18n.G("%s requires secret generation before deploy, run \"abra app secret generate %s --all\"", recipe.Name, appDomain))
}
if hasSecretToSkip {
log.Warn(i18n.G("%s requires secret insertion before deploy (#generate=false)", recipe.Name))
}
}
if len(appSecrets) > 0 {
rows := [][]string{}
for k, v := range appSecrets {
rows = append(rows, []string{k, v})
}
overview := formatter.CreateOverview(i18n.G("SECRETS OVERVIEW"), rows)
fmt.Println(overview)
log.Warn(i18n.G(
"secrets are %s shown again, please save them %s",
formatter.BoldUnderlineStyle.Render("NOT"),
formatter.BoldUnderlineStyle.Render("NOW"),
))
}
app, err := app.Get(appDomain)
if err != nil {
log.Fatal(err)
}
if err := app.WriteRecipeVersion(recipeVersion, false); err != nil {
log.Fatal(i18n.G("writing recipe version failed: %s", err))
}
},
}
// ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
func ensureDomainFlag() error {
if domain == "" {
prompt := &survey.Input{
Message: "Specify app domain",
// AppSecrets represents all app secrest
type AppSecrets map[string]string
// createSecrets creates all secrets for a new app.
func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secret, sanitisedAppName string) (AppSecrets, error) {
// NOTE(d1): trim to match app.StackName() implementation
if len(sanitisedAppName) > config.MAX_SANITISED_APP_NAME_LENGTH {
log.Debug(i18n.G("trimming %s to %s to avoid runtime limits", sanitisedAppName, sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH]))
sanitisedAppName = sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH]
}
secrets, err := secret.GenerateSecrets(cl, secretsConfig, newAppServer)
if err != nil {
return nil, err
}
if saveInPass {
for secretName := range secrets {
secretValue := secrets[secretName]
if err := secret.PassInsertSecret(
secretValue,
secretName,
appDomain,
newAppServer,
); err != nil {
return nil, err
}
}
if err := survey.AskOne(prompt, &domain); err != nil {
}
return secrets, nil
}
// ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
func ensureDomainFlag(recipe recipePkg.Recipe, server string) error {
if appDomain == "" && !internal.NoInput {
prompt := &survey.Input{
Message: i18n.G("Specify app domain"),
Default: fmt.Sprintf("%s.%s", recipe.Name, server),
}
if err := survey.AskOne(prompt, &appDomain); err != nil {
return err
}
}
if appDomain == "" {
return errors.New(i18n.G("no domain provided"))
}
return nil
}
// promptForSecrets asks if we should generate secrets for a new app.
func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret) error {
if len(secretsConfig) == 0 {
log.Debug(i18n.G("%s has no secrets to generate, skipping...", recipeName))
return nil
}
if !generateSecrets && !internal.NoInput {
prompt := &survey.Confirm{
Message: i18n.G("Generate app secrets?"),
}
if err := survey.AskOne(prompt, &generateSecrets); err != nil {
return err
}
}
return nil
}
@@ -112,9 +335,15 @@ func ensureServerFlag() error {
return err
}
if newAppServer == "" {
if len(servers) == 1 {
newAppServer = servers[0]
log.Info(i18n.G("single server detected, choosing %s automatically", newAppServer))
return nil
}
if newAppServer == "" && !internal.NoInput {
prompt := &survey.Select{
Message: "Select app server:",
Message: i18n.G("Select app server:"),
Options: servers,
}
if err := survey.AskOne(prompt, &newAppServer); err != nil {
@@ -122,109 +351,66 @@ func ensureServerFlag() error {
}
}
if newAppServer == "" {
return errors.New(i18n.G("no server provided"))
}
return nil
}
// ensureServerFlag checks if the AppName flag was used. if not, asks the user for it.
func ensureAppNameFlag() error {
if newAppName == "" {
prompt := &survey.Input{
Message: "Specify app name:",
Default: config.SanitiseAppName(domain),
}
if err := survey.AskOne(prompt, &newAppName); err != nil {
return err
}
}
return nil
}
// createSecrets creates all secrets for a new app.
func createSecrets(sanitisedAppName string) (secrets, error) {
appEnvPath := path.Join(config.ABRA_DIR, "servers", newAppServer, fmt.Sprintf("%s.env", sanitisedAppName))
appEnv, err := config.ReadEnv(appEnvPath)
if err != nil {
return nil, err
}
secretEnvVars := secret.ReadSecretEnvVars(appEnv)
secrets, err := secret.GenerateSecrets(secretEnvVars, sanitisedAppName, newAppServer)
if err != nil {
return nil, err
}
if internal.Pass {
for secretName := range secrets {
secretValue := secrets[secretName]
if err := secret.PassInsertSecret(secretValue, secretName, sanitisedAppName, newAppServer); err != nil {
return nil, err
}
}
}
return secrets, nil
}
// action is the main command-line action for this package
func action(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
if err := config.EnsureAbraDirExists(); err != nil {
logrus.Fatal(err)
}
recipeMeta, err := catalogue.GetRecipeMeta(recipe.Name)
if err != nil {
logrus.Fatal(err)
}
latestVersion := recipeMeta.LatestVersion()
if err := recipePkg.EnsureVersion(recipe.Name, latestVersion); err != nil {
logrus.Fatal(err)
}
if err := ensureServerFlag(); err != nil {
logrus.Fatal(err)
}
if err := ensureDomainFlag(); err != nil {
logrus.Fatal(err)
}
if err := ensureAppNameFlag(); err != nil {
logrus.Fatal(err)
}
sanitisedAppName := config.SanitiseAppName(newAppName)
if len(sanitisedAppName) > 45 {
logrus.Fatalf("'%s' cannot be longer than 45 characters", sanitisedAppName)
}
logrus.Debugf("'%s' sanitised as '%s' for new app", newAppName, sanitisedAppName)
if err := config.CopyAppEnvSample(recipe.Name, newAppName, newAppServer); err != nil {
logrus.Fatal(err)
}
if internal.Secrets {
secrets, err := createSecrets(sanitisedAppName)
if err != nil {
logrus.Fatal(err)
}
secretCols := []string{"Name", "Value"}
secretTable := abraFormatter.CreateTable(secretCols)
for secret := range secrets {
secretTable.Append([]string{secret, secrets[secret]})
}
if len(secrets) > 0 {
defer secretTable.Render()
}
}
tableCol := []string{"Name", "Domain", "Type", "Server"}
table := abraFormatter.CreateTable(tableCol)
table.Append([]string{sanitisedAppName, domain, recipe.Name, newAppServer})
defer table.Render()
return nil
var (
newAppServer string
appDomain string
saveInPass bool
generateSecrets bool
)
func init() {
AppNewCommand.Flags().StringVarP(
&newAppServer,
i18n.G("server"),
i18n.G("s"),
"",
i18n.G("specify server for new app"),
)
AppNewCommand.RegisterFlagCompletionFunc(
i18n.G("server"),
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.ServerNameComplete()
},
)
AppNewCommand.Flags().StringVarP(
&appDomain,
i18n.G("domain"),
i18n.G("D"),
"",
i18n.G("domain name for app"),
)
AppNewCommand.Flags().BoolVarP(
&saveInPass,
i18n.G("pass"),
i18n.G("p"),
false,
i18n.G("store secrets in a local pass store"),
)
AppNewCommand.Flags().BoolVarP(
&generateSecrets,
i18n.G("secrets"),
i18n.G("S"),
false,
i18n.G("automatically generate secrets"),
)
AppNewCommand.Flags().BoolVarP(
&internal.Chaos,
i18n.G("chaos"),
i18n.G("C"),
false,
i18n.G("ignore uncommitted recipes changes"),
)
}
+190 -44
View File
@@ -2,70 +2,216 @@ package app
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
abraService "coopcloud.tech/abra/pkg/service"
stack "coopcloud.tech/abra/pkg/upstream/stack"
dockerFormatter "github.com/docker/cli/cli/command/formatter"
containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra"
)
var appPsCommand = &cli.Command{
Name: "ps",
Usage: "Check app status",
Aliases: []string{"p"},
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
// translators: `abra app ps` aliases. use a comma separated list of aliases
// with no spaces in between
var appPsAliases = i18n.G("p")
var AppPsCommand = &cobra.Command{
// translators: `app ps` command
Use: i18n.G("ps <domain> [flags]"),
Aliases: strings.Split(appPsAliases, ","),
// translators: Short description for `app ps` command
Short: i18n.G("Check app deployment status"),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
ctx := context.Background()
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
filters := filters.NewArgs()
filters.Add("name", app.StackName())
containers, err := cl.ContainerList(ctx, types.ContainerListOptions{Filters: filters})
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
tableCol := []string{"id", "image", "command", "created", "status", "ports", "names"}
table := abraFormatter.CreateTable(tableCol)
if !deployMeta.IsDeployed {
log.Fatal(i18n.G("%s is not deployed?", app.Name))
}
for _, container := range containers {
tableRow := []string{
abraFormatter.ShortenID(container.ID),
abraFormatter.RemoveSha(container.Image),
abraFormatter.Truncate(container.Command),
abraFormatter.HumanDuration(container.Created),
container.Status,
formatter.DisplayablePorts(container.Ports),
strings.Join(container.Names, ", "),
chaosVersion := config.CHAOS_DEFAULT
statuses, err := appPkg.GetAppStatuses([]appPkg.App{app}, true)
if statusMeta, ok := statuses[app.StackName()]; ok {
if isChaos, exists := statusMeta["chaos"]; exists && isChaos == "true" {
if cVersion, exists := statusMeta["chaosVersion"]; exists {
chaosVersion = cVersion
if strings.HasSuffix(chaosVersion, config.DIRTY_DEFAULT) {
chaosVersion = formatter.BoldDirtyDefault(chaosVersion)
}
}
}
table.Append(tableRow)
}
table.Render()
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
showPSOutput(app, cl, deployMeta.Version, chaosVersion)
},
}
// showPSOutput renders ps output.
func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chaosVersion string) {
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil {
log.Fatal(err)
return
}
deployOpts := stack.Deploy{
Composefiles: composeFiles,
Namespace: app.StackName(),
Prune: false,
ResolveImage: stack.ResolveImageAlways,
}
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
if err != nil {
log.Fatal(err)
return
}
services := compose.Services
sort.Slice(services, func(i, j int) bool {
return services[i].Name < services[j].Name
})
var rows [][]string
allContainerStats := make(map[string]map[string]string)
for _, service := range services {
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name))
containers, err := cl.ContainerList(context.Background(), containerTypes.ListOptions{Filters: filters})
if err != nil {
log.Fatal(err)
return
}
var containerStats map[string]string
if len(containers) == 0 {
containerStats = map[string]string{
"version": deployedVersion,
"chaos": chaosVersion,
"service": service.Name,
"image": i18n.G("unknown"),
"created": i18n.G("unknown"),
"status": i18n.G("unknown"),
"state": i18n.G("unknown"),
"ports": i18n.G("unknown"),
}
} else {
container := containers[0]
containerStats = map[string]string{
"version": deployedVersion,
"chaos": chaosVersion,
"service": abraService.ContainerToServiceName(container.Names, app.StackName()),
"image": formatter.RemoveSha(container.Image),
"created": formatter.HumanDuration(container.Created),
"status": container.Status,
"state": container.State,
"ports": dockerFormatter.DisplayablePorts(container.Ports),
}
}
allContainerStats[containerStats["service"]] = containerStats
// NOTE(d1): don't clobber these variables for --machine output
dVersion := deployedVersion
cVersion := chaosVersion
if containerStats["service"] != "app" {
// NOTE(d1): don't repeat info which only relevant for the "app" service
dVersion = ""
cVersion = ""
}
row := []string{
containerStats["service"],
containerStats["status"],
containerStats["image"],
dVersion,
cVersion,
}
rows = append(rows, row)
}
if internal.MachineReadable {
rendered, err := json.Marshal(allContainerStats)
if err != nil {
log.Fatal(i18n.G("unable to convert to JSON: %s", err))
}
fmt.Println(string(rendered))
return
}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
headers := []string{
i18n.G("SERVICE"),
i18n.G("STATUS"),
i18n.G("IMAGE"),
i18n.G("VERSION"),
i18n.G("CHAOS"),
}
table.
Headers(headers...).
Rows(rows...)
if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
}
func init() {
AppPsCommand.Flags().BoolVarP(
&internal.MachineReadable,
i18n.G("machine"),
i18n.G("m"),
false,
i18n.G("print machine-readable output"),
)
AppPsCommand.Flags().BoolVarP(
&internal.Chaos,
i18n.G("chaos"),
i18n.G("C"),
false,
i18n.G("ignore uncommitted recipes changes"),
)
}
+109 -112
View File
@@ -2,79 +2,108 @@ package app
import (
"context"
"fmt"
"os"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/spf13/cobra"
)
// Volumes stores the variable from VolumesFlag
var Volumes bool
// translators: `abra app remove` aliases. use a comma separated list of aliases with
// no spaces in between
var appRemoveAliases = i18n.G("rm")
// VolumesFlag is used to specify if volumes should be deleted when deleting an app
var VolumesFlag = &cli.BoolFlag{
Name: "volumes",
Value: false,
Destination: &Volumes,
}
var AppRemoveCommand = &cobra.Command{
// translators: `app remove` command
Use: i18n.G("remove <domain> [flags]"),
Aliases: strings.Split(appRemoveAliases, ","),
// translators: Short description for `app remove` command
Short: i18n.G("Remove all app data, locally and remotely"),
Long: i18n.G(`Remove everything related to an app which is already undeployed.
var appRemoveCommand = &cli.Command{
Name: "remove",
Usage: "Remove an already undeployed app",
Aliases: []string{"rm"},
Flags: []cli.Flag{
VolumesFlag,
internal.ForceFlag,
By default, it will prompt for confirmation before proceeding. All secrets,
volumes and the local app env file will be deleted.
Only run this command when you are sure you want to completely remove the app
and all associated app data. This is a destructive action, Be Careful!
If you would like to delete specific volumes or secrets, please use removal
sub-commands under "app volume" and "app secret" instead.
Please note, if you delete the local app env file without removing volumes and
secrets first, Abra will *not* be able to help you remove them afterwards.
To delete everything without prompt, use the "--force/-f" or the "--no-input/n"
flag.`),
Example: i18n.G(" abra app remove 1312.net"),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if !internal.Force && !internal.NoInput {
log.Warn(i18n.G("ALERTA ALERTA: deleting %s data and config (local/remote)", app.Name))
if !internal.Force {
response := false
prompt := &survey.Confirm{
Message: fmt.Sprintf("about to delete %s, are you sure?", app.Name),
}
prompt := &survey.Confirm{Message: i18n.G("are you sure?")}
if err := survey.AskOne(prompt, &response); err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
if !response {
logrus.Fatal("user aborted app removal")
log.Fatal(i18n.G("aborting as requested"))
}
}
appFiles, err := config.LoadAppFiles("")
if err != nil {
logrus.Fatal(err)
}
ctx := context.Background()
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
if !internal.Force {
// FIXME: only query for app we are interested in, not all of them!
statuses, err := config.GetAppStatuses(appFiles)
if err != nil {
logrus.Fatal(err)
}
if statuses[app.Name] == "deployed" {
logrus.Fatalf("'%s' is still deployed. Run \"abra app %s undeploy\" or pass --force", app.Name, app.Name)
}
log.Fatal(err)
}
fs := filters.NewArgs()
fs.Add("name", app.Name)
secretList, err := cl.SecretList(ctx, types.SecretListOptions{Filters: fs})
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
if deployMeta.IsDeployed {
log.Fatal(i18n.G("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name))
}
fs, err := app.Filters(false, false)
if err != nil {
log.Fatal(err)
}
configs, err := client.GetConfigs(cl, context.Background(), app.Server, fs)
if err != nil {
log.Fatal(err)
}
configNames := client.GetConfigNames(configs)
if len(configNames) > 0 {
if err := client.RemoveConfigs(cl, context.Background(), configNames, internal.Force); err != nil {
log.Fatal(i18n.G("removing configs failed: %s", err))
}
log.Info(i18n.G("%d config(s) removed successfully", len(configNames)))
} else {
log.Info(i18n.G("no configs to remove"))
}
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: fs})
if err != nil {
log.Fatal(err)
}
secrets := make(map[string]string)
@@ -86,85 +115,53 @@ var appRemoveCommand = &cli.Command{
}
if len(secrets) > 0 {
var secretNamesToRemove []string
if !internal.Force {
secretsPrompt := &survey.MultiSelect{
Message: "which secrets do you want to remove?",
Options: secretNames,
Default: secretNames,
}
if err := survey.AskOne(secretsPrompt, &secretNamesToRemove); err != nil {
logrus.Fatal(err)
}
}
for _, name := range secretNamesToRemove {
err := cl.SecretRemove(ctx, secrets[name])
for _, name := range secretNames {
err := cl.SecretRemove(context.Background(), secrets[name])
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
logrus.Info(fmt.Sprintf("secret: %s removed", name))
log.Info(i18n.G("secret: %s removed", name))
}
} else {
logrus.Info("no secrets to remove")
log.Info(i18n.G("no secrets to remove"))
}
volumeListOKBody, err := cl.VolumeList(ctx, fs)
volumeList := volumeListOKBody.Volumes
fs, err = app.Filters(false, true)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
var vols []string
for _, vol := range volumeList {
vols = append(vols, vol.Name)
volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, fs)
if err != nil {
log.Fatal(err)
}
volumeNames := client.GetVolumeNames(volumeList)
if len(vols) > 0 {
if Volumes {
var removeVols []string
if !internal.Force {
volumesPrompt := &survey.MultiSelect{
Message: "which volumes do you want to remove?",
Options: vols,
Default: vols,
}
if err := survey.AskOne(volumesPrompt, &removeVols); err != nil {
logrus.Fatal(err)
}
}
for _, vol := range removeVols {
err := cl.VolumeRemove(ctx, vol, internal.Force) // last argument is for force removing
if err != nil {
logrus.Fatal(err)
}
logrus.Info(fmt.Sprintf("volume %s removed", vol))
}
} else {
logrus.Info("no volumes were removed")
if len(volumeNames) > 0 {
err := client.RemoveVolumes(cl, context.Background(), volumeNames, internal.Force, 5)
if err != nil {
log.Fatal(i18n.G("removing volumes failed: %s", err))
}
log.Info(i18n.G("%d volume(s) removed successfully", len(volumeNames)))
} else {
logrus.Info("no volumes to remove")
log.Info(i18n.G("no volumes to remove"))
}
err = os.Remove(app.Path)
if err != nil {
logrus.Fatal(err)
if err = os.Remove(app.Path); err != nil {
log.Fatal(err)
}
logrus.Info(fmt.Sprintf("file: %s removed", app.Path))
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
log.Info(i18n.G("file: %s removed", app.Path))
},
}
func init() {
AppRemoveCommand.Flags().BoolVarP(
&internal.Force,
i18n.G("force"),
i18n.G("f"),
false,
i18n.G("perform action without further prompt"),
)
}
+174
View File
@@ -0,0 +1,174 @@
package app
import (
"context"
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/ui"
upstream "coopcloud.tech/abra/pkg/upstream/service"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types"
"github.com/spf13/cobra"
)
// translators: `abra app restart` aliases. use a comma separated list of aliases with
// no spaces in between
var appRestartAliases = i18n.G("re")
var AppRestartCommand = &cobra.Command{
// translators: `app restart` command
Use: i18n.G("restart <domain> [[service] | --all-services] [flags]"),
Aliases: strings.Split(appRestartAliases, ","),
// translators: Short description for `app restart` command
Short: i18n.G("Restart an app"),
Long: i18n.G(`This command restarts services within a deployed app.
Run "abra app ps <domain>" to see a list of service names.
Pass "--all-services/-a" to restart all services.`),
Example: i18n.G(` # restart a single app service
abra app restart 1312.net app
# restart all app services
abra app restart 1312.net -a`),
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
if !allServices {
return autocomplete.ServiceNameComplete(args[0])
}
return nil, cobra.ShellCompDirectiveDefault
default:
return nil, cobra.ShellCompDirectiveError
}
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
var serviceName string
if len(args) == 2 {
serviceName = args[1]
}
if serviceName == "" && !allServices {
log.Fatal(i18n.G("missing [service]"))
}
if serviceName != "" && allServices {
log.Fatal(i18n.G("cannot use [service] and --all-services/-a together"))
}
var serviceNames []string
if allServices {
var err error
serviceNames, err = appPkg.GetAppServiceNames(app.Name)
if err != nil {
log.Fatal(err)
}
} else {
serviceNames = append(serviceNames, serviceName)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
log.Fatal(err)
}
if !deployMeta.IsDeployed {
log.Fatal(i18n.G("%s is not deployed?", app.Name))
}
for _, serviceName := range serviceNames {
stackServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
service, _, err := cl.ServiceInspectWithRaw(
context.Background(),
stackServiceName,
types.ServiceInspectOptions{},
)
if err != nil {
log.Fatal(err)
}
log.Debug(i18n.G("attempting to scale %s to 0", stackServiceName))
if err := upstream.RunServiceScale(context.Background(), cl, stackServiceName, 0); err != nil {
log.Fatal(err)
}
f, err := app.Filters(true, false, serviceName)
if err != nil {
log.Fatal(err)
}
waitOpts := stack.WaitOpts{
Services: []ui.ServiceMeta{{Name: stackServiceName, ID: service.ID}},
AppName: app.Name,
ServerName: app.Server,
Filters: f,
NoInput: internal.NoInput,
NoLog: true,
Quiet: true,
}
if err := stack.WaitOnServices(cmd.Context(), cl, waitOpts); err != nil {
log.Fatal(err)
}
log.Debug(i18n.G("%s has been scaled to 0", stackServiceName))
log.Debug(i18n.G("attempting to scale %s to 1", stackServiceName))
if err := upstream.RunServiceScale(context.Background(), cl, stackServiceName, 1); err != nil {
log.Fatal(err)
}
if err := stack.WaitOnServices(cmd.Context(), cl, waitOpts); err != nil {
log.Fatal(err)
}
log.Debug(i18n.G("%s has been scaled to 1", stackServiceName))
log.Info(i18n.G("%s service successfully restarted", serviceName))
}
},
}
var allServices bool
func init() {
AppRestartCommand.Flags().BoolVarP(
&internal.Chaos,
i18n.G("chaos"),
i18n.G("C"),
false,
i18n.G("ignore uncommitted recipes changes"),
)
AppRestartCommand.Flags().BoolVarP(
&allServices,
i18n.G("all-services"),
i18n.GC("a", "app restart"),
false,
i18n.G("restart all services"),
)
}
+120 -57
View File
@@ -1,79 +1,142 @@
package app
import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra"
)
var restoreAllServices bool
var restoreAllServicesFlag = &cli.BoolFlag{
Name: "all",
Value: false,
Destination: &restoreAllServices,
Aliases: []string{"a"},
Usage: "Restore all services",
}
// translators: `abra app restore` aliases. use a comma separated list of
// aliases with no spaces in between
var appRestoreAliases = i18n.G("rs")
var appRestoreCommand = &cli.Command{
Name: "restore",
Usage: "Restore an app from a backup",
Aliases: []string{"r"},
Flags: []cli.Flag{restoreAllServicesFlag},
ArgsUsage: "<service> [<backup file>]",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
var AppRestoreCommand = &cobra.Command{
// translators: `app restore` command
Use: i18n.G("restore <domain> [flags]"),
Aliases: strings.Split(appRestoreAliases, ","),
// translators: Short description for `app restore` command
Short: i18n.G("Restore a snapshot"),
Long: i18n.G(`Snapshots are restored while apps are deployed.
if c.Args().Len() > 1 && restoreAllServices {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use <service>/<backup file> and '--all' together"))
Some restore scenarios may require service / app restarts.`),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
abraSh := path.Join(config.ABRA_DIR, "apps", app.Type, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("'%s' does not exist?", abraSh)
}
logrus.Fatal(err)
}
sourceCmd := fmt.Sprintf("source %s", abraSh)
execCmd := "abra_restore"
if !restoreAllServices {
serviceName := c.Args().Get(1)
if serviceName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no service(s) target provided"))
}
execCmd = fmt.Sprintf("abra_restore_%s", serviceName)
}
bytes, err := ioutil.ReadFile(abraSh)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
if !strings.Contains(string(bytes), execCmd) {
logrus.Fatalf("%s doesn't have a '%s' function", app.Type, execCmd)
log.Fatal(err)
}
backupFile := c.Args().Get(2)
if backupFile != "" {
execCmd = fmt.Sprintf("%s %s", execCmd, backupFile)
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
}
sourceAndExec := fmt.Sprintf("%s; %s", sourceCmd, execCmd)
cmd := exec.Command("bash", "-c", sourceAndExec)
if err := internal.RunCmd(cmd); err != nil {
logrus.Fatal(err)
execEnv := []string{
fmt.Sprintf("SERVICE=%s", app.Domain),
"MACHINE_LOGS=true",
}
return nil
if snapshot != "" {
log.Debug(i18n.G("including SNAPSHOT=%s in backupbot exec invocation", snapshot))
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
}
if targetPath != "" {
log.Debug(i18n.G("including TARGET=%s in backupbot exec invocation", targetPath))
execEnv = append(execEnv, fmt.Sprintf("TARGET=%s", targetPath))
}
if internal.NoInput {
log.Debug(i18n.G("including NONINTERACTIVE=%v in backupbot exec invocation", internal.NoInput))
execEnv = append(execEnv, fmt.Sprintf("NONINTERACTIVE=%v", internal.NoInput))
}
if len(volumes) > 0 {
allVolumes := strings.Join(volumes, ",")
log.Debug(i18n.G("including VOLUMES=%s in backupbot exec invocation", allVolumes))
execEnv = append(execEnv, fmt.Sprintf("VOLUMES=%s", allVolumes))
}
if len(services) > 0 {
allServices := strings.Join(services, ",")
log.Debug(i18n.G("including CONTAINER=%s in backupbot exec invocation", allServices))
execEnv = append(execEnv, fmt.Sprintf("CONTAINER=%s", allServices))
}
if hooks {
log.Debug(i18n.G("including NO_COMMANDS=%v in backupbot exec invocation", false))
execEnv = append(execEnv, fmt.Sprintf("NO_COMMANDS=%v", false))
}
if _, err := internal.RunBackupCmdRemote(cl, "restore", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
}
},
}
var (
targetPath string
hooks bool
services []string
volumes []string
)
func init() {
AppRestoreCommand.Flags().StringVarP(
&targetPath,
i18n.G("target"),
i18n.G("t"),
"/",
i18n.G("target path"),
)
AppRestoreCommand.Flags().StringArrayVarP(
&services,
i18n.G("services"),
i18n.G("s"),
[]string{},
i18n.G("restore specific services"),
)
AppRestoreCommand.Flags().StringArrayVarP(
&volumes,
i18n.G("volumes"),
i18n.G("v"),
[]string{},
i18n.G("restore specific volumes"),
)
AppRestoreCommand.Flags().BoolVarP(
&hooks,
i18n.G("hooks"),
i18n.G("H"),
false,
i18n.G("enable pre/post-hook command execution"),
)
AppRestoreCommand.Flags().BoolVarP(
&internal.Chaos,
i18n.G("chaos"),
i18n.G("C"),
false,
i18n.G("ignore uncommitted recipes changes"),
)
}
+351 -51
View File
@@ -1,82 +1,382 @@
package app
import (
"errors"
"fmt"
"strings"
"context"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/deploy"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/lint"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra"
)
var appRollbackCommand = &cli.Command{
Name: "rollback",
Usage: "Roll an app back to a previous version",
Aliases: []string{"r"},
ArgsUsage: "[<version>]",
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
// translators: `abra app rollback` aliases. use a comma separated list of
// aliases with no spaces in between
var appRollbackAliases = i18n.G("rl")
var AppRollbackCommand = &cobra.Command{
// translators: `app rollback` command
Use: i18n.G("rollback <domain> [version] [flags]"),
Aliases: strings.Split(appRollbackAliases, ","),
// translators: Short description for `app rollback` command
Short: i18n.G("Roll an app back to a previous version"),
Long: i18n.G(`This command rolls an app back to a previous version.
Unlike "abra app deploy", chaos operations are not supported here. Only recipe
versions are supported values for "[version]".
It is possible to "--force/-f" an downgrade if you want to re-deploy a specific
version.
Only the deployed version is consulted when trying to determine what downgrades
are available. The live deployment version is the "source of truth" in this
case. The stored .env version is not consulted.
A downgrade can be destructive, please ensure you have a copy of your app data
beforehand. See "abra app backup" for more.`),
Example: i18n.G(` # standard rollback
abra app rollback 1312.net
# rollback to specific version
abra app rollback 1312.net 2.0.0+1.2.3`),
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
app, err := appPkg.Get(args[0])
if err != nil {
return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
}
return autocomplete.RecipeVersionComplete(app.Recipe.Name)
default:
return nil, cobra.ShellCompDirectiveError
}
},
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Run: func(cmd *cobra.Command, args []string) {
var (
downgradeWarnMessages []string
chosenDowngrade string
availableDowngrades []string
)
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
ctx := context.Background()
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
recipeMeta, err := catalogue.GetRecipeMeta(app.Type)
deployMeta, err := ensureDeployed(cl, app)
if err != nil {
logrus.Fatal(err)
}
if len(recipeMeta.Versions) == 0 {
logrus.Fatalf("no catalogue versions available for '%s'", app.Type)
log.Fatal(err)
}
deployedVersions, isDeployed, err := appPkg.DeployedVersions(ctx, cl, app)
if err := lint.LintForErrors(app.Recipe); err != nil {
log.Fatal(err)
}
versions, err := app.Recipe.Tags()
if err != nil {
logrus.Fatal(err)
}
if !isDeployed {
logrus.Fatalf("'%s' is not deployed?", app.Name)
}
if _, exists := deployedVersions["app"]; !exists {
logrus.Fatalf("no versioned 'app' service for '%s', cannot determine version", app.Name)
log.Fatal(err)
}
version := c.Args().Get(1)
if version == "" {
// TODO:
// using deployedVersions["app"], get index+1 version from catalogue
// otherwise bail out saying there is nothing to rollback to
// NOTE(d1): we've no idea what the live deployment version is, so every
// possible downgrade can be shown. it's up to the user to make the choice
if deployMeta.Version == config.UNKNOWN_DEFAULT {
availableDowngrades = versions
}
if len(args) == 2 && args[1] != "" {
chosenDowngrade = args[1]
if err := validateDowngradeVersionArg(chosenDowngrade, app, deployMeta); err != nil {
log.Fatal(err)
}
availableDowngrades = append(availableDowngrades, chosenDowngrade)
}
if deployMeta.Version != config.UNKNOWN_DEFAULT && chosenDowngrade == "" {
downgradeAvailable, err := ensureDowngradesAvailable(versions, &availableDowngrades, deployMeta)
if err != nil {
log.Fatal(err)
}
if !downgradeAvailable {
log.Info(i18n.G("no available downgrades"))
return
}
}
if internal.Force || internal.NoInput || chosenDowngrade != "" {
if len(availableDowngrades) > 0 {
chosenDowngrade = availableDowngrades[len(availableDowngrades)-1]
}
} else {
// TODO
// ensure this version is listed in the catalogue
// ensure this version is "older" (lower down in the list)
if err := chooseDowngrade(availableDowngrades, deployMeta, &chosenDowngrade); err != nil {
log.Fatal(err)
}
}
// TODO
// display table of existing state and expected state and prompt
// run the deployment with this target version!
if internal.Force &&
chosenDowngrade == "" &&
deployMeta.Version != config.UNKNOWN_DEFAULT {
chosenDowngrade = deployMeta.Version
}
logrus.Fatal("command not implemented yet, coming soon TM")
if chosenDowngrade == "" {
log.Fatal(i18n.G("unknown deployed version, unable to downgrade"))
}
return nil
log.Debug(i18n.G("choosing %s as version to rollback", chosenDowngrade))
if _, err := app.Recipe.EnsureVersion(chosenDowngrade); err != nil {
log.Fatal(err)
}
if err := deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
log.Fatal(err)
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil {
log.Fatal(err)
}
stackName := app.StackName()
deployOpts := stack.Deploy{
Composefiles: composeFiles,
Namespace: stackName,
Prune: false,
ResolveImage: stack.ResolveImageAlways,
Detach: false,
SendRegistryAuth: true,
}
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
if err != nil {
log.Fatal(err)
}
newRecipeWithDowngradeVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, chosenDowngrade)
appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithDowngradeVersion)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
if internal.Chaos {
appPkg.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
}
// Gather secrets
secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, internal.ShowUnchanged)
if err != nil {
log.Fatal(err)
}
// Gather configs
configInfo, err := deploy.GatherConfigsForDeploy(cl, app, compose, app.Env, internal.ShowUnchanged)
if err != nil {
log.Fatal(err)
}
// Gather images
imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose, internal.ShowUnchanged)
if err != nil {
log.Fatal(err)
}
deployedVersion := deployMeta.Version
if deployMeta.IsChaos {
deployedVersion = deployMeta.ChaosVersion
}
// NOTE(d1): no release notes implemeneted for rolling back
if err := internal.DeployOverview(
app,
deployedVersion,
chosenDowngrade,
"",
downgradeWarnMessages,
secretInfo,
configInfo,
imageInfo,
); err != nil {
log.Fatal(err)
}
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
if err != nil {
log.Fatal(err)
}
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
if err != nil {
log.Fatal(err)
}
f, err := app.Filters(true, false, serviceNames...)
if err != nil {
log.Fatal(err)
}
if err := stack.RunDeploy(
cl,
deployOpts,
compose,
stackName,
app.Server,
internal.DontWaitConverge,
internal.NoInput,
f,
); err != nil {
log.Fatal(err)
}
if err := app.WriteRecipeVersion(chosenDowngrade, false); err != nil {
log.Fatal(i18n.G("writing recipe version failed: %s", err))
}
},
}
// chooseDowngrade prompts the user to choose an downgrade interactively.
func chooseDowngrade(
availableDowngrades []string,
deployMeta stack.DeployMeta,
chosenDowngrade *string,
) error {
msg := i18n.G("please select a downgrade (version: %s):", deployMeta.Version)
if deployMeta.IsChaos {
chaosVersion := formatter.BoldDirtyDefault(deployMeta.ChaosVersion)
msg = i18n.G(
"please select a downgrade (version: %s, chaos: %s):",
deployMeta.Version,
chaosVersion,
)
}
prompt := &survey.Select{
Message: msg,
Options: internal.SortVersionsDesc(availableDowngrades),
}
if err := survey.AskOne(prompt, chosenDowngrade); err != nil {
return err
}
return nil
}
// validateDownpgradeVersionArg validates the specific version.
func validateDowngradeVersionArg(
specificVersion string,
app appPkg.App,
deployMeta stack.DeployMeta,
) error {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return errors.New(i18n.G("current deployment '%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name))
}
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil {
return errors.New(i18n.G("'%s' is not a known version for %s", specificVersion, app.Recipe.Name))
}
if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) &&
!parsedSpecificVersion.Equals(parsedDeployedVersion) {
return errors.New(i18n.G("%s is not a downgrade for %s?", deployMeta.Version, specificVersion))
}
if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
return errors.New(i18n.G("%s is not a downgrade for %s?", deployMeta.Version, specificVersion))
}
return nil
}
// ensureDowngradesAvailable ensures that there are available downgrades.
func ensureDowngradesAvailable(
versions []string,
availableDowngrades *[]string,
deployMeta stack.DeployMeta,
) (bool, error) {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return false, err
}
for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
return false, err
}
if parsedVersion.IsLessThan(parsedDeployedVersion) &&
!(parsedVersion.Equals(parsedDeployedVersion)) {
*availableDowngrades = append(*availableDowngrades, version)
}
}
if len(*availableDowngrades) == 0 && !internal.Force {
return false, nil
}
return true, nil
}
func init() {
AppRollbackCommand.Flags().BoolVarP(
&internal.Force,
i18n.G("force"),
i18n.G("f"),
false,
i18n.G("perform action without further prompt"),
)
AppRollbackCommand.Flags().BoolVarP(
&internal.NoDomainChecks,
i18n.G("no-domain-checks"),
i18n.G("D"),
false,
i18n.G("disable public DNS checks"),
)
AppRollbackCommand.Flags().BoolVarP(
&internal.DontWaitConverge,
i18n.G("no-converge-checks"),
i18n.G("c"),
false,
i18n.G("disable converge logic checks"),
)
AppRollbackCommand.Flags().BoolVarP(
&internal.ShowUnchanged,
i18n.G("show-unchanged"),
i18n.G("U"),
false,
i18n.G("show all configs & images, including unchanged ones"),
)
}
+78 -81
View File
@@ -2,124 +2,121 @@ package app
import (
"context"
"errors"
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/client/container"
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/spf13/cobra"
)
var user string
var userFlag = &cli.StringFlag{
Name: "user",
Value: "",
Destination: &user,
}
// translators: `abra app run` aliases. use a comma separated list of aliases
// with no spaces in between
var appRunAliases = i18n.G("r")
var noTTY bool
var noTTYFlag = &cli.BoolFlag{
Name: "no-tty",
Value: false,
Destination: &noTTY,
}
var AppRunCommand = &cobra.Command{
// translators: `app run` command
Use: i18n.G("run <domain> <service> <cmd> [[args] [flags] | [flags] -- [args]]"),
Aliases: strings.Split(appRunAliases, ","),
// translators: Short description for `app run` command
Short: i18n.G("Run a command inside a service container"),
Example: i18n.G(` # run <cmd> with args/flags
abra app run 1312.net app -- ls -lha
var appRunCommand = &cli.Command{
Name: "run",
Flags: []cli.Flag{
noTTYFlag,
userFlag,
# run <cmd> without args/flags
abra app run 1312.net app bash --user nobody
# run <cmd> with both kinds of args/flags
abra app run 1312.net app --user nobody -- ls -lha`),
Args: cobra.MinimumNArgs(3),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
return autocomplete.ServiceNameComplete(args[0])
case 2:
return autocomplete.CommandNameComplete(args[0])
default:
return nil, cobra.ShellCompDirectiveError
}
},
Aliases: []string{"r"},
ArgsUsage: "<service> <args>...",
Usage: "Run a command in a service container",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if c.Args().Len() < 2 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <service> provided?"))
}
if c.Args().Len() < 3 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <args> provided?"))
}
ctx := context.Background()
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
serviceName := c.Args().Get(1)
serviceName := args[1]
stackAndServiceName := fmt.Sprintf("^%s_%s", app.StackName(), serviceName)
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("%s_%s", app.StackName(), serviceName))
filters.Add("name", stackAndServiceName)
containers, err := cl.ContainerList(ctx, types.ContainerListOptions{Filters: filters})
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, false)
if err != nil {
logrus.Fatal(err)
}
if len(containers) > 1 {
logrus.Fatalf("expected 1 container but got %d", len(containers))
log.Fatal(err)
}
cmd := c.Args().Slice()[2:]
execCreateOpts := types.ExecConfig{
userCmd := args[2:]
execCreateOpts := containertypes.ExecOptions{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: cmd,
Cmd: userCmd,
Detach: false,
Tty: true,
}
if user != "" {
execCreateOpts.User = user
if runAsUser != "" {
execCreateOpts.User = runAsUser
}
if noTTY {
execCreateOpts.Tty = false
}
// FIXME: an absolutely monumental hack to instantiate another command-line
// client withing our command-line client so that we pass something down
// the tubes that satisfies the necessary interface requirements. We should
// refactor our vendored container code to not require all this cruft. For
// now, It Works.
dcli, err := command.NewDockerCli()
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
if err := container.RunExec(dcli, cl, containers[0].ID, &execCreateOpts); err != nil {
logrus.Fatal(err)
}
return nil
},
BashComplete: func(c *cli.Context) {
switch c.NArg() {
case 0:
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
for _, a := range appNames {
fmt.Println(a)
}
case 1:
appName := c.Args().First()
serviceNames, err := config.GetAppServiceNames(appName)
if err != nil {
logrus.Warn(err)
}
for _, s := range serviceNames {
fmt.Println(s)
}
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
log.Fatal(err)
}
},
}
var (
noTTY bool
runAsUser string
)
func init() {
AppRunCommand.Flags().BoolVarP(&noTTY,
i18n.G("no-tty"),
i18n.G("t"),
false,
i18n.G("do not request a TTY"),
)
AppRunCommand.Flags().StringVarP(
&runAsUser,
i18n.G("user"),
i18n.G("u"),
"",
i18n.G("run command as user"),
)
}
+574 -184
View File
@@ -4,212 +4,406 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
"sort"
"strconv"
"strings"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/secret"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra"
)
var allSecrets bool
var allSecretsFlag = &cli.BoolFlag{
Name: "all",
Aliases: []string{"A"},
Value: false,
Destination: &allSecrets,
Usage: "Generate all secrets",
}
// translators: `abra app secret generate` aliases. use a comma separated list of aliases with
// no spaces in between
var appSecretGenerateAliases = i18n.G("g")
var appSecretGenerateCommand = &cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate secrets",
ArgsUsage: "<secret> <version>",
Flags: []cli.Flag{allSecretsFlag, internal.PassFlag},
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if c.Args().Len() == 1 && !allSecrets {
err := errors.New("missing arguments <secret>/<version> or '--all'")
internal.ShowSubcommandHelpAndError(c, err)
}
if c.Args().Get(1) != "" && allSecrets {
err := errors.New("cannot use '<secret> <version>' and '--all' together")
internal.ShowSubcommandHelpAndError(c, err)
}
secretsToCreate := make(map[string]string)
secretEnvVars := secret.ReadSecretEnvVars(app.Env)
if allSecrets {
secretsToCreate = secretEnvVars
} else {
secretName := c.Args().Get(1)
secretVersion := c.Args().Get(2)
matches := false
for sec := range secretEnvVars {
parsed := secret.ParseSecretEnvVarName(sec)
if secretName == parsed {
secretsToCreate[sec] = secretVersion
}
}
if !matches {
logrus.Fatalf("'%s' doesn't exist in the env config?", secretName)
var AppSecretGenerateCommand = &cobra.Command{
// translators: `app secret generate` command
Use: i18n.G("generate <domain> [[secret] [version] | --all] [flags]"),
Aliases: strings.Split(appSecretGenerateAliases, ","),
// translators: Short description for `app secret generate` command
Short: i18n.G("Generate secrets"),
Args: cobra.RangeArgs(1, 3),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string,
) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
app, err := appPkg.Get(args[0])
if err != nil {
return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
}
return autocomplete.SecretComplete(app.Recipe.Name)
default:
return nil, cobra.ShellCompDirectiveDefault
}
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
secretVals, err := secret.GenerateSecrets(secretsToCreate, app.StackName(), app.Server)
if len(args) <= 2 && !generateAllSecrets {
log.Fatal(i18n.G("missing arguments [secret]/[version] or '--all'"))
}
if len(args) > 2 && generateAllSecrets {
log.Fatal(i18n.G("cannot use '[secret] [version]' and '--all' together"))
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
if internal.Pass {
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
if err != nil {
log.Fatal(err)
}
if !generateAllSecrets {
secretName := args[1]
secretVersion := args[2]
s, ok := secrets[secretName]
if !ok {
log.Fatal(i18n.G("%s doesn't exist in the env config?", secretName))
}
s.Version = secretVersion
secrets = map[string]secret.Secret{
secretName: s,
}
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
secretVals, err := secret.GenerateSecrets(cl, secrets, app.Server)
if err != nil {
log.Fatal(err)
}
if storeInPass {
for name, data := range secretVals {
if err := secret.PassInsertSecret(data, name, app.StackName(), app.Server); err != nil {
logrus.Fatal(err)
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
log.Fatal(err)
}
}
}
if len(secretVals) == 0 {
logrus.Warn("no secrets generated")
log.Warn(i18n.G("no secrets generated"))
os.Exit(1)
}
tableCol := []string{"name", "value"}
table := abraFormatter.CreateTable(tableCol)
for name, val := range secretVals {
table.Append([]string{name, val})
headers := []string{i18n.G("NAME"), i18n.G("VALUE")}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table.Render()
logrus.Warn("generated secrets are not shown again, please take note of them *now*")
return nil
table.Headers(headers...)
var rows [][]string
for name, val := range secretVals {
row := []string{name, val}
rows = append(rows, row)
table.Row(row...)
}
if internal.MachineReadable {
out, err := formatter.ToJSON(headers, rows)
if err != nil {
log.Fatal(i18n.G("unable to render to JSON: %s", err))
}
fmt.Println(out)
return
}
if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
log.Warn(i18n.G(
"generated secrets %s shown again, please take note of them %s",
formatter.BoldStyle.Render(i18n.G("NOT")),
formatter.BoldStyle.Render(i18n.G("NOW")),
))
},
}
var appSecretInsertCommand = &cli.Command{
Name: "insert",
Aliases: []string{"i"},
Usage: "Insert secret",
Flags: []cli.Flag{internal.PassFlag},
ArgsUsage: "<secret> <version> <data>",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
// translators: `abra app secret insert` aliases. use a comma separated list of aliases with
// no spaces in between
var appSecretInsertAliases = i18n.G("i")
if c.Args().Len() != 4 {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments?"))
var AppSecretInsertCommand = &cobra.Command{
// translators: `app secret insert` command
Use: i18n.G("insert <domain> <secret> <version> [<data>] [flags]"),
Aliases: strings.Split(appSecretInsertAliases, ","),
// translators: Short description for `app secret insert` command
Short: i18n.G("Insert secret"),
Long: i18n.G(`This command inserts a secret into an app environment.
Arbitrary secret insertion is not supported. Secrets that are inserted must
match those configured in the recipe beforehand.
This command can be useful when you want to manually generate secrets for an app
environment. Typically, you can let Abra generate them for you on app creation
(see "abra app new --secrets/-S" for more).`),
Example: i18n.G(` # insert regular secret
abra app secret insert 1312.net my_secret v1 mySuperSecret
# insert secret as file
abra app secret insert 1312.net my_secret v1 secret.txt -f
# insert secret from stdin
echo "mmySuperSecret" | abra app secret insert 1312.net my_secret v1`),
Args: cobra.MinimumNArgs(3),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string,
) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
app, err := appPkg.Get(args[0])
if err != nil {
return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
}
return autocomplete.SecretComplete(app.Recipe.Name)
default:
return nil, cobra.ShellCompDirectiveDefault
}
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
name := c.Args().Get(1)
version := c.Args().Get(2)
data := c.Args().Get(3)
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
name := args[1]
version := args[2]
data, err := readSecretData(args)
if err != nil {
log.Fatal(err)
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil {
log.Fatal(err)
}
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
if err != nil {
log.Fatal(err)
}
var isRecipeSecret bool
for secretName := range secrets {
if secretName == name {
isRecipeSecret = true
}
}
if !isRecipeSecret {
log.Fatal(i18n.G("no secret %s available for recipe %s?", name, app.Recipe.Name))
}
if insertFromFile {
raw, err := os.ReadFile(data)
if err != nil {
log.Fatal(i18n.G("reading secret from file: %s", err))
}
data = string(raw)
}
if trimInput {
data = strings.TrimSpace(data)
}
secretName := fmt.Sprintf("%s_%s_%s", app.StackName(), name, version)
if err := client.StoreSecret(secretName, data, app.Server); err != nil {
logrus.Fatal(err)
if err := client.StoreSecret(cl, secretName, data); err != nil {
log.Fatal(err)
}
if internal.Pass {
if err := secret.PassInsertSecret(data, name, app.StackName(), app.Server); err != nil {
logrus.Fatal(err)
log.Info(i18n.G("%s successfully stored on server", secretName))
if storeInPass {
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
log.Fatal(err)
}
}
return nil
},
}
var appSecretRmCommand = &cli.Command{
Name: "remove",
Usage: "Remove a secret",
Aliases: []string{"rm"},
Flags: []cli.Flag{allSecretsFlag, internal.PassFlag},
ArgsUsage: "<secret>",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
func readSecretData(args []string) (string, error) {
if len(args) == 4 {
return args[3], nil
}
if c.Args().Get(1) != "" && allSecrets {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<secret>' and '--all' together"))
if len(args) != 3 {
return "", errors.New(i18n.G("need 3 or 4 arguments"))
}
// First check if data is provided by stdin
fi, err := os.Stdin.Stat()
if err != nil {
return "", err
}
if fi.Mode()&os.ModeNamedPipe != 0 {
// Can't insert from stdin and read from file
if insertFromFile {
return "", errors.New(i18n.G("can not insert from file and read from stdin"))
}
if c.Args().Get(1) == "" && !allSecrets {
internal.ShowSubcommandHelpAndError(c, errors.New("no secret(s) specified?"))
}
ctx := context.Background()
cl, err := client.New(app.Server)
log.Debug(i18n.G("reading secret data from stdin"))
bytes, err := io.ReadAll(os.Stdin)
if err != nil {
logrus.Fatal(err)
return "", errors.New(i18n.G("reading data from stdin: %s", err))
}
filters := filters.NewArgs()
filters.Add("name", app.StackName())
secretList, err := cl.SecretList(ctx, types.SecretListOptions{Filters: filters})
if err != nil {
logrus.Fatal(err)
}
return string(bytes), nil
}
if internal.NoInput {
return "", errors.New(i18n.G("must provide <data> argument if --no-input is passed"))
}
secretToRm := c.Args().Get(1)
for _, cont := range secretList {
secretName := cont.Spec.Annotations.Name
parsed := secret.ParseGeneratedSecretName(secretName, app)
if allSecrets {
if err := cl.SecretRemove(ctx, secretName); err != nil {
logrus.Fatal(err)
}
if internal.Pass {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
logrus.Fatal(err)
}
}
} else {
if parsed == secretToRm {
if err := cl.SecretRemove(ctx, secretName); err != nil {
logrus.Fatal(err)
}
if internal.Pass {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
logrus.Fatal(err)
}
}
}
}
log.Debug(i18n.G("secret data not provided on command-line or stdin, prompting"))
var prompt survey.Prompt
if !insertFromFile {
prompt = &survey.Password{
Message: i18n.G("specify secret value"),
}
return nil
},
} else {
prompt = &survey.Input{
Message: i18n.G("specify secret file"),
}
}
var data string
if err := survey.AskOne(prompt, &data); err != nil {
return "", err
}
return data, nil
}
var appSecretLsCommand = &cli.Command{
Name: "list",
Usage: "List all secrets",
Aliases: []string{"ls"},
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
secrets := secret.ReadSecretEnvVars(app.Env)
// secretRm removes a secret.
func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string) error {
if err := cl.SecretRemove(context.Background(), secretName); err != nil {
return err
}
tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"}
table := abraFormatter.CreateTable(tableCol)
log.Info(i18n.G("deleted %s successfully from server", secretName))
ctx := context.Background()
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
if removeFromPass {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
return err
}
filters := filters.NewArgs()
filters.Add("name", app.StackName())
secretList, err := cl.SecretList(ctx, types.SecretListOptions{Filters: filters})
log.Info(i18n.G("deleted %s successfully from local pass store", secretName))
}
return nil
}
// translators: `abra app secret remove` aliases. use a comma separated list of aliases with
// no spaces in between
var appSecretRemoveAliases = i18n.G("rm")
var AppSecretRmCommand = &cobra.Command{
// translators: `app secret remove` command
Use: i18n.G("remove <domain> [[secret] | --all] [flags]"),
Aliases: strings.Split(appSecretRemoveAliases, ","),
// translators: Short description for `app secret remove` command
Short: i18n.G("Remove a secret"),
Long: i18n.G(`This command removes a secret from an app environment.
Arbitrary secret removal is not supported. Secrets that are removed must
match those configured in the recipe beforehand.`),
Example: i18n.G(" abra app secret rm 1312.net oauth_key"),
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string,
) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
if !rmAllSecrets {
app, err := appPkg.Get(args[0])
if err != nil {
return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
}
return autocomplete.SecretComplete(app.Recipe.Name)
}
return nil, cobra.ShellCompDirectiveDefault
default:
return nil, cobra.ShellCompDirectiveError
}
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
if err != nil {
log.Fatal(err)
}
if len(args) == 2 && rmAllSecrets {
log.Fatal(i18n.G("cannot use [secret] and --all/-a together"))
}
if len(args) != 2 && !rmAllSecrets {
log.Fatal(i18n.G("no secret(s) specified?"))
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
filters, err := app.Filters(false, false)
if err != nil {
log.Fatal(err)
}
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters})
if err != nil {
log.Fatal(err)
}
remoteSecretNames := make(map[string]bool)
@@ -217,47 +411,243 @@ var appSecretLsCommand = &cli.Command{
remoteSecretNames[cont.Spec.Annotations.Name] = true
}
for sec := range secrets {
createdRemote := false
secretName := secret.ParseSecretEnvVarName(sec)
secVal, err := secret.ParseSecretEnvVarValue(secrets[sec])
if err != nil {
logrus.Fatal(err)
}
secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, secVal.Version)
if _, ok := remoteSecretNames[secretRemoteName]; ok {
createdRemote = true
}
tableRow := []string{secretName, secVal.Version, secretRemoteName, strconv.FormatBool(createdRemote)}
table.Append(tableRow)
var secretToRm string
if len(args) == 2 {
secretToRm = args[1]
}
table.Render()
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
match := false
for secretName, val := range secrets {
secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, val.Version)
if _, ok := remoteSecretNames[secretRemoteName]; ok {
if secretToRm != "" {
if secretName == secretToRm {
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
log.Fatal(err)
}
return
}
} else {
match = true
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
log.Fatal(err)
}
}
}
}
if c.NArg() > 0 {
if !match && secretToRm != "" {
log.Fatal(i18n.G("%s doesn't exist on server?", secretToRm))
}
if !match {
log.Fatal(i18n.G("no secrets to remove?"))
}
},
}
// translators: `abra app secret ls` aliases. use a comma separated list of aliases with
// no spaces in between
var appSecretLsAliases = i18n.G("ls")
var AppSecretLsCommand = &cobra.Command{
// translators: `app secret list` command
Use: i18n.G("list <domain>"),
Aliases: strings.Split(appSecretLsAliases, ","),
// translators: Short description for `app secret list` command
Short: i18n.G("List all secrets"),
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string,
) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
headers := []string{i18n.G("NAME"), i18n.G("VERSION"), i18n.G("GENERATED NAME"), i18n.G("CREATED ON SERVER")}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table.Headers(headers...)
secStats, err := secret.PollSecretsStatus(cl, app)
if err != nil {
log.Fatal(err)
}
// Sort secrets to ensure reproducible output
sort.Slice(secStats, func(i, j int) bool {
return secStats[i].LocalName < secStats[j].LocalName
})
var rows [][]string
for _, secStat := range secStats {
row := []string{
secStat.LocalName,
secStat.Version,
secStat.RemoteName,
strconv.FormatBool(secStat.CreatedOnRemote),
}
rows = append(rows, row)
table.Row(row...)
}
if len(rows) > 0 {
if internal.MachineReadable {
out, err := formatter.ToJSON(headers, rows)
if err != nil {
log.Fatal(i18n.G("unable to render to JSON: %s", err))
}
fmt.Println(out)
return
}
if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
return
}
for _, a := range appNames {
fmt.Println(a)
}
log.Warn(i18n.G("no secrets stored for %s", app.Name))
},
}
var appSecretCommand = &cli.Command{
Name: "secret",
Aliases: []string{"s"},
Usage: "Manage app secrets",
ArgsUsage: "<command>",
Subcommands: []*cli.Command{
appSecretGenerateCommand,
appSecretInsertCommand,
appSecretRmCommand,
appSecretLsCommand,
},
var AppSecretCommand = &cobra.Command{
// translators: `app secret` command group
Use: i18n.G("secret [cmd] [args] [flags]"),
Aliases: []string{i18n.G("s")},
// translators: Short description for `app secret` command group
Short: i18n.G("Manage app secrets"),
}
var (
storeInPass bool
insertFromFile bool
trimInput bool
rmAllSecrets bool
generateAllSecrets bool
removeFromPass bool
)
func init() {
AppSecretGenerateCommand.Flags().BoolVarP(
&internal.MachineReadable,
i18n.G("machine"),
i18n.G("m"),
false,
i18n.G("print machine-readable output"),
)
AppSecretGenerateCommand.Flags().BoolVarP(
&storeInPass,
i18n.G("pass"),
i18n.G("p"),
false,
i18n.G("store generated secrets in a local pass store"),
)
AppSecretGenerateCommand.Flags().BoolVarP(
&internal.Chaos,
i18n.G("chaos"),
i18n.G("C"),
false,
i18n.G("ignore uncommitted recipes changes"),
)
AppSecretGenerateCommand.Flags().BoolVarP(
&generateAllSecrets,
i18n.G("all"),
i18n.GC("a", "app secret generate"),
false,
i18n.G("generate all secrets"),
)
AppSecretInsertCommand.Flags().BoolVarP(
&storeInPass,
i18n.G("pass"),
i18n.G("p"),
false,
i18n.G("store generated secrets in a local pass store"),
)
AppSecretInsertCommand.Flags().BoolVarP(
&insertFromFile,
i18n.G("file"),
i18n.G("f"),
false,
i18n.G("treat input as a file"),
)
AppSecretInsertCommand.Flags().BoolVarP(
&trimInput,
i18n.G("trim"),
i18n.G("t"),
false,
i18n.G("trim input"),
)
AppSecretInsertCommand.Flags().BoolVarP(
&internal.Chaos,
i18n.G("chaos"),
i18n.G("C"),
false,
i18n.G("ignore uncommitted recipes changes"),
)
AppSecretRmCommand.Flags().BoolVarP(
&rmAllSecrets,
i18n.G("all"),
i18n.GC("a", "app secret rm"),
false,
i18n.G("remove all secrets"),
)
AppSecretRmCommand.Flags().BoolVarP(
&removeFromPass,
i18n.G("pass"),
i18n.G("p"),
false,
i18n.G("remove generated secrets from a local pass store"),
)
AppSecretRmCommand.Flags().BoolVarP(
&internal.Chaos,
i18n.G("chaos"),
i18n.G("C"),
false,
i18n.G("ignore uncommitted recipes changes"),
)
AppSecretLsCommand.Flags().BoolVarP(
&internal.Chaos,
i18n.G("chaos"),
i18n.G("C"),
false,
i18n.G("ignore uncommitted recipes changes"),
)
AppSecretLsCommand.Flags().BoolVarP(
&internal.MachineReadable,
i18n.G("machine"),
i18n.G("m"),
false,
i18n.G("print machine-readable output"),
)
}
+103
View File
@@ -0,0 +1,103 @@
package app
import (
"context"
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/service"
stack "coopcloud.tech/abra/pkg/upstream/stack"
containerTypes "github.com/docker/docker/api/types/container"
"github.com/spf13/cobra"
)
// translators: `abra app services` aliases. use a comma separated list of
// aliases with no spaces in between
var appServicesAliases = i18n.G("sr")
var AppServicesCommand = &cobra.Command{
// translators: `app services` command
Use: i18n.G("services <domain> [flags]"),
Aliases: strings.Split(appServicesAliases, ","),
// translators: Short description for `app services` command
Short: i18n.G("Display all services of an app"),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
log.Fatal(err)
}
if !deployMeta.IsDeployed {
log.Fatal(i18n.G("%s is not deployed?", app.Name))
}
filters, err := app.Filters(true, true)
if err != nil {
log.Fatal(err)
}
containers, err := cl.ContainerList(context.Background(), containerTypes.ListOptions{Filters: filters})
if err != nil {
log.Fatal(err)
}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
headers := []string{i18n.G("SERVICE (SHORT)"), i18n.G("SERVICE (LONG)")}
table.Headers(headers...)
var rows [][]string
for _, container := range containers {
var containerNames []string
for _, containerName := range container.Names {
trimmed := strings.TrimPrefix(containerName, "/")
containerNames = append(containerNames, trimmed)
}
serviceShortName := service.ContainerToServiceName(container.Names, app.StackName())
serviceLongName := fmt.Sprintf("%s_%s", app.StackName(), serviceShortName)
row := []string{
serviceShortName,
serviceLongName,
}
rows = append(rows, row)
}
table.Rows(rows...)
if len(rows) > 0 {
if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
}
},
}
+150 -29
View File
@@ -3,50 +3,171 @@ package app
import (
"context"
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
stack "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra"
)
var appUndeployCommand = &cli.Command{
Name: "undeploy",
Aliases: []string{"u"},
Usage: "Undeploy an app",
Description: `
This does not destroy any of the application data. However, you should remain
vigilant, as your swarm installation will consider any previously attached
volumes as eligiblef or pruning once undeployed.
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
// translators: `abra app undeploy` aliases. use a comma separated list of aliases with
// no spaces in between
var appUndeployAliases = i18n.G("un")
var AppUndeployCommand = &cobra.Command{
// translators: `app undeploy` command
Use: i18n.G("undeploy <domain> [flags]"),
// translators: Short description for `app undeploy` command
Aliases: strings.Split(appUndeployAliases, ","),
Short: i18n.G("Undeploy a deployed app"),
Long: i18n.G(`This does not destroy any application data.
However, you should remain vigilant, as your swarm installation will consider
any previously attached volumes as eligible for pruning once undeployed.
Passing "--prune/-p" does not remove those volumes.`),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
stackName := app.StackName()
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)
}
ctx := context.Background()
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
rmOpts := stack.Remove{Namespaces: []string{app.StackName()}}
if err := stack.RunRemove(ctx, cl, rmOpts); err != nil {
logrus.Fatal(err)
}
log.Debug(i18n.G("checking whether %s is already deployed", stackName))
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
logrus.Warn(err)
log.Fatal(err)
}
if c.NArg() > 0 {
return
if !deployMeta.IsDeployed {
log.Fatal(i18n.G("%s is not deployed?", app.Name))
}
for _, a := range appNames {
fmt.Println(a)
version := deployMeta.Version
if deployMeta.IsChaos {
version = deployMeta.ChaosVersion
}
if err := internal.DeployOverview(
app,
version,
config.MISSING_DEFAULT,
"",
nil,
nil,
nil,
nil,
); err != nil {
log.Fatal(err)
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil {
log.Fatal(err)
}
opts := stack.Deploy{Composefiles: composeFiles, Namespace: stackName}
compose, err := appPkg.GetAppComposeConfig(app.Name, opts, app.Env)
if err != nil {
log.Fatal(err)
}
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
if err != nil {
log.Fatal(err)
}
rmOpts := stack.Remove{
Namespaces: []string{stackName},
Detach: false,
}
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
log.Fatal(err)
}
if prune {
if err := pruneApp(cl, app); err != nil {
log.Fatal(err)
}
}
log.Info(i18n.G("undeploy succeeded 🟢"))
if err := app.WriteRecipeVersion(version, false); err != nil {
log.Fatal(i18n.G("writing recipe version failed: %s", err))
}
},
}
// pruneApp runs the equivalent of a "docker system prune" but only filtering
// against resources connected with the app deployment. It is not a system wide
// prune. Volumes are not pruned to avoid unwated data loss.
func pruneApp(cl *dockerClient.Client, app appPkg.App) error {
stackName := app.StackName()
ctx := context.Background()
pruneFilters := filters.NewArgs()
stackSearch := fmt.Sprintf("%s*", stackName)
pruneFilters.Add("label", stackSearch)
cr, err := cl.ContainersPrune(ctx, pruneFilters)
if err != nil {
return err
}
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
log.Info(i18n.G("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed))
nr, err := cl.NetworksPrune(ctx, pruneFilters)
if err != nil {
return err
}
log.Info(i18n.G("networks pruned: %d", len(nr.NetworksDeleted)))
ir, err := cl.ImagesPrune(ctx, pruneFilters)
if err != nil {
return err
}
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
log.Info(i18n.G("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed))
return nil
}
var (
prune bool
)
func init() {
AppUndeployCommand.Flags().BoolVarP(
&prune,
i18n.G("prune"),
i18n.G("p"),
false,
i18n.G("prune unused containers, networks, and dangling images"),
)
}
+499
View File
@@ -0,0 +1,499 @@
package app
import (
"context"
"errors"
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/deploy"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra"
)
// translators: `abra app upgrade` aliases. use a comma separated list of aliases with
// no spaces in between
var appUpgradeAliases = i18n.G("up")
var AppUpgradeCommand = &cobra.Command{
// translators: `app upgrade` command
Use: i18n.G("upgrade <domain> [version] [flags]"),
Aliases: strings.Split(appUpgradeAliases, ","),
// translators: Short description for `app upgrade` command
Short: i18n.G("Upgrade an app"),
Long: i18n.G(`Upgrade an app.
Unlike "abra app deploy", chaos operations are not supported here. Only recipe
versions are supported values for "[version]".
It is possible to "--force/-f" an upgrade if you want to re-deploy a specific
version.
Only the deployed version is consulted when trying to determine what upgrades
are available. The live deployment version is the "source of truth" in this
case. The stored .env version is not consulted.
An upgrade can be destructive, please ensure you have a copy of your app data
beforehand. See "abra app backup" for more.`),
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string,
) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
app, err := appPkg.Get(args[0])
if err != nil {
return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
}
return autocomplete.RecipeVersionComplete(app.Recipe.Name)
default:
return nil, cobra.ShellCompDirectiveError
}
},
Run: func(cmd *cobra.Command, args []string) {
var (
upgradeWarnMessages []string
chosenUpgrade string
availableUpgrades []string
upgradeReleaseNotes string
)
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(recipe.EnsureContext{
Chaos: internal.Chaos,
Offline: internal.Offline,
// Ignore the env version for now, to make sure we are at the latest commit.
// This enables us to get release notes, that were added after a release.
IgnoreEnvVersion: true,
}); err != nil {
log.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
deployMeta, err := ensureDeployed(cl, app)
if err != nil {
log.Fatal(err)
}
if err := lint.LintForErrors(app.Recipe); err != nil {
log.Fatal(err)
}
versions, err := app.Recipe.Tags()
if err != nil {
log.Fatal(err)
}
// NOTE(d1): we've no idea what the live deployment version is, so every
// possible upgrade can be shown. it's up to the user to make the choice
if deployMeta.Version == config.UNKNOWN_DEFAULT {
availableUpgrades = versions
}
if len(args) == 2 && args[1] != "" {
chosenUpgrade = args[1]
if err := validateUpgradeVersionArg(chosenUpgrade, app, deployMeta); err != nil {
log.Fatal(err)
}
availableUpgrades = append(availableUpgrades, chosenUpgrade)
}
if deployMeta.Version != config.UNKNOWN_DEFAULT && chosenUpgrade == "" {
upgradeAvailable, err := ensureUpgradesAvailable(app, versions, &availableUpgrades, deployMeta)
if err != nil {
log.Fatal(err)
}
if !upgradeAvailable {
log.Info(i18n.G("no available upgrades"))
return
}
}
if internal.Force || internal.NoInput || chosenUpgrade != "" {
if len(availableUpgrades) > 0 {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
}
} else {
if err := chooseUpgrade(availableUpgrades, deployMeta, &chosenUpgrade); err != nil {
log.Fatal(err)
}
}
if internal.Force &&
chosenUpgrade == "" &&
deployMeta.Version != config.UNKNOWN_DEFAULT {
chosenUpgrade = deployMeta.Version
}
if chosenUpgrade == "" {
log.Fatal(i18n.G("unknown deployed version, unable to upgrade"))
}
log.Debug(i18n.G("choosing %s as version to upgrade", chosenUpgrade))
// Get the release notes before checking out the new version in the
// recipe. This enables us to get release notes, that were added after
// a release.
if err := getReleaseNotes(app, versions, chosenUpgrade, deployMeta, &upgradeReleaseNotes); err != nil {
log.Fatal(err)
}
if _, err := app.Recipe.EnsureVersion(chosenUpgrade); err != nil {
log.Fatal(err)
}
if err := deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
log.Fatal(err)
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil {
log.Fatal(err)
}
stackName := app.StackName()
deployOpts := stack.Deploy{
Composefiles: composeFiles,
Namespace: stackName,
Prune: false,
ResolveImage: stack.ResolveImageAlways,
Detach: false,
SendRegistryAuth: true,
}
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
if err != nil {
log.Fatal(err)
}
newRecipeWithUpgradeVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, chosenUpgrade)
appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithUpgradeVersion)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
if internal.Chaos {
appPkg.SetChaosVersionLabel(compose, stackName, chosenUpgrade)
}
envVars, err := appPkg.CheckEnv(app)
if err != nil {
log.Fatal(err)
}
for _, envVar := range envVars {
if !envVar.Present {
upgradeWarnMessages = append(upgradeWarnMessages,
i18n.G("%s missing from %s.env", envVar.Name, app.Domain),
)
}
}
// Gather secrets
secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, internal.ShowUnchanged)
if err != nil {
log.Fatal(err)
}
// Gather configs
configInfo, err := deploy.GatherConfigsForDeploy(cl, app, compose, app.Env, internal.ShowUnchanged)
if err != nil {
log.Fatal(err)
}
// Gather images
imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose, internal.ShowUnchanged)
if err != nil {
log.Fatal(err)
}
if showReleaseNotes {
fmt.Print(upgradeReleaseNotes)
return
}
if upgradeReleaseNotes == "" {
upgradeWarnMessages = append(
upgradeWarnMessages,
i18n.G("no release notes for upgrading from %s to %s", deployMeta.Version, chosenUpgrade),
)
}
deployedVersion := deployMeta.Version
if deployMeta.IsChaos {
deployedVersion = deployMeta.ChaosVersion
}
if err := internal.DeployOverview(
app,
deployedVersion,
chosenUpgrade,
upgradeReleaseNotes,
upgradeWarnMessages,
secretInfo,
configInfo,
imageInfo,
); err != nil {
log.Fatal(err)
}
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
if err != nil {
log.Fatal(err)
}
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
if err != nil {
log.Fatal(err)
}
f, err := app.Filters(true, false, serviceNames...)
if err != nil {
log.Fatal(err)
}
if err := stack.RunDeploy(
cl,
deployOpts,
compose,
stackName,
app.Server,
internal.DontWaitConverge,
internal.NoInput,
f,
); err != nil {
log.Fatal(err)
}
postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"]
if ok && !internal.DontWaitConverge {
log.Debug(i18n.G("run the following post-deploy commands: %s", postDeployCmds))
if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
log.Fatal(i18n.G("attempting to run post deploy commands, saw: %s", err))
}
}
if err := app.WriteRecipeVersion(chosenUpgrade, false); err != nil {
log.Fatal(i18n.G("writing recipe version failed: %s", err))
}
},
}
// chooseUpgrade prompts the user to choose an upgrade interactively.
func chooseUpgrade(
availableUpgrades []string,
deployMeta stack.DeployMeta,
chosenUpgrade *string,
) error {
msg := i18n.G("please select an upgrade (version: %s):", deployMeta.Version)
if deployMeta.IsChaos {
chaosVersion := formatter.BoldDirtyDefault(deployMeta.ChaosVersion)
msg = i18n.G(
"please select an upgrade (version: %s, chaos: %s):",
deployMeta.Version,
chaosVersion,
)
}
prompt := &survey.Select{
Message: msg,
Options: internal.SortVersionsDesc(availableUpgrades),
}
if err := survey.AskOne(prompt, chosenUpgrade); err != nil {
return err
}
return nil
}
func getReleaseNotes(
app appPkg.App,
versions []string,
chosenUpgrade string,
deployMeta stack.DeployMeta,
upgradeReleaseNotes *string,
) error {
parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade)
if err != nil {
return errors.New(i18n.G("parsing chosen upgrade version failed: %s", err))
}
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return errors.New(i18n.G("parsing deployment version failed: %s", err))
}
for _, version := range internal.SortVersionsDesc(versions) {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
return errors.New(i18n.G("parsing recipe version failed: %s", err))
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) &&
parsedVersion.IsLessThan(parsedChosenUpgrade) {
note, err := app.Recipe.GetReleaseNotes(version, app.Domain)
if err != nil {
return err
}
if note != "" {
// NOTE(d1): trim any final newline on the end of the note itself before
// we manually handle newlines (for multiple release notes and
// ensuring space between the warning messages)
note = strings.TrimSuffix(note, "\n")
*upgradeReleaseNotes += fmt.Sprintf("%s\n", note)
}
}
}
return nil
}
// ensureUpgradesAvailable ensures that there are available upgrades.
func ensureUpgradesAvailable(
app appPkg.App,
versions []string,
availableUpgrades *[]string,
deployMeta stack.DeployMeta,
) (bool, error) {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return false, errors.New(i18n.G("parsing deployed version failed: %s", err))
}
for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
return false, errors.New(i18n.G("parsing recipe version failed: %s", err))
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) &&
!(parsedVersion.Equals(parsedDeployedVersion)) {
*availableUpgrades = append(*availableUpgrades, version)
}
}
if len(*availableUpgrades) == 0 && !internal.Force {
return false, nil
}
return true, nil
}
// validateUpgradeVersionArg validates the specific version.
func validateUpgradeVersionArg(
specificVersion string,
app appPkg.App,
deployMeta stack.DeployMeta,
) error {
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil {
return errors.New(i18n.G("'%s' is not a known version for %s", specificVersion, app.Recipe.Name))
}
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return errors.New(i18n.G("'%s' is not a known version", deployMeta.Version))
}
if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) &&
!parsedSpecificVersion.Equals(parsedDeployedVersion) {
return errors.New(i18n.G("%s is not an upgrade for %s?", deployMeta.Version, specificVersion))
}
if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
return errors.New(i18n.G("%s is not an upgrade for %s?", deployMeta.Version, specificVersion))
}
return nil
}
// ensureDeployed ensures the app is deployed and if so, returns deployment
// meta info.
func ensureDeployed(cl *dockerClient.Client, app appPkg.App) (stack.DeployMeta, error) {
log.Debug(i18n.G("checking whether %s is already deployed", app.StackName()))
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
return stack.DeployMeta{}, err
}
if !deployMeta.IsDeployed {
return stack.DeployMeta{}, errors.New(i18n.G("%s is not deployed?", app.Name))
}
return deployMeta, nil
}
var showReleaseNotes bool
func init() {
AppUpgradeCommand.Flags().BoolVarP(
&internal.Force,
i18n.G("force"),
i18n.G("f"),
false,
i18n.G("perform action without further prompt"),
)
AppUpgradeCommand.Flags().BoolVarP(
&internal.NoDomainChecks,
i18n.G("no-domain-checks"),
i18n.G("D"),
false,
i18n.G("disable public DNS checks"),
)
AppUpgradeCommand.Flags().BoolVarP(
&internal.DontWaitConverge,
i18n.G("no-converge-checks"),
i18n.G("c"),
false,
i18n.G("disable converge logic checks"),
)
AppUpgradeCommand.Flags().BoolVarP(
&showReleaseNotes,
i18n.G("releasenotes"),
i18n.G("r"),
false,
i18n.G("only show release notes"),
)
AppUpgradeCommand.Flags().BoolVarP(
&internal.ShowUnchanged,
i18n.G("show-unchanged"),
i18n.G("U"),
false,
i18n.G("show all configs & images, including unchanged ones"),
)
}
-108
View File
@@ -1,108 +0,0 @@
package app
import (
"fmt"
"sort"
"strings"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
// getImagePath returns the image name
func getImagePath(image string) (string, error) {
img, err := reference.ParseNormalizedNamed(image)
if err != nil {
return "", err
}
path := reference.Path(img)
if strings.Contains(path, "library") {
path = strings.Split(path, "/")[1]
}
logrus.Debugf("parsed '%s' from '%s'", path, image)
return path, nil
}
var appVersionCommand = &cli.Command{
Name: "version",
Aliases: []string{"v"},
Usage: "Show version of all services in app",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
if err != nil {
logrus.Fatal(err)
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := config.GetAppComposeConfig(app.Type, opts, app.Env)
if err != nil {
logrus.Fatal(err)
}
ch := make(chan stack.StackStatus, len(compose.Services))
for _, service := range compose.Services {
label := fmt.Sprintf("coop-cloud.%s.%s.version", app.StackName(), service.Name)
go func(s string, l string) {
ch <- stack.GetDeployedServicesByLabel(s, l)
}(app.Server, label)
}
tableCol := []string{"name", "image", "version", "digest"}
table := abraFormatter.CreateTable(tableCol)
statuses := make(map[string]stack.StackStatus)
for range compose.Services {
status := <-ch
if len(status.Services) > 0 {
serviceName := appPkg.ParseServiceName(status.Services[0].Spec.Name)
statuses[serviceName] = status
}
}
sort.SliceStable(compose.Services, func(i, j int) bool {
return compose.Services[i].Name < compose.Services[j].Name
})
for _, service := range compose.Services {
if status, ok := statuses[service.Name]; ok {
statusService := status.Services[0]
label := fmt.Sprintf("coop-cloud.%s.%s.version", app.StackName(), service.Name)
version, digest := appPkg.ParseVersionLabel(statusService.Spec.Labels[label])
image, err := getImagePath(statusService.Spec.Labels["com.docker.stack.image"])
if err != nil {
logrus.Fatal(err)
}
table.Append([]string{service.Name, image, version, digest})
continue
}
image, err := getImagePath(service.Image)
if err != nil {
logrus.Fatal(err)
}
table.Append([]string{service.Name, image, "?", "?"})
}
table.Render()
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}
+179 -67
View File
@@ -3,107 +3,219 @@ package app
import (
"context"
"fmt"
"strings"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/spf13/cobra"
)
var appVolumeListCommand = &cli.Command{
Name: "list",
Usage: "List volumes associated with an app",
Aliases: []string{"ls"},
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
// translators: `abra app volume list` aliases. use a comma separated list of aliases with
// no spaces in between
var appVolumeListAliases = i18n.G("ls")
ctx := context.Background()
volumeList, err := client.GetVolumes(ctx, app.Server, app.Name)
var AppVolumeListCommand = &cobra.Command{
// translators: `app volume list` command
Use: i18n.G("list <domain> [flags]"),
Aliases: strings.Split(appVolumeListAliases, ","),
// translators: Short description for `app list` command
Short: i18n.G("List volumes associated with an app"),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
table := abraFormatter.CreateTable([]string{"driver", "volume name"})
var volTable [][]string
for _, volume := range volumeList {
volRow := []string{
volume.Driver,
volume.Name,
filters, err := app.Filters(false, true)
if err != nil {
log.Fatal(err)
}
volumes, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
if err != nil {
log.Fatal(err)
}
headers := []string{i18n.G("NAME"), i18n.G("ON SERVER")}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table.Headers(headers...)
var rows [][]string
for _, volume := range volumes {
row := []string{volume.Name, volume.Mountpoint}
rows = append(rows, row)
}
table.Rows(rows...)
if len(rows) > 0 {
if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
volTable = append(volTable, volRow)
return
}
table.AppendBulk(volTable)
table.Render()
return nil
log.Warn(i18n.G("no volumes created for %s", app.Name))
},
}
var appVolumeRemoveCommand = &cli.Command{
Name: "remove",
Usage: "Remove volume(s) associated with an app",
Aliases: []string{"rm"},
Flags: []cli.Flag{
internal.ForceFlag,
},
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
// translators: `abra app volume remove` aliases. use a comma separated list of aliases with
// no spaces in between
var appVolumeRemoveAliases = i18n.G("rm")
ctx := context.Background()
volumeList, err := client.GetVolumes(ctx, app.Server, app.Name)
var AppVolumeRemoveCommand = &cobra.Command{
// translators: `app volume remove` command
Use: i18n.G("remove <domain> [volume] [flags]"),
// translators: Short description for `app volume remove` command
Short: i18n.G("Remove volume(s) associated with an app"),
Long: i18n.G(`Remove volumes associated with an app.
The app in question must be undeployed before you try to remove volumes. See
"abra app undeploy <domain>" for more.
The command is interactive and will show a multiple select input which allows
you to make a seclection. Use the "?" key to see more help on navigating this
interface.
Passing "--force/-f" will select all volumes for removal. Be careful.`),
Example: i18n.G(` # delete volumes interactively
abra app volume rm 1312.net
# delete specific volume
abra app volume rm 1312.net my_volume`),
Aliases: strings.Split(appVolumeRemoveAliases, ","),
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
var volumeToDelete string
if len(args) == 2 {
volumeToDelete = args[1]
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
log.Fatal(err)
}
if deployMeta.IsDeployed {
log.Fatal(i18n.G("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name))
}
filters, err := app.Filters(false, true)
if err != nil {
log.Fatal(err)
}
volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
if err != nil {
log.Fatal(err)
}
volumeNames := client.GetVolumeNames(volumeList)
if volumeToDelete != "" {
var exactMatch bool
fullVolumeToDeleteName := fmt.Sprintf("%s_%s", app.StackName(), volumeToDelete)
for _, volName := range volumeNames {
if volName == fullVolumeToDeleteName {
exactMatch = true
}
}
if !exactMatch {
log.Fatal(i18n.G("unable to remove volume: no volume with name '%s'?", volumeToDelete))
}
err := client.RemoveVolumes(cl, context.Background(), []string{fullVolumeToDeleteName}, internal.Force, 5)
if err != nil {
log.Fatal(i18n.G("removing volume %s failed: %s", volumeToDelete, err))
}
log.Info(i18n.G("volume %s removed successfully", volumeToDelete))
return
}
var volumesToRemove []string
if !internal.Force {
if !internal.Force && !internal.NoInput {
volumesPrompt := &survey.MultiSelect{
Message: "which volumes do you want to remove?",
Message: i18n.G("which volumes do you want to remove?"),
Help: i18n.G("'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled"),
VimMode: true,
Options: volumeNames,
Default: volumeNames,
}
if err := survey.AskOne(volumesPrompt, &volumesToRemove); err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
} else {
}
if internal.Force || internal.NoInput {
volumesToRemove = volumeNames
}
err = client.RemoveVolumes(ctx, app.Server, volumesToRemove, internal.Force)
if err != nil {
logrus.Fatal(err)
}
if len(volumesToRemove) > 0 {
err := client.RemoveVolumes(cl, context.Background(), volumesToRemove, internal.Force, 5)
if err != nil {
log.Fatal(i18n.G("removing volumes failed: %s", err))
}
logrus.Info("volumes removed successfully")
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
log.Info(i18n.G("%d volumes removed successfully", len(volumesToRemove)))
} else {
log.Info(i18n.G("no volumes removed"))
}
},
}
var appVolumeCommand = &cli.Command{
Name: "volume",
Aliases: []string{"v"},
Usage: "Manage app volumes",
ArgsUsage: "<command>",
Subcommands: []*cli.Command{
appVolumeListCommand,
appVolumeRemoveCommand,
},
// translators: `abra app volume` aliases. use a comma separated list of aliases with
// no spaces in between
var appVolumeAliases = i18n.G("vl")
var AppVolumeCommand = &cobra.Command{
// translators: `app volume` command group
Use: i18n.G("volume [cmd] [args] [flags]"),
Aliases: strings.Split(appVolumeAliases, ","),
Short: i18n.G("Manage app volumes"),
}
func init() {
AppVolumeRemoveCommand.Flags().BoolVarP(
&internal.Force,
i18n.G("force"),
i18n.G("f"),
false,
i18n.G("perform action without further prompt"),
)
}
+315 -10
View File
@@ -1,17 +1,322 @@
package catalogue
import (
"github.com/urfave/cli/v2"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"slices"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/go-git/go-git/v5"
"github.com/spf13/cobra"
)
// CatalogueCommand defines the `abra catalogue` command and sub-commands.
var CatalogueCommand = &cli.Command{
Name: "catalogue",
Usage: "Manage the recipe catalogue",
Aliases: []string{"c"},
ArgsUsage: "<recipe>",
Description: "This command helps recipe packagers interact with the recipe catalogue",
Subcommands: []*cli.Command{
catalogueGenerateCommand,
// translators: `abra catalogue sync` aliases. use a comma separated list of aliases with
// no spaces in between
var appCatalogueSyncAliases = i18n.G("s")
var CatalogueSyncCommand = &cobra.Command{
// translators: `catalogue sync` command
Use: i18n.G("sync [flags]"),
Aliases: strings.Split(appCatalogueSyncAliases, ","),
// translators: Short description for `catalogue sync` command
Short: i18n.G("Sync recipe catalogue for latest changes"),
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
if err := catalogue.EnsureCatalogue(); err != nil {
log.Fatal(err)
}
if err := catalogue.EnsureUpToDate(); err != nil {
log.Fatal(err)
}
log.Info(i18n.G("catalogue successfully synced"))
},
}
// translators: `abra catalogue` aliases. use a comma separated list of aliases with
// no spaces in between
var appCatalogueAliases = i18n.G("g")
var CatalogueGenerateCommand = &cobra.Command{
// translators: `catalogue generate` command
Use: i18n.G("generate [recipe] [flags]"),
Aliases: strings.Split(appCatalogueAliases, ","),
// translators: Short description for `catalogue generate` command
Short: i18n.G("Generate the recipe catalogue"),
Long: i18n.G(`Generate a new copy of the recipe catalogue.
N.B. this command **will** wipe local unstaged changes from your local recipes
if present. "--chaos/-C" on this command refers to the catalogue repository
("$ABRA_DIR/catalogue") and not the recipes. Please take care not to lose your
changes.
It is possible to generate new metadata for a single recipe by passing
[recipe]. The existing local catalogue will be updated, not overwritten.
It is quite easy to get rate limited by Docker Hub when running this command.
If you have a Hub account you can "docker login" and Abra will automatically
use those details.
Publish your new release to git.coopcloud.tech with "--publish/-p". This
requires that you have permission to git push to these repositories and have
your SSH keys configured on your account. Enable ssh-agent and make sure to add
your private key and enter your passphrase beforehand.
eval ` + "`ssh-agent`" + `
ssh-add ~/.ssh/<my-ssh-private-key-for-git-coopcloud-tech>`),
Example: ` # publish catalogue
eval ` + "`ssh-agent`" + `
ssh-add ~/.ssh/id_ed25519
abra catalogue generate -p`,
Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
var recipeName string
if len(args) > 0 {
recipeName = args[0]
}
if os.Getenv("SSH_AUTH_SOCK") == "" {
log.Warn(i18n.G("ssh: SSH_AUTH_SOCK missing, --publish/-p will fail. see \"abra catalogue generate --help\""))
}
if recipeName != "" {
internal.ValidateRecipe(args, cmd.Name())
}
if err := catalogue.EnsureCatalogue(); err != nil {
log.Fatal(err)
}
if !internal.Chaos {
if err := catalogue.EnsureIsClean(); err != nil {
log.Fatal(err)
}
}
repos, err := recipe.ReadReposMetadata(internal.Debug)
if err != nil {
log.Fatal(err)
}
barLength := len(repos)
if recipeName != "" {
barLength = 1
}
if !skipUpdates {
if err := recipe.UpdateRepositories(repos, recipeName, internal.Debug); err != nil {
log.Fatal(err)
}
}
var warnings []string
catl := make(recipe.RecipeCatalogue)
catlBar := formatter.CreateProgressbar(barLength, i18n.G("collecting catalogue metadata"))
for _, recipeMeta := range repos {
if recipeName != "" && recipeName != recipeMeta.Name {
if !internal.Debug {
catlBar.Add(1)
}
continue
}
r := recipe.Get(recipeMeta.Name)
versions, warnMsgs, err := r.GetRecipeVersions()
if err != nil {
warnings = append(warnings, err.Error())
}
if len(warnMsgs) > 0 {
warnings = append(warnings, warnMsgs...)
}
features, category, warnMsgs, err := recipe.GetRecipeFeaturesAndCategory(r)
if err != nil {
warnings = append(warnings, err.Error())
}
if len(warnMsgs) > 0 {
warnings = append(warnings, warnMsgs...)
}
catl[recipeMeta.Name] = recipe.RecipeMeta{
Name: recipeMeta.Name,
Repository: recipeMeta.CloneURL,
SSHURL: recipeMeta.SSHURL,
Icon: recipeMeta.AvatarURL,
DefaultBranch: recipeMeta.DefaultBranch,
Description: recipeMeta.Description,
Website: recipeMeta.Website,
Versions: versions,
Category: category,
Features: features,
}
if !internal.Debug {
catlBar.Add(1)
}
}
if err := catlBar.Close(); err != nil {
log.Fatal(err)
}
var uniqueWarnings []string
for _, w := range warnings {
if !slices.Contains(uniqueWarnings, w) {
uniqueWarnings = append(uniqueWarnings, w)
}
}
for _, warnMsg := range uniqueWarnings {
log.Warn(warnMsg)
}
recipesJSON, err := json.MarshalIndent(catl, "", " ")
if err != nil {
log.Fatal(err)
}
if recipeName == "" {
if err := ioutil.WriteFile(config.RECIPES_JSON, recipesJSON, 0764); err != nil {
log.Fatal(err)
}
} else {
catlFS, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
log.Fatal(err)
}
catlFS[recipeName] = catl[recipeName]
updatedRecipesJSON, err := json.MarshalIndent(catlFS, "", " ")
if err != nil {
log.Fatal(err)
}
if err := ioutil.WriteFile(config.RECIPES_JSON, updatedRecipesJSON, 0764); err != nil {
log.Fatal(err)
}
}
log.Info(i18n.G("generated recipe catalogue: %s", config.RECIPES_JSON))
cataloguePath := path.Join(config.ABRA_DIR, "catalogue")
if publishChanges {
isClean, err := gitPkg.IsClean(cataloguePath)
if err != nil {
log.Fatal(err)
}
if isClean {
if !internal.Dry {
log.Fatal(i18n.G("no changes discovered in %s, nothing to publish?", cataloguePath))
}
}
msg := i18n.G("chore: publish new catalogue release changes")
if err := gitPkg.Commit(cataloguePath, msg, internal.Dry); err != nil {
log.Fatal(err)
}
repo, err := git.PlainOpen(cataloguePath)
if err != nil {
log.Fatal(err)
}
sshURL := fmt.Sprintf(config.TOOLSHED_SSH_URL_TEMPLATE, config.CATALOGUE_JSON_REPO_NAME)
if err := gitPkg.CreateRemote(repo, "origin-ssh", sshURL, internal.Dry); err != nil {
log.Fatal(err)
}
if err := gitPkg.Push(cataloguePath, "origin-ssh", false, internal.Dry); err != nil {
log.Fatal(err)
}
}
repo, err := git.PlainOpen(cataloguePath)
if err != nil {
log.Fatal(err)
}
head, err := repo.Head()
if err != nil {
log.Fatal(err)
}
if !internal.Dry && publishChanges {
url := fmt.Sprintf("%s/%s/commit/%s", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME, head.Hash())
log.Info(i18n.G("new changes published: %s", url))
}
if internal.Dry {
log.Info(i18n.G("dry run: no changes published"))
}
},
}
// CatalogueCommand defines the `abra catalogue` command and sub-commands.
var CatalogueCommand = &cobra.Command{
// translators: `catalogue` command group
Use: i18n.G("catalogue [cmd] [args] [flags]"),
// translators: Short description for `catalogue` command group
Short: i18n.G("Manage the recipe catalogue"),
Aliases: []string{"c"},
}
var (
publishChanges bool
skipUpdates bool
)
func init() {
CatalogueGenerateCommand.Flags().BoolVarP(
&publishChanges,
i18n.G("publish"),
i18n.G("p"),
false,
i18n.G("publish changes to git.coopcloud.tech"),
)
CatalogueGenerateCommand.Flags().BoolVarP(
&internal.Dry,
i18n.G("dry-run"),
i18n.G("r"),
false,
i18n.G("report changes that would be made"),
)
CatalogueGenerateCommand.Flags().BoolVarP(
&skipUpdates,
i18n.G("skip-updates"),
i18n.G("s"),
false,
i18n.G("skip updating recipe repositories"),
)
CatalogueGenerateCommand.Flags().BoolVarP(
&internal.Chaos,
i18n.G("chaos"),
i18n.G("C"),
false,
i18n.G("ignore uncommitted recipes changes"),
)
}
-41
View File
@@ -1,41 +0,0 @@
package catalogue
import (
"path"
"strings"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/git"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var catalogueGenerateCommand = &cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate a new copy of the catalogue",
BashComplete: func(c *cli.Context) {},
Action: func(c *cli.Context) error {
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
for recipeName, recipeMeta := range catl {
recipeDir := path.Join(config.ABRA_DIR, "apps", strings.ToLower(recipeName))
if err := git.Clone(recipeDir, recipeMeta.Repository); err != nil {
logrus.Fatal(err)
}
if err := git.EnsureUpToDate(recipeDir); err != nil {
logrus.Fatal(err)
}
}
// for reach app, build the recipemeta from parsing
// spit out a JSON file
return nil
},
}
-88
View File
@@ -1,88 +0,0 @@
// Package cli provides the interface for the command-line.
package cli
import (
"fmt"
"os"
"coopcloud.tech/abra/cli/app"
"coopcloud.tech/abra/cli/catalogue"
"coopcloud.tech/abra/cli/recipe"
"coopcloud.tech/abra/cli/server"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
// Verbose stores the variable from VerboseFlag.
var Verbose bool
// VerboseFlag turns on/off verbose logging down to the INFO level.
var VerboseFlag = &cli.BoolFlag{
Name: "verbose",
Aliases: []string{"V"},
Value: false,
Destination: &Verbose,
Usage: "Show INFO messages",
}
// Debug stores the variable from DebugFlag.
var Debug bool
// DebugFlag turns on/off verbose logging down to the DEBUG level.
var DebugFlag = &cli.BoolFlag{
Name: "debug",
Aliases: []string{"d"},
Value: false,
Destination: &Debug,
Usage: "Show DEBUG messages",
}
// RunApp runs CLI abra app.
func RunApp(version, commit string) {
app := &cli.App{
Name: "abra",
Usage: `The Co-op Cloud command-line utility belt 🎩🐇
____ ____ _ _
/ ___|___ ___ _ __ / ___| | ___ _ _ __| |
| | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |
| |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |
\____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|
|_|
`,
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Commands: []*cli.Command{
app.AppCommand,
server.ServerCommand,
recipe.RecipeCommand,
catalogue.CatalogueCommand,
VersionCommand,
UpgradeCommand,
},
Flags: []cli.Flag{
VerboseFlag,
DebugFlag,
},
Authors: []*cli.Author{
&cli.Author{
Name: "Autonomic Co-op",
Email: "helo@autonomic.zone",
},
},
}
app.EnableBashCompletion = true
app.Before = func(c *cli.Context) error {
if Debug {
logrus.SetLevel(logrus.DebugLevel)
}
return nil
}
logrus.Debugf("Flying abra version '%s', commit '%s', enjoy the ride", version, commit)
if err := app.Run(os.Args); err != nil {
logrus.Fatal(err)
}
}
+71
View File
@@ -0,0 +1,71 @@
package cli
import (
"os"
"strings"
"coopcloud.tech/abra/pkg/i18n"
"github.com/spf13/cobra"
)
// translators: `abra autocomplete` aliases. use a comma separated list of
// aliases with no spaces in between
var autocompleteAliases = i18n.G("ac")
var AutocompleteCommand = &cobra.Command{
// translators: `autocomplete` command
Use: i18n.G("autocomplete [bash|zsh|fish|powershell]"),
Aliases: strings.Split(autocompleteAliases, ","),
// translators: Short description for `autocomplete` command
Short: i18n.G("Generate autocompletion script"),
Long: i18n.G(`To load completions:
Bash:
# Load autocompletion for the current Bash session
$ source <(abra autocomplete bash)
# To load autocompletion for each session, execute once:
# Linux:
$ abra autocomplete bash | sudo tee /etc/bash_completion.d/abra
# macOS:
$ abra autocomplete bash | sudo tee $(brew --prefix)/etc/bash_completion.d/abra
Zsh:
# If shell autocompletion is not already enabled in your environment,
# you will need to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load autocompletions for each session, execute once:
$ abra autocomplete zsh > "${fpath[1]}/_abra"
# You will need to start a new shell for this setup to take effect.
fish:
$ abra autocomplete fish | source
# To load autocompletions for each session, execute once:
$ abra autocomplete fish > ~/.config/fish/completions/abra.fish
PowerShell:
PS> abra autocomplete powershell | Out-String | Invoke-Expression
# To load autocompletions for every new session, run:
PS> abra autocomplete powershell > abra.ps1
# and source this file from your PowerShell profile.`),
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
},
}
-39
View File
@@ -1,39 +0,0 @@
package formatter
import (
"fmt"
"os"
"strings"
"time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/go-units"
"github.com/olekukonko/tablewriter"
)
func ShortenID(str string) string {
return str[:12]
}
func Truncate(str string) string {
return fmt.Sprintf(`"%s"`, formatter.Ellipsis(str, 19))
}
// RemoveSha remove image sha from a string that are added in some docker outputs
func RemoveSha(str string) string {
return strings.Split(str, "@")[0]
}
// HumanDuration from docker/cli RunningFor() to be accessible outside of the class
func HumanDuration(timestamp int64) string {
date := time.Unix(timestamp, 0)
now := time.Now().UTC()
return units.HumanDuration(now.Sub(date)) + " ago"
}
// CreateTable prepares a table layout for output.
func CreateTable(columns []string) *tablewriter.Table {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader(columns)
return table
}
+76
View File
@@ -0,0 +1,76 @@
package internal
import (
"context"
"errors"
"io"
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/service"
"coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
)
// RetrieveBackupBotContainer gets the deployed backupbot container.
func RetrieveBackupBotContainer(cl *dockerClient.Client) (types.Container, error) {
ctx := context.Background()
chosenService, err := service.GetServiceByLabel(ctx, cl, config.BackupbotLabel, NoInput)
if err != nil {
return types.Container{}, errors.New(i18n.G("no backupbot discovered, is it deployed?"))
}
log.Debug(i18n.G("retrieved %s as backup enabled service", chosenService.Spec.Name))
filters := filters.NewArgs()
filters.Add("name", chosenService.Spec.Name)
targetContainer, err := containerPkg.GetContainer(
ctx,
cl,
filters,
NoInput,
)
if err != nil {
return types.Container{}, err
}
return targetContainer, nil
}
// RunBackupCmdRemote runs a backup related command on a remote backupbot container.
func RunBackupCmdRemote(
cl *dockerClient.Client,
backupCmd string,
containerID string,
execEnv []string) (io.Writer, error) {
execBackupListOpts := containertypes.ExecOptions{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: []string{"/usr/bin/backup", "--", backupCmd},
Detach: false,
Env: execEnv,
Tty: true,
}
log.Debug(i18n.G("running backup %s on %s with exec config %v", backupCmd, containerID, execBackupListOpts))
// FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli()
if err != nil {
return nil, err
}
out, err := container.RunExec(dcli, cl, containerID, &execBackupListOpts)
if err != nil {
return nil, err
}
return out, nil
}
+23
View File
@@ -0,0 +1,23 @@
package internal
var (
// NOTE(d1): global
Debug bool
NoInput bool
Offline bool
Help bool
Version bool
// NOTE(d1): sub-command specific
Chaos bool
DeployLatest bool
DontWaitConverge bool
Dry bool
Force bool
MachineReadable bool
Major bool
Minor bool
NoDomainChecks bool
Patch bool
ShowUnchanged bool
)
+107
View File
@@ -2,10 +2,117 @@ package internal
import (
"bufio"
"context"
"errors"
"fmt"
"io/ioutil"
"os/exec"
"strings"
appPkg "coopcloud.tech/abra/pkg/app"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/archive"
)
// RunCmdRemote executes an abra.sh command in the target service
func RunCmdRemote(
cl *dockerClient.Client,
app appPkg.App,
disableTTY bool,
abraSh, serviceName, cmdName, cmdArgs, remoteUser string) error {
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, false)
if err != nil {
return err
}
log.Debug(i18n.G("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 := containertypes.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 := containertypes.ExecOptions{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: findShell,
Detach: false,
Tty: false,
}
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
log.Info(i18n.G("%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)}
}
log.Debug(i18n.G("running command: %s", strings.Join(cmd, " ")))
if remoteUser != "" {
log.Debug(i18n.G("running command with user %s", remoteUser))
execCreateOpts.User = remoteUser
}
execCreateOpts.Cmd = cmd
execCreateOpts.Tty = true
if disableTTY {
execCreateOpts.Tty = false
log.Debug(i18n.G("not requesting a remote TTY"))
}
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
return err
}
return nil
}
func EnsureCommand(abraSh, recipeName, execCmd string) error {
bytes, err := ioutil.ReadFile(abraSh)
if err != nil {
return err
}
if !strings.Contains(string(bytes), execCmd) {
return errors.New(i18n.G("%s doesn't have a %s function", recipeName, execCmd))
}
return nil
}
// RunCmd runs a shell command and streams stdout/stderr in real-time.
func RunCmd(cmd *exec.Cmd) error {
r, err := cmd.StdoutPipe()
-51
View File
@@ -1,51 +0,0 @@
package internal
import (
"github.com/urfave/cli/v2"
)
// Secrets stores the variable from SecretsFlag
var Secrets bool
// SecretsFlag turns on/off automatically generating secrets
var SecretsFlag = &cli.BoolFlag{
Name: "secrets",
Aliases: []string{"S"},
Value: false,
Usage: "Automatically generate secrets",
Destination: &Secrets,
}
// Pass stores the variable from PassFlag
var Pass bool
// PassFlag turns on/off storing generated secrets in pass
var PassFlag = &cli.BoolFlag{
Name: "pass",
Aliases: []string{"P"},
Value: false,
Usage: "Store the generated secrets in a local pass store",
Destination: &Pass,
}
// Context is temp
var Context string
// ContextFlag is temp
var ContextFlag = &cli.StringFlag{
Name: "context",
Value: "",
Aliases: []string{"c"},
Destination: &Context,
}
// Force force functionality without asking.
var Force bool
// ForceFlag turns on/off force functionality.
var ForceFlag = &cli.BoolFlag{
Name: "force",
Value: false,
Aliases: []string{"f"},
Destination: &Force,
}
+318
View File
@@ -0,0 +1,318 @@
package internal
import (
"errors"
"fmt"
"os"
"sort"
"strings"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/lipgloss"
dockerClient "github.com/docker/docker/client"
)
var borderStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.ThickBorder()).
Padding(0, 1, 0, 1).
MaxWidth(79).
BorderForeground(lipgloss.Color("63"))
var headerStyle = lipgloss.NewStyle().
Underline(true).
Bold(true).
PaddingBottom(1)
var leftStyle = lipgloss.NewStyle().
Bold(true)
var rightStyle = lipgloss.NewStyle()
// horizontal is a JoinHorizontal helper function.
func horizontal(left, mid, right string) string {
return lipgloss.JoinHorizontal(lipgloss.Left, left, mid, right)
}
func formatComposeFiles(composeFiles string) string {
return strings.ReplaceAll(composeFiles, ":", "\n")
}
// DeployOverview shows a deployment overview
func DeployOverview(
app appPkg.App,
deployedVersion string,
toDeployVersion string,
releaseNotes string,
warnMessages []string,
secrets []string,
configs []string,
images []string,
) error {
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = formatComposeFiles(composeFiles)
}
server := app.Server
if app.Server == "default" {
server = "local"
}
domain := fmt.Sprintf("https://%s", app.Domain)
if domain == "" {
domain = config.MISSING_DEFAULT
}
envVersion := app.Recipe.EnvVersionRaw
if envVersion == "" {
envVersion = config.MISSING_DEFAULT
}
rows := [][]string{
{i18n.G("DOMAIN"), domain},
{i18n.G("RECIPE"), app.Recipe.Name},
{i18n.G("SERVER"), server},
{i18n.G("CONFIG"), deployConfig},
{"", ""},
{i18n.G("CURRENT DEPLOYMENT"), formatter.BoldDirtyDefault(deployedVersion)},
{i18n.G("ENV VERSION"), formatter.BoldDirtyDefault(envVersion)},
{i18n.G("NEW DEPLOYMENT"), formatter.BoldDirtyDefault(toDeployVersion)},
}
if len(images) > 0 {
imageRows := [][]string{
{"", ""},
{i18n.G("IMAGES"), strings.Join(images, "\n")},
}
rows = append(rows, imageRows...)
}
if len(secrets) > 0 {
secretsRows := [][]string{
{"", ""},
{i18n.G("SECRETS"), strings.Join(secrets, "\n")},
}
rows = append(rows, secretsRows...)
}
if len(configs) > 0 {
configsRows := [][]string{
{"", ""},
{i18n.G("CONFIGS"), strings.Join(configs, "\n")},
}
rows = append(rows, configsRows...)
}
deployType := getDeployType(deployedVersion, toDeployVersion)
overview := formatter.CreateOverview(i18n.G("%s OVERVIEW", deployType), rows)
fmt.Println(overview)
if releaseNotes != "" {
fmt.Print(releaseNotes)
}
for _, msg := range warnMessages {
log.Warn(msg)
}
if NoInput {
return nil
}
response := false
prompt := &survey.Confirm{Message: i18n.G("proceed?")}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
log.Fatal(i18n.G("deployment cancelled"))
}
return nil
}
func getDeployType(currentVersion, newVersion string) string {
if newVersion == config.MISSING_DEFAULT {
return i18n.G("UNDEPLOY")
}
if strings.Contains(newVersion, "+U") {
return i18n.G("CHAOS DEPLOY")
}
if strings.Contains(currentVersion, "+U") {
return i18n.G("UNCHAOS DEPLOY")
}
if currentVersion == newVersion {
return ("REDEPLOY")
}
if currentVersion == config.MISSING_DEFAULT {
return i18n.G("NEW DEPLOY")
}
currentParsed, err := tagcmp.Parse(currentVersion)
if err != nil {
return i18n.G("DEPLOY")
}
newParsed, err := tagcmp.Parse(newVersion)
if err != nil {
return i18n.G("DEPLOY")
}
if currentParsed.IsLessThan(newParsed) {
return i18n.G("UPGRADE")
}
return i18n.G("DOWNGRADE")
}
// MoveOverview shows a overview before moving an app to a different server
func MoveOverview(
app appPkg.App,
newServer string,
secrets []string,
volumes []string,
) {
server := app.Server
if app.Server == "default" {
server = "local"
}
domain := app.Domain
if domain == "" {
domain = config.MISSING_DEFAULT
}
secretsOverview := strings.Join(secrets, "\n")
if len(secrets) == 0 {
secretsOverview = config.MISSING_DEFAULT
}
volumesOverview := strings.Join(volumes, "\n")
if len(volumes) == 0 {
volumesOverview = config.MISSING_DEFAULT
}
rows := [][]string{
{i18n.G("DOMAIN"), domain},
{i18n.G("RECIPE"), app.Recipe.Name},
{i18n.G("OLD SERVER"), server},
{i18n.G("NEW SERVER"), newServer},
{i18n.G("SECRETS"), secretsOverview},
{i18n.G("VOLUMES"), volumesOverview},
}
overview := formatter.CreateOverview(i18n.G("MOVE OVERVIEW"), rows)
fmt.Println(overview)
}
func PromptProcced() error {
if NoInput {
return nil
}
if Dry {
return errors.New(i18n.G("dry run"))
}
response := false
prompt := &survey.Confirm{Message: i18n.G("proceed?")}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
return errors.New(i18n.G("cancelled"))
}
return nil
}
// PostCmds parses a string of commands and executes them inside of the respective services
// the commands string must have the following format:
// "<service> <command> <arguments>|<service> <command> <arguments>|... "
func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
if _, err := os.Stat(app.Recipe.AbraShPath); err != nil {
if os.IsNotExist(err) {
return errors.New(i18n.G("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name))
}
return err
}
for _, command := range strings.Split(commands, "|") {
commandParts := strings.Split(command, " ")
if len(commandParts) < 2 {
return errors.New(i18n.G("not enough arguments: %s", command))
}
targetServiceName := commandParts[0]
cmdName := commandParts[1]
parsedCmdArgs := ""
if len(commandParts) > 2 {
parsedCmdArgs = fmt.Sprintf("%s ", strings.Join(commandParts[2:], " "))
}
log.Info(i18n.G("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName))
if err := EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
return err
}
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
if err != nil {
return err
}
matchingServiceName := false
for _, serviceName := range serviceNames {
if serviceName == targetServiceName {
matchingServiceName = true
}
}
if !matchingServiceName {
return fmt.Errorf("no service %s for %s?", targetServiceName, app.Name)
}
log.Debug(i18n.G("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName))
requestTTY := true
if err := RunCmdRemote(
cl,
app,
requestTTY,
app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs, ""); err != nil {
return err
}
}
return nil
}
// SortVersionsDesc sorts versions in descending order.
func SortVersionsDesc(versions []string) []string {
var tags []tagcmp.Tag
for _, v := range versions {
parsed, _ := tagcmp.Parse(v) // skips unsupported tags
tags = append(tags, parsed)
}
sort.Sort(tagcmp.ByTagDesc(tags))
var desc []string
for _, t := range tags {
desc = append(desc, t.String())
}
return desc
}
+17
View File
@@ -0,0 +1,17 @@
package internal
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSortVersionsDesc(t *testing.T) {
versions := SortVersionsDesc([]string{
"0.2.3+1.2.2",
"1.0.0+2.2.2",
})
assert.Equal(t, "1.0.0+2.2.2", versions[0])
assert.Equal(t, "0.2.3+1.2.2", versions[1])
}
+11
View File
@@ -0,0 +1,11 @@
package internal
import "coopcloud.tech/abra/pkg/recipe"
func GetEnsureContext() recipe.EnsureContext {
return recipe.EnsureContext{
Chaos,
Offline,
DeployLatest,
}
}
-18
View File
@@ -1,18 +0,0 @@
package internal
import (
"os"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
// ShowSubcommandHelpAndError exits the program on error, logs the error to the
// terminal, and shows the help command.
func ShowSubcommandHelpAndError(c *cli.Context, err interface{}) {
if err2 := cli.ShowSubcommandHelp(c); err2 != nil {
logrus.Error(err2)
}
logrus.Error(err)
os.Exit(1)
}
+118
View File
@@ -0,0 +1,118 @@
package internal
import (
"errors"
"fmt"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/AlecAivazis/survey/v2"
"github.com/distribution/reference"
)
// PromptBumpType prompts for version bump type
func PromptBumpType(tagString, latestRelease, changeOverview string) error {
if (!Major && !Minor && !Patch) && tagString == "" {
fmt.Print(i18n.G(`
You need to make a decision about what kind of an update this new recipe
version is. If someone else performs this upgrade, do they have to do some
migration work or take care of some breaking changes? This can be signaled in
the version you specify on the recipe deploy label and is called a semantic
version.
The latest published version is %s.
%s
Here is a semver cheat sheet (more on https://semver.org):
major: new features/bug fixes, backwards incompatible (e.g 1.0.0 -> 2.0.0).
the upgrade won't work without some preparation work and others need
to take care when performing it. "it could go wrong".
minor: new features/bug fixes, backwards compatible (e.g. 0.1.0 -> 0.2.0).
the upgrade should Just Work and there are no breaking changes in
the app and the recipe config. "it should go fine".
patch: bug fixes, backwards compatible (e.g. 0.0.1 -> 0.0.2). this upgrade
should also Just Work and is mostly to do with minor bug fixes
and/or security patches. "nothing to worry about".
`, latestRelease, changeOverview))
var chosenBumpType string
prompt := &survey.Select{
Message: fmt.Sprintf("select recipe version increment type"),
Options: []string{i18n.G("major"), i18n.G("minor"), i18n.G("patch")},
}
if err := survey.AskOne(prompt, &chosenBumpType); err != nil {
return err
}
SetBumpType(chosenBumpType)
}
return nil
}
// GetBumpType figures out which bump type is specified
func GetBumpType() string {
var bumpType string
if Major {
bumpType = i18n.G("major")
} else if Minor {
bumpType = i18n.G("minor")
} else if Patch {
bumpType = i18n.G("patch")
} else {
log.Fatal(i18n.G("no version bump type specififed?"))
}
return bumpType
}
// SetBumpType figures out which bump type is specified
func SetBumpType(bumpType string) {
if bumpType == i18n.G("major") {
Major = true
} else if bumpType == i18n.G("minor") {
Minor = true
} else if bumpType == i18n.G("patch") {
Patch = true
} else {
log.Fatal(i18n.G("no version bump type specififed?"))
}
}
// GetMainAppImage retrieves the main 'app' image name
func GetMainAppImage(recipe recipe.Recipe) (string, error) {
var path string
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return "", err
}
for _, service := range config.Services {
if service.Name == "app" {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return "", err
}
path = reference.Path(img)
path = formatter.StripTagMeta(path)
return path, nil
}
}
if path == "" {
return path, errors.New(i18n.G("%s has no main 'app' service?", recipe.Name))
}
return path, nil
}
+140 -23
View File
@@ -1,60 +1,177 @@
package internal
import (
"errors"
"strings"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/AlecAivazis/survey/v2"
)
// ValidateRecipe ensures the recipe arg is valid.
func ValidateRecipe(c *cli.Context) recipe.Recipe {
recipeName := c.Args().First()
func ValidateRecipe(args []string, cmdName string) recipe.Recipe {
var recipeName string
if len(args) > 0 {
recipeName = args[0]
}
var recipes []string
catl, err := recipe.ReadRecipeCatalogue(Offline)
if err != nil {
log.Fatal(err)
}
knownRecipes := make(map[string]bool)
for name := range catl {
knownRecipes[name] = true
}
localRecipes, err := recipe.GetRecipesLocal()
if err != nil {
log.Debug(i18n.G("can't read local recipes: %s", err))
} else {
for _, recipeLocal := range localRecipes {
if _, ok := knownRecipes[recipeLocal]; !ok {
knownRecipes[recipeLocal] = true
}
}
}
for recipeName := range knownRecipes {
recipes = append(recipes, recipeName)
}
if recipeName == "" && !NoInput {
prompt := &survey.Select{
Message: i18n.G("Select recipe"),
Options: recipes,
}
if err := survey.AskOne(prompt, &recipeName); err != nil {
log.Fatal(err)
}
}
if recipeName == "" {
ShowSubcommandHelpAndError(c, errors.New("no recipe provided"))
log.Fatal(i18n.G("no recipe name provided"))
}
recipe, err := recipe.Get(recipeName)
if _, ok := knownRecipes[recipeName]; !ok {
if !strings.Contains(recipeName, "/") {
log.Fatal(i18n.G("no recipe '%s' exists?", recipeName))
}
}
chosenRecipe := recipe.Get(recipeName)
if err := chosenRecipe.EnsureExists(); err != nil {
log.Fatal(err)
}
_, err = chosenRecipe.GetComposeConfig(nil)
if err != nil {
logrus.Fatal(err)
if cmdName == i18n.G("generate") {
if strings.Contains(err.Error(), "missing a compose") {
log.Fatal(err)
}
log.Warn(err)
} else {
if strings.Contains(err.Error(), "template_driver is not allowed") {
log.Warn(i18n.G("ensure %s recipe compose.* files include \"version: '3.8'\"", recipeName))
}
log.Fatal(i18n.G("unable to validate recipe: %s", err))
}
}
logrus.Debugf("validated '%s' as recipe argument", recipeName)
log.Debug(i18n.G("validated %s as recipe argument", recipeName))
return recipe
return chosenRecipe
}
// ValidateApp ensures the app name arg is valid.
func ValidateApp(c *cli.Context) config.App {
appName := c.Args().First()
if appName == "" {
ShowSubcommandHelpAndError(c, errors.New("no app provided"))
func ValidateApp(args []string) app.App {
if len(args) == 0 {
log.Fatal(i18n.G("no app provided"))
}
appName := args[0]
app, err := app.Get(appName)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
logrus.Debugf("validated '%s' as app argument", appName)
log.Debug(i18n.G("validated %s as app argument", appName))
return app
}
// ValidateDomain ensures the domain name arg is valid.
func ValidateDomain(c *cli.Context) string {
domainName := c.Args().First()
if domainName == "" {
ShowSubcommandHelpAndError(c, errors.New("no domain provided"))
func ValidateDomain(args []string) string {
var domainName string
if len(args) > 0 {
domainName = args[0]
}
logrus.Debugf("validated '%s' as domain argument", domainName)
if domainName == "" && !NoInput {
prompt := &survey.Input{
Message: i18n.G("Specify a domain name"),
Default: "1312.net",
}
if err := survey.AskOne(prompt, &domainName); err != nil {
log.Fatal(err)
}
}
if domainName == "" {
log.Fatal(i18n.G("no domain provided"))
}
log.Debug(i18n.G("validated %s as domain argument", domainName))
return domainName
}
// ValidateServer ensures the server name arg is valid.
func ValidateServer(args []string) string {
var serverName string
if len(args) > 0 {
serverName = args[0]
}
serverNames, err := config.ReadServerNames()
if err != nil {
log.Fatal(err)
}
if serverName == "" && !NoInput {
prompt := &survey.Select{
Message: i18n.G("Specify a server name"),
Options: serverNames,
}
if err := survey.AskOne(prompt, &serverName); err != nil {
log.Fatal(err)
}
}
matched := false
for _, name := range serverNames {
if name == serverName {
matched = true
}
}
if serverName == "" {
log.Fatal(i18n.G("no server provided"))
}
if !matched {
log.Fatal(i18n.G("server doesn't exist?"))
}
log.Debug(i18n.G("validated %s as server argument", serverName))
return serverName
}
+38
View File
@@ -0,0 +1,38 @@
package recipe
import (
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra"
)
// translators: `abra recipe diff` aliases. use a comma separated list of aliases
// with no spaces in between
var recipeDiffAliases = i18n.G("d")
var RecipeDiffCommand = &cobra.Command{
// translators: `recipe diff` command
Use: i18n.G("diff <recipe> [flags]"),
Aliases: strings.Split(recipeDiffAliases, ","),
// translators: Short description for `recipe diff` command
Short: i18n.G("Show unstaged changes in recipe config"),
Long: i18n.G("This command requires /usr/bin/git."),
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
r := internal.ValidateRecipe(args, cmd.Name())
if err := gitPkg.DiffUnstaged(r.Dir); err != nil {
log.Fatal(err)
}
},
}
+142
View File
@@ -0,0 +1,142 @@
package recipe
import (
"os"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/go-git/go-git/v5"
gitCfg "github.com/go-git/go-git/v5/config"
"github.com/spf13/cobra"
)
// translators: `abra recipe fetch` aliases. use a comma separated list of aliases
// with no spaces in between
var recipeFetchAliases = i18n.G("f")
var RecipeFetchCommand = &cobra.Command{
// translators: `recipe fetch` command
Use: i18n.G("fetch [recipe | --all] [flags]"),
Aliases: strings.Split(recipeFetchAliases, ","),
// translators: Short description for `recipe fetch` command
Short: i18n.G("Clone recipe(s) locally"),
Long: i18n.G(`Using "--force/-f" Git syncs an existing recipe. It does not erase unstaged changes.`),
Args: cobra.RangeArgs(0, 1),
Example: i18n.G(` # fetch from recipe catalogue
abra recipe fetch gitea
# fetch from remote recipe
abra recipe fetch git.foo.org/recipes/myrecipe
# fetch with ssh remote for hacking
abra recipe fetch gitea --ssh`),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
var recipeName string
if len(args) > 0 {
recipeName = args[0]
}
if recipeName == "" && !fetchAllRecipes {
log.Fatal(i18n.G("missing [recipe] or --all/-a"))
}
if recipeName != "" && fetchAllRecipes {
log.Fatal(i18n.G("cannot use [recipe] and --all/-a together"))
}
if recipeName != "" {
r := recipe.Get(recipeName)
if _, err := os.Stat(r.Dir); !os.IsNotExist(err) {
if !force {
log.Warn(i18n.G("%s is already fetched", r.Name))
return
}
}
r = internal.ValidateRecipe(args, cmd.Name())
if sshRemote {
if r.SSHURL == "" {
log.Warn(i18n.G("unable to discover SSH remote for %s", r.Name))
return
}
repo, err := git.PlainOpen(r.Dir)
if err != nil {
log.Fatal(i18n.G("unable to open %s: %s", r.Dir, err))
}
if err = repo.DeleteRemote("origin"); err != nil {
log.Fatal(i18n.G("unable to remove default remote in %s: %s", r.Dir, err))
}
if _, err := repo.CreateRemote(&gitCfg.RemoteConfig{
Name: "origin",
URLs: []string{r.SSHURL},
}); err != nil {
log.Fatal(i18n.G("unable to set SSH remote in %s: %s", r.Dir, err))
}
}
return
}
catalogue, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
log.Fatal(err)
}
catlBar := formatter.CreateProgressbar(len(catalogue), i18n.G("fetching latest recipes..."))
ensureCtx := internal.GetEnsureContext()
for recipeName := range catalogue {
r := recipe.Get(recipeName)
if err := r.Ensure(ensureCtx); err != nil {
log.Error(err)
}
catlBar.Add(1)
}
},
}
var (
fetchAllRecipes bool
sshRemote bool
force bool
)
func init() {
RecipeFetchCommand.Flags().BoolVarP(
&fetchAllRecipes,
i18n.G("all"),
i18n.GC("a", "recipe fetch"),
false,
i18n.G("fetch all recipes"),
)
RecipeFetchCommand.Flags().BoolVarP(
&sshRemote,
i18n.G("ssh"),
i18n.G("s"),
false,
i18n.G("automatically set ssh remote"),
)
RecipeFetchCommand.Flags().BoolVarP(
&force,
i18n.G("force"),
i18n.G("f"),
false,
i18n.G("force re-fetch"),
)
}
+124 -81
View File
@@ -1,106 +1,149 @@
package recipe
import (
"fmt"
"os"
"strconv"
"strings"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/tagcmp"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra"
)
var recipeLintCommand = &cli.Command{
Name: "lint",
Usage: "Lint a recipe",
Aliases: []string{"l"},
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
// translators: `abra recipe lint` aliases. use a comma separated list of
// aliases with no spaces in between
var recipeLintAliases = i18n.G("l")
expectedVersion := false
if recipe.Config.Version == "3.8" {
expectedVersion = true
var RecipeLintCommand = &cobra.Command{
// translators: `recipe lint` command
Use: i18n.G("lint <recipe> [flags]"),
// translators: Short description for `recipe lint` command
Short: i18n.G("Lint a recipe"),
Aliases: strings.Split(recipeLintAliases, ","),
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name())
if err := recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
envSampleProvided := false
envSample := fmt.Sprintf("%s/%s/.env.sample", config.APPS_DIR, recipe.Name)
if _, err := os.Stat(envSample); !os.IsNotExist(err) {
envSampleProvided = true
} else if err != nil {
logrus.Fatal(err)
headers := []string{
i18n.G("ref"),
i18n.G("rule"),
i18n.G("severity"),
i18n.G("satisfied"),
i18n.G("skipped"),
i18n.G("resolve"),
}
serviceNamedApp := false
traefikEnabled := false
healthChecksForAllServices := true
allImagesTagged := true
noUnstableTags := true
semverLikeTags := true
for _, service := range recipe.Config.Services {
if service.Name == "app" {
serviceNamedApp = true
}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
for label := range service.Deploy.Labels {
if label == "traefik.enable" {
if service.Deploy.Labels[label] == "true" {
traefikEnabled = true
table.Headers(headers...)
hasError := false
var rows [][]string
var warnMessages []string
for level := range lint.LintRules {
for _, rule := range lint.LintRules[level] {
if onlyError && rule.Level != "error" {
log.Debug(i18n.G("skipping %s, does not have level \"error\"", rule.Ref))
continue
}
skipped := false
if rule.Skip(recipe) {
skipped = true
}
skippedOutput := "-"
if skipped {
skippedOutput = "✅"
}
satisfied := false
if !skipped {
ok, err := rule.Function(recipe)
if err != nil {
warnMessages = append(warnMessages, err.Error())
}
if !ok && rule.Level == i18n.G("error") {
hasError = true
}
if ok {
satisfied = true
}
}
}
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
logrus.Fatal(err)
}
if reference.IsNameOnly(img) {
allImagesTagged = false
}
satisfiedOutput := "✅"
if !satisfied {
satisfiedOutput = "❌"
if skipped {
satisfiedOutput = "-"
}
}
tag := img.(reference.NamedTagged).Tag()
if tag == "latest" {
noUnstableTags = false
}
row := []string{
rule.Ref,
rule.Description,
rule.Level,
satisfiedOutput,
skippedOutput,
rule.HowToResolve,
}
if !tagcmp.IsParsable(tag) {
semverLikeTags = false
}
if service.HealthCheck == nil {
healthChecksForAllServices = false
rows = append(rows, row)
table.Row(row...)
}
}
tableCol := []string{"rule", "satisfied"}
table := formatter.CreateTable(tableCol)
table.Append([]string{"compose files have the expected version", strconv.FormatBool(expectedVersion)})
table.Append([]string{"environment configuration is provided", strconv.FormatBool(envSampleProvided)})
table.Append([]string{"recipe contains a service named 'app'", strconv.FormatBool(serviceNamedApp)})
table.Append([]string{"traefik routing enabled on at least one service", strconv.FormatBool(traefikEnabled)})
table.Append([]string{"all services have a healthcheck enabled", strconv.FormatBool(healthChecksForAllServices)})
table.Append([]string{"all images are using a tag", strconv.FormatBool(allImagesTagged)})
table.Append([]string{"no usage of unstable 'latest' tags", strconv.FormatBool(noUnstableTags)})
table.Append([]string{"all tags are using a semver-like format", strconv.FormatBool(semverLikeTags)})
table.Render()
if len(rows) > 0 {
if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
return nil
},
BashComplete: func(c *cli.Context) {
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for name, _ := range catl {
fmt.Println(name)
for _, warnMsg := range warnMessages {
log.Warn(warnMsg)
}
if hasError {
log.Warn(i18n.G("critical errors present in %s config", recipe.Name))
}
}
},
}
var (
onlyError bool
)
func init() {
RecipeLintCommand.Flags().BoolVarP(
&internal.Chaos,
i18n.G("chaos"),
i18n.G("C"),
false,
i18n.G("ignore uncommitted recipes changes"),
)
RecipeLintCommand.Flags().BoolVarP(
&onlyError,
i18n.G("error"),
i18n.G("e"),
false,
i18n.G("only show errors"),
)
}
+98 -21
View File
@@ -3,37 +3,114 @@ package recipe
import (
"fmt"
"sort"
"strconv"
"strings"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/catalogue"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra"
)
var recipeListCommand = &cli.Command{
Name: "list",
Usage: "List available recipes",
Aliases: []string{"ls"},
Action: func(c *cli.Context) error {
catl, err := catalogue.ReadRecipeCatalogue()
// translators: `abra recipe list` aliases. use a comma separated list of
// aliases with no spaces in between
var recipeListAliases = i18n.G("ls")
var RecipeListCommand = &cobra.Command{
// translators: `recipe list` command
Use: i18n.G("list"),
// translators: Short description for `recipe list` command
Short: i18n.G("List recipes"),
Aliases: strings.Split(recipeListAliases, ","),
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
logrus.Fatal(err.Error())
log.Fatal(err)
}
recipes := catl.Flatten()
sort.Sort(catalogue.ByRecipeName(recipes))
sort.Sort(recipe.ByRecipeName(recipes))
tableCol := []string{"name", "category", "status"}
table := formatter.CreateTable(tableCol)
for _, recipe := range recipes {
status := fmt.Sprintf("%v", recipe.Features.Status)
tableRow := []string{recipe.Name, recipe.Category, status}
table.Append(tableRow)
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table.Render()
headers := []string{
i18n.G("name"),
i18n.G("category"),
i18n.G("status"),
i18n.G("healthcheck"),
i18n.G("backups"),
i18n.G("email"),
i18n.G("tests"),
i18n.G("SSO"),
}
return nil
table.Headers(headers...)
var rows [][]string
for _, recipe := range recipes {
row := []string{
recipe.Name,
recipe.Category,
strconv.Itoa(recipe.Features.Status),
recipe.Features.Healthcheck,
recipe.Features.Backups,
recipe.Features.Email,
recipe.Features.Tests,
recipe.Features.SSO,
}
if pattern != "" {
if strings.Contains(recipe.Name, pattern) {
table.Row(row...)
rows = append(rows, row)
}
} else {
table.Row(row...)
rows = append(rows, row)
}
}
if len(rows) > 0 {
if internal.MachineReadable {
out, err := formatter.ToJSON(headers, rows)
if err != nil {
log.Fatal(i18n.G("unable to render to JSON: %s", err))
}
fmt.Println(out)
return
}
if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
}
},
}
var (
pattern string
)
func init() {
RecipeListCommand.Flags().BoolVarP(
&internal.MachineReadable,
i18n.G("machine"),
i18n.G("m"),
false,
i18n.G("print machine-readable output"),
)
RecipeListCommand.Flags().StringVarP(
&pattern,
i18n.G("pattern"),
i18n.G("p"),
"",
i18n.G("filter by recipe"),
)
}
+104 -48
View File
@@ -1,79 +1,135 @@
package recipe
import (
"bytes"
"fmt"
"os"
"path"
"strings"
"text/template"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/git"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra"
)
var recipeNewCommand = &cli.Command{
Name: "new",
Usage: "Create a new recipe",
Aliases: []string{"n"},
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
// recipeMetadata is the recipe metadata for the README.md
type recipeMetadata struct {
Name string
Description string
Category string
Status string
Image string
Healthcheck string
Backups string
Email string
Tests string
SSO string
}
directory := path.Join(config.APPS_DIR, recipe.Name)
if _, err := os.Stat(directory); !os.IsNotExist(err) {
logrus.Fatalf("'%s' recipe directory already exists?", directory)
return nil
// translators: `abra recipe new` aliases. use a comma separated list of
// aliases with no spaces in between
var recipeNewAliases = i18n.G("n")
var RecipeNewCommand = &cobra.Command{
// translators: `recipe new` command
Use: i18n.G("new <recipe> [flags]"),
Aliases: strings.Split(recipeNewAliases, ","),
// translators: Short description for `abra recipe new` command
Short: i18n.G("Create a new recipe"),
Long: i18n.G(`A community managed recipe template is used.`),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
recipeName := args[0]
r := recipe.Get(recipeName)
if _, err := os.Stat(r.Dir); !os.IsNotExist(err) {
log.Fatal(i18n.G("%s recipe directory already exists?", r.Dir))
}
url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL)
if err := git.Clone(directory, url); err != nil {
return err
url := i18n.G("%s/example.git", config.REPOS_BASE_URL)
if err := git.Clone(r.Dir, url); err != nil {
log.Fatal(err)
}
gitRepo := path.Join(config.APPS_DIR, recipe.Name, ".git")
gitRepo := path.Join(r.Dir, ".git")
if err := os.RemoveAll(gitRepo); err != nil {
logrus.Fatal(err)
return nil
log.Fatal(err)
}
logrus.Debugf("removed git repo in '%s'", gitRepo)
log.Debug(i18n.G("removed .git repo in %s", gitRepo))
toParse := []string{
path.Join(config.APPS_DIR, recipe.Name, "README.md"),
path.Join(config.APPS_DIR, recipe.Name, ".env.sample"),
path.Join(config.APPS_DIR, recipe.Name, ".drone.yml"),
}
for _, path := range toParse {
file, err := os.OpenFile(path, os.O_RDWR, 0755)
if err != nil {
logrus.Fatal(err)
return nil
}
meta := newRecipeMeta(recipeName)
for _, path := range []string{r.ReadmePath, r.SampleEnvPath} {
tpl, err := template.ParseFiles(path)
if err != nil {
logrus.Fatal(err)
return nil
log.Fatal(err)
}
// TODO: ask for description and probably other things so that the
// template repository is more "ready" to go than the current best-guess
// mode of templating
if err := tpl.Execute(file, struct {
Name string
Description string
}{recipe.Name, "TODO"}); err != nil {
logrus.Fatal(err)
return nil
var templated bytes.Buffer
if err := tpl.Execute(&templated, meta); err != nil {
log.Fatal(err)
}
if err := os.WriteFile(path, templated.Bytes(), 0o644); err != nil {
log.Fatal(err)
}
}
logrus.Infof(
"new recipe '%s' created in %s, happy hacking!\n",
recipe.Name, path.Join(config.APPS_DIR, recipe.Name),
)
if err := git.Init(r.Dir, true, gitName, gitEmail); err != nil {
log.Fatal(err)
}
return nil
log.Info(i18n.G("new recipe '%s' created: %s", recipeName, path.Join(r.Dir)))
log.Info(i18n.G("happy hacking 🎉"))
},
}
// newRecipeMeta creates a new recipeMetadata instance with defaults
func newRecipeMeta(recipeName string) recipeMetadata {
return recipeMetadata{
Name: recipeName,
Description: "> One line description of the recipe",
Category: "Apps",
Status: "0",
Image: fmt.Sprintf("[`%s`](https://hub.docker.com/r/%s), 4, upstream", recipeName, recipeName),
Healthcheck: "No",
Backups: "No",
Email: "No",
Tests: "No",
SSO: "No",
}
}
var (
gitName string
gitEmail string
)
func init() {
RecipeNewCommand.Flags().StringVarP(
&gitName,
i18n.G("git-name"),
i18n.G("N"),
"",
i18n.G("Git (user) name to do commits with"),
)
RecipeNewCommand.Flags().StringVarP(
&gitEmail,
i18n.G("git-email"),
i18n.G("e"),
"",
i18n.G("Git email name to do commits with"),
)
}
+23 -19
View File
@@ -1,26 +1,30 @@
package recipe
import (
"github.com/urfave/cli/v2"
"strings"
"coopcloud.tech/abra/pkg/i18n"
"github.com/spf13/cobra"
)
// translators: `abra recipe` aliases. use a comma separated list of aliases
// with no spaces in between
var recipeAliases = i18n.G("r")
// RecipeCommand defines all recipe related sub-commands.
var RecipeCommand = &cli.Command{
Name: "recipe",
Usage: "Manage recipes",
ArgsUsage: "<recipe>",
Aliases: []string{"r"},
Description: `
A recipe is a blueprint for an app. It is a bunch of configuration files which
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.
`,
Subcommands: []*cli.Command{
recipeListCommand,
recipeVersionCommand,
recipeNewCommand,
recipeUpgradeCommand,
recipeSyncCommand,
recipeLintCommand,
},
var RecipeCommand = &cobra.Command{
// translators: `recipe` command group
Use: i18n.G("recipe [cmd] [args] [flags]"),
Aliases: strings.Split(recipeAliases, ","),
// translators: Short description for `recipe` command group
Short: i18n.G("Manage recipes"),
Long: i18n.G(`A recipe is a blueprint for an app.
It is a bunch of config files which describe how to deploy and maintain an app.
Recipes are maintained by the Co-op Cloud community and you can use Abra to
read them, deploy them and create apps for you.
Anyone who uses a recipe can become a maintainer. Maintainers typically make
sure the recipe is in good working order and the config upgraded in a timely
manner.`),
}
+652
View File
@@ -0,0 +1,652 @@
package recipe
import (
"errors"
"fmt"
"os"
"path"
"strconv"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/distribution/reference"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/spf13/cobra"
)
// Errors
var errEmptyVersionsInCatalogue = errors.New(i18n.G("catalogue versions list is unexpectedly empty"))
// translators: `abra recipe release` aliases. use a comma separated list of
// aliases with no spaces in between
var recipeReleaseAliases = i18n.G("rl")
var RecipeReleaseCommand = &cobra.Command{
// translators: `recipe release` command
Use: i18n.G("release <recipe> [version] [flags]"),
Aliases: strings.Split(recipeReleaseAliases, ","),
// translators: Short description for `recipe release` command
Short: i18n.G("Release a new recipe version"),
Long: i18n.G(`Create a new version of a recipe.
These versions are then published on the Co-op Cloud recipe catalogue. These
versions take the following form:
a.b.c+x.y.z
Where the "a.b.c" part is a semantic version determined by the maintainer. The
"x.y.z" part is the image tag of the recipe "app" service (the main container
which contains the software to be used, by naming convention).
We maintain a semantic versioning scheme ("a.b.c") alongside the recipe
versioning scheme ("x.y.z") in order to maximise the chances that the nature of
recipe updates are properly communicated. I.e. developers of an app might
publish a minor version but that might lead to changes in the recipe which are
major and therefore require intervention while doing the upgrade work.
This command will publish your new release to git.coopcloud.tech. This
requires that you have permission to git push to these repositories and have
your SSH keys configured on your account. Enable ssh-agent and make sure to add
your private key and enter your passphrase beforehand.
eval ` + "`ssh-agent`" + `
ssh-add ~/.ssh/<my-ssh-private-key-for-git-coopcloud-tech>`),
Example: ` # publish release
eval ` + "`ssh-agent`" + `
ssh-add ~/.ssh/id_ed25519
abra recipe release gitea`,
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string,
) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.RecipeNameComplete()
case 1:
return autocomplete.RecipeVersionComplete(args[0])
default:
return nil, cobra.ShellCompDirectiveDefault
}
},
Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name())
imagesTmp, err := GetImageVersions(recipe)
if err != nil {
log.Fatal(err)
}
mainApp, err := internal.GetMainAppImage(recipe)
if err != nil {
log.Fatal(err)
}
mainAppVersion := imagesTmp[mainApp]
if mainAppVersion == "" {
log.Fatal(i18n.G("main app service version for %s is empty?", recipe.Name))
}
repo, err := git.PlainOpen(recipe.Dir)
if err != nil {
log.Fatal(err)
}
preCommitHead, err := repo.Head()
if err != nil {
log.Fatal(err)
}
isClean, err := gitPkg.IsClean(recipe.Dir)
if err != nil {
log.Fatal(err)
}
if !isClean {
log.Fatal(i18n.G("working directory not clean in %s, aborting", recipe.Dir))
}
tags, err := recipe.Tags()
if err != nil {
log.Fatal(err)
}
var tagString string
if len(args) == 2 {
tagString = args[1]
}
if (internal.Major || internal.Minor || internal.Patch) && tagString != "" {
log.Fatal(i18n.G("cannot specify tag and bump type at the same time"))
}
if len(tags) == 0 && tagString == "" {
log.Warn(i18n.G("no git tags found for %s", recipe.Name))
if internal.NoInput {
log.Fatal(i18n.G("unable to continue, input required for initial version"))
}
fmt.Println(i18n.G(`
The following options are two types of initial semantic version that you can
pick for %s that will be published in the recipe catalogue. This follows the
semver convention (more on https://semver.org), here is a short cheatsheet
0.1.0: development release, still hacking. when you make a major upgrade
you increment the "y" part (i.e. 0.1.0 -> 0.2.0) and only move to
using the "x" part when things are stable.
1.0.0: public release, assumed to be working. you already have a stable
and reliable deployment of this app and feel relatively confident
about it.
If you want people to be able alpha test your current config for %s but don't
think it is quite reliable, go with 0.1.0 and people will know that things are
likely to change.
`, recipe.Name, recipe.Name))
var chosenVersion string
edPrompt := &survey.Select{
Message: i18n.G("which version do you want to begin with?"),
Options: []string{"0.1.0", "1.0.0"},
}
if err := survey.AskOne(edPrompt, &chosenVersion); err != nil {
log.Fatal(err)
}
tagString = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion)
}
if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
catl, err := recipePkg.ReadRecipeCatalogue(false)
if err != nil {
log.Fatal(err)
}
changesTable, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
latestRelease := tags[len(tags)-1]
latestRecipeVersion, err := getLatestVersion(recipe, catl)
if err != nil && err != errEmptyVersionsInCatalogue {
log.Fatal(err)
}
changesTable.Headers(i18n.G("SERVICE"), latestRelease, i18n.G("PROPOSED CHANGES"))
allRecipeVersions := catl[recipe.Name].Versions
for _, recipeVersion := range allRecipeVersions {
if serviceVersions, ok := recipeVersion[latestRecipeVersion]; ok {
for serviceName := range serviceVersions {
serviceMeta := serviceVersions[serviceName]
existingImageTag := fmt.Sprintf("%s:%s", serviceMeta.Image, serviceMeta.Tag)
newImageTag := fmt.Sprintf("%s:%s", serviceMeta.Image, imagesTmp[serviceMeta.Image])
if existingImageTag == newImageTag {
continue
}
changesTable.Row([]string{serviceName, existingImageTag, newImageTag}...)
}
}
}
changeOverview := changesTable.Render()
if err := internal.PromptBumpType("", latestRelease, changeOverview); err != nil {
log.Fatal(err)
}
}
if tagString == "" {
var lastGitTag tagcmp.Tag
iter, err := repo.Tags()
if err != nil {
log.Fatal(err)
}
if err := iter.ForEach(func(ref *plumbing.Reference) error {
obj, err := repo.TagObject(ref.Hash())
if err != nil {
log.Fatal(i18n.G("tag at commit %s is unannotated or otherwise broken", ref.Hash()))
return err
}
tagcmpTag, err := tagcmp.Parse(obj.Name)
if err != nil {
return err
}
if (lastGitTag == tagcmp.Tag{}) {
lastGitTag = tagcmpTag
} else if tagcmpTag.IsGreaterThan(lastGitTag) {
lastGitTag = tagcmpTag
}
return nil
}); err != nil {
log.Fatal(err)
}
// bumpType is used to decide what part of the tag should be incremented
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 {
// a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 {
log.Fatal(i18n.G("you can only use one version flag: --major, --minor or --patch"))
}
}
newTag := lastGitTag
if bumpType > 0 {
if internal.Patch {
now, err := strconv.Atoi(newTag.Patch)
if err != nil {
log.Fatal(err)
}
newTag.Patch = strconv.Itoa(now + 1)
} else if internal.Minor {
now, err := strconv.Atoi(newTag.Minor)
if err != nil {
log.Fatal(err)
}
newTag.Patch = "0"
newTag.Minor = strconv.Itoa(now + 1)
} else if internal.Major {
now, err := strconv.Atoi(newTag.Major)
if err != nil {
log.Fatal(err)
}
newTag.Patch = "0"
newTag.Minor = "0"
newTag.Major = strconv.Itoa(now + 1)
}
}
newTag.Metadata = mainAppVersion
log.Debug(i18n.G("choosing %s as new version for %s", newTag.String(), recipe.Name))
tagString = newTag.String()
}
if _, err := tagcmp.Parse(tagString); err != nil {
log.Fatal(i18n.G("invalid version %s specified", tagString))
}
for _, tag := range tags {
previousTagLeftHand := strings.Split(tag, "+")[0]
newTagStringLeftHand := strings.Split(tagString, "+")[0]
if previousTagLeftHand == newTagStringLeftHand {
log.Fatal(i18n.G("%s+... conflicts with a previous release: %s", newTagStringLeftHand, tag))
}
}
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
if cleanErr := cleanTag(recipe, tagString); cleanErr != nil {
log.Fatal(i18n.G("unable to clean up tag after failed release attempt: %s", cleanErr))
}
if resetErr := resetCommit(recipe, preCommitHead); resetErr != nil {
log.Fatal(i18n.G("unable to reset commit after failed release attempt: %s", resetErr))
}
log.Error(err)
log.Fatal(i18n.G("release failed. any changes made have been reverted"))
}
},
}
// GetImageVersions retrieves image versions for a recipe
func GetImageVersions(recipe recipePkg.Recipe) (map[string]string, error) {
services := make(map[string]string)
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return nil, err
}
missingTag := false
for _, service := range config.Services {
if service.Image == "" {
continue
}
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return services, err
}
path := reference.Path(img)
path = formatter.StripTagMeta(path)
var tag string
switch img.(type) {
case reference.NamedTagged:
tag = img.(reference.NamedTagged).Tag()
case reference.Named:
if service.Name == "app" {
missingTag = true
}
continue
}
services[path] = tag
}
if missingTag {
return services, errors.New(i18n.G("app service is missing image tag?"))
}
return services, nil
}
// createReleaseFromTag creates a new release based on a supplied recipe version string
func createReleaseFromTag(recipe recipePkg.Recipe, tagString, mainAppVersion string) error {
var err error
repo, err := git.PlainOpen(recipe.Dir)
if err != nil {
return err
}
mainService := "app"
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", tagString)
if !internal.Dry {
if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil {
log.Fatal(err)
}
} else {
log.Info(i18n.G("dry run: not syncing label %s for recipe %s", tagString, recipe.Name))
}
if err := addReleaseNotes(recipe, tagString); err != nil {
return errors.New(i18n.G("failed to add release notes: %s", err.Error()))
}
if err := commitRelease(recipe, tagString); err != nil {
return errors.New(i18n.G("failed to commit changes: %s", err.Error()))
}
if err := tagRelease(tagString, repo); err != nil {
return errors.New(i18n.G("failed to tag release: %s", err.Error()))
}
if err := pushRelease(recipe, tagString); err != nil {
return errors.New(i18n.G("failed to publish new release: %s", err.Error()))
}
return nil
}
// btoi converts a boolean value into an integer
func btoi(b bool) int {
if b {
return 1
}
return 0
}
// getTagCreateOptions constructs git tag create options
func getTagCreateOptions(tag string) (git.CreateTagOptions, error) {
msg := i18n.G("chore: publish %s release", tag)
return git.CreateTagOptions{Message: msg}, nil
}
// addReleaseNotes checks if the release/next release note exists and moves the
// file to release/<tag>.
func addReleaseNotes(recipe recipePkg.Recipe, tag string) error {
releaseDir := path.Join(recipe.Dir, "release")
if _, err := os.Stat(releaseDir); errors.Is(err, os.ErrNotExist) {
if err := os.Mkdir(releaseDir, 0755); err != nil {
return err
}
}
tagReleaseNotePath := path.Join(releaseDir, tag)
if _, err := os.Stat(tagReleaseNotePath); err == nil {
// Release note for current tag already exist exists.
return nil
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
var addNextAsReleaseNotes bool
nextReleaseNotePath := path.Join(releaseDir, "next")
if _, err := os.Stat(nextReleaseNotePath); err == nil {
// release/next note exists. Move it to release/<tag>
if internal.Dry {
log.Debug(i18n.G("dry run: move release note from 'next' to %s", tag))
return nil
}
if !internal.NoInput {
prompt := &survey.Confirm{
Message: i18n.G("use release note in release/next?"),
}
if err := survey.AskOne(prompt, &addNextAsReleaseNotes); err != nil {
return err
}
if !addNextAsReleaseNotes {
return nil
}
}
if err := os.Rename(nextReleaseNotePath, tagReleaseNotePath); err != nil {
return err
}
if err := gitPkg.Add(recipe.Dir, path.Join("release", "next"), internal.Dry); err != nil {
return err
}
if err := gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry); err != nil {
return err
}
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
// NOTE(d1): No release note exists for the current release. Or, we've
// already used release/next as the release note
if internal.NoInput || addNextAsReleaseNotes {
return nil
}
prompt := &survey.Input{
Message: i18n.G("add release note? (leave empty to skip)"),
}
var releaseNote string
if err := survey.AskOne(prompt, &releaseNote); err != nil {
return err
}
if releaseNote == "" {
return nil
}
if err := os.WriteFile(tagReleaseNotePath, []byte(releaseNote), 0o644); err != nil {
return err
}
if err := gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry); err != nil {
return err
}
return nil
}
func commitRelease(recipe recipePkg.Recipe, tag string) error {
if internal.Dry {
log.Debug(i18n.G("dry run: no changes committed"))
return nil
}
isClean, err := gitPkg.IsClean(recipe.Dir)
if err != nil {
return err
}
if isClean {
if !internal.Dry {
return errors.New(i18n.G("no changes discovered in %s, nothing to publish?", recipe.Dir))
}
}
msg := fmt.Sprintf("chore: publish %s release", tag)
if err := gitPkg.Commit(recipe.Dir, msg, internal.Dry); err != nil {
return err
}
return nil
}
func tagRelease(tagString string, repo *git.Repository) error {
if internal.Dry {
log.Debug(i18n.G("dry run: no git tag created (%s)", tagString))
return nil
}
head, err := repo.Head()
if err != nil {
return err
}
createTagOptions, err := getTagCreateOptions(tagString)
if err != nil {
return err
}
_, err = repo.CreateTag(tagString, head.Hash(), &createTagOptions)
if err != nil {
return err
}
hash := formatter.SmallSHA(head.Hash().String())
log.Debug(i18n.G("created tag %s at %s", tagString, hash))
return nil
}
func pushRelease(recipe recipePkg.Recipe, tagString string) error {
if internal.Dry {
log.Info(i18n.G("dry run: no changes published"))
return nil
}
if os.Getenv("SSH_AUTH_SOCK") == "" {
return errors.New(i18n.G("ssh-agent not found. see \"abra recipe release --help\" and try again"))
}
if err := recipe.Push(internal.Dry); err != nil {
return err
}
url := fmt.Sprintf("%s/src/tag/%s", recipe.GitURL, tagString)
log.Info(i18n.G("new release published: %s", url))
return nil
}
// resetCommit hard resets to the state before release was started.
// This will only remove changes made by the release process due to requiring
// a clean working directory.
func resetCommit(recipe recipePkg.Recipe, head *plumbing.Reference) error {
repo, err := git.PlainOpen(recipe.Dir)
if err != nil {
return errors.New(i18n.G("unable to open repo in %s: %s", recipe.Dir, err))
}
worktree, err := repo.Worktree()
if err != nil {
return errors.New(i18n.G("unable to open work tree in %s: %s", recipe.Dir, err))
}
opts := &git.ResetOptions{Commit: head.Hash(), Mode: git.HardReset}
if err := worktree.Reset(opts); err != nil {
return errors.New(i18n.G("unable to hard reset %s: %s", recipe.Dir, err))
}
log.Debug(i18n.G("reset commit to pre-release state"))
return nil
}
// cleanTag removes a freshly created tag
func cleanTag(recipe recipePkg.Recipe, tag string) error {
repo, err := git.PlainOpen(recipe.Dir)
if err != nil {
return errors.New(i18n.G("unable to open repo in %s: %s", recipe.Dir, err))
}
if err := repo.DeleteTag(tag); err != nil {
if !strings.Contains(err.Error(), "not found") {
return errors.New(i18n.G("unable to delete tag %s: %s", tag, err))
}
}
log.Debug(i18n.G("removed freshly created tag %s", tag))
return nil
}
func getLatestVersion(recipe recipePkg.Recipe, catl recipePkg.RecipeCatalogue) (string, error) {
versions, err := recipePkg.GetRecipeCatalogueVersions(recipe.Name, catl)
if err != nil {
return "", err
}
if len(versions) > 0 {
return versions[len(versions)-1], nil
}
return "", errEmptyVersionsInCatalogue
}
func init() {
RecipeReleaseCommand.Flags().BoolVarP(
&internal.Dry,
i18n.G("dry-run"),
i18n.G("r"),
false,
i18n.G("report changes that would be made"),
)
RecipeReleaseCommand.Flags().BoolVarP(
&internal.Major,
i18n.G("major"),
i18n.G("x"),
false,
i18n.G("increase the major part of the version"),
)
RecipeReleaseCommand.Flags().BoolVarP(
&internal.Minor,
i18n.G("minor"),
i18n.G("y"),
false,
i18n.G("increase the minor part of the version"),
)
RecipeReleaseCommand.Flags().BoolVarP(
&internal.Patch,
i18n.G("patch"),
i18n.G("z"),
false,
i18n.G("increase the patch part of the version"),
)
}
+33
View File
@@ -0,0 +1,33 @@
package recipe
import (
"testing"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/stretchr/testify/assert"
)
func TestGetLatestVersionReturnsErrorWhenVersionsIsEmpty(t *testing.T) {
recipe := recipePkg.Recipe{}
catalogue := recipePkg.RecipeCatalogue{}
_, err := getLatestVersion(recipe, catalogue)
assert.Equal(t, err, errEmptyVersionsInCatalogue)
}
func TestGetLatestVersionReturnsLastVersion(t *testing.T) {
recipe := recipePkg.Recipe{
Name: "test",
}
versions := []map[string]map[string]recipePkg.ServiceMeta{
make(map[string]map[string]recipePkg.ServiceMeta),
make(map[string]map[string]recipePkg.ServiceMeta),
}
versions[0]["0.0.3"] = make(map[string]recipePkg.ServiceMeta)
versions[1]["0.0.2"] = make(map[string]recipePkg.ServiceMeta)
catalogue := make(recipePkg.RecipeCatalogue)
catalogue["test"] = recipePkg.RecipeMeta{
Versions: versions,
}
version, _ := getLatestVersion(recipe, catalogue)
assert.Equal(t, version, "0.0.3")
}
+55
View File
@@ -0,0 +1,55 @@
package recipe
import (
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5"
"github.com/spf13/cobra"
)
// translators: `abra recipe reset` aliases. use a comma separated list of
// aliases with no spaces in between
var recipeResetAliases = i18n.G("rs")
var RecipeResetCommand = &cobra.Command{
// translators: `recipe reset` command
Use: i18n.G("reset <recipe> [flags]"),
Aliases: strings.Split(recipeResetAliases, ","),
// translators: Short description for `recipe reset` command
Short: i18n.G("Remove all unstaged changes from recipe config"),
Long: i18n.G("WARNING: this will delete your changes. Be Careful."),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
r := internal.ValidateRecipe(args, cmd.Name())
repo, err := git.PlainOpen(r.Dir)
if err != nil {
log.Fatal(err)
}
ref, err := repo.Head()
if err != nil {
log.Fatal(err)
}
worktree, err := repo.Worktree()
if err != nil {
log.Fatal(err)
}
opts := &git.ResetOptions{Commit: ref.Hash(), Mode: git.HardReset}
if err := worktree.Reset(opts); err != nil {
log.Fatal(err)
}
},
}
-65
View File
@@ -1,65 +0,0 @@
package recipe
import (
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var recipeSyncCommand = &cli.Command{
Name: "sync",
Usage: "Generate new recipe labels",
Aliases: []string{"s"},
Description: `
This command will generate labels for each service which correspond to the
following format:
coop-cloud.${STACK_NAME}.${SERVICE_NAME}.version=${IMAGE_TAG}-${IMAGE_DIGEST}
The <recipe> configuration will be updated on the local file system. These
labels are consumed by abra in other command invocations and used to determine
the versioning metadata of up-and-running containers are.
`,
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
hasAppService := false
for _, service := range recipe.Config.Services {
if service.Name == "app" {
hasAppService = true
}
}
if !hasAppService {
logrus.Fatal(fmt.Sprintf("no 'app' service defined in '%s'", recipe.Name))
}
for _, service := range recipe.Config.Services {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("detected image '%s' for service '%s'", img, service.Name)
digest, err := client.GetTagDigest(img)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("retrieved digest '%s' for '%s'", digest, img)
tag := img.(reference.NamedTagged).Tag()
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.%s.version=%s-%s", service.Name, tag, digest)
if err := recipe.UpdateLabel(service.Name, label); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("added label '%s' to service '%s'", label, service.Name)
}
return nil
},
}
+349 -69
View File
@@ -1,81 +1,170 @@
package recipe
import (
"bufio"
"encoding/json"
"fmt"
"os"
"path"
"sort"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"github.com/distribution/reference"
"github.com/spf13/cobra"
)
var recipeUpgradeCommand = &cli.Command{
Name: "upgrade",
Usage: "Upgrade recipe image tags",
Aliases: []string{"u"},
Description: `
This command reads and attempts to parse all image tags within the given
<recipe> configuration and prompt with more recent tags to upgrade to. It will
update the relevant compose file tags on the local file system.
type imgPin struct {
image string
version tagcmp.Tag
}
// anUpgrade represents a single service upgrade (as within a recipe), and the
// list of tags that it can be upgraded to, for serialization purposes.
type anUpgrade struct {
Service string `json:"service"`
Image string `json:"image"`
Tag string `json:"tag"`
UpgradeTags []string `json:"upgrades"`
}
// translators: `abra recipe upgrade` aliases. use a comma separated list of
// aliases with no spaces in between
var recipeUpgradeAliases = i18n.G("u")
var RecipeUpgradeCommand = &cobra.Command{
// translators: `recipe upgrade` command
Use: i18n.G("upgrade <recipe> [flags]"),
Aliases: strings.Split(recipeUpgradeAliases, ","),
// translators: Short description for `recipe upgrade` command
Short: i18n.G("Upgrade recipe image tags"),
Long: i18n.G(`Upgrade a given <recipe> configuration.
It will update the relevant compose file tags on the local file system.
Some image tags cannot be parsed because they do not follow some sort of
semver-like convention. In this case, all possible tags will be listed and it
is up to the end-user to decide.
This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync
<recipe>".
`,
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
The command is interactive and will show a select input which allows you to
make a seclection. Use the "?" key to see more help on navigating this
interface.`),
Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string,
) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name())
for _, service := range recipe.Config.Services {
catlVersions, err := catalogue.VersionsOfService(recipe.Name, service.Name)
if err != nil {
logrus.Fatal(err)
if err := recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 {
// a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 {
log.Fatal(i18n.G("you can only use one of: --major, --minor, --patch."))
}
logrus.Debugf("read '%s' from the recipe catalogue for '%s'", catlVersions, service.Name)
}
if internal.MachineReadable {
// -m implies -n in this case
internal.NoInput = true
}
upgradeList := make(map[string]anUpgrade)
// check for versions file and load pinned versions
versionsPresent := false
versionsPath := path.Join(recipe.Dir, "versions")
servicePins := make(map[string]imgPin)
if _, err := os.Stat(versionsPath); err == nil {
log.Debug(i18n.G("found versions file for %s", recipe.Name))
file, err := os.Open(versionsPath)
if err != nil {
log.Fatal(err)
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
splitLine := strings.Split(line, " ")
if splitLine[0] != "pin" || len(splitLine) != 3 {
log.Fatal(i18n.G("malformed version pin specification: %s", line))
}
pinSlice := strings.Split(splitLine[2], ":")
pinTag, err := tagcmp.Parse(pinSlice[1])
if err != nil {
log.Fatal(err)
}
pin := imgPin{
image: pinSlice[0],
version: pinTag,
}
servicePins[splitLine[1]] = pin
}
if err := scanner.Err(); err != nil {
log.Error(err)
}
versionsPresent = true
} else {
log.Debug(i18n.G("did not find versions file for %s", recipe.Name))
}
config, err := recipe.GetComposeConfig(nil)
if err != nil {
log.Fatal(err)
}
for _, service := range config.Services {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
regVersions, err := client.GetRegistryTags(img)
if err != nil {
log.Fatal(err)
}
image := reference.Path(img)
regVersions, err := client.GetRegistryTags(image)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("retrieved '%s' from remote registry for '%s'", regVersions, image)
log.Debug(i18n.G("retrieved %s from remote registry for %s", regVersions, image))
image = formatter.StripTagMeta(image)
if strings.Contains(image, "library") {
// ParseNormalizedNamed prepends 'library' to images like nginx:<tag>,
// postgres:<tag>, i.e. images which do not have a username in the
// first position of the string
image = strings.Split(image, "/")[1]
}
semverLikeTag := true
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
logrus.Debugf("'%s' not considered semver-like", img.(reference.NamedTagged).Tag())
semverLikeTag = false
switch img.(type) {
case reference.NamedTagged:
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
log.Debug(i18n.G("%s not considered semver-like", img.(reference.NamedTagged).Tag()))
}
default:
log.Warn(i18n.G("unable to read tag for image %s, is it missing? skipping upgrade for %s", image, service.Name))
continue
}
tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag())
if err != nil && semverLikeTag {
logrus.Fatal(err)
if err != nil {
log.Warn(i18n.G("unable to parse %s, error was: %s, skipping upgrade for %s", image, err.Error(), service.Name))
continue
}
logrus.Debugf("parsed '%s' for '%s'", tag, service.Name)
log.Debug(i18n.G("parsed %s for %s", tag, service.Name))
var compatible []tagcmp.Tag
for _, regVersion := range regVersions {
other, err := tagcmp.Parse(regVersion.Name)
other, err := tagcmp.Parse(regVersion)
if err != nil {
continue // skip tags that cannot be parsed
}
@@ -85,16 +174,21 @@ This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync
}
}
logrus.Debugf("detected potential upgradable tags '%s' for '%s'", compatible, service.Name)
log.Debug(i18n.G("detected potential upgradable tags %s for %s", compatible, service.Name))
sort.Sort(tagcmp.ByTagDesc(compatible))
if len(compatible) == 0 && semverLikeTag {
logrus.Info(fmt.Sprintf("no new versions available for '%s', '%s' is the latest", image, tag))
if len(compatible) == 0 && !allTags {
log.Info(i18n.G("no new versions available for %s, assuming %s is the latest (use -a/--all-tags to see all anyway)", image, tag))
continue // skip on to the next tag and don't update any compose files
}
var compatibleStrings []string
catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name, internal.Offline)
if err != nil {
log.Fatal(err)
}
compatibleStrings := []string{"skip"}
for _, compat := range compatible {
skip := false
for _, catlVersion := range catlVersions {
@@ -107,34 +201,220 @@ This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync
}
}
logrus.Debugf("detected compatible upgradable tags '%s' for '%s'", compatibleStrings, service.Name)
log.Debug(i18n.G("detected compatible upgradable tags %s for %s", compatibleStrings, service.Name))
msg := fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
tag := img.(reference.NamedTagged).Tag()
logrus.Warning(fmt.Sprintf("unable to determine versioning semantics of '%s', listing all tags", tag))
msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
compatibleStrings = []string{}
for _, regVersion := range regVersions {
compatibleStrings = append(compatibleStrings, regVersion.Name)
var upgradeTag string
_, ok := servicePins[service.Name]
if versionsPresent && ok {
pinnedTag := servicePins[service.Name].version
if tag.IsLessThan(pinnedTag) {
pinnedTagString := pinnedTag.String()
contains := false
for _, v := range compatible {
if pinnedTag.IsUpgradeCompatible(v) {
contains = true
upgradeTag = v.String()
break
}
}
if contains {
log.Info(i18n.G("upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString))
} else {
log.Info(i18n.G("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString))
continue
}
} else {
log.Fatal(i18n.G("service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String()))
continue
}
} else {
if bumpType != 0 {
for _, upTag := range compatible {
upElement, err := tag.UpgradeDelta(upTag)
if err != nil {
return
}
delta := upElement.UpgradeType()
if delta <= bumpType {
upgradeTag = upTag.String()
break
}
}
if upgradeTag == "" {
log.Warn(i18n.G("not upgrading from %s to %s for %s, because the upgrade type is more serious than what user wants", tag.String(), compatible[0].String(), image))
continue
}
} else {
msg := i18n.G("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, tag)
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || allTags {
tag := img.(reference.NamedTagged).Tag()
if !allTags {
log.Warn(i18n.G("unable to determine versioning semantics of %s, listing all tags", tag))
}
msg = i18n.G("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
compatibleStrings = []string{"skip"}
for _, regVersion := range regVersions {
compatibleStrings = append(compatibleStrings, regVersion)
}
}
// there is always at least the item "skip" in compatibleStrings (a list of
// possible upgradable tags) and at least one other tag.
upgradableTags := compatibleStrings[1:]
upgrade := anUpgrade{
Service: service.Name,
Image: image,
Tag: tag.String(),
UpgradeTags: make([]string, len(upgradableTags)),
}
for n, s := range upgradableTags {
var sb strings.Builder
if _, err := sb.WriteString(s); err != nil {
}
upgrade.UpgradeTags[n] = sb.String()
}
upgradeList[upgrade.Service] = upgrade
if internal.NoInput {
upgradeTag = "skip"
} else {
prompt := &survey.Select{
Message: msg,
Help: i18n.G("enter / return to confirm, choose 'skip' to not upgrade this tag, vim mode is enabled"),
VimMode: true,
Options: compatibleStrings,
}
if err := survey.AskOne(prompt, &upgradeTag); err != nil {
log.Fatal(err)
}
}
}
}
if upgradeTag != "skip" {
ok, err := recipe.UpdateTag(image, upgradeTag)
if err != nil {
log.Fatal(err)
}
if ok {
log.Info(i18n.G("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image))
}
} else {
if !internal.NoInput {
log.Warn(i18n.G("not upgrading %s, skipping as requested", image))
}
}
}
if internal.NoInput {
if internal.MachineReadable {
jsonstring, err := json.Marshal(upgradeList)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(jsonstring))
return
}
for _, upgrade := range upgradeList {
log.Info(i18n.G("can upgrade service: %s, image: %s, tag: %s ::", upgrade.Service, upgrade.Image, upgrade.Tag))
for _, utag := range upgrade.UpgradeTags {
log.Infof(" %s", utag)
}
}
}
isClean, err := gitPkg.IsClean(recipe.Dir)
if err != nil {
log.Fatal(err)
}
if !isClean {
log.Info(i18n.G("%s currently has these unstaged changes 👇", recipe.Name))
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
log.Fatal(err)
}
if !internal.NoInput && !createCommit {
prompt := &survey.Confirm{
Message: i18n.G("commit changes?"),
Default: true,
}
if err := survey.AskOne(prompt, &createCommit); err != nil {
log.Fatal(err)
}
}
var upgradeTag string
prompt := &survey.Select{
Message: msg,
Options: compatibleStrings,
}
if err := survey.AskOne(prompt, &upgradeTag); err != nil {
logrus.Fatal(err)
if createCommit {
msg := i18n.G("chore: update image tags")
if err := gitPkg.Commit(recipe.Dir, msg, internal.Dry); err != nil {
log.Fatal(err)
}
log.Info(i18n.G("committed changes as '%s'", msg))
}
if err := recipe.UpdateTag(image, upgradeTag); err != nil {
logrus.Fatal(err)
} else {
if createCommit {
log.Warn(i18n.G("no changes, skip creating commit"))
}
logrus.Debugf("tag updated from '%s' to '%s' for '%s'", image, upgradeTag, recipe.Name)
}
return nil
},
}
var (
allTags bool
createCommit bool
)
func init() {
RecipeUpgradeCommand.Flags().BoolVarP(
&internal.Major,
i18n.G("major"),
i18n.G("x"),
false,
i18n.G("increase the major part of the version"),
)
RecipeUpgradeCommand.Flags().BoolVarP(
&internal.Minor,
i18n.G("minor"),
i18n.G("y"),
false,
i18n.G("increase the minor part of the version"),
)
RecipeUpgradeCommand.Flags().BoolVarP(
&internal.Patch,
i18n.G("patch"),
i18n.G("z"),
false,
i18n.G("increase the patch part of the version"),
)
RecipeUpgradeCommand.Flags().BoolVarP(
&internal.MachineReadable,
i18n.G("machine"),
i18n.G("m"),
false,
i18n.G("print machine-readable output"),
)
RecipeUpgradeCommand.Flags().BoolVarP(
&allTags,
i18n.G("all-tags"),
i18n.GC("a", "recipe upgrade"),
false,
i18n.G("list all tags, not just upgrades"),
)
RecipeUpgradeCommand.Flags().BoolVarP(
&createCommit,
i18n.G("commit"),
i18n.GC("c", "recipe upgrade"),
false,
i18n.G("commit changes"),
)
}
+122 -24
View File
@@ -1,45 +1,143 @@
package recipe
import (
"coopcloud.tech/abra/cli/formatter"
"fmt"
"sort"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra"
)
var recipeVersionCommand = &cli.Command{
Name: "versions",
Usage: "List recipe versions",
Aliases: []string{"v"},
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
// translators: `abra recipe versions` aliases. use a comma separated list of aliases
// with no spaces in between
var recipeVersionsAliases = i18n.G("v")
catalogue, err := catalogue.ReadRecipeCatalogue()
var RecipeVersionCommand = &cobra.Command{
// translators: `recipe versions` command
Use: i18n.G("versions <recipe> [flags]"),
Aliases: strings.Split(recipeVersionsAliases, ","),
// translators: Short description for `recipe versions` command
Short: i18n.G("List recipe versions"),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
var warnMessages []string
recipe := internal.ValidateRecipe(args, cmd.Name())
catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
recipeMeta, ok := catalogue[recipe.Name]
recipeMeta, ok := catl[recipe.Name]
if !ok {
logrus.Fatalf("'%s' recipe doesn't exist?", recipe.Name)
warnMessages = append(warnMessages, i18n.G("retrieved versions from local recipe repository"))
recipeVersions, warnMsg, err := recipe.GetRecipeVersions()
if err != nil {
warnMessages = append(warnMessages, err.Error())
}
if len(warnMsg) > 0 {
warnMessages = append(warnMessages, warnMsg...)
}
recipeMeta = recipePkg.RecipeMeta{Versions: recipeVersions}
}
tableCol := []string{"Version", "Service", "Image", "Tag", "Digest"}
table := formatter.CreateTable(tableCol)
if len(recipeMeta.Versions) == 0 {
log.Fatal(i18n.G("%s has no published versions?", recipe.Name))
}
for i := len(recipeMeta.Versions) - 1; i >= 0; i-- {
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table.Headers(i18n.G("SERVICE"), i18n.G("IMAGE"), i18n.G("TAG"), i18n.G("VERSION"))
for version, meta := range recipeMeta.Versions[i] {
var allRows [][]string
var rows [][]string
for _, serviceVersion := range recipeMeta.Versions {
for tag, meta := range serviceVersion {
for service, serviceMeta := range meta {
table.Append([]string{tag, service, serviceMeta.Image, serviceMeta.Tag, serviceMeta.Digest})
recipeVersion := version
if service != "app" {
recipeVersion = ""
}
rows = append(rows, []string{
service,
serviceMeta.Image,
serviceMeta.Tag,
recipeVersion,
})
allRows = append(allRows, []string{
version,
service,
serviceMeta.Image,
serviceMeta.Tag,
recipeVersion,
})
}
sort.Slice(rows, sortServiceByName(rows))
table.Rows(rows...)
if !internal.MachineReadable {
if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
continue
}
if internal.MachineReadable {
sort.Slice(allRows, sortServiceByName(allRows))
headers := []string{i18n.G("VERSION"), i18n.G("SERVICE"), i18n.G("NAME"), i18n.G("TAG")}
out, err := formatter.ToJSON(headers, allRows)
if err != nil {
log.Fatal(i18n.G("unable to render to JSON: %s", err))
}
fmt.Println(out)
continue
}
}
}
table.SetAutoMergeCells(true)
table.Render()
return nil
if !internal.MachineReadable {
for _, warnMsg := range warnMessages {
log.Warn(warnMsg)
}
}
},
}
func sortServiceByName(versions [][]string) func(i, j int) bool {
return func(i, j int) bool {
return versions[i][0] < versions[j][0]
}
}
func init() {
RecipeVersionCommand.Flags().BoolVarP(
&internal.MachineReadable,
i18n.G("machine"),
i18n.G("m"),
false,
i18n.G("print machine-readable output"),
)
}
+319
View File
@@ -0,0 +1,319 @@
package cli
import (
"errors"
"fmt"
"os"
"strings"
"coopcloud.tech/abra/cli/app"
"coopcloud.tech/abra/cli/catalogue"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/cli/recipe"
"coopcloud.tech/abra/cli/server"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
charmLog "github.com/charmbracelet/log"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
)
var (
// translators: `abra` usage template. please translate only words like
// "Aliases" and "Example" and nothing inside the {{ ... }}
usageTemplate = i18n.G(`Usage:{{if .Runnable}}
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
{{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}
Aliases:
{{.NameAndAliases}}{{end}}{{if .HasExample}}
Examples:
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
`)
helpCmd = &cobra.Command{
Use: i18n.G("help [command]"),
// translators: Short description for `help` command
Short: i18n.G("Help about any command"),
Long: i18n.G(`Help provides help for any command in the application.
Simply type abra help [path to command] for full details.`),
Run: func(c *cobra.Command, args []string) {
cmd, _, e := c.Root().Find(args)
if cmd == nil || e != nil {
c.Print(i18n.G("unknown help topic %#q\n", args))
if err := c.Root().Usage(); err != nil {
log.Fatal(err)
}
} else {
cmd.InitDefaultHelpFlag()
cmd.InitDefaultVersionFlag()
if err := cmd.Help(); err != nil {
log.Fatal(err)
}
}
},
}
)
func Run(version, commit string) {
rootCmd := &cobra.Command{
// translators: `abra` binary name
Use: i18n.G("abra [cmd] [args] [flags]"),
// translators: Short description for `abra` binary
Short: i18n.G("The Co-op Cloud command-line utility belt 🎩🐇"),
// translators: Long description for `abra` binary. This needs to be
// translated in the same way as the Short description so that everything
// matches up
Long: i18n.G(`The Co-op Cloud command-line utility belt 🎩🐇
Config:
$ABRA_DIR: %s`, config.ABRA_DIR),
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
ValidArgs: []string{
// translators: `abra app` command for autocompletion
i18n.G("app"),
// translators: `abra autocomplete` command for autocompletion
i18n.G("autocomplete"),
// translators: `abra catalogue` command for autocompletion
i18n.G("catalogue"),
// translators: `abra man` command for autocompletion
i18n.G("man"),
// translators: `abra recipe` command for autocompletion
i18n.G("recipe"),
// translators: `abra server` command for autocompletion
i18n.G("server"),
// translators: `abra upgrade` command for autocompletion
i18n.G("upgrade"),
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
dirs := []map[string]os.FileMode{
{config.ABRA_DIR: 0764},
{config.SERVERS_DIR: 0700},
{config.RECIPES_DIR: 0764},
{config.LOGS_DIR: 0764},
}
for _, dir := range dirs {
for path, perm := range dir {
if err := os.Mkdir(path, perm); err != nil {
if !os.IsExist(err) {
return errors.New(i18n.G("unable to create %s: %s", path, err))
}
continue
}
}
}
log.Logger.SetStyles(charmLog.DefaultStyles())
charmLog.SetDefault(log.Logger)
if internal.MachineReadable {
log.SetOutput(os.Stderr)
}
if internal.Debug {
log.SetLevel(log.DebugLevel)
log.SetOutput(os.Stderr)
log.SetReportCaller(true)
}
log.Debug(i18n.G(
"abra version: %s, commit: %s, lang: %s",
version, formatter.SmallSHA(commit), i18n.Locale,
))
return nil
},
}
rootCmd.CompletionOptions.DisableDefaultCmd = true
rootCmd.SetUsageTemplate(usageTemplate)
rootCmd.SetHelpCommand(helpCmd)
// translators: `abra man` aliases. use a comma separated list of aliases
// with no spaces in between
manAliases := i18n.G("m")
manCommand := &cobra.Command{
// translators: `man` command
Use: i18n.G("man [flags]"),
Aliases: strings.Split(manAliases, ","),
// translators: Short description for `man` command
Short: i18n.G("Generate manpage"),
Example: i18n.G(` # generate the man pages into /usr/local/share/man/man1
abra_path=$(which abra) # pass abra absolute path to sudo below
sudo $abra_path man
sudo mandb
# read the man pages
man abra
man abra-app-deploy`),
Run: func(cmd *cobra.Command, args []string) {
header := &doc.GenManHeader{
Title: "ABRA",
Section: "1",
}
manDir := "/usr/local/share/man/man1"
if _, err := os.Stat(manDir); os.IsNotExist(err) {
log.Fatal(i18n.G("unable to proceed, %s does not exist?", manDir))
}
err := doc.GenManTree(rootCmd, header, manDir)
if err != nil {
log.Fatal(err)
}
log.Info(i18n.G("don't forget to run 'sudo mandb'"))
},
}
rootCmd.PersistentFlags().BoolVarP(
&internal.Debug,
i18n.G("debug"),
i18n.G("d"),
false,
i18n.G("show debug messages"),
)
rootCmd.PersistentFlags().BoolVarP(
&internal.NoInput,
i18n.G("no-input"),
i18n.G("n"),
false,
i18n.G("toggle non-interactive mode"),
)
rootCmd.PersistentFlags().BoolVarP(
&internal.Help,
i18n.G("help"),
i18n.G("h"),
false,
i18n.G("help for abra"),
)
rootCmd.PersistentFlags().BoolVarP(
&internal.Offline,
i18n.G("offline"),
i18n.G("o"),
false,
i18n.G("prefer offline & filesystem access"),
)
rootCmd.Flags().BoolVarP(
&internal.Version,
i18n.G("version"),
i18n.G("v"),
false,
i18n.G("version for abra"),
)
catalogue.CatalogueCommand.AddCommand(
catalogue.CatalogueGenerateCommand,
catalogue.CatalogueSyncCommand,
)
server.ServerCommand.AddCommand(
server.ServerAddCommand,
server.ServerListCommand,
server.ServerPruneCommand,
server.ServerRemoveCommand,
)
recipe.RecipeCommand.AddCommand(
recipe.RecipeDiffCommand,
recipe.RecipeFetchCommand,
recipe.RecipeLintCommand,
recipe.RecipeListCommand,
recipe.RecipeNewCommand,
recipe.RecipeReleaseCommand,
recipe.RecipeResetCommand,
recipe.RecipeUpgradeCommand,
recipe.RecipeVersionCommand,
)
rootCmd.AddCommand(
UpgradeCommand,
AutocompleteCommand,
manCommand,
app.AppCommand,
catalogue.CatalogueCommand,
server.ServerCommand,
recipe.RecipeCommand,
)
app.AppCmdCommand.AddCommand(
app.AppCmdListCommand,
)
app.AppSecretCommand.AddCommand(
app.AppSecretGenerateCommand,
app.AppSecretInsertCommand,
app.AppSecretRmCommand,
app.AppSecretLsCommand,
)
app.AppVolumeCommand.AddCommand(
app.AppVolumeListCommand,
app.AppVolumeRemoveCommand,
)
app.AppBackupCommand.AddCommand(
app.AppBackupListCommand,
app.AppBackupDownloadCommand,
app.AppBackupCreateCommand,
app.AppBackupSnapshotsCommand,
)
app.AppEnvCommand.AddCommand(
app.AppEnvListCommand,
app.AppEnvPullCommand,
)
app.AppCommand.AddCommand(
app.AppBackupCommand,
app.AppCheckCommand,
app.AppCmdCommand,
app.AppConfigCommand,
app.AppCpCommand,
app.AppDeployCommand,
app.AppListCommand,
app.AppLogsCommand,
app.AppNewCommand,
app.AppPsCommand,
app.AppRemoveCommand,
app.AppRestartCommand,
app.AppRestoreCommand,
app.AppRollbackCommand,
app.AppMoveCommand,
app.AppRunCommand,
app.AppSecretCommand,
app.AppServicesCommand,
app.AppUndeployCommand,
app.AppUpgradeCommand,
app.AppVolumeCommand,
app.AppLabelsCommand,
app.AppEnvCommand,
)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
+180 -60
View File
@@ -1,91 +1,211 @@
package server
import (
"context"
"os/user"
"os"
"path/filepath"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"coopcloud.tech/abra/pkg/config"
contextPkg "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/dns"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/server"
sshPkg "coopcloud.tech/abra/pkg/ssh"
"github.com/spf13/cobra"
)
var serverAddCommand = &cli.Command{
Name: "add",
Usage: "Add a new server",
Description: `
This command adds a new server that abra will communicate with, to deploy apps.
// translators: `abra server add` aliases. use a comma separated list of
// aliases with no spaces in between
var serverAddAliases = i18n.GC("a", "server add")
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.
var ServerAddCommand = &cobra.Command{
// translators: `server add` command
Use: i18n.G("add [[server] | --local] [flags]"),
Aliases: strings.Split(serverAddAliases, ","),
// translators: Short description for `server add` command
Short: i18n.G("Add a new server"),
Long: i18n.G(`Add a new server to your configuration so that it can be managed by Abra.
For example:
Abra relies on the standard SSH command-line and ~/.ssh/config for client
connection details. You must configure an entry per-host in your ~/.ssh/config
for each server:
abra server add varia.zone 12345 glodemodem
Host 1312.net 1312
Hostname 1312.net
User antifa
Port 12345
IdentityFile ~/.ssh/antifa@somewhere
Abra will construct the following SSH connection string then:
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
Co-op Cloud config located on the server itself, and not on your local
developer machine. The domain is then set to "default".`),
Example: i18n.G(" abra server add 1312.net"),
Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
if !local {
return autocomplete.ServerNameComplete()
}
return nil, cobra.ShellCompDirectiveDefault
},
Run: func(cmd *cobra.Command, args []string) {
if len(args) > 0 && local {
log.Fatal(i18n.G("cannot use [server] and --local together"))
}
ssh://globemodem@varia.zone:12345
if len(args) == 0 && !local {
log.Fatal(i18n.G("missing argument or --local/-l flag"))
}
All communication between Abra and the server will use this SSH connection.
name := "default"
if !local {
name = internal.ValidateDomain(args)
}
`,
Aliases: []string{"a"},
ArgsUsage: "<domain> [<user>] [<port>]",
Action: func(c *cli.Context) error {
domainName := internal.ValidateDomain(c)
// NOTE(d1): reasonable 5 second timeout for connections which can't
// succeed. The connection is attempted twice, so this results in 10
// seconds.
timeout := client.WithTimeout(5)
var username string
var port string
username = c.Args().Get(1)
if username == "" {
systemUser, err := user.Current()
if local {
created, err := createServerDir(name)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
username = systemUser.Username
}
port = c.Args().Get(2)
if port == "" {
port = "22"
}
log.Debug(i18n.G("attempting to create client for %s", name))
store := client.NewDefaultDockerContextStore()
contexts, err := store.Store.List()
if err != nil {
logrus.Fatal(err)
}
for _, context := range contexts {
if context.Name == domainName {
logrus.Fatalf("server at '%s' already exists?", domainName)
if _, err := client.New(name, timeout); err != nil {
cleanUp(name)
log.Fatal(err)
}
if created {
log.Info(i18n.G("local server successfully added"))
} else {
log.Warn(i18n.G("local server already exists"))
}
return
}
logrus.Debugf("creating context with domain '%s', username '%s' and port '%s'", domainName, username, port)
if err := client.CreateContext(domainName, username, port); err != nil {
logrus.Fatal(err)
}
ctx := context.Background()
cl, err := client.New(domainName)
_, err := createServerDir(name)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
if _, err := cl.Info(ctx); err != nil {
logrus.Fatalf("unable to make a connection to '%s'?", domainName)
logrus.Debug(err)
created, err := newContext(name)
if err != nil {
cleanUp(name)
log.Fatal(i18n.G("unable to create local context: %s", err))
}
logrus.Debugf("remote connection to '%s' is definitely up", domainName)
logrus.Infof("server at '%s' has been added", domainName)
log.Debug(i18n.G("attempting to create client for %s", name))
return nil
if _, err := client.New(name, timeout); err != nil {
cleanUp(name)
log.Fatal(i18n.G("ssh %s error: %s", name, sshPkg.Fatal(name, err)))
}
if created {
log.Info(i18n.G("%s successfully added", name))
if _, err := dns.EnsureIPv4(name); err != nil {
log.Warn(i18n.G("unable to resolve IPv4 for %s", name))
}
return
}
log.Warn(i18n.G("%s already exists", name))
},
}
// cleanUp cleans up the partially created context/client details for a failed
// "server add" attempt.
func cleanUp(name string) {
if name != "default" {
log.Debug(i18n.G("serverAdd: cleanUp: cleaning up context for %s", name))
if err := client.DeleteContext(name); err != nil {
log.Fatal(err)
}
}
serverDir := filepath.Join(config.SERVERS_DIR, name)
files, err := config.GetAllFilesInDirectory(serverDir)
if err != nil {
log.Fatal(i18n.G("serverAdd: cleanUp: unable to list files in %s: %s", serverDir, err))
}
if len(files) > 0 {
log.Debug(i18n.G("serverAdd: cleanUp: %s is not empty, aborting cleanup", serverDir))
return
}
if err := os.RemoveAll(serverDir); err != nil {
log.Fatal(i18n.G("serverAdd: cleanUp: failed to remove %s: %s", serverDir, err))
}
}
// newContext creates a new internal Docker context for a server. This is how
// Docker manages SSH connection details. These are stored to disk in
// ~/.docker. Abra can manage this completely for the user, so it's an
// implementation detail.
func newContext(name string) (bool, error) {
store := contextPkg.NewDefaultDockerContextStore()
contexts, err := store.Store.List()
if err != nil {
return false, err
}
for _, context := range contexts {
if context.Name == name {
log.Debug(i18n.G("context for %s already exists", name))
return false, nil
}
}
log.Debugf(i18n.G("creating context with domain %s", name))
if err := client.CreateContext(name); err != nil {
return false, nil
}
return true, nil
}
// createServerDir creates the ~/.abra/servers/... directory for a new server.
func createServerDir(name string) (bool, error) {
if err := server.CreateServerDir(name); err != nil {
if !os.IsExist(err) {
return false, err
}
log.Debug(i18n.G("server dir for %s already created", name))
return false, nil
}
return true, nil
}
var (
local bool
)
func init() {
ServerAddCommand.Flags().BoolVarP(
&local,
i18n.G("local"),
i18n.G("l"),
false,
i18n.G("use local server"),
)
}
-78
View File
@@ -1,78 +0,0 @@
package server
import (
"context"
"fmt"
"net"
"time"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var serverInitCommand = &cli.Command{
Name: "init",
Usage: "Initialise server for deploying apps",
Aliases: []string{"i"},
HideHelp: true,
ArgsUsage: "<domain>",
Description: `
Initialise swarm mode on the target <domain>.
This initialisation explicitly chooses the "single host swarm" mode which uses
the default IPv4 address as the advertising address. This can be re-configured
later for more advanced use cases.
`,
Action: func(c *cli.Context) error {
domainName := internal.ValidateDomain(c)
cl, err := client.New(domainName)
if err != nil {
return err
}
resolver := &net.Resolver{
PreferGo: false,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{
Timeout: time.Millisecond * time.Duration(10000),
}
// comrade librehosters DNS resolver https://snopyta.org/service/dns/
return d.DialContext(ctx, "udp", "95.216.24.230:53")
},
}
logrus.Debugf("created DNS resolver via 95.216.24.230")
ctx := context.Background()
ips, err := resolver.LookupIPAddr(ctx, domainName)
if err != nil {
logrus.Fatal(err)
}
if len(ips) == 0 {
return fmt.Errorf("unable to retrieve ipv4 address for %s", domainName)
}
ipv4 := ips[0].IP.To4().String()
initReq := swarm.InitRequest{
ListenAddr: "0.0.0.0:2377",
AdvertiseAddr: ipv4,
}
if _, err := cl.SwarmInit(ctx, initReq); err != nil {
return err
}
logrus.Debugf("initialised swarm on '%s'", domainName)
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
if _, err := cl.NetworkCreate(ctx, "proxy", netOpts); err != nil {
return err
}
logrus.Debug("swarm overlay network 'proxy' created")
return nil
},
}
+79 -24
View File
@@ -1,55 +1,110 @@
package server
import (
"fmt"
"strings"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
contextPkg "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"github.com/docker/cli/cli/connhelper/ssh"
"github.com/spf13/cobra"
)
var serverListCommand = &cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List managed servers",
ArgsUsage: " ",
HideHelp: true,
Action: func(c *cli.Context) error {
dockerContextStore := client.NewDefaultDockerContextStore()
// translators: `abra server list` aliases. use a comma separated list of
// aliases with no spaces in between
var serverListAliases = i18n.G("ls")
var ServerListCommand = &cobra.Command{
// translators: `server list` command
Use: i18n.G("list [flags]"),
Aliases: strings.Split(serverListAliases, ","),
// translators: Short description for `server list` command
Short: i18n.G("List managed servers"),
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
dockerContextStore := contextPkg.NewDefaultDockerContextStore()
contexts, err := dockerContextStore.Store.List()
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
tableColumns := []string{"Name", "Connection"}
table := formatter.CreateTable(tableColumns)
defer table.Render()
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
headers := []string{i18n.G("NAME"), i18n.G("HOST")}
table.Headers(headers...)
serverNames, err := config.ReadServerNames()
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
var rows [][]string
for _, serverName := range serverNames {
var row []string
for _, ctx := range contexts {
endpoint, err := client.GetContextEndpoint(ctx)
for _, dockerCtx := range contexts {
endpoint, err := contextPkg.GetContextEndpoint(dockerCtx)
if err != nil && strings.Contains(err.Error(), "does not exist") {
// No local context found, we can continue safely
continue
}
if ctx.Name == serverName {
row = []string{serverName, endpoint}
if dockerCtx.Name == serverName {
sp, err := ssh.ParseURL(endpoint)
if err != nil {
log.Fatal(err)
}
if sp.Host == "" {
sp.Host = i18n.G("unknown")
}
row = []string{serverName, sp.Host}
rows = append(rows, row)
}
}
if len(row) == 0 {
row = []string{serverName, "UNKNOWN"}
if serverName == "default" {
row = []string{serverName, i18n.G("local")}
} else {
row = []string{serverName, i18n.G("unknown")}
}
rows = append(rows, row)
}
table.Append(row)
table.Row(row...)
}
return nil
if internal.MachineReadable {
out, err := formatter.ToJSON(headers, rows)
if err != nil {
log.Fatal(i18n.G("unable to render to JSON: %s", err))
}
fmt.Println(out)
return
}
if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
},
}
func init() {
ServerListCommand.Flags().BoolVarP(
&internal.MachineReadable,
i18n.G("machine"),
i18n.G("m"),
false,
i18n.G("print machine-readable output"),
)
}
-264
View File
@@ -1,264 +0,0 @@
package server
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"time"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"github.com/hetznercloud/hcloud-go/hcloud"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var hetznerCloudType string
var hetznerCloudImage string
var hetznerCloudSSHKeys cli.StringSlice
var hetznerCloudLocation string
var hetznerCloudAPIToken string
var serverNewHetznerCloudCommand = &cli.Command{
Name: "hetzner",
Usage: "Create a new Hetzner virtual server",
ArgsUsage: "<name>",
Description: `
Create a new Hetzner virtual server.
This command uses the uses the Hetzner Cloud API bindings to send a server
creation request. You must already have a Hetzner Cloud account and an account
API token before using this command.
Your token can be loaded from the environment using the HCLOUD_TOKEN
environment variable or otherwise passing the "--env/-e" flag.
`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
Usage: "Server type",
Destination: &hetznerCloudType,
Value: "cx11",
},
&cli.StringFlag{
Name: "image",
Aliases: []string{"i"},
Usage: "Image type",
Value: "debian-10",
Destination: &hetznerCloudImage,
},
&cli.StringSliceFlag{
Name: "ssh-keys",
Aliases: []string{"s"},
Usage: "SSH keys",
Destination: &hetznerCloudSSHKeys,
},
&cli.StringFlag{
Name: "location",
Aliases: []string{"l"},
Usage: "Server location",
Value: "hel1",
Destination: &hetznerCloudLocation,
},
&cli.StringFlag{
Name: "token",
Aliases: []string{"T"},
Usage: "Hetzner Cloud API token",
EnvVars: []string{"HCLOUD_TOKEN"},
Destination: &hetznerCloudAPIToken,
},
},
Action: func(c *cli.Context) error {
name := c.Args().First()
if name == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no name provided"))
}
if hetznerCloudAPIToken == "" {
logrus.Fatal("Hetzner Cloud API token is missing")
}
ctx := context.Background()
client := hcloud.NewClient(hcloud.WithToken(hetznerCloudAPIToken))
logrus.Debugf("successfully created hetzner cloud API client")
var sshKeys []*hcloud.SSHKey
for _, sshKey := range c.StringSlice("ssh-keys") {
sshKey, _, err := client.SSHKey.GetByName(ctx, sshKey)
if err != nil {
logrus.Fatal(err)
}
sshKeys = append(sshKeys, sshKey)
}
serverOpts := hcloud.ServerCreateOpts{
Name: name,
ServerType: &hcloud.ServerType{Name: hetznerCloudType},
Image: &hcloud.Image{Name: hetznerCloudImage},
SSHKeys: sshKeys,
Location: &hcloud.Location{Name: hetznerCloudLocation},
}
res, _, err := client.Server.Create(ctx, serverOpts)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("new server '%s' created", name)
tableColumns := []string{"Name", "IPv4", "Root Password"}
table := formatter.CreateTable(tableColumns)
if len(sshKeys) > 0 {
table.Append([]string{name, res.Server.PublicNet.IPv4.IP.String(), "N/A (using SSH keys)"})
} else {
table.Append([]string{name, res.Server.PublicNet.IPv4.IP.String(), res.RootPassword})
}
table.Render()
return nil
},
}
var capsulInstance string
var capsulType string
var capsulImage string
var capsulSSHKey string
var capsulAPIToken string
var serverNewCapsulCommand = &cli.Command{
Name: "capsul",
Usage: "Create a new Capsul virtual server",
ArgsUsage: "<name>",
Description: `
Create a new Capsul virtual server.
This command uses the uses the Capsul API bindings of your chosen instance to
send a server creation request. You must already have an account on your chosen
Capsul instance before using this command.
Your token can be loaded from the environment using the CAPSUL_TOKEN
environment variable or otherwise passing the "--env/-e" flag.
`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "instance",
Aliases: []string{"I"},
Usage: "Capsul instance",
Destination: &capsulInstance,
Value: "yolo.servers.coop",
},
&cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
Usage: "Server type",
Value: "f1-xs",
Destination: &capsulType,
},
&cli.StringFlag{
Name: "image",
Aliases: []string{"i"},
Usage: "Image type",
Value: "debian10",
Destination: &capsulImage,
},
&cli.StringFlag{
Name: "ssh-key",
Aliases: []string{"s"},
Usage: "SSH key",
Value: "",
Destination: &capsulSSHKey,
},
&cli.StringFlag{
Name: "token",
Aliases: []string{"T"},
Usage: "Capsul instance API token",
EnvVars: []string{"CAPSUL_TOKEN"},
Destination: &capsulAPIToken,
},
},
Action: func(c *cli.Context) error {
name := c.Args().First()
if name == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no name provided"))
}
if capsulAPIToken == "" {
logrus.Fatal("Capsul API token is missing")
}
// yep, the response time is quite slow, something to fix on the Capsul side
client := &http.Client{Timeout: 20 * time.Second}
capsulCreateURL := fmt.Sprintf("https://%s/api/capsul/create", capsulInstance)
logrus.Debugf("using '%s' as capsul create url", capsulCreateURL)
values := map[string]string{
"name": name,
"size": capsulType,
"os": capsulImage,
"ssh_key_0": capsulSSHKey,
}
payload, err := json.Marshal(values)
if err != nil {
logrus.Fatal(err)
}
req, err := http.NewRequest("POST", capsulCreateURL, bytes.NewBuffer(payload))
if err != nil {
logrus.Fatal(err)
}
req.Header = http.Header{
"Content-Type": []string{"application/json"},
"Authorization": []string{capsulAPIToken},
}
res, err := client.Do(req)
if err != nil {
logrus.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
body, err := ioutil.ReadAll(res.Body)
if err != nil {
panic(err)
}
logrus.Fatal(string(body))
}
type capsulCreateResponse struct{ ID string }
var resp capsulCreateResponse
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("capsul created with ID: '%s'", resp.ID)
tableColumns := []string{"Name", "ID"}
table := formatter.CreateTable(tableColumns)
table.Append([]string{name, resp.ID})
table.Render()
return nil
},
}
var serverNewCommand = &cli.Command{
Name: "new",
Aliases: []string{"n"},
Usage: "Create a new server using a 3rd party provider",
Description: `
Use a provider plugin to create a new server which can then be used to house a
new Co-op Cloud installation.
`,
ArgsUsage: "<provider>",
Subcommands: []*cli.Command{
serverNewHetznerCloudCommand,
serverNewCapsulCommand,
},
}
+111
View File
@@ -0,0 +1,111 @@
package server
import (
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"github.com/docker/docker/api/types/filters"
"github.com/spf13/cobra"
)
// translators: `abra server prune` aliases. use a comma separated list of
// aliases with no spaces in between
var serverPruneliases = i18n.G("p")
var ServerPruneCommand = &cobra.Command{
// translators: `server prune` command
Use: i18n.G("prune <server> [flags]"),
Aliases: strings.Split(serverPruneliases, ","),
// translators: Short description for `server prune` command
Short: i18n.G("Prune resources on a server"),
Long: i18n.G(`Prunes unused containers, networks, and dangling images.
Use "--volumes/-v" to remove volumes that are not associated with a deployed
app. This can result in unwanted data loss if not used carefully.`),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.ServerNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
serverName := internal.ValidateServer(args)
cl, err := client.New(serverName)
if err != nil {
log.Fatal(err)
}
var filterArgs filters.Args
cr, err := cl.ContainersPrune(cmd.Context(), filterArgs)
if err != nil {
log.Fatal(err)
}
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
log.Info(i18n.G("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed))
nr, err := cl.NetworksPrune(cmd.Context(), filterArgs)
if err != nil {
log.Fatal(err)
}
log.Info(i18n.G("networks pruned: %d", len(nr.NetworksDeleted)))
pruneFilters := filters.NewArgs()
if allFilter {
log.Debug(i18n.G("removing all images, not only dangling ones"))
pruneFilters.Add("dangling", "false")
}
ir, err := cl.ImagesPrune(cmd.Context(), pruneFilters)
if err != nil {
log.Fatal(err)
}
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
log.Info(i18n.G("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed))
if volumesFilter {
vr, err := cl.VolumesPrune(cmd.Context(), filterArgs)
if err != nil {
log.Fatal(err)
}
volSpaceReclaimed := formatter.ByteCountSI(vr.SpaceReclaimed)
log.Info(i18n.G("volumes pruned: %d; space reclaimed: %s", len(vr.VolumesDeleted), volSpaceReclaimed))
}
return
},
}
var (
allFilter bool
volumesFilter bool
)
func init() {
ServerPruneCommand.Flags().BoolVarP(
&allFilter,
i18n.G("all"),
i18n.GC("a", "server prune"),
false,
i18n.G("remove all unused images"),
)
ServerPruneCommand.Flags().BoolVarP(
&volumesFilter,
i18n.G("volumes"),
i18n.G("v"),
false,
i18n.G("remove volumes"),
)
}
+41 -17
View File
@@ -1,30 +1,54 @@
package server
import (
"os"
"path/filepath"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra"
)
var serverRemoveCommand = &cli.Command{
Name: "remove",
Aliases: []string{"rm"},
Usage: "Remove a server",
Description: `
This does not destroy the actual server. It simply removes it from Abra
internal bookkeeping so that it is not managed any more.
`,
HideHelp: true,
Action: func(c *cli.Context) error {
domainName := internal.ValidateDomain(c)
// translators: `abra server remove` aliases. use a comma separated list of
// aliases with no spaces in between
var serverRemoveAliases = i18n.G("rm")
if err := client.DeleteContext(domainName); err != nil {
logrus.Fatal(err)
var ServerRemoveCommand = &cobra.Command{
// translators: `server remove` command
Use: i18n.G("remove <server> [flags]"),
Aliases: strings.Split(serverRemoveAliases, ","),
// translators: Short description for `server remove` command
Short: i18n.G("Remove a managed server"),
Long: i18n.G(`Remove a managed server.
Abra will remove the internal bookkeeping ($ABRA_DIR/servers/...) and
underlying client connection context. This server will then be lost in time,
like tears in rain.`),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.ServerNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
serverName := internal.ValidateServer(args)
if err := client.DeleteContext(serverName); err != nil {
log.Fatal(err)
}
logrus.Infof("server at '%s' has been forgotten", domainName)
if err := os.RemoveAll(filepath.Join(config.SERVERS_DIR, serverName)); err != nil {
log.Fatal(err)
}
return nil
log.Info(i18n.G("%s is now lost in time, like tears in rain", serverName))
return
},
}
+14 -19
View File
@@ -1,26 +1,21 @@
package server
import (
"github.com/urfave/cli/v2"
"strings"
"coopcloud.tech/abra/pkg/i18n"
"github.com/spf13/cobra"
)
// ServerCommand defines the `abra server` command and its subcommands
var ServerCommand = &cli.Command{
Name: "server",
Aliases: []string{"s"},
Usage: "Manage servers",
Description: `
Manage the lifecycle of a server.
// translators: `abra server` aliases. use a comma separated list of aliases
// with no spaces in between
var serverAliases = i18n.G("s")
These commands support creating new servers using 3rd party integrations,
initialising existing servers to support Co-op Cloud deployments and managing
the connections to those servers.
`,
Subcommands: []*cli.Command{
serverNewCommand,
serverInitCommand,
serverAddCommand,
serverListCommand,
serverRemoveCommand,
},
// ServerCommand defines the `abra server` command and its subcommands
var ServerCommand = &cobra.Command{
// translators: `server` command group
Use: i18n.G("server [cmd] [args] [flags]"),
Aliases: strings.Split(serverAliases, ","),
// translators: Short description for `server` command group
Short: i18n.G("Manage servers"),
}
+52 -11
View File
@@ -1,23 +1,64 @@
// Package cli provides the interface for the command-line.
package cli
import (
"fmt"
"os/exec"
"strings"
"coopcloud.tech/abra/cli/internal"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra"
)
// translators: `abra upgrade` aliases. use a comma separated list of aliases with
// no spaces in between
var upgradeAliases = i18n.G("u")
// UpgradeCommand upgrades abra in-place.
var UpgradeCommand = &cli.Command{
Name: "upgrade",
Usage: "Upgrade abra",
Action: func(c *cli.Context) error {
cmd := exec.Command("bash", "-c", "curl -s https://install.abra.coopcloud.tech | bash")
logrus.Debugf("attempting to run '%s'", cmd)
if err := internal.RunCmd(cmd); err != nil {
logrus.Fatal(err)
var UpgradeCommand = &cobra.Command{
// translators: `upgrade` command
Use: i18n.G("upgrade [flags]"),
Aliases: strings.Split(upgradeAliases, ","),
// translators: Short description for `upgrade` command
Short: i18n.G("Upgrade abra"),
Long: i18n.G(`Upgrade abra in-place with the latest stable or release candidate.
By default, the latest stable release is downloaded.
Use "--rc/-r" to install the latest release candidate. Please bear in mind that
it may contain absolutely catastrophic deal-breaker bugs. Thank you very much
for the testing efforts 💗`),
Example: i18n.G(" abra upgrade --rc"),
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
mainURL := "https://install.abra.coopcloud.tech"
c := exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash", mainURL))
if releaseCandidate {
releaseCandidateURL := "https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer"
c = exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash -s -- --rc", releaseCandidateURL))
}
log.Debugf(i18n.G("attempting to run %s", c))
if err := internal.RunCmd(c); err != nil {
log.Fatal(err)
}
return nil
},
}
var (
releaseCandidate bool
)
func init() {
UpgradeCommand.Flags().BoolVarP(
&releaseCandidate,
"rc",
"r",
false,
i18n.G("install release candidate (may contain bugs)"),
)
}
-15
View File
@@ -1,15 +0,0 @@
package cli
import (
"github.com/urfave/cli/v2"
)
// VersionCommand prints the version of abra.
var VersionCommand = &cli.Command{
Name: "version",
Usage: "Print version",
Action: func(c *cli.Context) error {
cli.VersionPrinter(c)
return nil
},
}
+3 -4
View File
@@ -5,14 +5,13 @@ import (
"coopcloud.tech/abra/cli"
)
// Version is the current version of abra.
// Version is the current version of Abra.
var Version string
// Commit is the current commit of abra.
// Commit is the current git commit of Abra.
var Commit string
func main() {
// If not set in the ld-flags
if Version == "" {
Version = "dev"
}
@@ -20,5 +19,5 @@ func main() {
Commit = " "
}
cli.RunApp(Version, Commit)
cli.Run(Version, Commit)
}
Generated
+61
View File
@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1778443072,
"narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+37
View File
@@ -0,0 +1,37 @@
{
description = "The Co-op Cloud utility belt";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
self,
nixpkgs,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
packages = rec {
abra = pkgs.callPackage ./package.nix { };
default = abra;
};
apps = rec {
abra = flake-utils.lib.mkApp { drv = self.packages.${system}.abra; };
default = abra;
};
devShells.default = pkgs.mkShell {
packages = with pkgs; [
go_1_26
gnumake
];
};
}
);
}
+145 -76
View File
@@ -1,94 +1,163 @@
module coopcloud.tech/abra
go 1.17
go 1.26.0
require (
coopcloud.tech/tagcmp v0.0.0-20210906102006-2a8edd82d75d
github.com/AlecAivazis/survey/v2 v2.3.1
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4
github.com/docker/cli v20.10.8+incompatible
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v20.10.8+incompatible
github.com/docker/go-units v0.4.0
github.com/go-git/go-git/v5 v5.4.2
github.com/hetznercloud/hcloud-go v1.32.0
github.com/moby/sys/signal v0.5.0
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
github.com/olekukonko/tablewriter v0.0.5
coopcloud.tech/tagcmp v0.0.0-20260515102403-c26951b55977
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v1.0.0
github.com/distribution/reference v0.6.0
github.com/docker/cli v28.4.0+incompatible
github.com/docker/docker v28.5.2+incompatible
github.com/docker/go-units v0.5.0
github.com/go-git/go-git/v5 v5.19.1
github.com/google/go-cmp v0.7.0
github.com/leonelquinteros/gotext v1.7.2
github.com/moby/sys/signal v0.7.1
github.com/moby/term v0.5.2
github.com/pkg/errors v0.9.1
github.com/schultz-is/passgen v1.0.1
github.com/sirupsen/logrus v1.8.1
github.com/urfave/cli/v2 v2.3.0
gotest.tools/v3 v3.0.3
github.com/schollz/progressbar/v3 v3.19.0
golang.org/x/term v0.44.0
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.2
)
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.4.17 // indirect
github.com/Microsoft/hcsshim v0.8.21 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.4.1 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/containerd/cgroups v1.0.1 // indirect
github.com/containerd/containerd v1.5.5 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/fvbommel/sortorder v1.0.2 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.5.0 // indirect
github.com/google/go-cmp v0.5.5 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.9.0 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-isatty v0.0.8 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/miekg/pkcs11 v1.0.3 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/moby/sys/mount v0.2.0 // indirect
github.com/moby/sys/mountinfo v0.4.1 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.21 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.1.0 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/morikuni/aec v1.1.0 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/opencontainers/runc v1.0.2 // indirect
github.com/prometheus/client_golang v1.11.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.26.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/spf13/cobra v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/xanzy/ssh-agent v0.3.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/opencontainers/runc v1.1.13 // indirect
github.com/opencontainers/runtime-spec v1.1.0 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.opencensus.io v0.22.3 // indirect
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
golang.org/x/net v0.0.0-20210326060303-6b1517762897 // indirect
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect
golang.org/x/text v0.3.4 // indirect
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect
google.golang.org/grpc v1.33.2 // indirect
google.golang.org/protobuf v1.26.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/otel v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/otel/sdk v1.42.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect
go.opentelemetry.io/otel/trace v1.42.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
require (
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.9.5 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/spf13/cobra v1.10.1
github.com/stretchr/testify v1.11.1
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
golang.org/x/sys v0.46.0
)
replace github.com/docker/cli v28.4.0+incompatible => git.coopcloud.tech/toolshed/docker-cli v28.5.3-0.20260202112816-30df2d0b3a00+incompatible
replace github.com/spf13/cobra => github.com/decentral1se/cobra v1.10.2
+430 -202
View File
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
{
buildGo126Module,
fetchgit,
lib,
installShellFiles,
}:
buildGo126Module rec {
pname = "abra";
version = "0.13.0-beta";
rev = "06a57ded025a43c80f94d4e65299add8a31830dc";
src = fetchgit {
url = "https://git.coopcloud.tech/toolshed/abra.git";
tag = version;
hash = "sha256-rgoK0TY0WLSQ39lPvVM80zW/qJF40VFBSxYDOaKXZQo=";
};
vendorHash = null;
nativeBuildInputs = [
installShellFiles
];
env.CGO_ENABLED = 0;
buildPhase = ''
runHook preBuild
go build -ldflags="-s -w -X 'main.Commit=${rev}' -X 'main.Version=${version}'" ./cmd/abra
runHook postBuild
'';
installPhase = ''
runHook preInstall
install -D abra $out/bin/abra
runHook postInstall
'';
postInstall = ''
export ABRA_DIR="$out"
$out/bin/abra autocomplete bash >abra.bash
$out/bin/abra autocomplete fish >abra.fish
$out/bin/abra autocomplete zsh >abra.zsh
installShellCompletion abra.{bash,fish,zsh}
'';
meta = with lib; {
description = "The Co-op Cloud utility belt";
homepage = "https://docs.coopcloud.tech/abra";
changelog = "https://git.coopcloud.tech/toolshed/abra/releases/tag/${version}";
mainProgram = "abra";
license = licenses.gpl3Plus;
maintainers = "devydave";
};
}
+668 -52
View File
@@ -1,85 +1,701 @@
package app
import (
"context"
"bufio"
"errors"
"fmt"
"os"
"path"
"regexp"
"sort"
"strings"
"coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
apiclient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/convert"
"coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/abra/pkg/log"
loader "coopcloud.tech/abra/pkg/upstream/stack"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types/filters"
"github.com/schollz/progressbar/v3"
)
// Get retrieves an app
func Get(appName string) (config.App, error) {
files, err := config.LoadAppFiles("")
func Get(appName string) (App, error) {
files, err := LoadAppFiles("")
if err != nil {
return config.App{}, err
return App{}, err
}
app, err := config.GetApp(files, appName)
app, err := GetApp(files, appName)
if err != nil {
return config.App{}, err
return App{}, err
}
logrus.Debugf("retrieved '%s' for '%s'", app, appName)
log.Debug(i18n.G("loaded app %s: %s", appName, app))
return app, nil
}
// deployedServiceSpec represents a deployed service of an app.
type deployedServiceSpec struct {
Name string
Version string
}
// VersionSpec represents a deployed app and associated metadata.
type VersionSpec map[string]deployedServiceSpec
// DeployedVersions lists metadata (e.g. versions) for deployed
func DeployedVersions(ctx context.Context, cl *apiclient.Client, app config.App) (VersionSpec, bool, error) {
services, err := stack.GetStackServices(ctx, cl, app.StackName())
if err != nil {
return VersionSpec{}, false, err
// GetApp loads an apps settings, reading it from file, in preparation to use
// it. It should only be used when ready to use the env file to keep IO
// operations down.
func GetApp(apps AppFiles, name AppName) (App, error) {
appFile, exists := apps[name]
if !exists {
return App{}, errors.New(i18n.G("cannot find app with name %s", name))
}
appSpec := make(VersionSpec)
for _, service := range services {
serviceName := ParseServiceName(service.Spec.Name)
label := fmt.Sprintf("coop-cloud.%s.%s.version", app.StackName(), serviceName)
if deployLabel, ok := service.Spec.Labels[label]; ok {
version, _ := ParseVersionLabel(deployLabel)
appSpec[serviceName] = deployedServiceSpec{Name: serviceName, Version: version}
app, err := ReadAppEnvFile(appFile, name)
if err != nil {
return App{}, err
}
return app, nil
}
// GetApps returns a slice of Apps with their env files read from a given
// slice of AppFiles.
func GetApps(appFiles AppFiles, recipeFilter string) ([]App, error) {
var apps []App
for name := range appFiles {
app, err := GetApp(appFiles, name)
if err != nil {
return nil, err
}
if recipeFilter != "" {
if app.Recipe.Name == recipeFilter {
apps = append(apps, app)
}
} else {
apps = append(apps, app)
}
}
deployed := len(services) > 0
return apps, nil
}
if deployed {
logrus.Debugf("detected '%s' as deployed versions of '%s'", appSpec, app.Name)
} else {
logrus.Debugf("detected '%s' as not deployed", app.Name)
// App reprents an app with its env file read into memory
type App struct {
Name AppName
Recipe recipe.Recipe
Domain string
Env envfile.AppEnv
Server string
Path string
}
// String outputs a human-friendly string representation.
func (a App) String() string {
out := fmt.Sprintf("{name: %s, ", a.Name)
out += fmt.Sprintf("recipe: %s, ", a.Recipe)
out += fmt.Sprintf("domain: %s, ", a.Domain)
out += fmt.Sprintf("env %s, ", a.Env)
out += fmt.Sprintf("server %s, ", a.Server)
out += fmt.Sprintf("path %s}", a.Path)
return out
}
// Type aliases to make code hints easier to understand
// AppName is AppName
type AppName = string
// AppFile represents app env files on disk without reading the contents
type AppFile struct {
Path string
Server string
}
// AppFiles is a slice of appfiles
type AppFiles map[AppName]AppFile
// See documentation of config.StackName
func (a App) StackName() string {
if _, exists := a.Env["STACK_NAME"]; exists {
return a.Env["STACK_NAME"]
}
return appSpec, len(services) > 0, nil
stackName := StackName(a.Name)
a.Env["STACK_NAME"] = stackName
return stackName
}
// ParseVersionLabel parses a $VERSION-$DIGEST app service label.
func ParseVersionLabel(label string) (string, string) {
// versions may look like v4.2-abcd or v4.2-alpine-abcd
idx := strings.LastIndex(label, "-")
version := label[:idx]
digest := label[idx+1:]
logrus.Debugf("parsed '%s' as version from '%s'", version, label)
logrus.Debugf("parsed '%s' as digest from '%s'", digest, label)
return version, digest
// StackName gets whatever the docker safe (uses the right delimiting
// character, e.g. "_") stack name is for the app. In general, you don't want
// to use this to show anything to end-users, you want use a.Name instead.
func StackName(appName string) string {
stackName := SanitiseAppName(appName)
if len(stackName) > config.MAX_SANITISED_APP_NAME_LENGTH {
log.Debug(i18n.G("trimming %s to %s to avoid runtime limits", stackName, stackName[:config.MAX_SANITISED_APP_NAME_LENGTH]))
stackName = stackName[:config.MAX_SANITISED_APP_NAME_LENGTH]
}
return stackName
}
// ParseVersionName parses a $STACK_NAME_$SERVICE_NAME service label.
func ParseServiceName(label string) string {
idx := strings.LastIndex(label, "_")
serviceName := label[idx+1:]
logrus.Debugf("parsed '%s' as service name from '%s'", serviceName, label)
return serviceName
// Filters retrieves app filters for querying the container runtime. By default
// it filters on all services in the app. It is also possible to pass an
// otional list of service names, which get filtered instead.
//
// Due to upstream issues, filtering works different depending on what you're
// querying. So, for example, secrets don't work with regex! The caller needs
// to implement their own validation that the right secrets are matched. In
// order to handle these cases, we provide the `appendServiceNames` /
// `exactMatch` modifiers.
func (a App) Filters(appendServiceNames, exactMatch bool, services ...string) (filters.Args, error) {
filters := filters.NewArgs()
if len(services) > 0 {
for _, serviceName := range services {
filters.Add("name", ServiceFilter(a.StackName(), serviceName, exactMatch))
}
return filters, nil
}
// When not appending the service name, just add one filter for the whole
// stack.
if !appendServiceNames {
f := fmt.Sprintf("%s", a.StackName())
if exactMatch {
f = fmt.Sprintf("^%s", f)
}
filters.Add("name", f)
return filters, nil
}
composeFiles, err := a.Recipe.GetComposeFiles(a.Env)
if err != nil {
return filters, err
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := GetAppComposeConfig(a.Recipe.Name, opts, a.Env)
if err != nil {
return filters, err
}
for _, service := range compose.Services {
f := ServiceFilter(a.StackName(), service.Name, exactMatch)
filters.Add("name", f)
}
return filters, nil
}
// ServiceFilter creates a filter string for filtering a service in the docker
// container runtime. When exact match is true, it uses regex to match the
// string exactly.
func ServiceFilter(stack, service string, exact bool) string {
if exact {
return fmt.Sprintf("^%s_%s", stack, service)
}
return fmt.Sprintf("%s_%s", stack, service)
}
// ByServer sort a slice of Apps
type ByServer []App
func (a ByServer) Len() int { return len(a) }
func (a ByServer) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByServer) Less(i, j int) bool {
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
}
// ByServerAndRecipe sort a slice of Apps
type ByServerAndRecipe []App
func (a ByServerAndRecipe) Len() int { return len(a) }
func (a ByServerAndRecipe) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByServerAndRecipe) Less(i, j int) bool {
if a[i].Server == a[j].Server {
return strings.ToLower(a[i].Recipe.Name) < strings.ToLower(a[j].Recipe.Name)
}
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
}
// ByRecipe sort a slice of Apps
type ByRecipe []App
func (a ByRecipe) Len() int { return len(a) }
func (a ByRecipe) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByRecipe) Less(i, j int) bool {
return strings.ToLower(a[i].Recipe.Name) < strings.ToLower(a[j].Recipe.Name)
}
// ByName sort a slice of Apps
type ByName []App
func (a ByName) Len() int { return len(a) }
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByName) Less(i, j int) bool {
return strings.ToLower(a[i].Name) < strings.ToLower(a[j].Name)
}
func ReadAppEnvFile(appFile AppFile, name AppName) (App, error) {
env, err := envfile.ReadEnv(appFile.Path)
if err != nil {
return App{}, errors.New(i18n.G("env file for %s couldn't be read: %s", name, err.Error()))
}
app, err := NewApp(env, name, appFile)
if err != nil {
return App{}, errors.New(i18n.G("env file for %s has issues: %s", name, err.Error()))
}
return app, nil
}
// NewApp creates new App object
func NewApp(env envfile.AppEnv, name string, appFile AppFile) (App, error) {
domain := env["DOMAIN"]
recipeName, exists := env["RECIPE"]
if !exists {
recipeName, exists = env["TYPE"]
if !exists {
return App{}, errors.New(i18n.G("%s is missing the TYPE env var?", name))
}
}
return App{
Name: name,
Domain: domain,
Recipe: recipe.Get(recipeName),
Env: env,
Server: appFile.Server,
Path: appFile.Path,
}, nil
}
// LoadAppFiles gets all app files for a given set of servers or all servers.
func LoadAppFiles(servers ...string) (AppFiles, error) {
appFiles := make(AppFiles)
if len(servers) == 1 {
if servers[0] == "" {
// Empty servers flag, one string will always be passed
var err error
servers, err = config.GetAllFoldersInDirectory(config.SERVERS_DIR)
if err != nil {
return appFiles, err
}
}
}
log.Debug(i18n.G("collecting metadata from %v servers: %s", len(servers), strings.Join(servers, ", ")))
for _, server := range servers {
serverDir := path.Join(config.SERVERS_DIR, server)
files, err := config.GetAllFilesInDirectory(serverDir)
if err != nil {
return appFiles, errors.New(i18n.G("server %s doesn't exist? Run \"abra server ls\" to check", server))
}
for _, file := range files {
appName := strings.TrimSuffix(file.Name(), ".env")
appFilePath := path.Join(config.SERVERS_DIR, server, file.Name())
appFiles[appName] = AppFile{
Path: appFilePath,
Server: server,
}
}
}
return appFiles, nil
}
// GetAppServiceNames retrieves a list of app service names.
func GetAppServiceNames(appName string) ([]string, error) {
var serviceNames []string
appFiles, err := LoadAppFiles("")
if err != nil {
return serviceNames, err
}
app, err := GetApp(appFiles, appName)
if err != nil {
return serviceNames, err
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil {
return serviceNames, err
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := GetAppComposeConfig(app.Recipe.Name, opts, app.Env)
if err != nil {
return serviceNames, err
}
for _, service := range compose.Services {
serviceNames = append(serviceNames, service.Name)
}
return serviceNames, nil
}
// GetAppNames retrieves a list of app names.
func GetAppNames() ([]string, error) {
var appNames []string
appFiles, err := LoadAppFiles("")
if err != nil {
return appNames, err
}
apps, err := GetApps(appFiles, "")
if err != nil {
return appNames, err
}
for _, app := range apps {
appNames = append(appNames, app.Name)
}
return appNames, nil
}
// TemplateAppEnvSample copies the example env file for the app into the users
// env files.
func TemplateAppEnvSample(r recipe.Recipe, appName, server, domain string) error {
envSample, err := os.ReadFile(r.SampleEnvPath)
if err != nil {
return err
}
appEnvPath := path.Join(config.ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName))
if _, err := os.Stat(appEnvPath); !os.IsNotExist(err) {
return errors.New(i18n.G("%s already exists?", appEnvPath))
}
err = os.WriteFile(appEnvPath, envSample, 0o664)
if err != nil {
return err
}
read, err := os.ReadFile(appEnvPath)
if err != nil {
return err
}
newContents := strings.Replace(
string(read),
fmt.Sprintf("%s.example.com", r.Name),
domain,
-1,
)
err = os.WriteFile(appEnvPath, []byte(newContents), 0)
if err != nil {
return err
}
log.Debug(i18n.G("copied & templated %s to %s", r.SampleEnvPath, appEnvPath))
return nil
}
// SanitiseAppName makes a app name usable with Docker by replacing illegal
// characters.
func SanitiseAppName(name string) string {
return strings.ReplaceAll(name, ".", "_")
}
// GetAppStatuses queries servers to check the deployment status of given apps.
func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]string, error) {
statuses := make(map[string]map[string]string)
servers := make(map[string]struct{})
for _, app := range apps {
if _, ok := servers[app.Server]; !ok {
servers[app.Server] = struct{}{}
}
}
var bar *progressbar.ProgressBar
if !MachineReadable {
bar = formatter.CreateProgressbar(len(servers), i18n.G("querying remote servers..."))
}
ch := make(chan stack.StackStatus, len(servers))
for server := range servers {
cl, err := client.New(server)
if err != nil {
log.Warn(err)
ch <- stack.StackStatus{}
continue
}
go func(s string) {
ch <- stack.GetAllDeployedServices(cl, s)
if !MachineReadable {
bar.Add(1)
}
}(server)
}
for range servers {
status := <-ch
if status.Err != nil {
return statuses, status.Err
}
for _, service := range status.Services {
result := make(map[string]string)
name := service.Spec.Labels[convert.LabelNamespace]
if _, ok := statuses[name]; !ok {
result["status"] = "deployed"
}
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", name)
chaos, ok := service.Spec.Labels[labelKey]
if ok {
result["chaos"] = chaos
}
labelKey = fmt.Sprintf("coop-cloud.%s.chaos-version", name)
if chaosVersion, ok := service.Spec.Labels[labelKey]; ok {
result["chaosVersion"] = chaosVersion
}
labelKey = fmt.Sprintf("coop-cloud.%s.version", name)
if version, ok := service.Spec.Labels[labelKey]; ok {
result["version"] = version
} else {
continue
}
statuses[name] = result
}
}
log.Debug(i18n.G("retrieved app statuses: %s", statuses))
return statuses, nil
}
// GetAppComposeConfig retrieves a compose specification for a recipe. This
// specification is the result of a merge of all the compose.**.yml files in
// the recipe repository.
func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv envfile.AppEnv) (*composetypes.Config, error) {
compose, err := loader.LoadComposefile(opts, appEnv)
if err != nil {
return &composetypes.Config{}, err
}
log.Debug(i18n.G("retrieved %s for %s", compose.Filename, recipe))
return compose, nil
}
// ExposeAllEnv exposes all env variables to the app container
func ExposeAllEnv(
stackName string,
compose *composetypes.Config,
appEnv envfile.AppEnv,
toDeployVersion string) {
for _, service := range compose.Services {
if service.Name == "app" {
log.Debug(i18n.G("adding env vars to %s service config", stackName))
for k, v := range appEnv {
_, exists := service.Environment[k]
if !exists {
value := v
if k == "TYPE" || k == "RECIPE" {
// NOTE(d1): don't use the wrong version from the app env
// since we are deploying a new version
value = toDeployVersion
}
service.Environment[k] = &value
log.Debug(i18n.G("%s: %s: %s", stackName, k, value))
}
}
}
}
}
func CheckEnv(app App) ([]envfile.EnvVar, error) {
var envVars []envfile.EnvVar
envSample, err := app.Recipe.SampleEnv()
if err != nil {
return envVars, err
}
var keys []string
for key := range envSample {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
if _, ok := app.Env[key]; ok {
envVars = append(envVars, envfile.EnvVar{Name: key, Present: true})
} else {
envVars = append(envVars, envfile.EnvVar{Name: key, Present: false})
}
}
return envVars, nil
}
// ReadAbraShCmdNames reads the names of commands.
func ReadAbraShCmdNames(abraSh string) ([]string, error) {
var cmdNames []string
file, err := os.Open(abraSh)
if err != nil {
if os.IsNotExist(err) {
return cmdNames, nil
}
return cmdNames, err
}
defer file.Close()
cmdNameRegex, err := regexp.Compile(`(\w+)(\(\).*\{)`)
if err != nil {
return cmdNames, err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
matches := cmdNameRegex.FindStringSubmatch(line)
if len(matches) > 0 {
cmdNames = append(cmdNames, matches[1])
}
}
if len(cmdNames) > 0 {
log.Debug(i18n.G("read %s from %s", strings.Join(cmdNames, " "), abraSh))
} else {
log.Debug(i18n.G("read 0 command names from %s", abraSh))
}
return cmdNames, nil
}
// Wipe removes the version from the app .env file.
func (a App) WipeRecipeVersion() error {
file, err := os.Open(a.Path)
if err != nil {
return err
}
defer file.Close()
var (
lines []string
scanner = bufio.NewScanner(file)
)
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "RECIPE=") && !strings.HasPrefix(line, "TYPE=") {
lines = append(lines, line)
continue
}
if strings.HasPrefix(line, "#") {
lines = append(lines, line)
continue
}
splitted := strings.Split(line, ":")
lines = append(lines, splitted[0])
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
if err := os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm); err != nil {
log.Fatal(err)
}
log.Debug(i18n.G("version wiped from %s.env", a.Domain))
return nil
}
// WriteRecipeVersion writes the recipe version to the app .env file.
func (a App) WriteRecipeVersion(version string, dryRun bool) error {
if version == config.UNKNOWN_DEFAULT {
log.Debug(i18n.G("version is unknown, skipping env write"))
return nil
}
file, err := os.Open(a.Path)
if err != nil {
return err
}
defer file.Close()
var (
dirtyVersion string
skipped bool
lines []string
scanner = bufio.NewScanner(file)
)
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "RECIPE=") && !strings.HasPrefix(line, "TYPE=") {
lines = append(lines, line)
continue
}
if strings.HasPrefix(line, "#") {
lines = append(lines, line)
continue
}
if strings.Contains(line, version) && !a.Recipe.Dirty && !strings.HasSuffix(line, config.DIRTY_DEFAULT) {
skipped = true
lines = append(lines, line)
continue
}
splitted := strings.Split(line, ":")
line = fmt.Sprintf("%s:%s", splitted[0], version)
lines = append(lines, line)
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
if a.Recipe.Dirty && dirtyVersion != "" {
version = dirtyVersion
}
if !dryRun {
if err := os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm); err != nil {
log.Fatal(err)
}
} else {
log.Debug(i18n.G("skipping writing version %s because dry run", version))
}
if !skipped {
log.Debug(i18n.G("version %s saved to %s.env", version, a.Domain))
} else {
log.Debug(i18n.G("skipping version %s write as already exists in %s.env", version, a.Domain))
}
return nil
}
+288
View File
@@ -0,0 +1,288 @@
package app_test
import (
"encoding/json"
"fmt"
"reflect"
"testing"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/test"
"github.com/docker/docker/api/types/filters"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
)
var (
expectedAppEnv = envfile.AppEnv{
"DOMAIN": test.AppName,
"RECIPE": test.RecipeName,
}
expectedApp = appPkg.App{
Name: test.AppName,
Recipe: recipePkg.Get(expectedAppEnv["RECIPE"]),
Domain: expectedAppEnv["DOMAIN"],
Env: expectedAppEnv,
Path: expectedAppFile.Path,
Server: expectedAppFile.Server,
}
expectedAppFile = appPkg.AppFile{
Path: test.AppEnvPath,
Server: test.ServerName,
}
expectedAppFiles = map[string]appPkg.AppFile{
test.AppName: expectedAppFile,
}
)
func TestNewApp(t *testing.T) {
app, err := appPkg.NewApp(expectedAppEnv, test.AppName, expectedAppFile)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(app, expectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp)
}
}
func TestReadAppEnvFile(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
app, err := appPkg.ReadAppEnvFile(expectedAppFile, test.AppName)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(app, expectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp)
}
}
func TestGetApp(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
app, err := appPkg.GetApp(expectedAppFiles, test.AppName)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(app, expectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp)
}
}
func TestGetComposeFiles(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
r := recipe.Get(test.AbraTestRecipe)
if err := r.EnsureExists(); err != nil {
t.Fatal(err)
}
tests := []struct {
appEnv map[string]string
composeFiles []string
}{
{
map[string]string{},
[]string{
fmt.Sprintf("%s/compose.yml", r.Dir),
},
},
{
map[string]string{"COMPOSE_FILE": "compose.yml"},
[]string{
fmt.Sprintf("%s/compose.yml", r.Dir),
},
},
{
map[string]string{"COMPOSE_FILE": "compose.extra_secret.yml"},
[]string{
fmt.Sprintf("%s/compose.extra_secret.yml", r.Dir),
},
},
{
map[string]string{"COMPOSE_FILE": "compose.yml:compose.extra_secret.yml"},
[]string{
fmt.Sprintf("%s/compose.yml", r.Dir),
fmt.Sprintf("%s/compose.extra_secret.yml", r.Dir),
},
},
}
for _, test := range tests {
composeFiles, err := r.GetComposeFiles(test.appEnv)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, composeFiles, test.composeFiles)
}
}
func TestGetComposeFilesError(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
r := recipe.Get(test.AbraTestRecipe)
err := r.EnsureExists()
if err != nil {
t.Fatal(err)
}
tests := []struct{ appEnv map[string]string }{
{map[string]string{"COMPOSE_FILE": "compose.yml::compose.foo.yml"}},
{map[string]string{"COMPOSE_FILE": "doesnt.exist.yml"}},
}
for _, test := range tests {
_, err := r.GetComposeFiles(test.appEnv)
if err == nil {
t.Fatalf("should have failed: %v", test.appEnv)
}
}
}
func TestFilters(t *testing.T) {
oldDir := config.RECIPES_DIR
config.RECIPES_DIR = "./testdata"
defer func() {
config.RECIPES_DIR = oldDir
}()
app, err := appPkg.NewApp(envfile.AppEnv{
"DOMAIN": "test.example.com",
"RECIPE": "test-recipe",
}, "test_example_com", appPkg.AppFile{
Path: "./testdata/filtertest.end",
Server: "local",
})
if err != nil {
t.Fatal(err)
}
f, err := app.Filters(false, false)
if err != nil {
t.Error(err)
}
compareFilter(t, f, map[string]map[string]bool{
"name": {
"test_example_com": true,
},
})
f2, err := app.Filters(false, true)
if err != nil {
t.Error(err)
}
compareFilter(t, f2, map[string]map[string]bool{
"name": {
"^test_example_com": true,
},
})
f3, err := app.Filters(true, false)
if err != nil {
t.Error(err)
}
compareFilter(t, f3, map[string]map[string]bool{
"name": {
"test_example_com_bar": true,
"test_example_com_foo": true,
},
})
f4, err := app.Filters(true, true)
if err != nil {
t.Error(err)
}
compareFilter(t, f4, map[string]map[string]bool{
"name": {
"^test_example_com_bar": true,
"^test_example_com_foo": true,
},
})
f5, err := app.Filters(false, false, "foo")
if err != nil {
t.Error(err)
}
compareFilter(t, f5, map[string]map[string]bool{
"name": {
"test_example_com_foo": true,
},
})
}
func compareFilter(t *testing.T, f1 filters.Args, f2 map[string]map[string]bool) {
t.Helper()
j1, err := f1.MarshalJSON()
if err != nil {
t.Error(err)
}
j2, err := json.Marshal(f2)
if err != nil {
t.Error(err)
}
if diff := cmp.Diff(string(j2), string(j1)); diff != "" {
t.Errorf("filters mismatch (-want +got):\n%s", diff)
}
}
func TestWriteRecipeVersionOverwrite(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
app, err := appPkg.GetApp(expectedAppFiles, test.AppName)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := app.WipeRecipeVersion(); err != nil {
t.Fatal(err)
}
})
assert.Equal(t, "", app.Recipe.EnvVersion)
if err := app.WriteRecipeVersion("foo", false); err != nil {
t.Fatal(err)
}
app, err = appPkg.GetApp(expectedAppFiles, test.AppName)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "foo", app.Recipe.EnvVersion)
}
func TestWriteRecipeVersionUnknown(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
app, err := appPkg.GetApp(expectedAppFiles, test.AppName)
if err != nil {
t.Fatal(err)
}
if err := app.WriteRecipeVersion(config.UNKNOWN_DEFAULT, false); err != nil {
t.Fatal(err)
}
assert.NotEqual(t, config.UNKNOWN_DEFAULT, app.Recipe.EnvVersion)
}
+90
View File
@@ -0,0 +1,90 @@
package app
import (
"errors"
"fmt"
"strconv"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
composetypes "github.com/docker/cli/cli/compose/types"
)
// SetRecipeLabel adds the label 'coop-cloud.${STACK_NAME}.recipe=${RECIPE}' to the app container
// to signal which recipe is connected to the deployed app
func SetRecipeLabel(compose *composetypes.Config, stackName string, recipe string) {
for _, service := range compose.Services {
if service.Name == "app" {
log.Debug(i18n.G("set recipe label 'coop-cloud.%s.recipe' to %s for %s", stackName, recipe, stackName))
labelKey := fmt.Sprintf("coop-cloud.%s.recipe", stackName)
service.Deploy.Labels[labelKey] = recipe
}
}
}
// SetChaosLabel adds the label 'coop-cloud.${STACK_NAME}.chaos=true/false' to the app container
// to signal if the app is deployed in chaos mode
func SetChaosLabel(compose *composetypes.Config, stackName string, chaos bool) {
for _, service := range compose.Services {
if service.Name == "app" {
log.Debug(i18n.G("set label 'coop-cloud.%s.chaos' to %v for %s", stackName, chaos, stackName))
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", stackName)
service.Deploy.Labels[labelKey] = strconv.FormatBool(chaos)
}
}
}
// SetChaosVersionLabel adds the label 'coop-cloud.${STACK_NAME}.chaos-version=$(GIT_COMMIT)' to the app container
func SetChaosVersionLabel(compose *composetypes.Config, stackName string, chaosVersion string) {
for _, service := range compose.Services {
if service.Name == "app" {
log.Debug(i18n.G("set label 'coop-cloud.%s.chaos-version' to %v for %s", stackName, chaosVersion, stackName))
labelKey := fmt.Sprintf("coop-cloud.%s.chaos-version", stackName)
service.Deploy.Labels[labelKey] = chaosVersion
}
}
}
func SetVersionLabel(compose *composetypes.Config, stackName string, version string) {
for _, service := range compose.Services {
if service.Name == "app" {
log.Debug(i18n.G("set label 'coop-cloud.%s.version' to %v for %s", stackName, version, stackName))
labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName)
service.Deploy.Labels[labelKey] = version
}
}
}
// GetLabel reads docker labels in the format of "coop-cloud.${STACK_NAME}.${LABEL}" from the local compose files
func GetLabel(compose *composetypes.Config, stackName string, label string) string {
for _, service := range compose.Services {
if service.Name == "app" {
labelKey := fmt.Sprintf("coop-cloud.%s.%s", stackName, label)
log.Debug(i18n.G("get label '%s'", labelKey))
if labelValue, ok := service.Deploy.Labels[labelKey]; ok {
return labelValue
}
}
}
log.Debug(i18n.G("no %s label found for %s", label, stackName))
return ""
}
// GetTimeoutFromLabel reads the timeout value from docker label
// `coop-cloud.${STACK_NAME}.timeout=...` if present. A value is present if the
// operator uses a `TIMEOUT=...` in their app env.
func GetTimeoutFromLabel(compose *composetypes.Config, stackName string) (int, error) {
var timeout int
if timeoutLabel := GetLabel(compose, stackName, "timeout"); timeoutLabel != "" {
log.Debug(i18n.G("timeout label: %s", timeoutLabel))
var err error
timeout, err = strconv.Atoi(timeoutLabel)
if err != nil {
return timeout, errors.New(i18n.G("unable to convert timeout label %s to int: %s", timeoutLabel, err))
}
}
return timeout, nil
}
+63
View File
@@ -0,0 +1,63 @@
package app_test
import (
"testing"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/test"
testPkg "coopcloud.tech/abra/pkg/test"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/stretchr/testify/assert"
)
func TestGetTimeoutFromLabel(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
tests := []struct {
configuredTimeout string
expectedTimeout int
}{
{"0", 0},
{"DOESNTEXIST", 0}, // NOTE(d1): test when missing from .env
{"80", 80},
{"120", 120},
}
for _, test := range tests {
app, err := appPkg.GetApp(expectedAppFiles, testPkg.AppName)
if err != nil {
t.Fatal(err)
}
if test.configuredTimeout != "DOESNTEXIST" {
app.Env["TIMEOUT"] = test.configuredTimeout
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil {
t.Fatal(err)
}
deployOpts := stack.Deploy{
Composefiles: composeFiles,
Namespace: app.StackName(),
Prune: false,
ResolveImage: stack.ResolveImageAlways,
Detach: false,
}
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
if err != nil {
t.Fatal(err)
}
timeout, err := appPkg.GetTimeoutFromLabel(compose, app.StackName())
if err != nil {
t.Fatal(err)
}
assert.Equal(t, timeout, test.expectedTimeout)
}
}
+2
View File
@@ -0,0 +1,2 @@
RECIPE=test-recipe
DOMAIN=test.example.com
+6
View File
@@ -0,0 +1,6 @@
version: "3.8"
services:
foo:
image: debian
bar:
image: debian
+135
View File
@@ -0,0 +1,135 @@
package autocomplete
import (
"sort"
"strings"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra"
)
// AppNameComplete copletes app names.
func AppNameComplete() ([]string, cobra.ShellCompDirective) {
appFiles, err := app.LoadAppFiles("")
if err != nil {
err := i18n.G("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError
}
var appNames []string
for appName := range appFiles {
appNames = append(appNames, appName)
}
return appNames, cobra.ShellCompDirectiveDefault
}
func ServiceNameComplete(appName string) ([]string, cobra.ShellCompDirective) {
serviceNames, err := app.GetAppServiceNames(appName)
if err != nil {
err := i18n.G("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError
}
return serviceNames, cobra.ShellCompDirectiveDefault
}
// RecipeNameComplete completes recipe names.
func RecipeNameComplete() ([]string, cobra.ShellCompDirective) {
catl, err := recipe.ReadRecipeCatalogue(true)
if err != nil {
err := i18n.G("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError
}
localRecipes, err := recipe.GetRecipesLocal()
if err != nil && !strings.Contains(err.Error(), "empty") {
err := i18n.G("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError
}
var recipeNames []string
for name := range catl {
recipeNames = append(recipeNames, name)
}
for _, recipeLocal := range localRecipes {
recipeNames = append(recipeNames, recipeLocal)
}
return recipeNames, cobra.ShellCompDirectiveDefault
}
// RecipeVersionComplete completes versions for the recipe.
func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirective) {
catl, err := recipe.ReadRecipeCatalogue(true)
if err != nil {
err := i18n.G("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError
}
var recipeVersions []string
for _, v := range catl[recipeName].Versions {
for v2 := range v {
recipeVersions = append(recipeVersions, v2)
}
}
return recipeVersions, cobra.ShellCompDirectiveDefault
}
// ServerNameComplete completes server names.
func ServerNameComplete() ([]string, cobra.ShellCompDirective) {
files, err := app.LoadAppFiles("")
if err != nil {
err := i18n.G("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError
}
var serverNames []string
for _, appFile := range files {
serverNames = append(serverNames, appFile.Server)
}
return serverNames, cobra.ShellCompDirectiveDefault
}
// CommandNameComplete completes recipe commands.
func CommandNameComplete(appName string) ([]string, cobra.ShellCompDirective) {
app, err := app.Get(appName)
if err != nil {
err := i18n.G("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError
}
cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath)
if err != nil {
err := i18n.G("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError
}
sort.Strings(cmdNames)
return cmdNames, cobra.ShellCompDirectiveDefault
}
// SecretsComplete completes recipe secrets.
func SecretComplete(recipeName string) ([]string, cobra.ShellCompDirective) {
r := recipe.Get(recipeName)
config, err := r.GetComposeConfig(nil)
if err != nil {
err := i18n.G("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError
}
var secretNames []string
for name := range config.Secrets {
secretNames = append(secretNames, name)
}
return secretNames, cobra.ShellCompDirectiveDefault
}
+51 -221
View File
@@ -1,257 +1,87 @@
// Package catalogue provides ways of interacting with recipe catalogues which
// are JSON data structures which contain meta information about recipes (e.g.
// what versions of the Nextcloud recipe are available?).
package catalogue
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"strings"
"time"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/web"
"github.com/sirupsen/logrus"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5"
)
// RecipeCatalogueURL is the only current recipe catalogue available.
const RecipeCatalogueURL = "https://apps.coopcloud.tech"
// EnsureCatalogue ensures that the catalogue is cloned locally & present.
func EnsureCatalogue() error {
catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) {
log.Debug(i18n.G("catalogue is missing, retrieving now"))
// image represents a recipe container image.
type image struct {
Image string `json:"image"`
Rating string `json:"rating"`
Source string `json:"source"`
URL string `json:"url"`
}
// features represent what top-level features a recipe supports (e.g. does this
// recipe support backups?).
type features struct {
Backups string `json:"backups"`
Email string `json:"email"`
Healthcheck string `json:"healthcheck"`
Image image `json:"image"`
Status int `json:"status"`
Tests string `json:"tests"`
}
// tag represents a git tag.
type tag = string
// service represents a service within a recipe.
type service = string
// serviceMeta represents meta info associated with a service.
type serviceMeta struct {
Digest string `json:"digest"`
Image string `json:"image"`
Tag string `json:"tag"`
}
// RecipeMeta represents metadata for a recipe in the abra catalogue.
type RecipeMeta struct {
Category string `json:"category"`
DefaultBranch string `json:"default_branch"`
Description string `json:"description"`
Features features `json:"features"`
Icon string `json:"icon"`
Name string `json:"name"`
Repository string `json:"repository"`
Versions []map[tag]map[service]serviceMeta `json:"versions"`
Website string `json:"website"`
}
// LatestVersion returns the latest version of a recipe.
func (r RecipeMeta) LatestVersion() string {
var version string
// apps.json versions are sorted so the last key is latest
latest := r.Versions[len(r.Versions)-1]
for tag := range latest {
version = tag
}
logrus.Debugf("choosing '%s' as latest version of '%s'", version, r.Name)
return version
}
// Name represents a recipe name.
type Name = string
// RecipeCatalogue represents the entire recipe catalogue.
type RecipeCatalogue map[Name]RecipeMeta
// Flatten converts AppCatalogue to slice
func (r RecipeCatalogue) Flatten() []RecipeMeta {
recipes := make([]RecipeMeta, 0, len(r))
for name := range r {
recipes = append(recipes, r[name])
}
return recipes
}
// ByRecipeName sorts recipes by name.
type ByRecipeName []RecipeMeta
func (r ByRecipeName) Len() int { return len(r) }
func (r ByRecipeName) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r ByRecipeName) Less(i, j int) bool {
return strings.ToLower(r[i].Name) < strings.ToLower(r[j].Name)
}
// recipeCatalogueFSIsLatest checks whether the recipe catalogue stored locally
// is up to date.
func recipeCatalogueFSIsLatest() (bool, error) {
httpClient := &http.Client{Timeout: web.Timeout}
res, err := httpClient.Head(RecipeCatalogueURL)
if err != nil {
return false, err
}
lastModified := res.Header["Last-Modified"][0]
parsed, err := time.Parse(time.RFC1123, lastModified)
if err != nil {
return false, err
}
info, err := os.Stat(config.APPS_JSON)
if err != nil {
if os.IsNotExist(err) {
logrus.Debugf("no recipe catalogue found in file system cache")
return false, nil
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME)
if err := gitPkg.Clone(catalogueDir, url); err != nil {
return err
}
return false, err
}
localModifiedTime := info.ModTime().Unix()
remoteModifiedTime := parsed.Unix()
if localModifiedTime < remoteModifiedTime {
logrus.Debug("file system cached recipe catalogue is out-of-date")
return false, nil
}
logrus.Debug("file system cached recipe catalogue is up-to-date")
return true, nil
}
// ReadRecipeCatalogue reads the recipe catalogue.
func ReadRecipeCatalogue() (RecipeCatalogue, error) {
recipes := make(RecipeCatalogue)
recipeFSIsLatest, err := recipeCatalogueFSIsLatest()
if err != nil {
return nil, err
}
if !recipeFSIsLatest {
logrus.Debugf("reading recipe catalogue from web to get latest")
if err := readRecipeCatalogueWeb(&recipes); err != nil {
return nil, err
}
return recipes, nil
}
logrus.Debugf("reading recipe catalogue from file system cache to get latest")
if err := readRecipeCatalogueFS(&recipes); err != nil {
return nil, err
}
return recipes, nil
}
// readRecipeCatalogueFS reads the catalogue from the file system.
func readRecipeCatalogueFS(target interface{}) error {
recipesJSONFS, err := ioutil.ReadFile(config.APPS_JSON)
if err != nil {
return err
}
if err := json.Unmarshal(recipesJSONFS, &target); err != nil {
return err
}
logrus.Debugf("read recipe catalogue from file system cache in '%s'", config.APPS_JSON)
return nil
}
// readRecipeCatalogueWeb reads the catalogue from the web.
func readRecipeCatalogueWeb(target interface{}) error {
if err := web.ReadJSON(RecipeCatalogueURL, &target); err != nil {
return err
}
recipesJSON, err := json.MarshalIndent(target, "", " ")
// EnsureIsClean makes sure that the catalogue has no unstaged changes.
func EnsureIsClean() error {
isClean, err := gitPkg.IsClean(config.CATALOGUE_DIR)
if err != nil {
return err
}
if err := ioutil.WriteFile(config.APPS_JSON, recipesJSON, 0644); err != nil {
return err
if !isClean {
return errors.New(i18n.G("%s has locally unstaged changes? please commit/remove your changes before proceeding", config.CATALOGUE_DIR))
}
logrus.Debugf("read recipe catalogue from web at '%s'", RecipeCatalogueURL)
return nil
}
// VersionsOfService lists the version of a service.
func VersionsOfService(recipe, serviceName string) ([]string, error) {
catalogue, err := ReadRecipeCatalogue()
// EnsureUpToDate ensures that the local catalogue is up to date.
func EnsureUpToDate() error {
repo, err := git.PlainOpen(config.CATALOGUE_DIR)
if err != nil {
return nil, err
return err
}
rec, ok := catalogue[recipe]
if !ok {
return nil, fmt.Errorf("recipe '%s' does not exist?", recipe)
remotes, err := repo.Remotes()
if err != nil {
return err
}
versions := []string{}
alreadySeen := make(map[string]bool)
for _, serviceVersion := range rec.Versions {
for tag := range serviceVersion {
if _, ok := alreadySeen[tag]; !ok {
alreadySeen[tag] = true
versions = append(versions, tag)
}
if len(remotes) == 0 {
log.Debug(i18n.G("cannot ensure %s is up-to-date, no git remotes configured", config.CATALOGUE_DIR))
return nil
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
branch, err := gitPkg.CheckoutDefaultBranch(repo, config.CATALOGUE_DIR)
if err != nil {
return err
}
opts := &git.PullOptions{
Force: true,
ReferenceName: branch,
}
if err := worktree.Pull(opts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") {
return err
}
}
logrus.Debugf("detected versions '%s' for '%s'", strings.Join(versions, ", "), recipe)
log.Debug(i18n.G("fetched latest git changes for %s", config.CATALOGUE_DIR))
return versions, nil
}
// GetRecipeMeta retrieves the recipe metadata from the recipe catalogue.
func GetRecipeMeta(recipeName string) (RecipeMeta, error) {
catl, err := ReadRecipeCatalogue()
if err != nil {
return RecipeMeta{}, err
}
recipeMeta, ok := catl[recipeName]
if !ok {
err := fmt.Errorf("recipe '%s' does not exist?", recipeName)
return RecipeMeta{}, err
}
if err := recipe.EnsureExists(recipeName); err != nil {
return RecipeMeta{}, err
}
logrus.Debugf("recipe metadata retrieved for '%s'", recipeName)
return recipeMeta, nil
return nil
}
+95 -24
View File
@@ -2,39 +2,97 @@
package client
import (
"context"
"errors"
"net/http"
"os"
"path"
"strings"
"coopcloud.tech/abra/pkg/config"
contextPkg "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
sshPkg "coopcloud.tech/abra/pkg/ssh"
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
// New initiates a new Docker client.
func New(contextName string) (*client.Client, error) {
context, err := GetContext(contextName)
if err != nil {
return nil, err
}
// Conf is a Docker client configuration.
type Conf struct {
Timeout int
}
ctxEndpoint, err := GetContextEndpoint(context)
if err != nil {
return nil, err
}
// Opt is a Docker client option.
type Opt func(c *Conf)
helper := newConnectionHelper(ctxEndpoint)
httpClient := &http.Client{
// No tls, no proxy
Transport: &http.Transport{
DialContext: helper.Dialer,
},
// WithTimeout specifies a timeout for a Docker client.
func WithTimeout(timeout int) Opt {
return func(c *Conf) {
c.Timeout = timeout
}
}
// New initiates a new Docker client. New client connections are validated so
// that we ensure connections via SSH to the daemon can succeed. It takes into
// account that you may only want the local client and not communicate via SSH.
// For this use-case, please pass "default" as the serverName.
func New(serverName string, opts ...Opt) (*client.Client, error) {
var clientOpts []client.Opt
clientOpts = append(clientOpts,
client.WithHTTPClient(httpClient),
client.WithHost(helper.Host),
client.WithDialContext(helper.Dialer),
)
ctx, err := GetContext(serverName)
if err != nil {
serverDir := path.Join(config.SERVERS_DIR, serverName)
if _, err := os.Stat(serverDir); err != nil {
return nil, errors.New(i18n.G("server missing, run \"abra server add %s\"?", serverName))
}
// NOTE(p4u1): when the docker context does not exist but the server folder
// is there, let's create a new docker context.
if err = CreateContext(serverName); err != nil {
return nil, errors.New(i18n.G("server missing context, context creation failed: %s", err))
}
ctx, err = GetContext(serverName)
if err != nil {
return nil, errors.New(i18n.G("server missing context, run \"abra server add %s\"?", serverName))
}
}
ctxEndpoint, err := contextPkg.GetContextEndpoint(ctx)
if err != nil {
return nil, err
}
var isUnix bool
if strings.Contains(ctxEndpoint, "unix://") {
isUnix = true
}
if serverName != "default" && !isUnix {
conf := &Conf{}
for _, opt := range opts {
opt(conf)
}
helper, err := commandconnPkg.NewConnectionHelper(ctxEndpoint, conf.Timeout)
if err != nil {
return nil, err
}
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: helper.Dialer,
},
}
clientOpts = append(clientOpts,
client.WithHTTPClient(httpClient),
client.WithHost(helper.Host),
client.WithDialContext(helper.Dialer),
)
}
version := os.Getenv("DOCKER_API_VERSION")
if version != "" {
@@ -45,10 +103,23 @@ func New(contextName string) (*client.Client, error) {
cl, err := client.NewClientWithOpts(clientOpts...)
if err != nil {
logrus.Fatalf("unable to create Docker client: %s", err)
return nil, err
}
logrus.Debugf("created client for '%s'", contextName)
log.Debug(i18n.G("created client for %s", serverName))
info, err := cl.Info(context.Background())
if err != nil {
return cl, sshPkg.Fatal(serverName, err)
}
if info.Swarm.LocalNodeState == "inactive" {
if serverName != "default" && !isUnix {
return cl, errors.New(i18n.G("swarm mode not enabled on %s?", serverName))
}
return cl, errors.New(i18n.G("swarm mode not enabled on local server?"))
}
return cl, nil
}
+39
View File
@@ -0,0 +1,39 @@
package client
import (
"context"
"errors"
"coopcloud.tech/abra/pkg/i18n"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
)
func GetConfigs(cl *client.Client, ctx context.Context, server string, fs filters.Args) ([]swarm.Config, error) {
configList, err := cl.ConfigList(ctx, swarm.ConfigListOptions{Filters: fs})
if err != nil {
return configList, err
}
return configList, nil
}
func GetConfigNames(configs []swarm.Config) []string {
var confNames []string
for _, conf := range configs {
confNames = append(confNames, conf.Spec.Name)
}
return confNames
}
func RemoveConfigs(cl *client.Client, ctx context.Context, configNames []string, force bool) error {
for _, confName := range configNames {
if err := cl.ConfigRemove(context.Background(), confName); err != nil {
return errors.New(i18n.G("conf %s: %s", confName, err))
}
}
return nil
}
-45
View File
@@ -1,45 +0,0 @@
package client
import (
"github.com/docker/cli/cli/connhelper"
"github.com/docker/cli/cli/context/docker"
dCliContextStore "github.com/docker/cli/cli/context/store"
dClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
func newConnectionHelper(daemonURL string) *connhelper.ConnectionHelper {
helper, err := connhelper.GetConnectionHelper(daemonURL)
if err != nil {
logrus.Fatal(err)
}
return helper
}
func getDockerEndpoint(host string) (docker.Endpoint, error) {
skipTLSVerify := false
ep := docker.Endpoint{
EndpointMeta: docker.EndpointMeta{
Host: host,
SkipTLSVerify: skipTLSVerify,
},
}
// try to resolve a docker client, validating the configuration
opts, err := ep.ClientOpts()
if err != nil {
return docker.Endpoint{}, err
}
if _, err := dClient.NewClientWithOpts(opts...); err != nil {
return docker.Endpoint{}, err
}
return ep, nil
}
func getDockerEndpointMetadataAndTLS(host string) (docker.EndpointMeta, *dCliContextStore.EndpointTLSData, error) {
ep, err := getDockerEndpoint(host)
if err != nil {
return docker.EndpointMeta{}, nil, err
}
return ep.EndpointMeta, ep.TLSData.ToStoreTLSData(), nil
}
-7
View File
@@ -1,7 +0,0 @@
# github.com/docker/cli/cli/command/container
Due to this literally just being copy-pasted from the lib, the Apache license
will be posted in this folder. Small edits to the source code have been to
function names and parts we don't need deleted.
Same vibe as [../convert](../convert).
+16 -55
View File
@@ -4,36 +4,33 @@ import (
"errors"
"fmt"
command "github.com/docker/cli/cli/command"
"coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
dConfig "github.com/docker/cli/cli/config"
context "github.com/docker/cli/cli/context"
"github.com/docker/cli/cli/context/docker"
contextStore "github.com/docker/cli/cli/context/store"
"github.com/moby/term"
"github.com/sirupsen/logrus"
)
type Context = contextStore.Metadata
func CreateContext(contextName string, user string, port string) error {
host := contextName
if user != "" {
host = fmt.Sprintf("%s@%s", user, host)
}
if port != "" {
host = fmt.Sprintf("%s:%s", host, port)
}
host = fmt.Sprintf("ssh://%s", host)
// CreateContext creates a new Docker context.
func CreateContext(contextName string) error {
host := fmt.Sprintf("ssh://%s", contextName)
if err := createContext(contextName, host); err != nil {
return err
}
logrus.Debugf("created the '%s' context", contextName)
log.Debug(i18n.G("created the %s context", contextName))
return nil
}
// createContext interacts with Docker Context to create a Docker context config
func createContext(name string, host string) error {
s := NewDefaultDockerContextStore()
s := context.NewDefaultDockerContextStore()
contextMetadata := contextStore.Metadata{
Endpoints: make(map[string]interface{}),
Name: name,
@@ -43,7 +40,7 @@ func createContext(name string, host string) error {
Endpoints: make(map[string]contextStore.EndpointTLSData),
}
dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(host)
dockerEP, dockerTLS, err := commandconnPkg.GetDockerEndpointMetadataAndTLS(host)
if err != nil {
return err
}
@@ -66,63 +63,27 @@ func createContext(name string, host string) error {
func DeleteContext(name string) error {
if name == "default" {
return errors.New("context 'default' cannot be removed")
return errors.New(i18n.G("context 'default' cannot be removed"))
}
if _, err := GetContext(name); err != nil {
return err
}
// remove any context that might be loaded
// TODO: Check if the context we are removing is the active one rather than doing it all the time
cfg := dConfig.LoadDefaultConfigFile(nil)
cfg.CurrentContext = ""
if err := cfg.Save(); err != nil {
return err
}
return NewDefaultDockerContextStore().Remove(name)
return context.NewDefaultDockerContextStore().Remove(name)
}
func GetContext(contextName string) (contextStore.Metadata, error) {
ctx, err := NewDefaultDockerContextStore().GetMetadata(contextName)
ctx, err := context.NewDefaultDockerContextStore().GetMetadata(contextName)
if err != nil {
return contextStore.Metadata{}, err
}
return ctx, nil
}
func GetContextEndpoint(ctx contextStore.Metadata) (string, error) {
// safe to use docker key hardcoded since abra doesn't use k8s... yet...
endpointmeta, ok := ctx.Endpoints["docker"].(context.EndpointMetaBase)
if !ok {
err := errors.New("context lacks Docker endpoint")
return "", err
}
return endpointmeta.Host, nil
}
func newContextStore(dir string, config contextStore.Config) contextStore.Store {
return contextStore.New(dir, config)
}
func NewDefaultDockerContextStore() *command.ContextStoreWithDefault {
// Grabbing the stderr from Docker commands
// Much easier to fit this into the code we are using to replicate docker cli commands
_, _, stderr := term.StdStreams()
// TODO: Look into custom docker configs in case users want that
dockerConfig := dConfig.LoadDefaultConfigFile(stderr)
contextDir := dConfig.ContextStoreDir()
storeConfig := command.DefaultContextStoreConfig()
store := newContextStore(contextDir, storeConfig)
dockerContextStore := &command.ContextStoreWithDefault{
Store: store,
Resolver: func() (*command.DefaultContext, error) {
// nil for the Opts because it works without it and its a cli thing
return command.ResolveDefaultContext(nil, dockerConfig, storeConfig, stderr)
},
}
return dockerContextStore
}
-52
View File
@@ -1,52 +0,0 @@
package client_test
import (
"testing"
"coopcloud.tech/abra/pkg/client"
dContext "github.com/docker/cli/cli/context"
dCliContextStore "github.com/docker/cli/cli/context/store"
)
type TestContext struct {
context dCliContextStore.Metadata
expected_endpoint string
}
func dockerContext(host, key string) TestContext {
dockerContext := dCliContextStore.Metadata{
Name: "foo",
Metadata: nil,
Endpoints: map[string]interface{}{
key: dContext.EndpointMetaBase{
Host: host,
SkipTLSVerify: false,
},
},
}
return TestContext{
context: dockerContext,
expected_endpoint: host,
}
}
func TestGetContextEndpoint(t *testing.T) {
var testDockerContexts = []TestContext{
dockerContext("ssh://foobar", "docker"),
dockerContext("ssh://foobar", "k8"),
}
for _, context := range testDockerContexts {
endpoint, err := client.GetContextEndpoint(context.context)
if err != nil {
if err.Error() != "context lacks Docker endpoint" {
t.Error(err)
}
} else {
if endpoint != context.expected_endpoint {
t.Errorf("did not get correct context endpoint. Expected: %s, received: %s", context.expected_endpoint, endpoint)
}
}
}
}
-10
View File
@@ -1,10 +0,0 @@
# github.com/docker/cli/cli/compose/convert
DISCLAIMER: This is like the entire `github.com/docker/cli/cli/compose/convert`
package. This should be an easy import but importing it creates DEPENDENCY
HELL. I tried for an hour to fix it but it would work. TRY TO FIX AT YOUR OWN
RISK!!!
Due to this literally just being copy-pasted from the lib, the Apache license
will be posted in this folder. Small edits to the source code have been to
function names and parts we don't need deleted.
+16 -156
View File
@@ -1,170 +1,30 @@
package client
import (
"encoding/json"
"context"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"coopcloud.tech/abra/pkg/web"
"github.com/docker/distribution/reference"
"coopcloud.tech/abra/pkg/i18n"
"github.com/containers/image/docker"
"github.com/containers/image/types"
"github.com/distribution/reference"
)
type RawTag struct {
Layer string
Name string
}
// GetRegistryTags retrieves all tags of an image from a container registry.
func GetRegistryTags(img reference.Named) ([]string, error) {
var tags []string
type RawTags []RawTag
ref, err := docker.ParseReference(fmt.Sprintf("//%s", img))
if err != nil {
return tags, errors.New(i18n.G("failed to parse image %s, saw: %s", img, err.Error()))
}
var registryURL = "https://registry.hub.docker.com/v1/repositories/%s/tags"
func GetRegistryTags(image string) (RawTags, error) {
var tags RawTags
tagsUrl := fmt.Sprintf(registryURL, image)
if err := web.ReadJSON(tagsUrl, &tags); err != nil {
ctx := context.Background()
tags, err = docker.GetRepositoryTags(ctx, &types.SystemContext{}, ref)
if err != nil {
return tags, err
}
return tags, nil
}
// getRegv2Token retrieves a registry v2 authentication token.
func getRegv2Token(image reference.Named) (string, error) {
img := reference.Path(image)
authTokenURL := fmt.Sprintf("https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull", img)
req, err := http.NewRequest("GET", authTokenURL, nil)
if err != nil {
return "", err
}
client := &http.Client{Timeout: web.Timeout}
res, err := client.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
_, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", nil
}
tokenRes := struct {
Token string
Expiry string
Issued string
}{}
if err := json.Unmarshal(body, &tokenRes); err != nil {
return "", err
}
return tokenRes.Token, nil
}
// GetTagDigest retrieves an image digest from a v2 registry
func GetTagDigest(image reference.Named) (string, error) {
img := reference.Path(image)
tag := image.(reference.NamedTagged).Tag()
manifestURL := fmt.Sprintf("https://index.docker.io/v2/%s/manifests/%s", img, tag)
req, err := http.NewRequest("GET", manifestURL, nil)
if err != nil {
return "", err
}
token, err := getRegv2Token(image)
if err != nil {
return "", err
}
req.Header = http.Header{
"Accept": []string{
"application/vnd.docker.distribution.manifest.v2+json",
"application/vnd.docker.distribution.manifest.list.v2+json",
},
"Authorization": []string{fmt.Sprintf("Bearer %s", token)},
}
client := &http.Client{Timeout: web.Timeout}
res, err := client.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
_, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
registryResT1 := struct {
SchemaVersion int
MediaType string
Manifests []struct {
MediaType string
Size int
Digest string
Platform struct {
Architecture string
Os string
}
}
}{}
registryResT2 := struct {
SchemaVersion int
MediaType string
Config struct {
MediaType string
Size int
Digest string
}
Layers []struct {
MediaType string
Size int
Digest string
}
}{}
if err := json.Unmarshal(body, &registryResT1); err != nil {
return "", err
}
var digest string
for _, manifest := range registryResT1.Manifests {
if string(manifest.Platform.Architecture) == "amd64" {
digest = strings.Split(manifest.Digest, ":")[1][:7]
}
}
if digest == "" {
if err := json.Unmarshal(body, &registryResT2); err != nil {
return "", err
}
digest = strings.Split(registryResT2.Config.Digest, ":")[1][:7]
}
if digest == "" {
return "", fmt.Errorf("Unable to retrieve amd64 digest for '%s'", image)
}
return digest, nil
}
+11 -9
View File
@@ -4,22 +4,24 @@ import (
"context"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
)
func StoreSecret(secretName, secretValue, server string) error {
cl, err := New(server)
if err != nil {
return err
}
ctx := context.Background()
func StoreSecret(cl *client.Client, secretName, secretValue string) error {
ann := swarm.Annotations{Name: secretName}
spec := swarm.SecretSpec{Annotations: ann, Data: []byte(secretValue)}
// We don't bother with the secret IDs for now
if _, err := cl.SecretCreate(ctx, spec); err != nil {
if _, err := cl.SecretCreate(context.Background(), spec); err != nil {
return err
}
return nil
}
func GetSecretNames(secrets []swarm.Secret) []string {
var secretNames []string
for _, secret := range secrets {
secretNames = append(secretNames, secret.Spec.Name)
}
return secretNames
}

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