Compare commits

..

2214 Commits

Author SHA1 Message Date
decentral1se 6da0aa1224 chore: make deps
continuous-integration/drone/push Build is failing
2026-01-17 00:13:07 +01:00
ammaratef45 182fc41c58 added integration test
continuous-integration/drone/push Build is failing
2026-01-16 14:27:27 -08:00
ammaratef45 304ac87cec ensure repo is up to date before printing status
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2026-01-14 16:58:21 -08:00
namnatulco 5b3929d885 modified install script to output a safe PATH-update (fixes #735)
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-01-02 10:19:10 +01:00
Weblate c41df874d1 chore: update translation files
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is failing
2025-12-21 16:39:02 +01:00
ammaratef45 42f9e6d458 return error instead of handling it inside getLatestVersion
continuous-integration/drone/push Build is failing
2025-12-19 06:50:35 -08:00
ammaratef45 9e7bc31d4d avoiding #732 by checking for empty versions list for recipe sync
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-12-18 14:41:58 -08:00
Weblate b79c4f33b6 chore: update translation files
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/tag Build is failing
continuous-integration/drone/push Build is passing
2025-11-09 11:46:38 +01:00
decentral1se bb93e4266a fix: show domain with https (clickable)
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #643
2025-11-09 10:56:21 +01:00
decentral1se a2cc70b2f5 test: reinstate debug
continuous-integration/drone/push Build is failing
2025-11-09 10:43:09 +01:00
decentral1se ce1aa3d870 test: ensure recipe sync is robust
continuous-integration/drone/push Build is passing
2025-11-09 10:41:32 +01:00
decentral1se d75700c8a9 test: ensure env vars updated
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #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
continuous-integration/drone/push Build is passing
See #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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-11-04 11:49:44 -03:00
Weblate 35eb9d4a89 chore: update translation files
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2025-11-04 15:34:27 +01:00
Weblate 797b8d899b chore: update translation files
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2025-11-04 14:53:15 +01:00
decentral1se c3a2048eba fix: throw away unknown version
continuous-integration/drone/push Build is failing
See #715
2025-11-04 13:52:49 +00:00
Apfelwurm 1bdc11ba62 fix no-input app deployment when no tty is present
continuous-integration/drone/push Build is failing
2025-11-04 13:52:28 +00:00
Weblate cc8703310c chore: update translation files
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-11-03 17:29:04 +01:00
decentral1se b0cf2a1f8e chore: make i18n
continuous-integration/drone/push Build is passing
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 #713
2025-11-02 10:44:34 +00:00
Weblate ee8de8ef5c chore: update translation files
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2025-10-31 21:38:01 +01:00
decentral1se 2cca04de90 fix(move): does not error when secret already exists on new server
See #709
2025-10-31 21:37:55 +01:00
Weblate f2f79e2df8 chore: update translation files
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2025-10-31 21:31:49 +01:00
decentral1se dc2cd85d91 feat!: abra app env pull
`abra app env` -> `abra app env list`.

See #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
continuous-integration/drone/push Build is passing
2025-10-25 20:01:23 +02:00
decentral1se e5f5154197 test: kadabra is gone
continuous-integration/drone/push Build is passing
2025-10-23 20:58:08 +02:00
decentral1se 6c1c0a8a8a refactor: use xgettext-go from makefile variable
continuous-integration/drone/push Build is passing
Easier to hack when customising xgettext-go.
2025-10-23 09:26:51 +02:00
Weblate 662f45008c chore: update translation files
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
See #688
2025-10-23 09:14:15 +02:00
Weblate d58552b748 chore: update translation files
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
See #688
2025-10-23 09:10:36 +02:00
Weblate 3f6a22747f chore: update translation files
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
See #688
2025-10-23 09:08:50 +02:00
Weblate fd4ee75ab7 chore: update translation files
continuous-integration/drone/push Build is passing
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)
continuous-integration/drone/push Build is passing
2025-10-19 15:46:18 +02:00
Weblate fcb3167394 chore: update translation files
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2025-10-19 15:32:38 +02:00
3wordchant 0dc5c307af chore: update i18n
continuous-integration/drone/push Build is passing
2025-10-18 16:03:11 -04:00
3wordchant fc5855ff28 feat: Add hexadecimal secret generation
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
Closes #695
2025-10-18 15:03:02 -04:00
decentral1se 5b504a1550 Revert "feat: cctuip lands in main"
continuous-integration/drone/push Build is passing
See #691 (comment)
2025-10-17 19:27:23 +02:00
Weblate fc16a21f1c chore: update translation files
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-10-03 20:35:47 +02:00
decentral1se d0ccb805c6 refactor: isolate expensive IsDirty() call
continuous-integration/drone/push Build is failing
See #689
2025-10-03 20:35:09 +02:00
decentral1se 2460dd9438 fix: pagination with multiline(true)
See #689
2025-10-03 20:13:35 +02:00
Weblate 9c648a2566 chore: update translation files
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-10-01 21:13:41 +02:00
decentral1se 4b4c56d406 fix: skip borked tags on app list
See #656
2025-10-01 21:13:18 +02:00
decentral1se 4314195dd7 test: dont run xgettext-go on release
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #663
2025-10-01 12:20:16 +02:00
Weblate df4447b038 chore: update translation files
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-10-01 12:12:07 +02:00
decentral1se a430b1e4fd fix: dont show unchanged images/tags
See #677
2025-10-01 12:11:29 +02:00
Weblate 896c434f38 chore: update translation files
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-10-01 11:49:19 +02:00
decentral1se 89d5fc91b0 fix: gracefully explode of missing context
See #675
2025-10-01 11:48:51 +02:00
Weblate 5af3c5f56e chore: update translation files
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #669
2025-10-01 09:09:40 +02:00
Weblate 40aab6a6c1 chore: update translation files
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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 #668
2025-10-01 08:19:47 +02:00
decentral1se c794d533be fix: avoid hanging when tasks randomly surge
See #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 #659
2025-10-01 08:19:39 +02:00
decentral1se fbb1f16470 fix: dont overwrite label when chaos
See #668
2025-10-01 08:19:38 +02:00
decentral1se 2473cafdf5 test: use new CI option for xgettext-go
continuous-integration/drone/push Build is passing
2025-09-30 19:23:23 +02:00
Weblate 0ccfbd253e chore: update translation files
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is failing
2025-09-30 19:20:35 +02:00
Weblate 4fa9f536eb chore: update translation files
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is failing
See #647
See toolshed/xgettext-go#1
2025-09-30 19:08:52 +02:00
Weblate 0db1ee87fc chore: update translation files
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-09-14 13:02:21 -04:00
decentral1se 1c4abcf12f chore: bump installer
continuous-integration/drone/tag Build is failing
continuous-integration/drone/push Build is passing
2025-09-10 22:06:12 +02:00
decentral1se f590870672 chore: make i18n
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2025-09-09 17:37:36 -04:00
3wordchant 26c920e570 chore: i18n.G, update POT
continuous-integration/drone/push Build is passing
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()
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-09-09 16:55:23 -04:00
3wordchant 5bb6241172 test: fix "bail if env has a hash but no --chaos" test
continuous-integration/drone/push Build is passing
2025-09-09 16:03:22 -04:00
3wordchant 66e6a2c47e chore: update POT
continuous-integration/drone/push Build is passing
2025-09-09 15:59:45 -04:00
3wordchant d866527138 fix: set "chaos" if a specified* version is "chaos-y"
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
*either from env file or command-line
2025-09-09 13:25:18 -04:00
3wordchant 39d1997edf chore: update POT
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #652
2025-09-06 08:38:38 +02:00
decentral1se 02add8c3ef chore: make i18n
continuous-integration/drone/push Build is passing
2025-09-06 08:28:53 +02:00
decentral1se 560d609013 test: ensure autocomplete output works
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
See #649
2025-09-06 08:26:50 +02:00
decentral1se b4c9fbfe6d fix: use local flag
See #648
2025-09-06 08:24:37 +02:00
3wordchant 7f456a3f24 chore: 4matting
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2025-09-05 17:09:39 -04:00
3wordchant bef92d53a8 fix: don't translate blank string
continuous-integration/drone/push Build is failing
2025-09-05 13:33:51 -04:00
3wordchant a4b47b431b fix: temporary fix to #648
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-09-05 13:29:31 -04:00
decentral1se bddf8039af test: ensure previous versions not deleted
continuous-integration/drone/push Build is passing
See #615
2025-09-04 08:26:21 +00:00
Weblate d74e760940 Update translation files
continuous-integration/drone/push Build is passing
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..
continuous-integration/drone/push Build is passing
..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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #628
2025-09-03 08:58:31 +02:00
decentral1se e1f029d2db chore: make i18n
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2025-09-01 13:48:10 +02:00
decentral1se f0e2b012c6 chore: make i18n
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See toolshed/organising#521
2025-08-30 12:45:48 +02:00
decentral1se 09176801e1 feat: warn for secret generation
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-08-30 12:27:29 +02:00
decentral1se 36d4648114 feat: add config dir help
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See toolshed/organising#470
2025-08-30 12:12:40 +02:00
decentral1se 83ca2a63d1 fix: support ValidArgs translation
continuous-integration/drone/push Build is passing
See #632
2025-08-30 12:02:13 +02:00
decentral1se e25ce5d1a0 chore: make i18n
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
See #627
2025-08-30 11:39:49 +02:00
decentral1se 52f02ad9b9 translate: support usage translations
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #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
continuous-integration/drone/push Build is passing
See #617
2025-08-29 16:38:49 +02:00
decentral1se 86ba006e17 chore: fix merge conflict from weblate
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-08-29 09:58:42 +02:00
decentral1se e019142c9e fix: show when logs dont get collected
See #575
2025-08-29 09:58:26 +02:00
ChasquiLabo e23c6197b5 Translated using Weblate (Spanish)
continuous-integration/drone/push Build is passing
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?)
continuous-integration/drone/push Build is passing
2025-08-28 11:44:21 -04:00
3wordchant 02b520200e Mark command short descriptions with translators: tag
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-28 11:33:22 -04:00
3wordchant acb6170768 Add make find-tests
continuous-integration/drone/push Build is passing
2025-08-28 14:26:27 +00:00
decentral1se e04af4e582 chore: make i18n
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-08-28 16:18:10 +02:00
decentral1se 8bf0d7addc fix: wait for containers to go away
See #564
2025-08-28 16:17:37 +02:00
3wordchant 20909695e0 Translated using Weblate (Spanish)
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2025-08-27 15:29:45 -04:00
3wordchant d3d3358a79 Sort files for make update-pot
continuous-integration/drone/push Build is failing
2025-08-27 13:00:40 -04:00
3wordchant cb310c56b5 Add comments_tag
continuous-integration/drone/push Build is failing
2025-08-27 12:58:06 -04:00
3wordchant 67c0a64f60 Run xgettext-go
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is failing
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)
continuous-integration/drone/push Build is passing
Reviewed-on: #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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-08-26 13:37:15 -04:00
3wordchant 7dd7f763f4 Moar PR feedback
continuous-integration/drone/push Build is passing
2025-08-26 13:15:06 -04:00
3wordchant 7b7477062f Implement PR feedback
continuous-integration/drone/push Build is passing
2025-08-25 18:12:15 -04:00
3wordchant 238647a987 Prompt for secrets if not provided on CLI
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Re #646
2025-08-25 17:16:50 -04:00
decentral1se f39eab8f1e test: pass ABRA_DIR to unit test on CI
continuous-integration/drone/push Build is passing
2025-08-25 11:55:33 +02:00
decentral1se 6a52575ae0 refactor!: do not set default timeout
continuous-integration/drone/push Build is failing
See #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
continuous-integration/drone/push Build is passing
See #579
2025-08-24 15:56:26 +02:00
decentral1se 4cb660c348 fix: more robust -p failure handling
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #576
2025-08-24 13:05:03 +02:00
decentral1se 42dde0930d fix: handle translations
continuous-integration/drone/push Build is passing
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 #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
continuous-integration/drone/push Build is passing
See #483
2025-08-23 17:55:56 +02:00
decentral1se 5cf6048ecb test: also wipe env version
continuous-integration/drone/push Build is passing
2025-08-19 09:49:02 +02:00
decentral1se 3e2797c433 test: fixups for latest nightly failures
continuous-integration/drone/push Build is passing
2025-08-19 09:41:37 +02:00
decentral1se df89e8143a chore: clean cruft / formatting / add whitespace
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
See #560
2025-08-18 09:47:10 +02:00
decentral1se a90e239547 refactor!: ensure insert/remove not arbitrary
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
See #601
2025-08-18 08:56:52 +02:00
decentral1se 60b3af1fa4 test: retrieve abra-test-recipe by hand now
continuous-integration/drone/push Build is passing
2025-08-18 06:32:41 +00:00
decentral1se 5f4b5e0fad fix: return error, not log.Fatal
See #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
continuous-integration/drone/push Build is passing
See #581
2025-08-18 08:29:14 +02:00
decentral1se f5a843bd90 feat: remove old app configs
continuous-integration/drone/push Build is passing
See #577
2025-08-17 13:27:35 +00:00
decentral1se fac372dc73 test: use latest release for check
continuous-integration/drone/push Build is passing
2025-08-17 15:18:05 +02:00
decentral1se 8a3be01c3e fix: $ABRA_DIR/servers subdirs also 0700
continuous-integration/drone/push Build is passing
See 38f308910a
See #580
2025-08-17 14:13:47 +02:00
decentral1se 4193d63d23 test: advertise locally to avoid multiple ip error
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-08-17 14:04:00 +02:00
decentral1se 38f308910a fix: $ABRA_DIR/servers=0700, $ABRA_DIR/servers/foo=0600
See #580
2025-08-17 14:03:15 +02:00
decentral1se 4aaa7400b8 fix: also ensure server is created with 0600
continuous-integration/drone/push Build is passing
See 6849e3554d
2025-08-17 13:32:37 +02:00
decentral1se 091611b984 feat: add volume arg to volume rm
continuous-integration/drone/push Build is passing
See #574
2025-08-17 11:16:30 +00:00
decentral1se 2cfc40dc28 fix: ensure recipe with undeploy
continuous-integration/drone/push Build is passing
See #573
2025-08-17 09:36:48 +00:00
decentral1se 6849e3554d fix: ensure $ABRA_DIR/servers is 0600
continuous-integration/drone/push Build is passing
Also remove deprecated folders while I'm here: `vendor` / `backup`

See #580
2025-08-17 09:17:00 +00:00
decentral1se 452de7fdc2 docs: show HOWTO generate in abra man help
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #568
2025-08-13 13:08:48 +02:00
decentral1se 952d768ab0 docs: show app secret rm example
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Closes #558
2025-08-12 21:34:57 +02:00
iexos 2c91d2040e fix: app ls -S didn't show updates sometimes (#561)
continuous-integration/drone/push Build is passing
2025-08-12 13:10:16 +00:00
3wordchant eff4435971 ABRA_TEST_DOMAIN → TEST_SERVER
continuous-integration/drone/push Build is passing
2025-08-12 12:18:49 +00:00
3wordchant 032fe99086 Appease formatter
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2025-08-12 05:17:15 +00:00
p4u1 157d131b37 feat: Retrieves auth token from image
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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)
continuous-integration/drone/push Build is passing
continuous-integration/drone Build was killed
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-07 15:01:31 +01:00
decentral1se 2fbef41a3a test: clean up properly
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-04-24 14:29:54 +02:00
decentral1se 6fb41e5300 fix: dont parse chaos version
continuous-integration/drone/push Build is passing
See #547
2025-04-24 11:24:14 +00:00
decentral1se 1432f480c7 fix: -T/--tty disables TTY remote request
continuous-integration/drone/push Build is passing
See #499
2025-04-24 08:57:53 +00:00
decentral1se 83af39771b test: drop unused tagHash
continuous-integration/drone/push Build is passing
2025-04-24 10:57:34 +02:00
decentral1se 4d1333202e test: flaky test when no RC is available
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
See #528
2025-04-22 22:56:10 +02:00
decentral1se 229e8eb9da feat: --ssh/--force for recipe fetch
continuous-integration/drone/push Build is passing
See toolshed/organising#546
2025-04-22 09:35:36 +00:00
decentral1se b3ab95750e fix: trim final newline on release note
continuous-integration/drone/push Build is passing
Follow-up #544
2025-04-22 09:23:28 +02:00
decentral1se de009921a2 fix: show release notes once
continuous-integration/drone/push Build is passing
See #543
2025-04-22 05:47:03 +00:00
decentral1se d081bbaefa feat: auto select single server
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See toolshed/organising#513
2025-04-21 21:06:29 +02:00
decentral1se 515b5466ca docs: add missing arg
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #540
2025-04-21 20:17:39 +02:00
decentral1se 6965799bdc chore: publish 0.10.0-beta
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-04-21 19:11:31 +02:00
decentral1se f75c9a6259 test: clean up test server correctly
continuous-integration/drone/push Build is passing
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"
continuous-integration/drone/push Build is passing
2025-04-19 07:28:15 +00:00
p4u1 fa084a61d2 fix(lint): Improves error message if a lint rule errors
continuous-integration/drone/push Build is passing
This was detected while debugging #534
2025-04-16 05:12:19 +00:00
decentral1se 895a7fe7d6 fix: don't overwrite recipeVersion
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
See #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
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-03-23 11:08:17 +01:00
decentral1se cdfb8a08bb chore: publish 0.10.0-rc2-beta
continuous-integration/drone/tag Build is failing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #507
2025-03-23 10:27:58 +01:00
decentral1se 47045ca8f1 feat: improved deploy progress reporting
continuous-integration/drone/push Build is passing
See #478
2025-03-23 09:13:36 +00:00
Apfelwurm d0f982456e add charset modifier to secret generation (#521)
continuous-integration/drone/push Build is passing
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: #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)
continuous-integration/drone/push Build is passing
Fixes #488

Reviewed-on: #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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-16 13:54:07 +01:00
decentral1se d1e49d17ce test: on-demand integration tests
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-16 13:22:50 +01:00
decentral1se 1574aa0631 refactor!: status between service/image
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #487 (comment)
2025-03-16 12:42:09 +01:00
decentral1se 1723025fbf build: go 1.24
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Follows #510.
See https://build.coopcloud.tech/toolshed/abra/2620/1/5.
2025-03-16 11:49:38 +01:00
p4u1 0a371ec360 fix: integration tests
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-13 08:31:11 +01:00
p4u1 e58a716fe1 feat(deploy): Simplifies deploy overview (#508)
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
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: #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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2025-02-11 09:57:09 +00:00
p4u1 7b6ea76437 fix(secret): Checks for enough arguments
continuous-integration/drone/push Build is passing
2025-02-11 09:55:03 +00:00
p4u1 9069758969 fix(cmd): Uses uppercase t for tty shorthand flag
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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)
continuous-integration/drone/push Build is passing
Fixes #494

Reviewed-on: #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
continuous-integration/drone/push Build is passing
#486
2025-01-17 17:46:41 +01:00
decentral1se 64ad60663f test: adjust for new abra-test-recipe version
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #470
2025-01-09 13:14:47 +00:00
decentral1se cb3f46b46e fix: redirect to stderr for machine output
continuous-integration/drone/push Build is passing
See #477
2025-01-09 11:23:36 +00:00
decentral1se 41e514ae9a test: reset after deploy
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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 #478
2025-01-09 11:54:37 +01:00
decentral1se eb6fe4ba6e fix: dont set chaos label if no chaos
See #478
2025-01-09 11:54:36 +01:00
decentral1se 993172d31b test: ensure .env version written
continuous-integration/drone/push Build is passing
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 #471
2025-01-08 13:42:35 +00:00
decentral1se b6009057a8 docs: note temp autocomplete, less whitespace
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-08 12:10:17 +01:00
decentral1se b978f04910 fix: use "sudo tee" to avoid permissions error
See #474
2025-01-08 12:09:51 +01:00
decentral1se 3ac29d54d9 chore: go update des/vendor
continuous-integration/drone/push Build is passing
2025-01-07 16:59:56 +01:00
decentral1se 877c17fab5 test: re-enable this one
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-01-05 12:47:46 +01:00
decentral1se ac0b9cd052 chore: new RC
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2025-01-05 12:42:42 +01:00
decentral1se 4923984e84 fix: not flaky catalogue generate
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #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)"
continuous-integration/drone/push Build is passing
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)
continuous-integration/drone/push Build is failing
2025-01-04 11:47:33 +01:00
decentral1se 936c1b0626 fix: use new syntax
continuous-integration/drone/push Build is failing
2025-01-04 11:20:17 +01:00
decentral1se b576cba227 fix: use abra-bot
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2025-01-04 11:09:14 +01:00
decentral1se d087f3debf chore: go mod tidy
continuous-integration/drone Build is failing
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"
continuous-integration/drone/push Build is passing
See toolshed/organising#636
2025-01-03 08:24:03 +01:00
decentral1se 2dc8034c16 fix: no dot dirs for server selection
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See toolshed/organising#661
2025-01-02 21:20:40 +01:00
decentral1se f07c64f7b8 fix: sort abra app env output
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See coop-cloud/abra#461
2025-01-02 11:12:38 +01:00
decentral1se 5975be6870 fix: unstaged changes handling
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See toolshed/organising#651
2024-12-31 16:37:02 +01:00
decentral1se bfed51a69c fix: no newline on status in logs
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2024-12-30 18:10:01 +01:00
decentral1se a159583874 chore: make format
continuous-integration/drone/push Build is failing
2024-12-30 18:07:58 +01:00
decentral1se e3b0500875 fix: dont output error twice
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is passing
2024-12-29 18:12:53 +00:00
decentral1se 3727c7fa78 test: ensure catalogue
continuous-integration/drone/push Build is passing
2024-12-29 00:44:47 +01:00
decentral1se 9a4414fd13 test: fix failing upgrade test
continuous-integration/drone/push Build is passing
2024-12-29 00:14:16 +01:00
decentral1se 9f189680f3 fix: less newline
continuous-integration/drone/push Build is passing
2024-12-28 23:47:50 +01:00
decentral1se 356e527f1f refactor!: upgrade/rollback vertical render / ui fixes
continuous-integration/drone/push Build is passing
See toolshed/organising#658
2024-12-28 23:35:47 +01:00
decentral1se 7ec61c6d03 fix: sort versions upgrade/rollback/list
continuous-integration/drone/push Build is passing
See toolshed/organising#649
2024-12-28 23:10:22 +01:00
decentral1se fab93a559a fix: more robust <app> autocomplete + error handling
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
See toolshed/organising#633
2024-12-28 19:42:01 +00:00
decentral1se 866c5c4536 test: even moar integration suite patches
continuous-integration/drone/push Build is passing
2024-12-28 17:16:53 +01:00
decentral1se dc4c6784cb test: integration test patches
continuous-integration/drone/push Build is passing
2024-12-28 16:39:58 +01:00
decentral1se 97959ef5da refactor!: vertical render & UI/UX fixes
continuous-integration/drone/push Build is passing
See coop-cloud/abra#454
2024-12-28 15:00:31 +00:00
p4u1 b6573720ec fix: Adds chaos flag to app/cp command
continuous-integration/drone/push Build is passing
2024-12-28 13:56:43 +01:00
decentral1se 4e8995cc0e fix: moar integration test patches
continuous-integration/drone/push Build is passing
See toolshed/organising#650
2024-12-27 21:55:01 +01:00
decentral1se efb3fd8759 test: moar fixes
continuous-integration/drone/push Build is passing
See toolshed/organising#650
2024-12-27 21:16:15 +01:00
decentral1se 008582c3d9 test: fixes for test suite post-cobra migrate
continuous-integration/drone/push Build is passing
See toolshed/organising#650
2024-12-27 20:44:07 +01:00
decentral1se 8fa20e2c7f feat: new backup/restore
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-12-27 19:27:56 +01:00
decentral1se aa1dc795ef fix: disable default complete func
continuous-integration/drone/push Build is passing
2024-12-27 13:55:45 +01:00
decentral1se 18df498295 chore: deps and vendor
continuous-integration/drone/push Build is passing
2024-12-27 13:47:45 +01:00
decentral1se 671e1ca276 refactor!: cobra migrate
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-12-27 13:32:29 +01:00
decentral1se 0df2b15c33 fix: reinstate no-input as a global flag
continuous-integration/drone/push Build is passing
2024-12-23 11:27:11 +01:00
decentral1se 3f29084664 chore: refactor / docstrings
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2024-12-15 09:53:28 -08:00
Ammar Hussein 730fef09a3 add test for SwitchToMain
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2024-12-14 18:41:34 -08:00
Ammar Hussein 8d076a308a bubble up errors on branch switch
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2024-12-14 18:26:22 -08:00
Ammar Hussein 9510c04aeb new recipe default branch main instead of master
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2024-12-12 19:08:18 -08:00
decentral1se d9e60afd71 chore: upgrade go version
continuous-integration/drone/push Build is passing
2024-12-02 01:47:45 +01:00
decentral1se 31fa9b1a7a chore: make deps, go mod vendor
continuous-integration/drone/push Build is failing
2024-12-02 01:45:06 +01:00
Ammar Hussein f664599836 [fix] chaos mode always fails deploy
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-30 20:10:04 -08:00
Ammar Hussein bba1640913 Merge branch 'ammaratef45-removeDomainCheck'
continuous-integration/drone/push Build is passing
2024-11-27 11:48:12 -08:00
Ammar Hussein 7b54c2b5b9 remove whitespace
continuous-integration/drone/pr Build is failing
2024-11-27 11:38:49 -08:00
Ammar Hussein 8ee1947fe9 remove -D on server add
continuous-integration/drone/pr Build is failing
2024-11-25 17:23:00 -08:00
decentral1se b313b0a145 fix: use old auto-completion for 0.9.x compat
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See toolshed/organising#644

Partial revert of 1f8662cd95
2024-10-27 08:54:43 +01:00
decentral1se 1f9b863be0 fix: appease formatter, ignore vendor
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-21 16:46:39 +02:00
decentral1se 3b3ce85ef9 fix: rebase coop-cloud/organising#533
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
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
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2024-07-17 14:05:46 +02:00
decentral1se e5a6dea10c fix: catch ctrl-c again; less cryptic logging
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
See coop-cloud/organising#627
2024-07-16 23:19:36 +02:00
decentral1se e42a1bca29 fix: add chaos/deploy versiosn back to ps output
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
Otherwise, we can't select previous versions.
2024-07-16 17:35:15 +02:00
decentral1se de006782b6 refactor: tablewriter -> lipgloss
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
See coop-cloud/abra#432
2024-07-10 16:03:28 +02:00
decentral1se ff90b43929 fix: use struct data for HEAD retrieval
continuous-integration/drone/push Build is passing
See ce7dda1eae
2024-07-10 15:51:11 +02:00
p4u1 c5724d56f8 fix(config): Removes config file name from abra dir
continuous-integration/drone/push Build is passing
2024-07-10 13:42:24 +00:00
decentral1se ce7dda1eae fix: use recipe struct data
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Follow up for coop-cloud/abra#432
2024-07-10 15:40:45 +02:00
decentral1se d38f3ab7f5 test: speed up test
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Follow up for coop-cloud/abra#413
2024-07-10 13:06:46 +02:00
p4u1 f6b7510da6 feat: introduce remote recipes
continuous-integration/drone/push Build is passing
Reviewed-on: coop-cloud/abra#432
2024-07-10 10:25:06 +00:00
p4u1 7596982282 feat: update new version in env file
continuous-integration/drone/pr Build is passing
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
continuous-integration/drone/push Build is passing
2024-07-09 17:23:06 +02:00
decentral1se 7f910b4e5b test: recipe test fixups
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-09 09:41:49 +02:00
decentral1se c33ca1c6bc fix!: chaos consistency (deploy/undeploy/rollback/upgrade)
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2024-07-08 14:58:57 +02:00
decentral1se f003430a8d fix: use recipe name, not app name
continuous-integration/drone/push Build is passing
2024-07-08 14:54:15 +02:00
decentral1se 5426464092 refactor!: drop version, show versions in ps
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
Reviewed-on: coop-cloud/abra#430
2024-07-08 12:18:58 +00:00
p4u1 41c16db670 test: fix test failure
continuous-integration/drone/pr Build is passing
2024-07-08 14:10:17 +02:00
p4u1 87ecc05962 refactor(recipe): remove direct usage of config.RECIPE_DIR
continuous-integration/drone/pr Build is failing
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
continuous-integration/drone/push Build is passing
2024-07-08 01:52:17 +02:00
decentral1se 735f521bc0 refactor(errors)!: remove WIP/broken command
continuous-integration/drone/push Build is passing
2024-07-08 01:33:06 +02:00
decentral1se 96a25425a4 refactor(ps)!: remove -w, "watch ..." does it better
continuous-integration/drone/push Build is passing
2024-07-08 01:10:58 +02:00
decentral1se 1a8dca9804 fix(deploy): only output when actually waiting
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-07-07 12:21:51 +02:00
decentral1se f871f9beee test: reduce duplication
continuous-integration/drone/push Build is passing
2024-07-07 12:13:07 +02:00
decentral1se 0f8f0f908f test: ensure catalogue
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is failing
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)
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-02 17:18:05 +02:00
decentral1se 9554ad40c8 refactor: use adapted upstream detach=false logic [ci skip]
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
Also, don't build image if tests fail.
2024-06-28 06:12:32 +02:00
decentral1se 34de38928a test: include catalogue
continuous-integration/drone/push Build is failing
2024-06-26 23:46:35 +02:00
decentral1se f58522d822 fix: dont always download the catalogue
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
See coop-cloud/organising#592
2024-06-25 16:48:41 +02:00
decentral1se 712ebfb701 test: update and fix cleanup for "server add"
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2024-06-22 16:49:59 +00:00
decentral1se 40517171f7 test: separate test for git name/email
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2024-06-22 16:38:32 +00:00
p4u1 9ec99c7712 test: return/echo from git helper functions
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-22 17:04:33 +02:00
decentral1se aa3910f8df refactor!: drop all SSH opts / config handling
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-21 17:10:12 +02:00
decentral1se 91ea2c01a5 fix: fix old app version deploy wrt. compose files
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See coop-cloud/organising#617
2024-06-21 16:14:40 +02:00
decentral1se 316fdd3643 fix: abra app new checks out latest version
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See coop-cloud/organising#618
2024-06-21 15:51:34 +02:00
decentral1se e07ae8cccd chore: make format/check
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-19 19:17:22 +02:00
decentral1se 300a4ead01 fix: stop using deprecated APIs
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
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
continuous-integration/drone/push Build is passing
2024-05-22 10:21:31 +02:00
moritz 8a0274cac0 fix(recipe): output correct formatted json for recipe version
continuous-integration/drone/push Build is passing
2024-05-21 16:59:59 +02:00
moritz e609924af0 feat(upgrade): add --releasenotes: show release notes and skip upgrading
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-14 00:07:58 +02:00
moritz 459abecfa5 only show container that should be deployed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-13 23:26:02 +02:00
moritz 183ad8f576 machine readable ps output
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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)
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2024-04-01 18:51:53 +01:00
decentral1se 2ecace3e90 fix: add missing packages on final layer
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#598
2024-04-01 13:57:51 +02:00
p4u1 d5ac3958a4 feat: add retries to app volume remove
continuous-integration/drone/push Build is passing
2024-03-27 05:38:24 +00:00
3wordchant 72c20e0039 fix: make installer work again
continuous-integration/drone/push Build is passing
2024-03-26 21:07:38 -03:00
decentral1se 575f9905f1 Revert "Revert "feat: backup revolution""
continuous-integration/drone/push Build is passing
This reverts commit 2c515ce70a.
2024-03-12 10:34:40 +01:00
decentral1se e3a0af5840 build: upgrade goreleaser
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#474
2024-03-12 10:11:14 +01:00
decentral1se 9a3a39a185 chore: new 0.9.x series
continuous-integration/drone/push Build was killed
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)
continuous-integration/drone/push Build is passing
In preparation for the new abra release, let's fix all integration tests

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

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

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

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

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

