Compare commits

..

1135 Commits

Author SHA1 Message Date
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
155 changed files with 19043 additions and 2488 deletions
+50 -29
View File
@@ -1,43 +1,64 @@
---
kind: pipeline
name: linters
name: coopcloud.tech/abra
steps:
- name: run shellcheck
image: koalaman/shellcheck-alpine:v0.7.1
- name: make check
image: golang:1.18
commands:
- shellcheck abra
- make check
- name: run unit tests
image: decentral1se/docker-dind-bats-kcov
- name: make build
image: golang:1.18
commands:
- bats tests
- make build
- name: collect code coverage
failure: ignore # until we fix this
image: decentral1se/docker-dind-bats-kcov
- name: make test
image: golang:1.18
commands:
- kcov . bats tests || true
- make test
- name: send code coverage report to codecov
failure: ignore # until we fix this
image: plugins/codecov
- name: notify on failure
image: plugins/matrix
settings:
token:
from_secret: codecov_token
required: true
- name: notify rocket chat
image: plugins/slack
settings:
webhook:
from_secret: rc_builds_url
username: comradebritney
channel: "internal.builds"
template: "{{repo.owner}}/{{repo.name}} build failed: {{build.link}}"
homeserver: https://matrix.autonomic.zone
roomid: "IFazIpLtxiScqbHqoa:autonomic.zone"
userid: "@autono-bot:autonomic.zone"
accesstoken:
from_secret: autono_bot_access_token
depends_on:
- make check
- make build
- make test
when:
status:
- failure
trigger:
branch:
- main
- name: fetch
image: docker:git
commands:
- git fetch --tags
depends_on:
- make check
- make build
- make test
when:
event: tag
- name: release
image: golang:1.18
environment:
GITEA_TOKEN:
from_secret: goreleaser_gitea_token
volumes:
- name: deps
path: /go
commands:
- curl -sL https://git.io/goreleaser | bash
depends_on:
- fetch
when:
event: tag
volumes:
- name: deps
temp: {}
+4
View File
@@ -0,0 +1,4 @@
GANDI_TOKEN=...
HCLOUD_TOKEN=...
REGISTRY_PASSWORD=...
REGISTRY_USERNAME=...
+6
View File
@@ -0,0 +1,6 @@
go env -w GOPRIVATE=coopcloud.tech
# export PASSWORD_STORE_DIR=$(pwd)/../../autonomic/passwords/passwords/
# export HCLOUD_TOKEN=$(pass show logins/hetzner/cicd/api_key)
# export CAPSUL_TOKEN=...
# export GITEA_TOKEN=...
+8
View File
@@ -0,0 +1,8 @@
---
name: "Do not use this issue tracker"
about: "Do not use this issue tracker"
title: "Do not use this issue tracker"
labels: []
---
Please report your issue on [`coop-cloud/organising`](https://git.coopcloud.tech/coop-cloud/organising)
+8 -2
View File
@@ -1,2 +1,8 @@
coverage/
/.venv
*fmtcoverage.html
.e2e.env
.envrc
.vscode/
abra
dist/
tests/integration/.abra/catalogue
vendor/
+46
View File
@@ -0,0 +1,46 @@
---
project_name: abra
gitea_urls:
api: https://git.coopcloud.tech/api/v1
download: https://git.coopcloud.tech/
skip_tls_verify: false
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
dir: cmd/abra
goos:
- linux
- darwin
goarch:
- 386
- amd64
- arm
- arm64
goarm:
- 5
- 6
- 7
ldflags:
- "-X 'main.Commit={{ .Commit }}'"
- "-X 'main.Version={{ .Version }}'"
archives:
- replacements:
386: i386
amd64: x86_64
format: binary
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: desc
filters:
exclude:
- "^WIP:"
- "^style:"
- "^test:"
- "^tests:"
- "^Revert"
+10
View File
@@ -0,0 +1,10 @@
# authors
> If you're looking at this and you hack on Abra and you're not listed here,
> please do add yourself! This is a community project, let's show
- 3wordchant
- decentral1se
- kawaiipunk
- knoflook
- roxxers
-82
View File
@@ -1,82 +0,0 @@
# abra x.x.x (UNRELEASED)
# abra 0.6.0 (2021-03-17)
- Show version and digest of app if labelled ([98e674b8e8](https://git.autonomic.zone/coop-cloud/abra/commit/98e674b8e83458a83dcbf331e8e34c7188559c4a))
- Implement basic version checking on deployment ([#82](https://git.autonomic.zone/coop-cloud/abra/issues/82))
- New `app-catalogue.sh` script to auto-generate app list for documentation ([f163d4b](https://git.autonomic.zone/coop-cloud/abra/commit/f163d4b0fa920232e9d995a22d20fe78b174b3a9))
- Support app service rollbacks with `abra <app> rollback <service>` ([#76](https://git.autonomic.zone/coop-cloud/abra/issues/76))
- Detect when latest version is deployed and perform a no-op ([#87](https://git.autonomic.zone/coop-cloud/abra/issues/87))
- Allow cloning of app repos with different main branches using `-b, --branch=<branch>` ([#80](https://git.autonomic.zone/coop-cloud/abra/issues/80))
- Protect against lengthy app names which gives Docker trouble later on ([#83](https://git.autonomic.zone/coop-cloud/abra/issues/83))
- Support removal of secrets and volumes when `rm`'ing apps ([#44](https://git.autonomic.zone/coop-cloud/abra/issues/44))
- Always choose the default IPv4 address with `abra server <host> init` ([#91](https://git.autonomic.zone/coop-cloud/abra/issues/91))
- Add `--type=<type>` filtering option to `abra <app> ls` ([0828189](https://git.autonomic.zone/coop-cloud/abra/commit/0828189))
- Check for bash 4+ ([#96](https://git.autonomic.zone/coop-cloud/abra/commit/0828189))
- Add `--dev` option to installer using `git clone` ([88d2a75](https://git.autonomic.zone/coop-cloud/abra/commit/88d2a75))
- Support `--dev` on the `abra upgrade` command also ([bcc15ec](https://git.autonomic.zone/coop-cloud/abra/commit/bcc15ec))
- Vendor [yq](https://github.com/mikefarah/yq/releases) automatically ([3b59adf](https://git.autonomic.zone/coop-cloud/abra/commit/3b59adf))
- Extend version handling logic to support all underlying services ([#90](https://git.autonomic.zone/coop-cloud/abra/issues/90))
- Fix development installation script symlink issue ([#98](https://git.autonomic.zone/coop-cloud/abra/issues/98))
- Add `app-version.sh` script to help packagers version apps ([28618bd](https://git.autonomic.zone/coop-cloud/abra/commit/28618bd))
- Add git digest to `abra version` output ([8b41416](https://git.autonomic.zone/coop-cloud/abra/commit/8b41416))
# abra 0.5.0 (2021-03-01)
- `secret auto` merged into `secret generate` and `app new --auto` is now `app new --secrets` ([#64](https://git.autonomic.zone/coop-cloud/abra/pulls/64))
- Avoid outputting length during secret generation when not in use ([#67](https://git.autonomic.zone/coop-cloud/abra/issues/67))
- Support graceful failure when missing secret generation commands ([44d3ac3](https://git.autonomic.zone/coop-cloud/abra/commit/44d3ac3a1cb86edc9b9e91eea1a00e70eae14965))
- Fix secret detection when using new `.env` file format in apps ([5532452](https://git.autonomic.zone/coop-cloud/abra/commit/55324524ca77141666ffe6cc41b62cc71cf89ace))
- Support choosing an `$EDITOR` when editing configs ([29cc392](https://git.autonomic.zone/coop-cloud/abra/commit/29cc392dff3e93e48e0e2edd3ce11b405c66a95a))
- "server" shell completion fixed ([8839bd4](https://git.autonomic.zone/coop-cloud/abra/commit/8839bd45951d00dccf4ef81ece445bcc49e13ee6))
- Drop `multilogs` command ([#56](https://git.autonomic.zone/coop-cloud/abra/pulls/56))
- Remove `server use` command ([#51](https://git.autonomic.zone/coop-cloud/abra/issues/51))
- `new <app>` becomes `new <type>` ([#48](https://git.autonomic.zone/coop-cloud/abra/issues/48))
- `check` is run on `deploy` now and configurable ([77ba565](https://git.autonomic.zone/coop-cloud/abra/commit/77ba5652b2fe15820f5edfa0f642636f7b8eae7e))
- App configurations are always updated now ([#42](https://git.autonomic.zone/coop-cloud/abra/issues/42))
- We use docker format `.env` files (no "export" syntax) from now now ([#55](https://git.autonomic.zone/coop-cloud/abra/pulls/55))
- Rename `<domain>` option to `<app>` and `APP` variable to `TYPE`, see ([#47](https://git.autonomic.zone/coop-cloud/abra/issues/47))
- Use Docker-in-Docker (dind), and `dind-bats-kcov` Docker image, for `make test` ([1600b62](https://git.autonomic.zone/coop-cloud/abra/commit/1600b6277fbbffc4c6de1e4ba799c7bbe72ec6a0))
- Add built-in documentation using `abra help <subcommand>...`, see ([#50](https://git.autonomic.zone/coop-cloud/abra/issues/50))
- `version` subcommand ([e6b24fe](https://git.autonomic.zone/coop-cloud/abra/commit/e6b24fe))
- Use `# length=x` comments to generate passwords with `pwgen` and drop `KEY`/`PASSWORD` logic ([#68](https://git.autonomic.zone/coop-cloud/abra/issues/68))
- Global `--skip-update|-U` / `--skip-check|-C` options to make things quicker ([37e8b00](https://git.autonomic.zone/coop-cloud/abra/commit/37e8b00))
- `app backup` and `app restore` commands; requires per-app definition ([#70](https://git.autonomic.zone/coop-cloud/abra/issues/70))
- Rename per-type `abra-commands.sh` to `abra.sh`, and include config versions as type-level instead of app-level config ([#43](https://git.autonomic.zone/coop-cloud/abra/issues/43))
- Show per-subcommand help by adding `-h/--help` to a command line ([#38](https://git.autonomic.zone/coop-cloud/abra/issues/78))
# abra 0.4.1 (2020-12-24)
- Bug-fixes on `app ls --status` & custom commands
- Add `app ls --server=...` and alias
# abra 0.4.0 (2020-12-24)
- New command-line interface based on docopt
- `~/.abra` directory instead of expecting local `.env` files
- Integration tests & code coverage
# abra 0.3.1 (2020-09-27)
- Fix installer version
# abra 0.3.0 (2020-09-27)
- Add multilogs stack logs implementation ([#8](https://git.autonomic.zone/compose-stacks/abra/issues/8))
- Add beginnings of "monorepo" functionality
# 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
+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/>.
+42
View File
@@ -0,0 +1,42 @@
ABRA := ./cmd/abra
COMMIT := $(shell git rev-list -1 HEAD)
GOPATH := $(shell go env GOPATH)
LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
DIST_LDFLAGS := $(LDFLAGS)" -s -w"
export GOPRIVATE=coopcloud.tech
all: format check build test
run:
@go run -ldflags=$(LDFLAGS) $(ABRA)
install:
@go install -ldflags=$(LDFLAGS) $(ABRA)
build-dev:
@go build -ldflags=$(LDFLAGS) $(ABRA)
build:
@go build -ldflags=$(DIST_LDFLAGS) $(ABRA)
clean:
@rm '$(GOPATH)/bin/abra'
format:
@gofmt -s -w .
check:
@test -z $$(gofmt -l .) || (echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1)
test:
@go test ./... -cover -v
loc:
@find . -name "*.go" | xargs wc -l
loc-author:
@git ls-files -z | \
xargs -0rn 1 -P "$$(nproc)" -I{} sh -c 'git blame -w -M -C -C --line-porcelain -- {} | grep -I --line-buffered "^author "' | \
sort -f | \
uniq -ic | \
sort -n
+7 -52
View File
@@ -1,57 +1,12 @@
# abra
# `abra`
[![Build Status](https://drone.autonomic.zone/api/badges/coop-cloud/abra/status.svg)](https://drone.autonomic.zone/coop-cloud/abra)
[![codecov](https://codecov.io/gh/Autonomic-Cooperative/abra/branch/main/graph/badge.svg?token=aX3I5NMRsj)](undefined)
[![Build Status](https://build.coopcloud.tech/api/badges/coop-cloud/abra/status.svg?ref=refs/heads/main)](https://build.coopcloud.tech/coop-cloud/abra)
[![Go Report Card](https://goreportcard.com/badge/git.coopcloud.tech/coop-cloud/abra)](https://goreportcard.com/report/git.coopcloud.tech/coop-cloud/abra)
> https://cloud.autonomic.zone
The Co-op Cloud utility belt 🎩🐇
The cooperative cloud utility belt 🎩🐇
<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>
`abra` is a command-line tool for managing your own [Co-op Cloud](https://cloud.autonomic.zone). It can provision new servers, create applications, deploy them, run backup and restore operations and a whole lot of other things. It is the go-to tool for day-to-day operations when managing a Co-op Cloud instance.
`abra` is our flagship client & command-line tool which has been developed specifically in the context of the Co-op Cloud project for the purpose of making the day-to-day operations of [operators](https://docs.coopcloud.tech/operators/) and [maintainers](https://docs.coopcloud.tech/maintainers/) pleasant & convenient. It is libre software, written in [Go](https://go.dev) and maintained and extended by the community :heart:
## Change log
See [CHANGELOG.md](./CHANGELOG.md).
## Documentation
> [docs.cloud.autonomic.zone](https://docs.cloud.autonomic.zone/)
## Install
Install the latest stable release:
```sh
curl https://install.abra.autonomic.zone | bash
```
or the bleeding-edge development version:
```sh
curl https://install.abra.autonomic.zone | bash -s -- --dev
```
The source for this script is [here](./installer/installer).
## Update
Run `abra upgrade` to automatically download and install the latest release
version.
To update the development version, run `abra upgrade --dev`.
## Hack
It's written in Bash version 4 or greater!
Install it via `curl https://install.abra.autonomic.zone | bash -s -- --dev`, then you can hack on the source in `~/.abra/src`.
The command-line interface is generated via [docopt](http://docopt.org/). If you add arguments then you need to run `make docopt` ro regenerate the parser.
Please remember to update the [CHANGELOG](./CHANGELOG.md) when you make a change.
To deploy a new version of the installer scripts:
```sh
make release-installer
```
Please see [docs.coopcloud.tech/abra](https://docs.coopcloud.tech/abra) for help on install, upgrade, hacking, troubleshooting & more!
-1856
View File
File diff suppressed because it is too large Load Diff
-100
View File
@@ -1,100 +0,0 @@
#!/bin/bash
# Usage: ./app-catalogue.sh
#
# Gather metadata from Co-op Cloud apps in $ABRA_DIR/apps (default
# ~/.abra/apps), and format it as a Markdown table for this page:
# https://docs.cloud.autonomic.zone/apps/
stack_dir="${ABRA_DIR:-$HOME/.abra}/apps/"
cd "$stack_dir"
# load all README files into ENV_FILES array
mapfile -t readmes < <(find -L . -name "README.md")
# FIXME 3wc: requires bash 4, use for loop instead
base_url="https://git.autonomic.zone/coop-cloud"
cat_apps=()
cat_development=()
cat_utilities=()
cat_graveyard=()
get_var() {
echo "$1" | grep "$2" | sed 's/^[^:]*: //'
}
# shellcheck disable=SC2120
trim() {
# accept input as argument or from STDIN, see here:
# https://zwbetz.com/passing-input-to-a-bash-function-via-arguments-or-stdin/
# shellcheck disable=SC2155
local input="$([[ -p /dev/stdin ]] && cat - || echo "$@")"
[[ -z "$input" ]] && return 1
echo "$input" | tr -d ' '
}
# shellcheck disable=SC2120
prettify() {
# as above
# shellcheck disable=SC2155
local input="$([[ -p /dev/stdin ]] && cat - || echo "$@")"
[[ -z "$input" ]] && return 1
echo "$input" | sed -e 's/Yes/✅/' -e 's/No/❌/' -e 's/N\/A/⛔/'
}
for readme in "${readmes[@]}"; do
type="$(basename "${readme%README.md}")"
if [ "$type" = "example" ]; then
continue
fi
title="$(grep '^# ' "$type/README.md" | sed 's/^# //' )"
# find section between 'metadata' and 'endmetadata' comments
metadata="$(awk '/-- metadata --/,/-- endmetadata --/' "$type/README.md")"
status="$(get_var "$metadata" "Status")"
category="$(get_var "$metadata" "Category" | cut -d',' -f2 | trim)"
if [ -z "$category" ]; then
echo "ERROR: missing category for $type"
continue
fi
image="$(get_var "$metadata" "Image" | cut -d',' -f2 | trim)"
healthcheck="$(get_var "$metadata" "Healthcheck" | prettify)"
backups="$(get_var "$metadata" "Backups" | prettify)"
email="$(get_var "$metadata" "Email" | prettify)"
tests="$(get_var "$metadata" "Tests" | prettify)"
sso="$(get_var "$metadata" "SSO" | prettify)"
row="| [$title]($base_url/$type) | $status | $image | $healthcheck | $backups | $email | $tests | $sso |"
category_lower="$(echo "$category" | tr '[:upper:]' '[:lower:]')"
eval "cat_$category_lower+=( '$row' )"
done
headers="
| **Name** | **Status** | **Image** | **Healtcheck** | **Backups** | **Email** | **CI** | **Single-Sign-On** |
| --- | --- | --- | --- | --- | --- | --- | --- |"
echo "## Applications"
echo "$headers"
printf '%s\n' "${cat_apps[@]}" | sort
echo
echo "## Developer tools"
echo "$headers"
printf '%s\n' "${cat_development[@]}" | sort
echo
echo "## Utilities"
echo "$headers"
printf '%s\n' "${cat_utilities[@]}" | sort
echo
echo "## Graveyard"
echo "$headers"
printf '%s\n' "${cat_graveyard[@]}" | sort
-42
View File
@@ -1,42 +0,0 @@
#!/bin/bash
# Usage: ./app-version.sh <image> <service>
# Example: ./app-version.sh drone/drone:1.10.1 app
#
# Accepts a full format hub.docker.com image tag which it pulls locally and
# generates output which can be used to put in the abra.sh for app packaging.
# Requires the yq program https://mikefarah.gitbook.io/yq/
error() {
echo "$(tput setaf 1)ERROR: $*$(tput sgr0)"
exit 1
}
IMAGE="$1"
SERVICE="$2"
if ! docker pull -q "$IMAGE" > /dev/null 2>&1; then
error "Failed to download image, is the tag correct?"
fi
version=$(echo "$IMAGE" | cut -d ':' -f2)
digest=$(docker image inspect -f "{{.Id}}" "$IMAGE" | cut -d ':' -f2- | cut -c 1-8)
echo "--- Add the following to your abra.sh ---"
echo "export ABRA_TYPE_${SERVICE^^}_VERSION=${version}"
echo "export ABRA_TYPE_${SERVICE^^}_DIGEST=${digest}"
version_lookup="ABRA_TYPE_${SERVICE^^}_VERSION"
digest_lookup="ABRA_TYPE_${SERVICE^^}_DIGEST"
label='- "coop-cloud.${STACK_NAME}.'
label+="${SERVICE}"
label+='.version=${'
label+="${version_lookup}"
label+='}-${'
label+="${digest_lookup}"
label+='}"'
echo
echo "--- And don't forget to label the actual service in the compose file ---"
echo "$label"
+36
View File
@@ -0,0 +1,36 @@
package app
import (
"github.com/urfave/cli"
)
var AppCommand = cli.Command{
Name: "app",
Aliases: []string{"a"},
Usage: "Manage apps",
ArgsUsage: "<domain>",
Description: "This command provides functionality for managing the life cycle of your apps",
Subcommands: []cli.Command{
appNewCommand,
appConfigCommand,
appRestartCommand,
appDeployCommand,
appUpgradeCommand,
appUndeployCommand,
appRemoveCommand,
appCheckCommand,
appListCommand,
appPsCommand,
appLogsCommand,
appCpCommand,
appRunCommand,
appRollbackCommand,
appSecretCommand,
appVolumeCommand,
appVersionCommand,
appErrorsCommand,
appCmdCommand,
appBackupCommand,
appRestoreCommand,
},
}
+389
View File
@@ -0,0 +1,389 @@
package app
import (
"archive/tar"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/system"
"github.com/klauspost/pgzip"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
type backupConfig struct {
preHookCmd string
postHookCmd string
backupPaths []string
}
var appBackupCommand = cli.Command{
Name: "backup",
Aliases: []string{"bk"},
Usage: "Run app backup",
ArgsUsage: "<domain> [<service>]",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Description: `
This command runs an app backup.
A backup command and pre/post hook commands are defined in the recipe
configuration. Abra reads this configuration and run the comands in the context
of the deployed services. Pass <service> if you only want to back up a single
service. All backups are placed in the ~/.abra/backups directory.
A single backup file is produced for all backup paths specified for a service.
If we have the following backup configuration:
- "backupbot.backup.path=/var/lib/foo,/var/lib/bar"
And we run "abra app backup example.com app", Abra will produce a file that
looks like:
~/.abra/backups/example_com_app_609341138.tar.gz
This file is a compressed archive which contains all backup paths. To see paths, run:
tar -tf ~/.abra/backups/example_com_app_609341138.tar.gz
(Make sure to change the name of the backup file)
This single file can be used to restore your app. See "abra app restore" for more.
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
recipe, err := recipe.Get(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
backupConfigs := make(map[string]backupConfig)
for _, service := range recipe.Config.Services {
if backupsEnabled, ok := service.Deploy.Labels["backupbot.backup"]; ok {
if backupsEnabled == "true" {
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), service.Name)
bkConfig := backupConfig{}
logrus.Debugf("backup config detected for %s", fullServiceName)
if paths, ok := service.Deploy.Labels["backupbot.backup.path"]; ok {
logrus.Debugf("detected backup paths for %s: %s", fullServiceName, paths)
bkConfig.backupPaths = strings.Split(paths, ",")
}
if preHookCmd, ok := service.Deploy.Labels["backupbot.backup.pre-hook"]; ok {
logrus.Debugf("detected pre-hook command for %s: %s", fullServiceName, preHookCmd)
bkConfig.preHookCmd = preHookCmd
}
if postHookCmd, ok := service.Deploy.Labels["backupbot.backup.post-hook"]; ok {
logrus.Debugf("detected post-hook command for %s: %s", fullServiceName, postHookCmd)
bkConfig.postHookCmd = postHookCmd
}
backupConfigs[service.Name] = bkConfig
}
}
}
serviceName := c.Args().Get(1)
if serviceName != "" {
backupConfig, ok := backupConfigs[serviceName]
if !ok {
logrus.Fatalf("no backup config for %s? does %s exist?", serviceName, serviceName)
}
logrus.Infof("running backup for the %s service", serviceName)
if err := runBackup(app, serviceName, backupConfig); err != nil {
logrus.Fatal(err)
}
} else {
for serviceName, backupConfig := range backupConfigs {
logrus.Infof("running backup for the %s service", serviceName)
if err := runBackup(app, serviceName, backupConfig); err != nil {
logrus.Fatal(err)
}
}
}
return nil
},
}
// runBackup does the actual backup logic.
func runBackup(app config.App, serviceName string, bkConfig backupConfig) error {
if len(bkConfig.backupPaths) == 0 {
return fmt.Errorf("backup paths are empty for %s?", serviceName)
}
cl, err := client.New(app.Server)
if err != nil {
return err
}
// FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli()
if err != nil {
return err
}
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, true)
if err != nil {
return err
}
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
if bkConfig.preHookCmd != "" {
splitCmd := internal.SafeSplit(bkConfig.preHookCmd)
logrus.Debugf("split pre-hook command for %s into %s", fullServiceName, splitCmd)
preHookExecOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: splitCmd,
Detach: false,
Tty: true,
}
if err := container.RunExec(dcli, cl, targetContainer.ID, &preHookExecOpts); err != nil {
return fmt.Errorf("failed to run %s on %s: %s", bkConfig.preHookCmd, targetContainer.ID, err.Error())
}
logrus.Infof("succesfully ran %s pre-hook command: %s", fullServiceName, bkConfig.preHookCmd)
}
var tempBackupPaths []string
for _, remoteBackupPath := range bkConfig.backupPaths {
timestamp := strconv.Itoa(time.Now().Nanosecond())
sanitisedPath := strings.ReplaceAll(remoteBackupPath, "/", "_")
localBackupPath := filepath.Join(config.BACKUP_DIR, fmt.Sprintf("%s%s_%s.tar.gz", fullServiceName, sanitisedPath, timestamp))
logrus.Debugf("temporarily backing up %s:%s to %s", fullServiceName, remoteBackupPath, localBackupPath)
logrus.Infof("backing up %s:%s", fullServiceName, remoteBackupPath)
content, _, err := cl.CopyFromContainer(context.Background(), targetContainer.ID, remoteBackupPath)
if err != nil {
logrus.Debugf("failed to copy %s from container: %s", remoteBackupPath, err.Error())
if err := cleanupTempArchives(tempBackupPaths); err != nil {
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
}
return fmt.Errorf("failed to copy %s from container: %s", remoteBackupPath, err.Error())
}
defer content.Close()
_, srcBase := archive.SplitPathDirEntry(remoteBackupPath)
preArchive := archive.RebaseArchiveEntries(content, srcBase, remoteBackupPath)
if err := copyToFile(localBackupPath, preArchive); err != nil {
logrus.Debugf("failed to create tar archive (%s): %s", localBackupPath, err.Error())
if err := cleanupTempArchives(tempBackupPaths); err != nil {
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
}
return fmt.Errorf("failed to create tar archive (%s): %s", localBackupPath, err.Error())
}
tempBackupPaths = append(tempBackupPaths, localBackupPath)
}
logrus.Infof("compressing and merging archives...")
if err := mergeArchives(tempBackupPaths, fullServiceName); err != nil {
logrus.Debugf("failed to merge archive files: %s", err.Error())
if err := cleanupTempArchives(tempBackupPaths); err != nil {
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
}
return fmt.Errorf("failed to merge archive files: %s", err.Error())
}
if err := cleanupTempArchives(tempBackupPaths); err != nil {
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
}
if bkConfig.postHookCmd != "" {
splitCmd := internal.SafeSplit(bkConfig.postHookCmd)
logrus.Debugf("split post-hook command for %s into %s", fullServiceName, splitCmd)
postHookExecOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: splitCmd,
Detach: false,
Tty: true,
}
if err := container.RunExec(dcli, cl, targetContainer.ID, &postHookExecOpts); err != nil {
return err
}
logrus.Infof("succesfully ran %s post-hook command: %s", fullServiceName, bkConfig.postHookCmd)
}
return nil
}
func copyToFile(outfile string, r io.Reader) error {
tmpFile, err := system.TempFileSequential(filepath.Dir(outfile), ".tar_temp")
if err != nil {
return err
}
tmpPath := tmpFile.Name()
_, err = io.Copy(tmpFile, r)
tmpFile.Close()
if err != nil {
os.Remove(tmpPath)
return err
}
if err = os.Rename(tmpPath, outfile); err != nil {
os.Remove(tmpPath)
return err
}
return nil
}
func cleanupTempArchives(tarPaths []string) error {
for _, tarPath := range tarPaths {
if err := os.RemoveAll(tarPath); err != nil {
return err
}
logrus.Debugf("remove temporary archive file %s", tarPath)
}
return nil
}
func mergeArchives(tarPaths []string, serviceName string) error {
var out io.Writer
var cout *pgzip.Writer
timestamp := strconv.Itoa(time.Now().Nanosecond())
localBackupPath := filepath.Join(config.BACKUP_DIR, fmt.Sprintf("%s_%s.tar.gz", serviceName, timestamp))
fout, err := os.Create(localBackupPath)
if err != nil {
return fmt.Errorf("Failed to open %s: %s", localBackupPath, err)
}
defer fout.Close()
out = fout
cout = pgzip.NewWriter(out)
out = cout
tw := tar.NewWriter(out)
for _, tarPath := range tarPaths {
if err := addTar(tw, tarPath); err != nil {
return fmt.Errorf("failed to merge %s: %v", tarPath, err)
}
}
if err := tw.Close(); err != nil {
return fmt.Errorf("failed to close tar writer %v", err)
}
if cout != nil {
if err := cout.Flush(); err != nil {
return fmt.Errorf("failed to flush: %s", err)
} else if err = cout.Close(); err != nil {
return fmt.Errorf("failed to close compressed writer: %s", err)
}
}
logrus.Infof("backed up %s to %s", serviceName, localBackupPath)
return nil
}
func addTar(tw *tar.Writer, pth string) (err error) {
var tr *tar.Reader
var rc io.ReadCloser
var hdr *tar.Header
if tr, rc, err = openTarFile(pth); err != nil {
return
}
for {
if hdr, err = tr.Next(); err != nil {
if err == io.EOF {
err = nil
}
break
}
if err = tw.WriteHeader(hdr); err != nil {
break
} else if _, err = io.Copy(tw, tr); err != nil {
break
}
}
if err == nil {
err = rc.Close()
} else {
rc.Close()
}
return
}
func openTarFile(pth string) (tr *tar.Reader, rc io.ReadCloser, err error) {
var fin *os.File
var n int
buff := make([]byte, 1024)
if fin, err = os.Open(pth); err != nil {
return
}
if n, err = fin.Read(buff); err != nil {
fin.Close()
return
} else if n == 0 {
fin.Close()
err = fmt.Errorf("%s is empty", pth)
return
}
if _, err = fin.Seek(0, 0); err != nil {
fin.Close()
return
}
rc = fin
tr = tar.NewReader(rc)
return tr, rc, nil
}
+57
View File
@@ -0,0 +1,57 @@
package app
import (
"os"
"path"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var appCheckCommand = cli.Command{
Name: "check",
Aliases: []string{"chk"},
Usage: "Check if app is configured correctly",
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
envSamplePath := path.Join(config.RECIPES_DIR, app.Recipe, ".env.sample")
if _, err := os.Stat(envSamplePath); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("%s does not exist?", envSamplePath)
}
logrus.Fatal(err)
}
envSample, err := config.ReadEnv(envSamplePath)
if err != nil {
logrus.Fatal(err)
}
var missing []string
for k := range envSample {
if _, ok := app.Env[k]; !ok {
missing = append(missing, k)
}
}
if len(missing) > 0 {
missingEnvVars := strings.Join(missing, ", ")
logrus.Fatalf("%s is missing %s", app.Path, missingEnvVars)
}
logrus.Infof("all necessary environment variables defined for %s", app.Name)
return nil
},
BashComplete: autocomplete.AppNameComplete,
}
+233
View File
@@ -0,0 +1,233 @@
package app
import (
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/archive"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var localCmd bool
var localCmdFlag = &cli.BoolFlag{
Name: "local, l",
Usage: "Run command locally",
Destination: &localCmd,
}
var remoteUser string
var remoteUserFlag = &cli.StringFlag{
Name: "user, u",
Value: "",
Usage: "User to run command within a service context",
Destination: &remoteUser,
}
var appCmdCommand = cli.Command{
Name: "command",
Aliases: []string{"cmd"},
Usage: "Run app commands",
Description: `
This command runs app specific commands.
These commands are bash functions, defined in the abra.sh of the recipe itself.
They can be run within the context of a service (e.g. app) or locally on your
work station by passing "--local". Arguments can be passed into these functions
using the "-- <args>" syntax.
Example:
abra app cmd example.com app create_user -- me@example.com
`,
ArgsUsage: "<domain> [<service>] <command>",
Flags: []cli.Flag{
internal.DebugFlag,
localCmdFlag,
remoteUserFlag,
},
BashComplete: autocomplete.AppNameComplete,
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if localCmd && remoteUser != "" {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use --local & <user> together"))
}
abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("%s does not exist for %s?", abraSh, app.Name)
}
logrus.Fatal(err)
}
var parsedCmdArgs string
var cmdArgsIdx int
var hasCmdArgs bool
for idx, arg := range c.Args() {
if arg == "--" {
cmdArgsIdx = idx
hasCmdArgs = true
}
if hasCmdArgs && idx > cmdArgsIdx {
parsedCmdArgs += fmt.Sprintf("%s ", c.Args().Get(idx))
}
}
if localCmd {
cmdName := c.Args().Get(1)
if err := ensureCommand(abraSh, app.Recipe, cmdName); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("--local detected, running %s on local work station", cmdName)
var sourceAndExec string
if hasCmdArgs {
logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs)
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; . %s; %s %s", app.StackName(), abraSh, cmdName, parsedCmdArgs)
} else {
logrus.Debug("did not detect any command arguments")
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; . %s; %s", app.StackName(), abraSh, cmdName)
}
cmd := exec.Command("/bin/sh", "-c", sourceAndExec)
if err := internal.RunCmd(cmd); err != nil {
logrus.Fatal(err)
}
} else {
targetServiceName := c.Args().Get(1)
cmdName := c.Args().Get(2)
if err := ensureCommand(abraSh, app.Recipe, cmdName); err != nil {
logrus.Fatal(err)
}
serviceNames, err := config.GetAppServiceNames(app.Name)
if err != nil {
logrus.Fatal(err)
}
matchingServiceName := false
for _, serviceName := range serviceNames {
if serviceName == targetServiceName {
matchingServiceName = true
}
}
if !matchingServiceName {
logrus.Fatalf("no service %s for %s?", targetServiceName, app.Name)
}
logrus.Debugf("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName)
if hasCmdArgs {
logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs)
} else {
logrus.Debug("did not detect any command arguments")
}
if err := runCmdRemote(app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil {
logrus.Fatal(err)
}
}
return nil
},
}
func ensureCommand(abraSh, recipeName, execCmd string) error {
bytes, err := ioutil.ReadFile(abraSh)
if err != nil {
return err
}
if !strings.Contains(string(bytes), execCmd) {
return fmt.Errorf("%s doesn't have a %s function", recipeName, execCmd)
}
return nil
}
func runCmdRemote(app config.App, abraSh, serviceName, cmdName, cmdArgs string) error {
cl, err := client.New(app.Server)
if err != nil {
return err
}
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, true)
if err != nil {
return err
}
logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(targetContainer.ID), app.Server)
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
content, err := archive.TarWithOptions(abraSh, toTarOpts)
if err != nil {
return err
}
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(context.Background(), targetContainer.ID, "/tmp", content, copyOpts); err != nil {
return err
}
var cmd []string
if cmdArgs != "" {
cmd = []string{"/bin/sh", "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; . /tmp/abra.sh; %s %s", serviceName, app.StackName(), cmdName, cmdArgs)}
} else {
cmd = []string{"/bin/sh", "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; . /tmp/abra.sh; %s", serviceName, app.StackName(), cmdName)}
}
logrus.Debugf("running command: %s", strings.Join(cmd, " "))
execCreateOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: cmd,
Detach: false,
Tty: true,
}
if remoteUser != "" {
logrus.Debugf("running command with user %s", remoteUser)
execCreateOpts.User = remoteUser
}
// FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli()
if err != nil {
return err
}
if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
return err
}
return nil
}
+64
View File
@@ -0,0 +1,64 @@
package app
import (
"errors"
"os"
"os/exec"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var appConfigCommand = cli.Command{
Name: "config",
Aliases: []string{"cfg"},
Usage: "Edit app config",
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
appName := c.Args().First()
if appName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no app provided"))
}
files, err := config.LoadAppFiles("")
if err != nil {
logrus.Fatal(err)
}
appFile, exists := files[appName]
if !exists {
logrus.Fatalf("cannot find app with name %s", appName)
}
ed, ok := os.LookupEnv("EDITOR")
if !ok {
edPrompt := &survey.Select{
Message: "Which editor do you wish to use?",
Options: []string{"vi", "vim", "nvim", "nano", "pico", "emacs"},
}
if err := survey.AskOne(edPrompt, &ed); err != nil {
logrus.Fatal(err)
}
}
cmd := exec.Command(ed, appFile.Path)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
logrus.Fatal(err)
}
return nil
},
BashComplete: autocomplete.AppNameComplete,
}
+151
View File
@@ -0,0 +1,151 @@
package app
import (
"context"
"fmt"
"os"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/formatter"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/archive"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var appCpCommand = cli.Command{
Name: "cp",
Aliases: []string{"c"},
ArgsUsage: "<domain> <src> <dst>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
Usage: "Copy files to/from a running app service",
Description: `
This command supports copying files to and from any app service file system.
If you want to copy a myfile.txt to the root of the app service:
abra app cp <domain> myfile.txt app:/
And if you want to copy that file back to your current working directory locally:
abra app cp <domain> app:/myfile.txt .
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
src := c.Args().Get(1)
dst := c.Args().Get(2)
if src == "" {
logrus.Fatal("missing <src> argument")
} else if dst == "" {
logrus.Fatal("missing <dest> argument")
}
parsedSrc := strings.SplitN(src, ":", 2)
parsedDst := strings.SplitN(dst, ":", 2)
errorMsg := "one of <src>/<dest> arguments must take $SERVICE:$PATH form"
if len(parsedSrc) == 2 && len(parsedDst) == 2 {
logrus.Fatal(errorMsg)
} else if len(parsedSrc) != 2 {
if len(parsedDst) != 2 {
logrus.Fatal(errorMsg)
}
} else if len(parsedDst) != 2 {
if len(parsedSrc) != 2 {
logrus.Fatal(errorMsg)
}
}
var service string
var srcPath string
var dstPath string
isToContainer := false // <container:src> <dst>
if len(parsedSrc) == 2 {
service = parsedSrc[0]
srcPath = parsedSrc[1]
dstPath = dst
logrus.Debugf("assuming transfer is coming FROM the container")
} else if len(parsedDst) == 2 {
service = parsedDst[0]
dstPath = parsedDst[1]
srcPath = src
isToContainer = true // <src> <container:dst>
logrus.Debugf("assuming transfer is going TO the container")
}
if !isToContainer {
if _, err := os.Stat(dstPath); os.IsNotExist(err) {
logrus.Fatalf("%s does not exist locally?", dstPath)
}
}
err := configureAndCp(c, app, srcPath, dstPath, service, isToContainer)
if err != nil {
logrus.Fatal(err)
}
return nil
},
BashComplete: autocomplete.AppNameComplete,
}
func configureAndCp(
c *cli.Context,
app config.App,
srcPath string,
dstPath string,
service string,
isToContainer bool) error {
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service))
container, err := container.GetContainer(context.Background(), cl, filters, internal.NoInput)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server)
if isToContainer {
if _, err := os.Stat(srcPath); err != nil {
logrus.Fatalf("%s does not exist?", srcPath)
}
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
content, err := archive.TarWithOptions(srcPath, toTarOpts)
if err != nil {
logrus.Fatal(err)
}
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(context.Background(), container.ID, dstPath, content, copyOpts); err != nil {
logrus.Fatal(err)
}
} else {
content, _, err := cl.CopyFromContainer(context.Background(), container.ID, srcPath)
if err != nil {
logrus.Fatal(err)
}
defer content.Close()
fromTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
if err := archive.Untar(content, dstPath, fromTarOpts); err != nil {
logrus.Fatal(err)
}
}
return nil
}
+37
View File
@@ -0,0 +1,37 @@
package app
import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"github.com/urfave/cli"
)
var appDeployCommand = cli.Command{
Name: "deploy",
Aliases: []string{"d"},
Usage: "Deploy an app",
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
internal.ChaosFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
},
Before: internal.SubCommandBefore,
Description: `
This command deploys an app. It does not support incrementing the version of a
deployed app, for this you need to look at the "abra app upgrade <domain>"
command.
You may pass "--force" to re-deploy the same version again. This can be useful
if the container runtime has gotten into a weird state.
Chas mode ("--chaos") will deploy your local checkout of a recipe as-is,
including unstaged changes and can be useful for live hacking and testing new
recipes.
`,
Action: internal.DeployAction,
BashComplete: autocomplete.AppNameComplete,
}
+143
View File
@@ -0,0 +1,143 @@
package app
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var appErrorsCommand = cli.Command{
Name: "errors",
Usage: "List errors for a deployed app",
ArgsUsage: "<domain>",
Description: `
This command lists errors for a deployed app.
This is a best-effort implementation and an attempt to gather a number of tips
& tricks for finding errors together into one convenient command. When an app
is failing to deploy or having issues, it could be a lot of things.
This command currently takes into account:
Is the service deployed?
Is the service killed by an OOM error?
Is the service reporting an error (like in "ps --no-trunc" output)
Is the service healthcheck failing? what are the healthcheck logs?
Got any more ideas? Please let us know:
https://git.coopcloud.tech/coop-cloud/organising/issues/new/choose
This command is best accompanied by "abra app logs <domain>" which may reveal
further information which can help you debug the cause of an app failure via
the logs.
`,
Aliases: []string{"e"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.WatchFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
logrus.Fatal(err)
}
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
if !internal.Watch {
if err := checkErrors(c, cl, app); err != nil {
logrus.Fatal(err)
}
return nil
}
for {
if err := checkErrors(c, cl, app); err != nil {
logrus.Fatal(err)
}
time.Sleep(2 * time.Second)
}
return nil
},
}
func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App) error {
recipe, err := recipe.Get(app.Recipe)
if err != nil {
return err
}
for _, service := range recipe.Config.Services {
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name))
containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters})
if err != nil {
return err
}
if len(containers) == 0 {
logrus.Warnf("%s is not up, something seems wrong", service.Name)
continue
}
container := containers[0]
containerState, err := cl.ContainerInspect(context.Background(), container.ID)
if err != nil {
logrus.Fatal(err)
}
if containerState.State.OOMKilled {
logrus.Warnf("%s has been killed due to an out of memory error", service.Name)
}
if containerState.State.Error != "" {
logrus.Warnf("%s reports this error: %s", service.Name, containerState.State.Error)
}
if containerState.State.Health != nil {
if containerState.State.Health.Status != "healthy" {
logrus.Warnf("%s healthcheck status is %s", service.Name, containerState.State.Health.Status)
logrus.Warnf("%s healthcheck has failed %s times", service.Name, strconv.Itoa(containerState.State.Health.FailingStreak))
for _, log := range containerState.State.Health.Log {
logrus.Warnf("%s healthcheck logs: %s", service.Name, strings.TrimSpace(log.Output))
}
}
}
}
return nil
}
func getServiceName(names []string) string {
containerName := strings.Join(names, " ")
trimmed := strings.TrimPrefix(containerName, "/")
return strings.Split(trimmed, ".")[0]
}
+264
View File
@@ -0,0 +1,264 @@
package app
import (
"fmt"
"sort"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/ssh"
"coopcloud.tech/tagcmp"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var status bool
var statusFlag = &cli.BoolFlag{
Name: "status, S",
Usage: "Show app deployment status",
Destination: &status,
}
var appRecipe string
var recipeFlag = &cli.StringFlag{
Name: "recipe, r",
Value: "",
Usage: "Show apps of a specific recipe",
Destination: &appRecipe,
}
var listAppServer string
var listAppServerFlag = &cli.StringFlag{
Name: "server, s",
Value: "",
Usage: "Show apps of a specific server",
Destination: &listAppServer,
}
type appStatus struct {
server string
recipe string
appName string
domain string
status string
version string
upgrade string
}
type serverStatus struct {
apps []appStatus
appCount int
versionCount int
unversionedCount int
latestCount int
upgradeCount int
}
var appListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List all managed apps",
Description: `
This command looks at your local file system listing of apps and servers (e.g.
in ~/.abra/) to generate a report of all your apps.
By passing the "--status/-S" flag, you can query all your servers for the
actual live deployment status. Depending on how many servers you manage, this
can take some time.
`,
Flags: []cli.Flag{
internal.DebugFlag,
statusFlag,
listAppServerFlag,
recipeFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
appFiles, err := config.LoadAppFiles(listAppServer)
if err != nil {
logrus.Fatal(err)
}
apps, err := config.GetApps(appFiles)
if err != nil {
logrus.Fatal(err)
}
sort.Sort(config.ByServerAndRecipe(apps))
statuses := make(map[string]map[string]string)
var catl recipe.RecipeCatalogue
if status {
alreadySeen := make(map[string]bool)
for _, app := range apps {
if _, ok := alreadySeen[app.Server]; !ok {
if err := ssh.EnsureHostKey(app.Server); err != nil {
logrus.Fatal(fmt.Sprintf(internal.SSHFailMsg, app.Server))
}
alreadySeen[app.Server] = true
}
}
statuses, err = config.GetAppStatuses(appFiles)
if err != nil {
logrus.Fatal(err)
}
var err error
catl, err = recipe.ReadRecipeCatalogue()
if err != nil {
logrus.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 appRecipe == "" {
// count server, no filtering
totalServersCount++
}
}
if app.Recipe == appRecipe || appRecipe == "" {
if appRecipe != "" {
// only count server if matches filter
totalServersCount++
}
appStats := appStatus{}
stats.appCount++
totalAppsCount++
if status {
status := "unknown"
version := "unknown"
if statusMeta, ok := statuses[app.StackName()]; ok {
if currentVersion, exists := statusMeta["version"]; exists {
if currentVersion != "" {
version = currentVersion
}
}
if statusMeta["status"] != "" {
status = statusMeta["status"]
}
stats.versionCount++
} else {
stats.unversionedCount++
}
appStats.status = status
appStats.version = version
var newUpdates []string
if version != "unknown" {
updates, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil {
logrus.Fatal(err)
}
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
logrus.Fatal(err)
}
for _, update := range updates {
parsedUpdate, err := tagcmp.Parse(update)
if err != nil {
logrus.Fatal(err)
}
if update != version && parsedUpdate.IsGreaterThan(parsedVersion) {
newUpdates = append(newUpdates, update)
}
}
}
if len(newUpdates) == 0 {
if version == "unknown" {
appStats.upgrade = "unknown"
} else {
appStats.upgrade = "latest"
stats.latestCount++
}
} else {
newUpdates = internal.ReverseStringList(newUpdates)
appStats.upgrade = strings.Join(newUpdates, "\n")
stats.upgradeCount++
}
}
appStats.server = app.Server
appStats.recipe = app.Recipe
appStats.appName = app.Name
appStats.domain = app.Domain
stats.apps = append(stats.apps, appStats)
}
allStats[app.Server] = stats
}
alreadySeen := make(map[string]bool)
for _, app := range apps {
if _, ok := alreadySeen[app.Server]; ok {
continue
}
serverStat := allStats[app.Server]
tableCol := []string{"recipe", "domain"}
if status {
tableCol = append(tableCol, []string{"status", "version", "upgrade"}...)
}
table := formatter.CreateTable(tableCol)
for _, appStat := range serverStat.apps {
tableRow := []string{appStat.recipe, appStat.domain}
if status {
tableRow = append(tableRow, []string{appStat.status, appStat.version, appStat.upgrade}...)
}
table.Append(tableRow)
}
if table.NumLines() > 0 {
table.Render()
if status {
fmt.Println(fmt.Sprintf(
"server: %s | total apps: %v | versioned: %v | unversioned: %v | latest: %v | upgrade: %v",
app.Server,
serverStat.appCount,
serverStat.versionCount,
serverStat.unversionedCount,
serverStat.latestCount,
serverStat.upgradeCount,
))
} else {
fmt.Println(fmt.Sprintf("server: %s | total apps: %v", app.Server, serverStat.appCount))
}
}
if len(allStats) > 1 && table.NumLines() > 0 {
fmt.Println() // newline separator for multiple servers
}
alreadySeen[app.Server] = true
}
if len(allStats) > 1 {
fmt.Println(fmt.Sprintf("total servers: %v | total apps: %v ", totalServersCount, totalAppsCount))
}
return nil
},
}
+129
View File
@@ -0,0 +1,129 @@
package app
import (
"context"
"fmt"
"io"
"os"
"sync"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/service"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var logOpts = types.ContainerLogsOptions{
Details: false,
Follow: true,
ShowStderr: true,
ShowStdout: true,
Tail: "20",
Timestamps: true,
}
// stackLogs lists logs for all stack services
func stackLogs(c *cli.Context, app config.App, client *dockerClient.Client) {
filters, err := app.Filters(true, false)
if err != nil {
logrus.Fatal(err)
}
serviceOpts := types.ServiceListOptions{Filters: filters}
services, err := client.ServiceList(context.Background(), serviceOpts)
if err != nil {
logrus.Fatal(err)
}
var wg sync.WaitGroup
for _, service := range services {
wg.Add(1)
go func(s string) {
if internal.StdErrOnly {
logOpts.ShowStdout = false
}
logs, err := client.ServiceLogs(context.Background(), s, logOpts)
if err != nil {
logrus.Fatal(err)
}
defer logs.Close()
_, err = io.Copy(os.Stdout, logs)
if err != nil && err != io.EOF {
logrus.Fatal(err)
}
}(service.ID)
}
wg.Wait()
os.Exit(0)
}
var appLogsCommand = cli.Command{
Name: "logs",
Aliases: []string{"l"},
ArgsUsage: "<domain> [<service>]",
Usage: "Tail app logs",
Flags: []cli.Flag{
internal.StdErrOnlyFlag,
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
serviceName := c.Args().Get(1)
if serviceName == "" {
logrus.Debugf("tailing logs for all %s services", app.Recipe)
stackLogs(c, app, cl)
} else {
logrus.Debugf("tailing logs for %s", serviceName)
if err := tailServiceLogs(c, cl, app, serviceName); err != nil {
logrus.Fatal(err)
}
}
return nil
},
}
func tailServiceLogs(c *cli.Context, cl *dockerClient.Client, app config.App, serviceName string) error {
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
chosenService, err := service.GetService(context.Background(), cl, filters, internal.NoInput)
if err != nil {
logrus.Fatal(err)
}
if internal.StdErrOnly {
logOpts.ShowStdout = false
}
logs, err := cl.ServiceLogs(context.Background(), chosenService.ID, logOpts)
if err != nil {
logrus.Fatal(err)
}
defer logs.Close()
_, err = io.Copy(os.Stdout, logs)
if err != nil && err != io.EOF {
logrus.Fatal(err)
}
return nil
}
+46
View File
@@ -0,0 +1,46 @@
package app
import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"github.com/urfave/cli"
)
var appNewDescription = `
This command takes a recipe and uses it to create a new app. This new app
configuration is stored in your ~/.abra directory under the appropriate server.
This 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".
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.
`
var appNewCommand = cli.Command{
Name: "new",
Aliases: []string{"n"},
Usage: "Create a new app",
Description: appNewDescription,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.NewAppServerFlag,
internal.DomainFlag,
internal.PassFlag,
internal.SecretsFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "[<recipe>]",
Action: internal.NewAction,
BashComplete: autocomplete.RecipeNameComplete,
}
+101
View File
@@ -0,0 +1,101 @@
package app
import (
"context"
"strings"
"time"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/service"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/buger/goterm"
dockerFormatter "github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var appPsCommand = cli.Command{
Name: "ps",
Aliases: []string{"p"},
Usage: "Check app status",
ArgsUsage: "<domain>",
Description: "This command shows a more detailed status output of a specific deployed app.",
Flags: []cli.Flag{
internal.WatchFlag,
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
logrus.Fatal(err)
}
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
if !internal.Watch {
showPSOutput(c, app, cl)
return nil
}
goterm.Clear()
for {
goterm.MoveCursor(1, 1)
showPSOutput(c, app, cl)
goterm.Flush()
time.Sleep(2 * time.Second)
}
},
}
// showPSOutput renders ps output.
func showPSOutput(c *cli.Context, app config.App, cl *dockerClient.Client) {
filters, err := app.Filters(true, true)
if err != nil {
logrus.Fatal(err)
}
containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters})
if err != nil {
logrus.Fatal(err)
}
tableCol := []string{"service name", "image", "created", "status", "state", "ports"}
table := formatter.CreateTable(tableCol)
for _, container := range containers {
var containerNames []string
for _, containerName := range container.Names {
trimmed := strings.TrimPrefix(containerName, "/")
containerNames = append(containerNames, trimmed)
}
tableRow := []string{
service.ContainerToServiceName(container.Names, app.StackName()),
formatter.RemoveSha(container.Image),
formatter.HumanDuration(container.Created),
container.Status,
container.State,
dockerFormatter.DisplayablePorts(container.Ports),
}
table.Append(tableRow)
}
table.Render()
}
+174
View File
@@ -0,0 +1,174 @@
package app
import (
"context"
"fmt"
"os"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// Volumes stores the variable from VolumesFlag
var Volumes bool
// VolumesFlag is used to specify if volumes should be deleted when deleting an app
var VolumesFlag = &cli.BoolFlag{
Name: "volumes, V",
Destination: &Volumes,
}
var appRemoveCommand = cli.Command{
Name: "remove",
Aliases: []string{"rm"},
ArgsUsage: "<domain>",
Usage: "Remove an already undeployed app",
Flags: []cli.Flag{
VolumesFlag,
internal.ForceFlag,
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if !internal.Force && !internal.NoInput {
response := false
prompt := &survey.Confirm{
Message: fmt.Sprintf("about to remove %s, are you sure?", app.Name),
}
if err := survey.AskOne(prompt, &response); err != nil {
logrus.Fatal(err)
}
if !response {
logrus.Fatal("aborting as requested")
}
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
logrus.Fatal(err)
}
if isDeployed {
logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name)
}
fs, err := app.Filters(false, false)
if err != nil {
logrus.Fatal(err)
}
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: fs})
if err != nil {
logrus.Fatal(err)
}
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 {
var secretNamesToRemove []string
if !internal.Force && !internal.NoInput {
secretsPrompt := &survey.MultiSelect{
Message: "which secrets do you want to remove?",
Help: "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled",
VimMode: true,
Options: secretNames,
Default: secretNames,
}
if err := survey.AskOne(secretsPrompt, &secretNamesToRemove); err != nil {
logrus.Fatal(err)
}
}
if internal.Force || internal.NoInput {
secretNamesToRemove = secretNames
}
for _, name := range secretNamesToRemove {
err := cl.SecretRemove(context.Background(), secrets[name])
if err != nil {
logrus.Fatal(err)
}
logrus.Info(fmt.Sprintf("secret: %s removed", name))
}
} else {
logrus.Info("no secrets to remove")
}
fs, err = app.Filters(false, true)
if err != nil {
logrus.Fatal(err)
}
volumeListOKBody, err := cl.VolumeList(context.Background(), fs)
volumeList := volumeListOKBody.Volumes
if err != nil {
logrus.Fatal(err)
}
var vols []string
for _, vol := range volumeList {
vols = append(vols, vol.Name)
}
if len(vols) > 0 {
if Volumes {
var removeVols []string
if !internal.Force && !internal.NoInput {
volumesPrompt := &survey.MultiSelect{
Message: "which volumes do you want to remove?",
Help: "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled",
VimMode: true,
Options: vols,
Default: vols,
}
if err := survey.AskOne(volumesPrompt, &removeVols); err != nil {
logrus.Fatal(err)
}
}
for _, vol := range removeVols {
err := cl.VolumeRemove(context.Background(), vol, internal.Force) // last argument is for force removing
if err != nil {
logrus.Fatal(err)
}
logrus.Info(fmt.Sprintf("volume %s removed", vol))
}
} else {
logrus.Info("no volumes were removed")
}
} else {
if Volumes {
logrus.Info("no volumes to remove")
}
}
err = os.Remove(app.Path)
if err != nil {
logrus.Fatal(err)
}
logrus.Info(fmt.Sprintf("file: %s removed", app.Path))
return nil
},
BashComplete: autocomplete.AppNameComplete,
}
+70
View File
@@ -0,0 +1,70 @@
package app
import (
"context"
"errors"
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
upstream "coopcloud.tech/abra/pkg/upstream/service"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var appRestartCommand = cli.Command{
Name: "restart",
Aliases: []string{"re"},
Usage: "Restart an app",
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
Description: `This command restarts a service within a deployed app.`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
serviceNameShort := c.Args().Get(1)
if serviceNameShort == "" {
err := errors.New("missing service?")
internal.ShowSubcommandHelpAndError(c, err)
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
serviceName := fmt.Sprintf("%s_%s", app.StackName(), serviceNameShort)
logrus.Debugf("attempting to scale %s to 0 (restart logic)", serviceName)
if err := upstream.RunServiceScale(context.Background(), cl, serviceName, 0); err != nil {
logrus.Fatal(err)
}
if err := stack.WaitOnService(context.Background(), cl, serviceName, app.Name); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("%s has been scaled to 0 (restart logic)", serviceName)
logrus.Debugf("attempting to scale %s to 1 (restart logic)", serviceName)
if err := upstream.RunServiceScale(context.Background(), cl, serviceName, 1); err != nil {
logrus.Fatal(err)
}
if err := stack.WaitOnService(context.Background(), cl, serviceName, app.Name); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("%s has been scaled to 1 (restart logic)", serviceName)
logrus.Infof("%s service successfully restarted", serviceNameShort)
return nil
},
}
+201
View File
@@ -0,0 +1,201 @@
package app
import (
"context"
"errors"
"fmt"
"os"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/archive"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
type restoreConfig struct {
preHookCmd string
postHookCmd string
}
var appRestoreCommand = cli.Command{
Name: "restore",
Aliases: []string{"rs"},
Usage: "Run app restore",
ArgsUsage: "<domain> <service> <file>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Description: `
This command runs an app restore.
Pre/post hook commands are defined in the recipe configuration. Abra reads this
configuration and run the comands in the context of the service before
restoring the backup.
Unlike "abra app backup", restore must be run on a per-service basis. You can
not restore all services in one go. Backup files produced by Abra are
compressed archives which use absolute paths. This allows Abra to restore
according to standard tar command logic.
Example:
abra app restore example.com app ~/.abra/backups/example_com_app_609341138.tar.gz
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
serviceName := c.Args().Get(1)
if serviceName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("missing <service>?"))
}
backupPath := c.Args().Get(2)
if backupPath == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("missing <file>?"))
}
if _, err := os.Stat(backupPath); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("%s doesn't exist?", backupPath)
}
}
recipe, err := recipe.Get(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
restoreConfigs := make(map[string]restoreConfig)
for _, service := range recipe.Config.Services {
if restoreEnabled, ok := service.Deploy.Labels["backupbot.restore"]; ok {
if restoreEnabled == "true" {
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), service.Name)
rsConfig := restoreConfig{}
logrus.Debugf("restore config detected for %s", fullServiceName)
if preHookCmd, ok := service.Deploy.Labels["backupbot.restore.pre-hook"]; ok {
logrus.Debugf("detected pre-hook command for %s: %s", fullServiceName, preHookCmd)
rsConfig.preHookCmd = preHookCmd
}
if postHookCmd, ok := service.Deploy.Labels["backupbot.restore.post-hook"]; ok {
logrus.Debugf("detected post-hook command for %s: %s", fullServiceName, postHookCmd)
rsConfig.postHookCmd = postHookCmd
}
restoreConfigs[service.Name] = rsConfig
}
}
}
rsConfig, ok := restoreConfigs[serviceName]
if !ok {
rsConfig = restoreConfig{}
}
if err := runRestore(app, backupPath, serviceName, rsConfig); err != nil {
logrus.Fatal(err)
}
return nil
},
}
// runRestore does the actual restore logic.
func runRestore(app config.App, backupPath, serviceName string, rsConfig restoreConfig) error {
cl, err := client.New(app.Server)
if err != nil {
return err
}
// FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli()
if err != nil {
return err
}
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, true)
if err != nil {
return err
}
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
if rsConfig.preHookCmd != "" {
splitCmd := internal.SafeSplit(rsConfig.preHookCmd)
logrus.Debugf("split pre-hook command for %s into %s", fullServiceName, splitCmd)
preHookExecOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: splitCmd,
Detach: false,
Tty: true,
}
if err := container.RunExec(dcli, cl, targetContainer.ID, &preHookExecOpts); err != nil {
return err
}
logrus.Infof("succesfully ran %s pre-hook command: %s", fullServiceName, rsConfig.preHookCmd)
}
backupReader, err := os.Open(backupPath)
if err != nil {
return err
}
content, err := archive.DecompressStream(backupReader)
if err != nil {
return err
}
// we use absolute paths so tar knows what to do. it will restore files
// according to the paths set in the compresed archive
restorePath := "/"
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(context.Background(), targetContainer.ID, restorePath, content, copyOpts); err != nil {
return err
}
logrus.Infof("restored %s to %s", backupPath, fullServiceName)
if rsConfig.postHookCmd != "" {
splitCmd := internal.SafeSplit(rsConfig.postHookCmd)
logrus.Debugf("split post-hook command for %s into %s", fullServiceName, splitCmd)
postHookExecOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: splitCmd,
Detach: false,
Tty: true,
}
if err := container.RunExec(dcli, cl, targetContainer.ID, &postHookExecOpts); err != nil {
return err
}
logrus.Infof("succesfully ran %s post-hook command: %s", fullServiceName, rsConfig.postHookCmd)
}
return nil
}
+194
View File
@@ -0,0 +1,194 @@
package app
import (
"context"
"fmt"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var appRollbackCommand = cli.Command{
Name: "rollback",
Aliases: []string{"rl"},
Usage: "Roll an app back to a previous version",
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
internal.ChaosFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
},
Before: internal.SubCommandBefore,
Description: `
This command rolls an app back to a previous version if one exists.
You may pass "--force/-f" to downgrade to the same version again. This can be
useful if the container runtime has gotten into a weird state.
This action could be destructive, please ensure you have a copy of your app
data beforehand.
Chas mode ("--chaos") will deploy your local checkout of a recipe as-is,
including unstaged changes and can be useful for live hacking and testing new
recipes.
`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName()
if !internal.Chaos {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
r, err := recipe.Get(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
if err := lint.LintForErrors(r); err != nil {
logrus.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("checking whether %s is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
logrus.Fatal(err)
}
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
catl, err := recipe.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil {
logrus.Fatal(err)
}
if len(versions) == 0 && !internal.Chaos {
logrus.Fatalf("no published releases for %s in the recipe catalogue?", app.Recipe)
}
var availableDowngrades []string
if deployedVersion == "unknown" {
availableDowngrades = versions
logrus.Warnf("failed to determine version of deployed %s", app.Name)
}
if deployedVersion != "unknown" && !internal.Chaos {
for _, version := range versions {
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
if err != nil {
logrus.Fatal(err)
}
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
logrus.Fatal(err)
}
if parsedVersion != parsedDeployedVersion && parsedVersion.IsLessThan(parsedDeployedVersion) {
availableDowngrades = append(availableDowngrades, version)
}
}
if len(availableDowngrades) == 0 {
logrus.Info("no available downgrades, you're on oldest ✌️")
return nil
}
}
availableDowngrades = internal.ReverseStringList(availableDowngrades)
var chosenDowngrade string
if !internal.Chaos {
if internal.Force || internal.NoInput {
chosenDowngrade = availableDowngrades[0]
logrus.Debugf("choosing %s as version to downgrade to (--force)", chosenDowngrade)
} else {
prompt := &survey.Select{
Message: fmt.Sprintf("Please select a downgrade (current version: %s):", deployedVersion),
Options: availableDowngrades,
}
if err := survey.AskOne(prompt, &chosenDowngrade); err != nil {
return err
}
}
}
if !internal.Chaos {
if err := recipe.EnsureVersion(app.Recipe, chosenDowngrade); err != nil {
logrus.Fatal(err)
}
}
if internal.Chaos {
logrus.Warn("chaos mode engaged")
var err error
chosenDowngrade, err = recipe.ChaosVersion(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
logrus.Fatal(err)
}
for k, v := range abraShEnv {
app.Env[k] = v
}
composeFiles, err := config.GetAppComposeFiles(app.Recipe, app.Env)
if err != nil {
logrus.Fatal(err)
}
deployOpts := stack.Deploy{
Composefiles: composeFiles,
Namespace: stackName,
Prune: false,
ResolveImage: stack.ResolveImageAlways,
}
compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
if err != nil {
logrus.Fatal(err)
}
if !internal.Force {
if err := internal.NewVersionOverview(app, deployedVersion, chosenDowngrade, ""); err != nil {
logrus.Fatal(err)
}
}
if err := stack.RunDeploy(cl, deployOpts, compose, app.StackName(), internal.DontWaitConverge); err != nil {
logrus.Fatal(err)
}
return nil
},
}
+100
View File
@@ -0,0 +1,100 @@
package app
import (
"context"
"errors"
"fmt"
"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/upstream/container"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var user string
var userFlag = &cli.StringFlag{
Name: "user, u",
Value: "",
Destination: &user,
}
var noTTY bool
var noTTYFlag = &cli.BoolFlag{
Name: "no-tty, t",
Destination: &noTTY,
}
var appRunCommand = cli.Command{
Name: "run",
Aliases: []string{"r"},
Flags: []cli.Flag{
internal.DebugFlag,
noTTYFlag,
userFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> <service> <args>...",
Usage: "Run a command in a service container",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if len(c.Args()) < 2 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <service> provided?"))
}
if len(c.Args()) < 3 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <args> provided?"))
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
serviceName := c.Args().Get(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 {
logrus.Fatal(err)
}
cmd := c.Args()[2:]
execCreateOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: cmd,
Detach: false,
Tty: true,
}
if user != "" {
execCreateOpts.User = user
}
if noTTY {
execCreateOpts.Tty = false
}
// FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli()
if err != nil {
logrus.Fatal(err)
}
if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
logrus.Fatal(err)
}
return nil
},
}
+348
View File
@@ -0,0 +1,348 @@
package app
import (
"context"
"errors"
"fmt"
"os"
"strconv"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/secret"
"github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var allSecrets bool
var allSecretsFlag = &cli.BoolFlag{
Name: "all, a",
Destination: &allSecrets,
Usage: "Generate all secrets",
}
var rmAllSecrets bool
var rmAllSecretsFlag = &cli.BoolFlag{
Name: "all, a",
Destination: &rmAllSecrets,
Usage: "Remove all secrets",
}
var appSecretGenerateCommand = cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate secrets",
ArgsUsage: "<domain> <secret> <version>",
Flags: []cli.Flag{
internal.DebugFlag,
allSecretsFlag,
internal.PassFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if len(c.Args()) == 1 && !allSecrets {
err := errors.New("missing arguments <secret>/<version> or '--all'")
internal.ShowSubcommandHelpAndError(c, err)
}
if c.Args().Get(1) != "" && allSecrets {
err := errors.New("cannot use '<secret> <version>' and '--all' together")
internal.ShowSubcommandHelpAndError(c, err)
}
secretsToCreate := make(map[string]string)
secretEnvVars := secret.ReadSecretEnvVars(app.Env)
if allSecrets {
secretsToCreate = secretEnvVars
} else {
secretName := c.Args().Get(1)
secretVersion := c.Args().Get(2)
matches := false
for sec := range secretEnvVars {
parsed := secret.ParseSecretEnvVarName(sec)
if secretName == parsed {
secretsToCreate[sec] = secretVersion
matches = true
}
}
if !matches {
logrus.Fatalf("%s doesn't exist in the env config?", secretName)
}
}
secretVals, err := secret.GenerateSecrets(secretsToCreate, app.StackName(), app.Server)
if err != nil {
logrus.Fatal(err)
}
if internal.Pass {
for name, data := range secretVals {
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
logrus.Fatal(err)
}
}
}
if len(secretVals) == 0 {
logrus.Warn("no secrets generated")
os.Exit(1)
}
tableCol := []string{"name", "value"}
table := formatter.CreateTable(tableCol)
for name, val := range secretVals {
table.Append([]string{name, val})
}
table.Render()
logrus.Warn("generated secrets are not shown again, please take note of them *now*")
return nil
},
}
var appSecretInsertCommand = cli.Command{
Name: "insert",
Aliases: []string{"i"},
Usage: "Insert secret",
Flags: []cli.Flag{
internal.DebugFlag,
internal.PassFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> <secret-name> <version> <data>",
BashComplete: autocomplete.AppNameComplete,
Description: `
This command inserts a secret into an app environment.
This can be useful when you want to manually generate secrets for an app
environment. Typically, you can let Abra generate them for you on app creation
(see "abra app new --secrets" for more).
Example:
abra app secret insert myapp db_pass v1 mySecretPassword
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if len(c.Args()) != 4 {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments?"))
}
name := c.Args().Get(1)
version := c.Args().Get(2)
data := c.Args().Get(3)
secretName := fmt.Sprintf("%s_%s_%s", app.StackName(), name, version)
if err := client.StoreSecret(secretName, data, app.Server); err != nil {
logrus.Fatal(err)
}
logrus.Infof("%s successfully stored on server", secretName)
if internal.Pass {
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
logrus.Fatal(err)
}
}
return nil
},
}
// secretRm removes a secret.
func secretRm(cl *dockerClient.Client, app config.App, secretName, parsed string) error {
if err := cl.SecretRemove(context.Background(), secretName); err != nil {
return err
}
logrus.Infof("deleted %s successfully from server", secretName)
if internal.PassRemove {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
return err
}
logrus.Infof("deleted %s successfully from local pass store", secretName)
}
return nil
}
var appSecretRmCommand = cli.Command{
Name: "remove",
Aliases: []string{"rm"},
Usage: "Remove a secret",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
rmAllSecretsFlag,
internal.PassRemoveFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> [<secret-name>]",
BashComplete: autocomplete.AppNameComplete,
Description: `
This command removes app secrets.
Example:
abra app secret remove myapp db_pass
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
secrets := secret.ReadSecretEnvVars(app.Env)
if c.Args().Get(1) != "" && rmAllSecrets {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<secret-name>' and '--all' together"))
}
if c.Args().Get(1) == "" && !rmAllSecrets {
internal.ShowSubcommandHelpAndError(c, errors.New("no secret(s) specified?"))
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
filters, err := app.Filters(false, false)
if err != nil {
logrus.Fatal(err)
}
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters})
if err != nil {
logrus.Fatal(err)
}
remoteSecretNames := make(map[string]bool)
for _, cont := range secretList {
remoteSecretNames[cont.Spec.Annotations.Name] = true
}
match := false
secretToRm := c.Args().Get(1)
for sec := range secrets {
secretName := secret.ParseSecretEnvVarName(sec)
secVal, err := secret.ParseSecretEnvVarValue(secrets[sec])
if err != nil {
logrus.Fatal(err)
}
secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, secVal.Version)
if _, ok := remoteSecretNames[secretRemoteName]; ok {
if secretToRm != "" {
if secretName == secretToRm {
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
logrus.Fatal(err)
}
return nil
}
} else {
match = true
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
logrus.Fatal(err)
}
}
}
}
if !match && secretToRm != "" {
logrus.Fatalf("%s doesn't exist on server?", secretToRm)
}
if !match {
logrus.Fatal("no secrets to remove?")
}
return nil
},
}
var appSecretLsCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
Usage: "List all secrets",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
secrets := secret.ReadSecretEnvVars(app.Env)
tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"}
table := formatter.CreateTable(tableCol)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
filters, err := app.Filters(false, false)
if err != nil {
logrus.Fatal(err)
}
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters})
if err != nil {
logrus.Fatal(err)
}
remoteSecretNames := make(map[string]bool)
for _, cont := range secretList {
remoteSecretNames[cont.Spec.Annotations.Name] = true
}
for sec := range secrets {
createdRemote := false
secretName := secret.ParseSecretEnvVarName(sec)
secVal, err := secret.ParseSecretEnvVarValue(secrets[sec])
if err != nil {
logrus.Fatal(err)
}
secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, secVal.Version)
if _, ok := remoteSecretNames[secretRemoteName]; ok {
createdRemote = true
}
tableRow := []string{secretName, secVal.Version, secretRemoteName, strconv.FormatBool(createdRemote)}
table.Append(tableRow)
}
if table.NumLines() > 0 {
table.Render()
} else {
logrus.Warnf("no secrets stored for %s", app.Name)
}
return nil
},
BashComplete: autocomplete.AppNameComplete,
}
var appSecretCommand = cli.Command{
Name: "secret",
Aliases: []string{"s"},
Usage: "Manage app secrets",
ArgsUsage: "<domain>",
Subcommands: []cli.Command{
appSecretGenerateCommand,
appSecretInsertCommand,
appSecretRmCommand,
appSecretLsCommand,
},
}
+61
View File
@@ -0,0 +1,61 @@
package app
import (
"context"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var appUndeployCommand = cli.Command{
Name: "undeploy",
Aliases: []string{"un"},
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
Usage: "Undeploy an app",
Description: `
This does not destroy any of the application data. However, you should remain
vigilant, as your swarm installation will consider any previously attached
volumes as eligiblef or pruning once undeployed.
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName()
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("checking whether %s is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
logrus.Fatal(err)
}
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
if err := internal.DeployOverview(app, deployedVersion, "continue with undeploy?"); err != nil {
logrus.Fatal(err)
}
rmOpts := stack.Remove{Namespaces: []string{app.StackName()}}
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
logrus.Fatal(err)
}
return nil
},
BashComplete: autocomplete.AppNameComplete,
}
+204
View File
@@ -0,0 +1,204 @@
package app
import (
"context"
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var appUpgradeCommand = cli.Command{
Name: "upgrade",
Aliases: []string{"up"},
Usage: "Upgrade an app",
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
internal.ChaosFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
},
Before: internal.SubCommandBefore,
Description: `
This command supports upgrading an app. You can use it to choose and roll out a
new upgrade to an existing app.
This command specifically supports incrementing the version of running apps, as
opposed to "abra app deploy <domain>" which will not change the version of a
deployed app.
You may pass "--force/-f" to upgrade to the same version again. This can be
useful if the container runtime has gotten into a weird state.
This action could be destructive, please ensure you have a copy of your app
data beforehand.
Chas mode ("--chaos") will deploy your local checkout of a recipe as-is,
including unstaged changes and can be useful for live hacking and testing new
recipes.
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName()
if !internal.Chaos {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
r, err := recipe.Get(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
if err := lint.LintForErrors(r); err != nil {
logrus.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("checking whether %s is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
logrus.Fatal(err)
}
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
catl, err := recipe.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil {
logrus.Fatal(err)
}
if len(versions) == 0 && !internal.Chaos {
logrus.Fatalf("no published releases for %s in the recipe catalogue?", app.Recipe)
}
var availableUpgrades []string
if deployedVersion == "unknown" {
availableUpgrades = versions
logrus.Warnf("failed to determine version of deployed %s", app.Name)
}
if deployedVersion != "unknown" && !internal.Chaos {
for _, version := range versions {
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
if err != nil {
logrus.Fatal(err)
}
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
logrus.Fatal(err)
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) {
availableUpgrades = append(availableUpgrades, version)
}
}
if len(availableUpgrades) == 0 && !internal.Force {
logrus.Infof("no available upgrades, you're on latest (%s) ✌️", deployedVersion)
return nil
}
}
availableUpgrades = internal.ReverseStringList(availableUpgrades)
var chosenUpgrade string
if len(availableUpgrades) > 0 && !internal.Chaos {
if internal.Force || internal.NoInput {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
logrus.Debugf("choosing %s as version to upgrade to", chosenUpgrade)
} else {
prompt := &survey.Select{
Message: fmt.Sprintf("Please select an upgrade (current version: %s):", deployedVersion),
Options: availableUpgrades,
}
if err := survey.AskOne(prompt, &chosenUpgrade); err != nil {
return err
}
}
}
// if release notes written after git tag published, read them before we
// check out the tag and then they'll appear to be missing. this covers
// when we obviously will forget to write release notes before publishing
releaseNotes, err := internal.GetReleaseNotes(app.Recipe, chosenUpgrade)
if err != nil {
return err
}
if !internal.Chaos {
if err := recipe.EnsureVersion(app.Recipe, chosenUpgrade); err != nil {
logrus.Fatal(err)
}
}
if internal.Chaos {
logrus.Warn("chaos mode engaged")
var err error
chosenUpgrade, err = recipe.ChaosVersion(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
logrus.Fatal(err)
}
for k, v := range abraShEnv {
app.Env[k] = v
}
composeFiles, err := config.GetAppComposeFiles(app.Recipe, app.Env)
if err != nil {
logrus.Fatal(err)
}
deployOpts := stack.Deploy{
Composefiles: composeFiles,
Namespace: stackName,
Prune: false,
ResolveImage: stack.ResolveImageAlways,
}
compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
if err != nil {
logrus.Fatal(err)
}
if err := internal.NewVersionOverview(app, deployedVersion, chosenUpgrade, releaseNotes); err != nil {
logrus.Fatal(err)
}
if err := stack.RunDeploy(cl, deployOpts, compose, app.StackName(), internal.DontWaitConverge); err != nil {
logrus.Fatal(err)
}
return nil
},
BashComplete: autocomplete.AppNameComplete,
}
+101
View File
@@ -0,0 +1,101 @@
package app
import (
"context"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// getImagePath returns the image name
func getImagePath(image string) (string, error) {
img, err := reference.ParseNormalizedNamed(image)
if err != nil {
return "", err
}
path := reference.Path(img)
path = formatter.StripTagMeta(path)
logrus.Debugf("parsed %s from %s", path, image)
return path, nil
}
var appVersionCommand = cli.Command{
Name: "version",
Aliases: []string{"v"},
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
Usage: "Show app versions",
Description: `
This command shows all information about versioning related to a deployed app.
This includes the individual image names, tags and digests. But also the Co-op
Cloud recipe version.
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName()
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("checking whether %s is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
logrus.Fatal(err)
}
if deployedVersion == "unknown" {
logrus.Fatalf("failed to determine version of deployed %s", app.Name)
}
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
recipeMeta, err := recipe.GetRecipeMeta(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
versionsMeta := make(map[string]recipe.ServiceMeta)
for _, recipeVersion := range recipeMeta.Versions {
if currentVersion, exists := recipeVersion[deployedVersion]; exists {
versionsMeta = currentVersion
}
}
if len(versionsMeta) == 0 {
logrus.Fatalf("could not retrieve deployed version (%s) from recipe catalogue?", deployedVersion)
}
tableCol := []string{"version", "service", "image", "digest"}
table := formatter.CreateTable(tableCol)
table.SetAutoMergeCellsByColumnIndex([]int{0})
for serviceName, versionMeta := range versionsMeta {
table.Append([]string{deployedVersion, serviceName, versionMeta.Image, versionMeta.Digest})
}
table.Render()
return nil
},
BashComplete: autocomplete.AppNameComplete,
}
+133
View File
@@ -0,0 +1,133 @@
package app
import (
"context"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var appVolumeListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
Usage: "List volumes associated with an app",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
filters, err := app.Filters(false, true)
if err != nil {
logrus.Fatal(err)
}
volumeList, err := client.GetVolumes(context.Background(), app.Server, filters)
if err != nil {
logrus.Fatal(err)
}
table := formatter.CreateTable([]string{"name", "created", "mounted"})
var volTable [][]string
for _, volume := range volumeList {
volRow := []string{volume.Name, volume.CreatedAt, volume.Mountpoint}
volTable = append(volTable, volRow)
}
table.AppendBulk(volTable)
if table.NumLines() > 0 {
table.Render()
} else {
logrus.Warnf("no volumes created for %s", app.Name)
}
return nil
},
}
var appVolumeRemoveCommand = cli.Command{
Name: "remove",
Usage: "Remove volume(s) associated with an app",
Description: `
This command supports removing volumes associated with an app. The app in
question must be undeployed before you try to remove volumes. See "abra app
undeploy <domain>" for more.
The command is interactive and will show a multiple select input which allows
you to make a seclection. Use the "?" key to see more help on navigating this
interface.
Passing "--force/-f" will select all volumes for removal. Be careful.
`,
ArgsUsage: "<domain>",
Aliases: []string{"rm"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
filters, err := app.Filters(false, true)
if err != nil {
logrus.Fatal(err)
}
volumeList, err := client.GetVolumes(context.Background(), app.Server, filters)
if err != nil {
logrus.Fatal(err)
}
volumeNames := client.GetVolumeNames(volumeList)
var volumesToRemove []string
if !internal.Force && !internal.NoInput {
volumesPrompt := &survey.MultiSelect{
Message: "which volumes do you want to remove?",
Help: "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled",
VimMode: true,
Options: volumeNames,
Default: volumeNames,
}
if err := survey.AskOne(volumesPrompt, &volumesToRemove); err != nil {
logrus.Fatal(err)
}
}
if internal.Force || internal.NoInput {
volumesToRemove = volumeNames
}
err = client.RemoveVolumes(context.Background(), app.Server, volumesToRemove, internal.Force)
if err != nil {
logrus.Fatal(err)
}
logrus.Info("volumes removed successfully")
return nil
},
BashComplete: autocomplete.AppNameComplete,
}
var appVolumeCommand = cli.Command{
Name: "volume",
Aliases: []string{"vl"},
Usage: "Manage app volumes",
ArgsUsage: "<domain>",
Subcommands: []cli.Command{
appVolumeListCommand,
appVolumeRemoveCommand,
},
}
+317
View File
@@ -0,0 +1,317 @@
package catalogue
import (
"encoding/json"
"fmt"
"io/ioutil"
"path"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/limit"
"coopcloud.tech/abra/pkg/recipe"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// CatalogueSkipList is all the repos that are not recipes.
var CatalogueSkipList = map[string]bool{
"abra": true,
"abra-apps": true,
"abra-aur": true,
"abra-bash": true,
"abra-capsul": true,
"abra-gandi": true,
"abra-hetzner": true,
"apps": true,
"aur-abra-git": true,
"auto-apps-json": true,
"auto-mirror": true,
"backup-bot": true,
"backup-bot-two": true,
"beta.coopcloud.tech": true,
"comrade-renovate-bot": true,
"coopcloud.tech": true,
"coturn": true,
"docker-cp-deploy": true,
"docker-dind-bats-kcov": true,
"docs.coopcloud.tech": true,
"drone-abra": true,
"example": true,
"gardening": true,
"go-abra": true,
"organising": true,
"outline-with-patch": true,
"pyabra": true,
"radicle-seed-node": true,
"recipes-catalogue-json": true,
"recipes-wishlist": true,
"recipes.coopcloud.tech": true,
"stack-ssh-deploy": true,
"swarm-cronjob": true,
"tagcmp": true,
"traefik-cert-dumper": true,
"tyop": true,
}
var catalogueGenerateCommand = cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate the recipe catalogue",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.PublishFlag,
internal.DryFlag,
internal.SkipUpdatesFlag,
},
Before: internal.SubCommandBefore,
Description: `
This command generates a new copy of the recipe catalogue which can be found on:
https://recipes.coopcloud.tech
It polls the entire git.coopcloud.tech/coop-cloud/... recipe repository
listing, parses README.md and git tags of those repositories to produce recipe
metadata and produces a recipes JSON file.
It is possible to generate new metadata for a single recipe by passing
<recipe>. The existing local catalogue will be updated, not overwritten.
It is quite easy to get rate limited by Docker Hub when running this command.
If you have a Hub account you can have Abra log you in to avoid this. Pass
"--user" and "--pass".
Push your new release git.coopcloud.tech with "-p/--publish". This requires
that you have permission to git push to these repositories and have your SSH
keys configured on your account.
`,
ArgsUsage: "[<recipe>]",
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
if recipeName != "" {
internal.ValidateRecipe(c, true)
}
repos, err := recipe.ReadReposMetadata()
if err != nil {
logrus.Fatal(err)
}
var barLength int
var logMsg string
if recipeName != "" {
barLength = 1
logMsg = fmt.Sprintf("ensuring %v recipe is cloned & up-to-date", barLength)
} else {
barLength = len(repos)
logMsg = fmt.Sprintf("ensuring %v recipes are cloned & up-to-date, this could take some time...", barLength)
}
if !internal.SkipUpdates {
logrus.Warn(logMsg)
if err := updateRepositories(repos, recipeName); err != nil {
logrus.Fatal(err)
}
}
catl := make(recipe.RecipeCatalogue)
catlBar := formatter.CreateProgressbar(barLength, "generating catalogue metadata...")
for _, recipeMeta := range repos {
if recipeName != "" && recipeName != recipeMeta.Name {
catlBar.Add(1)
continue
}
if _, exists := CatalogueSkipList[recipeMeta.Name]; exists {
catlBar.Add(1)
continue
}
versions, err := recipe.GetRecipeVersions(recipeMeta.Name)
if err != nil {
logrus.Warn(err)
}
features, category, err := recipe.GetRecipeFeaturesAndCategory(recipeMeta.Name)
if err != nil {
logrus.Warn(err)
}
catl[recipeMeta.Name] = recipe.RecipeMeta{
Name: recipeMeta.Name,
Repository: recipeMeta.CloneURL,
SSHURL: recipeMeta.SSHURL,
Icon: recipeMeta.AvatarURL,
DefaultBranch: recipeMeta.DefaultBranch,
Description: recipeMeta.Description,
Website: recipeMeta.Website,
Versions: versions,
Category: category,
Features: features,
}
catlBar.Add(1)
}
recipesJSON, err := json.MarshalIndent(catl, "", " ")
if err != nil {
logrus.Fatal(err)
}
if recipeName == "" {
if err := ioutil.WriteFile(config.RECIPES_JSON, recipesJSON, 0764); err != nil {
logrus.Fatal(err)
}
} else {
catlFS, err := recipe.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
catlFS[recipeName] = catl[recipeName]
updatedRecipesJSON, err := json.MarshalIndent(catlFS, "", " ")
if err != nil {
logrus.Fatal(err)
}
if err := ioutil.WriteFile(config.RECIPES_JSON, updatedRecipesJSON, 0764); err != nil {
logrus.Fatal(err)
}
}
logrus.Infof("generated new recipe catalogue in %s", config.RECIPES_JSON)
cataloguePath := path.Join(config.ABRA_DIR, "catalogue")
if internal.Publish {
isClean, err := gitPkg.IsClean(cataloguePath)
if err != nil {
logrus.Fatal(err)
}
if isClean {
if !internal.Dry {
logrus.Fatalf("no changes discovered in %s, nothing to publish?", cataloguePath)
}
}
msg := "chore: publish new catalogue release changes"
if err := gitPkg.Commit(cataloguePath, "**.json", msg, internal.Dry); err != nil {
logrus.Fatal(err)
}
repo, err := git.PlainOpen(cataloguePath)
if err != nil {
logrus.Fatal(err)
}
sshURL := fmt.Sprintf(config.SSH_URL_TEMPLATE, config.CATALOGUE_JSON_REPO_NAME)
if err := gitPkg.CreateRemote(repo, "origin-ssh", sshURL, internal.Dry); err != nil {
logrus.Fatal(err)
}
if err := gitPkg.Push(cataloguePath, "origin-ssh", false, internal.Dry); err != nil {
logrus.Fatal(err)
}
}
repo, err := git.PlainOpen(cataloguePath)
if err != nil {
logrus.Fatal(err)
}
head, err := repo.Head()
if err != nil {
logrus.Fatal(err)
}
if !internal.Dry && internal.Publish {
url := fmt.Sprintf("%s/%s/commit/%s", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME, head.Hash())
logrus.Infof("new changes published: %s", url)
}
if internal.Dry {
logrus.Info("dry run: no changes published")
}
return nil
},
BashComplete: autocomplete.RecipeNameComplete,
}
// CatalogueCommand defines the `abra catalogue` command and sub-commands.
var CatalogueCommand = cli.Command{
Name: "catalogue",
Usage: "Manage the recipe catalogue",
Aliases: []string{"c"},
ArgsUsage: "<recipe>",
Description: "This command helps recipe packagers interact with the recipe catalogue",
Subcommands: []cli.Command{
catalogueGenerateCommand,
},
}
func updateRepositories(repos recipe.RepoCatalogue, recipeName string) error {
var barLength int
if recipeName != "" {
barLength = 1
} else {
barLength = len(repos)
}
cloneLimiter := limit.New(10)
retrieveBar := formatter.CreateProgressbar(barLength, "ensuring recipes are cloned & up-to-date...")
ch := make(chan string, barLength)
for _, repoMeta := range repos {
go func(rm recipe.RepoMeta) {
cloneLimiter.Begin()
defer cloneLimiter.End()
if recipeName != "" && recipeName != rm.Name {
ch <- rm.Name
retrieveBar.Add(1)
return
}
if _, exists := CatalogueSkipList[rm.Name]; exists {
ch <- rm.Name
retrieveBar.Add(1)
return
}
recipeDir := path.Join(config.RECIPES_DIR, rm.Name)
if err := gitPkg.Clone(recipeDir, rm.CloneURL); err != nil {
logrus.Fatal(err)
}
isClean, err := gitPkg.IsClean(recipeDir)
if err != nil {
logrus.Fatal(err)
}
if !isClean {
logrus.Fatalf("%s has locally unstaged changes", rm.Name)
}
if err := recipe.EnsureUpToDate(rm.Name); err != nil {
logrus.Fatal(err)
}
ch <- rm.Name
retrieveBar.Add(1)
}(repoMeta)
}
for range repos {
<-ch // wait for everything
}
return nil
}
+201
View File
@@ -0,0 +1,201 @@
// Package cli provides the interface for the command-line.
package cli
import (
"errors"
"fmt"
"os"
"os/exec"
"path"
"coopcloud.tech/abra/cli/app"
"coopcloud.tech/abra/cli/catalogue"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/cli/recipe"
"coopcloud.tech/abra/cli/record"
"coopcloud.tech/abra/cli/server"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/web"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// AutoCompleteCommand helps people set up auto-complete in their shells
var AutoCompleteCommand = cli.Command{
Name: "autocomplete",
Aliases: []string{"ac"},
Usage: "Configure shell autocompletion (recommended)",
Description: `
This command helps set up autocompletion in your shell by downloading the
relevant autocompletion files and laying out what additional information must
be loaded.
Example:
abra autocomplete bash
Supported shells are as follows:
fizsh
zsh
bash
`,
ArgsUsage: "<shell>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Action: func(c *cli.Context) error {
shellType := c.Args().First()
if shellType == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no shell provided"))
}
supportedShells := map[string]bool{
"bash": true,
"zsh": true,
"fizsh": true,
}
if _, ok := supportedShells[shellType]; !ok {
logrus.Fatalf("%s is not a supported shell right now, sorry", shellType)
}
if shellType == "fizsh" {
shellType = "zsh" // handled the same on the autocompletion side
}
autocompletionDir := path.Join(config.ABRA_DIR, "autocompletion")
if err := os.Mkdir(autocompletionDir, 0764); err != nil {
if !os.IsExist(err) {
logrus.Fatal(err)
}
logrus.Debugf("%s already created", autocompletionDir)
}
autocompletionFile := path.Join(config.ABRA_DIR, "autocompletion", shellType)
if _, err := os.Stat(autocompletionFile); err != nil && os.IsNotExist(err) {
url := fmt.Sprintf("https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/autocomplete/%s", shellType)
logrus.Infof("fetching %s", url)
if err := web.GetFile(autocompletionFile, url); err != nil {
logrus.Fatal(err)
}
}
switch shellType {
case "bash":
fmt.Println(fmt.Sprintf(`
# Run the following commands to install autocompletion
sudo mkdir /etc/bash_completion.d/
sudo cp %s /etc/bash_completion.d/abra
echo "source /etc/bash_completion.d/abra" >> ~/.bashrc
# And finally run "abra app ps <hit tab key>" to test things are working, you should see app domains listed!
`, autocompletionFile))
case "zsh":
fmt.Println(fmt.Sprintf(`
# Run the following commands to install autocompletion
sudo mkdir /etc/zsh/completion.d/
sudo cp %s /etc/zsh/completion.d/abra
echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc
# And finally run "abra app ps <hit tab key>" to test things are working, you should see app domains listed!
`, autocompletionFile))
}
return nil
},
}
// UpgradeCommand upgrades abra in-place.
var UpgradeCommand = cli.Command{
Name: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade Abra itself",
Description: `
This command allows you to upgrade Abra in-place with the latest stable or
release candidate.
If you would like to install the latest release candidate, please pass the
"-r/--rc" option. Please bear in mind that the latest release candidate may
have some catastrophic bugs contained in it. In any case, thank you very much
for the testing efforts!
`,
Flags: []cli.Flag{internal.RCFlag},
Action: func(c *cli.Context) error {
mainURL := "https://install.abra.coopcloud.tech"
cmd := exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash", mainURL))
if internal.RC {
releaseCandidateURL := "https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer"
cmd = exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash -s -- --rc", releaseCandidateURL))
}
logrus.Debugf("attempting to run %s", cmd)
if err := internal.RunCmd(cmd); err != nil {
logrus.Fatal(err)
}
return nil
},
}
func newAbraApp(version, commit string) *cli.App {
app := &cli.App{
Name: "abra",
Usage: `The Co-op Cloud command-line utility belt 🎩🐇
____ ____ _ _
/ ___|___ ___ _ __ / ___| | ___ _ _ __| |
| | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |
| |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |
\____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|
|_|
`,
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Commands: []cli.Command{
app.AppCommand,
server.ServerCommand,
recipe.RecipeCommand,
catalogue.CatalogueCommand,
record.RecordCommand,
UpgradeCommand,
AutoCompleteCommand,
},
BashComplete: autocomplete.SubcommandComplete,
}
app.EnableBashCompletion = true
app.Before = func(c *cli.Context) error {
paths := []string{
config.ABRA_DIR,
path.Join(config.SERVERS_DIR),
path.Join(config.RECIPES_DIR),
path.Join(config.VENDOR_DIR),
path.Join(config.BACKUP_DIR),
}
for _, path := range paths {
if err := os.Mkdir(path, 0764); err != nil {
if !os.IsExist(err) {
logrus.Fatal(err)
}
continue
}
}
logrus.Debugf("abra version %s, commit %s", version, commit)
return nil
}
return app
}
// RunApp runs CLI abra app.
func RunApp(version, commit string) {
app := newAbraApp(version, commit)
if err := app.Run(os.Args); err != nil {
logrus.Fatal(err)
}
}
+35
View File
@@ -0,0 +1,35 @@
package internal
import (
"strings"
)
// SafeSplit splits up a string into a list of commands safely.
func SafeSplit(s string) []string {
split := strings.Split(s, " ")
var result []string
var inquote string
var block string
for _, i := range split {
if inquote == "" {
if strings.HasPrefix(i, "'") || strings.HasPrefix(i, "\"") {
inquote = string(i[0])
block = strings.TrimPrefix(i, inquote) + " "
} else {
result = append(result, i)
}
} else {
if !strings.HasSuffix(i, inquote) {
block += i + " "
} else {
block += strings.TrimSuffix(i, inquote)
inquote = ""
result = append(result, block)
block = ""
}
}
}
return result
}
+433
View File
@@ -0,0 +1,433 @@
package internal
import (
"os"
logrusStack "github.com/Gurpartap/logrus-stack"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// Secrets stores the variable from SecretsFlag
var Secrets bool
// SecretsFlag turns on/off automatically generating secrets
var SecretsFlag = &cli.BoolFlag{
Name: "secrets, S",
Usage: "Automatically generate secrets",
Destination: &Secrets,
}
// Pass stores the variable from PassFlag
var Pass bool
// PassFlag turns on/off storing generated secrets in pass
var PassFlag = &cli.BoolFlag{
Name: "pass, p",
Usage: "Store the generated secrets in a local pass store",
Destination: &Pass,
}
// PassRemove stores the variable for PassRemoveFlag
var PassRemove bool
// PassRemoveFlag turns on/off removing generated secrets from pass
var PassRemoveFlag = &cli.BoolFlag{
Name: "pass, p",
Usage: "Remove generated secrets from a local pass store",
Destination: &PassRemove,
}
// Force force functionality without asking.
var Force bool
// ForceFlag turns on/off force functionality.
var ForceFlag = &cli.BoolFlag{
Name: "force, f",
Usage: "Perform action without further prompt. Use with care!",
Destination: &Force,
}
// Chaos engages chaos mode.
var Chaos bool
// ChaosFlag turns on/off chaos functionality.
var ChaosFlag = &cli.BoolFlag{
Name: "chaos, C",
Usage: "Deploy uncommitted recipes changes. Use with care!",
Destination: &Chaos,
}
// DNSProvider specifies a DNS provider.
var DNSProvider string
// DNSProviderFlag selects a DNS provider.
var DNSProviderFlag = &cli.StringFlag{
Name: "provider, p",
Value: "",
Usage: "DNS provider",
Destination: &DNSProvider,
}
var NoInput bool
var NoInputFlag = &cli.BoolFlag{
Name: "no-input, n",
Usage: "Toggle non-interactive mode",
Destination: &NoInput,
}
var DNSType string
var DNSTypeFlag = &cli.StringFlag{
Name: "record-type, rt",
Value: "",
Usage: "Domain name record type (e.g. A)",
Destination: &DNSType,
}
var DNSName string
var DNSNameFlag = &cli.StringFlag{
Name: "record-name, rn",
Value: "",
Usage: "Domain name record name (e.g. mysubdomain)",
Destination: &DNSName,
}
var DNSValue string
var DNSValueFlag = &cli.StringFlag{
Name: "record-value, rv",
Value: "",
Usage: "Domain name record value (e.g. 192.168.1.1)",
Destination: &DNSValue,
}
var DNSTTL string
var DNSTTLFlag = &cli.StringFlag{
Name: "record-ttl, rl",
Value: "600s",
Usage: "Domain name TTL value (seconds)",
Destination: &DNSTTL,
}
var DNSPriority int
var DNSPriorityFlag = &cli.IntFlag{
Name: "record-priority, rp",
Value: 10,
Usage: "Domain name priority value",
Destination: &DNSPriority,
}
var ServerProvider string
var ServerProviderFlag = &cli.StringFlag{
Name: "provider, p",
Usage: "3rd party server provider",
Destination: &ServerProvider,
}
var CapsulInstanceURL string
var CapsulInstanceURLFlag = &cli.StringFlag{
Name: "capsul-url, cu",
Value: "yolo.servers.coop",
Usage: "capsul instance URL",
Destination: &CapsulInstanceURL,
}
var CapsulName string
var CapsulNameFlag = &cli.StringFlag{
Name: "capsul-name, cn",
Value: "",
Usage: "capsul name",
Destination: &CapsulName,
}
var CapsulType string
var CapsulTypeFlag = &cli.StringFlag{
Name: "capsul-type, ct",
Value: "f1-xs",
Usage: "capsul type",
Destination: &CapsulType,
}
var CapsulImage string
var CapsulImageFlag = &cli.StringFlag{
Name: "capsul-image, ci",
Value: "debian10",
Usage: "capsul image",
Destination: &CapsulImage,
}
var CapsulSSHKeys cli.StringSlice
var CapsulSSHKeysFlag = &cli.StringSliceFlag{
Name: "capsul-ssh-keys, cs",
Usage: "capsul SSH key",
Value: &CapsulSSHKeys,
}
var CapsulAPIToken string
var CapsulAPITokenFlag = &cli.StringFlag{
Name: "capsul-token, ca",
Usage: "capsul API token",
EnvVar: "CAPSUL_TOKEN",
Destination: &CapsulAPIToken,
}
var HetznerCloudName string
var HetznerCloudNameFlag = &cli.StringFlag{
Name: "hetzner-name, hn",
Value: "",
Usage: "hetzner cloud name",
Destination: &HetznerCloudName,
}
var HetznerCloudType string
var HetznerCloudTypeFlag = &cli.StringFlag{
Name: "hetzner-type, ht",
Usage: "hetzner cloud type",
Destination: &HetznerCloudType,
Value: "cx11",
}
var HetznerCloudImage string
var HetznerCloudImageFlag = &cli.StringFlag{
Name: "hetzner-image, hi",
Usage: "hetzner cloud image",
Value: "debian-10",
Destination: &HetznerCloudImage,
}
var HetznerCloudSSHKeys cli.StringSlice
var HetznerCloudSSHKeysFlag = &cli.StringSliceFlag{
Name: "hetzner-ssh-keys, hs",
Usage: "hetzner cloud SSH keys (e.g. me@foo.com)",
Value: &HetznerCloudSSHKeys,
}
var HetznerCloudLocation string
var HetznerCloudLocationFlag = &cli.StringFlag{
Name: "hetzner-location, hl",
Usage: "hetzner cloud server location",
Value: "hel1",
Destination: &HetznerCloudLocation,
}
var HetznerCloudAPIToken string
var HetznerCloudAPITokenFlag = &cli.StringFlag{
Name: "hetzner-token, ha",
Usage: "hetzner cloud API token",
EnvVar: "HCLOUD_TOKEN",
Destination: &HetznerCloudAPIToken,
}
// Debug stores the variable from DebugFlag.
var Debug bool
// DebugFlag turns on/off verbose logging down to the DEBUG level.
var DebugFlag = &cli.BoolFlag{
Name: "debug, d",
Destination: &Debug,
Usage: "Show DEBUG messages",
}
// RC signifies the latest release candidate
var RC bool
// RCFlag chooses the latest release candidate for install
var RCFlag = &cli.BoolFlag{
Name: "rc, r",
Destination: &RC,
Usage: "Insatll the latest release candidate",
}
var Major bool
var MajorFlag = &cli.BoolFlag{
Name: "major, x",
Usage: "Increase the major part of the version",
Destination: &Major,
}
var Minor bool
var MinorFlag = &cli.BoolFlag{
Name: "minor, y",
Usage: "Increase the minor part of the version",
Destination: &Minor,
}
var Patch bool
var PatchFlag = &cli.BoolFlag{
Name: "patch, z",
Usage: "Increase the patch part of the version",
Destination: &Patch,
}
var Dry bool
var DryFlag = &cli.BoolFlag{
Name: "dry-run, r",
Usage: "Only reports changes that would be made",
Destination: &Dry,
}
var Publish bool
var PublishFlag = &cli.BoolFlag{
Name: "publish, p",
Usage: "Publish changes to git.coopcloud.tech",
Destination: &Publish,
}
var Domain string
var DomainFlag = &cli.StringFlag{
Name: "domain, D",
Value: "",
Usage: "Choose a domain name",
Destination: &Domain,
}
var NewAppServer string
var NewAppServerFlag = &cli.StringFlag{
Name: "server, s",
Value: "",
Usage: "Show apps of a specific server",
Destination: &NewAppServer,
}
var NoDomainChecks bool
var NoDomainChecksFlag = &cli.BoolFlag{
Name: "no-domain-checks, D",
Usage: "Disable app domain sanity checks",
Destination: &NoDomainChecks,
}
var StdErrOnly bool
var StdErrOnlyFlag = &cli.BoolFlag{
Name: "stderr, s",
Usage: "Only tail stderr",
Destination: &StdErrOnly,
}
var DontWaitConverge bool
var DontWaitConvergeFlag = &cli.BoolFlag{
Name: "no-converge-checks, c",
Usage: "Don't wait for converge logic checks",
Destination: &DontWaitConverge,
}
var Watch bool
var WatchFlag = &cli.BoolFlag{
Name: "watch, w",
Usage: "Watch status by polling repeatedly",
Destination: &Watch,
}
var OnlyErrors bool
var OnlyErrorFlag = &cli.BoolFlag{
Name: "errors, e",
Usage: "Only show errors",
Destination: &OnlyErrors,
}
var SkipUpdates bool
var SkipUpdatesFlag = &cli.BoolFlag{
Name: "skip-updates, s",
Usage: "Skip updating recipe repositories",
Destination: &SkipUpdates,
}
var AllTags bool
var AllTagsFlag = &cli.BoolFlag{
Name: "all-tags, a",
Usage: "List all tags, not just upgrades",
Destination: &AllTags,
}
// SSHFailMsg is a hopefully helpful SSH failure message
var SSHFailMsg = `
Woops, Abra is unable to connect to connect to %s.
Here are a few tips for debugging your local SSH config. Abra uses plain 'ol
SSH to make connections to servers, so if your SSH config is working, Abra is
working.
In the first place, Abra will always try to read your Docker context connection
string for SSH connection details. You can view your server context configs
with the following command. Are they correct?
abra server ls
Is your ssh-agent running? You can start it by running the following command:
eval "$(ssh-agent)"
If your SSH private key loaded? You can check by running the following command:
ssh-add -L
If, you can add it with:
ssh-add ~/.ssh/<private-key-part>
If you are using a non-default public/private key, you can configure this in
your ~/.ssh/config file which Abra will read in order to figure out connection
details:
Host foo.coopcloud.tech
Hostname foo.coopcloud.tech
User bar
Port 12345
IdentityFile ~/.ssh/bar@foo.coopcloud.tech
If you're only using password authentication, you can use the following config:
Host foo.coopcloud.tech
Hostname foo.coopcloud.tech
User bar
Port 12345
PreferredAuthentications=password
PubkeyAuthentication=no
Good luck!
`
var ServerAddFailMsg = `
Failed to add server %s.
This could be caused by two things.
Abra isn't picking up your SSH configuration or you need to specify it on the
command-line (e.g you use a non-standard port or username to connect). Run
"server add" with "-d/--debug" to learn more about what Abra is doing under the
hood.
Docker is not installed on your server. You can pass "-p/--provision" to
install Docker and initialise Docker Swarm mode. See help output for "server
add"
See "abra server add -h" for more.
`
// SubCommandBefore wires up pre-action machinery (e.g. --debug handling).
func SubCommandBefore(c *cli.Context) error {
if Debug {
logrus.SetLevel(logrus.DebugLevel)
logrus.SetFormatter(&logrus.TextFormatter{})
logrus.SetOutput(os.Stderr)
logrus.AddHook(logrusStack.StandardHook())
}
return nil
}
+39
View File
@@ -0,0 +1,39 @@
package internal
import (
"bufio"
"fmt"
"os/exec"
)
// 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
}
+266
View File
@@ -0,0 +1,266 @@
package internal
import (
"context"
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/dns"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// DeployAction is the main command-line action for this package
func DeployAction(c *cli.Context) error {
app := ValidateApp(c)
if !Chaos {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
r, err := recipe.Get(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
if err := lint.LintForErrors(r); err != nil {
logrus.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("checking whether %s is already deployed", app.StackName())
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
logrus.Fatal(err)
}
if isDeployed {
if Force || Chaos {
logrus.Warnf("%s is already deployed but continuing (--force/--chaos)", app.Name)
} else {
logrus.Fatalf("%s is already deployed", app.Name)
}
}
version := deployedVersion
if version == "unknown" && !Chaos {
catl, err := recipe.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil {
logrus.Fatal(err)
}
if len(versions) > 0 {
version = versions[len(versions)-1]
logrus.Debugf("choosing %s as version to deploy", version)
if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
logrus.Fatal(err)
}
} else {
head, err := git.GetRecipeHead(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
version = formatter.SmallSHA(head.String())
logrus.Warn("no versions detected, using latest commit")
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
}
if version == "unknown" && !Chaos {
logrus.Debugf("choosing %s as version to deploy", version)
if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
logrus.Fatal(err)
}
}
if version != "unknown" && !Chaos {
if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
logrus.Fatal(err)
}
}
if Chaos {
logrus.Warnf("chaos mode engaged")
var err error
version, err = recipe.ChaosVersion(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
logrus.Fatal(err)
}
for k, v := range abraShEnv {
app.Env[k] = v
}
composeFiles, err := config.GetAppComposeFiles(app.Recipe, app.Env)
if err != nil {
logrus.Fatal(err)
}
deployOpts := stack.Deploy{
Composefiles: composeFiles,
Namespace: app.StackName(),
Prune: false,
ResolveImage: stack.ResolveImageAlways,
}
compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
if err != nil {
logrus.Fatal(err)
}
if err := DeployOverview(app, version, "continue with deployment?"); err != nil {
logrus.Fatal(err)
}
if !NoDomainChecks {
domainName := app.Env["DOMAIN"]
if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
logrus.Fatal(err)
}
} else {
logrus.Warn("skipping domain checks as requested")
}
if err := stack.RunDeploy(cl, deployOpts, compose, app.Name, DontWaitConverge); err != nil {
logrus.Fatal(err)
}
return nil
}
// DeployOverview shows a deployment overview
func DeployOverview(app config.App, version, message string) error {
tableCol := []string{"server", "recipe", "config", "domain", "version"}
table := formatter.CreateTable(tableCol)
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
}
server := app.Server
if app.Server == "default" {
server = "local"
}
table.Append([]string{server, app.Recipe, deployConfig, app.Domain, version})
table.Render()
if NoInput {
return nil
}
response := false
prompt := &survey.Confirm{
Message: message,
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
logrus.Fatal("exiting as requested")
}
return nil
}
// NewVersionOverview shows an upgrade or downgrade overview
func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes string) error {
tableCol := []string{"server", "recipe", "config", "domain", "current version", "to be deployed"}
table := formatter.CreateTable(tableCol)
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
}
server := app.Server
if app.Server == "default" {
server = "local"
}
table.Append([]string{server, app.Recipe, deployConfig, app.Domain, currentVersion, newVersion})
table.Render()
if releaseNotes == "" {
var err error
releaseNotes, err = GetReleaseNotes(app.Recipe, newVersion)
if err != nil {
return err
}
}
if releaseNotes != "" && newVersion != "" {
fmt.Println()
fmt.Println(fmt.Sprintf("%s release notes:\n\n%s", newVersion, releaseNotes))
} else {
logrus.Warnf("no release notes available for %s", newVersion)
}
if NoInput {
return nil
}
response := false
prompt := &survey.Confirm{
Message: "continue with deployment?",
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
logrus.Fatal("exiting as requested")
}
return nil
}
// GetReleaseNotes prints release notes for a recipe version
func GetReleaseNotes(recipeName, version string) (string, error) {
if version == "" {
return "", nil
}
fpath := path.Join(config.RECIPES_DIR, recipeName, "release", version)
if _, err := os.Stat(fpath); !os.IsNotExist(err) {
releaseNotes, err := ioutil.ReadFile(fpath)
if err != nil {
return "", err
}
return string(releaseNotes), nil
}
return "", nil
}
+18
View File
@@ -0,0 +1,18 @@
package internal
import (
"os"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// ShowSubcommandHelpAndError exits the program on error, logs the error to the
// terminal, and shows the help command.
func ShowSubcommandHelpAndError(c *cli.Context, err interface{}) {
if err2 := cli.ShowSubcommandHelp(c); err2 != nil {
logrus.Error(err2)
}
logrus.Error(err)
os.Exit(1)
}
+10
View File
@@ -0,0 +1,10 @@
package internal
// ReverseStringList reverses a list of a strings. Roll on Go generics.
func ReverseStringList(strings []string) []string {
for i, j := 0, len(strings)-1; i < j; i, j = i+1, j-1 {
strings[i], strings[j] = strings[j], strings[i]
}
return strings
}
+196
View File
@@ -0,0 +1,196 @@
package internal
import (
"fmt"
"path"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret"
"coopcloud.tech/abra/pkg/ssh"
"github.com/AlecAivazis/survey/v2"
"github.com/olekukonko/tablewriter"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// AppSecrets represents all app secrest
type AppSecrets map[string]string
// RecipeName is used for configuring recipe name programmatically
var RecipeName string
// createSecrets creates all secrets for a new app.
func createSecrets(sanitisedAppName string) (AppSecrets, error) {
appEnvPath := path.Join(config.ABRA_DIR, "servers", NewAppServer, fmt.Sprintf("%s.env", Domain))
appEnv, err := config.ReadEnv(appEnvPath)
if err != nil {
return nil, err
}
secretEnvVars := secret.ReadSecretEnvVars(appEnv)
secrets, err := secret.GenerateSecrets(secretEnvVars, sanitisedAppName, NewAppServer)
if err != nil {
return nil, err
}
if Pass {
for secretName := range secrets {
secretValue := secrets[secretName]
if err := secret.PassInsertSecret(secretValue, secretName, Domain, 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 recipe.Recipe, server string) error {
if Domain == "" && !NoInput {
prompt := &survey.Input{
Message: "Specify app domain",
Default: fmt.Sprintf("%s.%s", recipe.Name, server),
}
if err := survey.AskOne(prompt, &Domain); err != nil {
return err
}
}
if Domain == "" {
return fmt.Errorf("no domain provided")
}
return nil
}
// promptForSecrets asks if we should generate secrets for a new app.
func promptForSecrets(appName string) error {
app, err := app.Get(appName)
if err != nil {
return err
}
secretEnvVars := secret.ReadSecretEnvVars(app.Env)
if len(secretEnvVars) == 0 {
logrus.Debugf("%s has no secrets to generate, skipping...", app.Recipe)
return nil
}
if !Secrets && !NoInput {
prompt := &survey.Confirm{
Message: "Generate app secrets?",
}
if err := survey.AskOne(prompt, &Secrets); err != nil {
return err
}
}
return nil
}
// ensureServerFlag checks if the server flag was used. if not, asks the user for it.
func ensureServerFlag() error {
servers, err := config.GetServers()
if err != nil {
return err
}
if NewAppServer == "" && !NoInput {
prompt := &survey.Select{
Message: "Select app server:",
Options: servers,
}
if err := survey.AskOne(prompt, &NewAppServer); err != nil {
return err
}
}
if NewAppServer == "" {
return fmt.Errorf("no server provided")
}
return nil
}
// NewAction is the new app creation logic
func NewAction(c *cli.Context) error {
recipe := ValidateRecipeWithPrompt(c, false)
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err)
}
if err := ensureServerFlag(); err != nil {
logrus.Fatal(err)
}
if err := ensureDomainFlag(recipe, NewAppServer); err != nil {
logrus.Fatal(err)
}
sanitisedAppName := config.SanitiseAppName(Domain)
logrus.Debugf("%s sanitised as %s for new app", Domain, sanitisedAppName)
if err := config.TemplateAppEnvSample(recipe.Name, Domain, NewAppServer, Domain); err != nil {
logrus.Fatal(err)
}
if err := promptForSecrets(Domain); err != nil {
logrus.Fatal(err)
}
var secrets AppSecrets
var secretTable *tablewriter.Table
if Secrets {
if err := ssh.EnsureHostKey(NewAppServer); err != nil {
logrus.Fatal(err)
}
var err error
secrets, err = createSecrets(sanitisedAppName)
if err != nil {
logrus.Fatal(err)
}
secretCols := []string{"Name", "Value"}
secretTable = formatter.CreateTable(secretCols)
for secret := range secrets {
secretTable.Append([]string{secret, secrets[secret]})
}
}
if NewAppServer == "default" {
NewAppServer = "local"
}
tableCol := []string{"server", "recipe", "domain"}
table := formatter.CreateTable(tableCol)
table.Append([]string{NewAppServer, recipe.Name, Domain})
fmt.Println("")
fmt.Println(fmt.Sprintf("A new %s app has been created! Here is an overview:", recipe.Name))
fmt.Println("")
table.Render()
fmt.Println("")
fmt.Println("You can configure this app by running the following:")
fmt.Println(fmt.Sprintf("\n abra app config %s", Domain))
fmt.Println("")
fmt.Println("You can deploy this app by running the following:")
fmt.Println(fmt.Sprintf("\n abra app deploy %s", Domain))
fmt.Println("")
if len(secrets) > 0 {
fmt.Println("Here are your generated secrets:")
fmt.Println("")
secretTable.Render()
fmt.Println("")
logrus.Warn("generated secrets are not shown again, please take note of them *now*")
}
return nil
}
+110
View File
@@ -0,0 +1,110 @@
package internal
import (
"fmt"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
)
// PromptBumpType prompts for version bump type
func PromptBumpType(tagString, latestRelease string) error {
if (!Major && !Minor && !Patch) && tagString == "" {
fmt.Printf(`
You need to make a decision about what kind of an update this new recipe
version is. If someone else performs this upgrade, do they have to do some
migration work or take care of some breaking changes? This can be signaled in
the version you specify on the recipe deploy label and is called a semantic
version.
The latest published version is %s.
Here is a semver cheat sheet (more on https://semver.org):
major: new features/bug fixes, backwards incompatible (e.g 1.0.0 -> 2.0.0).
the upgrade won't work without some preparation work and others need
to take care when performing it. "it could go wrong".
minor: new features/bug fixes, backwards compatible (e.g. 0.1.0 -> 0.2.0).
the upgrade should Just Work and there are no breaking changes in
the app and the recipe config. "it should go fine".
patch: bug fixes, backwards compatible (e.g. 0.0.1 -> 0.0.2). this upgrade
should also Just Work and is mostly to do with minor bug fixes
and/or security patches. "nothing to worry about".
`, latestRelease)
var chosenBumpType string
prompt := &survey.Select{
Message: fmt.Sprintf("select recipe version increment type"),
Options: []string{"major", "minor", "patch"},
}
if err := survey.AskOne(prompt, &chosenBumpType); err != nil {
return err
}
SetBumpType(chosenBumpType)
}
return nil
}
// GetBumpType figures out which bump type is specified
func GetBumpType() string {
var bumpType string
if Major {
bumpType = "major"
} else if Minor {
bumpType = "minor"
} else if Patch {
bumpType = "patch"
} else {
logrus.Fatal("no version bump type specififed?")
}
return bumpType
}
// SetBumpType figures out which bump type is specified
func SetBumpType(bumpType string) {
if bumpType == "major" {
Major = true
} else if bumpType == "minor" {
Minor = true
} else if bumpType == "patch" {
Patch = true
} else {
logrus.Fatal("no version bump type specififed?")
}
}
// GetMainAppImage retrieves the main 'app' image name
func GetMainAppImage(recipe recipe.Recipe) (string, error) {
var path string
for _, service := range recipe.Config.Services {
if service.Name == "app" {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return "", err
}
path = reference.Path(img)
path = formatter.StripTagMeta(path)
return path, nil
}
}
if path == "" {
return path, fmt.Errorf("%s has no main 'app' service?", recipe.Name)
}
return path, nil
}
+518
View File
@@ -0,0 +1,518 @@
package internal
import (
"errors"
"fmt"
"os"
"strings"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/ssh"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// AppName is used for configuring app name programmatically
var AppName string
// ValidateRecipe ensures the recipe arg is valid.
func ValidateRecipe(c *cli.Context, ensureLatest bool) recipe.Recipe {
recipeName := c.Args().First()
if recipeName == "" {
ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
}
chosenRecipe, err := recipe.Get(recipeName)
if err != nil {
if c.Command.Name == "generate" {
if strings.Contains(err.Error(), "missing a compose") {
logrus.Fatal(err)
}
logrus.Warn(err)
} else {
logrus.Fatal(err)
}
}
if ensureLatest {
if err := recipe.EnsureLatest(recipeName); err != nil {
logrus.Fatal(err)
}
}
logrus.Debugf("validated %s as recipe argument", recipeName)
return chosenRecipe
}
// ValidateRecipeWithPrompt ensures a recipe argument is present before
// validating, asking for input if required.
func ValidateRecipeWithPrompt(c *cli.Context, ensureLatest bool) recipe.Recipe {
recipeName := c.Args().First()
if recipeName == "" && !NoInput {
var recipes []string
catl, err := recipe.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
knownRecipes := make(map[string]bool)
for name := range catl {
knownRecipes[name] = true
}
localRecipes, err := recipe.GetRecipesLocal()
if err != nil {
logrus.Fatal(err)
}
for _, recipeLocal := range localRecipes {
if _, ok := knownRecipes[recipeLocal]; !ok {
knownRecipes[recipeLocal] = true
}
}
for recipeName := range knownRecipes {
recipes = append(recipes, recipeName)
}
prompt := &survey.Select{
Message: "Select recipe",
Options: recipes,
}
if err := survey.AskOne(prompt, &recipeName); err != nil {
logrus.Fatal(err)
}
}
if RecipeName != "" {
recipeName = RecipeName
logrus.Debugf("programmatically setting recipe name to %s", recipeName)
}
if recipeName == "" {
ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
}
chosenRecipe, err := recipe.Get(recipeName)
if err != nil {
logrus.Fatal(err)
}
if ensureLatest {
if err := recipe.EnsureLatest(recipeName); err != nil {
logrus.Fatal(err)
}
}
logrus.Debugf("validated %s as recipe argument", recipeName)
return chosenRecipe
}
// ValidateApp ensures the app name arg is valid.
func ValidateApp(c *cli.Context) config.App {
appName := c.Args().First()
if AppName != "" {
appName = AppName
logrus.Debugf("programmatically setting app name to %s", appName)
}
if appName == "" {
ShowSubcommandHelpAndError(c, errors.New("no app provided"))
}
app, err := app.Get(appName)
if err != nil {
logrus.Fatal(err)
}
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if err := ssh.EnsureHostKey(app.Server); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("validated %s as app argument", appName)
return app
}
// ValidateDomain ensures the domain name arg is valid.
func ValidateDomain(c *cli.Context) string {
domainName := c.Args().First()
if domainName == "" && !NoInput {
prompt := &survey.Input{
Message: "Specify a domain name",
Default: "example.com",
}
if err := survey.AskOne(prompt, &domainName); err != nil {
logrus.Fatal(err)
}
}
if domainName == "" {
ShowSubcommandHelpAndError(c, errors.New("no domain provided"))
}
logrus.Debugf("validated %s as domain argument", domainName)
return domainName
}
// ValidateSubCmdFlags ensures flag order conforms to correct order
func ValidateSubCmdFlags(c *cli.Context) bool {
for argIdx, arg := range c.Args() {
if !strings.HasPrefix(arg, "--") {
for _, flag := range c.Args()[argIdx:] {
if strings.HasPrefix(flag, "--") {
return false
}
}
}
}
return true
}
// ValidateServer ensures the server name arg is valid.
func ValidateServer(c *cli.Context) string {
serverName := c.Args().First()
serverNames, err := config.ReadServerNames()
if err != nil {
logrus.Fatal(err)
}
if serverName == "" && !NoInput {
prompt := &survey.Select{
Message: "Specify a server name",
Options: serverNames,
}
if err := survey.AskOne(prompt, &serverName); err != nil {
logrus.Fatal(err)
}
}
matched := false
for _, name := range serverNames {
if name == serverName {
matched = true
}
}
if !matched {
ShowSubcommandHelpAndError(c, errors.New("server doesn't exist?"))
}
if serverName == "" {
ShowSubcommandHelpAndError(c, errors.New("no server provided"))
}
logrus.Debugf("validated %s as server argument", serverName)
return serverName
}
// EnsureDNSProvider ensures a DNS provider is chosen.
func EnsureDNSProvider() error {
if DNSProvider == "" && !NoInput {
prompt := &survey.Select{
Message: "Select DNS provider",
Options: []string{"gandi"},
}
if err := survey.AskOne(prompt, &DNSProvider); err != nil {
return err
}
}
if DNSProvider == "" {
return fmt.Errorf("missing DNS provider?")
}
return nil
}
// EnsureDNSTypeFlag ensures a DNS type flag is present.
func EnsureDNSTypeFlag(c *cli.Context) error {
if DNSType == "" && !NoInput {
prompt := &survey.Input{
Message: "Specify DNS record type",
Default: "A",
}
if err := survey.AskOne(prompt, &DNSType); err != nil {
return err
}
}
if DNSType == "" {
ShowSubcommandHelpAndError(c, errors.New("no record type provided"))
}
return nil
}
// EnsureDNSNameFlag ensures a DNS name flag is present.
func EnsureDNSNameFlag(c *cli.Context) error {
if DNSName == "" && !NoInput {
prompt := &survey.Input{
Message: "Specify DNS record name",
Default: "mysubdomain",
}
if err := survey.AskOne(prompt, &DNSName); err != nil {
return err
}
}
if DNSName == "" {
ShowSubcommandHelpAndError(c, errors.New("no record name provided"))
}
return nil
}
// EnsureDNSValueFlag ensures a DNS value flag is present.
func EnsureDNSValueFlag(c *cli.Context) error {
if DNSValue == "" && !NoInput {
prompt := &survey.Input{
Message: "Specify DNS record value",
Default: "192.168.1.2",
}
if err := survey.AskOne(prompt, &DNSValue); err != nil {
return err
}
}
if DNSValue == "" {
ShowSubcommandHelpAndError(c, errors.New("no record value provided"))
}
return nil
}
// EnsureZoneArgument ensures a zone argument is present.
func EnsureZoneArgument(c *cli.Context) (string, error) {
zone := c.Args().First()
if zone == "" && !NoInput {
prompt := &survey.Input{
Message: "Specify a domain name zone",
Default: "example.com",
}
if err := survey.AskOne(prompt, &zone); err != nil {
return zone, err
}
}
if zone == "" {
ShowSubcommandHelpAndError(c, errors.New("no zone value provided"))
}
return zone, nil
}
// EnsureServerProvider ensures a 3rd party server provider is chosen.
func EnsureServerProvider() error {
if ServerProvider == "" && !NoInput {
prompt := &survey.Select{
Message: "Select server provider",
Options: []string{"capsul", "hetzner-cloud"},
}
if err := survey.AskOne(prompt, &ServerProvider); err != nil {
return err
}
}
if ServerProvider == "" {
return fmt.Errorf("missing server provider?")
}
return nil
}
// EnsureNewCapsulVPSFlags ensure all flags are present.
func EnsureNewCapsulVPSFlags(c *cli.Context) error {
if CapsulName == "" && !NoInput {
prompt := &survey.Input{
Message: "specify capsul name",
}
if err := survey.AskOne(prompt, &CapsulName); err != nil {
return err
}
}
if !NoInput {
prompt := &survey.Input{
Message: "specify capsul instance URL",
Default: CapsulInstanceURL,
}
if err := survey.AskOne(prompt, &CapsulInstanceURL); err != nil {
return err
}
}
if !NoInput {
prompt := &survey.Input{
Message: "specify capsul type",
Default: CapsulType,
}
if err := survey.AskOne(prompt, &CapsulType); err != nil {
return err
}
}
if !NoInput {
prompt := &survey.Input{
Message: "specify capsul image",
Default: CapsulImage,
}
if err := survey.AskOne(prompt, &CapsulImage); err != nil {
return err
}
}
if len(CapsulSSHKeys.Value()) == 0 && !NoInput {
var sshKeys string
prompt := &survey.Input{
Message: "specify capsul SSH keys (e.g. me@foo.com)",
Default: "",
}
if err := survey.AskOne(prompt, &CapsulSSHKeys); err != nil {
return err
}
CapsulSSHKeys = cli.StringSlice(strings.Split(sshKeys, ","))
}
if CapsulAPIToken == "" && !NoInput {
token, ok := os.LookupEnv("CAPSUL_TOKEN")
if !ok {
prompt := &survey.Input{
Message: "specify capsul API token",
}
if err := survey.AskOne(prompt, &CapsulAPIToken); err != nil {
return err
}
} else {
CapsulAPIToken = token
}
}
if CapsulName == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul name?"))
}
if CapsulInstanceURL == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul instance url?"))
}
if CapsulType == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul type?"))
}
if CapsulImage == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul image?"))
}
if len(CapsulSSHKeys.Value()) == 0 {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul ssh keys?"))
}
if CapsulAPIToken == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul API token?"))
}
return nil
}
// EnsureNewHetznerCloudVPSFlags ensure all flags are present.
func EnsureNewHetznerCloudVPSFlags(c *cli.Context) error {
if HetznerCloudName == "" && !NoInput {
prompt := &survey.Input{
Message: "specify hetzner cloud VPS name",
}
if err := survey.AskOne(prompt, &HetznerCloudName); err != nil {
return err
}
}
if !NoInput {
prompt := &survey.Input{
Message: "specify hetzner cloud VPS type",
Default: HetznerCloudType,
}
if err := survey.AskOne(prompt, &HetznerCloudType); err != nil {
return err
}
}
if !NoInput {
prompt := &survey.Input{
Message: "specify hetzner cloud VPS image",
Default: HetznerCloudImage,
}
if err := survey.AskOne(prompt, &HetznerCloudImage); err != nil {
return err
}
}
if len(HetznerCloudSSHKeys.Value()) == 0 && !NoInput {
var sshKeys string
prompt := &survey.Input{
Message: "specify hetzner cloud SSH keys (e.g. me@foo.com)",
Default: "",
}
if err := survey.AskOne(prompt, &sshKeys); err != nil {
return err
}
HetznerCloudSSHKeys = cli.StringSlice(strings.Split(sshKeys, ","))
}
if !NoInput {
prompt := &survey.Input{
Message: "specify hetzner cloud VPS location",
Default: HetznerCloudLocation,
}
if err := survey.AskOne(prompt, &HetznerCloudLocation); err != nil {
return err
}
}
if HetznerCloudAPIToken == "" && !NoInput {
token, ok := os.LookupEnv("HCLOUD_TOKEN")
if !ok {
prompt := &survey.Input{
Message: "specify hetzner cloud API token",
}
if err := survey.AskOne(prompt, &HetznerCloudAPIToken); err != nil {
return err
}
} else {
HetznerCloudAPIToken = token
}
}
if HetznerCloudName == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud VPS name?"))
}
if HetznerCloudType == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud VPS type?"))
}
if HetznerCloudImage == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud image?"))
}
if HetznerCloudLocation == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud VPS location?"))
}
if HetznerCloudAPIToken == "" {
ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud API token?"))
}
return nil
}
+79
View File
@@ -0,0 +1,79 @@
package recipe
import (
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var recipeLintCommand = cli.Command{
Name: "lint",
Usage: "Lint a recipe",
Aliases: []string{"l"},
ArgsUsage: "<recipe>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.OnlyErrorFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c, true)
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err)
}
tableCol := []string{"ref", "rule", "satisfied", "severity", "resolve"}
table := formatter.CreateTable(tableCol)
hasError := false
bar := formatter.CreateProgressbar(-1, "running recipe lint rules...")
for level := range lint.LintRules {
for _, rule := range lint.LintRules[level] {
ok, err := rule.Function(recipe)
if err != nil {
logrus.Warn(err)
}
if !ok && rule.Level == "error" {
hasError = true
}
var result string
if ok {
result = "yes"
} else {
result = "NO"
}
if internal.OnlyErrors {
if !ok && rule.Level == "error" {
table.Append([]string{rule.Ref, rule.Description, result, rule.Level, rule.HowToResolve})
bar.Add(1)
}
} else {
table.Append([]string{rule.Ref, rule.Description, result, rule.Level, rule.HowToResolve})
bar.Add(1)
}
}
}
if table.NumLines() > 0 {
fmt.Println()
table.Render()
}
if hasError {
logrus.Warn("watch out, some critical errors are present in your recipe config")
}
return nil
},
}
+77
View File
@@ -0,0 +1,77 @@
package recipe
import (
"fmt"
"sort"
"strconv"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var pattern string
var patternFlag = &cli.StringFlag{
Name: "pattern, p",
Value: "",
Usage: "Simple string to filter recipes",
Destination: &pattern,
}
var recipeListCommand = cli.Command{
Name: "list",
Usage: "List available recipes",
Aliases: []string{"ls"},
Flags: []cli.Flag{
internal.DebugFlag,
patternFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
catl, err := recipe.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err.Error())
}
recipes := catl.Flatten()
sort.Sort(recipe.ByRecipeName(recipes))
tableCol := []string{"name", "category", "status", "healthcheck", "backups", "email", "tests", "SSO"}
table := formatter.CreateTable(tableCol)
len := 0
for _, recipe := range recipes {
tableRow := []string{
recipe.Name,
recipe.Category,
strconv.Itoa(recipe.Features.Status),
recipe.Features.Healthcheck,
recipe.Features.Backups,
recipe.Features.Email,
recipe.Features.Tests,
recipe.Features.SSO,
}
if pattern != "" {
if strings.Contains(recipe.Name, pattern) {
table.Append(tableRow)
len++
}
} else {
table.Append(tableRow)
len++
}
}
table.SetCaption(true, fmt.Sprintf("total recipes: %v", len))
if table.NumLines() > 0 {
table.Render()
}
return nil
},
}
+140
View File
@@ -0,0 +1,140 @@
package recipe
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"text/template"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/git"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// 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
}
var recipeNewCommand = cli.Command{
Name: "new",
Aliases: []string{"n"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
Usage: "Create a new recipe",
ArgsUsage: "<recipe>",
Description: `
This command creates a new recipe.
Abra uses our built-in example repository which is available here:
https://git.coopcloud.tech/coop-cloud/example
Files within the example repository make use of the Golang templating system
which Abra uses to inject values into the generated recipe folder (e.g. name of
recipe and domain in the sample environment config).
`,
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
if recipeName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
}
directory := path.Join(config.RECIPES_DIR, recipeName)
if _, err := os.Stat(directory); !os.IsNotExist(err) {
logrus.Fatalf("%s recipe directory already exists?", directory)
}
url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL)
if err := git.Clone(directory, url); err != nil {
logrus.Fatal(err)
}
gitRepo := path.Join(config.RECIPES_DIR, recipeName, ".git")
if err := os.RemoveAll(gitRepo); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("removed example git repo in %s", gitRepo)
meta := newRecipeMeta(recipeName)
toParse := []string{
path.Join(config.RECIPES_DIR, recipeName, "README.md"),
path.Join(config.RECIPES_DIR, recipeName, ".env.sample"),
}
for _, path := range toParse {
tpl, err := template.ParseFiles(path)
if err != nil {
logrus.Fatal(err)
}
var templated bytes.Buffer
if err := tpl.Execute(&templated, meta); err != nil {
logrus.Fatal(err)
}
if err := ioutil.WriteFile(path, templated.Bytes(), 0644); err != nil {
logrus.Fatal(err)
}
}
newGitRepo := path.Join(config.RECIPES_DIR, recipeName)
if err := git.Init(newGitRepo, true); err != nil {
logrus.Fatal(err)
}
fmt.Print(fmt.Sprintf(`
Your new %s recipe has been created in %s.
In order to share your recipe, you can upload it the git repository to:
https://git.coopcloud.tech/coop-cloud/%s
If you're not sure how to do that, come chat with us:
https://docs.coopcloud.tech/contact
See "abra recipe -h" for additional recipe maintainer commands.
Happy Hacking!
`, recipeName, path.Join(config.RECIPES_DIR, recipeName), recipeName))
return nil
},
}
// newRecipeMeta creates a new recipeMetadata instance with defaults
func newRecipeMeta(recipeName string) recipeMetadata {
return recipeMetadata{
Name: recipeName,
Description: "> One line description of the recipe",
Category: "Apps",
Status: "0",
Image: fmt.Sprintf("[`%s`](https://hub.docker.com/r/%s), 4, upstream", recipeName, recipeName),
Healthcheck: "No",
Backups: "No",
Email: "No",
Tests: "No",
SSO: "No",
}
}
+32
View File
@@ -0,0 +1,32 @@
package recipe
import (
"github.com/urfave/cli"
)
// RecipeCommand defines all recipe related sub-commands.
var RecipeCommand = cli.Command{
Name: "recipe",
Aliases: []string{"r"},
Usage: "Manage recipes",
ArgsUsage: "<recipe>",
Description: `
A recipe is a blueprint for an app. It is a bunch of config files which
describe how to deploy and maintain an app. Recipes are maintained by the Co-op
Cloud community and you can use Abra to read them and create apps for you.
Anyone who uses a recipe can become a maintainer. Maintainers typically make
sure the recipe is in good working order and the config upgraded in a timely
manner. Abra supports convenient automation for recipe maintainenace, see the
"abra recipe upgrade", "abra recipe sync" and "abra recipe release" commands.
`,
Subcommands: []cli.Command{
recipeListCommand,
recipeVersionCommand,
recipeReleaseCommand,
recipeNewCommand,
recipeUpgradeCommand,
recipeSyncCommand,
recipeLintCommand,
},
}
+454
View File
@@ -0,0 +1,454 @@
package recipe
import (
"fmt"
"path"
"strconv"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var recipeReleaseCommand = cli.Command{
Name: "release",
Aliases: []string{"rl"},
Usage: "Release a new recipe version",
ArgsUsage: "<recipe> [<version>]",
Description: `
This command is used to specify 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. And
the "x.y.z" part is the image tag of the recipe "app" service (the main
container which contains the software to be used).
We maintain a semantic versioning scheme ("a.b.c") alongside the libre app
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 "-p/--publish". This
requires that you have permission to git push to these repositories and have
your SSH keys configured on your account.
`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.DryFlag,
internal.MajorFlag,
internal.MinorFlag,
internal.PatchFlag,
internal.PublishFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeWithPrompt(c, false)
imagesTmp, err := getImageVersions(recipe)
if err != nil {
logrus.Fatal(err)
}
mainApp, err := internal.GetMainAppImage(recipe)
if err != nil {
logrus.Fatal(err)
}
mainAppVersion := imagesTmp[mainApp]
if mainAppVersion == "" {
logrus.Fatalf("main app service version for %s is empty?", recipe.Name)
}
tagString := c.Args().Get(1)
if tagString != "" {
if _, err := tagcmp.Parse(tagString); err != nil {
logrus.Fatalf("cannot parse %s, invalid tag specified?", tagString)
}
}
if (internal.Major || internal.Minor || internal.Patch) && tagString != "" {
logrus.Fatal("cannot specify tag and bump type at the same time")
}
if tagString != "" {
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
logrus.Fatal(err)
}
}
tags, err := recipe.Tags()
if err != nil {
logrus.Fatal(err)
}
if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
var err error
tagString, err = getLabelVersion(recipe, false)
if err != nil {
logrus.Fatal(err)
}
}
if len(tags) > 0 {
logrus.Warnf("previous git tags detected, assuming this is a new semver release")
if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil {
logrus.Fatal(err)
}
} else {
logrus.Warnf("no tag specified and no previous tag available for %s, assuming this is the initial release", recipe.Name)
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
if cleanUpErr := cleanUpTag(tagString, recipe.Name); err != nil {
logrus.Fatal(cleanUpErr)
}
logrus.Fatal(err)
}
}
return nil
},
}
// getImageVersions retrieves image versions for a recipe
func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
var services = make(map[string]string)
missingTag := false
for _, service := range recipe.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, fmt.Errorf("app service is missing image tag?")
}
return services, nil
}
// createReleaseFromTag creates a new release based on a supplied recipe version string
func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error {
var err error
directory := path.Join(config.RECIPES_DIR, recipe.Name)
repo, err := git.PlainOpen(directory)
if err != nil {
return err
}
tag, err := tagcmp.Parse(tagString)
if err != nil {
return err
}
if tag.MissingMinor {
tag.Minor = "0"
tag.MissingMinor = false
}
if tag.MissingPatch {
tag.Patch = "0"
tag.MissingPatch = false
}
if tagString == "" {
tagString = fmt.Sprintf("%s+%s", tag.String(), mainAppVersion)
}
if err := commitRelease(recipe, tagString); err != nil {
logrus.Fatal(err)
}
if err := tagRelease(tagString, repo); err != nil {
logrus.Fatal(err)
}
if err := pushRelease(recipe, tagString); err != nil {
logrus.Fatal(err)
}
return nil
}
// btoi converts a boolean value into an integer
func btoi(b bool) int {
if b {
return 1
}
return 0
}
// getTagCreateOptions constructs git tag create options
func getTagCreateOptions(tag string) (git.CreateTagOptions, error) {
msg := fmt.Sprintf("chore: publish %s release", tag)
return git.CreateTagOptions{Message: msg}, nil
}
func commitRelease(recipe recipe.Recipe, tag string) error {
if internal.Dry {
logrus.Debugf("dry run: no changes committed")
return nil
}
isClean, err := gitPkg.IsClean(recipe.Dir())
if err != nil {
return err
}
if isClean {
if !internal.Dry {
return fmt.Errorf("no changes discovered in %s, nothing to publish?", recipe.Dir())
}
}
msg := fmt.Sprintf("chore: publish %s release", tag)
repoPath := path.Join(config.RECIPES_DIR, recipe.Name)
if err := gitPkg.Commit(repoPath, ".", msg, internal.Dry); err != nil {
return err
}
return nil
}
func tagRelease(tagString string, repo *git.Repository) error {
if internal.Dry {
logrus.Debugf("dry run: no git tag created (%s)", tagString)
return nil
}
head, err := repo.Head()
if err != nil {
return err
}
createTagOptions, err := getTagCreateOptions(tagString)
if err != nil {
return err
}
_, err = repo.CreateTag(tagString, head.Hash(), &createTagOptions)
if err != nil {
return err
}
hash := formatter.SmallSHA(head.Hash().String())
logrus.Debugf(fmt.Sprintf("created tag %s at %s", tagString, hash))
return nil
}
func pushRelease(recipe recipe.Recipe, tagString string) error {
if internal.Dry {
logrus.Info("dry run: no changes published")
return nil
}
if !internal.Publish && !internal.NoInput {
prompt := &survey.Confirm{
Message: "publish new release?",
}
if err := survey.AskOne(prompt, &internal.Publish); err != nil {
return err
}
}
if internal.Publish {
if err := recipe.Push(internal.Dry); err != nil {
return err
}
url := fmt.Sprintf("%s/%s/src/tag/%s", config.REPOS_BASE_URL, recipe.Name, tagString)
logrus.Infof("new release published: %s", url)
} else {
logrus.Info("no -p/--publish passed, not publishing")
}
return nil
}
func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error {
directory := path.Join(config.RECIPES_DIR, recipe.Name)
repo, err := git.PlainOpen(directory)
if err != nil {
return err
}
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 {
if (bumpType & (bumpType - 1)) != 0 {
return fmt.Errorf("you can only use one of: --major, --minor, --patch")
}
}
var lastGitTag tagcmp.Tag
for _, tag := range tags {
parsed, err := tagcmp.Parse(tag)
if err != nil {
return err
}
if (lastGitTag == tagcmp.Tag{}) {
lastGitTag = parsed
} else if parsed.IsGreaterThan(lastGitTag) {
lastGitTag = parsed
}
}
newTag := lastGitTag
if internal.Patch {
now, err := strconv.Atoi(newTag.Patch)
if err != nil {
return err
}
newTag.Patch = strconv.Itoa(now + 1)
} else if internal.Minor {
now, err := strconv.Atoi(newTag.Minor)
if err != nil {
return err
}
newTag.Patch = "0"
newTag.Minor = strconv.Itoa(now + 1)
} else if internal.Major {
now, err := strconv.Atoi(newTag.Major)
if err != nil {
return err
}
newTag.Patch = "0"
newTag.Minor = "0"
newTag.Major = strconv.Itoa(now + 1)
}
if tagString == "" {
if err := internal.PromptBumpType(tagString, lastGitTag.String()); err != nil {
return err
}
}
if internal.Major || internal.Minor || internal.Patch {
newTag.Metadata = mainAppVersion
tagString = newTag.String()
}
if lastGitTag.String() == tagString {
logrus.Fatalf("latest git tag (%s) and synced lable (%s) are the same?", lastGitTag, tagString)
}
if !internal.NoInput {
prompt := &survey.Confirm{
Message: fmt.Sprintf("current: %s, new: %s, correct?", lastGitTag, tagString),
}
var ok bool
if err := survey.AskOne(prompt, &ok); err != nil {
logrus.Fatal(err)
}
if !ok {
logrus.Fatal("exiting as requested")
}
}
if err := commitRelease(recipe, tagString); err != nil {
logrus.Fatalf("failed to commit changes: %s", err.Error())
}
if err := tagRelease(tagString, repo); err != nil {
logrus.Fatalf("failed to tag release: %s", err.Error())
}
if err := pushRelease(recipe, tagString); err != nil {
logrus.Fatalf("failed to publish new release: %s", err.Error())
}
return nil
}
// cleanUpTag removes a freshly created tag
func cleanUpTag(tag, recipeName string) error {
directory := path.Join(config.RECIPES_DIR, recipeName)
repo, err := git.PlainOpen(directory)
if err != nil {
return err
}
if err := repo.DeleteTag(tag); err != nil {
if !strings.Contains(err.Error(), "not found") {
return err
}
}
logrus.Debugf("removed freshly created tag %s", tag)
return nil
}
func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
initTag, err := recipePkg.GetVersionLabelLocal(recipe)
if err != nil {
return "", err
}
if initTag == "" {
logrus.Fatalf("unable to read version for %s from synced label. Did you try running \"abra recipe sync %s\" already?", recipe.Name, recipe.Name)
}
logrus.Warnf("discovered %s as currently synced recipe label", initTag)
if prompt && !internal.NoInput {
var response bool
prompt := &survey.Confirm{Message: fmt.Sprintf("use %s as the new version?", initTag)}
if err := survey.AskOne(prompt, &response); err != nil {
return "", err
}
if !response {
return "", fmt.Errorf("please fix your synced label for %s and re-run this command", recipe.Name)
}
}
return initTag, nil
}
+204
View File
@@ -0,0 +1,204 @@
package recipe
import (
"fmt"
"path"
"strconv"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
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/sirupsen/logrus"
"github.com/urfave/cli"
)
var recipeSyncCommand = cli.Command{
Name: "sync",
Aliases: []string{"s"},
Usage: "Sync recipe version label",
ArgsUsage: "<recipe> [<version>]",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.DryFlag,
internal.MajorFlag,
internal.MinorFlag,
internal.PatchFlag,
},
Before: internal.SubCommandBefore,
Description: `
This command will generate labels for the main recipe service (i.e. by
convention, the service named 'app') which corresponds to 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.
`,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeWithPrompt(c, false)
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err)
}
mainApp, err := internal.GetMainAppImage(recipe)
if err != nil {
logrus.Fatal(err)
}
imagesTmp, err := getImageVersions(recipe)
if err != nil {
logrus.Fatal(err)
}
mainAppVersion := imagesTmp[mainApp]
tags, err := recipe.Tags()
if err != nil {
logrus.Fatal(err)
}
nextTag := c.Args().Get(1)
if len(tags) == 0 && nextTag == "" {
logrus.Warnf("no git tags found for %s", recipe.Name)
fmt.Println(fmt.Sprintf(`
The following options are two types of initial semantic version that you can
pick for %s that will be published in the recipe catalogue. This follows the
semver convention (more on https://semver.org), here is a short cheatsheet
0.1.0: development release, still hacking. when you make a major upgrade
you increment the "y" part (i.e. 0.1.0 -> 0.2.0) and only move to
using the "x" part when things are stable.
1.0.0: public release, assumed to be working. you already have a stable
and reliable deployment of this app and feel relatively confident
about it.
If you want people to be able alpha test your current config for %s but don't
think it is quite reliable, go with 0.1.0 and people will know that things are
likely to change.
`, recipe.Name, recipe.Name))
var chosenVersion string
edPrompt := &survey.Select{
Message: "which version do you want to begin with?",
Options: []string{"0.1.0", "1.0.0"},
}
if err := survey.AskOne(edPrompt, &chosenVersion); err != nil {
logrus.Fatal(err)
}
nextTag = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion)
}
if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
latestRelease := tags[len(tags)-1]
if err := internal.PromptBumpType("", latestRelease); err != nil {
logrus.Fatal(err)
}
}
if nextTag == "" {
recipeDir := path.Join(config.RECIPES_DIR, recipe.Name)
repo, err := git.PlainOpen(recipeDir)
if err != nil {
logrus.Fatal(err)
}
var lastGitTag tagcmp.Tag
iter, err := repo.Tags()
if err != nil {
logrus.Fatal(err)
}
if err := iter.ForEach(func(ref *plumbing.Reference) error {
obj, err := repo.TagObject(ref.Hash())
if err != nil {
return err
}
tagcmpTag, err := tagcmp.Parse(obj.Name)
if err != nil {
return err
}
if (lastGitTag == tagcmp.Tag{}) {
lastGitTag = tagcmpTag
} else if tagcmpTag.IsGreaterThan(lastGitTag) {
lastGitTag = tagcmpTag
}
return nil
}); err != nil {
logrus.Fatal(err)
}
// bumpType is used to decide what part of the tag should be incremented
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 {
// a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 {
logrus.Fatal("you can only use one version flag: --major, --minor or --patch")
}
}
newTag := lastGitTag
if bumpType > 0 {
if internal.Patch {
now, err := strconv.Atoi(newTag.Patch)
if err != nil {
logrus.Fatal(err)
}
newTag.Patch = strconv.Itoa(now + 1)
} else if internal.Minor {
now, err := strconv.Atoi(newTag.Minor)
if err != nil {
logrus.Fatal(err)
}
newTag.Patch = "0"
newTag.Minor = strconv.Itoa(now + 1)
} else if internal.Major {
now, err := strconv.Atoi(newTag.Major)
if err != nil {
logrus.Fatal(err)
}
newTag.Patch = "0"
newTag.Minor = "0"
newTag.Major = strconv.Itoa(now + 1)
}
}
newTag.Metadata = mainAppVersion
logrus.Debugf("choosing %s as new version for %s", newTag.String(), recipe.Name)
nextTag = newTag.String()
}
if _, err := tagcmp.Parse(nextTag); err != nil {
logrus.Fatalf("invalid version %s specified", nextTag)
}
mainService := "app"
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag)
if !internal.Dry {
if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil {
logrus.Fatal(err)
}
} else {
logrus.Infof("dry run: not syncing label %s for recipe %s", nextTag, recipe.Name)
}
return nil
},
BashComplete: autocomplete.RecipeNameComplete,
}
+264
View File
@@ -0,0 +1,264 @@
package recipe
import (
"bufio"
"fmt"
"os"
"path"
"sort"
"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/formatter"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
type imgPin struct {
image string
version tagcmp.Tag
}
var recipeUpgradeCommand = cli.Command{
Name: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade recipe image tags",
Description: `
This command reads and attempts to parse all image tags within the given
<recipe> configuration and prompt with more recent tags to upgrade to. It will
update the relevant compose file tags on the local file system.
Some image tags cannot be parsed because they do not follow some sort of
semver-like convention. In this case, all possible tags will be listed and it
is up to the end-user to decide.
The command is interactive and will show a select input which allows you to
make a seclection. Use the "?" key to see more help on navigating this
interface.
You may invoke this command in "wizard" mode and be prompted for input:
abra recipe upgrade
`,
BashComplete: autocomplete.RecipeNameComplete,
ArgsUsage: "<recipe>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.PatchFlag,
internal.MinorFlag,
internal.MajorFlag,
internal.AllTagsFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeWithPrompt(c, true)
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 {
// a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 {
logrus.Fatal("you can only use one of: --major, --minor, --patch.")
}
}
// check for versions file and load pinned versions
versionsPresent := false
recipeDir := path.Join(config.RECIPES_DIR, recipe.Name)
versionsPath := path.Join(recipeDir, "versions")
var servicePins = make(map[string]imgPin)
if _, err := os.Stat(versionsPath); err == nil {
logrus.Debugf("found versions file for %s", recipe.Name)
file, err := os.Open(versionsPath)
if err != nil {
logrus.Fatal(err)
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
splitLine := strings.Split(line, " ")
if splitLine[0] != "pin" || len(splitLine) != 3 {
logrus.Fatalf("malformed version pin specification: %s", line)
}
pinSlice := strings.Split(splitLine[2], ":")
pinTag, err := tagcmp.Parse(pinSlice[1])
if err != nil {
logrus.Fatal(err)
}
pin := imgPin{
image: pinSlice[0],
version: pinTag,
}
servicePins[splitLine[1]] = pin
}
if err := scanner.Err(); err != nil {
logrus.Error(err)
}
versionsPresent = true
} else {
logrus.Debugf("did not find versions file for %s", recipe.Name)
}
for _, service := range recipe.Config.Services {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
logrus.Fatal(err)
}
regVersions, err := client.GetRegistryTags(img)
if err != nil {
logrus.Fatal(err)
}
image := reference.Path(img)
logrus.Debugf("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()) {
logrus.Debugf("%s not considered semver-like", img.(reference.NamedTagged).Tag())
}
default:
logrus.Warnf("unable to read tag for image %s, is it missing? skipping upgrade for %s", image, service.Name)
continue
}
tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag())
if err != nil {
logrus.Warnf("unable to parse %s, error was: %s, skipping upgrade for %s", image, err.Error(), service.Name)
continue
}
logrus.Debugf("parsed %s for %s", tag, service.Name)
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)
}
}
logrus.Debugf("detected potential upgradable tags %s for %s", compatible, service.Name)
sort.Sort(tagcmp.ByTagDesc(compatible))
if len(compatible) == 0 && !internal.AllTags {
logrus.Info(fmt.Sprintf("no new versions available for %s, assuming %s is the latest (use -a/--all-tags to see all anyway)", image, tag))
continue // skip on to the next tag and don't update any compose files
}
catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name)
if err != nil {
logrus.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())
}
}
logrus.Debugf("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 {
logrus.Infof("upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString)
} else {
logrus.Infof("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString)
continue
}
} else {
logrus.Fatalf("service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String())
continue
}
} else {
if bumpType != 0 {
for _, upTag := range compatible {
upElement, err := tag.UpgradeDelta(upTag)
if err != nil {
return err
}
delta := upElement.UpgradeType()
if delta <= bumpType {
upgradeTag = upTag.String()
break
}
}
if upgradeTag == "" {
logrus.Warnf("not upgrading from %s to %s for %s, because the upgrade type is more serious than what user wants", tag.String(), compatible[0].String(), image)
continue
}
} else {
msg := fmt.Sprintf("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, tag)
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || internal.AllTags {
tag := img.(reference.NamedTagged).Tag()
if !internal.AllTags {
logrus.Warning(fmt.Sprintf("unable to determine versioning semantics of %s, listing all tags", tag))
}
msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
compatibleStrings = []string{"skip"}
for _, regVersion := range regVersions {
compatibleStrings = append(compatibleStrings, regVersion)
}
}
prompt := &survey.Select{
Message: msg,
Help: "enter / return to confirm, choose 'skip' to not upgrade this tag, vim mode is enabled",
VimMode: true,
Options: compatibleStrings,
}
if err := survey.AskOne(prompt, &upgradeTag); err != nil {
logrus.Fatal(err)
}
}
}
if upgradeTag != "skip" {
ok, err := recipe.UpdateTag(image, upgradeTag)
if err != nil {
logrus.Fatal(err)
}
if ok {
logrus.Infof("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image)
}
} else {
logrus.Warnf("not upgrading %s, skipping as requested", image)
}
}
return nil
},
}
+56
View File
@@ -0,0 +1,56 @@
package recipe
import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var recipeVersionCommand = cli.Command{
Name: "versions",
Aliases: []string{"v"},
Usage: "List recipe versions",
ArgsUsage: "<recipe>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c, false)
catalogue, err := recipePkg.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
recipeMeta, ok := catalogue[recipe.Name]
if !ok {
logrus.Fatalf("%s recipe doesn't exist?", recipe.Name)
}
tableCol := []string{"Version", "Service", "Image", "Tag", "Digest"}
table := formatter.CreateTable(tableCol)
for i := len(recipeMeta.Versions) - 1; i >= 0; i-- {
for tag, meta := range recipeMeta.Versions[i] {
for service, serviceMeta := range meta {
table.Append([]string{tag, service, serviceMeta.Image, serviceMeta.Tag, serviceMeta.Digest})
}
}
}
table.SetAutoMergeCells(true)
if table.NumLines() > 0 {
table.Render()
} else {
logrus.Fatalf("%s has no published versions?", recipe.Name)
}
return nil
},
}
+82
View File
@@ -0,0 +1,82 @@
package record
import (
"context"
"fmt"
"strconv"
"coopcloud.tech/abra/cli/internal"
gandiPkg "coopcloud.tech/abra/pkg/dns/gandi"
"coopcloud.tech/abra/pkg/formatter"
"github.com/libdns/gandi"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// RecordListCommand lists domains.
var RecordListCommand = cli.Command{
Name: "list",
Usage: "List domain name records",
Aliases: []string{"ls"},
ArgsUsage: "<zone>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.DNSProviderFlag,
},
Before: internal.SubCommandBefore,
Description: `
This command lists all domain name records managed by a 3rd party provider for
a specific zone.
You must specify a zone (e.g. example.com) under which your domain name records
are listed. This zone must already be created on your provider account.
`,
Action: func(c *cli.Context) error {
if err := internal.EnsureDNSProvider(); err != nil {
logrus.Fatal(err)
}
zone, err := internal.EnsureZoneArgument(c)
if err != nil {
logrus.Fatal(err)
}
var provider gandi.Provider
switch internal.DNSProvider {
case "gandi":
provider, err = gandiPkg.New()
if err != nil {
logrus.Fatal(err)
}
default:
logrus.Fatalf("%s is not a supported DNS provider", internal.DNSProvider)
}
records, err := provider.GetRecords(context.Background(), zone)
if err != nil {
logrus.Fatal(err)
}
tableCol := []string{"type", "name", "value", "TTL", "priority"}
table := formatter.CreateTable(tableCol)
for _, record := range records {
value := record.Value
if len(record.Value) > 30 {
value = fmt.Sprintf("%s...", record.Value[:30])
}
table.Append([]string{
record.Type,
record.Name,
value,
record.TTL.String(),
strconv.Itoa(record.Priority),
})
}
table.Render()
return nil
},
}
+148
View File
@@ -0,0 +1,148 @@
package record
import (
"context"
"fmt"
"strconv"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/dns"
gandiPkg "coopcloud.tech/abra/pkg/dns/gandi"
"coopcloud.tech/abra/pkg/formatter"
"github.com/libdns/gandi"
"github.com/libdns/libdns"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// RecordNewCommand creates a new domain name record.
var RecordNewCommand = cli.Command{
Name: "new",
Usage: "Create a new domain record",
Aliases: []string{"n"},
ArgsUsage: "<zone>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.DNSProviderFlag,
internal.DNSTypeFlag,
internal.DNSNameFlag,
internal.DNSValueFlag,
internal.DNSTTLFlag,
internal.DNSPriorityFlag,
},
Before: internal.SubCommandBefore,
Description: `
This command creates a new domain name record for a specific zone.
You must specify a zone (e.g. example.com) under which your domain name records
are listed. This zone must already be created on your provider account.
Example:
abra record new foo.com -p gandi -t A -n myapp -v 192.168.178.44
You may also invoke this command in "wizard" mode and be prompted for input:
abra record new
`,
Action: func(c *cli.Context) error {
zone, err := internal.EnsureZoneArgument(c)
if err != nil {
logrus.Fatal(err)
}
if err := internal.EnsureDNSProvider(); err != nil {
logrus.Fatal(err)
}
var provider gandi.Provider
switch internal.DNSProvider {
case "gandi":
provider, err = gandiPkg.New()
if err != nil {
logrus.Fatal(err)
}
default:
logrus.Fatalf("%s is not a supported DNS provider", internal.DNSProvider)
}
if err := internal.EnsureDNSTypeFlag(c); err != nil {
logrus.Fatal(err)
}
if err := internal.EnsureDNSNameFlag(c); err != nil {
logrus.Fatal(err)
}
if err := internal.EnsureDNSValueFlag(c); err != nil {
logrus.Fatal(err)
}
ttl, err := dns.GetTTL(internal.DNSTTL)
if err != nil {
return err
}
record := libdns.Record{
Type: internal.DNSType,
Name: internal.DNSName,
Value: internal.DNSValue,
TTL: ttl,
}
if internal.DNSType == "MX" || internal.DNSType == "SRV" || internal.DNSType == "URI" {
record.Priority = internal.DNSPriority
}
records, err := provider.GetRecords(context.Background(), zone)
if err != nil {
logrus.Fatal(err)
}
for _, existingRecord := range records {
if existingRecord.Type == record.Type &&
existingRecord.Name == record.Name &&
existingRecord.Value == record.Value {
logrus.Fatalf("%s record for %s already exists?", record.Type, zone)
}
}
createdRecords, err := provider.SetRecords(
context.Background(),
zone,
[]libdns.Record{record},
)
if err != nil {
logrus.Fatal(err)
}
if len(createdRecords) == 0 {
logrus.Fatal("provider library reports that no record was created?")
}
createdRecord := createdRecords[0]
tableCol := []string{"type", "name", "value", "TTL", "priority"}
table := formatter.CreateTable(tableCol)
value := createdRecord.Value
if len(createdRecord.Value) > 30 {
value = fmt.Sprintf("%s...", createdRecord.Value[:30])
}
table.Append([]string{
createdRecord.Type,
createdRecord.Name,
value,
createdRecord.TTL.String(),
strconv.Itoa(createdRecord.Priority),
})
table.Render()
logrus.Info("record created")
return nil
},
}
+37
View File
@@ -0,0 +1,37 @@
package record
import (
"github.com/urfave/cli"
)
// RecordCommand supports managing DNS entries.
var RecordCommand = cli.Command{
Name: "record",
Usage: "Manage domain name records",
Aliases: []string{"rc"},
ArgsUsage: "<record>",
Description: `
This command supports managing domain name records via 3rd party providers such
as Gandi DNS. It supports listing, creating and removing all types of records
that you might need for managing Co-op Cloud apps.
The following providers are supported:
Gandi DNS https://www.gandi.net
You need an account with such a provider already. Typically, you need to
provide an API token on the Abra command-line when using these commands so that
you can authenticate with your provider account.
New providers can be integrated, we welcome change sets. See the underlying DNS
library documentation for more. It supports many existing providers and allows
to implement new provider support easily.
https://pkg.go.dev/github.com/libdns/libdns
`,
Subcommands: []cli.Command{
RecordListCommand,
RecordNewCommand,
RecordRemoveCommand,
},
}
+136
View File
@@ -0,0 +1,136 @@
package record
import (
"context"
"fmt"
"strconv"
"coopcloud.tech/abra/cli/internal"
gandiPkg "coopcloud.tech/abra/pkg/dns/gandi"
"coopcloud.tech/abra/pkg/formatter"
"github.com/AlecAivazis/survey/v2"
"github.com/libdns/gandi"
"github.com/libdns/libdns"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// RecordRemoveCommand lists domains.
var RecordRemoveCommand = cli.Command{
Name: "remove",
Usage: "Remove a domain name record",
Aliases: []string{"rm"},
ArgsUsage: "<zone>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.DNSProviderFlag,
internal.DNSTypeFlag,
internal.DNSNameFlag,
},
Before: internal.SubCommandBefore,
Description: `
This command removes a domain name record for a specific zone.
It uses the type of record and name to match existing records and choose one
for deletion. You must specify a zone (e.g. example.com) under which your
domain name records are listed. This zone must already be created on your
provider account.
Example:
abra record remove foo.com -p gandi -t A -n myapp
You may also invoke this command in "wizard" mode and be prompted for input:
abra record rm
`,
Action: func(c *cli.Context) error {
zone, err := internal.EnsureZoneArgument(c)
if err != nil {
logrus.Fatal(err)
}
if err := internal.EnsureDNSProvider(); err != nil {
logrus.Fatal(err)
}
var provider gandi.Provider
switch internal.DNSProvider {
case "gandi":
provider, err = gandiPkg.New()
if err != nil {
logrus.Fatal(err)
}
default:
logrus.Fatalf("%s is not a supported DNS provider", internal.DNSProvider)
}
if err := internal.EnsureDNSTypeFlag(c); err != nil {
logrus.Fatal(err)
}
if err := internal.EnsureDNSNameFlag(c); err != nil {
logrus.Fatal(err)
}
records, err := provider.GetRecords(context.Background(), zone)
if err != nil {
logrus.Fatal(err)
}
var toDelete libdns.Record
for _, record := range records {
if record.Type == internal.DNSType && record.Name == internal.DNSName {
toDelete = record
break
}
}
if (libdns.Record{}) == toDelete {
logrus.Fatal("provider library reports no matching record?")
}
tableCol := []string{"type", "name", "value", "TTL", "priority"}
table := formatter.CreateTable(tableCol)
value := toDelete.Value
if len(toDelete.Value) > 30 {
value = fmt.Sprintf("%s...", toDelete.Value[:30])
}
table.Append([]string{
toDelete.Type,
toDelete.Name,
value,
toDelete.TTL.String(),
strconv.Itoa(toDelete.Priority),
})
table.Render()
if !internal.NoInput {
response := false
prompt := &survey.Confirm{
Message: "continue with record deletion?",
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
logrus.Fatal("exiting as requested")
}
}
_, err = provider.DeleteRecords(context.Background(), zone, []libdns.Record{toDelete})
if err != nil {
logrus.Fatal(err)
}
logrus.Info("record successfully deleted")
return nil
},
}
+511
View File
@@ -0,0 +1,511 @@
package server
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
contextPkg "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/dns"
"coopcloud.tech/abra/pkg/server"
"coopcloud.tech/abra/pkg/ssh"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var (
dockerInstallMsg = `
A docker installation cannot be found on %s. This is a required system
dependency for running Co-op Cloud on your server. If you would like, Abra can
attempt to install Docker for you using the upstream non-interactive
installation script.
See the following documentation for more:
https://docs.docker.com/engine/install/debian/#install-using-the-convenience-script
N.B Docker doesn't recommend it for production environments but many use it for
such purposes. Docker stable is now installed by default by this script. The
source for this script can be seen here:
https://github.com/docker/docker-install
`
)
var local bool
var localFlag = &cli.BoolFlag{
Name: "local, l",
Usage: "Use local server",
Destination: &local,
}
var provision bool
var provisionFlag = &cli.BoolFlag{
Name: "provision, p",
Usage: "Provision server so it can deploy apps",
Destination: &provision,
}
var sshAuth string
var sshAuthFlag = &cli.StringFlag{
Name: "ssh-auth, s",
Value: "identity-file",
Usage: "Select SSH authentication method (identity-file, password)",
Destination: &sshAuth,
}
var askSudoPass bool
var askSudoPassFlag = &cli.BoolFlag{
Name: "ask-sudo-pass, a",
Usage: "Ask for sudo password",
Destination: &askSudoPass,
}
func cleanUp(domainName string) {
logrus.Warnf("cleaning up context for %s", domainName)
if err := client.DeleteContext(domainName); err != nil {
logrus.Fatal(err)
}
logrus.Warnf("cleaning up server directory for %s", domainName)
if err := os.RemoveAll(filepath.Join(config.SERVERS_DIR, domainName)); err != nil {
logrus.Fatal(err)
}
}
func installDockerLocal(c *cli.Context) error {
fmt.Println(fmt.Sprintf(dockerInstallMsg, "this local server"))
response := false
prompt := &survey.Confirm{
Message: fmt.Sprintf("attempt install docker on local server?"),
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
logrus.Fatal("exiting as requested")
}
for _, exe := range []string{"wget", "bash"} {
exists, err := ensureLocalExecutable(exe)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("%s missing, please install it", exe)
}
}
cmd := exec.Command("bash", "-c", "wget -O- https://get.docker.com | bash")
if err := internal.RunCmd(cmd); err != nil {
return err
}
return nil
}
func newLocalServer(c *cli.Context, domainName string) error {
if err := createServerDir(domainName); err != nil {
return err
}
cl, err := newClient(c, domainName)
if err != nil {
return err
}
if provision {
exists, err := ensureLocalExecutable("docker")
if err != nil {
return err
}
if !exists {
if err := installDockerLocal(c); err != nil {
return err
}
}
if err := initSwarmLocal(c, cl, domainName); err != nil {
if !strings.Contains(err.Error(), "proxy already exists") {
logrus.Fatal(err)
}
}
}
logrus.Info("local server has been added")
return nil
}
func newContext(c *cli.Context, domainName, username, port string) error {
store := contextPkg.NewDefaultDockerContextStore()
contexts, err := store.Store.List()
if err != nil {
return err
}
for _, context := range contexts {
if context.Name == domainName {
logrus.Debugf("context for %s already exists", domainName)
return nil
}
}
logrus.Debugf("creating context with domain %s, username %s and port %s", domainName, username, port)
if err := client.CreateContext(domainName, username, port); err != nil {
return err
}
return nil
}
func newClient(c *cli.Context, domainName string) (*dockerClient.Client, error) {
cl, err := client.New(domainName)
if err != nil {
return &dockerClient.Client{}, err
}
return cl, nil
}
func installDocker(c *cli.Context, cl *dockerClient.Client, sshCl *ssh.Client, domainName string) error {
exists, err := ensureRemoteExecutable("docker", sshCl)
if err != nil {
return err
}
if !exists {
fmt.Println(fmt.Sprintf(dockerInstallMsg, domainName))
response := false
prompt := &survey.Confirm{
Message: fmt.Sprintf("attempt install docker on %s?", domainName),
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
logrus.Fatal("exiting as requested")
}
exes := []string{"wget", "bash"}
if askSudoPass {
exes = append(exes, "ssh-askpass")
}
for _, exe := range exes {
exists, err := ensureRemoteExecutable(exe, sshCl)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("%s missing on remote, please install it", exe)
}
}
var sudoPass string
if askSudoPass {
cmd := "wget -O- https://get.docker.com | bash"
prompt := &survey.Password{
Message: "sudo password?",
}
if err := survey.AskOne(prompt, &sudoPass); err != nil {
return err
}
logrus.Debugf("running %s on %s now with sudo password", cmd, domainName)
if sudoPass == "" {
return fmt.Errorf("missing sudo password but requested --ask-sudo-pass?")
}
logrus.Warn("installing docker, this could take some time...")
if err := ssh.RunSudoCmd(cmd, sudoPass, sshCl); err != nil {
fmt.Print(fmt.Sprintf(`
Abra was unable to bootstrap Docker, see below for logs:
%s
If nothing works, you try running the Docker install script manually on your server:
wget -O- https://get.docker.com | bash
`, string(err.Error())))
logrus.Fatal("Process exited with status 1")
}
logrus.Infof("docker is installed on %s", domainName)
remoteUser := sshCl.SSHClient.Conn.User()
logrus.Infof("adding %s to docker group", remoteUser)
permsCmd := fmt.Sprintf("sudo usermod -aG docker %s", remoteUser)
if err := ssh.RunSudoCmd(permsCmd, sudoPass, sshCl); err != nil {
return err
}
} else {
cmd := "wget -O- https://get.docker.com | bash"
logrus.Debugf("running %s on %s now without sudo password", cmd, domainName)
logrus.Warn("installing docker, this could take some time...")
if out, err := sshCl.Exec(cmd); err != nil {
fmt.Print(fmt.Sprintf(`
Abra was unable to bootstrap Docker, see below for logs:
%s
This could be due to a number of things but one of the most common is that your
server user account does not have sudo access, and if it does, you need to pass
"--ask-sudo-pass" in order to supply Abra with your password.
If nothing works, you try running the Docker install script manually on your server:
wget -O- https://get.docker.com | bash
`, string(out)))
logrus.Fatal(err)
}
logrus.Infof("docker is installed on %s", domainName)
}
}
return nil
}
func initSwarmLocal(c *cli.Context, cl *dockerClient.Client, domainName string) error {
initReq := swarm.InitRequest{ListenAddr: "0.0.0.0:2377"}
if _, err := cl.SwarmInit(context.Background(), initReq); err != nil {
if strings.Contains(err.Error(), "is already part of a swarm") ||
strings.Contains(err.Error(), "must specify a listening address") {
logrus.Infof("swarm mode already initialised on %s", domainName)
} else {
return err
}
} else {
logrus.Infof("initialised swarm mode on local server")
}
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
if _, err := cl.NetworkCreate(context.Background(), "proxy", netOpts); err != nil {
if !strings.Contains(err.Error(), "proxy already exists") {
return err
}
logrus.Info("swarm overlay network already created on local server")
} else {
logrus.Infof("swarm overlay network created on local server")
}
return nil
}
func initSwarm(c *cli.Context, cl *dockerClient.Client, domainName string) error {
ipv4, err := dns.EnsureIPv4(domainName)
if err != nil {
return err
}
initReq := swarm.InitRequest{
ListenAddr: "0.0.0.0:2377",
AdvertiseAddr: ipv4,
}
if _, err := cl.SwarmInit(context.Background(), initReq); err != nil {
if strings.Contains(err.Error(), "is already part of a swarm") ||
strings.Contains(err.Error(), "must specify a listening address") {
logrus.Infof("swarm mode already initialised on %s", domainName)
} else {
return err
}
} else {
logrus.Infof("initialised swarm mode on %s", domainName)
}
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
if _, err := cl.NetworkCreate(context.Background(), "proxy", netOpts); err != nil {
if !strings.Contains(err.Error(), "proxy already exists") {
return err
}
logrus.Infof("swarm overlay network already created on %s", domainName)
} else {
logrus.Infof("swarm overlay network created on %s", domainName)
}
return nil
}
func createServerDir(domainName string) error {
if err := server.CreateServerDir(domainName); err != nil {
if !os.IsExist(err) {
return err
}
logrus.Debugf("server dir for %s already created", domainName)
}
return nil
}
var serverAddCommand = cli.Command{
Name: "add",
Aliases: []string{"a"},
Usage: "Add a server to your configuration",
Description: `
This command adds a new server to your configuration so that it can be managed
by Abra. This command can also provision your server ("--provision/-p") with a
Docker installation so that it is capable of hosting Co-op Cloud apps.
Abra will default to expecting that you have a running ssh-agent and are using
SSH keys to connect to your new server. Abra will also read your SSH config
(matching "Host" as <domain>). SSH connection details precedence follows as
such: command-line > SSH config > guessed defaults.
If you have no SSH key configured for this host and are instead using password
authentication, you may pass "--ssh-auth password" to have Abra ask you for the
password. "--ask-sudo-pass" may be passed if you run your provisioning commands
via sudo privilege escalation.
The <domain> argument must be a publicy accessible domain name which points to
your server. You should working SSH access to this server already, Abra will
assume port 22 and will use your current system username to make an initial
connection. You can use the <user> and <port> arguments to adjust this.
Example:
abra server add varia.zone glodemodem 12345 -p
Abra will construct the following SSH connection and Docker context:
ssh://globemodem@varia.zone:12345
All communication between Abra and the server will use this SSH connection.
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.
`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
localFlag,
provisionFlag,
sshAuthFlag,
askSudoPassFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> [<user>] [<port>]",
Action: func(c *cli.Context) error {
if len(c.Args()) > 0 && local || !internal.ValidateSubCmdFlags(c) {
err := errors.New("cannot use <domain> and --local together")
internal.ShowSubcommandHelpAndError(c, err)
}
if sshAuth != "password" && sshAuth != "identity-file" {
err := errors.New("--ssh-auth only accepts identity-file or password")
internal.ShowSubcommandHelpAndError(c, err)
}
domainName := internal.ValidateDomain(c)
if local {
if err := newLocalServer(c, "default"); err != nil {
logrus.Fatal(err)
}
return nil
}
username := c.Args().Get(1)
if username == "" {
systemUser, err := user.Current()
if err != nil {
return err
}
username = systemUser.Username
}
port := c.Args().Get(2)
if port == "" {
port = "22"
}
if err := createServerDir(domainName); err != nil {
logrus.Fatal(err)
}
if err := newContext(c, domainName, username, port); err != nil {
logrus.Fatal(err)
}
cl, err := newClient(c, domainName)
if err != nil {
cleanUp(domainName)
logrus.Debugf("failed to construct client for %s, saw %s", domainName, err.Error())
logrus.Fatalf(fmt.Sprintf(internal.ServerAddFailMsg, domainName))
}
if provision {
logrus.Debugf("attempting to construct SSH client for %s", domainName)
sshCl, err := ssh.New(domainName, sshAuth, username, port)
if err != nil {
cleanUp(domainName)
logrus.Fatalf(fmt.Sprintf(internal.ServerAddFailMsg, domainName))
}
defer sshCl.Close()
logrus.Debugf("successfully created SSH client for %s", domainName)
if err := installDocker(c, cl, sshCl, domainName); err != nil {
logrus.Fatal(err)
}
if err := initSwarm(c, cl, domainName); err != nil {
logrus.Fatal(err)
}
}
if _, err := cl.Info(context.Background()); err != nil {
cleanUp(domainName)
logrus.Fatalf(fmt.Sprintf(internal.ServerAddFailMsg, domainName))
}
return nil
},
}
// ensureLocalExecutable ensures that an executable is present on the local machine
func ensureLocalExecutable(exe string) (bool, error) {
out, err := exec.Command("which", exe).Output()
if err != nil {
return false, err
}
return string(out) != "", nil
}
// ensureRemoteExecutable ensures that an executable is present on a remote machine
func ensureRemoteExecutable(exe string, sshCl *ssh.Client) (bool, error) {
out, err := sshCl.Exec(fmt.Sprintf("which %s", exe))
if err != nil && string(out) != "" {
return false, err
}
return string(out) != "", nil
}
+67
View File
@@ -0,0 +1,67 @@
package server
import (
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/formatter"
"github.com/docker/cli/cli/connhelper/ssh"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var serverListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List managed servers",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
dockerContextStore := context.NewDefaultDockerContextStore()
contexts, err := dockerContextStore.Store.List()
if err != nil {
logrus.Fatal(err)
}
tableColumns := []string{"name", "host", "user", "port"}
table := formatter.CreateTable(tableColumns)
defer table.Render()
serverNames, err := config.ReadServerNames()
if err != nil {
logrus.Fatal(err)
}
for _, serverName := range serverNames {
var row []string
for _, ctx := range contexts {
endpoint, err := context.GetContextEndpoint(ctx)
if err != nil && strings.Contains(err.Error(), "does not exist") {
// No local context found, we can continue safely
continue
}
if ctx.Name == serverName {
sp, err := ssh.ParseURL(endpoint)
if err != nil {
logrus.Fatal(err)
}
row = []string{serverName, sp.Host, sp.User, sp.Port}
}
}
if len(row) == 0 {
if serverName == "default" {
row = []string{serverName, "local", "n/a", "n/a"}
} else {
row = []string{serverName, "unknown", "unknown", "unknown"}
}
}
table.Append(row)
}
return nil
},
}
+262
View File
@@ -0,0 +1,262 @@
package server
import (
"context"
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/libcapsul"
"github.com/AlecAivazis/survey/v2"
"github.com/hetznercloud/hcloud-go/hcloud"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
func newHetznerCloudVPS(c *cli.Context) error {
if err := internal.EnsureNewHetznerCloudVPSFlags(c); err != nil {
return err
}
client := hcloud.NewClient(hcloud.WithToken(internal.HetznerCloudAPIToken))
var sshKeysRaw []string
var sshKeys []*hcloud.SSHKey
for _, sshKey := range c.StringSlice("hetzner-ssh-keys") {
if sshKey == "" {
continue
}
sshKey, _, err := client.SSHKey.GetByName(context.Background(), sshKey)
if err != nil {
return err
}
sshKeys = append(sshKeys, sshKey)
sshKeysRaw = append(sshKeysRaw, sshKey.Name)
}
serverOpts := hcloud.ServerCreateOpts{
Name: internal.HetznerCloudName,
ServerType: &hcloud.ServerType{Name: internal.HetznerCloudType},
Image: &hcloud.Image{Name: internal.HetznerCloudImage},
SSHKeys: sshKeys,
Location: &hcloud.Location{Name: internal.HetznerCloudLocation},
}
sshKeyIDs := strings.Join(sshKeysRaw, "\n")
if sshKeyIDs == "" {
sshKeyIDs = "N/A (password auth)"
}
tableColumns := []string{"name", "type", "image", "ssh-keys", "location"}
table := formatter.CreateTable(tableColumns)
table.Append([]string{
internal.HetznerCloudName,
internal.HetznerCloudType,
internal.HetznerCloudImage,
sshKeyIDs,
internal.HetznerCloudLocation,
})
table.Render()
response := false
prompt := &survey.Confirm{
Message: "continue with hetzner cloud VPS creation?",
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
logrus.Fatal("exiting as requested")
}
res, _, err := client.Server.Create(context.Background(), serverOpts)
if err != nil {
return err
}
var rootPassword string
if len(sshKeys) > 0 {
rootPassword = "N/A (using SSH keys)"
} else {
rootPassword = res.RootPassword
}
ip := res.Server.PublicNet.IPv4.IP.String()
fmt.Println(fmt.Sprintf(`
Your new Hetzner Cloud VPS has successfully been created! Here are the details:
name: %s
IP address: %s
root password: %s
You can access this new VPS via SSH using the following command:
ssh root@%s
Please note, this server is not managed by Abra yet (i.e. "abra server ls" will
not list this server)! You will need to assign a domain name record ("abra
record new") and add the server to your Abra configuration ("abra server add")
to have a working server that you can deploy Co-op Cloud apps to.
When setting up domain name records, you probably want to set up the following
2 A records. This supports deploying apps to your root domain (e.g.
example.com) and other apps on sub-domains (e.g. foo.example.com,
bar.example.com).
@ 1800 IN A %s
* 1800 IN A %s
`,
internal.HetznerCloudName, ip, rootPassword,
ip, ip, ip,
))
return nil
}
func newCapsulVPS(c *cli.Context) error {
if err := internal.EnsureNewCapsulVPSFlags(c); err != nil {
return err
}
capsulCreateURL := fmt.Sprintf("https://%s/api/capsul/create", internal.CapsulInstanceURL)
var sshKeys []string
for _, sshKey := range c.StringSlice("capsul-ssh-keys") {
if sshKey == "" {
continue
}
sshKeys = append(sshKeys, sshKey)
}
tableColumns := []string{"instance", "name", "type", "image", "ssh-keys"}
table := formatter.CreateTable(tableColumns)
table.Append([]string{
internal.CapsulInstanceURL,
internal.CapsulName,
internal.CapsulType,
internal.CapsulImage,
strings.Join(sshKeys, "\n"),
})
table.Render()
response := false
prompt := &survey.Confirm{
Message: "continue with capsul creation?",
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
logrus.Fatal("exiting as requested")
}
capsulClient := libcapsul.New(capsulCreateURL, internal.CapsulAPIToken)
resp, err := capsulClient.Create(
internal.CapsulName,
internal.CapsulType,
internal.CapsulImage,
sshKeys,
)
if err != nil {
return err
}
fmt.Println(fmt.Sprintf(`
Your new Capsul has successfully been created! Here are the details:
Capsul name: %s
Capsul ID: %v
You will need to log into your Capsul instance web interface to retrieve the IP
address. You can learn all about how to get SSH access to your new Capsul on:
%s/about-ssh
Please note, this server is not managed by Abra yet (i.e. "abra server ls" will
not list this server)! You will need to assign a domain name record ("abra
record new") and add the server to your Abra configuration ("abra server add")
to have a working server that you can deploy Co-op Cloud apps to.
When setting up domain name records, you probably want to set up the following
2 A records. This supports deploying apps to your root domain (e.g.
example.com) and other apps on sub-domains (e.g. foo.example.com,
bar.example.com).
@ 1800 IN A <your-capsul-ip>
* 1800 IN A <your-capsul-ip>
`, internal.CapsulName, resp.ID, internal.CapsulInstanceURL))
return nil
}
var serverNewCommand = cli.Command{
Name: "new",
Aliases: []string{"n"},
Usage: "Create a new server using a 3rd party provider",
Description: `
This command creates a new server via a 3rd party provider.
The following providers are supported:
Capsul https://git.cyberia.club/Cyberia/capsul-flask
Hetzner Cloud https://docs.hetzner.com/cloud
You may invoke this command in "wizard" mode and be prompted for input:
abra record new
API tokens are read from the environment if specified, e.g.
export HCLOUD_TOKEN=...
Where "$provider_TOKEN" is the expected env var format.
`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ServerProviderFlag,
// Capsul
internal.CapsulInstanceURLFlag,
internal.CapsulTypeFlag,
internal.CapsulImageFlag,
internal.CapsulSSHKeysFlag,
internal.CapsulAPITokenFlag,
// Hetzner
internal.HetznerCloudNameFlag,
internal.HetznerCloudTypeFlag,
internal.HetznerCloudImageFlag,
internal.HetznerCloudSSHKeysFlag,
internal.HetznerCloudLocationFlag,
internal.HetznerCloudAPITokenFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
if err := internal.EnsureServerProvider(); err != nil {
logrus.Fatal(err)
}
switch internal.ServerProvider {
case "capsul":
if err := newCapsulVPS(c); err != nil {
logrus.Fatal(err)
}
case "hetzner-cloud":
if err := newHetznerCloudVPS(c); err != nil {
logrus.Fatal(err)
}
}
return nil
},
}
+184
View File
@@ -0,0 +1,184 @@
package server
import (
"context"
"fmt"
"os"
"path/filepath"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"github.com/AlecAivazis/survey/v2"
"github.com/hetznercloud/hcloud-go/hcloud"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var rmServer bool
var rmServerFlag = &cli.BoolFlag{
Name: "server, s",
Usage: "remove the actual server also",
Destination: &rmServer,
}
func rmHetznerCloudVPS(c *cli.Context) error {
if internal.HetznerCloudName == "" && !internal.NoInput {
prompt := &survey.Input{
Message: "specify hetzner cloud VPS name",
}
if err := survey.AskOne(prompt, &internal.HetznerCloudName); err != nil {
return err
}
}
if internal.HetznerCloudAPIToken == "" && !internal.NoInput {
token, ok := os.LookupEnv("HCLOUD_TOKEN")
if !ok {
prompt := &survey.Input{
Message: "specify hetzner cloud API token",
}
if err := survey.AskOne(prompt, &internal.HetznerCloudAPIToken); err != nil {
return err
}
} else {
internal.HetznerCloudAPIToken = token
}
}
client := hcloud.NewClient(hcloud.WithToken(internal.HetznerCloudAPIToken))
server, _, err := client.Server.Get(context.Background(), internal.HetznerCloudName)
if err != nil {
return err
}
if server == nil {
logrus.Fatalf("library provider reports that %s doesn't exist?", internal.HetznerCloudName)
}
fmt.Println(fmt.Sprintf(`
You have requested that Abra delete the following server (%s). Please be
absolutely sure that this is indeed the server that you would like to have
removed. There will be no going back once you confirm, the server will be
destroyed.
`, server.Name))
tableColumns := []string{"name", "type", "image", "location"}
table := formatter.CreateTable(tableColumns)
table.Append([]string{
server.Name,
server.ServerType.Name,
server.Image.Name,
server.Datacenter.Name,
})
table.Render()
response := false
prompt := &survey.Confirm{
Message: "continue with hetzner cloud VPS removal?",
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
logrus.Fatal("exiting as requested")
}
_, err = client.Server.Delete(context.Background(), server)
if err != nil {
return err
}
logrus.Infof("%s has been deleted from your hetzner cloud account", internal.HetznerCloudName)
return nil
}
var serverRemoveCommand = cli.Command{
Name: "remove",
Aliases: []string{"rm"},
ArgsUsage: "[<server>]",
Usage: "Remove a managed server",
Description: `
This command removes a server from Abra management.
Depending on whether you used a 3rd party provider to create this server ("abra
server new"), you can also destroy the virtual server as well. Pass
"--server/-s" to tell Abra to try to delete this VPS.
Otherwise, Abra will remove the internal bookkeeping (~/.abra/servers/...) and
underlying client connection context. This server will then be lost in time,
like tears in rain.
`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
rmServerFlag,
internal.ServerProviderFlag,
// Hetzner
internal.HetznerCloudNameFlag,
internal.HetznerCloudAPITokenFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
serverName := internal.ValidateServer(c)
warnMsg := `Did not pass -s/--server for actual server deletion, prompting!
Abra doesn't currently know if it helped you create this server with one of the
3rd party integrations (e.g. Capsul). You have a choice here to actually,
really and finally destroy this server using those integrations. If you want to
do this, choose Yes.
If you just want to remove the server config files & context, choose No.
`
if !rmServer {
logrus.Warn(fmt.Sprintf(warnMsg))
response := false
prompt := &survey.Confirm{
Message: "delete actual live server?",
}
if err := survey.AskOne(prompt, &response); err != nil {
logrus.Fatal(err)
}
if response {
logrus.Info("setting -s/--server and attempting to remove actual server")
rmServer = true
}
}
if rmServer {
if err := internal.EnsureServerProvider(); err != nil {
logrus.Fatal(err)
}
switch internal.ServerProvider {
case "capsul":
logrus.Warn("capsul provider does not support automatic removal yet, sorry!")
case "hetzner-cloud":
if err := rmHetznerCloudVPS(c); err != nil {
logrus.Fatal(err)
}
}
}
if err := client.DeleteContext(serverName); err != nil {
logrus.Fatal(err)
}
if err := os.RemoveAll(filepath.Join(config.SERVERS_DIR, serverName)); err != nil {
logrus.Fatal(err)
}
logrus.Infof("server at %s has been lost in time, like tears in rain", serverName)
return nil
},
}
+27
View File
@@ -0,0 +1,27 @@
package server
import (
"github.com/urfave/cli"
)
// ServerCommand defines the `abra server` command and its subcommands
var ServerCommand = cli.Command{
Name: "server",
Aliases: []string{"s"},
Usage: "Manage servers",
Description: `
These commands support creating, managing and removing servers using 3rd party
integrations.
Servers can be created from scratch using the "abra server new" command. If you
already have a server, you can add it to your configuration using "abra server
add". Abra can provision servers so that they are ready to deploy Co-op Cloud
apps, see available flags on "server add" for more.
`,
Subcommands: []cli.Command{
serverNewCommand,
serverAddCommand,
serverListCommand,
serverRemoveCommand,
},
}
+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.RunApp(Version, Commit)
}
-52
View File
@@ -1,52 +0,0 @@
#compdef abra
_abra () {
local context state line curcontext="$curcontext" ret=1
_arguments -n : \
{-h,--help}'[Help message]' \
'1:commands:(app server)' \
'*::arguments:->arguments' \
&& ret=0
case $state in
(arguments)
curcontext="${curcontext%:*:*}:abra-arguments-$words[1]:"
case $words[1] in
(app)
_arguments \
'1: :_abra_apps' \
&& ret=0
;;
(server)
_arguments \
'1:servers:_abra_servers' \
&& ret=0
;;
esac
;;
esac
return ret
}
_abra_servers() {
_path_files -/W $HOME/.abra/servers
}
_abra_apps()
{
local newapps apps=($HOME/.abra/servers/*/*.env)
typeset -a apps
newapps=()
for app in $apps; do
newapps+=($(_abra_basename "${app}"))
done
_describe -t apps 'app' newapps
}
_abra_basename()
{
printf -- "${1##*/}"
}
_abra "$@"
-117
View File
@@ -1,117 +0,0 @@
#!/usr/bin/env bash
_abra_basename()
{
echo "${1##*/}"
}
_abra_servers()
{
# FIXME 3wc: copied from abra/get_servers()
shopt -s nullglob dotglob
local SERVERS=(~/.abra/servers/*)
shopt -u nullglob dotglob
for SERVER in "${SERVERS[@]}"; do
_abra_basename "${SERVER}"
done
}
_abra_complete_servers()
{
mapfile -t COMPREPLY < <(compgen -W "$(_abra_servers)" -- "$1")
}
_abra_apps()
{
shopt -s nullglob dotglob
local APPS=(~/.abra/servers/*/*.env)
shopt -u nullglob dotglob
for APP in "${APPS[@]}"; do
_abra_basename "${APP%.env}"
done
}
_abra_complete_apps()
{
mapfile -t COMPREPLY < <(compgen -W "$(_abra_apps)" -- "$1")
}
_abra_complete()
{
compopt +o default +o nospace
COMPREPLY=()
local -r cmds='
app
server
'
local -r short_opts='-e -h -s -v'
local -r long_opts='--env --help --stack --version'
# Scan through the command line and find the abra command
# (if present), as well as its expected position.
local cmd
local cmd_index=1 # Expected index of the command token.
local i
for (( i = 1; i < ${#COMP_WORDS[@]}; i++ )); do
local word="${COMP_WORDS[i]}"
case "$word" in
-*)
((cmd_index++))
;;
*)
cmd="$word"
break
;;
esac
done
local cur="${COMP_WORDS[COMP_CWORD]}"
if (( COMP_CWORD < cmd_index )); then
# Offer option completions.
case "$cur" in
--*)
mapfile -t COMPREPLY < <(compgen -W "$long_opts" -- "$cur")
;;
-*)
mapfile -t COMPREPLY < <(compgen -W "$short_opts" -- "$cur")
;;
*)
# Skip completion; we should never get here.
;;
esac
elif (( COMP_CWORD == cmd_index )); then
# Offer command name completions.
mapfile -t COMPREPLY < <(compgen -W "$cmds" -- "$cur")
else
# Offer command argument completions.
case "$cmd" in
server)
# Offer exactly one server name completion.
if (( COMP_CWORD == cmd_index + 1 )); then
_abra_complete_servers "$cur"
fi
;;
app)
# Offer exactly one app completion.
if (( COMP_CWORD == cmd_index + 1 )); then
_abra_complete_apps "$cur"
fi
;;
#help)
# # Offer exactly one command name completion.
# if (( COMP_CWORD == cmd_index + 1 )); then
# COMPREPLY=($(compgen -W "$cmds" -- "$cur"))
# fi
# ;;
*)
# Unknown command or unknowable argument.
;;
esac
fi
}
complete -o default -F _abra_complete abra
+54
View File
@@ -0,0 +1,54 @@
module coopcloud.tech/abra
go 1.16
require (
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52
github.com/AlecAivazis/survey/v2 v2.3.4
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731094149-b031ea1211e7
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
github.com/docker/cli v20.10.14+incompatible
github.com/docker/distribution v2.8.1+incompatible
github.com/docker/docker v20.10.14+incompatible
github.com/docker/go-units v0.4.0
github.com/go-git/go-git/v5 v5.4.2
github.com/hetznercloud/hcloud-go v1.33.1
github.com/moby/sys/signal v0.7.0
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.8.6
github.com/schultz-is/passgen v1.0.1
github.com/sirupsen/logrus v1.8.1
gotest.tools/v3 v3.1.0
)
require (
coopcloud.tech/libcapsul v0.0.0-20211022074848-c35e78fe3f3e
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 // indirect
github.com/buger/goterm v1.0.4
github.com/containerd/containerd v1.5.9 // indirect
github.com/containers/image v3.0.2+incompatible
github.com/containers/storage v1.38.2 // indirect
github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/fvbommel/sortorder v1.0.2 // indirect
github.com/gliderlabs/ssh v0.3.3
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/hashicorp/go-retryablehttp v0.7.1
github.com/kevinburke/ssh_config v1.2.0
github.com/klauspost/pgzip v1.2.5
github.com/libdns/gandi v1.0.2
github.com/libdns/libdns v0.2.1
github.com/moby/sys/mount v0.2.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20211202193544-a5463b7f9c84 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/spf13/cobra v1.3.0 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/urfave/cli v1.22.5
github.com/xanzy/ssh-agent v0.3.1 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190809123943-df4f5c81cb3b // indirect
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27
)
+1637
View File
File diff suppressed because it is too large Load Diff
-34
View File
@@ -1,34 +0,0 @@
#!/bin/bash
ABRA_VERSION="0.6.0"
GIT_URL="https://git.autonomic.zone/coop-cloud/abra"
ABRA_SRC="$GIT_URL/raw/tag/$ABRA_VERSION/abra"
function install_abra_release {
mkdir -p "$HOME/.local/bin"
curl "$ABRA_SRC" > "$HOME/.local/bin/abra"
chmod +x "$HOME/.local/bin/abra"
echo "abra installed to $HOME/.local/bin/abra"
}
function install_abra_dev {
mkdir -p "$HOME/.abra/"
if [[ ! -d "$HOME/.abra/src" ]]; then
git clone "$GIT_URL" "$HOME/.abra/src"
fi
( cd "$HOME/.abra/src" && git pull origin main && cd -)
mkdir -p "$HOME/.local/bin"
ln -sf "$HOME/.abra/src/abra" "$HOME/.local/bin/abra"
echo "abra installed to $HOME/.local/bin/abra (development bleeding edge)"
}
function run_installation {
if [ "$1" = "--dev" ]; then
install_abra_dev
else
install_abra_release
fi
}
run_installation "$@"
exit 0
-50
View File
@@ -1,50 +0,0 @@
.PHONY: test shellcheck docopt kcov codecov release-installer
test:
@sudo DOCKER_CONTEXT=default docker run \
-v $$(pwd):/workdir \
--privileged \
-d \
--name=abra-test-dind \
-e DOCKER_TLS_CERTDIR="" \
decentral1se/docker-dind-bats-kcov
@DOCKER_CONTEXT=default docker exec \
-it \
abra-test-dind \
sh -c "cd /workdir && bats /workdir/tests"
@DOCKER_CONTEXT=default docker stop abra-test-dind
@DOCKER_CONTEXT=default docker rm abra-test-dind
shellcheck:
@docker run \
-it \
--rm \
-v $$(pwd):/workdir \
koalaman/shellcheck-alpine \
shellcheck /workdir/abra
docopt:
@if [ ! -d ".venv" ]; then \
python3 -m venv .venv && \
.venv/bin/pip install -U pip setuptools wheel && \
.venv/bin/pip install docopt-sh; \
fi
.venv/bin/docopt.sh abra
kcov:
@docker run \
-it \
--rm \
-v $$(pwd):/workdir \
kcov/kcov:latest \
sh -c "kcov /workdir/coverage /workdir/abra || true"
codecov: SHELL:=/bin/bash
codecov:
@bash <(curl -s https://codecov.io/bash) \
-s coverage -t $$(pass show hosts/swarm.autonomic.zone/drone/codecov/token)
release-installer:
@docker stack rm abra-installer-script && \
cd installer && \
docker stack deploy -c compose.yml abra-installer-script
+85
View File
@@ -0,0 +1,85 @@
package app
import (
"context"
"fmt"
"strings"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/upstream/stack"
apiclient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
// Get retrieves an app
func Get(appName string) (config.App, error) {
files, err := config.LoadAppFiles("")
if err != nil {
return config.App{}, err
}
app, err := config.GetApp(files, appName)
if err != nil {
return config.App{}, err
}
logrus.Debugf("retrieved %s for %s", app, appName)
return app, nil
}
// deployedServiceSpec represents a deployed service of an app.
type deployedServiceSpec struct {
Name string
Version string
}
// VersionSpec represents a deployed app and associated metadata.
type VersionSpec map[string]deployedServiceSpec
// DeployedVersions lists metadata (e.g. versions) for deployed
func DeployedVersions(ctx context.Context, cl *apiclient.Client, app config.App) (VersionSpec, bool, error) {
services, err := stack.GetStackServices(ctx, cl, app.StackName())
if err != nil {
return VersionSpec{}, false, err
}
appSpec := make(VersionSpec)
for _, service := range services {
serviceName := ParseServiceName(service.Spec.Name)
label := fmt.Sprintf("coop-cloud.%s.%s.version", app.StackName(), serviceName)
if deployLabel, ok := service.Spec.Labels[label]; ok {
version, _ := ParseVersionLabel(deployLabel)
appSpec[serviceName] = deployedServiceSpec{Name: serviceName, Version: version}
}
}
deployed := len(services) > 0
if deployed {
logrus.Debugf("detected %s as deployed versions of %s", appSpec, app.Name)
} else {
logrus.Debugf("detected %s as not deployed", app.Name)
}
return appSpec, len(services) > 0, nil
}
// ParseVersionLabel parses a $VERSION-$DIGEST app service label.
func ParseVersionLabel(label string) (string, string) {
// versions may look like v4.2-abcd or v4.2-alpine-abcd
idx := strings.LastIndex(label, "-")
version := label[:idx]
digest := label[idx+1:]
logrus.Debugf("parsed %s as version from %s", version, label)
logrus.Debugf("parsed %s as digest from %s", digest, label)
return version, digest
}
// ParseServiceName parses a $STACK_NAME_$SERVICE_NAME service label.
func ParseServiceName(label string) string {
idx := strings.LastIndex(label, "_")
serviceName := label[idx+1:]
logrus.Debugf("parsed %s as service name from %s", serviceName, label)
return serviceName
}
+63
View File
@@ -0,0 +1,63 @@
package autocomplete
import (
"fmt"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// AppNameComplete copletes app names
func AppNameComplete(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
}
// RecipeNameComplete completes recipe names
func RecipeNameComplete(c *cli.Context) {
catl, err := recipe.ReadRecipeCatalogue()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for name := range catl {
fmt.Println(name)
}
}
// SubcommandComplete completes subcommands.
func SubcommandComplete(c *cli.Context) {
if c.NArg() > 0 {
return
}
subcmds := []string{
"app",
"autocomplete",
"catalogue",
"recipe",
"record",
"server",
"upgrade",
}
for _, cmd := range subcmds {
fmt.Println(cmd)
}
}
+65
View File
@@ -0,0 +1,65 @@
// Package client provides Docker client initiatialisation functions.
package client
import (
"net/http"
"os"
"time"
contextPkg "coopcloud.tech/abra/pkg/context"
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
// New initiates a new Docker client.
func New(contextName string) (*client.Client, error) {
var clientOpts []client.Opt
if contextName != "default" {
context, err := GetContext(contextName)
if err != nil {
return nil, err
}
ctxEndpoint, err := contextPkg.GetContextEndpoint(context)
if err != nil {
return nil, err
}
helper, err := commandconnPkg.NewConnectionHelper(ctxEndpoint)
if err != nil {
return nil, err
}
httpClient := &http.Client{
// No tls, no proxy
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
}
logrus.Debugf("created client for %s", contextName)
return cl, nil
}
+91
View File
@@ -0,0 +1,91 @@
package client
import (
"errors"
"fmt"
"coopcloud.tech/abra/pkg/context"
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"
"github.com/sirupsen/logrus"
)
type Context = contextStore.Metadata
func CreateContext(contextName string, user string, port string) error {
host := contextName
if user != "" {
host = fmt.Sprintf("%s@%s", user, host)
}
if port != "" {
host = fmt.Sprintf("%s:%s", host, port)
}
host = fmt.Sprintf("ssh://%s", host)
if err := createContext(contextName, host); err != nil {
return err
}
logrus.Debugf("created the %s context", contextName)
return nil
}
// createContext interacts with Docker Context to create a Docker context config
func createContext(name string, host string) error {
s := 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("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
}
+57
View File
@@ -0,0 +1,57 @@
package client
import (
"context"
"fmt"
"strings"
"github.com/containers/image/docker"
"github.com/containers/image/types"
"github.com/docker/distribution/reference"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
// 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, fmt.Errorf("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
}
// GetTagDigest retrieves an image digest from a container registry.
func GetTagDigest(cl *client.Client, image reference.Named) (string, error) {
target := fmt.Sprintf("//%s", reference.Path(image))
ref, err := docker.ParseReference(target)
if err != nil {
return "", fmt.Errorf("failed to parse image %s, saw: %s", image, err.Error())
}
ctx := context.Background()
img, err := ref.NewImage(ctx, nil)
if err != nil {
logrus.Debugf("failed to query remote registry for %s, saw: %s", image, err.Error())
return "", fmt.Errorf("unable to read digest for %s", image)
}
defer img.Close()
digest := img.ConfigInfo().Digest.String()
if digest == "" {
return digest, fmt.Errorf("unable to read digest for %s", image)
}
return strings.Split(digest, ":")[1][:7], nil
}
+25
View File
@@ -0,0 +1,25 @@
package client
import (
"context"
"github.com/docker/docker/api/types/swarm"
)
func StoreSecret(secretName, secretValue, server string) error {
cl, err := New(server)
if err != nil {
return err
}
ctx := context.Background()
ann := swarm.Annotations{Name: secretName}
spec := swarm.SecretSpec{Annotations: ann, Data: []byte(secretValue)}
// We don't bother with the secret IDs for now
if _, err := cl.SecretCreate(ctx, spec); err != nil {
return err
}
return nil
}
+49
View File
@@ -0,0 +1,49 @@
package client
import (
"context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
)
func GetVolumes(ctx context.Context, server string, fs filters.Args) ([]*types.Volume, error) {
cl, err := New(server)
if err != nil {
return nil, err
}
volumeListOKBody, err := cl.VolumeList(ctx, fs)
volumeList := volumeListOKBody.Volumes
if err != nil {
return volumeList, err
}
return volumeList, nil
}
func GetVolumeNames(volumes []*types.Volume) []string {
var volumeNames []string
for _, vol := range volumes {
volumeNames = append(volumeNames, vol.Name)
}
return volumeNames
}
func RemoveVolumes(ctx context.Context, server string, volumeNames []string, force bool) error {
cl, err := New(server)
if err != nil {
return err
}
for _, volName := range volumeNames {
err := cl.VolumeRemove(ctx, volName, force)
if err != nil {
return err
}
}
return nil
}
+158
View File
@@ -0,0 +1,158 @@
package compose
import (
"fmt"
"io/ioutil"
"path"
"path/filepath"
"strings"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/upstream/stack"
loader "coopcloud.tech/abra/pkg/upstream/stack"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
)
// UpdateTag updates an image tag in-place on file system local compose files.
func UpdateTag(pattern, image, tag, recipeName string) (bool, error) {
composeFiles, err := filepath.Glob(pattern)
if err != nil {
return false, err
}
logrus.Debugf("considering %s config(s) for tag update", strings.Join(composeFiles, ", "))
for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}}
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath)
if err != nil {
return false, err
}
compose, err := loader.LoadComposefile(opts, sampleEnv)
if err != nil {
return false, err
}
for _, service := range compose.Services {
if service.Image == "" {
continue // may be a compose.$optional.yml file
}
img, _ := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return false, err
}
var composeTag string
switch img.(type) {
case reference.NamedTagged:
composeTag = img.(reference.NamedTagged).Tag()
default:
logrus.Debugf("unable to parse %s, skipping", img)
continue
}
composeImage := formatter.StripTagMeta(reference.Path(img))
logrus.Debugf("parsed %s from %s", composeTag, service.Image)
if image == composeImage {
bytes, err := ioutil.ReadFile(composeFile)
if err != nil {
return false, err
}
old := fmt.Sprintf("%s:%s", composeImage, composeTag)
new := fmt.Sprintf("%s:%s", composeImage, tag)
replacedBytes := strings.Replace(string(bytes), old, new, -1)
logrus.Debugf("updating %s to %s in %s", old, new, compose.Filename)
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil {
return false, err
}
}
}
}
return false, nil
}
// UpdateLabel updates a label in-place on file system local compose files.
func UpdateLabel(pattern, serviceName, label, recipeName string) error {
composeFiles, err := filepath.Glob(pattern)
if err != nil {
return err
}
logrus.Debugf("considering %s config(s) for label update", strings.Join(composeFiles, ", "))
for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}}
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath)
if err != nil {
return err
}
compose, err := loader.LoadComposefile(opts, sampleEnv)
if err != nil {
return err
}
serviceExists := false
var service composetypes.ServiceConfig
for _, s := range compose.Services {
if s.Name == serviceName {
service = s
serviceExists = true
}
}
if !serviceExists {
continue
}
discovered := false
for oldLabel, value := range service.Deploy.Labels {
if strings.HasPrefix(oldLabel, "coop-cloud") {
discovered = true
bytes, err := ioutil.ReadFile(composeFile)
if err != nil {
return err
}
old := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", value)
replacedBytes := strings.Replace(string(bytes), old, label, -1)
if old == label {
logrus.Warnf("%s is already set, nothing to do?", label)
return nil
}
logrus.Debugf("updating %s to %s in %s", old, label, compose.Filename)
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil {
return err
}
logrus.Infof("synced label %s to service %s", label, serviceName)
}
}
if !discovered {
logrus.Warn("no existing label found, automagic insertion not supported yet")
logrus.Fatalf("add '- \"%s\"' manually to the 'app' service in %s", label, composeFile)
}
}
return nil
}
+440
View File
@@ -0,0 +1,440 @@
package config
import (
"fmt"
"html/template"
"io/ioutil"
"os"
"path"
"strings"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/upstream/convert"
loader "coopcloud.tech/abra/pkg/upstream/stack"
stack "coopcloud.tech/abra/pkg/upstream/stack"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
)
// Type aliases to make code hints easier to understand
// AppEnv is a map of the values in an apps env config
type AppEnv = map[string]string
// 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
// App reprents an app with its env file read into memory
type App struct {
Name AppName
Recipe string
Domain string
Env AppEnv
Server string
Path string
}
// StackName gets what the docker safe stack name is for the app. This should
// not not shown to the user, use a.Name for that. Give the output of this
// command to Docker only.
func (a App) StackName() string {
if _, exists := a.Env["STACK_NAME"]; exists {
return a.Env["STACK_NAME"]
}
stackName := SanitiseAppName(a.Name)
if len(stackName) > 45 {
logrus.Debugf("trimming %s to %s to avoid runtime limits", stackName, stackName[:45])
stackName = stackName[:45]
}
a.Env["STACK_NAME"] = stackName
return stackName
}
// Filters retrieves exact app filters for querying the container runtime. Due
// to upstream issues, filtering works different depending on what you're
// querying. So, for example, secrets don't work with regex! The caller needs
// to implement their own validation that the right secrets are matched. In
// order to handle these cases, we provide the `appendServiceNames` /
// `exactMatch` modifiers.
func (a App) Filters(appendServiceNames, exactMatch bool) (filters.Args, error) {
filters := filters.NewArgs()
composeFiles, err := GetAppComposeFiles(a.Recipe, a.Env)
if err != nil {
return filters, err
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := GetAppComposeConfig(a.Recipe, opts, a.Env)
if err != nil {
return filters, err
}
for _, service := range compose.Services {
var filter string
if appendServiceNames {
if exactMatch {
filter = fmt.Sprintf("^%s_%s", a.StackName(), service.Name)
} else {
filter = fmt.Sprintf("%s_%s", a.StackName(), service.Name)
}
} else {
if exactMatch {
filter = fmt.Sprintf("^%s", a.StackName())
} else {
filter = fmt.Sprintf("%s", a.StackName())
}
}
filters.Add("name", filter)
}
return filters, nil
}
// ByServer sort a slice of Apps
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) < strings.ToLower(a[j].Recipe)
}
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) < strings.ToLower(a[j].Recipe)
}
// 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 := ReadEnv(appFile.Path)
if err != nil {
return App{}, fmt.Errorf("env file for %s couldn't be read: %s", name, err.Error())
}
logrus.Debugf("read env %s from %s", env, appFile.Path)
app, err := newApp(env, name, appFile)
if err != nil {
return App{}, fmt.Errorf("env file for %s has issues: %s", name, err.Error())
}
return app, nil
}
// newApp creates new App object
func newApp(env AppEnv, name string, appFile AppFile) (App, error) {
domain := env["DOMAIN"]
recipe, exists := env["RECIPE"]
if !exists {
recipe, exists = env["TYPE"]
if !exists {
return App{}, fmt.Errorf("%s is missing the RECIPE env var", name)
}
}
return App{
Name: name,
Domain: domain,
Recipe: recipe,
Env: env,
Server: appFile.Server,
Path: appFile.Path,
}, nil
}
// LoadAppFiles gets all app files for a given set of servers or all servers
func LoadAppFiles(servers ...string) (AppFiles, error) {
appFiles := make(AppFiles)
if len(servers) == 1 {
if servers[0] == "" {
// Empty servers flag, one string will always be passed
var err error
servers, err = GetAllFoldersInDirectory(SERVERS_DIR)
if err != nil {
return nil, err
}
}
}
logrus.Debugf("collecting metadata from %v servers: %s", len(servers), strings.Join(servers, ", "))
for _, server := range servers {
serverDir := path.Join(SERVERS_DIR, server)
files, err := getAllFilesInDirectory(serverDir)
if err != nil {
return nil, fmt.Errorf("server %s doesn't exist? Run \"abra server ls\" to check", server)
}
for _, file := range files {
appName := strings.TrimSuffix(file.Name(), ".env")
appFilePath := path.Join(SERVERS_DIR, server, file.Name())
appFiles[appName] = AppFile{
Path: appFilePath,
Server: server,
}
}
}
return appFiles, nil
}
// GetApp loads an apps settings, reading it from file, in preparation to use it
//
// ONLY use when ready to use the env file to keep IO down
func GetApp(apps AppFiles, name AppName) (App, error) {
appFile, exists := apps[name]
if !exists {
return App{}, fmt.Errorf("cannot find app with name %s", name)
}
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) ([]App, error) {
var apps []App
for name := range appFiles {
app, err := GetApp(appFiles, name)
if err != nil {
return nil, err
}
apps = append(apps, app)
}
return apps, 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 := GetAppComposeFiles(app.Recipe, app.Env)
if err != nil {
return serviceNames, err
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := GetAppComposeConfig(app.Recipe, 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(recipeName, appName, server, domain string) error {
envSamplePath := path.Join(RECIPES_DIR, recipeName, ".env.sample")
envSample, err := ioutil.ReadFile(envSamplePath)
if err != nil {
return err
}
appEnvPath := path.Join(ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName))
if _, err := os.Stat(appEnvPath); os.IsExist(err) {
return fmt.Errorf("%s already exists?", appEnvPath)
}
err = ioutil.WriteFile(appEnvPath, envSample, 0664)
if err != nil {
return err
}
file, err := os.OpenFile(appEnvPath, os.O_RDWR, 0664)
if err != nil {
return err
}
defer file.Close()
tpl, err := template.ParseFiles(appEnvPath)
if err != nil {
return err
}
type templateVars struct {
Name string
Domain string
}
tvars := templateVars{Name: recipeName, Domain: domain}
if err := tpl.Execute(file, tvars); err != nil {
return err
}
logrus.Debugf("copied & templated %s to %s", envSamplePath, appEnvPath)
return nil
}
// SanitiseAppName makes a app name usable with Docker by replacing illegal characters
func SanitiseAppName(name string) string {
return strings.ReplaceAll(name, ".", "_")
}
// GetAppStatuses queries servers to check the deployment status of given apps
func GetAppStatuses(appFiles AppFiles) (map[string]map[string]string, error) {
statuses := make(map[string]map[string]string)
var unique []string
servers := make(map[string]struct{})
for _, appFile := range appFiles {
if _, ok := servers[appFile.Server]; !ok {
servers[appFile.Server] = struct{}{}
unique = append(unique, appFile.Server)
}
}
bar := formatter.CreateProgressbar(len(servers), "querying remote servers...")
ch := make(chan stack.StackStatus, len(servers))
for server := range servers {
go func(s string) {
ch <- stack.GetAllDeployedServices(s)
bar.Add(1)
}(server)
}
for range servers {
status := <-ch
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.version", name)
if version, ok := service.Spec.Labels[labelKey]; ok {
result["version"] = version
} else {
continue
}
statuses[name] = result
}
}
logrus.Debugf("retrieved app statuses: %s", statuses)
return statuses, nil
}
// GetAppComposeFiles gets the list of compose files for an app which should be
// merged into a composetypes.Config while respecting the COMPOSE_FILE env var.
func GetAppComposeFiles(recipe string, appEnv AppEnv) ([]string, error) {
var composeFiles []string
if _, ok := appEnv["COMPOSE_FILE"]; !ok {
logrus.Debug("no COMPOSE_FILE detected, loading compose.yml")
path := fmt.Sprintf("%s/%s/compose.yml", RECIPES_DIR, recipe)
composeFiles = append(composeFiles, path)
return composeFiles, nil
}
composeFileEnvVar := appEnv["COMPOSE_FILE"]
envVars := strings.Split(composeFileEnvVar, ":")
logrus.Debugf("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", "))
for _, file := range strings.Split(composeFileEnvVar, ":") {
path := fmt.Sprintf("%s/%s/%s", RECIPES_DIR, recipe, file)
composeFiles = append(composeFiles, path)
}
logrus.Debugf("retrieved %s configs for %s", strings.Join(composeFiles, ", "), recipe)
return composeFiles, 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 AppEnv) (*composetypes.Config, error) {
compose, err := loader.LoadComposefile(opts, appEnv)
if err != nil {
return &composetypes.Config{}, err
}
logrus.Debugf("retrieved %s for %s", compose.Filename, recipe)
return compose, nil
}
+36
View File
@@ -0,0 +1,36 @@
package config
import (
"reflect"
"testing"
)
func TestNewApp(t *testing.T) {
app, err := newApp(expectedAppEnv, appName, expectedAppFile)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(app, expectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp)
}
}
func TestReadAppEnvFile(t *testing.T) {
app, err := readAppEnvFile(expectedAppFile, appName)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(app, expectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp)
}
}
func TestGetApp(t *testing.T) {
app, err := GetApp(expectedAppFiles, appName)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(app, expectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp)
}
}
+160
View File
@@ -0,0 +1,160 @@
package config
import (
"bufio"
"fmt"
"io/fs"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"github.com/Autonomic-Cooperative/godotenv"
"github.com/sirupsen/logrus"
)
var ABRA_DIR = os.ExpandEnv("$HOME/.abra")
var SERVERS_DIR = path.Join(ABRA_DIR, "servers")
var RECIPES_DIR = path.Join(ABRA_DIR, "recipes")
var VENDOR_DIR = path.Join(ABRA_DIR, "vendor")
var BACKUP_DIR = path.Join(ABRA_DIR, "backups")
var RECIPES_JSON = path.Join(ABRA_DIR, "catalogue", "recipes.json")
var REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
var CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json"
var SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git"
// GetServers retrieves all servers.
func GetServers() ([]string, error) {
var servers []string
servers, err := GetAllFoldersInDirectory(SERVERS_DIR)
if err != nil {
return servers, err
}
logrus.Debugf("retrieved %v servers: %s", len(servers), servers)
return servers, nil
}
// ReadEnv loads an app envivornment into a map.
func ReadEnv(filePath string) (AppEnv, error) {
var envFile AppEnv
envFile, err := godotenv.Read(filePath)
if err != nil {
return nil, err
}
logrus.Debugf("read %s from %s", envFile, filePath)
return envFile, nil
}
// ReadServerNames retrieves all server names.
func ReadServerNames() ([]string, error) {
serverNames, err := GetAllFoldersInDirectory(SERVERS_DIR)
if err != nil {
return nil, err
}
logrus.Debugf("read %s from %s", strings.Join(serverNames, ","), SERVERS_DIR)
return serverNames, nil
}
// getAllFilesInDirectory returns filenames of all files in directory
func getAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
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 {
logrus.Warningf("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, fmt.Errorf("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 {
logrus.Warningf("broken symlink in your abra config folders: %s", filePath)
} else if stat, err := os.Stat(realDir); err == nil && stat.IsDir() {
// path is a directory
folders = append(folders, file.Name())
}
}
}
return folders, 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
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "export") {
splitVals := strings.Split(line, "export ")
envVarDef := splitVals[len(splitVals)-1]
keyVal := strings.Split(envVarDef, "=")
if len(keyVal) != 2 {
return envVars, fmt.Errorf("couldn't parse %s", line)
}
envVars[keyVal[0]] = keyVal[1]
}
}
logrus.Debugf("read %s from %s", envVars, abraSh)
return envVars, nil
}
+84
View File
@@ -0,0 +1,84 @@
package config
import (
"os"
"path"
"reflect"
"strings"
"testing"
)
var testFolder = os.ExpandEnv("$PWD/../../tests/resources/test_folder")
var validAbraConf = os.ExpandEnv("$PWD/../../tests/resources/valid_abra_config")
// make sure these are in alphabetical order
var tFolders = []string{"folder1", "folder2"}
var tFiles = []string{"bar.env", "foo.env"}
var appName = "ecloud"
var serverName = "evil.corp"
var expectedAppEnv = AppEnv{
"DOMAIN": "ecloud.evil.corp",
"RECIPE": "ecloud",
}
var expectedApp = App{
Name: appName,
Recipe: expectedAppEnv["RECIPE"],
Domain: expectedAppEnv["DOMAIN"],
Env: expectedAppEnv,
Path: expectedAppFile.Path,
Server: expectedAppFile.Server,
}
var expectedAppFile = AppFile{
Path: path.Join(validAbraConf, "servers", serverName, appName+".env"),
Server: serverName,
}
var expectedAppFiles = map[string]AppFile{
appName: expectedAppFile,
}
// var expectedServerNames = []string{"evil.corp"}
func TestGetAllFoldersInDirectory(t *testing.T) {
folders, err := GetAllFoldersInDirectory(testFolder)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(folders, tFolders) {
t.Fatalf("did not get expected folders. Expected: (%s), Got: (%s)", strings.Join(tFolders, ","), strings.Join(folders, ","))
}
}
func TestGetAllFilesInDirectory(t *testing.T) {
files, err := getAllFilesInDirectory(testFolder)
if err != nil {
t.Fatal(err)
}
var fileNames []string
for _, file := range files {
fileNames = append(fileNames, file.Name())
}
if !reflect.DeepEqual(fileNames, tFiles) {
t.Fatalf("did not get expected files. Expected: (%s), Got: (%s)", strings.Join(tFiles, ","), strings.Join(fileNames, ","))
}
}
func TestReadEnv(t *testing.T) {
env, err := ReadEnv(expectedAppFile.Path)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(env, expectedAppEnv) {
t.Fatalf(
"did not get expected application settings. Expected: DOMAIN=%s RECIPE=%s; Got: DOMAIN=%s RECIPE=%s",
expectedAppEnv["DOMAIN"],
expectedAppEnv["RECIPE"],
env["DOMAIN"],
env["RECIPE"],
)
}
}
+70
View File
@@ -0,0 +1,70 @@
package container
import (
"context"
"fmt"
"strings"
"coopcloud.tech/abra/pkg/formatter"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
// GetContainer retrieves a container. If noInput is false and the retrievd
// count of containers does not match 1, then a prompt is presented to let the
// user choose. A count of 0 is handled gracefully.
func GetContainer(c context.Context, cl *client.Client, filters filters.Args, noInput bool) (types.Container, error) {
containerOpts := types.ContainerListOptions{Filters: filters}
containers, err := cl.ContainerList(c, containerOpts)
if err != nil {
return types.Container{}, err
}
if len(containers) == 0 {
filter := filters.Get("name")[0]
return types.Container{}, fmt.Errorf("no containers matching the %v filter found?", filter)
}
if len(containers) != 1 {
var containersRaw []string
for _, container := range containers {
containerName := strings.Join(container.Names, " ")
trimmed := strings.TrimPrefix(containerName, "/")
created := formatter.HumanDuration(container.Created)
containersRaw = append(containersRaw, fmt.Sprintf("%s (created %v)", trimmed, created))
}
if noInput {
err := fmt.Errorf("expected 1 container but found %v: %s", len(containers), strings.Join(containersRaw, " "))
return types.Container{}, err
}
logrus.Warnf("ambiguous container list received, prompting for input")
var response string
prompt := &survey.Select{
Message: "which container are you looking for?",
Options: containersRaw,
}
if err := survey.AskOne(prompt, &response); err != nil {
return types.Container{}, err
}
chosenContainer := strings.TrimSpace(strings.Split(response, " ")[0])
for _, container := range containers {
containerName := strings.TrimSpace(strings.Join(container.Names, " "))
trimmed := strings.TrimPrefix(containerName, "/")
if trimmed == chosenContainer {
return container, nil
}
}
logrus.Panic("failed to match chosen container")
}
return containers[0], nil
}
+44
View File
@@ -0,0 +1,44 @@
package context
import (
"errors"
"github.com/docker/cli/cli/command"
dConfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/context"
contextStore "github.com/docker/cli/cli/context/store"
cliflags "github.com/docker/cli/cli/flags"
"github.com/moby/term"
)
func NewDefaultDockerContextStore() *command.ContextStoreWithDefault {
_, _, stderr := term.StdStreams()
dockerConfig := dConfig.LoadDefaultConfigFile(stderr)
contextDir := dConfig.ContextStoreDir()
storeConfig := command.DefaultContextStoreConfig()
store := newContextStore(contextDir, storeConfig)
opts := &cliflags.CommonOptions{Context: "default"}
dockerContextStore := &command.ContextStoreWithDefault{
Store: store,
Resolver: func() (*command.DefaultContext, error) {
return command.ResolveDefaultContext(opts, dockerConfig, storeConfig, stderr)
},
}
return dockerContextStore
}
func GetContextEndpoint(ctx contextStore.Metadata) (string, error) {
endpointmeta, ok := ctx.Endpoints["docker"].(context.EndpointMetaBase)
if !ok {
err := errors.New("context lacks Docker endpoint")
return "", err
}
return endpointmeta.Host, nil
}
func newContextStore(dir string, config contextStore.Config) contextStore.Store {
return contextStore.New(dir, config)
}
+103
View File
@@ -0,0 +1,103 @@
package dns
import (
"context"
"fmt"
"net"
"os"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
)
// NewToken constructs a new DNS provider token.
func NewToken(provider, providerTokenEnvVar string) (string, error) {
if token, present := os.LookupEnv(providerTokenEnvVar); present {
return token, nil
}
logrus.Debugf("no %s in environment, asking via stdin", providerTokenEnvVar)
var token string
prompt := &survey.Input{
Message: fmt.Sprintf("%s API token?", provider),
}
if err := survey.AskOne(prompt, &token); err != nil {
return "", err
}
return token, nil
}
// EnsureIPv4 ensures that an ipv4 address is set for a domain name
func EnsureIPv4(domainName string) (string, error) {
var ipv4 string
// comrade librehosters DNS resolver -> https://www.privacy-handbuch.de/handbuch_93d.htm
freifunkDNS := "5.1.66.255:53"
resolver := &net.Resolver{
PreferGo: false,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{
Timeout: time.Millisecond * time.Duration(10000),
}
return d.DialContext(ctx, "udp", freifunkDNS)
},
}
ctx := context.Background()
ips, err := resolver.LookupIPAddr(ctx, domainName)
if err != nil {
return ipv4, err
}
if len(ips) == 0 {
return ipv4, fmt.Errorf("unable to retrieve ipv4 address for %s", domainName)
}
ipv4 = ips[0].IP.To4().String()
logrus.Debugf("%s points to %s (resolver: %s)", domainName, ipv4, freifunkDNS)
return ipv4, nil
}
// EnsureDomainsResolveSameIPv4 ensures that domains resolve to the same ipv4 address
func EnsureDomainsResolveSameIPv4(domainName, server string) (string, error) {
var ipv4 string
domainIPv4, err := EnsureIPv4(domainName)
if err != nil {
return ipv4, err
}
if domainIPv4 == "" {
return ipv4, fmt.Errorf("cannot resolve ipv4 for %s?", domainName)
}
serverIPv4, err := EnsureIPv4(server)
if err != nil {
return ipv4, err
}
if serverIPv4 == "" {
return ipv4, fmt.Errorf("cannot resolve ipv4 for %s?", server)
}
if domainIPv4 != serverIPv4 {
err := "app domain %s (%s) does not appear to resolve to app server %s (%s)?"
return ipv4, fmt.Errorf(err, domainName, domainIPv4, server, serverIPv4)
}
return ipv4, nil
}
// GetTTL parses a ttl string into a duration
func GetTTL(ttl string) (time.Duration, error) {
val, err := time.ParseDuration(ttl)
if err != nil {
return val, err
}
return val, nil
}
+15
View File
@@ -0,0 +1,15 @@
package gandi
import (
"coopcloud.tech/abra/pkg/dns"
"github.com/libdns/gandi"
)
// New constructs a new DNs provider.
func New() (gandi.Provider, error) {
token, err := dns.NewToken("Gandi", "GANDI_TOKEN")
if err != nil {
return gandi.Provider{}, err
}
return gandi.Provider{APIToken: token}, nil
}
+71
View File
@@ -0,0 +1,71 @@
package formatter
import (
"os"
"strings"
"time"
"github.com/docker/go-units"
"github.com/olekukonko/tablewriter"
"github.com/schollz/progressbar/v3"
"github.com/sirupsen/logrus"
)
func ShortenID(str string) string {
return str[:12]
}
func SmallSHA(hash string) string {
return hash[:8]
}
// RemoveSha remove image sha from a string that are added in some docker outputs
func RemoveSha(str string) string {
return strings.Split(str, "@")[0]
}
// HumanDuration from docker/cli RunningFor() to be accessible outside of the class
func HumanDuration(timestamp int64) string {
date := time.Unix(timestamp, 0)
now := time.Now().UTC()
return units.HumanDuration(now.Sub(date)) + " ago"
}
// CreateTable prepares a table layout for output.
func CreateTable(columns []string) *tablewriter.Table {
table := tablewriter.NewWriter(os.Stdout)
table.SetAutoWrapText(false)
table.SetHeader(columns)
return table
}
// CreateProgressbar generates a progress bar
func CreateProgressbar(length int, title string) *progressbar.ProgressBar {
return progressbar.NewOptions(
length,
progressbar.OptionClearOnFinish(),
progressbar.OptionSetPredictTime(false),
progressbar.OptionShowCount(),
progressbar.OptionFullWidth(),
progressbar.OptionSetDescription(title),
)
}
// StripTagMeta strips front-matter image tag data that we don't need for parsing.
func StripTagMeta(image string) string {
originalImage := image
if strings.Contains(image, "docker.io") {
image = strings.Split(image, "/")[1]
}
if strings.Contains(image, "library") {
image = strings.Split(image, "/")[1]
}
if originalImage != image {
logrus.Debugf("stripped %s to %s for parsing", originalImage, image)
}
return image
}
+35
View File
@@ -0,0 +1,35 @@
package git
import (
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
)
// GetCurrentBranch retrieves the current branch of a repository
func GetCurrentBranch(repository *git.Repository) (string, error) {
branchRefs, err := repository.Branches()
if err != nil {
return "", err
}
headRef, err := repository.Head()
if err != nil {
return "", err
}
var currentBranchName string
err = branchRefs.ForEach(func(branchRef *plumbing.Reference) error {
if branchRef.Hash() == headRef.Hash() {
currentBranchName = branchRef.Name().String()
return nil
}
return nil
})
if err != nil {
return "", err
}
return currentBranchName, nil
}
+40
View File
@@ -0,0 +1,40 @@
package git
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus"
)
// Clone runs a git clone which accounts for different default branches.
func Clone(dir, url string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) {
logrus.Debugf("%s does not exist, attempting to git clone from %s", dir, url)
_, err := git.PlainClone(dir, false, &git.CloneOptions{URL: url, Tags: git.AllTags})
if err != nil {
logrus.Debugf("cloning %s default branch failed, attempting from main branch", url)
_, err := git.PlainClone(dir, false, &git.CloneOptions{
URL: url,
Tags: git.AllTags,
ReferenceName: plumbing.ReferenceName("refs/heads/main"),
})
if err != nil {
if strings.Contains(err.Error(), "authentication required") {
name := filepath.Base(dir)
return fmt.Errorf("unable to clone %s, does %s exist?", name, url)
}
return err
}
}
logrus.Debugf("%s has been git cloned successfully", dir)
} else {
logrus.Debugf("%s already exists", dir)
}
return nil
}
+56
View File
@@ -0,0 +1,56 @@
package git
import (
"fmt"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
)
// Commit runs a git commit
func Commit(repoPath, glob, commitMessage string, dryRun bool) error {
if commitMessage == "" {
return fmt.Errorf("no commit message specified?")
}
commitRepo, err := git.PlainOpen(repoPath)
if err != nil {
return err
}
commitWorktree, err := commitRepo.Worktree()
if err != nil {
return err
}
patterns, err := GetExcludesFiles()
if err != nil {
return err
}
if len(patterns) > 0 {
commitWorktree.Excludes = append(patterns, commitWorktree.Excludes...)
}
if !dryRun {
err = commitWorktree.AddGlob(glob)
if err != nil {
return err
}
logrus.Debugf("staged %s for commit", glob)
} else {
logrus.Debugf("dry run: did not stage %s for commit", glob)
}
if !dryRun {
_, err = commitWorktree.Commit(commitMessage, &git.CommitOptions{})
if err != nil {
return err
}
logrus.Debug("git changes commited")
} else {
logrus.Debug("dry run: no changes commited")
}
return nil
}
+14
View File
@@ -0,0 +1,14 @@
package git
import (
"fmt"
"os"
)
// EnsureGitRepo ensures a git repo .git folder exists
func EnsureGitRepo(repoPath string) error {
if _, err := os.Stat(repoPath); os.IsNotExist(err) {
return fmt.Errorf("no .git directory in %s?", repoPath)
}
return nil
}
+38
View File
@@ -0,0 +1,38 @@
package git
import (
"github.com/go-git/go-git/v5"
gitPkg "github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
)
// Init inits a new repo and commits all the stuff if you want
func Init(repoPath string, commit bool) error {
if _, err := gitPkg.PlainInit(repoPath, false); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("initialised new git repo in %s", repoPath)
if commit {
commitRepo, err := git.PlainOpen(repoPath)
if err != nil {
logrus.Fatal(err)
}
commitWorktree, err := commitRepo.Worktree()
if err != nil {
logrus.Fatal(err)
}
if err := commitWorktree.AddWithOptions(&git.AddOptions{All: true}); err != nil {
return err
}
if _, err = commitWorktree.Commit("init", &git.CommitOptions{}); err != nil {
return err
}
logrus.Debugf("init committed all files for new git repo in %s", repoPath)
}
return nil
}
+43
View File
@@ -0,0 +1,43 @@
package git
import (
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/sirupsen/logrus"
)
// Push pushes the latest changes & optionally tags to the default remote
func Push(repoDir string, remote string, tags bool, dryRun bool) error {
if dryRun {
logrus.Debugf("dry run: no git changes pushed in %s", repoDir)
return nil
}
commitRepo, err := git.PlainOpen(repoDir)
if err != nil {
return err
}
opts := &git.PushOptions{}
if remote != "" {
opts.RemoteName = remote
}
if err := commitRepo.Push(opts); err != nil {
return err
}
logrus.Debugf("git changes pushed")
if tags {
opts.RefSpecs = append(opts.RefSpecs, config.RefSpec("+refs/tags/*:refs/tags/*"))
if err := commitRepo.Push(opts); err != nil {
return err
}
logrus.Debugf("git tags pushed")
}
return nil
}
+187
View File
@@ -0,0 +1,187 @@
package git
import (
"io/ioutil"
"os"
"os/user"
"path"
"path/filepath"
"strings"
"coopcloud.tech/abra/pkg/config"
"github.com/go-git/go-git/v5"
gitConfigPkg "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
"github.com/sirupsen/logrus"
)
// GetRecipeHead retrieves latest HEAD metadata.
func GetRecipeHead(recipeName string) (*plumbing.Reference, error) {
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return nil, err
}
head, err := repo.Head()
if err != nil {
return nil, err
}
return head, nil
}
// IsClean checks if a repo has unstaged changes
func IsClean(repoPath string) (bool, error) {
repo, err := git.PlainOpen(repoPath)
if err != nil {
return false, err
}
worktree, err := repo.Worktree()
if err != nil {
return false, err
}
patterns, err := GetExcludesFiles()
if err != nil {
return false, err
}
if len(patterns) > 0 {
worktree.Excludes = append(patterns, worktree.Excludes...)
}
status, err := worktree.Status()
if err != nil {
return false, err
}
if status.String() != "" {
logrus.Debugf("discovered git status in %s: %s", repoPath, status.String())
} else {
logrus.Debugf("discovered clean git status in %s", repoPath)
}
return status.IsClean(), nil
}
// GetExcludesFiles reads the exlude files from a global gitignore
func GetExcludesFiles() ([]gitignore.Pattern, error) {
var err error
var patterns []gitignore.Pattern
cfg, err := parseGitConfig()
if err != nil {
return patterns, err
}
excludesfile := getExcludesFile(cfg)
patterns, err = parseExcludesFile(excludesfile)
if err != nil {
return patterns, err
}
return patterns, nil
}
func parseGitConfig() (*gitConfigPkg.Config, error) {
cfg := gitConfigPkg.NewConfig()
usr, err := user.Current()
if err != nil {
return nil, err
}
globalGitConfig := filepath.Join(usr.HomeDir, ".gitconfig")
if _, err := os.Stat(globalGitConfig); err != nil {
if os.IsNotExist(err) {
logrus.Debugf("no %s exists, not reading any global gitignore config", globalGitConfig)
return cfg, nil
}
return cfg, err
}
b, err := ioutil.ReadFile(globalGitConfig)
if err != nil {
return nil, err
}
if err := cfg.Unmarshal(b); err != nil {
return nil, err
}
return cfg, err
}
func getExcludesFile(cfg *gitConfigPkg.Config) string {
for _, sec := range cfg.Raw.Sections {
if sec.Name == "core" {
for _, opt := range sec.Options {
if opt.Key == "excludesfile" {
return opt.Value
}
}
}
}
return "~/.gitignore"
}
func parseExcludesFile(excludesfile string) ([]gitignore.Pattern, error) {
var ps []gitignore.Pattern
excludesfile, err := expandTilde(excludesfile)
if err != nil {
return nil, err
}
if _, err := os.Stat(excludesfile); err != nil {
if os.IsNotExist(err) {
logrus.Debugf("no %s exists, skipping reading gitignore paths", excludesfile)
return ps, nil
}
return ps, err
}
data, err := ioutil.ReadFile(excludesfile)
if err != nil {
return nil, err
}
var pathsRaw []string
for _, s := range strings.Split(string(data), "\n") {
if !strings.HasPrefix(s, "#") && len(strings.TrimSpace(s)) > 0 {
pathsRaw = append(pathsRaw, s)
ps = append(ps, gitignore.ParsePattern(s, nil))
}
}
logrus.Debugf("read global ignore paths: %s", strings.Join(pathsRaw, " "))
return ps, nil
}
func expandTilde(path string) (string, error) {
if !strings.HasPrefix(path, "~") {
return path, nil
}
var paths []string
u, err := user.Current()
if err != nil {
return "", err
}
for _, p := range strings.Split(path, string(filepath.Separator)) {
if p == "~" {
paths = append(paths, u.HomeDir)
} else {
paths = append(paths, p)
}
}
return filepath.Join(paths...), nil
}
+28
View File
@@ -0,0 +1,28 @@
package git
import (
"strings"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/sirupsen/logrus"
)
// CreateRemote creates a new git remote in a repository
func CreateRemote(repo *git.Repository, name, url string, dryRun bool) error {
if dryRun {
logrus.Debugf("dry run: remote %s (%s) not created", name, url)
return nil
}
if _, err := repo.CreateRemote(&config.RemoteConfig{
Name: name,
URLs: []string{url},
}); err != nil {
if !strings.Contains(err.Error(), "remote already exists") {
return err
}
}
return nil
}
+12
View File
@@ -0,0 +1,12 @@
package integration
import (
"os"
"testing"
)
func skipIfNotIntegration(t *testing.T) {
if os.Getenv("ABRA_INTEGRATION") == "" {
t.Skip("missing 'ABRA_INTEGRATION', not running integration tests")
}
}
+20
View File
@@ -0,0 +1,20 @@
package limit // https://github.com/tidwall/limiter
// Limiter is for limiting the number of concurrent operations. This
type Limiter struct{ sem chan struct{} }
// New returns a new Limiter. The limit param is the maximum number of
// concurrent operations.
func New(limit int) *Limiter {
return &Limiter{make(chan struct{}, limit)}
}
// Begin an operation.
func (l *Limiter) Begin() {
l.sem <- struct{}{}
}
// End the operation.
func (l *Limiter) End() {
<-l.sem
}
+338
View File
@@ -0,0 +1,338 @@
package lint
import (
"fmt"
"net/http"
"os"
"path"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
)
var Warn = "warn"
var Critical = "critical"
type LintFunction func(recipe.Recipe) (bool, error)
type LintRule struct {
Ref string
Level string
Description string
HowToResolve string
Function LintFunction
}
var LintRules = map[string][]LintRule{
"warn": {
{
Ref: "R001",
Level: "warn",
Description: "compose config has expected version",
HowToResolve: "ensure 'version: \"3.8\"' in compose configs",
Function: LintComposeVersion,
},
{
Ref: "R002",
Level: "warn",
Description: "healthcheck enabled for all services",
HowToResolve: "wire up healthchecks",
Function: LintHealthchecks,
},
{
Ref: "R003",
Level: "warn",
Description: "all images use a tag",
HowToResolve: "use a tag for all images",
Function: LintAllImagesTagged,
},
{
Ref: "R004",
Level: "warn",
Description: "no unstable tags",
HowToResolve: "tag all images with stable tags",
Function: LintNoUnstableTags,
},
{
Ref: "R005",
Level: "warn",
Description: "tags use semver-like format",
HowToResolve: "use semver-like tags",
Function: LintSemverLikeTags,
},
{
Ref: "R006",
Level: "warn",
Description: "has published catalogue version",
HowToResolve: "publish a recipe version to the catalogue",
Function: LintHasPublishedVersion,
},
{
Ref: "R007",
Level: "warn",
Description: "README.md metadata filled in",
HowToResolve: "fill out all the metadata",
Function: LintMetadataFilledIn,
},
{
Ref: "R013",
Level: "warn",
Description: "git.coopcloud.tech repo exists",
HowToResolve: "upload your recipe to git.coopcloud.tech/coop-cloud/...",
Function: LintHasRecipeRepo,
},
},
"error": {
{
Ref: "R008",
Level: "error",
Description: ".env.sample provided",
HowToResolve: "create an example .env.sample",
Function: LintEnvConfigPresent,
},
{
Ref: "R009",
Level: "error",
Description: "one service named 'app'",
HowToResolve: "name a servce 'app'",
Function: LintAppService,
},
{
Ref: "R010",
Level: "error",
Description: "traefik routing enabled",
HowToResolve: "include \"traefik.enable=true\" deploy label",
Function: LintTraefikEnabled,
},
{
Ref: "R011",
Level: "error",
Description: "all services have images",
HowToResolve: "ensure \"image: ...\" set on all services",
Function: LintImagePresent,
},
{
Ref: "R012",
Level: "error",
Description: "config version are vendored",
HowToResolve: "vendor config versions in an abra.sh",
Function: LintAbraShVendors,
},
},
}
func LintForErrors(recipe recipe.Recipe) error {
logrus.Debugf("linting for critical errors in %s configs", recipe.Name)
for level := range LintRules {
if level != "error" {
continue
}
for _, rule := range LintRules[level] {
ok, err := rule.Function(recipe)
if err != nil {
return err
}
if !ok {
return fmt.Errorf("lint error in %s configs: \"%s\" failed lint checks (%s)", recipe.Name, rule.Description, rule.Ref)
}
}
}
logrus.Debugf("linting successful, %s is well configured", recipe.Name)
return nil
}
func LintComposeVersion(recipe recipe.Recipe) (bool, error) {
if recipe.Config.Version == "3.8" {
return true, nil
}
return true, nil
}
func LintEnvConfigPresent(recipe recipe.Recipe) (bool, error) {
envSample := fmt.Sprintf("%s/%s/.env.sample", config.RECIPES_DIR, recipe.Name)
if _, err := os.Stat(envSample); !os.IsNotExist(err) {
return true, nil
}
return false, nil
}
func LintAppService(recipe recipe.Recipe) (bool, error) {
for _, service := range recipe.Config.Services {
if service.Name == "app" {
return true, nil
}
}
return false, nil
}
func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) {
for _, service := range recipe.Config.Services {
for label := range service.Deploy.Labels {
if label == "traefik.enable" {
if service.Deploy.Labels[label] == "true" {
return true, nil
}
}
}
}
return false, nil
}
func LintHealthchecks(recipe recipe.Recipe) (bool, error) {
for _, service := range recipe.Config.Services {
if service.HealthCheck == nil {
return false, nil
}
}
return true, nil
}
func LintAllImagesTagged(recipe recipe.Recipe) (bool, error) {
for _, service := range recipe.Config.Services {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return false, err
}
if reference.IsNameOnly(img) {
return false, nil
}
}
return true, nil
}
func LintNoUnstableTags(recipe recipe.Recipe) (bool, error) {
for _, service := range recipe.Config.Services {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return false, err
}
var tag string
switch img.(type) {
case reference.NamedTagged:
tag = img.(reference.NamedTagged).Tag()
case reference.Named:
return false, nil
}
if tag == "latest" {
return false, nil
}
}
return true, nil
}
func LintSemverLikeTags(recipe recipe.Recipe) (bool, error) {
for _, service := range recipe.Config.Services {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return false, err
}
var tag string
switch img.(type) {
case reference.NamedTagged:
tag = img.(reference.NamedTagged).Tag()
case reference.Named:
return false, nil
}
if !tagcmp.IsParsable(tag) {
return false, nil
}
}
return true, nil
}
func LintImagePresent(recipe recipe.Recipe) (bool, error) {
for _, service := range recipe.Config.Services {
if service.Image == "" {
return false, nil
}
}
return true, nil
}
func LintHasPublishedVersion(recipe recipe.Recipe) (bool, error) {
catl, err := recipePkg.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
versions, err := recipePkg.GetRecipeCatalogueVersions(recipe.Name, catl)
if err != nil {
logrus.Fatal(err)
}
if len(versions) == 0 {
return false, nil
}
return true, nil
}
func LintMetadataFilledIn(r recipe.Recipe) (bool, error) {
features, category, err := recipe.GetRecipeFeaturesAndCategory(r.Name)
if err != nil {
return false, err
}
if category == "" {
return false, nil
}
if features.Backups == "" ||
features.Email == "" ||
features.Healthcheck == "" ||
features.Image.Image == "" ||
features.SSO == "" {
return false, nil
}
return true, nil
}
func LintAbraShVendors(recipe recipe.Recipe) (bool, error) {
for _, service := range recipe.Config.Services {
if len(service.Configs) > 0 {
abraSh := path.Join(config.RECIPES_DIR, recipe.Name, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) {
return false, err
}
return false, err
}
}
}
return true, nil
}
func LintHasRecipeRepo(recipe recipe.Recipe) (bool, error) {
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, recipe.Name)
res, err := http.Get(url)
if err != nil {
return false, err
}
if res.StatusCode != 200 {
return false, err
}
return true, nil
}
+1072
View File
File diff suppressed because it is too large Load Diff

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