Reviewed-on: coop-cloud/abra#400
Reviewed-by: decentral1se <decentral1se@noreply.git.coopcloud.tech>
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2024-01-19 15:08:41 +00:00
decentral1se c5687dfbd7 feat: backup revolution
continuous-integration/drone/push Build is passing
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)
continuous-integration/drone/push Build is passing
This fixes a regression introduced by #395

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

Closes coop-cloud/organising#518

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

Reviewed-on: coop-cloud/abra#393
Reviewed-by: moritz <moritz.m@local-it.org>
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2023-12-12 14:46:20 +00:00
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
continuous-integration/drone/push Build is passing
2023-12-04 21:52:09 +00:00
p4u1 02b726db02 add comments to better explain how the length modifier gets added to the secret
continuous-integration/drone/push Build is passing
2023-12-04 17:30:26 +00:00
p4u1 2de6934322 feat: abra app cp enhancements
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
Follows coop-cloud/abra#391
2023-12-02 12:59:42 +01:00
p4u1 3957b7c965 proper env modifiers support
continuous-integration/drone/push Build is passing
This implements proper modifier support in the env file using this new fork of the godotenv library. The modifier implementation is quite basic for but can be improved later if needed. See this commit for the actual implementation.

Because we are now using proper modifer parsing, it does not affect the parsing of value, so this is possible again:
```
MY_VAR="#foo"
```
Closes coop-cloud/organising#535
2023-12-01 11:03:52 +00:00
moritz 0d83339d80 fix(ssh): increase connection timeout #482
continuous-integration/drone/push Build is passing
see coop-cloud/organising#482
2023-11-30 16:35:53 +01:00
decentral1se 6e54ec7213 test: skip failing test for now
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is failing
Adapted from coop-cloud/abra#384.

Thanks @cas.
2023-11-27 11:15:59 +01:00
p4u1 b1d3fcbb0b add integration test
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is failing
Cherry-picked from coop-cloud/abra#389

Thanks @p4u1.
2023-11-27 10:41:46 +01:00
decentral1se 92b073d5b6 chore: go mod tidy
continuous-integration/drone/push Build is failing
2023-11-27 10:28:43 +01:00
renovate-bot 9b0dd933b5 chore(deps): update module github.com/schollz/progressbar/v3 to v3.14.1
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-11-10 08:00:52 +00:00
renovate-bot f255fa1555 chore(deps): update module github.com/hashicorp/go-retryablehttp to v0.7.5
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-11-09 08:00:33 +00:00
renovate-bot 74200318ab chore(deps): update module github.com/schollz/progressbar/v3 to v3.14.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-11-07 08:01:11 +00:00
renovate-bot 609656b4e1 chore(deps): update module golang.org/x/sys to v0.14.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-11-06 08:00:33 +00:00
decentral1se 856c9f2f7d chore: go mod tidy
continuous-integration/drone/push Build is failing
2023-11-04 09:37:15 +01:00
renovate-bot bd5cdd3443 chore(deps): update module github.com/docker/docker to v24.0.7
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-10-30 08:00:53 +00:00
renovate-bot 79d274e074 chore(deps): update module github.com/docker/cli to v24.0.7
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-10-27 07:01:16 +00:00
renovate-bot 51e3df17f1 chore(deps): update module github.com/go-git/go-git/v5 to v5.10.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-10-26 07:00:33 +00:00
knoflook ccf0215495 hotfix: parse values starting with # correctly
continuous-integration/drone/push Build is failing
2023-10-23 19:21:45 +02:00
decentral1se 254df7f2be feat: app cmd ls
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See coop-cloud/organising#484
2023-10-17 21:16:31 +02:00
decentral1se 6a673ef101 refactor: filter by topic when building catalogue
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See coop-cloud/organising#377
2023-10-16 18:42:38 +02:00
decentral1se 7f7f7224c6 feat: diff on release flow
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Also, don't commit unstaged files.
2023-10-16 18:31:22 +02:00
decentral1se f96bf9a8ac feat: recipe reset, recipe diff
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See coop-cloud/organising#511
2023-10-15 12:56:52 +02:00
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
See coop-cloud/organising#503.
2023-10-09 20:54:44 +00:00
decentral1se 3e511446aa refactor: use app check emoji here too
continuous-integration/drone/push Build is passing
2023-10-09 22:53:46 +02:00
decentral1se 7566b4262b fix: set go version to 1.21
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-09 22:07:30 +02:00
decentral1se c249c6ae9c fix: fix: trim comments that are not modifers
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See coop-cloud/organising#505
2023-10-09 14:42:05 +02:00
decentral1se be693e9df0 fix: trim comments that are not modifers
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
See coop-cloud/organising#505
2023-10-08 22:42:34 +02:00
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See coop-cloud/organising#446
2023-10-06 10:56:33 +02:00
decentral1se 6fc4573a71 chore: go mod tidy
continuous-integration/drone/push Build is passing
2023-10-06 09:49:03 +02:00
renovate-bot cbe6676881 chore(deps): update module golang.org/x/sys to v0.13.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-10-06 07:00:49 +00:00
decentral1se b4fd39828f test: abra-integration-test-recipe -> abra-test-recipe
continuous-integration/drone/push Build is passing
See coop-cloud/abra-test-recipe#3
2023-10-05 14:22:11 +02:00
decentral1se 14f2d72aba refactor!: lowercase, hyphenate keys
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
This will potentially break scripts, so time to discuss!
2023-10-05 08:36:01 +02:00
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-10-04 14:05:02 +02:00
decentral1se c646263e9e fix: validate COMPOSE_FILE
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
See coop-cloud/organising#468.
See coop-cloud/organising#376.
2023-10-04 13:27:04 +02:00
decentral1se 422c642949 fix: ensure ipv4 is checked, not sometimes ipv6
continuous-integration/drone/push Build is passing
See coop-cloud/organising#490
2023-10-04 09:29:10 +00:00
decentral1se 379915587c fix: don't export from within function
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Also, don't explode on command function which has "export" in the name!

See coop-cloud/organising#498
2023-10-04 11:20:50 +02:00
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
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-10-03 07:00:43 +00:00
decentral1se f18f0b6f82 build: set ABRA_DIR explicitly
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-09-27 09:19:57 +02:00
decentral1se 1e4f1b4ade build: disable publish image for now
continuous-integration/drone/push Build was killed
continuous-integration/drone Build is failing
It's failing for unknown reasons and block releases.

See coop-cloud/recipes-catalogue-json#6
2023-09-25 17:51:30 +02:00
decentral1se 306fe02d1c chore: tag 0.8.x series
continuous-integration/drone/push Build was killed
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is passing
2023-09-22 09:47:36 +02:00
decentral1se 9b6eb613aa test: woops, keep unit test target default
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#451
2023-09-21 11:33:45 +02:00
decentral1se 8788558cf1 fix: only sync version label once
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#492
2023-09-21 10:58:17 +02:00
decentral1se 76035e003e fix: recipe workflow with integration tests
continuous-integration/drone/push Build is failing
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]
continuous-integration/drone/pr Build is failing
2023-09-20 13:51:06 +02:00
decentral1se 7a9224b2b2 chore: go mod tidy
continuous-integration/drone/push Build is passing
2023-09-19 12:38:02 +02:00
renovate-bot e73d1a8359 chore(deps): update module gotest.tools/v3 to v3.5.1
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-09-19 07:02:01 +00:00
decentral1se f8c49c82c8 fix: skip "abra-integration-test-recipe" also
continuous-integration/drone/push Build is passing
2023-09-18 14:02:38 +02:00
decentral1se ab7edd2a62 refactor!: drop "record" & "server new" command
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
These were alpha prototypes and we'll reconsider once other layers of
Abra are more stable.
2023-09-14 16:45:01 +02:00
decentral1se b1888dcf0f chore: go mod tidy
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-09-13 07:01:51 +00:00
decentral1se 83bf148304 chore: go mod tidy
continuous-integration/drone/push Build is passing
2023-09-07 14:34:40 +02:00
renovate-bot d80b882b83 chore(deps): update module github.com/docker/docker to v24.0.6
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-09-07 07:02:43 +00:00
renovate-bot c345c6f5f1 chore(deps): update module github.com/docker/cli to v24.0.6
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-09-06 07:01:56 +00:00
decentral1se f8c4fd72a3 chore: go mod tidy
continuous-integration/drone/push Build is passing
2023-09-05 13:56:34 +02:00
decentral1se 10f612f998 test: more integration tests
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-09-05 13:03:38 +02:00
decentral1se 58e78e4d7c fix: overridable ABRA_DIR
continuous-integration/drone/push Build is failing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-09-04 07:02:01 +00:00
decentral1se d9b4f4ef3b chore: go mod tidy
continuous-integration/drone/push Build is passing
2023-08-26 09:58:46 +02:00
renovate-bot c365dcf96d chore(deps): update module github.com/hetznercloud/hcloud-go to v1.50.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-08-25 07:02:00 +00:00
renovate-bot 0c6a7cc0b8 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.49.1
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-08-18 07:01:42 +00:00
decentral1se 6640cfab64 chore: go mod tidy
continuous-integration/drone/push Build is passing
2023-08-13 17:42:24 +02:00
renovate-bot 71addcd1b2 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.49.0
continuous-integration/drone/push Build is failing
2023-08-13 15:41:44 +00:00
decentral1se 60c0e55e3d fix: don't specify refs when pulling tags
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-08-09 07:02:07 +00:00
renovate-bot 2d826e47d0 chore(deps): update module golang.org/x/sys to v0.11.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-08-07 07:01:55 +00:00
rix 2db172ea5a Further changes to messages.
continuous-integration/drone/push Build is passing
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'
continuous-integration/drone/push Build was killed
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
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-08-02 00:48:27 +02:00
decentral1se 8bb98ed0ed fix: deploy fresh recipe without versions
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
See coop-cloud/organising#476
2023-08-01 21:47:34 +02:00
decentral1se 23f5745cb8 fix: skip recipe clone / up to date sync for some commands
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Continues work of 3dc5662821.
2023-08-01 21:19:20 +02:00
decentral1se 2cd453ae8d build: attempt to ignore goreleaser upgrades
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2023-07-31 07:02:04 +00:00
decentral1se 55c7aca3c0 chore: publish 0.8.0-rc2-beta
continuous-integration/drone/push Build was killed
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
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-07-27 07:06:22 +00:00
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
continuous-integration/drone/push Build was killed
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
continuous-integration/drone/push Build was killed
2023-07-26 08:52:33 +02:00
decentral1se 3dc5662821 fix: improved offline support
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is failing
This change was about trying to optimise for offline scenarios but
caused a lot of issues for the online case. It needs to be thought
through again.

See coop-cloud/organising#471.

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

  coop-cloud/organising#427

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

Related:

  coop-cloud/organising#444

Also fix `upgrade` to actually show the latest version
2023-07-25 15:08:32 +02:00
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
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-07-25 07:05:53 +00:00
renovate-bot d91731518b chore(deps): update module github.com/docker/cli to v24.0.5
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-07-25 07:05:47 +00:00
renovate-bot 2bfee5058d chore(deps): update module github.com/go-git/go-git/v5 to v5.8.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-07-24 07:03:29 +00:00
rix a7ce71d6cf Fix formatting.
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2023-07-14 19:47:09 +02:00
renovate-bot a86ba4e97b chore(deps): update module github.com/hetznercloud/hcloud-go to v2
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-07-14 07:03:04 +00:00
renovate-bot b5b3395138 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.48.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-07-13 07:03:02 +00:00
decentral1se 502b78ef5c chore: go mod tidy
continuous-integration/drone/push Build is passing
2023-07-05 09:50:38 +02:00
renovate-bot 3e2b4dae6a chore(deps): update module golang.org/x/sys to v0.10.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-07-05 07:02:41 +00:00
renovate-bot 573fe403b3 chore(deps): update module gotest.tools/v3 to v3.5.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-06-30 07:02:40 +00:00
decentral1se 76862e9d66 chore: go mod tidy
continuous-integration/drone/push Build is passing
2023-06-22 16:44:53 +02:00
renovate-bot e8e337a608 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.47.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-06-22 07:02:07 +00:00
renovate-bot 500389c5f5 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.46.1
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-06-19 07:02:59 +00:00
decentral1se dea665652c chore: go mod tidy
continuous-integration/drone/push Build is passing
2023-06-16 18:10:02 +02:00
renovate-bot e8cf84b523 chore(deps): update module golang.org/x/sys to v0.9.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-06-16 07:03:47 +00:00
renovate-bot fab25a6124 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.46.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-06-15 07:03:10 +00:00
renovate-bot e71377539c chore(deps): update module github.com/alecaivazis/survey/v2 to v2.3.7
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-06-14 07:02:51 +00:00
decentral1se 497ecf476a docs: wording [ci skip] 2023-06-12 00:09:52 +02:00
decentral1se ff1c043ec5 chore: go mod tidy
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-06-07 07:02:31 +00:00
renovate-bot f5835fe404 chore(deps): update coopcloud.tech/libcapsul digest to 878af47
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-06-07 07:02:13 +00:00
renovate-bot 07bbe9394f chore(deps): update module github.com/sirupsen/logrus to v1.9.3
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-06-05 07:02:38 +00:00
knoflook 6974681af5 fix: improve error message
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-24 07:02:28 +00:00
renovate-bot 4ce377cffe chore(deps): update module github.com/sirupsen/logrus to v1.9.2
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-19 07:03:02 +00:00
renovate-bot c7dd029689 chore(deps): update module github.com/docker/cli to v20.10.25
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-18 07:02:30 +00:00
renovate-bot 51319d2ae2 chore(deps): update module github.com/docker/docker to v20.10.25
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-16 07:03:23 +00:00
renovate-bot d1c2343a54 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.45.1
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-15 07:03:03 +00:00
renovate-bot 135ffde0e5 chore(deps): update module github.com/docker/distribution to v2.8.2
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-12 07:03:02 +00:00
decentral1se 6e4dd51b27 chore: go mod tidy
continuous-integration/drone/push Build is passing
2023-05-08 11:42:50 +02:00
renovate-bot 81b652718b chore(deps): update module github.com/hetznercloud/hcloud-go to v1.44.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-08 07:02:38 +00:00
decentral1se 442f46e17f chore: go mod tidy
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-05 07:03:15 +00:00
renovate-bot 8a4baa66ee chore(deps): update module github.com/klauspost/pgzip to v1.2.6
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-05 07:03:01 +00:00
renovate-bot 16ecbd0291 chore(deps): update module github.com/moby/term to v0.5.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-05-03 07:02:46 +00:00
renovate-bot f65b262c11 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.43.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-04-28 07:03:28 +00:00
cas c5d9d88359 Add some minor tweaks to machine readable pathway in recipe upgrade
continuous-integration/drone/push Build is failing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-04-26 07:02:52 +00:00
moritz 049da94629 fix(version): semver version ordering (!293)
continuous-integration/drone/push Build is passing
Solves coop-cloud/organising#427

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

Co-authored-by: Moritz <moritz.m@local-it.org>
Reviewed-on: coop-cloud/abra#293
2023-04-26 06:38:15 +00:00
moritz b2739dcdf2 fix(deploy) post deploy cmds
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
show only the chaos version if the app is a chaos deploy
2023-04-14 18:01:08 +02:00
moritz 31ec322c55 feat(deploy): set timeout via label (!290)
continuous-integration/drone/push Build is passing
Solves coop-cloud/organising#437

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

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

Co-authored-by: Moritz <moritz.m@local-it.org>
Reviewed-on: coop-cloud/abra#292
2023-04-14 14:12:31 +00:00
renovate-bot 5e508538f3 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.42.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-04-13 07:02:01 +00:00
moritz 9e05000476 fix(kadabra): always pull new recipe version
continuous-integration/drone/push Build is passing
2023-04-06 17:22:33 +02:00
decentral1se f088a0d327 chore: go mod tidy
continuous-integration/drone/push Build is passing
2023-04-06 10:00:14 +02:00
renovate-bot 3832403c97 chore(deps): update module github.com/docker/docker to v20.10.24
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-04-06 07:01:59 +00:00
renovate-bot 47058c897c chore(deps): update module github.com/docker/cli to v20.10.24
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-04-05 07:02:26 +00:00
renovate-bot 5d4c7f8ef0 chore(deps): update module golang.org/x/sys to v0.7.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-04-04 16:33:38 +00:00
moritz ee4315adb3 fix(rm): remove volumes correctly during app removal
continuous-integration/drone/push Build is passing
2023-03-30 13:40:44 +02:00
moritz 9ade250f01 feat(cmd): add --tty flag to run commands from a script
continuous-integration/drone/push Build is failing
2023-03-29 14:25:08 +02:00
decentral1se 81b032be85 chore: go mod tidy
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-03-17 08:03:41 +00:00
renovate-bot b1595c0ec9 chore(deps): update module github.com/schollz/progressbar/v3 to v3.13.1
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-03-15 08:02:29 +00:00
decentral1se 6c99a2980b chore: go mod tidy
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
Reviewed-on: coop-cloud/abra#280
2023-03-07 12:31:38 +00:00
moritz edff63b446 Revert "review: change label autoupdate -> auto-update"
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
This reverts commit 74baa76f5ee5e5dd7b71b1f14be97cc40dfc611b.
2023-03-07 13:24:46 +01:00
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-03-07 08:02:02 +00:00
decentral1se f29278f80a chore: go mod tidy
continuous-integration/drone/push Build is passing
2023-03-06 15:04:15 +01:00
renovate-bot a9a294cbb7 chore(deps): update module golang.org/x/sys to v0.6.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-03-06 08:02:17 +00:00
decentral1se 73004789a4 chore: go mod tidy
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-03-01 08:02:11 +00:00
moritz e4a89bcc4f fix(kadabra): only warn if a deployed app has no published release
continuous-integration/drone/push Build is passing
2023-02-28 15:34:27 +01:00
decentral1se eb07617e73 chore: publish new release 0.7.0-beta
continuous-integration/drone/push Build was killed
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
continuous-integration/drone/push Build is failing
2023-02-19 10:51:10 +01:00
decentral1se 3058178d84 fix: if all servers good, don't show empty table
continuous-integration/drone/push Build is passing
2023-02-19 10:34:47 +01:00
decentral1se d62c4e3400 refactor: improved logging on pruning
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2023-02-17 10:10:48 +01:00
codegod100 b14ec0cda4 review cleanups
continuous-integration/drone/push Build is passing
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"
continuous-integration/drone/push Build is passing
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'
continuous-integration/drone/push Build is passing
2023-02-14 13:59:35 +00:00
decentral1se 96a8cb7aff chore: go mod tidy
continuous-integration/drone/push Build is passing
2023-02-14 14:29:25 +01:00
renovate-bot 9b51d22c20 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.40.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-02-14 08:02:28 +00:00
philippr d789830ce4 feat: adds --since flag for abra app logs
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-02-14 00:19:38 +01:00
decentral1se e4b4084dfd fix: stream logs without hitting git.coopcloud.tech
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Medium-sized options refactor in here too!

See coop-cloud/organising#292.
2023-02-13 16:46:43 +01:00
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
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#409
2023-02-12 16:40:48 +01:00
decentral1se e94f947d20 fix: don't create clients twice per server
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2023-02-10 12:47:20 +01:00
decentral1se 5e3b039f93 fix: kadabra is now called kadabra not abra
continuous-integration/drone/push Build is passing
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)
continuous-integration/drone/push Build is passing
coop-cloud/organising#236

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

Co-authored-by: Moritz <moritz.m@local-it.org>
Reviewed-on: coop-cloud/abra#268
2023-02-08 18:53:04 +00:00
decentral1se f28af5e42f fix: use correctly formatted comments
continuous-integration/drone/push Build is passing
2023-02-08 11:28:38 +01:00
decentral1se fdf4854b0c fix: unbork comments
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build was killed
2023-02-08 10:56:39 +01:00
decentral1se ebb748b7e7 chore: publish next tag 0.7.0-rc1-beta
continuous-integration/drone/push Build was killed
2023-02-08 10:28:54 +01:00
decentral1se 2b3dbee24c chore: go mod tidy
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is failing
2023-02-07 21:16:18 +00:00
renovate-bot 7b2880d425 chore(deps): update module github.com/docker/docker to v23
continuous-integration/drone/push Build is failing
2023-02-07 21:16:06 +00:00
renovate-bot 928d6f5d7f chore(deps): update module golang.org/x/sys to v0.5.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-02-07 08:02:03 +00:00
decentral1se 29fa607190 fix: restrict pulling to specific branch
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is failing
2023-02-02 21:00:31 +01:00
decentral1se ef4df35995 fix: don't check twice (called in EnsureUpToDate)
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is failing
See coop-cloud/abra#266
2023-02-02 20:26:19 +01:00
decentral1se bf79552204 fix: improve permission denied message
continuous-integration/drone/push Build is failing
2023-02-02 20:07:45 +01:00
decentral1se 0a7fa54759 fix: cant pass client here
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#396
2023-02-02 20:06:49 +01:00
decentral1se 7c1a97be72 refactor!: consolidate SSH handling
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#389.
Closes coop-cloud/organising#341.
Closes coop-cloud/organising#326.
Closes coop-cloud/organising#380.
Closes coop-cloud/organising#360.
2023-02-02 08:37:14 +00:00
renovate-bot f20fbbc913 chore(deps): update golang docker tag to v1.20
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-02-02 08:02:02 +00:00
moritz 76717531bd resolve PR: include the service info in the log message
continuous-integration/drone/push Build is failing
2023-01-31 16:15:11 +01:00
moritz 6774893412 add env ENABLE_AUTO_UPDATE as label to enable/disable the auto update process
continuous-integration/drone/pr Build was killed
2023-01-31 16:12:02 +01:00
moritz ebb86391af add a label to signal that a deploy is a chaos deploy (!265)
continuous-integration/drone/push Build is failing
Resolves coop-cloud/organising#390 by adding the following label `coop-cloud.${STACK_NAME}.chaos=true` (according to the version label).
This is required for the auto updater coop-cloud/organising#236 (comment)

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

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

Co-authored-by: Moritz <moritz.m@local-it.org>
Reviewed-on: coop-cloud/abra#263
2023-01-31 14:13:43 +00:00
moritz 32851d4d99 fix: always fetch all repository tags
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-01-31 11:52:15 +01:00
decentral1se c47aa49373 fix: improved missing context message
continuous-integration/drone/push Build is failing
2023-01-24 10:48:53 +01:00
decentral1se cdee6b00c4 docs: better auto-completion help
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#350
2023-01-23 18:42:23 +01:00
decentral1se b089109c94 fix: more robust docker context problem handling
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
See coop-cloud/organising#325
See coop-cloud/organising#340
2023-01-23 14:56:34 +01:00
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'
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is failing
Also reformats the linting output to be more readable.

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

Closes coop-cloud/organising#365
2023-01-23 09:26:53 +01:00
decentral1se 49865c6a97 feat: app services command
continuous-integration/drone/push Build was killed
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
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#367
2023-01-22 17:52:36 +01:00
decentral1se 834d41ef50 docs: wording [ci skip] 2023-01-22 10:07:58 +01:00
decentral1se 6fe5aed408 fix!: remove digest handling
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#379
2023-01-22 08:54:13 +01:00
decentral1se 03041b88d0 chore: gofmt
continuous-integration/drone/push Build is failing
2023-01-21 23:26:23 +01:00
decentral1se 9338afb492 chore: go mod tidy
continuous-integration/drone/push Build is failing
2023-01-20 10:16:14 +01:00
renovate-bot 313ae0dbe2 chore(deps): update module github.com/docker/cli to v20.10.23
continuous-integration/drone/push Build is failing
2023-01-20 09:12:52 +00:00
renovate-bot 0dc7ec8570 chore(deps): update module github.com/docker/docker to v20.10.23
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-01-20 08:02:52 +00:00
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
continuous-integration/drone/push Build is failing
2023-01-15 19:48:07 -08:00
3wordchant 4f055096e9 chore: fix Drone build, ignore auto-recipes-catalogue-json
continuous-integration/drone/push Build is failing
2023-01-15 18:16:53 -08:00
3wordchant 6c93f980dc chore: tweak docker build
continuous-integration/drone/push Build was killed
2023-01-15 18:08:22 -08:00
3wordchant 57f52bbf33 chore: disable go cache for now, parallelise build
continuous-integration/drone/push Build is failing
2023-01-15 17:16:32 -08:00
3wordchant 9f5620d881 chore: attempt to fix drone build
continuous-integration/drone/push Build was killed
2023-01-15 17:11:50 -08:00
3wordchant 44c4555aae chore: attempt to enable go caching for docker image build
continuous-integration/drone/push Build was killed
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
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-01-06 08:02:14 +00:00
decentral1se 9aa4a98b0b chore: go mod tidy
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-01-05 08:02:28 +00:00
renovate-bot 9082761f86 chore(deps): update module golang.org/x/crypto to v0.5.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-01-05 08:02:09 +00:00
renovate-bot a3bd6e14d0 chore(deps): update module github.com/schollz/progressbar/v3 to v3.13.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-01-05 08:01:57 +00:00
decentral1se 49dd702d98 chore: go mod tidy
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-01-04 08:01:58 +00:00
renovate-bot b50718050b chore(deps): update module github.com/hetznercloud/hcloud-go to v1.39.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-12-30 08:01:53 +00:00
3wordchant 9e39e1dc88 docs: fix typo in error message
continuous-integration/drone/push Build is passing
2022-12-22 19:27:42 -08:00
decentral1se 1a3a53cfc2 chore: go mod tidy
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-12-19 08:01:53 +00:00
3wordchant b204b289d1 fix: disable progress bar with machine-readable output
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-12-16 10:20:51 -08:00
decentral1se 3a0d9f7ed7 chore: 0.6.0-beta release
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
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)
continuous-integration/drone/push Build is passing
Co-authored-by: Moritz <moritz.m@local-it.org>
Reviewed-on: coop-cloud/abra#235
2022-12-13 14:42:45 +00:00
decentral1se 4e78b060e0 chore: go mod tidy
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-12-12 08:01:31 +00:00
renovate-bot 5e8b9d9bf7 chore(deps): update module golang.org/x/crypto to v0.4.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-12-08 08:01:33 +00:00
renovate-bot 11dd665794 chore(deps): update module github.com/schollz/progressbar/v3 to v3.12.2
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-12-07 08:01:51 +00:00
renovate-bot ba163e9bf3 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.38.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-12-06 08:01:41 +00:00
cas 09048ee223 Done did make format
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-12-05 08:01:48 +00:00
renovate-bot 1751ba534e chore(deps): update module github.com/go-git/go-git/v5 to v5.5.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-12-05 08:01:25 +00:00
3wordchant a21d431541 fix: don't panic() 😅
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2022-11-17 13:23:19 +01:00
renovate-bot 4c2109e8ce chore(deps): update module golang.org/x/crypto to v0.3.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-11-17 08:01:01 +00:00
moritz 7f745ff19f feat(cmd)!: run abra.sh commands with /bin/bash if available.
continuous-integration/drone/push Build is passing
BREAKING CHANGE: abra.sh commands that depend on /bin/sh will break

Closes coop-cloud/organising#357.

See coop-cloud/abra#229.
2022-11-15 23:01:57 +01:00
moritz 521d3d1259 feat(autocomplete): add autocompletion for fish shell
continuous-integration/drone/push Build is passing
2022-11-15 22:24:34 +01:00
decentral1se 14187449a5 fix: fork passgen
continuous-integration/drone/push Build is passing
See coop-cloud/organising#358
2022-11-14 15:18:54 +01:00
decentral1se 2037f4cc19 chore: go mod tidy
continuous-integration/drone/push Build is passing
2022-11-11 17:40:42 +01:00
renovate-bot 05d492d30b chore(deps): update module github.com/hetznercloud/hcloud-go to v1.37.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-11-11 08:01:11 +00:00
moritz 9591e91ed6 feat(cmd): make env variables accessible for local abra.sh commands
continuous-integration/drone/push Build is passing
2022-11-10 11:12:35 +00:00
decentral1se f6f587e506 chore: go mod tidy
continuous-integration/drone/push Build is passing
2022-11-10 11:47:16 +01:00
renovate-bot 4f28dbee87 chore(deps): update module golang.org/x/crypto to v0.2.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-11-10 08:01:07 +00:00
renovate-bot ad1cc038e3 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.36.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-11-09 08:01:00 +00:00
renovate-bot 15dbd85d25 chore(deps): update module golang.org/x/sys to v0.2.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-11-08 08:00:59 +00:00
renovate-bot 2a97955586 chore(deps): update module github.com/schollz/progressbar/v3 to v3.12.1
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-11-07 08:00:59 +00:00
decentral1se 9e44d1dfba chore: go mod tidy
continuous-integration/drone/push Build is passing
2022-11-04 14:52:56 +01:00
renovate-bot 87ad8e2761 chore(deps): update module github.com/schollz/progressbar/v3 to v3.12.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-11-03 08:01:03 +00:00
renovate-bot cfe703b15d chore(deps): update module github.com/docker/cli to v20.10.21
continuous-integration/drone/push Build is failing
2022-10-27 08:44:38 +00:00
renovate-bot 96503fa9e9 chore(deps): update module github.com/docker/docker to v20.10.21
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-10-26 07:01:18 +00:00
decentral1se 07d49d8566 chore go mod tidy
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-10-20 07:00:57 +00:00
renovate-bot dc0b6c2c8c chore(deps): update module github.com/docker/docker to v20.10.20
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-10-19 07:00:57 +00:00
renovate-bot 54f242baf7 chore(deps): update module github.com/docker/cli to v20.10.20
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-10-19 07:00:51 +00:00
renovate-bot 07620c7d89 chore(deps): update module golang.org/x/sys to v0.1.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-10-18 07:01:15 +00:00
renovate-bot 1cae4cce4e chore(deps): update module gotest.tools/v3 to v3.4.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-10-10 07:01:50 +00:00
decentral1se 9347ade82c chore: go mod tidy
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-09-20 07:01:14 +00:00
renovate-bot 8cee8ae33a chore(deps): update module github.com/schollz/progressbar/v3 to v3.11.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-09-12 07:01:55 +00:00
renovate-bot 15b138e026 chore(deps): update module github.com/docker/docker to v20.10.18
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-09-12 07:01:39 +00:00
renovate-bot 4a8ed36dea chore(deps): update module github.com/docker/cli to v20.10.18
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-09-12 07:01:28 +00:00
renovate-bot 7d0c3cc496 chore(deps): update module github.com/alecaivazis/survey/v2 to v2.3.6
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-09-12 07:01:18 +00:00
renovate-bot 3cf479ffd5 chore(deps): update module github.com/docker/go-units to v0.5.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-09-01 07:01:24 +00:00
renovate-bot d402050a40 chore(deps): update module github.com/gliderlabs/ssh to v0.3.5
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-09-01 07:01:15 +00:00
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
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#334.
2022-08-14 22:20:17 +02:00
decentral1se 42a6818ff4 fix: app cmd parsing, usage & tests
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Note: the integration tests don't work due to ValidateApp still
attempting to validate the host key for the test app which doesn't
exist. This will be fixed in a future commit.
2022-08-14 16:18:58 +02:00
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-08-03 07:01:11 +00:00
decentral1se 50123f3810 chore: go mod tidy
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-08-01 07:01:38 +00:00
renovate-bot c8183aa6d1 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.35.2
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-08-01 07:01:21 +00:00
decentral1se 4711de29ae chore: go mod tidy
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-07-20 07:01:14 +00:00
renovate-bot 1aa6be704a chore(deps): update module github.com/schollz/progressbar/v3 to v3.8.7
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-07-20 07:01:06 +00:00
decentral1se e8e3cb8598 chore: go mod tidy
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-07-05 07:01:24 +00:00
renovate-bot 351bd7d4ba chore(deps): update module gotest.tools/v3 to v3.3.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-06-20 07:01:21 +00:00
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-06-08 07:01:45 +00:00
decentral1se 08d97be43a chore: go mod tidy
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-06-07 07:01:42 +00:00
renovate-bot c153c5da2e chore(deps): update module github.com/docker/cli to v20.10.17
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-06-07 07:01:28 +00:00
decentral1se 0540e42168 alpha -> beta
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-05-13 07:01:43 +00:00
renovate-bot 0837045d44 chore(deps): update module github.com/docker/cli to v20.10.16
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-05-13 07:01:33 +00:00
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-10 12:10:36 +02:00
renovate-bot e37f235fd4 chore(deps): update module github.com/gliderlabs/ssh to v0.3.4
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-05-10 07:01:27 +00:00
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-05-06 07:01:51 +00:00
renovate-bot 89fc875088 chore(deps): update module github.com/docker/cli to v20.10.15
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-05-06 07:01:31 +00:00
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-05-05 07:01:30 +00:00
decentral1se 578e91eeec chore: publish next tag 0.5.0-alpha
continuous-integration/drone/push Build was killed
2022-05-03 17:22:54 +02:00
decentral1se 49f79dbd45 fix!: new catalogue URL
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-05-03 17:08:52 +02:00
decentral1se 574d556bb9 chore: go mod tidy
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-04-29 07:01:30 +00:00
renovate-bot d0c6fa5b45 chore(deps): update module github.com/hetznercloud/hcloud-go to v1.33.2
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-04-27 07:02:33 +00:00
renovate-bot c947354ee3 chore(deps): update module gotest.tools/v3 to v3.2.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-04-25 07:01:44 +00:00
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-04-22 07:01:22 +00:00
decentral1se 4ba15df9b7 chore: 0.4.1-alpha
continuous-integration/drone/push Build was killed
2022-04-21 15:47:39 +02:00
knoflook 5721b357a2 fix: per service logs
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build was killed
2022-04-21 15:40:23 +02:00
decentral1se 6140abbcac fix: sync to latest before commits come in
continuous-integration/drone/push Build is passing
Follows from a4989e3834
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
continuous-integration/drone/push Build is passing
2022-04-20 13:37:51 +02:00
knoflook c214937e4a installer: download on aarch64
continuous-integration/drone/push Build is passing
2022-04-20 13:13:50 +02:00
decentral1se 3a3f41988b chore: publish 0.4.0-alpha
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2022-04-18 10:42:39 +02:00
renovate-bot 1a04439b1f chore(deps): update module github.com/hashicorp/go-retryablehttp to v0.7.1
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-04-14 07:01:24 +00:00
decentral1se 979f417a63 chore: gpl this sucka [ci skip] 2022-04-05 12:18:34 +02:00
decentral1se b27acb2f61 feat: backup/restore [ci skip]
continuous-integration/drone/pr Build is passing
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
continuous-integration/drone/push Build is passing
2022-04-01 10:21:16 +02:00
renovate-bot 061de96b62 chore(deps): update module github.com/kevinburke/ssh_config to v1.2.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-04-01 07:01:23 +00:00
decentral1se 6998298d32 chore: publish next tag 0.4.0-alpha-rc8
continuous-integration/drone/push Build was killed
2022-03-30 16:28:55 +02:00
decentral1se 323f4467c8 fix: filtering requires case-by-case handling
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build was killed
See https://github.com/moby/moby/issues/32985.
2022-03-30 16:25:38 +02:00
decentral1se e8e41850b5 fix: pass args to local function invocations too
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build was killed
2022-03-27 21:33:30 +02:00
decentral1se 860f1d6376 feat: bring back scripts interface
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#305.
2022-03-27 19:30:36 +00:00
decentral1se 338360096c feat: pass domain to new app envs
continuous-integration/drone/push Build is passing
See coop-cloud/organising#304.
2022-03-27 21:06:48 +02:00
decentral1se 7a8c7cd50f ci: drop static check
continuous-integration/drone/push Build is passing
2022-03-27 13:51:40 +02:00
decentral1se bafc8a8e34 chore: go mod tidy
continuous-integration/drone/push Build is failing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-03-24 08:01:35 +00:00
renovate-bot 978297c464 chore(deps): update module github.com/docker/cli to v20.10.14
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-03-24 08:01:27 +00:00
renovate-bot 11da4808fc chore(deps): update module github.com/alecaivazis/survey/v2 to v2.3.4
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-03-24 08:01:21 +00:00
decentral1se 4023e6a066 fix: wait until app created to check for secrets
continuous-integration/drone/push Build is failing
2022-03-18 11:10:15 +01:00
knoflook f432bfdd23 fix: warn when no repo on git
continuous-integration/drone/push Build is failing
2022-03-18 10:13:24 +01:00
renovate-bot 848e17578d chore(deps): update golang docker tag to v1.18
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build was killed
2022-03-16 08:01:41 +00:00
decentral1se 1615130929 fix: skip prompt for no passwords
continuous-integration/drone/push Build is passing
2022-03-15 10:54:05 +01:00
decentral1se 7f315315f0 fix: better prompts & matching for secret removal
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#302.
2022-03-12 16:47:19 +01:00
decentral1se f39e186b66 fix: match Force/NoInput where needed
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is passing
2022-03-11 19:37:50 +01:00
decentral1se e9cfb076c6 fix: strip length modifiers
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-03-11 08:01:57 +00:00
renovate-bot 99a6135f72 chore(deps): update module github.com/docker/cli to v20.10.13
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-03-11 08:01:45 +00:00
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-03-09 08:01:26 +00:00
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
continuous-integration/drone/push Build is passing
2022-03-08 18:04:47 +01:00
decentral1se 3381b8936d fix: better error handling & proper context deletion for server rm
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2022-02-22 10:52:14 +01:00
renovate-bot 91b4e021d0 chore(deps): update module github.com/containers/image to v5
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-02-22 08:01:12 +00:00
decentral1se 598e87dca2 chore: skip new repositories
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Sometimes the Branch(...) call gets confused with state in the
repository. Its more robust to use the default value we get from gitea.

See coop-cloud/organising#299.
2022-02-20 18:07:49 +01:00
decentral1se 09ac74d205 fix: check out default branch from tags
continuous-integration/drone/push Build is passing
Also fix error handling to match function signatures.
2022-02-18 11:17:43 +01:00
decentral1se 5da4afa0ec fix: only ensure latest after cloning
continuous-integration/drone/push Build is passing
2022-02-18 09:55:07 +01:00
decentral1se 9d5e805748 chore: go mod tidy
continuous-integration/drone/push Build is passing
2022-02-16 13:53:09 +01:00
renovate-bot 770ae5ed9b chore(deps): update module github.com/moby/sys/signal to v0.7.0
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-02-16 08:01:33 +00:00
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2022-02-14 17:10:53 +01:00
decentral1se fc5107865b fix: typo
continuous-integration/drone/push Build is passing
2022-02-10 10:59:19 +01:00
decentral1se 53ed1fc545 chore: go mod tidy
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is passing
2022-02-09 08:58:51 +00:00
decentral1se b5f23d3791 feat: show latest published version on sync
continuous-integration/drone/push Build is passing
2022-02-09 08:58:20 +00:00
decentral1se 2b2dcc01b4 fix: dont checkout latest if we dont have a copy
continuous-integration/drone/push Build is passing
2022-02-09 09:54:02 +01:00
decentral1se 0a208d049e chore: go mod tidy + patch upgrades
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-02-04 08:01:17 +00:00
renovate-bot 6fa090352d chore(deps): update module github.com/buger/goterm to v1.0.4
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is running
continuous-integration/drone/push Build is failing
2022-02-04 08:01:11 +00:00
decentral1se 227c02cd09 refactor!: make common flags single char again
continuous-integration/drone/push Build is passing
2022-02-03 14:19:51 +01:00
decentral1se bfeda40e34 fix: catch more ssh failure modes with help
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
Follows b2d17a1829.
2022-01-29 14:06:25 +01:00
decentral1se b2d17a1829 fix: ensure latest checked out for recipe upgrade
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2022-01-27 12:06:32 +01:00
decentral1se 224b8865bf test: newlines for output when Y'ing & N'ing
continuous-integration/drone/pr Build is running
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2022-01-25 11:39:38 +01:00
decentral1se 415df981ff test: long flags, drop docker, use run_tests for all tests
continuous-integration/drone/push Build is passing
2022-01-24 16:49:51 +01:00
knoflook 57728e58e8 test: improve semi-manual testing
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-01-21 16:48:42 +01:00
decentral1se c7062e0494 fix: initial subcmd completion
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Broken by migration to v1 API.
2022-01-20 11:42:04 +01:00
decentral1se cff7534bf9 chore: publish 0.4.0-alpha-rc6
continuous-integration/drone/push Build is passing
2022-01-19 13:33:32 +01:00
decentral1se 13e582349c fix: correctly override with ~/.ssh/config if failing to connect
continuous-integration/drone/push Build is passing
2022-01-19 13:28:57 +01:00
decentral1se b1b9612e01 fix: dont try to parse empty values on status lookup
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
Better flexible flags handling.
2022-01-18 14:38:20 +01:00
decentral1se c6db9ee355 chore: publish 0.4.0-alpha-rc5
continuous-integration/drone/push Build is passing
2022-01-18 11:39:02 +01:00
decentral1se 7733637767 fix: ensure catalogue cloned for catalogue reliant commands
continuous-integration/drone/push Build is passing
2022-01-18 11:19:33 +01:00
decentral1se 88f9796aaf fix: let us know if not pushing changes without dry-run (recipe release)
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-01-17 08:01:18 +00:00
decentral1se 3aec5d1d7e fix: ignore new test repo
continuous-integration/drone/push Build is passing
2022-01-12 16:11:18 +01:00
decentral1se e0fa1b6995 fix: let users know what was deleted
continuous-integration/drone/push Build is passing
2022-01-06 11:47:10 +01:00
decentral1se b69ab0df65 fix: chaos mode fixed for upgrade/rollback
continuous-integration/drone/push Build is passing
Follows 4b7ec6384c.
2022-01-06 10:32:24 +01:00
decentral1se 69a7d37fb7 chore: release 0.4.0-alpha-rc4
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2022-01-05 19:21:41 +01:00
decentral1se b22b63c2ba fix: only output if volumes selected for removal
continuous-integration/drone/push Build is passing
2022-01-05 19:00:09 +01:00
decentral1se d9f3a11265 fix: gracefully handle missing tag for syncing
continuous-integration/drone/push Build is passing
2022-01-05 18:04:46 +01:00
decentral1se d7cf11b876 fix: further fixes for gracefully handling missing tag
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2022-01-04 15:34:10 +01:00
decentral1se 25b44dc54e refactor!: use lowercase option to match others
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
You can't delete regardless of -f if an app is deployed, the runtime
will error out. Best just deal with this for all cases then on our side.
2022-01-04 11:38:07 +01:00
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2022-01-02 20:05:53 +01:00
decentral1se 4dec3c4646 fix: show order as in other tables
continuous-integration/drone/push Build is passing
2022-01-02 16:25:18 +01:00
decentral1se c900cebc30 fix: fix filtering by type for output
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-12-31 16:53:58 +01:00
decentral1se d957adb675 docs: update the release description
continuous-integration/drone/push Build is passing
2021-12-31 16:48:03 +01:00
decentral1se 5254af0fe4 fix: handle no changes edge case for recipe release
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-12-31 13:17:50 +01:00
decentral1se 866cdd1f29 feat: service name in ps output
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-12-31 12:10:11 +01:00
decentral1se 1245827dff fix: handle %s correctly
continuous-integration/drone/push Build is passing
2021-12-31 12:05:40 +01:00
decentral1se 9bdb07463c fix: handle filtered server list with sort
continuous-integration/drone/push Build is passing
2021-12-30 02:06:04 +01:00
decentral1se be26f80f03 fix: maintain sorted output
continuous-integration/drone/push Build is passing
2021-12-30 01:07:21 +01:00
decentral1se 930ff68bb2 refactor: drop unused function
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-12-28 03:42:44 +01:00
decentral1se 047d0e6fbc fix: working url
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-12-28 02:37:35 +01:00
decentral1se b4f48c3c59 feat: show release notes on upgrade
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-12-28 01:51:39 +01:00
decentral1se a700aca23d fix: add autocomplete for app run
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-12-27 19:56:27 +01:00
decentral1se eb1b6be4c5 fix: auto-config ssh urls and push to them
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-12-27 11:00:04 +01:00
decentral1se 37e48f262b fix: better wording
continuous-integration/drone/push Build is passing
2021-12-27 04:17:30 +01:00
decentral1se 06cc5d1cc3 fix: only update when really needed
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-12-26 04:10:50 +01:00
decentral1se cdc08ae95a fix: much hacking, maybe fixed catalogue generation
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-12-25 02:03:09 +01:00
decentral1se 14400d4ed8 fix: sync recipes from remotes
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-12-23 19:34:50 +01:00
decentral1se 014d32112e fix: ensure tags & commits are pushed
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-12-23 01:56:09 +01:00
decentral1se 190c1033e6 fix: handle skipping
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-12-23 00:15:28 +00:00
decentral1se 7a5aa1b005 test: make them work again
continuous-integration/drone/push Build is passing
2021-12-23 01:06:56 +01:00
decentral1se db453f0ab1 feat: auto flag for dns
continuous-integration/drone/push Build is failing
2021-12-22 20:46:50 +01:00
decentral1se a07e71f7df fix: grand ssh, provisioning, perms refactor
continuous-integration/drone/push Build is failing
See coop-cloud/organising#280.
See coop-cloud/organising#273.
2021-12-22 20:08:15 +01:00
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
continuous-integration/drone/push Build is failing
2021-12-22 02:50:16 +01:00
decentral1se 5513754c22 fix: push tags
continuous-integration/drone/push Build is failing
2021-12-22 02:01:48 +01:00
decentral1se 3a27d9d9fb fix: remove unexpanded var
continuous-integration/drone/push Build is failing
2021-12-22 01:50:17 +01:00
decentral1se 04b58230ea fix: release functionality working again
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is failing
2021-12-22 01:18:16 +01:00
decentral1se 74f688350b fix: actually call function
continuous-integration/drone/push Build is failing
2021-12-22 01:03:36 +01:00
decentral1se 737a22aacc refactor: less quotes
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is failing
2021-12-22 00:48:00 +01:00
decentral1se a18d0e290d docs: more context on vol rm
continuous-integration/drone/push Build is failing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-12-21 08:01:41 +00:00
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-12-14 08:01:21 +00:00
renovate-bot d999cedd97 chore(deps): update module github.com/docker/cli to v20.10.12
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-12-14 08:01:10 +00:00
decentral1se 8215bb455b fix: warn if secrets still exist
continuous-integration/drone/push Build is passing
2021-12-13 12:29:26 +01:00
decentral1se 37ab9a9c08 fix: improve ls output
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#252.
2021-12-12 17:51:58 +01:00
decentral1se 48dd9cdeed fix: simplify ps output
continuous-integration/drone/push Build is passing
2021-12-12 02:21:46 +01:00
decentral1se d02e1f247f fix: better version output
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#253.
2021-12-12 02:16:01 +01:00
decentral1se d087a60e09 Revert "fix: dont throw away changes"
continuous-integration/drone/push Build is passing
This reverts commit dd0f328a65.

Part of coop-cloud/organising#282.
2021-12-12 02:04:13 +01:00
decentral1se 48e16c414c fix: use correct error format
continuous-integration/drone/push Build is passing
2021-12-12 01:56:43 +01:00
decentral1se f3e55e5023 fix: support registry login details
continuous-integration/drone/push Build is passing
2021-12-12 01:52:28 +01:00
decentral1se ae6adace50 refactor: autocomplete package
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-12-11 20:11:59 +01:00
decentral1se 665396b679 fix: join path correctly
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-12-06 01:26:04 +01:00
decentral1se 6094dfaf92 docs: help with dns
continuous-integration/drone/push Build is passing
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"
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
This reverts commit aea5cc69c3.
2021-12-03 11:39:56 +01:00
knoflook 6c87d501e6 fix(installer): drop double echo
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-11-30 12:07:40 +01:00
d1admin 930c29f4a2 fix: switch order of command
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#270.
2021-11-26 22:14:29 +01:00
d1admin 962f566228 fix: go on with missing tag
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#263.
2021-11-26 21:24:15 +01:00
d1admin 3901258a96 fix: better message for existing swarm
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-11-24 12:11:35 +01:00
d1admin dd0f328a65 fix: dont throw away changes
continuous-integration/drone/push Build is passing
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'
continuous-integration/drone/push Build is passing
2021-11-22 20:41:34 +02:00
3wordchant d0a30f6b7b refactor: code style / error handling improvements
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#256.
2021-11-22 18:38:59 +01:00
d1admin cb32e88cde fix: support retryable http clients
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#257.
2021-11-22 18:28:18 +01:00
d1admin a18729bf98 fix: ensure changes are check for
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-11-22 16:15:51 +02:00
d1admin fdf4fc6737 fix: ensure validation takes place
continuous-integration/drone/push Build is passing
Part of coop-cloud/organising#243 (comment).
2021-11-21 15:00:04 +01:00
d1admin ef6a9abba9 fix: ensure clean slate for re-deploy
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-11-21 14:11:27 +01:00
d1admin fbdb792795 fix: add app name to ps output + docs
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-11-18 21:18:40 +01:00
d1admin e589709cb0 fix: attempt to include IdentityFile if available
continuous-integration/drone/push Build is passing
This is part of trying to debug:

    coop-cloud/organising#250

And also part of:

    coop-cloud/docs.coopcloud.tech#27

Where I now try to specify the same logic as `ssh -i <my-key-path>` in
the underlying connection logic. This should help with being more
explicit about what key is being used via the SSH config file.
2021-11-18 21:16:10 +01:00
d1admin 56c3e070f5 fix: log what keys are loaded with the ssh-agent
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-11-15 16:48:23 +01:00
d1admin 704e773a16 chore(deps): run go mod tidy
continuous-integration/drone Build is passing
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
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-11-15 08:01:39 +00:00
renovate-bot 7bdc1946a2 chore(deps): update module github.com/gliderlabs/ssh to v0.3.3
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-11-15 08:01:30 +00:00
renovate-bot 2439643895 chore(deps): update module github.com/docker/docker to v20.10.10
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2021-11-15 08:01:22 +00:00
renovate-bot 0876f677d1 chore(deps): update module github.com/docker/cli to v20.10.10
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-11-15 08:01:17 +00:00
renovate-bot 31dafb3ae4 chore(deps): update module github.com/alecaivazis/survey/v2 to v2.3.2
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-11-15 08:01:13 +00:00
d1admin 915083b426 fix: time out on 60 sec + of converge checks
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
See coop-cloud/organising#247.
2021-11-14 22:54:55 +01:00
d1admin 9122c0a9b8 fix: ensure domain/server resolve to same ipv4
continuous-integration/drone/push Build is passing
See coop-cloud/organising#227 (comment).
2021-11-14 22:47:18 +01:00
d1admin 85ff04202f fix: ensure ipv4 is present for app deploys
continuous-integration/drone/push Build is passing
See coop-cloud/organising#227.
2021-11-13 23:04:58 +01:00
d1admin ecba4e01f1 feat: autocomplete for app cp app names
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is failing
2021-11-12 08:25:38 +01:00
knoflook 5add4ccc1b refactor(installer): remove doubled code for RC
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-11-11 17:40:14 +01:00
knoflook 9220a8c09b feat(installer): download rc with --rc
continuous-integration/drone/pr Build is passing
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
continuous-integration/drone/push Build is passing
See coop-cloud/organising#209.
2021-11-10 09:06:55 +01:00
d1admin 215431696e feat: implement app restart
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#239.
2021-11-10 07:52:45 +01:00
d1admin cd361237e7 Revert "Revert "test: remove broken tests for client""
continuous-integration/drone/push Build is passing
This reverts commit 59031595ea.

Argh, reverted this by accident, heres another one!
2021-11-09 18:25:28 +01:00
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
continuous-integration/drone/push Build is passing
See coop-cloud/organising#242.
2021-11-09 17:44:13 +01:00
knoflook 17a5f1529a test: remove broken tests for client
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build was killed
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
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#237.
2021-11-08 15:37:23 +01:00
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
continuous-integration/drone/push Build is passing
2021-11-07 00:03:01 +01:00
d1admin 2b9395be1a feat: make sync use wizard mode
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
Some bugs squashed while testing this extensively.
2021-11-06 22:36:01 +01:00
d1admin f9726b6643 WIP: temporarily avoid SSH host key checking
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#234.
Closes coop-cloud/organising#142.
2021-11-05 12:33:32 +01:00
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
continuous-integration/drone/push Build was killed
This might cause the macosx build not to fail, I hope.

See https://github.com/docker/cli/tree/v20.10.10/cli/connhelper/commandconn
2021-11-03 09:57:35 +01:00
d1admin 0e0e2db755 chore: publish new version
continuous-integration/drone/push Build was killed
2021-11-03 09:44:11 +01:00
d1admin 04e24022f5 feat: auto-deploy traefik prototype
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#212.
2021-11-03 09:41:20 +01:00
d1admin c227972c12 WIP: make "abra app deploy" callable by code
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
Part of coop-cloud/organising#212.
2021-11-03 09:10:13 +01:00
d1admin f041083604 feat: support hetzner cloud server removal
continuous-integration/drone/push Build is passing
Part of coop-cloud/organising#212.
2021-11-03 08:34:36 +01:00
d1admin f57ae1e904 fix: remove debug statements
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-11-03 07:48:30 +01:00
d1admin 1caef09cd2 feat: autocomplete helper command
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#216.
2021-11-03 07:28:18 +01:00
d1admin e4e606efb0 feat: catalogue generate now rate limits
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-11-02 21:52:11 +01:00
d1admin e37b49201f fix: use IdleConnTimeout/ConnectTimeout
continuous-integration/drone/push Build is passing
This is an attempt to set sensible timeouts on abra connections. This
might not be the last word on this but it seems that SSH connections now
bail out correctly and other kinds of commands don't explode (e.g.
logs).

Closes coop-cloud/organising#222.
Closes coop-cloud/organising#218.
2021-11-02 15:49:11 +01:00
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
Follow up to bd92c52eed.
2021-11-02 14:54:53 +01:00
d1admin ab02c5f0dd feat: support better domain defaults
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#221.
2021-11-02 14:44:16 +01:00
d1admin f2b02e39a7 fix: allow config to open broken env files
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#223.
2021-11-02 14:38:53 +01:00
d1admin 31f6bd06a5 fix: use correct formatting function
continuous-integration/drone/push Build is passing
2021-11-02 14:24:40 +01:00
d1admin bd92c52eed fix: document secret names more coherently
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#215.
2021-11-02 14:21:55 +01:00
d1admin 0486091768 fix: handle flags order validatio better
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#214.
2021-11-02 14:08:54 +01:00
d1admin 3b77607f36 fix: better error messages for missing repos
continuous-integration/drone/push Build is failing
2021-11-02 13:36:40 +01:00
d1admin f833ccb864 fix: handle recipe name passing correctly
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#224.
2021-11-02 13:33:46 +01:00
d1admin 7022f42711 fix: docs and fix for new recipes
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#228.
2021-11-02 13:29:58 +01:00
d1admin c76bd25c1d Revert "chore: tweak libdns/gandi go.sum entry >.<"
continuous-integration/drone/push Build is failing
This reverts commit a6b5ac3410.

Mystery checksum ping/pong issue goes on.
2021-11-02 13:23:15 +01:00
3wordchant a6b5ac3410 chore: tweak libdns/gandi go.sum entry >.<
continuous-integration/drone/push Build is failing
2021-11-02 14:17:26 +02:00
knoflook 71225d2099 feat(installer): add hashsum checking
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is failing
2021-10-26 10:52:26 +02:00
d1admin b3496ad286 fix: log correctly on provisioning
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-10-25 18:14:10 +02:00
d1admin 3d46ce6db2 refactor: more seamless SSH connections
continuous-integration/drone/push Build is passing
2021-10-25 11:13:41 +02:00
d1admin 9e0d77d5c6 refactor: better SSH connection details handling
continuous-integration/drone/push Build is passing
2021-10-25 10:42:39 +02:00
d1admin f9e2d24550 docs: clarify when this can be connected to
continuous-integration/drone/push Build is passing
2021-10-25 10:09:55 +02:00
d1admin 8772217f41 fix: working provisioning post chaos testing
continuous-integration/drone/push Build is passing
2021-10-25 10:06:16 +02:00
d1admin a7970132c2 fix: server/record improved output + interactivity
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is failing
2021-10-24 23:15:38 +02:00
d1admin 1396f15c78 chore: new loc count by author
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-10-21 18:35:24 +02:00
d1admin 8b15f2de5b chore: publish new release
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-10-21 16:03:19 +02:00
d1admin cdb76e7276 fix: catch multiple containers correctly
continuous-integration/drone/push Build is passing
2021-10-21 16:01:54 +02:00
d1admin a170e26e27 fix: drop copy/pasta, keep timeouts
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-10-21 15:42:50 +02:00
d1admin 03b1882b81 chore: publish new tag
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2021-10-21 15:17:34 +02:00
d1admin 2fcdaca75f fix: dont duplicate info output
continuous-integration/drone/push Build is passing
2021-10-21 15:13:24 +02:00
d1admin c5f44cf340 feat: show undploy overview
continuous-integration/drone/push Build is passing
2021-10-21 15:10:43 +02:00
d1admin 7a5ad65178 fix: load timeout before other opts
continuous-integration/drone/push Build is passing
2021-10-21 15:06:03 +02:00
d1admin 6d4ee3de0d fix: force flag works for upgrade
continuous-integration/drone/push Build is passing
2021-10-21 11:44:47 +02:00
d1admin 63318fb6ff fix: handle chaos mode correctly for deploy
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#210.
2021-10-21 10:19:30 +02:00
d1admin 07ffa08a07 chore: remove unused files
continuous-integration/drone/push Build is passing
2021-10-20 21:04:09 +02:00
d1admin 0e5e7490b3 docs: some rewording and clarifying
continuous-integration/drone/push Build is passing
2021-10-20 17:52:54 +02:00
d1admin 640032b8fe fix: remove duplicate version command
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#155.
2021-10-20 17:34:01 +02:00
d1admin 7f1d9eeaec fix: check if record already exists
continuous-integration/drone/push Build is passing
2021-10-20 16:56:34 +02:00
d1admin 02d24104e1 feat: domain CRUD complete with Gandi provider
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
makes the code cleaner and easier to grab the app struct for testing
2021-10-20 09:45:38 +01:00
d1admin 8703370785 WIP: domain create
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-10-19 10:19:31 +02:00
d1admin 7f3f41ede4 docs: dns list docs
continuous-integration/drone/push Build is passing
2021-10-18 22:20:11 +02:00
d1admin 597b4b586e WIP: domain listing with Gandi
continuous-integration/drone/push Build is passing
Rethinking the interface already.
2021-10-18 22:16:29 +02:00
d1admin 7ea3df45d4 WIP: dns support via libdns
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#205.
2021-10-18 10:48:43 +02:00
d1admin 9dfbd21c61 fix: parse args correctly for validation
continuous-integration/drone/push Build is passing
2021-10-18 09:43:32 +02:00
d1admin 9526d1fde6 fix: ensure we have version checked out on deploy
continuous-integration/drone/push Build is passing
2021-10-18 09:30:43 +02:00
d1admin 62cc7ef92d feat: upgrade/downgrade support chaos mode
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-10-18 08:27:39 +02:00
d1admin 2f9b11f389 feat: support deploying with chaos mode
continuous-integration/drone/push Build is failing
2021-10-18 08:14:06 +02:00
d1admin 6d42e72f16 fix: allow for client creation on default context
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is failing
2021-10-16 18:57:21 +02:00
knoflook dc040a0b38 chore: change test context names
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-10-15 09:58:45 +02:00
3wordchant e820e0219d docs: how to enable bash autocomplete from source
continuous-integration/drone/push Build is passing
2021-10-14 22:37:32 +02:00
d1admin 75fb9a2774 chore: publish new version
continuous-integration/drone/push Build is passing
2021-10-14 13:31:18 +02:00
d1admin 0d500b636d feat: more info on version changing deployments
continuous-integration/drone/push Build is passing
2021-10-14 13:30:33 +02:00
d1admin 5dd97cace0 docs: expand deploy/upgrade/downgrade docs
continuous-integration/drone/push Build is passing
2021-10-14 12:26:07 +02:00
d1admin ae32b1eed2 fix: standardise checkout options
continuous-integration/drone/push Build is passing
2021-10-14 12:17:58 +02:00
d1admin 113bdf9e86 feat: add stats to app list
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-10-14 11:29:57 +02:00
d1admin b69aed3bcf feat: add rollback command
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#127.
2021-10-14 01:52:55 +02:00
d1admin 875255fd8c feat: add upgrade command
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#201.
2021-10-13 16:55:58 +02:00
d1admin 37022bf0c8 feat: make deploy only deploy
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is running
continuous-integration/drone/pr Build is passing
2021-10-13 16:36:07 +02:00
knoflook ece1130797 build: add automatic os and architecture detection to installer script
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-10-13 15:51:19 +02:00
knoflook c266316f7e build: remove python3 dependency from installer
continuous-integration/drone/pr Build is passing
2021-10-13 15:08:00 +02:00
d1admin d804276cf2 feat: add pre-deploy overview
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-10-12 13:25:23 +02:00
d1admin 4235e06943 chore: new 0.1.8-alpha release
continuous-integration/drone/push Build is passing
2021-10-12 11:19:26 +02:00
d1admin a9af0b3627 fix: let gofmt do its magic
continuous-integration/drone/push Build is passing
2021-10-12 10:34:10 +02:00
3wordchant a0b4886eba WIP: default to compose.yml instead of all of 'em
continuous-integration/drone/push Build is failing
2021-10-12 10:25:37 +02:00
d1admin 84489495dc fix: load STACK_NAME if not present
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#195.
2021-10-12 00:56:52 +02:00
knoflook 6db1fdcfba refactor!: recipe upgrade: use new tagcmp version
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2021-10-11 10:45:00 +02:00
d1admin ff4b978876 fix: only list new versions
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#192.
2021-10-11 01:17:52 +02:00
d1admin b68547b2c2 fix: dont overwrite generated catalogue
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#190.
2021-10-11 01:06:51 +02:00
d1admin 0140f96ca1 fix: make sure to clone recipe
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#193.
2021-10-11 00:34:23 +02:00
3wordchant 1cb45113db fix: default linux binary in installer, add context
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#184
2021-10-09 21:45:28 +02:00
d1admin c764243f3a fix: manage multiple version showing edge cases
continuous-integration/drone/push Build is passing
2021-10-08 10:50:48 +02:00
d1admin dde8afcd43 feat: support version/upgrade listing
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-10-06 08:48:23 +02:00
d1admin e78ced41fb fix: use freifunk DNS resolver
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#180.
2021-10-06 08:47:01 +02:00
d1admin e9113500d8 feat: allow to override STACK_NAME
continuous-integration/drone/push Build is passing
2021-10-05 20:40:16 +02:00
d1admin 7368cabc49 fix: format output correctly
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is running
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
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
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-10-01 19:49:18 +02:00
d1admin 09f49cdc76 chore: fix tests
continuous-integration/drone/push Build was killed
continuous-integration/drone/tag Build is passing
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
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is failing
2021-10-01 12:32:41 +02:00
3wordchant 3688ea9d69 feat: support local server with --local
continuous-integration/drone/push Build is failing
2021-10-01 11:59:17 +02:00
3wordchant 7c4cdc530c fix: don't crash if no abra.sh
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is passing
Reviewed-on: coop-cloud/abra#102
2021-09-29 20:24:55 +00:00
knoflook 96bb145981 feat: check and sanitize user-specified tag
continuous-integration/drone/pr Build is passing
2021-09-29 16:25:39 +02:00
knoflook c4c76f4848 feat: add a flag to commit your changes before creating a tag
continuous-integration/drone/pr Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
Reviewed-on: coop-cloud/abra#101
2021-09-28 19:26:56 +00:00
knoflook 857d12d23c feat: print stack traces for errors when debugging
continuous-integration/drone/pr Build is passing
2021-09-27 12:24:02 +02:00
knoflook 22c4d0d864 style: remove doubled debug message
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-09-24 11:05:49 +02:00
knoflook e700e44363 feat: add main apps version as a semver build metadata when releasing
continuous-integration/drone/pr Build is passing
2021-09-24 10:48:09 +02:00
knoflook 9faefd2592 feat: push the new tag with --push
continuous-integration/drone/pr Build is passing
2021-09-23 18:52:21 +02:00
knoflook cd179175f5 refactor: dont' create the same objects twice
continuous-integration/drone/pr Build is passing
2021-09-23 18:32:58 +02:00
knoflook c0f92ca13d feat: support --major/-x --minor/-y --patch/-z for tag calculation
continuous-integration/drone/pr Build is passing
2021-09-23 18:27:19 +02:00
knoflook 48d28c8dd1 feat: tag recipes with abra
continuous-integration/drone/pr Build is failing
2021-09-22 16:03:56 +02:00
d1admin e840328e44 chore: publish next release
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is passing
2021-09-22 09:04:19 +02:00
d1admin 6f43778691 fix: better UI/UX for app creation
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is failing
Closes coop-cloud/organising#150.
2021-09-22 07:48:17 +02:00
d1admin 95a9013658 fix: use appFiles to determine server list
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
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
continuous-integration/drone/push Build is passing
2021-09-17 07:38:38 +02:00
d1admin e00920643e WIP: implement async recipe cloning
continuous-integration/drone/push Build is failing
See coop-cloud/organising#159.
2021-09-16 16:28:11 +02:00
3wordchant 754fe81e01 feat: add templating during .. app new
continuous-integration/drone/push Build is failing
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
continuous-integration/drone/push Build is passing
2021-09-16 12:01:47 +01:00
roxxers 31edbbd32e fix: git metadata not removed in merge
continuous-integration/drone/push Build is passing
2021-09-16 11:35:18 +01:00
roxxers 0a1c73bf00 refactor: use cli context vs creating new one
continuous-integration/drone/push Build is failing
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
d1admin 357cc0593a chore: bump installer for new version
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-09-16 09:49:48 +02:00
d1admin 8e111dc32f fix: use correct debug function
continuous-integration/drone/push Build is passing
2021-09-16 09:48:28 +02:00
d1admin 20ecdb8061 fix: log which compose files are being loaded
continuous-integration/drone/push Build is failing
See coop-cloud/organising#167.
2021-09-16 09:45:02 +02:00
d1admin f87aad4688 fix: list all servers
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#166.
2021-09-16 09:26:12 +02:00
d1admin 6794236b77 feat: support service completion
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#165.
2021-09-16 09:10:05 +02:00
d1admin 6c9bb89a10 refactor: use our usual initialisation 2021-09-16 09:09:51 +02:00
d1admin 66aeeee768 fix: completion doesn't fail silently now
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#161.
2021-09-16 08:45:38 +02:00
d1admin 6c115926e3 fix: load sample env for new apps
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#170.
2021-09-16 08:40:48 +02:00
d1admin b6fe86f2ad fix: use correct args for debug log inputs
continuous-integration/drone/push Build is passing
2021-09-14 16:14:09 +02:00
d1admin d290a4ec0b WIP: the beginning of catalogue generation
continuous-integration/drone/push Build is failing
See coop-cloud/organising#159.
2021-09-14 16:00:15 +02:00
d1admin f93563588a docs: add template
continuous-integration/drone/push Build is failing
2021-09-11 12:20:27 +02:00
d1admin 59c55c0a2f fix: add complete for app run command
continuous-integration/drone/push Build is failing
2021-09-11 11:51:25 +02:00
d1admin 9fcdc45851 feat: debug logging
Closes coop-cloud/organising#164.
2021-09-11 11:45:26 +02:00
d1admin 27d665c3be refactor: move autocomplete into scripts folder 2021-09-10 23:45:28 +02:00
d1admin bc5fc0b0cb refactor: shorter names for autocomplete files 2021-09-10 23:44:32 +02:00
d1admin 99160967a8 refactor: domainName as arg and doc strings
continuous-integration/drone/push Build is passing
See coop-cloud/organising#163.
2021-09-10 15:04:01 +02:00
d1admin 683ef0c3de fix: make more server new command more robust
continuous-integration/drone/push Build is passing
See coop-cloud/organising#163.
2021-09-10 14:49:25 +02:00
d1admin 3c3d8dc0e7 WIP: add first run at app rollback command
continuous-integration/drone/push Build is passing
See coop-cloud/organising#146.
2021-09-10 11:49:29 +02:00
d1admin 855e9ea26d fix: dont output secrets table if nothing there
continuous-integration/drone/push Build is passing
See coop-cloud/organising#162.
2021-09-10 10:36:46 +02:00
d1admin 50d663ff6e fix: use correct var for storing server var
See coop-cloud/organising#162.
2021-09-10 10:36:39 +02:00
d1admin 39ad6e8aa8 fix: use recipeName instead of recipe.Name
This provides a correctly formatted recipe name for machine reading
(i.e. with `-` and such) instead of the more human readable version
(i.e. with spaces).

Closes coop-cloud/organising#162.
2021-09-10 09:56:58 +02:00
d1admin f39c8cbe21 fix: use our godotenv fork
continuous-integration/drone/push Build is passing
2021-09-09 21:26:10 +02:00
decentral1se e114b2a939 Merge pull request 'feat: auto-complete app and recipe names' (#89) from knoflook/abra:main into main
continuous-integration/drone/push Build is passing
Reviewed-on: coop-cloud/abra#89
2021-09-08 12:16:41 +00:00
knoflook 511619722f feat: autocomplete recipe names for more abra commands
continuous-integration/drone/pr Build is passing
2021-09-08 13:59:55 +02:00
knoflook cf2653fef8 refactor: drop unused function, rename GetAppsNames
continuous-integration/drone/pr Build is passing
2021-09-08 13:43:55 +02:00
d1admin 5ba40ad883 feat: include service tags
continuous-integration/drone/push Build is passing
Closes coop-cloud/abra#92.
2021-09-08 10:15:46 +02:00
d1admin 2e0c16d198 docs: retire TODO.md, use issues [ci skip] 2021-09-07 19:18:13 +02:00
knoflook 4c216fdf40 feat: auto-complete app and recipe names
continuous-integration/drone/pr Build is passing
2021-09-07 16:57:39 +02:00
knoflook 5f50c7960c Update 'TODO.md'
continuous-integration/drone/push Build is passing
2021-09-07 13:34:45 +00:00
d1admin 719e24eb80 chore: mark next point release
continuous-integration/drone/push Build was killed
continuous-integration/drone/tag Build is passing
2021-09-07 15:25:29 +02:00
d1admin c441a1ab52 Merge branch 'abra-upgrade' into main 2021-09-07 15:24:48 +02:00
d1admin b0460bd923 docs: mark abra upgrade as done 2021-09-07 15:23:33 +02:00
d1admin f1659b3bda feat: support abra upgrading 2021-09-07 15:23:10 +02:00
d1admin eb4a2b3339 build: fix arch download on installer script
Only support x86_64 for now as I'm moving fast.
2021-09-07 15:22:42 +02:00
decentral1se 265bfe92fd Merge pull request 'feat: bash and (fi)zsh completion along with docs' (#83) from knoflook/abra:bash-completion into main
continuous-integration/drone/push Build is passing
Reviewed-on: coop-cloud/abra#83
2021-09-07 13:22:28 +00:00
knoflook 1757fabb89 feat: bash and (fi)zsh completion along with docs
continuous-integration/drone/pr Build is passing
2021-09-07 13:18:21 +02:00
d1admin abf0ebf41d docs: process for releasing
continuous-integration/drone/push Build is passing
2021-09-07 13:01:36 +02:00
d1admin 45f1692c99 build: add installer script 2021-09-07 13:01:22 +02:00
decentral1se 48bc03db51 Merge pull request 'build: generate binaries directly' (#81) from binary-builds into main
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: coop-cloud/abra#81
2021-09-07 08:56:23 +00:00
d1admin f0e966afc3 docs: explain further our understanding of versioning
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-09-07 10:55:30 +02:00
d1admin a1d1166308 docs: unwrap text like the rest 2021-09-07 10:53:37 +02:00
d1admin 1438fdf3c2 build: generate binaries directly
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Closes coop-cloud/abra#80.
2021-09-07 10:49:51 +02:00
d1admin ddda02a6fc docs: mark this as ready to rip
continuous-integration/drone/push Build was killed
continuous-integration/drone/tag Build is passing
2021-09-07 10:25:08 +02:00
knoflook 4712131f36 docs: change go-abra to abra in README.md
continuous-integration/drone/push Build was killed
2021-09-07 08:19:42 +00:00
knoflook 50c321aecc fix: change the name of generated binary to abra from go-abra
continuous-integration/drone/push Build is passing
2021-09-07 10:10:26 +02:00
d1admin 5bfd233f2a docs: document our versioning praxis
continuous-integration/drone/push Build is passing
Closes coop-cloud/go-abra#77.
2021-09-07 10:05:42 +02:00
d1admin d19c56d75b fix: drop file for version handling [ci skip]
See coop-cloud/go-abra#77.
2021-09-07 09:39:12 +02:00
d1admin 20fa0da1bb build: pass ldflags in [ci skip] 2021-09-07 09:23:09 +02:00
d1admin 1de4f95267 docs: lower case that [ci skip] 2021-09-07 09:13:13 +02:00
d1admin 874550ff7f fix: use more descriptive name for token [ci skip] 2021-09-07 09:04:06 +02:00
d1admin e38917339d dosc: add gitea token [ci skip] 2021-09-07 09:01:26 +02:00
d1admin dcf1a90c31 fix: tables align output again
continuous-integration/drone/push Build is passing
Closes coop-cloud/go-abra#16.
2021-09-07 08:41:03 +02:00
d1admin a06870f5cb fix: generating secrets works again again
continuous-integration/drone/push Build is passing
Closes coop-cloud/go-abra#68.
2021-09-07 08:28:20 +02:00
d1admin b477bf8ece fix: get app new working again 2021-09-07 08:12:37 +02:00
d1admin 87f0985ebb fix: clone also the main branch
Closes coop-cloud/go-abra#65.
2021-09-07 08:12:17 +02:00
d1admin 2cb0fb8d66 refactor: match app/recipe new instead of create 2021-09-07 07:31:11 +02:00
d1admin 76372bb8cb chore: update to golang 1.17
continuous-integration/drone/push Build is passing
Closes coop-cloud/go-abra#70.
2021-09-07 07:21:52 +02:00
d1admin 160ccf9745 refactor: drop comments, add header [ci skip] 2021-09-07 07:16:48 +02:00
d1admin 96cfd8da2b ci: fix tag triggering to git-fetch 2021-09-07 07:16:08 +02:00
decentral1se ff8d9a554a Merge pull request 'feat: auto-release abra with goreleaser when a tag is pushed' (#73) from knoflook/go-abra:main into main
continuous-integration/drone/push Build is passing
Reviewed-on: coop-cloud/go-abra#73
2021-09-07 05:15:13 +00:00
d1admin cf94c5acd0 chore: run go mod tidy
continuous-integration/drone/push Build is passing
2021-09-06 17:50:32 +02:00
knoflook bb8124030e feat: auto-release abra with goreleaser when a tag is pushed
continuous-integration/drone/pr Build is passing
2021-09-06 17:22:18 +02:00
d1admin 448dadd292 fix: sort versions correctly
continuous-integration/drone/push Build is passing
Closes coop-cloud/go-abra#44.
2021-09-06 16:51:42 +02:00
d1admin 8aaedee39e fix: use new RecipeMeta struct
continuous-integration/drone/push Build is passing
2021-09-06 12:24:23 +02:00
d1admin f4d8b45859 fix: sort tags in descending order
Update tagcmp dep also.
2021-09-06 12:22:45 +02:00
d1admin 7ed37547a5 docs: add FIXME [ci skip] 2021-09-06 01:51:04 +02:00
d1admin 9862cf17a9 refactor: rename to RecipeMeta
continuous-integration/drone/push Build is failing
2021-09-06 01:47:59 +02:00
d1admin d1527741ba refactor: drop erroneous return 2021-09-06 01:44:55 +02:00
d1admin 9d6739a711 refactor: use new recipe struct 2021-09-06 01:43:21 +02:00
d1admin 356c8f8c4e refactor: construct recipe struct proper
continuous-integration/drone/push Build is failing
2021-09-06 01:41:16 +02:00
d1admin 6a1ecd0f85 refactor: consolidate recipe in-place editing functions
continuous-integration/drone/push Build is passing
2021-09-06 01:34:28 +02:00
d1admin b5d8fb1270 refactor: create compose package
continuous-integration/drone/push Build is passing
2021-09-06 01:15:59 +02:00
d1admin e1a10723ce refactor: de-indent and error handle up front
continuous-integration/drone/push Build is passing
2021-09-06 00:45:29 +02:00
d1admin a0625bf133 refactor: centralise recipe validation 2021-09-06 00:45:13 +02:00
d1admin 691a2c7a50 tests: fix App struct
continuous-integration/drone/push Build is passing
2021-09-06 00:34:49 +02:00
d1admin c03d187256 fix: error out correctly and fix doc string
continuous-integration/drone/push Build is failing
2021-09-06 00:26:45 +02:00
d1admin 5e05bcd8b0 docs: <server> is not always required, drop it
continuous-integration/drone/push Build is failing
2021-09-06 00:14:52 +02:00
d1admin d4333c2dc0 refactor: use app getting instead of boilerplate
continuous-integration/drone/push Build is failing
2021-09-05 23:17:35 +02:00
d1admin 48bcc9cb36 refactor: break up recipe cli package
continuous-integration/drone/push Build is passing
2021-09-05 22:33:07 +02:00
d1admin ec40d88134 refactor: centralise app name validation
continuous-integration/drone/push Build is passing
2021-09-05 22:04:48 +02:00
d1admin cc249e8187 fix: check for deployment of app before removing
continuous-integration/drone/push Build is passing
Closes coop-cloud/go-abra#61.

Fix thanks to @knoflook!
2021-09-05 21:54:52 +02:00
d1admin 273db078b0 fix: bail out if app doesn't exist
continuous-integration/drone/push Build is passing
Closes coop-cloud/go-abra#67.
Closes coop-cloud/go-abra#69.

Fix lifted from approach in
coop-cloud/go-abra#69. Thanks for
@knoflook!
2021-09-05 21:46:36 +02:00
d1admin d82f854ebd test: fix test suite to understand pkg/ directory
continuous-integration/drone/push Build is passing
2021-09-05 21:39:12 +02:00
d1admin b7742d5e18 refactor: use pkg directory structure 2021-09-05 21:37:03 +02:00
d1admin f59380a35e feat: add new target for LOC stats [ci skip] 2021-09-05 01:57:50 +02:00
d1admin c99f0fc908 refactor: recipe validation
continuous-integration/drone/push Build is passing
2021-09-05 01:55:10 +02:00
d1admin 317be4cc01 docs: short aliases [ci skip] 2021-09-05 01:34:56 +02:00
d1admin a3a66ef972 docs: short aliases, short descriptions [ci skip] 2021-09-05 01:21:16 +02:00
d1admin 7155a33d31 fix: recipe lint and logrus usage 2021-09-05 01:21:05 +02:00
d1admin d5f49594a9 docs: attempt to simplify app/server/recipe CLI docs [ci skip] 2021-09-05 01:01:31 +02:00
d1admin 5287f097e7 refactor: drop unused flags for now 2021-09-05 00:56:51 +02:00
d1admin 1961cdcfee docs: add autonomic as author 2021-09-05 00:54:36 +02:00
d1admin 0727223009 docs: place <app> in args usage [ci skip] 2021-09-05 00:44:45 +02:00
d1admin 07a43cb314 refactor: NewClientWithContext -> New, and use server only
continuous-integration/drone/push Build is passing
2021-09-05 00:41:31 +02:00
d1admin dac679db48 refactor: punctuation, error handling and package docs 2021-09-05 00:22:47 +02:00
d1admin 254a4d6d43 docs: document main package 2021-09-05 00:19:20 +02:00
d1admin fb75567729 docs: more docs for catalogue package [ci skip] 2021-09-05 00:17:28 +02:00
d1admin cb637ca89e fix: use upper case for doc [ci skip] 2021-09-05 00:15:19 +02:00
d1admin ff21237a21 refactor: clear up app/recipe usage
continuous-integration/drone/push Build is passing
See coop-cloud/go-abra#36.
2021-09-05 00:14:27 +02:00
d1admin 5e4114036b docs: more doc strings for secret package 2021-09-04 23:39:38 +02:00
d1admin 4e92057f61 refactor: make SecretValue internal 2021-09-04 23:35:56 +02:00
d1admin fadbbabe09 docs: package doc string for secret 2021-09-04 23:29:05 +02:00
d1admin ba7b18f703 refactor: pass functions into own file 2021-09-04 23:28:54 +02:00
d1admin 9bf11961d5 docs: add package docstring [ci skip] 2021-09-04 23:21:13 +02:00
d1admin a3e02540f6 refactor: use web module timeout everywhere 2021-09-04 23:18:34 +02:00
d1admin e68c7fc71c fix: respect COMPOSE_FILE when loading compose files
continuous-integration/drone/push Build is passing
Final part of coop-cloud/go-abra#57.
2021-09-04 22:02:49 +02:00
d1admin a8f30426ea refactor: drop dead code and garden formatting
continuous-integration/drone/push Build is passing
2021-09-04 21:23:47 +02:00
d1admin dc616fd3a0 refactor: drop swarm checking code for now
continuous-integration/drone/push Build is passing
Part of coop-cloud/go-abra#57.
2021-09-04 21:19:34 +02:00
d1admin 56796cf768 fix: use import only once 2021-09-04 21:12:53 +02:00
d1admin da049ad69a fix: drop swarmkit/etcd dep
continuous-integration/drone/push Build is passing
Part of coop-cloud/go-abra#57.
2021-09-04 21:08:14 +02:00
d1admin f65090bd2f chore: run go mod tidy [ci skip] 2021-09-04 20:51:38 +02:00
knoflook b1d4f12e7d Merge pull request 'docs: add app remove description' (#60) from knoflook/go-abra:dev into main
continuous-integration/drone/push Build is passing
Reviewed-on: coop-cloud/go-abra#60
2021-09-03 12:26:48 +00:00
knoflook 836420e369 docs: add app remove description
continuous-integration/drone/pr Build is passing
2021-09-03 14:22:40 +02:00
d1admin 5c56e4521d docs: shuffle TODOs from my side [ci skip] 2021-09-03 11:59:35 +02:00
d1admin 612fc5a531 feat: final round of hacks for deploy command
continuous-integration/drone/push Build is passing
2021-09-03 11:46:40 +02:00
d1admin 92c8e9aab9 fix: pass in stack name when deploying
continuous-integration/drone/push Build is passing
2021-09-02 19:49:41 +02:00
decentral1se 97188b57d9 Merge pull request 'fix: app rm quitting when there are no secrets/volumes to remove' (#56) from knoflook/go-abra:dev into main
continuous-integration/drone/push Build is passing
Reviewed-on: coop-cloud/go-abra#56
2021-09-02 17:34:44 +00:00
knoflook 7ab44cea57 Update 'TODO.md'
continuous-integration/drone/push Build is passing
2021-09-02 15:57:38 +00:00
knoflook c150856a66 fix: app rm quitting when there are no secrets/volumes to remove
continuous-integration/drone/pr Build is passing
2021-09-02 17:52:42 +02:00
d1admin 063fa66af9 WIP heinous appEnv threading for env var loading
continuous-integration/drone/push Build is passing
2021-09-01 15:01:20 +02:00
d1admin 09873b42ce WIP making a mess for stack deploy
continuous-integration/drone/push Build is passing
2021-09-01 14:03:39 +02:00
d1admin ac86912ead WIP chaos integrate deploy/deploy_composefile
continuous-integration/drone/push Build is passing
The best in copy/pasta technology.

See https://github.com/docker/cli/tree/master/cli/command/stack/swarm
for more.
2021-09-01 13:08:42 +02:00
roxxers 45c6be02b1 refactor: check for errors on secret rm
continuous-integration/drone/push Build is passing
2021-08-31 17:08:25 +01:00
roxxers 7835c1f91d fix: defers after checking for err
continuous-integration/drone/push Build is passing
2021-08-31 16:47:38 +01:00
roxxers 542e9eea5c refactor: rm unneeded sprintf 2021-08-31 16:47:16 +01:00
roxxers 32b2bf245b refactor: simplfiy for...range loops
continuous-integration/drone/push Build is passing
2021-08-31 16:17:08 +01:00
roxxers 3b93f893fd refactor: fix defer and handle error 2021-08-31 16:02:38 +01:00
roxxers 7dce352366 deps: just updating deps to not err out on my end
continuous-integration/drone/push Build is passing
2021-08-31 15:53:50 +01:00
roxxers 8fdac00a38 docs: mark aur integration as done
continuous-integration/drone/push Build is passing
2021-08-31 15:51:05 +01:00
d1admin dc193734df docs: mark as TODO [ci skip] 2021-08-31 12:11:44 +02:00
d1admin 57e641689a feat: add secret generate (untested, moving fast)
continuous-integration/drone/push Build is passing
2021-08-31 11:59:07 +02:00
d1admin d68f2f5686 feat: add app secret insert
continuous-integration/drone/push Build is passing
2021-08-31 10:50:02 +02:00
d1admin f9ae9c9a56 feat: add app secret rm
continuous-integration/drone/push Build is passing
2021-08-31 10:31:54 +02:00
d1admin 15651822f1 feat: implement secret ls
continuous-integration/drone/push Build is passing
Closes coop-cloud/go-abra#54.
2021-08-30 17:02:08 +02:00
d1admin 89b5f12fb1 docs: mark as next TODO [ci skip] 2021-08-30 01:38:15 +02:00
d1admin 66a8630101 feat: implement undeploy command
continuous-integration/drone/push Build is passing
2021-08-30 01:36:42 +02:00
d1admin e1630dc2b3 docs: mark as not in progress [ci skip] 2021-08-29 23:45:16 +02:00
d1admin 9310a85df8 docs: mark as next TODO [ci skip] 2021-08-29 21:20:52 +02:00
d1admin 440911f983 feat: finish app run command
continuous-integration/drone/push Build is passing
2021-08-29 21:20:21 +02:00
d1admin 8cc691ab52 WIP app run command
continuous-integration/drone/push Build is failing
2021-08-29 18:21:30 +02:00
d1admin bef2c862bf docs: wrapping 2021-08-29 18:21:10 +02:00
d1admin db4908c3ae feat: add restore command 2021-08-29 16:25:31 +02:00
d1admin ddbe9ffcb5 docs: mark as TODO [ci skip] 2021-08-29 14:16:27 +02:00
d1admin df236b6c25 docs: drop doctor, drop extra TODO, implied [ci skip] 2021-08-29 14:14:18 +02:00
d1admin 8651e22441 feat: implement app logs command
continuous-integration/drone/push Build is passing
2021-08-29 14:13:35 +02:00
d1admin 45e2442e83 fix: more robust length check 2021-08-29 14:13:07 +02:00
d1admin 547f785da5 feat: add app cp command
continuous-integration/drone/push Build is passing
2021-08-29 13:41:29 +02:00
d1admin 16297e8651 docs: mark as TODO
continuous-integration/drone/push Build is passing
2021-08-29 10:46:38 +02:00
d1admin acca710a5a feat: add app config command 2021-08-29 10:45:49 +02:00
d1admin 0825321b26 docs: shuffle TODO again
continuous-integration/drone/push Build is passing
2021-08-28 20:32:44 +02:00
d1admin cc45e722e8 docs: mark as TODO [ci skip] 2021-08-28 20:17:39 +02:00
d1admin 8ad51c1fd5 feat: implement check command 2021-08-28 20:13:56 +02:00
d1admin 23737ed3a7 docs: it aint WIP [ci skip] 2021-08-28 20:13:45 +02:00
d1admin 462599a791 docs: mark as TODO [ci skip] 2021-08-28 19:12:29 +02:00
d1admin 3a8296a8fe feat: implement backup command
continuous-integration/drone/push Build is passing
2021-08-28 19:10:19 +02:00
d1admin 73ebf998c7 docs: mark as next TODO
continuous-integration/drone/push Build was killed
2021-08-28 16:02:08 +02:00
d1admin ed551763de docs: shuffle TODO listing 2021-08-28 16:01:48 +02:00
d1admin ff0b0b5ad8 feat: finish ps command
continuous-integration/drone/push Build was killed
2021-08-28 16:00:16 +02:00
d1admin ce5a03d579 docs: mark as next TODO
continuous-integration/drone/push Build is passing
2021-08-25 14:24:40 +02:00
d1admin de5169ea24 fix: support trimming library is version listing
continuous-integration/drone/push Build is passing
2021-08-25 14:22:31 +02:00
d1admin 4f1cb86b6b feat: implement abra app version <app>
continuous-integration/drone/push Build is passing
2021-08-25 14:17:16 +02:00
d1admin 62ceca798c refactor: sort output of version command 2021-08-25 14:16:33 +02:00
d1admin b34acefa21 refactor: support listing unknown versions 2021-08-25 14:06:42 +02:00
d1admin c5bb680fed refactor: making app version command async 2021-08-25 13:30:55 +02:00
d1admin ed11634abf WIP abra app version <app> implementation 2021-08-25 13:06:49 +02:00
d1admin 3211994b2e docs: mark as in progress
continuous-integration/drone/push Build is passing
2021-08-24 10:18:37 +02:00
roxxers d2d0ce3d05 Merge pull request 'feat: initial commit for abra app volume ls/rm' (#51) from knoflook/go-abra:secret-create into main
continuous-integration/drone/push Build is passing
Reviewed-on: coop-cloud/go-abra#51
2021-08-18 18:29:13 +00:00
knoflook c3088a5158 refactor: create functions in client for removing and listing volumes
continuous-integration/drone/pr Build is passing
2021-08-17 20:39:07 +02:00
knoflook 5663659d23 feat: initial commit for abra app volume ls/rm 2021-08-17 20:39:06 +02:00
roxxers ca4001a805 docs: fixed typo in functiion comment
continuous-integration/drone/push Build is passing
2021-08-13 14:13:56 +01:00
roxxers e2f4ed11ec docs: updated report card to new repo 2021-08-13 14:13:37 +01:00
roxxers cc9c690cd0 build: added GOPRIVATE export to makefile
continuous-integration/drone/push Build is passing
2021-08-13 13:28:18 +01:00
roxxers 451c3d772d docs: updated go env steps fo install our pgks
continuous-integration/drone/push Build is passing
2021-08-13 13:23:54 +01:00
roxxers 98ec23761f refactor: de-vendor tagcmp into its own repo 2021-08-13 12:49:46 +01:00
roxxers d5b893d9de style: rm unneeded type assertions
continuous-integration/drone/push Build is passing
2021-08-12 14:56:06 +01:00
roxxers b143b544b6 fix: err not being checked & unneeded type assert 2021-08-12 14:53:42 +01:00
roxxers 6df08df509 style(tagcmp): simplify returns 2021-08-12 14:44:49 +01:00
roxxers 8f9ffa0667 style: correct error formatting ST1005
continuous-integration/drone/push Build is passing
2021-08-12 14:41:39 +01:00
roxxers 6f0eff5919 Merge pull request 'fix: abra app rm trying to remove secrets twice' (#50) from knoflook/go-abra:main into main
continuous-integration/drone/push Build is passing
Reviewed-on: coop-cloud/go-abra#50
2021-08-12 13:22:04 +00:00
knoflook cefad74e22 fix: app rm removing secrets and volumes twice
continuous-integration/drone/pr Build is passing
2021-08-12 12:59:11 +02:00
knoflook c2499e35d4 Update 'TODO.md'
continuous-integration/drone/push Build is passing
2021-08-12 09:36:57 +00:00
d1admin edd0b1e098 docs: add notice about conventional commits
continuous-integration/drone/push Build is passing
Closes coop-cloud/go-abra#3.
2021-08-10 20:11:36 +02:00
d1admin 33fc4844ba Point to new CI instance
continuous-integration/drone/push Build is failing
2021-08-10 20:08:08 +02:00
d1admin ba5e87f754 docs: mark that as done 2021-08-10 13:22:37 +02:00
d1admin cbe74b24c4 fix: support different type of registry response 2021-08-10 13:16:41 +02:00
d1admin 83671f42a2 feat: recipe sync 2021-08-10 12:55:23 +02:00
d1admin c6ea18311e refactor: drop this for now 2021-08-10 08:34:36 +02:00
d1admin 1c217b127b docs: add recipe upgrade docs 2021-08-10 08:34:11 +02:00
d1admin 2028b9d7c7 docs: drop that command for now 2021-08-10 08:26:19 +02:00
d1admin fa5f5f650d docs: mark as done for now 2021-08-10 08:25:08 +02:00
d1admin a12b53abab feat: support tag upgrades without semver-like tags 2021-08-10 08:24:36 +02:00
d1admin e39c6a05be feat: detect if tags are not parsable 2021-08-10 07:57:23 +02:00
d1admin 210baf1905 feat: first POC for recipe upgrade 2021-08-10 07:53:05 +02:00
d1admin 1b03836210 WIP: add compose updating to recipe upgrade 2021-08-09 17:36:21 +02:00
d1admin 334e417abf WIP include catalogue checking in upgrade command 2021-08-09 16:29:16 +02:00
d1admin 7b1a6dd4d7 WIP first run at the upgrade command 2021-08-09 16:17:40 +02:00
d1admin a18a9493f2 fix: add missing error handling 2021-08-09 16:17:31 +02:00
d1admin 16e844643a fix: catch suffix comparison bug
Upstream'd to coopcloud/tagcmp also.
2021-08-09 16:17:01 +02:00
d1admin 7ad812ad98 refactor: migrate JSON function to new package
We now use it to help read remote docker registries also.
2021-08-09 16:16:33 +02:00
d1admin 260edad142 feat: add vendored tagcmp temporarily
See coop-cloud/coopcloud.tech#20.
2021-08-09 13:53:40 +02:00
d1admin 5ac4604f8a docs: catch-up with TODO listing 2021-08-07 17:00:51 +02:00
d1admin 3d7961282a refactor: drop that back to TODO for now 2021-08-06 21:24:56 +02:00
d1admin 828417c92b refactor: add config.GetAppComposeFiles 2021-08-06 19:38:06 +02:00
d1admin 11ef64ead3 WIP: abra recipe upgrade on the way 2021-08-06 15:40:23 +02:00
d1admin c75c2254e4 refactor: spec out new release command breakdown 2021-08-06 12:34:59 +02:00
d1admin 36af302d5f refactor: dangling else, Sprintf formatting, printing 2021-08-06 12:20:14 +02:00
knoflook 6732edf8db feat: implement app remove
See coop-cloud/go-abra#43.
2021-08-06 12:00:24 +02:00
3wordchant 8554e68418 fix: line break after recipe create 2021-08-06 10:37:15 +02:00
d1admin 202f7ce561 WIP: spec'ing out the release command
See coop-cloud/go-abra#39.
2021-08-04 23:52:34 +02:00
d1admin 9378db1979 fix: look up ipv4 from host correctly
We use a custom resolver now instead of relying on the baked-in
Golang resolver which has issues. We use a friendly librehoster
DNS resolver and not Google because fuck that.
2021-08-04 22:51:07 +02:00
d1admin efb9d6f6a5 feat: finalise recipe lint command 2021-08-04 00:07:23 +02:00
d1admin 327e2afcd0 docs: remove marker, "re-open" release command 2021-08-04 00:07:05 +02:00
knoflook e22d22056d Update 'TODO.md' 2021-08-03 20:09:23 +00:00
d1admin 532bb8a336 WIP: recipe lint command 2021-08-03 19:25:32 +02:00
d1admin 3a42288a59 docs: add ref to new command
See coop-cloud/go-abra#40.
2021-08-03 13:59:10 +02:00
d1admin 471c982f63 refactor: use new internal arg failure func 2021-08-03 13:57:12 +02:00
d1admin 43238d379c docs: mark those as in-progress 2021-08-03 12:04:13 +02:00
roxxers 239c925d66 WIP: foundations for app deploy 2021-08-03 08:49:16 +01:00
roxxers b351760f6e refactor(typo): typo of hetzner in output for user 2021-08-02 23:26:57 +01:00
d1admin 102f4e22b5 docs: fix typo 2021-08-02 22:03:53 +02:00
d1admin 444ac52476 docs: mark that all done 2021-08-02 15:27:18 +02:00
d1admin 5294e84d5e feat: implement capsul create 2021-08-02 15:11:14 +02:00
d1admin 3e91174ce0 feat: implement hetzner new command 2021-08-02 14:05:39 +02:00
roxxers fa16ce20eb refactor: added more comments to functions
many more are required but in too tired to do more
2021-08-02 08:02:18 +01:00
roxxers 38d8b51bd5 refactor: moved a lot of flags & added comments
Comments added to fix the golint errors on exported things need comments
2021-08-02 07:36:35 +01:00
roxxers 9070806f8d refactor: deal with err from ShowSubcommandHelp 2021-08-02 05:58:47 +01:00
roxxers bb1eb372ef refactor: stack func to client, mv app to new file
Stack interaction is now under client.

App types and functions moved from env to app under config
2021-08-02 05:51:58 +01:00
roxxers d777eb2af1 refactor(style): errs should not start with upper 2021-08-02 04:20:02 +01:00
roxxers a3f574a8fa refactor: app new cmd to be easier to read 2021-08-02 04:18:20 +01:00
roxxers 30d11f48a7 refactor: break up cli pkg into nice small chunks 2021-08-02 02:10:41 +01:00
roxxers c2f53e493e deps: upgraded hcloud-go to direct dep 2021-08-02 01:10:19 +01:00
roxxers dc4e490497 refactor(style): error str shouldnt be capitalized 2021-08-02 01:09:25 +01:00
roxxers ffd1b3a771 refactor: function rename
`errorExit` renamed to `showSubcommandHelpAndError`
2021-08-02 01:08:17 +01:00
roxxers 8267d4202b feat: function to display help, error, & exit 2021-08-02 00:57:11 +01:00
d1admin d74b7636a1 WIP make a start on the hetzner command 2021-08-02 01:54:16 +02:00
roxxers 9d621404fd fix: avoid runtime error when list is empty 2021-08-02 00:37:23 +01:00
d1admin 4ae5e6123d refactor: add specific check for missing context 2021-08-02 01:06:41 +02:00
d1admin 19d435c5e5 feat: implement server init 2021-08-02 01:03:27 +02:00
d1admin 6be54c670a fix: error out if missing server arg 2021-08-02 00:37:25 +02:00
d1admin a1bce4661b docs: server CLI documentation 2021-08-02 00:30:03 +02:00
d1admin 8a5ee68b7b refactor: drop alias command
Save us some work and avoid confusion on two things doing the same thing
under different top-level sub-commands (this was just an experiment
after all).
2021-08-02 00:20:39 +02:00
d1admin 1846f965ec docs: mark this as done 2021-08-02 00:13:58 +02:00
roxxers 805defec09 docs(comment): updated comment to be upto date 2021-07-31 21:25:32 +01:00
roxxers f958b888b6 fix: TestReadEnv test due to refactor 2021-07-31 21:08:50 +01:00
roxxers 1768809872 chore: add vendor folder to gitignore 2021-07-31 21:02:17 +01:00
d1admin 8abc47d2e0 docs: some README love 2021-07-31 19:13:59 +02:00
d1admin bf7de84c66 chore: upgrade godotenv fork for multiline support
Also ran `go mod tidy`.
2021-07-31 19:03:31 +02:00
d1admin 760ac495b3 fix: handle error for reading apps 2021-07-31 18:47:32 +02:00
d1admin 4d12a75494 docs: more specifics in TODO file 2021-07-31 16:17:06 +02:00
d1admin 1442c71911 docs: mark that one as not in progress 2021-07-31 15:50:50 +02:00
d1admin e4c864a60c docs: mark that one as done 2021-07-31 15:50:23 +02:00
d1admin 42968fb8e1 feat: finally implement app new command 2021-07-31 15:50:04 +02:00
d1admin 932803453e WIP: still hacking on the app new command
Finally had to fork godotenv because it strips comments and we need
those to parse length values (e.g. "FOO=v1  # length=10") (or in other
words, motivation to move to the YAML format).

There is a new secret module now, with functionality for dealing with
generation and parsing of secrets.

The final output needs some work and there is also the final step of
implementing the sending of secrets to the docker daemon. Coming Soon
™️.
2021-07-31 12:49:22 +02:00
d1admin 5771f6c158 WIP another pass on the app new command 2021-07-30 22:55:00 +02:00
d1admin e728bcd7ac docs: CLI flag docs and rewording of usage 2021-07-30 22:54:30 +02:00
d1admin 769c5b899b refactor: abstract secret generation into package 2021-07-30 22:53:51 +02:00
d1admin f56ddef6c8 WIP: another step further into app new command 2021-07-30 20:14:17 +02:00
roxxers ac6b8ab147 chore(deps): upgrade containerd 1.5.3 -> 1.5.5 2021-07-30 16:34:06 +01:00
roxxers a581049cf1 refactor: simplify for loop 2021-07-30 16:32:06 +01:00
d1admin 58bdb456df refactor: use variable to make more readable 2021-07-30 17:09:23 +02:00
d1admin d97da9f45c fix: use correct path for checking app path 2021-07-30 17:07:51 +02:00
d1admin 064a0f271f WIP: further process on app new command 2021-07-30 13:16:28 +02:00
d1admin 6c36e77722 docs: add 3rd party integration TODOs 2021-07-29 12:32:16 +02:00
d1admin d422902e09 WIP: spec out first steps for app new command 2021-07-29 12:26:11 +02:00
d1admin e4ed2aeebf docs: better wording 2021-07-28 22:13:05 +02:00
d1admin f7b085dfa2 feat: add abra dir creation function 2021-07-28 22:10:42 +02:00
d1admin 1187d6bfd5 refactor: move catalogue logic into own package 2021-07-28 22:10:13 +02:00
d1admin bf0212c520 docs: more flag aliases (for app new command) 2021-07-28 14:27:23 +02:00
d1admin de3ea8188e WIP spec out app new command 2021-07-28 14:26:37 +02:00
d1admin bf7d437571 docs: more CLI documentation 2021-07-28 13:56:18 +02:00
d1admin 1ee572363a chore: mark command as in-progress 2021-07-28 11:30:30 +02:00
d1admin 2c1b8ee7e2 docs: document flags for app new command 2021-07-28 11:30:14 +02:00
d1admin 622e0127ea docs: fill out app listing CLI docs 2021-07-28 11:29:59 +02:00
d1admin d581d3313a docs: add missing command and drop prefix 2021-07-27 21:40:09 +02:00
d1admin 0e75350985 feat: prototype for app listing 2021-07-27 21:25:08 +02:00
d1admin cf7a8d114a chore: remove unused prototype code 2021-07-27 19:46:01 +02:00
d1admin ef1591d596 WIP: app status listing using concurrency
This being my first time using goroutines, it is pretty messy but the
idea has been shown to be workable! We can concurrently look up multiple
contexts for a much faster response time especially when using multiple
servers.

Remaining TODOs are:

- [ ] Get proper status reporting (deployed/inactive/unknown)
- [ ] Error handling (especially when missing contexts)
- [ ] Refactor and tidy
2021-07-27 12:52:09 +02:00
d1admin 429c7e4e50 docs: take a pass on CLI usage docs and add ASCII 2021-07-26 23:58:34 +02:00
d1admin 3bc612c44e WIP: status lookup for apps listing 2021-07-26 20:59:17 +02:00
d1admin 2c83113040 docs: add shorthand and usage docs for app ls flags 2021-07-26 19:59:50 +02:00
d1admin fae5a87ce2 fix: respect --type/-t logic for app listing
Reverts c27376c89b. Woops.
2021-07-26 19:59:26 +02:00
d1admin 145e6326c9 fix: use domain to follow original abra app ls 2021-07-26 19:49:51 +02:00
d1admin 5def18a9af fix: sort by server and type for app listing 2021-07-26 19:47:44 +02:00
d1admin 8656ae947a tests: fix App def to match new struct format
Follows from 01cbee824a.
2021-07-26 19:22:26 +02:00
d1admin c27376c89b fix: disable merging and rely on type being present 2021-07-26 19:16:38 +02:00
d1admin 01cbee824a WIP: app list command sorting 2021-07-26 18:23:28 +02:00
d1admin 337d3e9ae1 refactor: more conventional name for method 2021-07-26 17:50:40 +02:00
d1admin 60a70d2d83 refactor(recipe): better naming, sorting and types
In order to arrange various types of sorting for the app catalogue, it
seems like the recommended approach is to maintain a separate data
structure alongside the JSON map we get from apps.coopcloud.tech.

Therefore, I attempt to provide a ToList() method and accompanying
sort.Sort interface sorting implementations. For now, this is just
sorting by app name.

I am testing this type of implementation here before moving on to
arrange different types of sorting for the `app list` command.
2021-07-26 17:25:08 +02:00
d1admin 1f62ace524 refactor: use method to sort recipe apps listing 2021-07-26 15:43:35 +02:00
d1admin 13028db287 chore: go mod tidy for new deps (go-git) 2021-07-26 15:38:33 +02:00
d1admin 1f550c2470 feat: finish recipe create command 2021-07-25 19:28:29 +02:00
d1admin 359b07b562 WIP: recipe create 2021-07-25 00:07:35 +02:00
d1admin 45c3bce7ff fix: return if erroring out 2021-07-24 23:30:42 +02:00
d1admin 6eee02d90a feat: add recipe versions command 2021-07-24 23:18:23 +02:00
roxxers dfc91a86a1 feat: WIP server rm command
continuous-integration/drone/push Build is passing
2021-07-22 17:38:44 +01:00
roxxers dd86ec4ca8 refactor: client pkg with new context interaction
continuous-integration/drone/push Build is passing
2021-07-22 15:31:43 +01:00
d1admin fce1ab6c02 refactor: better naming for loop scoped variables
continuous-integration/drone/push Build is passing
2021-07-22 14:53:08 +02:00
d1admin 381de28e83 refactor: make ReadApps main API entrypoint
This allows AppsReadFS/AppsReadWeb to be used behind the scenes of this
API for the conditional loading logic. All functions are left as public
for now while we're experimenting.
2021-07-22 14:51:56 +02:00
d1admin 56cec1580a refactor: use app-less naming for this struct also
continuous-integration/drone/push Build is passing
2021-07-22 14:25:37 +02:00
roxxers ebfdb504ce docs: updated todo
continuous-integration/drone/push Build is passing
2021-07-22 12:49:08 +01:00
roxxers fc7dade6f8 feat: server add command
continuous-integration/drone/push Build is passing
Interacts with and stores infomaton in the docker store at ~/.docker

Equivalent to docker context add
2021-07-22 12:48:14 +01:00
roxxers 5e94050865 refactor: forgot there is a function in docker src
continuous-integration/drone/push Build is passing
2021-07-22 10:19:05 +01:00
roxxers fe86b50ee3 refactor: actual context getting
continuous-integration/drone/push Build is passing
2021-07-22 09:51:27 +01:00
decentral1se a4a8997f57 Merge pull request 'Support local apps.json loading' (#10) from apps-json-handling into main
continuous-integration/drone/push Build is passing
Reviewed-on: https://git.autonomic.zone/coop-cloud/go-abra/pulls/10
2021-07-21 22:44:50 +02:00
d1admin 1f6c0e8c4b feat: support local apps.json loading
continuous-integration/drone/pr Build is passing
This logic supports the following cases:

- Download a fresh apps.json and load it if missing
- Check if a local apps.json is old and get a fresh one if so
- Always save a local copy after downloading a fresh apps.json

The http.Head() call is faster than a http.Get() call (only carries back
respones headers) and aims to make the more general case more
performant: you have the latest copy of the apps.json and don't need to
download another one. This a direct port of our Bash implementation
logic.

Closes https://git.autonomic.zone/coop-cloud/go-abra/issues/9.
2021-07-21 22:42:51 +02:00
roxxers 6b370599fa refactor: simplified sort of app names
continuous-integration/drone/push Build is passing
2021-07-21 17:12:35 +01:00
roxxers 9216cc5d6a refactor: simplifing range statement
continuous-integration/drone/push Build is passing
2021-07-21 16:36:46 +01:00
d1admin 53576dc916 Revert "style: add missing type marker"
continuous-integration/drone/push Build is passing
This reverts commit e064f18730.

As discussed, this is explicitly using a type shorthand which is all
good.
2021-07-21 13:32:16 +02:00
decentral1se 05ff163386 Merge pull request 'Add recipe ls command' (#8) from recipe-ls into main
continuous-integration/drone/push Build is passing
Reviewed-on: https://git.autonomic.zone/coop-cloud/go-abra/pulls/8
2021-07-21 13:30:14 +02:00
d1admin 302ebcb394 feat: add recipe ls command
continuous-integration/drone/pr Build is passing
2021-07-21 13:28:46 +02:00
roxxers 0242dfcb0f fix: multiline vars can now be read using fork
continuous-integration/drone/push Build is passing
2021-07-21 12:05:50 +01:00
roxxers 29971c36a0 refactor: moved all fatal errors to logrus
continuous-integration/drone/push Build is passing
This will allow us to test commands that would normally exit
2021-07-21 09:04:34 +01:00
roxxers 2158dc851c test: makefile now runs all tests recursively
continuous-integration/drone/push Build is passing
2021-07-21 08:56:53 +01:00
roxxers a36e80db99 fix: fixing domain being required.
continuous-integration/drone/push Build is passing
Fixes gitea issue #5
2021-07-21 08:38:13 +01:00
roxxers b0c241ae98 bulid: added build-dev option
continuous-integration/drone/push Build is passing
this otion does no optimisations in the compile (removing debug stuff)
2021-07-21 08:20:17 +01:00
d1admin a74d214121 docs: use hyphen shortname and trim message
continuous-integration/drone/push Build is passing
2021-07-21 00:19:06 +02:00
d1admin e064f18730 style: add missing type marker
continuous-integration/drone/push Build is passing
2021-07-21 00:17:16 +02:00
d1admin 7b2100c568 feat: add version command
continuous-integration/drone/push Build is passing
2021-07-20 23:59:47 +02:00
d1admin c9ba7aef20 build: reduce binary size with optimisation flags
continuous-integration/drone/push Build is passing
Part of https://git.autonomic.zone/coop-cloud/go-abra/issues/4.
2021-07-20 22:45:49 +02:00
roxxers 16514b3151 feat: implemented type & servers flags in app ls
continuous-integration/drone/push Build is passing
2021-07-20 13:00:03 +01:00
d1admin 635c6d6080 test: integrate new test target into CI build
continuous-integration/drone/push Build is passing
2021-07-19 15:50:16 +02:00
roxxers dee013e4e4 test: added makefile entry for running tests
continuous-integration/drone/push Build is passing
2021-07-19 14:38:19 +01:00
roxxers a60ebf8710 tests: around 60% code coverage for config package
continuous-integration/drone/push Build is passing
2021-07-19 14:36:00 +01:00
roxxers cfe2f70151 refactor: moving logging to command functions
easier to unit test our util commands like this
2021-07-19 12:47:46 +01:00
roxxers bd9bc530d1 faet: a draft version of the app ls command
continuous-integration/drone/push Build is passing
2021-07-19 08:37:00 +01:00
roxxers f7059dbe98 refactor: filesystem io 2021-07-19 07:04:37 +01:00
roxxers 8c5e25bd01 chore: updated gitignore; added vscode settings 2021-07-19 06:57:50 +01:00
roxxers 6caa176308 WIP: Enviroment file loading and config management
continuous-integration/drone/push Build is passing
2021-07-18 10:49:31 +01:00
roxxers 7d5db5fee1 docs: updated todo
continuous-integration/drone/push Build is passing
2021-07-18 06:41:19 +01:00
roxxers 37c06c82bf feat: added error to GetContextEndpoint
continuous-integration/drone/push Build is passing
this ill make the progam not fail if there is a non-docker swarm context
2021-07-18 06:34:22 +01:00
roxxers a2bb0ed027 test: added the first test
I am not very good at unit testing but we need to start writing them
2021-07-18 06:31:09 +01:00
roxxers 38f610bdec feat: abra server ls
continuous-integration/drone/push Build is passing
WE DID IT! The first actual command to be ported.

Code is still a mess in terms of UX but its a milestone!
2021-07-18 04:21:26 +01:00
roxxers e13948f37e docs: added todo list for the port 2021-07-18 04:10:14 +01:00
roxxers d1f7e8011d refactor: Moved table function to fornatter
Makes more sense for it to be in there
2021-07-18 03:24:48 +01:00
roxxers 2134f57dd0 WIP: Messy code that is mostly just testing
continuous-integration/drone/push Build is passing
This is me trying to print all services in a stack.

Struggling to isolate stack and tasks which is needed for swarm
2021-07-17 09:30:56 +01:00
roxxers 6c748922b4 feat: added context flag to make dev easier
needed until we have proper server subcommand system
2021-07-17 09:29:25 +01:00
roxxers b9e06f2310 feat: added util formatting for the cil 2021-07-17 09:27:52 +01:00
roxxers be46695d82 feat: added flags to base command
they already exist just attatching them to the command
2021-07-17 05:11:48 +01:00
roxxers ae68f3aa95 chore: go mod tidy for dependancy
continuous-integration/drone/push Build is passing
2021-07-17 04:35:43 +01:00
roxxers 5e1b076bf9 feat: very basic context management
continuous-integration/drone/push Build is passing
Taken from this gist by github.com/agbaraka

https://gist.github.com/agbaraka/654a218f8ea13b3da8a47d47595f5d05

There is no in-built way of dealing with contexts using the golang sdk.

This means we have to make our own Dial helper borrowing from Docker CLI

This means all Docker API calls are made within the ssh connection

This uses `docker system dial-stdio`
2021-07-16 09:32:24 +01:00
d1admin c65ae974dd Add experimental staticcheck into linting CI
continuous-integration/drone/push Build is passing
See https://staticcheck.io for more. This is set to be ignored
on failure so that it doesn't disrupt current work flows but maybe
it is nice to add. Just drop the `ignore: ...` line and it will fail
builds.
2021-07-15 23:57:08 +02:00
d1admin 9b8f16345c Add a Go report card badge
continuous-integration/drone/push Build is passing
2021-07-15 23:35:43 +02:00
d1admin 4884c14ab3 Can't sort this as VERSION is not defined then
continuous-integration/drone/push Build is passing
2021-07-15 23:26:47 +02:00
d1admin a9d9d9de2f Silence command echoing to focus output on errors
continuous-integration/drone/push Build is passing
2021-07-15 23:01:31 +02:00
d1admin 980f2f7684 Add format and check targets + integrate into CI 2021-07-15 23:00:33 +02:00
d1admin 3b1dfb7562 Wire up notifications for failures
continuous-integration/drone/push Build is passing
2021-07-15 22:29:31 +02:00
d1admin 0a6ffd48cb Sort vars
continuous-integration/drone/push Build is passing
2021-07-15 22:27:30 +02:00
d1admin 567dae83cf Parametrize the abra path 2021-07-15 22:26:40 +02:00
d1admin 462a4d296f Add CI badge and link to OG abra repo
continuous-integration/drone/push Build is passing
2021-07-15 15:40:29 +02:00
d1admin 8373dea7fb Take a stab at a Drone CI/CD build 2021-07-15 15:37:54 +02:00
d1admin b13081d1a6 Add build, parametrize LDFLAGS and list all targets 2021-07-15 15:26:02 +02:00
d1admin 8b38b89647 Ignore built binaries 2021-07-15 15:25:54 +02:00
decentral1se 3a96f48ec5 Merge pull request 'Add install and clean rules to Makefile' (#1) from knoflook/go-abra:main into main
Reviewed-on: https://git.autonomic.zone/coop-cloud/go-abra/pulls/1
2021-07-15 15:07:47 +02:00
knoflook aa8db280e5 Add install and clean rules to Makefile 2021-07-15 14:19:05 +02:00
roxxers a78bb9123a feat: POC passgen 2021-07-15 08:23:26 +01:00
roxxers 881ccfd820 chore: added libs i plan to use in future 2021-07-15 08:06:12 +01:00
roxxers 1adca5ca0e feat: added app commands and flags for commands 2021-07-15 06:17:47 +01:00
roxxers 9a0bd6dc11 refactor(cli): moved commands and cli out of main 2021-07-15 03:44:07 +01:00
roxxers 2aa9029893 chore: added git-chglog options
this is for the future to add automated changelogs
2021-07-15 01:18:34 +01:00
roxxers a2a836c2a9 feat: added version and makefile
makefile allows for package variables to be defined
2021-07-13 23:47:47 +01:00
roxxers a7d748cb1f docs: Added readme explianing the repo 2021-07-13 23:12:56 +01:00
roxxers 9e52e9f676 Initial Commit
Added some cli commands
2021-07-13 22:34:32 +01:00
4201 changed files with 1076633 additions and 381 deletions
+7
View File
@@ -0,0 +1,7 @@
*.swo
*.swp
.dockerignore
Dockerfile
abra
dist
tags
+144 -8
View File
@@ -1,13 +1,149 @@
---
kind: pipeline
name: linters
name: coopcloud.tech/abra
steps:
- name: run shellcheck
image: debian:buster
- name: make check
image: golang:1.24
commands:
- apt update
- apt install -y shellcheck
- shellcheck abra installer
- make check
- name: xgettext-go
image: git.coopcloud.tech/toolshed/drone-xgettext-go:latest
settings:
keyword: i18n.G
keyword_ctx: i18n.GC
out: pkg/i18n/locales/abra.pot
comments_tag: translators
depends_on:
- make check
when:
event:
exclude:
- tag
- name: xgettext-go status
image: golang:1.24-alpine3.22
commands:
- 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.24
environment:
ABRA_DIR: $HOME/.abra
CATL_URL: https://git.coopcloud.tech/toolshed/recipes-catalogue-json.git
commands:
- mkdir -p $HOME/.abra
- git clone $CATL_URL $HOME/.abra/catalogue
- make test
depends_on:
- make check
- name: fetch
image: docker:git
commands:
- git fetch --tags
depends_on:
- make check
- make test
when:
event: tag
- name: release
image: goreleaser/goreleaser:v2.5.1
environment:
GITEA_TOKEN:
from_secret: goreleaser_gitea_token
volumes:
- name: deps
path: /go
commands:
- goreleaser release
depends_on:
- fetch
when:
event: tag
- 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:
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:
branch:
- main
action:
exclude:
- synchronized
+7
View File
@@ -0,0 +1,7 @@
# integration test suite
# export ABRA_DIR="$HOME/.abra_test"
# export TEST_SERVER=test.example.com
# export ABRA_CI=1
# release automation
# export GITEA_TOKEN=
+9
View File
@@ -0,0 +1,9 @@
*.tar.gz
*fmtcoverage.html
.e2e.env
.envrc
.vscode/
/abra
/bin
dist/
tests/integration/.bats
+51
View File
@@ -0,0 +1,51 @@
---
gitea_urls:
api: https://git.coopcloud.tech/api/v1
download: https://git.coopcloud.tech/
skip_tls_verify: false
before:
hooks:
- go mod tidy
builds:
- id: abra
binary: abra
dir: cmd/abra
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- 386
- amd64
- arm
- arm64
goarm:
- 5
- 6
- 7
ldflags:
- "-X 'main.Commit={{ .Commit }}'"
- "-X 'main.Version={{ .Version }}'"
- "-s"
- "-w"
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: desc
filters:
exclude:
- "^Merge"
- "^Revert"
- "^WIP:"
- "^chore(deps):"
- "^style:"
- "^test:"
- "^tests:"
+25
View File
@@ -0,0 +1,25 @@
# 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
- decentral1se
- fauno
- frando
- iexos
- kawaiipunk
- knoflook
- mayel
- moritz
- p4u1
- rix
- roxxers
- vera
- yksflip
-15
View File
@@ -1,15 +0,0 @@
# abra 0.2.0 (2020-09-24)
- Prepare for swarm install script using script.d ([#12](https://git.autonomic.zone/compose-stacks/planning/issues/12))
# abra 0.1.2 (2020-09-22)
- Add upgrade command ([#10](https://git.autonomic.zone/autonomic-cooperative/abra/issues/10))
# abra 0.1.1 (2020-09-22)
- Add installer script ([#9](https://git.autonomic.zone/autonomic-cooperative/abra/issues/9))
# abra 0.1.0 (2020-09-22)
- Initial pre-alpha release
+30
View File
@@ -0,0 +1,30 @@
# Build image
FROM golang:1.24-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
# Release image ("slim")
FROM alpine:3.19.1
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/>.
+82 -9
View File
@@ -1,14 +1,87 @@
default: install
ABRA := ./cmd/abra
XGETTEXT := ./bin/xgettext-go
COMMIT := $(shell git rev-list -1 HEAD)
GOPATH := $(shell go env GOPATH)
GOVERSION := 1.24
LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
DIST_LDFLAGS := $(LDFLAGS)" -s -w"
GCFLAGS := "all=-l -B"
DOMAIN := abra
POFILES := $(wildcard pkg/i18n/locales/*.po)
MOFILES := $(patsubst %.po,%.mo,$(POFILES))
LINGUAS := $(basename $(POFILES))
dev_install:
ln -sf $(PWD)/abra ~/.local/bin
export GOPRIVATE=coopcloud.tech
all: format check build
run:
@go run -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(ABRA)
install:
install abra /usr/bin/abra
@go install -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(ABRA)
get_yq:
wget https://github.com/mikefarah/yq/releases/download/3.3.2/yq_linux_amd64 && \
chmod +x yq_linux_amd64 && \
mv yq_linux_amd64 yq
build:
@go build -v -gcflags=$(GCFLAGS) -ldflags=$(DIST_LDFLAGS) $(ABRA)
.PHONY: dev_install install get_yq
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 $$(find . -type f -name '*.go' | grep -v "/vendor/")
check:
@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 -v
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)
@${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)
${XGETTEXT}:
@mkdir -p ./bin && \
wget -O ./bin/xgettext-go https://git.coopcloud.tech/toolshed/xgettext-go/raw/branch/main/xgettext-go && \
chmod +x ./bin/xgettext-go
.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
+9 -26
View File
@@ -1,31 +1,14 @@
# abra
# `abra`
[![Build Status](https://drone.autonomic.zone/api/badges/autonomic-cooperative/abra/status.svg)](https://drone.autonomic.zone/autonomic-cooperative/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/)
Docker stack magic 🎩🐇
The Co-op Cloud utility belt 🎩🐇
## Install
<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>
```sh
curl -fsSL https://install.abra.autonomic.zone | bash
```
`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 💖
Specific releases are available via the project [release page](https://git.autonomic.zone/autonomic-cooperative/abra/releases).
## Changes
See [CHANGELOG.md](./CHANGELOG.md).
## Hacking
```sh
git clone ssh://git@git.autonomic.zone:2222/autonomic-cooperative/abra.git
cd abra
make dev_install
```
See [autonomic-cooperative/installer-scripts](https://git.autonomic.zone/autonomic-cooperative/installer-scripts) for the installer script deployment. To make a release, just add an entry to [CHANGELOG.md](./CHANGELOG.md) and the [abra-installer](./script.d/abra-installer) / [swarm-installer](./script.d/swarm-installer) (following [semver](https://semver.org/) please) and then `git tag x.x.x && git push origin main --tags`. If you want the [installer-scripts](https://git.autonomic.zone/autonomic-cooperative/installer-scripts) deployment to pick that up, you'll need to change the version number in the [Makefile](https://git.autonomic.zone/autonomic-cooperative/installer-scripts/src/branch/main/Makefile) and run `make` in that repository and push the changes.
## Examples
- `abra run mariadb mysqldump gitea -p'GdIbMeS09SURRktBnm3jcTufsL5z0MPd' | gzip > ../git.autonomic.zone_mariadb_`date +%F`.sql.gz`
Please see [docs.coopcloud.tech/abra](https://docs.coopcloud.tech/abra) for help on install, upgrade, hacking, troubleshooting & more!
-282
View File
@@ -1,282 +0,0 @@
#!/bin/bash
PROGRAM_NAME=$(basename "$0")
ABRA_CONFIG=abra.yml
if [ -z "$COMPOSE_FILE" ]; then
COMPOSE_FILE="compose.yml"
fi
yml_pattern_exists() {
PATTERN=$1
if ! type yq > /dev/null 2>&1; then
echo "$(tput setaf 1)ERROR: yq program is not installed$(tput sgr0)"
exit
fi
if [ -f $ABRA_CONFIG ]; then
RESULT=$(yq read $ABRA_CONFIG "$PATTERN")
if [ "$RESULT" != 0 ]; then
return 0
fi
fi
return 1
}
if [ "$1" == "-a" ]; then
STACK_NAME=$2
shift 2
fi
if [ -f abra.yml ]; then
if yml_pattern_exists stack_name; then
STACK_NAME=$(yq read abra.yml stack_name)
fi
fi
if [ -z "$STACK_NAME" ]; then
echo "$(tput setaf 1)ERROR: \$STACK_NAME must be set (e.g. export STACK_NAME=my_cool_app)$(tput sgr0 )"
exit
fi
if type direnv > /dev/null 2>&1 && ! direnv status | grep -q 'Found RC allowed true'; then
echo "$(tput setaf 1)ERROR: direnv is blocked, run direnv allow$(tput sgr0)"
exit
fi
if [ -f abra-commands.sh ]; then
# shellcheck disable=SC1091
. abra-commands.sh
fi
sub_help() {
echo "Usage: $PROGRAM_NAME [-a STACK_NAME] <subcommand> [options]"
echo ""
echo "Subcommands:"
echo " cp SRC_PATH SERVICE:DEST_PATH copy files to a container"
echo " deploy let 'em rip"
echo " logs SERVICE [ARGS] tail logs from a deployed service"
echo " run SERVICE CMD run a command in the specified service's container"
echo " run_args SERVICE ARGS CMD run, passing extra args to docker exec"
echo " secret_generate SECRET VERSION [CMD] generate a secret, store it in pass & as a Docker secret"
echo " upgrade upgrade to the latest version"
echo " ... (custom commands)"
echo ""
echo "Make sure \$STACK_NAME is set using direnv or -a"
echo ""
echo "Runs compose.yml by default, set e.g. COMPOSE_FILE=\"compose.yml:compose2.yml\" to override"
}
sub_secret_help() {
echo "Usage: $PROGRAM_NAME [-a STACK_NAME] secret <subcommand> [options]"
echo ""
echo "Subcommands:"
echo " generate [PW] generate & store secret"
}
sub_secret_generate(){
SECRET=$1
VERSION=$2
PW=${3:-pwqgen}
if [ -z "$SECRET" ] || [ -z "$VERSION" ]; then
echo "Usage: $PROGRAM_NAME secret_generate SECRET VERSION"
exit
fi
$PW | tee \
>(docker secret create "${STACK_NAME}_${SECRET}_${VERSION}" -) \
>(pass insert "hosts/autonomic-swarm/${STACK_NAME}/${SECRET}" -m)
}
sub_secret() {
SUBCOMMAND=$1
shift
# shellcheck disable=SC2068
parse_subcommand "$SUBCOMMAND" "secret" $@
}
sub_run_args(){
SERVICE=$1
DOCKER_ARGS=$2
shift 2
if [ -z "$SERVICE" ]; then
echo "Usage: $PROGRAM_NAME run SERVICE [CMD]"
exit
fi
CONTAINER=$(docker container ls --format "table {{.ID}},{{.Names}}" \
| grep "${STACK_NAME}_${SERVICE}" | cut -d',' -f1)
if [ -z "$CONTAINER" ]; then
echo "Container not found! 🚨"
exit
fi
# shellcheck disable=SC2086
docker exec $DOCKER_ARGS -it "$CONTAINER" "$@"
return
}
sub_run(){
SERVICE=$1
shift
sub_run_args "$SERVICE" "" "$@"
}
sub_deploy (){
echo "About to deploy:"
echo " Compose: $(tput setaf 3)${PWD}/${COMPOSE_FILE}$(tput sgr0)"
if [ -n "$DOMAIN" ]; then
echo " Domain: $(tput setaf 2)${DOMAIN}$(tput sgr0)"
fi
echo " Stack: $(tput setaf 1)${STACK_NAME}$(tput sgr0)"
read -rp "Continue? (y/[n])? " choice
case "$choice" in
y|Y ) ;;
n|N ) return;;
* ) return;;
esac
# shellcheck disable=SC2086
if docker stack deploy -c ${COMPOSE_FILE/:/ -c } "$STACK_NAME"; then
if [ -n "$DOMAIN" ]; then
echo "$(tput setaf 2)Yay! App should be available at https://${DOMAIN}$(tput sgr0)"
else
echo "$(tput setaf 2)Yay! That worked. No \$DOMAIN defined, check logs.(tput sgr0)"
fi
else
echo "$(tput setaf 1)Oh no! Something went wrong 😕 Check errors above$(tput sgr0)"
fi
}
sub_logs (){
SERVICE=$1
shift
if [ $# -eq 0 ]; then
LOGS_ARGS="\
--follow \
--no-trunc \
--details \
--timestamps"
else
# shellcheck disable=SC2124
LOGS_ARGS=$@
fi
# shellcheck disable=SC2086
docker service logs "${STACK_NAME}_${SERVICE}" $LOGS_ARGS
}
sub_cp() {
SOURCE=$1
DEST=$2
SERVICE=$(echo "$SOURCE" | grep -o '^[^:]\+:' || echo "$DEST" | grep -o '^[^:]\+:')
SERVICE=$(echo "$SERVICE" | tr -d ':')
if [ -z "$SERVICE" ]; then
echo "$(tput setaf 1)ERROR: Can't find SERVICE in either SRC or DEST$(tput sgr0)"
echo ""
echo "Usage: $PROGRAM_NAME cp SERVICE:SRC_PATH DEST_PATH"
echo " $PROGRAM_NAME cp SRC_PATH SERVICE:DEST_PATH"
exit
fi
CONTAINER=$(docker container ls --format "table {{.ID}},{{.Names}}" \
| grep "${STACK_NAME}_${SERVICE}" | cut -d',' -f1)
if [ -z "$CONTAINER" ]; then
echo "$(tput setaf 1)ERROR: Can't find a ${STACK_NAME}_${SERVICE}$(tput sgr0)"
exit
fi
CP_ARGS=$(echo "$SOURCE $DEST" | sed "s/$SERVICE/$CONTAINER/")
# shellcheck disable=SC2086
docker cp $CP_ARGS
}
sub_context_help() {
echo "Usage: $PROGRAM_NAME [-a STACK_NAME] context <subcommand> [options]"
echo ""
echo "Subcommands:"
echo " init HOST [USER] [PORT] set up remote Docker context"
echo " use activate remote Docker context"
}
sub_context_init() {
HOST="$1"
USERNAME="$2"
PORT="$3"
if [ -n "$PORT" ]; then
PORT=":$PORT"
fi
if [ -n "$USERNAME" ]; then
USERNAME="$USERNAME@"
fi
docker context create "$HOST" \
--docker "host=ssh://$USERNAME$HOST$PORT"
}
sub_context_use() {
docker context use "$1"
}
sub_upgrade() {
curl -fsSL https://install.abra.autonomic.zone | bash
}
sub_context() {
SUBCOMMAND2=$1
shift
# shellcheck disable=SC2068
parse_subcommand "$SUBCOMMAND2" "context" $@
}
parse_subcommand() {
SUBCOMMAND="$1"
PREFIX=$2
if [ -n "$PREFIX" ]; then
PPREFIX="_$2"
SPREFIX="$2 "
SSPREFIX=" $2"
fi
case $SUBCOMMAND in
"" | "-h" | "--help")
"sub${PPREFIX}_help"
;;
*)
shift 2
"sub${PPREFIX}_${SUBCOMMAND}" "$@"
if [ $? = 127 ]; then
echo "Error: '$SPREFIX$SUBCOMMAND' is not a known subcommand." >&2
echo " Run '$PROGRAM_NAME$SSPREFIX --help' for a list of known subcommands." >&2
exit 1
fi
;;
esac
}
SUBCOMMAND=$1
shift
# shellcheck disable=SC2086,SC2068
parse_subcommand $SUBCOMMAND "" $@
+20
View File
@@ -0,0 +1,20 @@
package app
import (
"strings"
"coopcloud.tech/abra/pkg/i18n"
"github.com/spf13/cobra"
)
// 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"),
}
+339
View File
@@ -0,0 +1,339 @@
package app
import (
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
"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 backup list` aliases. use a comma separated list of aliases with
// no spaces in between
var appBackupListAliases = i18n.G("ls")
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()
},
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 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"),
)
}
+99
View File
@@ -0,0 +1,99 @@
package app
import (
"fmt"
"strings"
"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"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
)
// translators: `abra app check` aliases. use a comma separated list of aliases with
// no spaces in between
var appCheckAliases = i18n.G("chk")
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.
The goal is to ensure that recipe ".env.sample" env vars are defined in your
app ".env" file. Only env var definitions in the ".env.sample" which are
uncommented, e.g. "FOO=bar" are checked. If an app ".env" file does not include
these env vars, then "check" will complain.
Recipe maintainers may or may not provide defaults for env vars within their
recipes regardless of commenting or not (e.g. through the use of
${FOO:<default>} syntax). "check" does not confirm or deny this for you.`),
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)
}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
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 _, 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)
}
}
}
+65
View File
@@ -0,0 +1,65 @@
package app
import (
"os"
"os/exec"
"strings"
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/spf13/cobra"
)
// 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: 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 {
log.Fatal(err)
}
}
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)
}
},
}
+389
View File
@@ -0,0 +1,389 @@
package app
import (
"context"
"errors"
"io"
"os"
"path"
"path/filepath"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
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/spf13/cobra"
)
// translators: `abra app cp` aliases. use a comma separated list of aliases with
// no spaces in between
var appCpAliases = i18n.G("c")
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)
}
src := args[1]
dst := args[2]
srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst)
if err != nil {
log.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
container, err := containerPkg.GetContainerFromStackAndService(cl, app.StackName(), service)
if err != nil {
log.Fatal(err)
}
log.Debug(i18n.G("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server))
if toContainer {
err = CopyToContainer(cl, container.ID, srcPath, dstPath)
} else {
err = CopyFromContainer(cl, container.ID, srcPath, dstPath)
}
if err != nil {
log.Fatal(err)
}
},
}
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)
}
}
}
+440
View File
@@ -0,0 +1,440 @@
package app
import (
"context"
"errors"
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/secret"
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"
)
// 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 {
log.Fatal(err)
}
log.Debug(i18n.G("checking whether %s is already deployed", app.StackName()))
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
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)
}
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: stackName,
Prune: false,
ResolveImage: stack.ResolveImageAlways,
Detach: false,
}
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
if err != nil {
log.Fatal(err)
}
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
if internal.Chaos {
appPkg.SetChaosVersionLabel(compose, stackName, toDeployVersion)
}
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 {
log.Fatal(err)
}
for _, envVar := range envVars {
if !envVar.Present {
deployWarnMessages = append(deployWarnMessages,
i18n.G("%s missing from %s.env", envVar.Name, app.Domain),
)
}
}
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) {
versions, err := app.Recipe.Tags()
if err != nil {
return "", err
}
if len(versions) > 0 && !internal.Chaos {
return versions[len(versions)-1], 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"),
)
}
+341
View File
@@ -0,0 +1,341 @@
package app
import (
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"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"
)
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"`
}
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"`
}
// translators: `abra app list` aliases. use a comma separated list of aliases with
// no spaces in between
var appListAliases = i18n.G("ls")
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.
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 {
log.Fatal(err)
}
apps, err := appPkg.GetApps(appFiles, recipeFilter)
if err != nil {
log.Fatal(err)
}
sort.Sort(appPkg.ByServerAndRecipe(apps))
statuses := make(map[string]map[string]string)
if status {
alreadySeen := make(map[string]bool)
for _, app := range apps {
if _, ok := alreadySeen[app.Server]; !ok {
alreadySeen[app.Server] = true
}
}
statuses, err = appPkg.GetAppStatuses(apps, internal.MachineReadable)
if err != nil {
log.Fatal(err)
}
}
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++
}
}
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.EnsureUpToDate(); err != nil {
log.Warnf(
"Failed to ensure repo is up to date for recipe: %s err: %s",
app.Recipe.Name,
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.RegisterFlagCompletionFunc(
i18n.G("server"),
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.ServerNameComplete()
},
)
}
+113
View File
@@ -0,0 +1,113 @@
package app
import (
"context"
"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/logs"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/spf13/cobra"
)
// translators: `abra app logs` aliases. use a comma separated list of aliases with
// no spaces in between
var appLogsAliases = i18n.G("l")
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 {
return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
}
return autocomplete.ServiceNameComplete(app.Name)
default:
return nil, cobra.ShellCompDirectiveDefault
}
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
stackName := app.StackName()
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
log.Fatal(err)
}
if !deployMeta.IsDeployed {
log.Fatal(i18n.G("%s is not deployed?", app.Name))
}
var serviceNames []string
if len(args) == 2 {
serviceNames = []string{args[1]}
}
f, err := app.Filters(true, false, serviceNames...)
if err != nil {
log.Fatal(err)
}
opts := logs.TailOpts{
AppName: app.Name,
Services: serviceNames,
StdErr: stdErr,
Since: sinceLogs,
Filters: f,
}
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"),
)
}
+402
View File
@@ -0,0 +1,402 @@
package app
import (
"errors"
"fmt"
"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"
"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"
dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra"
)
var appNewDescription = i18n.G(`Creates a new app from a default recipe.
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 <domain>" to do so.
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
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.`)
// 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
}
},
Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name())
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
recipeVersions, _, err = recipe.GetRecipeVersions()
if err != nil {
log.Fatal(err)
}
}
if len(recipeVersions) > 0 {
latest := recipeVersions[len(recipeVersions)-1]
for tag := range latest {
recipeVersion = tag
}
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 {
log.Fatal(err)
}
composeFiles, err := recipe.GetComposeFiles(sampleEnv)
if err != nil {
log.Fatal(err)
}
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))
}
},
}
// 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
}
}
}
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
}
// ensureServerFlag checks if the server flag was used. if not, asks the user for it.
func ensureServerFlag() error {
servers, err := config.GetServers()
if err != nil {
return err
}
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: i18n.G("Select app server:"),
Options: servers,
}
if err := survey.AskOne(prompt, &newAppServer); err != nil {
return err
}
}
if newAppServer == "" {
return errors.New(i18n.G("no server provided"))
}
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"),
)
}
+217
View File
@@ -0,0 +1,217 @@
package app
import (
"context"
"encoding/json"
"fmt"
"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/config"
"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"
dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra"
)
// 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)
}
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))
}
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)
}
}
}
}
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"),
)
}
+167
View File
@@ -0,0 +1,167 @@
package app
import (
"context"
"os"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"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/spf13/cobra"
)
// translators: `abra app remove` aliases. use a comma separated list of aliases with
// no spaces in between
var appRemoveAliases = i18n.G("rm")
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.
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()
},
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))
response := false
prompt := &survey.Confirm{Message: i18n.G("are you sure?")}
if err := survey.AskOne(prompt, &response); err != nil {
log.Fatal(err)
}
if !response {
log.Fatal(i18n.G("aborting as requested"))
}
}
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 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)
var secretNames []string
for _, cont := range secretList {
secrets[cont.Spec.Annotations.Name] = cont.ID // we have to map the names to ID's
secretNames = append(secretNames, cont.Spec.Annotations.Name)
}
if len(secrets) > 0 {
for _, name := range secretNames {
err := cl.SecretRemove(context.Background(), secrets[name])
if err != nil {
log.Fatal(err)
}
log.Info(i18n.G("secret: %s removed", name))
}
} else {
log.Info(i18n.G("no secrets to remove"))
}
fs, err = app.Filters(false, true)
if err != nil {
log.Fatal(err)
}
volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, fs)
if err != nil {
log.Fatal(err)
}
volumeNames := client.GetVolumeNames(volumeList)
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 {
log.Info(i18n.G("no volumes to remove"))
}
if err = os.Remove(app.Path); err != nil {
log.Fatal(err)
}
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"),
)
}
+142
View File
@@ -0,0 +1,142 @@
package app
import (
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
"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 restore` aliases. use a comma separated list of
// aliases with no spaces in between
var appRestoreAliases = i18n.G("rs")
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.
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)
}
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 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"),
)
}
+381
View File
@@ -0,0 +1,381 @@
package app
import (
"errors"
"fmt"
"strings"
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"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra"
)
// 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
}
},
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)
}
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 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 {
if err := chooseDowngrade(availableDowngrades, deployMeta, &chosenDowngrade); err != nil {
log.Fatal(err)
}
}
if internal.Force &&
chosenDowngrade == "" &&
deployMeta.Version != config.UNKNOWN_DEFAULT {
chosenDowngrade = deployMeta.Version
}
if chosenDowngrade == "" {
log.Fatal(i18n.G("unknown deployed version, unable to downgrade"))
}
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,
}
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"),
)
}
+122
View File
@@ -0,0 +1,122 @@
package app
import (
"context"
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
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"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/spf13/cobra"
)
// translators: `abra app run` aliases. use a comma separated list of aliases
// with no spaces in between
var appRunAliases = i18n.G("r")
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
# 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
}
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
serviceName := args[1]
stackAndServiceName := fmt.Sprintf("^%s_%s", app.StackName(), serviceName)
filters := filters.NewArgs()
filters.Add("name", stackAndServiceName)
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, false)
if err != nil {
log.Fatal(err)
}
userCmd := args[2:]
execCreateOpts := containertypes.ExecOptions{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: userCmd,
Detach: false,
Tty: true,
}
if runAsUser != "" {
execCreateOpts.User = runAsUser
}
if noTTY {
execCreateOpts.Tty = false
}
dcli, err := command.NewDockerCli()
if err != nil {
log.Fatal(err)
}
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"),
)
}
+653
View File
@@ -0,0 +1,653 @@
package app
import (
"context"
"errors"
"fmt"
"io"
"os"
"sort"
"strconv"
"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/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"
dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra"
)
// 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 = &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)
}
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 {
log.Fatal(err)
}
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.Name, app.Server); err != nil {
log.Fatal(err)
}
}
}
if len(secretVals) == 0 {
log.Warn(i18n.G("no secrets generated"))
os.Exit(1)
}
headers := []string{i18n.G("NAME"), i18n.G("VALUE")}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
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")),
))
},
}
// translators: `abra app secret insert` aliases. use a comma separated list of aliases with
// no spaces in between
var appSecretInsertAliases = i18n.G("i")
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)
}
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(cl, secretName, data); err != nil {
log.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)
}
}
},
}
func readSecretData(args []string) (string, error) {
if len(args) == 4 {
return args[3], nil
}
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"))
}
log.Debug(i18n.G("reading secret data from stdin"))
bytes, err := io.ReadAll(os.Stdin)
if err != nil {
return "", errors.New(i18n.G("reading data from stdin: %s", err))
}
return string(bytes), nil
}
if internal.NoInput {
return "", errors.New(i18n.G("must provide <data> argument if --no-input is passed"))
}
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"),
}
} 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
}
// 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
}
log.Info(i18n.G("deleted %s successfully from server", secretName))
if removeFromPass {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
return err
}
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 {
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)
for _, cont := range secretList {
remoteSecretNames[cont.Spec.Annotations.Name] = true
}
var secretToRm string
if len(args) == 2 {
secretToRm = args[1]
}
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 !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
}
log.Warn(i18n.G("no secrets stored for %s", app.Name))
},
}
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)
}
}
},
}
+173
View File
@@ -0,0 +1,173 @@
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/config"
"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"
)
// 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)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
log.Debug(i18n.G("checking whether %s is already deployed", stackName))
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
log.Fatal(err)
}
if !deployMeta.IsDeployed {
log.Fatal(i18n.G("%s is not deployed?", app.Name))
}
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"),
)
}
+498
View File
@@ -0,0 +1,498 @@
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,
}
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,
fmt.Sprintf("no release notes available for %s", 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"),
)
}
+221
View File
@@ -0,0 +1,221 @@
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/upstream/stack"
"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra"
)
// translators: `abra app volume list` aliases. use a comma separated list of aliases with
// no spaces in between
var appVolumeListAliases = i18n.G("ls")
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 {
log.Fatal(err)
}
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)
}
return
}
log.Warn(i18n.G("no volumes created for %s", app.Name))
},
}
// translators: `abra app volume remove` aliases. use a comma separated list of aliases with
// no spaces in between
var appVolumeRemoveAliases = i18n.G("rm")
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 {
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 && !internal.NoInput {
volumesPrompt := &survey.MultiSelect{
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 {
log.Fatal(err)
}
}
if internal.Force || internal.NoInput {
volumesToRemove = volumeNames
}
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))
}
log.Info(i18n.G("%d volumes removed successfully", len(volumesToRemove)))
} else {
log.Info(i18n.G("no volumes removed"))
}
},
}
// 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"),
)
}
+322
View File
@@ -0,0 +1,322 @@
package catalogue
import (
"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"
)
// 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"),
)
}
+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)
}
},
}
+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
)
+146
View File
@@ -0,0 +1,146 @@
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()
if err != nil {
return err
}
cmd.Stderr = cmd.Stdout
done := make(chan struct{})
scanner := bufio.NewScanner(r)
go func() {
for scanner.Scan() {
line := scanner.Text()
fmt.Println(line)
}
done <- struct{}{}
}()
if err := cmd.Start(); err != nil {
return err
}
<-done
if err := cmd.Wait(); err != nil {
return err
}
return nil
}
+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: "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,
}
}
+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
}
+177
View File
@@ -0,0 +1,177 @@
package internal
import (
"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/AlecAivazis/survey/v2"
)
// ValidateRecipe ensures the recipe arg is valid.
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 == "" {
log.Fatal(i18n.G("no recipe name provided"))
}
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 {
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))
}
}
log.Debug(i18n.G("validated %s as recipe argument", recipeName))
return chosenRecipe
}
// ValidateApp ensures the app name arg is valid.
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 {
log.Fatal(err)
}
log.Debug(i18n.G("validated %s as app argument", appName))
return app
}
// ValidateDomain ensures the domain name arg is valid.
func ValidateDomain(args []string) string {
var domainName string
if len(args) > 0 {
domainName = args[0]
}
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"),
)
}
+149
View File
@@ -0,0 +1,149 @@
package recipe
import (
"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/lint"
"coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra"
)
// translators: `abra recipe lint` aliases. use a comma separated list of
// aliases with no spaces in between
var recipeLintAliases = i18n.G("l")
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)
}
headers := []string{
i18n.G("ref"),
i18n.G("rule"),
i18n.G("severity"),
i18n.G("satisfied"),
i18n.G("skipped"),
i18n.G("resolve"),
}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
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
}
}
satisfiedOutput := "✅"
if !satisfied {
satisfiedOutput = "❌"
if skipped {
satisfiedOutput = "-"
}
}
row := []string{
rule.Ref,
rule.Description,
rule.Level,
satisfiedOutput,
skippedOutput,
rule.HowToResolve,
}
rows = append(rows, row)
table.Row(row...)
}
}
if len(rows) > 0 {
if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
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"),
)
}
+116
View File
@@ -0,0 +1,116 @@
package recipe
import (
"fmt"
"sort"
"strconv"
"strings"
"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"
)
// 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 {
log.Fatal(err)
}
recipes := catl.Flatten()
sort.Sort(recipe.ByRecipeName(recipes))
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
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"),
}
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"),
)
}
+135
View File
@@ -0,0 +1,135 @@
package recipe
import (
"bytes"
"fmt"
"os"
"path"
"strings"
"text/template"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra"
)
// 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
}
// 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 := i18n.G("%s/example.git", config.REPOS_BASE_URL)
if err := git.Clone(r.Dir, url); err != nil {
log.Fatal(err)
}
gitRepo := path.Join(r.Dir, ".git")
if err := os.RemoveAll(gitRepo); err != nil {
log.Fatal(err)
}
log.Debug(i18n.G("removed .git repo in %s", gitRepo))
meta := newRecipeMeta(recipeName)
for _, path := range []string{r.ReadmePath, r.SampleEnvPath} {
tpl, err := template.ParseFiles(path)
if err != nil {
log.Fatal(err)
}
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)
}
}
if err := git.Init(r.Dir, true, gitName, gitEmail); err != nil {
log.Fatal(err)
}
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"),
)
}
+30
View File
@@ -0,0 +1,30 @@
package recipe
import (
"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 = &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.`),
}
+685
View File
@@ -0,0 +1,685 @@
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"
"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"
)
// 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.
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 release
eval ` + "`ssh-agent`" + `
ssh-add ~/.ssh/id_ed25519
abra recipe release gitea -p`,
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))
}
var tagString string
if len(args) == 2 {
tagString = args[1]
}
if tagString != "" {
if _, err := tagcmp.Parse(tagString); err != nil {
log.Fatal(i18n.G("cannot parse %s, invalid tag specified?", tagString))
}
}
if (internal.Major || internal.Minor || internal.Patch) && tagString != "" {
log.Fatal(i18n.G("cannot specify tag and bump type at the same time"))
}
repo, err := git.PlainOpen(recipe.Dir)
if err != nil {
log.Fatal(err)
}
preCommitHead, err := repo.Head()
if err != nil {
log.Fatal(err)
}
if tagString != "" {
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
if cleanErr := cleanTag(recipe, tagString); cleanErr != nil {
log.Fatal(cleanErr)
}
if cleanErr := cleanCommit(recipe, preCommitHead); cleanErr != nil {
log.Fatal(cleanErr)
}
log.Fatal(err)
}
}
tags, err := recipe.Tags()
if err != nil {
log.Fatal(err)
}
labelVersion, err := getLabelVersion(recipe, false)
if err != nil {
log.Fatal(err)
}
for _, tag := range tags {
previousTagLeftHand := strings.Split(tag, "+")[0]
newTagStringLeftHand := strings.Split(labelVersion, "+")[0]
if previousTagLeftHand == newTagStringLeftHand {
log.Fatal(i18n.G("%s+... conflicts with a previous release: %s", newTagStringLeftHand, tag))
}
}
if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
tagString = labelVersion
}
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 len(tags) > 0 {
log.Warn(i18n.G("previous git tags detected, assuming new semver release"))
if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil {
if cleanErr := cleanTag(recipe, tagString); cleanErr != nil {
log.Fatal(cleanErr)
}
if cleanErr := cleanCommit(recipe, preCommitHead); cleanErr != nil {
log.Fatal(cleanErr)
}
log.Fatal(err)
}
} else {
log.Warn(i18n.G("no tag specified and no previous tag available for %s, assuming initial release", recipe.Name))
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
if cleanErr := cleanTag(recipe, tagString); cleanErr != nil {
log.Fatal(cleanErr)
}
if cleanErr := cleanCommit(recipe, preCommitHead); cleanErr != nil {
log.Fatal(cleanErr)
}
log.Fatal(err)
}
}
return
},
}
// GetImageVersions retrieves image versions for a recipe
func GetImageVersions(recipe recipe.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 recipe.Recipe, tagString, mainAppVersion string) error {
var err error
repo, err := git.PlainOpen(recipe.Dir)
if err != nil {
return err
}
tag, err := tagcmp.Parse(tagString)
if err != nil {
return err
}
if tag.MissingMinor {
tag.Minor = "0"
tag.MissingMinor = false
}
if tag.MissingPatch {
tag.Patch = "0"
tag.MissingPatch = false
}
if tagString == "" {
tagString = fmt.Sprintf("%s+%s", tag.String(), mainAppVersion)
}
if err := addReleaseNotes(recipe, tagString); err != nil {
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 recipe.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 recipe.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 recipe.Recipe, tagString string) error {
if internal.Dry {
log.Info(i18n.G("dry run: no changes published"))
return nil
}
if !publish && !internal.NoInput {
prompt := &survey.Confirm{
Message: i18n.G("publish new release?"),
}
if err := survey.AskOne(prompt, &publish); err != nil {
return err
}
}
if publish {
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))
} else {
log.Info(i18n.G("no -p/--publish passed, not publishing"))
}
return nil
}
func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error {
repo, err := git.PlainOpen(recipe.Dir)
if err != nil {
return err
}
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 {
if (bumpType & (bumpType - 1)) != 0 {
return errors.New(i18n.G("you can only use one of: --major, --minor, --patch"))
}
}
var lastGitTag tagcmp.Tag
for _, tag := range tags {
parsed, err := tagcmp.Parse(tag)
if err != nil {
return err
}
if (lastGitTag == tagcmp.Tag{}) {
lastGitTag = parsed
} else if parsed.IsGreaterThan(lastGitTag) {
lastGitTag = parsed
}
}
newTag := lastGitTag
if internal.Patch {
now, err := strconv.Atoi(newTag.Patch)
if err != nil {
return err
}
newTag.Patch = strconv.Itoa(now + 1)
} else if internal.Minor {
now, err := strconv.Atoi(newTag.Minor)
if err != nil {
return err
}
newTag.Patch = "0"
newTag.Minor = strconv.Itoa(now + 1)
} else if internal.Major {
now, err := strconv.Atoi(newTag.Major)
if err != nil {
return err
}
newTag.Patch = "0"
newTag.Minor = "0"
newTag.Major = strconv.Itoa(now + 1)
}
if internal.Major || internal.Minor || internal.Patch {
newTag.Metadata = mainAppVersion
tagString = newTag.String()
}
if lastGitTag.String() == tagString {
return errors.New(i18n.G("latest git tag (%s) and synced label (%s) are the same?", lastGitTag, tagString))
}
if !internal.NoInput {
prompt := &survey.Confirm{
Message: i18n.G("current: %s, new: %s, correct?", lastGitTag, tagString),
}
var ok bool
if err := survey.AskOne(prompt, &ok); err != nil {
return err
}
if !ok {
return errors.New(i18n.G("exiting as requested"))
}
}
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
}
// cleanCommit soft removes the latest release commit. No change are lost the
// the commit itself is removed. This is the equivalent of `git reset HEAD~1`.
func cleanCommit(recipe recipe.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.MixedReset}
if err := worktree.Reset(opts); err != nil {
return errors.New(i18n.G("unable to soft reset %s: %s", recipe.Dir, err))
}
log.Debug(i18n.G("removed freshly created commit"))
return nil
}
// cleanTag removes a freshly created tag
func cleanTag(recipe recipe.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 getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
initTag, err := recipe.GetVersionLabelLocal()
if err != nil {
return "", err
}
if initTag == "" {
return "", errors.New(i18n.G("unable to read version for %s from synced label. Did you try running \"abra recipe sync %s\" already?", recipe.Name, recipe.Name))
}
log.Warn(i18n.G("discovered %s as currently synced recipe label", initTag))
if prompt && !internal.NoInput {
var response bool
prompt := &survey.Confirm{Message: i18n.G("use %s as the new version?", initTag)}
if err := survey.AskOne(prompt, &response); err != nil {
return "", err
}
if !response {
return "", errors.New(i18n.G("please fix your synced label for %s and re-run this command", recipe.Name))
}
}
return initTag, nil
}
var (
publish bool
)
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"),
)
RecipeReleaseCommand.Flags().BoolVarP(
&publish,
i18n.G("publish"),
i18n.G("p"),
false,
i18n.G("publish changes to git.coopcloud.tech"),
)
}
+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)
}
},
}
+313
View File
@@ -0,0 +1,313 @@
package recipe
import (
"errors"
"fmt"
"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/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/spf13/cobra"
)
// Errors
var emptyVersionsInCatalogue = errors.New(i18n.G("catalogue versions list is unexpectedly empty"))
// translators: `abra recipe reset` aliases. use a comma separated list of
// aliases with no spaces in between
var recipeSyncAliases = i18n.G("s")
var RecipeSyncCommand = &cobra.Command{
// translators: `recipe sync` command
Use: i18n.G("sync <recipe> [version] [flags]"),
Aliases: strings.Split(recipeSyncAliases, ","),
// translators: Short description for `recipe sync` command
Short: i18n.G("Sync recipe version label"),
Long: i18n.G(`Generate labels for the main recipe service.
By convention, the service named "app" using the following format:
coop-cloud.${STACK_NAME}.version=<version>
Where [version] can be specifed on the command-line or Abra can attempt to
auto-generate it for you. The <recipe> configuration will be updated on the
local file system.`),
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.ShellCompDirectiveError
}
},
Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name())
mainApp, err := internal.GetMainAppImage(recipe)
if err != nil {
log.Fatal(err)
}
imagesTmp, err := GetImageVersions(recipe)
if err != nil {
log.Fatal(err)
}
mainAppVersion := imagesTmp[mainApp]
tags, err := recipe.Tags()
if err != nil {
log.Fatal(err)
}
var nextTag string
if len(args) == 2 {
nextTag = args[1]
}
if len(tags) == 0 && nextTag == "" {
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)
}
nextTag = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion)
}
if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
var changeOverview string
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 != emptyVersionsInCatalogue {
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 nextTag == "" {
repo, err := git.PlainOpen(recipe.Dir)
if err != nil {
log.Fatal(err)
}
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))
nextTag = newTag.String()
}
if _, err := tagcmp.Parse(nextTag); err != nil {
log.Fatal(i18n.G("invalid version %s specified", nextTag))
}
mainService := "app"
label := i18n.G("coop-cloud.${STACK_NAME}.version=%s", nextTag)
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", nextTag, recipe.Name))
}
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)
}
}
},
}
func init() {
RecipeSyncCommand.Flags().BoolVarP(
&internal.Dry,
i18n.G("dry-run"),
i18n.G("r"),
false,
i18n.G("report changes that would be made"),
)
RecipeSyncCommand.Flags().BoolVarP(
&internal.Major,
i18n.G("major"),
i18n.G("x"),
false,
i18n.G("increase the major part of the version"),
)
RecipeSyncCommand.Flags().BoolVarP(
&internal.Minor,
i18n.G("minor"),
i18n.G("y"),
false,
i18n.G("increase the minor part of the version"),
)
RecipeSyncCommand.Flags().BoolVarP(
&internal.Patch,
i18n.G("patch"),
i18n.G("z"),
false,
i18n.G("increase the patch part of the version"),
)
}
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 "", emptyVersionsInCatalogue
}
+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, emptyVersionsInCatalogue)
}
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")
}
+386
View File
@@ -0,0 +1,386 @@
package recipe
import (
"bufio"
"encoding/json"
"fmt"
"os"
"path"
"sort"
"strings"
"coopcloud.tech/abra/cli/internal"
"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/distribution/reference"
"github.com/spf13/cobra"
)
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.
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())
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."))
}
}
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 {
log.Fatal(err)
}
regVersions, err := client.GetRegistryTags(img)
if err != nil {
log.Fatal(err)
}
image := reference.Path(img)
log.Debug(i18n.G("retrieved %s from remote registry for %s", regVersions, image))
image = formatter.StripTagMeta(image)
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 {
log.Warn(i18n.G("unable to parse %s, error was: %s, skipping upgrade for %s", image, err.Error(), service.Name))
continue
}
log.Debug(i18n.G("parsed %s for %s", tag, service.Name))
var compatible []tagcmp.Tag
for _, regVersion := range regVersions {
other, err := tagcmp.Parse(regVersion)
if err != nil {
continue // skip tags that cannot be parsed
}
if tag.IsCompatible(other) && tag.IsLessThan(other) && !tag.Equals(other) {
compatible = append(compatible, other)
}
}
log.Debug(i18n.G("detected potential upgradable tags %s for %s", compatible, service.Name))
sort.Sort(tagcmp.ByTagDesc(compatible))
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
}
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 {
if compat.String() == catlVersion {
skip = true
}
}
if !skip {
compatibleStrings = append(compatibleStrings, compat.String())
}
}
log.Debug(i18n.G("detected compatible upgradable tags %s for %s", compatibleStrings, service.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)
}
}
},
}
var (
allTags 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"),
)
}
+143
View File
@@ -0,0 +1,143 @@
package recipe
import (
"fmt"
"sort"
"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"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra"
)
// translators: `abra recipe versions` aliases. use a comma separated list of aliases
// with no spaces in between
var recipeVersionsAliases = i18n.G("v")
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 {
log.Fatal(err)
}
recipeMeta, ok := catl[recipe.Name]
if !ok {
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}
}
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 service, serviceMeta := range meta {
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
}
}
}
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"),
)
}
+320
View File
@@ -0,0 +1,320 @@
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,
"debug",
"d",
false,
i18n.G("show debug messages"),
)
rootCmd.PersistentFlags().BoolVarP(
&internal.NoInput,
"no-input",
"n",
false,
i18n.G("toggle non-interactive mode"),
)
rootCmd.PersistentFlags().BoolVarP(
&internal.Offline,
"offline",
"o",
false,
i18n.G("prefer offline & filesystem access"),
)
rootCmd.PersistentFlags().BoolVarP(
&internal.Help,
i18n.G("help"),
i18n.G("h"),
false,
i18n.G("help for abra"),
)
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.RecipeSyncCommand,
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)
}
}
+211
View File
@@ -0,0 +1,211 @@
package server
import (
"os"
"path/filepath"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
contextPkg "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/dns"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/server"
sshPkg "coopcloud.tech/abra/pkg/ssh"
"github.com/spf13/cobra"
)
// translators: `abra server add` aliases. use a comma separated list of
// aliases with no spaces in between
var serverAddAliases = i18n.GC("a", "server add")
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.
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:
Host 1312.net 1312
Hostname 1312.net
User antifa
Port 12345
IdentityFile ~/.ssh/antifa@somewhere
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"))
}
if len(args) == 0 && !local {
log.Fatal(i18n.G("missing argument or --local/-l flag"))
}
name := "default"
if !local {
name = internal.ValidateDomain(args)
}
// 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)
if local {
created, err := createServerDir(name)
if err != nil {
log.Fatal(err)
}
log.Debug(i18n.G("attempting to create client for %s", name))
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
}
_, err := createServerDir(name)
if err != nil {
log.Fatal(err)
}
created, err := newContext(name)
if err != nil {
cleanUp(name)
log.Fatal(i18n.G("unable to create local context: %s", err))
}
log.Debug(i18n.G("attempting to create client for %s", name))
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"),
)
}
+110
View File
@@ -0,0 +1,110 @@
package server
import (
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
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"
)
// 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 {
log.Fatal(err)
}
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 {
log.Fatal(err)
}
var rows [][]string
for _, serverName := range serverNames {
var row []string
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 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 {
if serverName == "default" {
row = []string{serverName, i18n.G("local")}
} else {
row = []string{serverName, i18n.G("unknown")}
}
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)
}
},
}
func init() {
ServerListCommand.Flags().BoolVarP(
&internal.MachineReadable,
i18n.G("machine"),
i18n.G("m"),
false,
i18n.G("print machine-readable output"),
)
}
+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"),
)
}
+54
View File
@@ -0,0 +1,54 @@
package server
import (
"os"
"path/filepath"
"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"
"github.com/spf13/cobra"
)
// translators: `abra server remove` aliases. use a comma separated list of
// aliases with no spaces in between
var serverRemoveAliases = i18n.G("rm")
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)
}
if err := os.RemoveAll(filepath.Join(config.SERVERS_DIR, serverName)); err != nil {
log.Fatal(err)
}
log.Info(i18n.G("%s is now lost in time, like tears in rain", serverName))
return
},
}
+21
View File
@@ -0,0 +1,21 @@
package server
import (
"strings"
"coopcloud.tech/abra/pkg/i18n"
"github.com/spf13/cobra"
)
// translators: `abra server` aliases. use a comma separated list of aliases
// with no spaces in between
var serverAliases = i18n.G("s")
// 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"),
}
+64
View File
@@ -0,0 +1,64 @@
// Package cli provides the interface for the command-line.
package cli
import (
"fmt"
"os/exec"
"strings"
"coopcloud.tech/abra/cli/internal"
"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 = &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)
}
},
}
var (
releaseCandidate bool
)
func init() {
UpgradeCommand.Flags().BoolVarP(
&releaseCandidate,
"rc",
"r",
false,
i18n.G("install release candidate (may contain bugs)"),
)
}
+23
View File
@@ -0,0 +1,23 @@
// Package main provides the command-line entrypoint.
package main
import (
"coopcloud.tech/abra/cli"
)
// Version is the current version of Abra.
var Version string
// Commit is the current git commit of Abra.
var Commit string
func main() {
if Version == "" {
Version = "dev"
}
if Commit == "" {
Commit = " "
}
cli.Run(Version, Commit)
}
+160
View File
@@ -0,0 +1,160 @@
module coopcloud.tech/abra
go 1.24.2
require (
coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca
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 v0.4.2
github.com/distribution/reference v0.6.0
github.com/docker/cli v29.1.5+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.16.4
github.com/google/go-cmp v0.7.0
github.com/leonelquinteros/gotext v1.7.2
github.com/moby/moby/client v0.2.1
github.com/moby/sys/signal v0.7.1
github.com/moby/term v0.5.2
github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.19.0
golang.org/x/term v0.39.0
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.2
)
require (
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.3.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // 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.1 // indirect
github.com/charmbracelet/x/ansi v0.11.4 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14 // 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.7.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.2 // 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/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.7.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.27.4 // 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 v1.4.0 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.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.19 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // 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.2.0 // indirect
github.com/moby/moby/api v1.53.0-rc.1 // 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/runc v1.1.13 // indirect
github.com/opencontainers/runtime-spec v1.1.0 // indirect
github.com/pjbgf/sha1cd v0.5.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.19.2 // 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
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.64.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 // indirect
google.golang.org/grpc v1.78.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.0 // 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.2
github.com/stretchr/testify v1.11.1
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
golang.org/x/sys v0.40.0
)
+1424
View File
File diff suppressed because it is too large Load Diff
+701
View File
@@ -0,0 +1,701 @@
package app
import (
"bufio"
"errors"
"fmt"
"os"
"path"
"regexp"
"sort"
"strings"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"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) (App, error) {
files, err := LoadAppFiles("")
if err != nil {
return App{}, err
}
app, err := GetApp(files, appName)
if err != nil {
return App{}, err
}
log.Debug(i18n.G("loaded app %s: %s", appName, app))
return app, nil
}
// 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))
}
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)
}
}
return apps, nil
}
// 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"]
}
stackName := StackName(a.Name)
a.Env["STACK_NAME"] = stackName
return stackName
}
// 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
}
// 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
}
+239
View File
@@ -0,0 +1,239 @@
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"
testPkg "coopcloud.tech/abra/pkg/test"
"github.com/docker/docker/api/types/filters"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
)
func TestNewApp(t *testing.T) {
app, err := appPkg.NewApp(testPkg.ExpectedAppEnv, testPkg.AppName, testPkg.ExpectedAppFile)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(app, testPkg.ExpectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, testPkg.ExpectedApp)
}
}
func TestReadAppEnvFile(t *testing.T) {
app, err := appPkg.ReadAppEnvFile(testPkg.ExpectedAppFile, testPkg.AppName)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(app, testPkg.ExpectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, testPkg.ExpectedApp)
}
}
func TestGetApp(t *testing.T) {
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(app, testPkg.ExpectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, testPkg.ExpectedApp)
}
}
func TestGetComposeFiles(t *testing.T) {
r := recipe.Get("abra-test-recipe")
err := r.EnsureExists()
if 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) {
r := recipe.Get("abra-test-recipe")
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) {
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
if err != nil {
t.Fatal(err)
}
defer 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(testPkg.ExpectedAppFiles, testPkg.AppName)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "foo", app.Recipe.EnvVersion)
}
func TestWriteRecipeVersionUnknown(t *testing.T) {
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.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
}
+62
View File
@@ -0,0 +1,62 @@
package app_test
import (
"testing"
appPkg "coopcloud.tech/abra/pkg/app"
testPkg "coopcloud.tech/abra/pkg/test"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/stretchr/testify/assert"
)
func TestGetTimeoutFromLabel(t *testing.T) {
testPkg.MkServerAppRecipe()
defer testPkg.RmServerAppRecipe()
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(testPkg.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
}
+87
View File
@@ -0,0 +1,87 @@
package catalogue
import (
"errors"
"fmt"
"os"
"path"
"strings"
"coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5"
)
// 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"))
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 nil
}
// 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 !isClean {
return errors.New(i18n.G("%s has locally unstaged changes? please commit/remove your changes before proceeding", config.CATALOGUE_DIR))
}
return nil
}
// EnsureUpToDate ensures that the local catalogue is up to date.
func EnsureUpToDate() error {
repo, err := git.PlainOpen(config.CATALOGUE_DIR)
if err != nil {
return err
}
remotes, err := repo.Remotes()
if err != nil {
return err
}
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
}
}
log.Debug(i18n.G("fetched latest git changes for %s", config.CATALOGUE_DIR))
return nil
}
+127
View File
@@ -0,0 +1,127 @@
// Package client provides Docker client initiatialisation functions.
package client
import (
"context"
"errors"
"net/http"
"os"
"path"
"strings"
"time"
"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"
)
// Conf is a Docker client configuration.
type Conf struct {
Timeout int
}
// Opt is a Docker client option.
type Opt func(c *Conf)
// 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
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,
IdleConnTimeout: 30 * time.Second,
},
}
clientOpts = append(clientOpts,
client.WithHTTPClient(httpClient),
client.WithHost(helper.Host),
client.WithDialContext(helper.Dialer),
)
}
version := os.Getenv("DOCKER_API_VERSION")
if version != "" {
clientOpts = append(clientOpts, client.WithVersion(version))
} else {
clientOpts = append(clientOpts, client.WithAPIVersionNegotiation())
}
cl, err := client.NewClientWithOpts(clientOpts...)
if err != nil {
return nil, err
}
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
}
+89
View File
@@ -0,0 +1,89 @@
package client
import (
"errors"
"fmt"
"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"
"github.com/docker/cli/cli/context/docker"
contextStore "github.com/docker/cli/cli/context/store"
)
type Context = contextStore.Metadata
// 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
}
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 := context.NewDefaultDockerContextStore()
contextMetadata := contextStore.Metadata{
Endpoints: make(map[string]interface{}),
Name: name,
}
contextTLSData := contextStore.ContextTLSData{
Endpoints: make(map[string]contextStore.EndpointTLSData),
}
dockerEP, dockerTLS, err := commandconnPkg.GetDockerEndpointMetadataAndTLS(host)
if err != nil {
return err
}
contextMetadata.Endpoints[docker.DockerEndpoint] = dockerEP
if dockerTLS != nil {
contextTLSData.Endpoints[docker.DockerEndpoint] = *dockerTLS
}
if err := s.CreateOrUpdate(contextMetadata); err != nil {
return err
}
if err := s.ResetTLSMaterial(name, &contextTLSData); err != nil {
return err
}
return nil
}
func DeleteContext(name string) error {
if name == "default" {
return errors.New(i18n.G("context 'default' cannot be removed"))
}
if _, err := GetContext(name); err != nil {
return err
}
cfg := dConfig.LoadDefaultConfigFile(nil)
cfg.CurrentContext = ""
if err := cfg.Save(); err != nil {
return err
}
return context.NewDefaultDockerContextStore().Remove(name)
}
func GetContext(contextName string) (contextStore.Metadata, error) {
ctx, err := context.NewDefaultDockerContextStore().GetMetadata(contextName)
if err != nil {
return contextStore.Metadata{}, err
}
return ctx, nil
}
+30
View File
@@ -0,0 +1,30 @@
package client
import (
"context"
"errors"
"fmt"
"coopcloud.tech/abra/pkg/i18n"
"github.com/containers/image/docker"
"github.com/containers/image/types"
"github.com/distribution/reference"
)
// GetRegistryTags retrieves all tags of an image from a container registry.
func GetRegistryTags(img reference.Named) ([]string, error) {
var tags []string
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()))
}
ctx := context.Background()
tags, err = docker.GetRepositoryTags(ctx, &types.SystemContext{}, ref)
if err != nil {
return tags, err
}
return tags, nil
}
+27
View File
@@ -0,0 +1,27 @@
package client
import (
"context"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
)
func StoreSecret(cl *client.Client, secretName, secretValue string) error {
ann := swarm.Annotations{Name: secretName}
spec := swarm.SecretSpec{Annotations: ann, Data: []byte(secretValue)}
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
}
+58
View File
@@ -0,0 +1,58 @@
package client
import (
"testing"
"github.com/docker/docker/api/types/swarm"
"github.com/stretchr/testify/assert"
)
func TestGetSecretNames(t *testing.T) {
tests := []struct {
name string
secrets []swarm.Secret
expected []string
description string
}{
{
name: "empty secrets list",
secrets: []swarm.Secret{},
expected: nil,
description: "should return nil for empty input",
},
{
name: "single secret",
secrets: []swarm.Secret{
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "database_password"}}},
},
expected: []string{"database_password"},
description: "should return single secret name",
},
{
name: "multiple secrets",
secrets: []swarm.Secret{
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "db_password"}}},
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "api_key"}}},
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "ssl_cert"}}},
},
expected: []string{"db_password", "api_key", "ssl_cert"},
description: "should return all secret names in order",
},
{
name: "secrets with empty names",
secrets: []swarm.Secret{
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: ""}}},
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "valid_name"}}},
},
expected: []string{"", "valid_name"},
description: "should include empty names if present",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetSecretNames(tt.secrets)
assert.Equal(t, tt.expected, result, tt.description)
})
}
}
+63
View File
@@ -0,0 +1,63 @@
package client
import (
"context"
"errors"
"time"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
)
func GetVolumes(cl *client.Client, ctx context.Context, server string, fs filters.Args) ([]*volume.Volume, error) {
volumeListOKBody, err := cl.VolumeList(ctx, volume.ListOptions{Filters: fs})
volumeList := volumeListOKBody.Volumes
if err != nil {
return volumeList, err
}
return volumeList, nil
}
func GetVolumeNames(volumes []*volume.Volume) []string {
var volumeNames []string
for _, vol := range volumes {
volumeNames = append(volumeNames, vol.Name)
}
return volumeNames
}
func RemoveVolumes(cl *client.Client, ctx context.Context, volumeNames []string, force bool, retries int) error {
for _, volName := range volumeNames {
err := retryFunc(5, func() error {
return cl.VolumeRemove(context.Background(), volName, force)
})
if err != nil {
return errors.New(i18n.G("volume %s: %s", volName, err))
}
}
return nil
}
// retryFunc retries the given function for the given retries. After the nth
// retry it waits (n + 1)^2 seconds before the next retry (starting with n=0).
// It returns an error if the function still failed after the last retry.
func retryFunc(retries int, fn func() error) error {
for i := 0; i < retries; i++ {
err := fn()
if err == nil {
return nil
}
if i+1 < retries {
sleep := time.Duration(i+1) * time.Duration(i+1)
log.Info(i18n.G("%s: waiting %d seconds before next retry", err, sleep))
time.Sleep(sleep * time.Second)
}
}
return errors.New(i18n.G("%d retries failed", retries))
}
+26
View File
@@ -0,0 +1,26 @@
package client
import (
"fmt"
"testing"
)
func TestRetryFunc(t *testing.T) {
err := retryFunc(1, func() error { return nil })
if err != nil {
t.Errorf("should not return an error: %s", err)
}
i := 0
fn := func() error {
i++
return fmt.Errorf("oh no, something went wrong!")
}
err = retryFunc(2, fn)
if err == nil {
t.Error("should return an error")
}
if i != 2 {
t.Errorf("The function should have been called 1 times, got %d", i)
}
}
+122
View File
@@ -0,0 +1,122 @@
package config
import (
"os"
"path"
"path/filepath"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"gopkg.in/yaml.v3"
)
// LoadAbraConfig returns the abra configuration. It tries to find a abra
// configuration file (see findAbraConfig for lookup logic). When no
// configuration was found it returns the default config.
func LoadAbraConfig() Abra {
wd, _ := os.Getwd()
configFile := findAbraConfig(wd)
if configFile == "" {
log.Debug(i18n.G("no config file found"))
return Abra{}
}
data, err := os.ReadFile(configFile)
if err != nil {
// Do nothing, when an error occurs
log.Debug(i18n.G("error reading config file: %s", err))
return Abra{}
}
config := Abra{}
err = yaml.Unmarshal(data, &config)
if err != nil {
// Do nothing, when an error occurs
log.Debug(i18n.G("error loading config file: %s", err))
return Abra{}
}
log.Debug(i18n.G("config file loaded from: %s", configFile))
config.configPath = filepath.Dir(configFile)
return config
}
// findAbraConfig recursively looks for a abra.y(a)ml file in the given directory.
// When the file was not found it calls the function again with the parent
// directory until the home directory is hit. When no abra config was found it
// returns an empty string.
func findAbraConfig(dir string) string {
dir, err := filepath.Abs(dir)
if err != nil {
return ""
}
if dir == os.ExpandEnv("$HOME") || dir == "/" {
return ""
}
p := path.Join(dir, "abra.yaml")
if _, err := os.Stat(p); err == nil {
return p
}
p = path.Join(dir, "abra.yml")
if _, err := os.Stat(p); err == nil {
return p
}
return findAbraConfig(filepath.Dir(dir))
}
// Abra defines the configuration file for abra.
type Abra struct {
configPath string
AbraDir string `yaml:"abraDir"`
}
// GetAbraDir returns the abra dir. It has the following logic:
// 1. check if $ABRA_DIR is set
// 2. check if abraDir was set in a config file
// 3. use $HOME/.abra when above two options failed
func (a Abra) GetAbraDir() string {
if dir, exists := os.LookupEnv("ABRA_DIR"); exists && dir != "" {
log.Debug(i18n.G("read abra dir from $ABRA_DIR"))
return dir
}
if a.AbraDir != "" {
log.Debug(i18n.G("read abra dir from config file"))
if path.IsAbs(a.AbraDir) {
return a.AbraDir
}
// Make the path absolute
return path.Join(a.configPath, a.AbraDir)
}
log.Debug(i18n.G("using default abra dir"))
return os.ExpandEnv("$HOME/.abra")
}
func (a Abra) GetServersDir() string { return path.Join(a.GetAbraDir(), "servers") }
func (a Abra) GetRecipesDir() string { return path.Join(a.GetAbraDir(), "recipes") }
func (a Abra) GetLogsDir() string { return path.Join(a.GetAbraDir(), "logs") }
func (a Abra) GetCatalogueDir() string { return path.Join(a.GetAbraDir(), "catalogue") }
var config = LoadAbraConfig()
var (
ABRA_DIR = config.GetAbraDir()
SERVERS_DIR = config.GetServersDir()
RECIPES_DIR = config.GetRecipesDir()
LOGS_DIR = config.GetLogsDir()
CATALOGUE_DIR = config.GetCatalogueDir()
RECIPES_JSON = path.Join(config.GetCatalogueDir(), "recipes.json")
REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json"
TOOLSHED_SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/toolshed/%s.git"
RECIPES_SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git"
// NOTE(d1): please note, this was done purely out of laziness on our part
// AFAICR. it's easy to punt the value into the label because that is what is
// expects. it's not particularly useful in terms of UI/UX but hey, nobody
// complained yet!
CHAOS_DEFAULT = "false"
DIRTY_DEFAULT = "+U"
MISSING_DEFAULT = "-"
UNKNOWN_DEFAULT = "unknown"
)
+133
View File
@@ -0,0 +1,133 @@
package config
import (
"log"
"os"
"path/filepath"
"testing"
)
func TestFindAbraConfig(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
tests := []struct {
Dir string
Config string
}{
{
Dir: "testdata/abraconfig1",
Config: filepath.Join(wd, "testdata/abraconfig1/abra.yaml"),
},
{
Dir: "testdata/abraconfig1/subdir",
Config: filepath.Join(wd, "testdata/abraconfig1/abra.yaml"),
},
{
Dir: "testdata/abraconfig2",
Config: filepath.Join(wd, "testdata/abraconfig2/abra.yml"),
},
{
Dir: "testdata/abraconfig2/subdir",
Config: filepath.Join(wd, "testdata/abraconfig2/abra.yml"),
},
{
Dir: "testdata",
Config: "",
},
}
for _, tc := range tests {
t.Run(tc.Dir, func(t *testing.T) {
config := findAbraConfig(tc.Dir)
if config != tc.Config {
t.Errorf("\nwant: %s\ngot: %s", tc.Config, config)
}
})
}
}
func TestLoadAbraConfigGetAbraDir(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
t.Setenv("ABRA_DIR", "")
t.Run("default", func(t *testing.T) {
cfg := LoadAbraConfig()
wantAbraDir := os.ExpandEnv("$HOME/.abra")
if cfg.GetAbraDir() != wantAbraDir {
t.Errorf("\nwant: %s\ngot: %s", wantAbraDir, cfg.GetAbraDir())
}
})
t.Run("from config file", func(t *testing.T) {
t.Cleanup(func() { os.Chdir(wd) })
err = os.Chdir(filepath.Join(wd, "testdata/abraconfig1"))
if err != nil {
log.Fatal(err)
}
cfg := LoadAbraConfig()
wantAbraDir := filepath.Join(wd, "testdata/abraconfig1/foobar")
if cfg.GetAbraDir() != wantAbraDir {
t.Errorf("\nwant: %s\ngot: %s", wantAbraDir, cfg.GetAbraDir())
}
})
t.Run("default when config file is empty", func(t *testing.T) {
t.Cleanup(func() { os.Chdir(wd) })
err := os.Chdir(filepath.Join(wd, "testdata/abraconfig2"))
if err != nil {
log.Fatal(err)
}
cfg := LoadAbraConfig()
wantAbraDir := os.ExpandEnv("$HOME/.abra")
if cfg.GetAbraDir() != wantAbraDir {
t.Errorf("\nwant: %s\ngot: %s", wantAbraDir, cfg.GetAbraDir())
}
})
t.Run("from env variable", func(t *testing.T) {
t.Setenv("ABRA_DIR", "foo")
cfg := LoadAbraConfig()
wantAbraDir := "foo"
if cfg.GetAbraDir() != wantAbraDir {
t.Errorf("\nwant: %s\ngot: %s", wantAbraDir, cfg.GetAbraDir())
}
})
}
func TestLoadAbraConfigServersDir(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
t.Setenv("ABRA_DIR", "")
t.Run("default", func(t *testing.T) {
cfg := LoadAbraConfig()
wantServersDir := os.ExpandEnv("$HOME/.abra/servers")
if cfg.GetServersDir() != wantServersDir {
t.Errorf("\nwant: %s\ngot: %s", wantServersDir, cfg.GetServersDir())
}
})
t.Run("from config file", func(t *testing.T) {
t.Cleanup(func() { os.Chdir(wd) })
err = os.Chdir(filepath.Join(wd, "testdata/abraconfig1"))
if err != nil {
log.Fatal(err)
}
cfg := LoadAbraConfig()
log.Println(cfg)
wantServersDir := filepath.Join(wd, "testdata/abraconfig1/foobar/servers")
if cfg.GetServersDir() != wantServersDir {
t.Errorf("\nwant: %s\ngot: %s", wantServersDir, cfg.GetServersDir())
}
})
}
+116
View File
@@ -0,0 +1,116 @@
package config
import (
"errors"
"io/fs"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
)
const MAX_SANITISED_APP_NAME_LENGTH = 45
const MAX_DOCKER_SECRET_LENGTH = 64
var BackupbotLabel = "coop-cloud.backupbot.enabled"
// GetServers retrieves all servers.
func GetServers() ([]string, error) {
var servers []string
servers, err := GetAllFoldersInDirectory(SERVERS_DIR)
if err != nil {
return servers, err
}
var filtered []string
for _, s := range servers {
if !strings.HasPrefix(s, ".") {
filtered = append(filtered, s)
}
}
log.Debug(i18n.G("retrieved %v servers: %s", len(filtered), filtered))
return filtered, nil
}
// ReadServerNames retrieves all server names.
func ReadServerNames() ([]string, error) {
serverNames, err := GetAllFoldersInDirectory(SERVERS_DIR)
if err != nil {
return nil, err
}
log.Debug(i18n.G("read %s from %s", strings.Join(serverNames, ","), SERVERS_DIR))
return serverNames, nil
}
// GetAllFilesInDirectory returns filenames of all files in directory
func GetAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
var realFiles []fs.FileInfo
files, err := ioutil.ReadDir(directory)
if err != nil {
return nil, err
}
for _, file := range files {
// Follow any symlinks
filePath := path.Join(directory, file.Name())
if filepath.Ext(strings.TrimSpace(filePath)) != ".env" {
continue
}
realPath, err := filepath.EvalSymlinks(filePath)
if err != nil {
log.Warn(i18n.G("broken symlink in your abra config folders: %s", filePath))
} else {
realFile, err := os.Stat(realPath)
if err != nil {
return nil, err
}
if !realFile.IsDir() {
realFiles = append(realFiles, file)
}
}
}
return realFiles, nil
}
// GetAllFoldersInDirectory returns both folder and symlink paths
func GetAllFoldersInDirectory(directory string) ([]string, error) {
var folders []string
files, err := ioutil.ReadDir(directory)
if err != nil {
return nil, err
}
if len(files) == 0 {
return nil, errors.New(i18n.G("directory is empty: %s", directory))
}
for _, file := range files {
// Check if file is directory or symlink
if file.IsDir() || file.Mode()&fs.ModeSymlink != 0 {
filePath := path.Join(directory, file.Name())
realDir, err := filepath.EvalSymlinks(filePath)
if err != nil {
log.Warn(i18n.G("broken symlink in your abra config folders: %s", filePath))
} else if stat, err := os.Stat(realDir); err == nil && stat.IsDir() {
// path is a directory
folders = append(folders, file.Name())
}
}
}
return folders, nil
}
+1
View File
@@ -0,0 +1 @@
abraDir: foobar
View File
View File
View File
+86
View File
@@ -0,0 +1,86 @@
package container
import (
"context"
"errors"
"fmt"
"strings"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2"
"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/client"
)
// GetContainer retrieves a container. If noInput is false and the retrievd
// count of containers does not match 1, then a prompt is presented to let the
// user choose. A count of 0 is handled gracefully.
func GetContainer(c context.Context, cl *client.Client, filters filters.Args, noInput bool) (types.Container, error) {
containerOpts := containerTypes.ListOptions{Filters: filters}
containers, err := cl.ContainerList(c, containerOpts)
if err != nil {
return types.Container{}, err
}
if len(containers) == 0 {
filter := filters.Get("name")[0]
return types.Container{}, errors.New(i18n.G("no containers matching the %v filter found?", filter))
}
if len(containers) > 1 {
var containersRaw []string
for _, container := range containers {
containerName := strings.Join(container.Names, " ")
trimmed := strings.TrimPrefix(containerName, "/")
created := formatter.HumanDuration(container.Created)
containersRaw = append(containersRaw, i18n.G("%s (created %v)", trimmed, created))
}
if noInput {
err := errors.New(i18n.G("expected 1 container but found %v: %s", len(containers), strings.Join(containersRaw, " ")))
return types.Container{}, err
}
log.Warnf(i18n.G("ambiguous container list received, prompting for input"))
var response string
prompt := &survey.Select{
Message: i18n.G("which container are you looking for?"),
Options: containersRaw,
}
if err := survey.AskOne(prompt, &response); err != nil {
return types.Container{}, err
}
chosenContainer := strings.TrimSpace(strings.Split(response, " ")[0])
for _, container := range containers {
containerName := strings.TrimSpace(strings.Join(container.Names, " "))
trimmed := strings.TrimPrefix(containerName, "/")
if trimmed == chosenContainer {
return container, nil
}
}
log.Fatal(i18n.G("failed to match chosen container"))
}
return containers[0], nil
}
// GetContainerFromStackAndService retrieves the container for the given stack and service.
func GetContainerFromStackAndService(cl *client.Client, stack, service string) (types.Container, error) {
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", stack, service))
container, err := GetContainer(context.Background(), cl, filters, true)
if err != nil {
return types.Container{}, err
}
return container, nil
}
+141
View File
@@ -0,0 +1,141 @@
package context
import (
"errors"
"os"
"coopcloud.tech/abra/pkg/i18n"
"github.com/docker/cli/cli/command"
dConfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/context"
"github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/store"
contextStore "github.com/docker/cli/cli/context/store"
cliflags "github.com/docker/cli/cli/flags"
dopts "github.com/docker/cli/opts"
"github.com/moby/moby/client"
)
func NewDefaultDockerContextStore() *command.ContextStoreWithDefault {
contextDir := dConfig.ContextStoreDir()
storeConfig := command.DefaultContextStoreConfig()
store := newContextStore(contextDir, storeConfig)
opts := &cliflags.ClientOptions{Context: "default"}
dockerContextStore := &command.ContextStoreWithDefault{
Store: store,
Resolver: func() (*command.DefaultContext, error) {
return resolveDefaultContext(opts, storeConfig)
},
}
return dockerContextStore
}
// resolveDefaultContext creates a Metadata for the current CLI invocation parameters
func resolveDefaultContext(opts *cliflags.ClientOptions, config store.Config) (*command.DefaultContext, error) {
contextTLSData := store.ContextTLSData{
Endpoints: make(map[string]store.EndpointTLSData),
}
contextMetadata := store.Metadata{
Endpoints: make(map[string]any),
Metadata: command.DockerContext{
Description: "",
},
Name: "default",
}
dockerEP, err := resolveDefaultDockerEndpoint(opts)
if err != nil {
return nil, err
}
contextMetadata.Endpoints[docker.DockerEndpoint] = dockerEP.EndpointMeta
if dockerEP.TLSData != nil {
contextTLSData.Endpoints[docker.DockerEndpoint] = *dockerEP.TLSData.ToStoreTLSData()
}
if err := config.ForeachEndpointType(func(n string, get store.TypeGetter) error {
if n == docker.DockerEndpoint { // handled above
return nil
}
ep := get()
if i, ok := ep.(command.EndpointDefaultResolver); ok {
meta, tls, err := i.ResolveDefault()
if err != nil {
return err
}
if meta == nil {
return nil
}
contextMetadata.Endpoints[n] = meta
if tls != nil {
contextTLSData.Endpoints[n] = *tls
}
}
// Nothing to be done
return nil
}); err != nil {
return nil, err
}
return &command.DefaultContext{Meta: contextMetadata, TLS: contextTLSData}, nil
}
// Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags)
func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint, error) {
// defaultToTLS determines whether we should use a TLS host as default
// if nothing was configured by the user.
defaultToTLS := opts.TLSOptions != nil
host, err := getServerHost(opts.Hosts, defaultToTLS)
if err != nil {
return docker.Endpoint{}, err
}
var (
skipTLSVerify bool
tlsData *context.TLSData
)
if opts.TLSOptions != nil {
skipTLSVerify = opts.TLSOptions.InsecureSkipVerify
tlsData, err = context.TLSDataFromFiles(opts.TLSOptions.CAFile, opts.TLSOptions.CertFile, opts.TLSOptions.KeyFile)
if err != nil {
return docker.Endpoint{}, err
}
}
return docker.Endpoint{
EndpointMeta: docker.EndpointMeta{
Host: host,
SkipTLSVerify: skipTLSVerify,
},
TLSData: tlsData,
}, nil
}
func getServerHost(hosts []string, defaultToTLS bool) (string, error) {
switch len(hosts) {
case 0:
return dopts.ParseHost(defaultToTLS, os.Getenv(client.EnvOverrideHost))
case 1:
return dopts.ParseHost(defaultToTLS, hosts[0])
default:
return "", errors.New("specify only one -H")
}
}
func GetContextEndpoint(ctx contextStore.Metadata) (string, error) {
endpointmeta, ok := ctx.Endpoints["docker"].(context.EndpointMetaBase)
if !ok {
err := errors.New(i18n.G("context lacks Docker endpoint"))
return "", err
}
return endpointmeta.Host, nil
}
func newContextStore(dir string, config contextStore.Config) contextStore.Store {
return contextStore.New(dir, config)
}
+316
View File
@@ -0,0 +1,316 @@
package deploy
import (
"context"
"errors"
"sort"
"strings"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret"
"github.com/distribution/reference"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types/swarm"
dockerClient "github.com/docker/docker/client"
)
// MergeAbraShEnv merges abra.sh env vars into the app env vars.
func MergeAbraShEnv(recipe recipe.Recipe, env envfile.AppEnv) error {
abraShEnv, err := envfile.ReadAbraShEnvVars(recipe.AbraShPath)
if err != nil {
return err
}
for k, v := range abraShEnv {
log.Debugf(i18n.G("read v:%s k: %s", v, k))
env[k] = v
}
return nil
}
// GetEntityNameAndVersion parses a full name like `app_example_com_someconf_v1` to extract name and version, ("someconf", "v1")
func GetEntityNameAndVersion(fullName string, stackName string) (string, string, error) {
name := strings.TrimPrefix(fullName, stackName+"_")
if lastUnderscore := strings.LastIndex(name, "_"); lastUnderscore != -1 {
return name[0:lastUnderscore], name[lastUnderscore+1:], nil
}
return "", "", errors.New(i18n.G("can't parse version from '%s'", fullName))
}
func GetSecretsForStack(cl *dockerClient.Client, app appPkg.App) (map[string]string, error) {
filters, err := app.Filters(false, false)
if err != nil {
return nil, err
}
// List all services in the stack
// NOTE: we could do cl.SecretList, but we want to know which secrets are actually attached
services, err := cl.ServiceList(context.Background(), swarm.ServiceListOptions{
Filters: filters,
})
if err != nil {
return nil, err
}
secrets := make(map[string]string)
for _, service := range services {
if service.Spec.TaskTemplate.ContainerSpec.Secrets != nil {
for _, secretRef := range service.Spec.TaskTemplate.ContainerSpec.Secrets {
secretName := secretRef.SecretName
if secretName == "" {
continue
}
secretBaseName, secretVersion, err := GetEntityNameAndVersion(secretName, app.StackName())
if err != nil {
log.Warn(err)
continue
}
existingSecretVersion, exists := secrets[secretBaseName]
if !exists {
// First time seeing this, add to map
secrets[secretBaseName] = secretVersion
} else {
// Just make sure the versions are the same..
if existingSecretVersion != secretVersion {
log.Warnf(i18n.G("different versions for secret '%s', '%s' and %s'", secretBaseName, existingSecretVersion, secretVersion))
}
}
}
}
}
return secrets, nil
}
// GetConfigsForStack retrieves all Docker configs attached to services in a given stack.
func GetConfigsForStack(cl *dockerClient.Client, app appPkg.App) (map[string]string, error) {
filters, err := app.Filters(false, false)
if err != nil {
return nil, err
}
// List all services in the stack
services, err := cl.ServiceList(context.Background(), swarm.ServiceListOptions{
Filters: filters,
})
if err != nil {
return nil, err
}
// Collect unique config names with versions
configs := make(map[string]string)
for _, service := range services {
if service.Spec.TaskTemplate.ContainerSpec != nil {
for _, configRef := range service.Spec.TaskTemplate.ContainerSpec.Configs {
configName := configRef.ConfigName
if configName == "" {
continue
}
configBaseName, configVersion, err := GetEntityNameAndVersion(configName, app.StackName())
if err != nil {
log.Warn(err)
continue
}
existingConfigVersion, ok := configs[configBaseName]
if !ok {
// First time seeing this, add to map
configs[configBaseName] = configVersion
} else {
// Just make sure the versions are the same..
if existingConfigVersion != configVersion {
log.Warnf(i18n.G("different versions for config '%s', '%s' and %s'", configBaseName, existingConfigVersion, configVersion))
}
}
}
}
}
return configs, nil
}
// GetImagesForStack retrieves all Docker images for services in a given stack.
func GetImagesForStack(cl *dockerClient.Client, app appPkg.App) (map[string]string, error) {
filters, err := app.Filters(false, false)
if err != nil {
return nil, err
}
// List all services in the stack
services, err := cl.ServiceList(context.Background(), swarm.ServiceListOptions{
Filters: filters,
})
if err != nil {
return nil, err
}
// Collect unique image names with versions
images := make(map[string]string)
for _, service := range services {
if service.Spec.TaskTemplate.ContainerSpec != nil {
imageName := service.Spec.TaskTemplate.ContainerSpec.Image
imageParsed, err := reference.ParseNormalizedNamed(imageName)
if err != nil {
log.Warn(err)
continue
}
imageBaseName := reference.Path(imageParsed)
imageTag := imageParsed.(reference.NamedTagged).Tag()
existingImageVersion, ok := images[imageBaseName]
if !ok {
// First time seeing this, add to map
images[imageBaseName] = imageTag
} else {
// Just make sure the versions are the same..
if existingImageVersion != imageTag {
log.Warnf(i18n.G("different versions for image '%s', '%s' and %s'", imageBaseName, existingImageVersion, imageTag))
}
}
}
}
return images, nil
}
func GatherSecretsForDeploy(cl *dockerClient.Client, app appPkg.App, showUnchanged bool) ([]string, error) {
// Get current secrets from existing deployment
currentSecrets, err := GetSecretsForStack(cl, app)
if err != nil {
return nil, err
}
log.Debugf(i18n.G("current secrets: %v", currentSecrets))
newSecrets, err := secret.PollSecretsStatus(cl, app)
if err != nil {
return nil, err
}
log.Debugf(i18n.G("new secrets: %v", newSecrets))
// Sort secrets to ensure reproducible output
sort.Slice(newSecrets, func(i, j int) bool {
return newSecrets[i].LocalName < newSecrets[j].LocalName
})
var secretInfo []string
for _, newSecret := range newSecrets {
if currentVersion, exists := currentSecrets[newSecret.LocalName]; exists {
if currentVersion == newSecret.Version {
if showUnchanged {
secretInfo = append(secretInfo, i18n.G("%s: %s (unchanged)", newSecret.LocalName, newSecret.Version))
}
} else {
secretInfo = append(secretInfo, i18n.G("%s: %s → %s", newSecret.LocalName, currentVersion, newSecret.Version))
}
} else {
secretInfo = append(secretInfo, i18n.G("%s: %s (new)", newSecret.LocalName, newSecret.Version))
}
}
return secretInfo, nil
}
func GatherConfigsForDeploy(cl *dockerClient.Client, app appPkg.App, compose *composetypes.Config, abraShEnv map[string]string, showUnchanged bool) ([]string, error) {
// Get current configs from existing deployment
currentConfigs, err := GetConfigsForStack(cl, app)
if err != nil {
return nil, err
}
log.Debugf(i18n.G("deployed config names: %v", currentConfigs))
// Get new configs from the compose specification
newConfigs := compose.Configs
var configInfo []string
for configName := range newConfigs {
log.Debugf(i18n.G("searching abra.sh for version for %s", configName))
versionKey := strings.ToUpper(configName) + "_VERSION"
newVersion, exists := abraShEnv[versionKey]
if !exists {
log.Warnf(i18n.G("no version found for config %s", configName))
configInfo = append(configInfo, i18n.G("%s: ? (missing version)", configName))
continue
}
if currentVersion, exists := currentConfigs[configName]; exists {
if currentVersion == newVersion {
if showUnchanged {
configInfo = append(configInfo, i18n.G("%s: %s (unchanged)", configName, newVersion))
}
} else {
configInfo = append(configInfo, i18n.G("%s: %s → %s", configName, currentVersion, newVersion))
}
} else {
configInfo = append(configInfo, i18n.G("%s: %s (new)", configName, newVersion))
}
}
return configInfo, nil
}
func GatherImagesForDeploy(cl *dockerClient.Client, app appPkg.App, compose *composetypes.Config, showUnchanged bool) ([]string, error) {
// Get current images from existing deployment
currentImages, err := GetImagesForStack(cl, app)
if err != nil {
return nil, err
}
log.Debugf(i18n.G("deployed images: %v", currentImages))
// Proposed new images from the compose files
newImages := make(map[string]string)
for _, service := range compose.Services {
imageParsed, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
log.Warn(err)
continue
}
imageBaseName := reference.Path(imageParsed)
imageTag := imageParsed.(reference.NamedTagged).Tag()
existingImageVersion, ok := newImages[imageBaseName]
if !ok {
// First time seeing this, add to map
newImages[imageBaseName] = imageTag
} else {
// Just make sure the versions are the same..
if existingImageVersion != imageTag {
log.Warnf(i18n.G("different versions for image '%s', '%s' and %s'", imageBaseName, existingImageVersion, imageTag))
}
}
}
log.Debugf(i18n.G("proposed images: %v", newImages))
var imageInfo []string
for newImageName, newImageVersion := range newImages {
if currentVersion, exists := currentImages[newImageName]; exists {
if currentVersion == newImageVersion {
if showUnchanged {
imageInfo = append(imageInfo, i18n.G("%s: %s (unchanged)", formatter.StripTagMeta(newImageName), newImageVersion))
}
} else {
imageInfo = append(imageInfo, i18n.G("%s: %s → %s", formatter.StripTagMeta(newImageName), currentVersion, newImageVersion))
}
} else {
imageInfo = append(imageInfo, i18n.G("%s: %s (new)", formatter.StripTagMeta(newImageName), newImageVersion))
}
}
return imageInfo, nil
}
+89
View File
@@ -0,0 +1,89 @@
package deploy
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetEntityNameAndVersion(t *testing.T) {
tests := []struct {
name string
fullName string
stackName string
expected string
expectedVer string
expectError bool
}{
{
name: "valid config with version",
fullName: "myapp_database_v2",
stackName: "myapp",
expected: "database",
expectedVer: "v2",
expectError: false,
},
{
name: "valid config with numeric version",
fullName: "myapp_redis_1",
stackName: "myapp",
expected: "redis",
expectedVer: "1",
expectError: false,
},
{
name: "config without underscore in name",
fullName: "myapp_db_v1",
stackName: "myapp",
expected: "db",
expectedVer: "v1",
expectError: false,
},
{
name: "config with multiple underscores",
fullName: "myapp_my_database_v3",
stackName: "myapp",
expected: "my_database",
expectedVer: "v3",
expectError: false,
},
{
name: "invalid config - no version",
fullName: "myapp_database",
stackName: "myapp",
expectError: true,
},
{
name: "empty config name",
fullName: "myapp__v1",
stackName: "myapp",
expected: "",
expectedVer: "v1",
expectError: false,
},
{
name: "wrong stack prefix",
fullName: "otherapp_database_v1",
stackName: "myapp",
expected: "otherapp_database",
expectedVer: "v1",
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
name, version, err := GetEntityNameAndVersion(tt.fullName, tt.stackName)
if tt.expectError {
assert.Error(t, err)
assert.Empty(t, name)
assert.Empty(t, version)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, name)
assert.Equal(t, tt.expectedVer, version)
}
})
}
}
+60
View File
@@ -0,0 +1,60 @@
package dns
import (
"errors"
"net"
"coopcloud.tech/abra/pkg/i18n"
)
// EnsureIPv4 ensures that an ipv4 address is set for a domain name
func EnsureIPv4(domainName string) (string, error) {
ipv4, err := net.ResolveIPAddr("ip4", domainName)
if err != nil {
return "", errors.New(i18n.G("%s: unable to resolve IPv4 address: %s", domainName, err))
}
if ipv4 == nil {
return "", errors.New(i18n.G("%s: no IPv4 available", domainName))
}
return ipv4.String(), nil
}
// EnsureDomainsResolveSameIPv4 ensures that domains resolve to the same ipv4 address
func EnsureDomainsResolveSameIPv4(domainName, server string) (string, error) {
if server == "default" || server == "local" {
return "", nil
}
var ipv4 string
domainIPv4, err := EnsureIPv4(domainName)
if err != nil {
return ipv4, err
}
if domainIPv4 == "" {
return ipv4, errors.New(i18n.G("cannot resolve ipv4 for %s?", domainName))
}
serverIPv4, err := EnsureIPv4(server)
if err != nil {
return ipv4, err
}
if serverIPv4 == "" {
return ipv4, errors.New(i18n.G("cannot resolve ipv4 for %s?", server))
}
if domainIPv4 != serverIPv4 {
return ipv4, errors.New(
i18n.G(
"app domain %s (%s) does not appear to resolve to app server %s (%s)?",
domainName, domainIPv4, server, serverIPv4,
),
)
}
return ipv4, nil
}
+64
View File
@@ -0,0 +1,64 @@
package dns
import (
"fmt"
"testing"
"gotest.tools/v3/assert"
)
func TestEnsureDomainsResolveSameIPv4(t *testing.T) {
tests := []struct {
domainName string
serverName string
shouldValidate bool
}{
// NOTE(d1): DNS records get checked, so use something that is maintained
// within the federation. if you're here because of a failing test, try
// `dig +short <domain>` to ensure stuff matches first! If flakyness
// becomes an issue we can look into mocking
{"docs.coopcloud.tech", "swarm-0.coopcloud.tech", true},
{"docs.coopcloud.tech", "coopcloud.tech", true},
// NOTE(d1): special case handling for "--local"
{"", "default", true},
{"", "local", true},
{"", "", false},
{"123", "", false},
}
for _, test := range tests {
_, err := EnsureDomainsResolveSameIPv4(test.domainName, test.serverName)
if err != nil && test.shouldValidate {
t.Fatal(err)
}
if err == nil && !test.shouldValidate {
t.Fatal(fmt.Errorf("should have failed but did not: %v", test))
}
}
}
func TestEnsureIpv4(t *testing.T) {
// NOTE(d1): DNS records get checked, so use something that is maintained
// within the federation. if you're here because of a failing test, try `dig
// +short <domain>` to ensure stuff matches first! If flakyness becomes an
// issue we can look into mocking
domainName := "collabora.ostrom.collective.tools"
serverName := "ostrom.collective.tools"
for i := 0; i < 15; i++ {
domainIpv4, err := EnsureIPv4(domainName)
if err != nil {
t.Fatal(err)
}
serverIpv4, err := EnsureIPv4(serverName)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, domainIpv4, serverIpv4)
}
}
+91
View File
@@ -0,0 +1,91 @@
package envfile
import (
"bufio"
"errors"
"os"
"regexp"
"strings"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"git.coopcloud.tech/toolshed/godotenv"
)
// AppEnv is a map of the values in an apps env config
type AppEnv = map[string]string
// AppModifiers is a map of modifiers in an apps env config
type AppModifiers = map[string]map[string]string
// ReadEnv loads an app envivornment into a map.
func ReadEnv(filePath string) (AppEnv, error) {
var envVars AppEnv
envVars, _, err := godotenv.Read(filePath)
if err != nil {
return nil, err
}
return envVars, nil
}
// ReadEnv loads an app envivornment and their modifiers in two different maps.
func ReadEnvWithModifiers(filePath string) (AppEnv, AppModifiers, error) {
var envVars AppEnv
envVars, mods, err := godotenv.Read(filePath)
if err != nil {
return nil, mods, err
}
log.Debug(i18n.G("read %s from %s", envVars, filePath))
return envVars, mods, nil
}
// ReadAbraShEnvVars reads env vars from an abra.sh recipe file.
func ReadAbraShEnvVars(abraSh string) (map[string]string, error) {
envVars := make(map[string]string)
file, err := os.Open(abraSh)
if err != nil {
if os.IsNotExist(err) {
return envVars, nil
}
return envVars, err
}
defer file.Close()
exportRegex, err := regexp.Compile(`^export\s+(\w+=\w+)`)
if err != nil {
return envVars, err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
txt := scanner.Text()
if exportRegex.MatchString(txt) {
splitVals := strings.Split(txt, "export ")
envVarDef := splitVals[len(splitVals)-1]
keyVal := strings.Split(envVarDef, "=")
if len(keyVal) != 2 {
return envVars, errors.New(i18n.G("couldn't parse %s", txt))
}
envVars[keyVal[0]] = keyVal[1]
}
}
if len(envVars) > 0 {
log.Debug(i18n.G("read %s from %s", envVars, abraSh))
} else {
log.Debug(i18n.G("read 0 env var exports from %s", abraSh))
}
return envVars, nil
}
type EnvVar struct {
Name string
Present bool
}

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