Compare commits

..

377 Commits

Author SHA1 Message Date
decentral1se d68444be9e Mark release 2021-03-17 12:54:29 +01:00
decentral1se f7bc8efabe Update to latest when upgrading 2021-03-17 12:53:02 +01:00
decentral1se f5284ba725 Point to installer for hacking 2021-03-17 12:50:02 +01:00
decentral1se 293d3ff558 Merge pull request 'Show git digest in abra version if we're running a development version' (#103) from digest-version into main
Reviewed-on: https://git.autonomic.zone/coop-cloud/abra/pulls/103
2021-03-17 12:48:45 +01:00
3wc c00319ab01 Update CHANGELOG 2021-03-16 12:55:47 +02:00
3wc 8b4141670c Include git digest in dev version output
Re: #100
2021-03-16 12:54:42 +02:00
decentral1se 23c852125d Listen to shellcheck and do things 2021-03-16 10:13:15 +01:00
decentral1se b4eae2e5e5 Add missing quote 2021-03-16 09:24:21 +01:00
decentral1se 9e953319cf Get label parsing done correctly 2021-03-16 09:23:13 +01:00
decentral1se 0814fa9146 Add export to the output also 2021-03-16 09:04:42 +01:00
decentral1se 0e1b6c858b Add change log entry 2021-03-16 08:58:12 +01:00
decentral1se 28618bd3ac Add packager helper script 2021-03-16 08:57:05 +01:00
decentral1se b04bfea1c7 Use local marker and scan services only once 2021-03-16 08:38:11 +01:00
decentral1se bc0ef0d6fc Fix deployments when versions are missing 2021-03-16 07:32:31 +01:00
decentral1se 16c91fedd1 Add newline back 2021-03-16 07:00:23 +01:00
decentral1se 9f5945094c Document new dev update command 2021-03-16 06:59:35 +01:00
decentral1se 76513a1f35 Add change log entry 2021-03-16 06:51:35 +01:00
decentral1se 86eb8d7fde Handle existing files use-case for dev installer
Closes https://git.autonomic.zone/coop-cloud/abra/issues/98.
2021-03-16 06:51:10 +01:00
decentral1se e31b3d3173 Add change log entry 2021-03-16 06:25:29 +01:00
decentral1se 58b13d7528 Use app type naming and use yellow 2021-03-16 06:22:32 +01:00
decentral1se b0fca49ecb Generalise top level deploy output 2021-03-16 06:19:52 +01:00
decentral1se 79dacf557e Implement edge-case handling for versioning 2021-03-16 06:19:37 +01:00
decentral1se d6caf03301 Spacing for readability 2021-03-16 05:58:08 +01:00
decentral1se 9b90712d28 Look up image name and show it also 2021-03-16 05:57:01 +01:00
decentral1se 1dd3fe6fcd Use COMPOSE var here 2021-03-16 05:56:52 +01:00
decentral1se 29953c17d9 Add change log entry 2021-03-16 05:48:08 +01:00
decentral1se 3b59adfe34 Vendor yq program 2021-03-16 05:47:13 +01:00
decentral1se 00c8a988e1 Fix parsing of stack_name/service concatenation 2021-03-16 05:04:05 +01:00
decentral1se 524fb6a44c Use STACK_NAME instead of Gitea 2021-03-15 18:56:26 +01:00
decentral1se e99bedf9e4 WIP version output summary which handles services
See https://git.autonomic.zone/coop-cloud/organising/issues/47.
2021-03-15 18:30:37 +01:00
decentral1se 0d98c442a2 Add change log entry 2021-03-15 17:04:14 +01:00
decentral1se bcc15ecdb0 Support dev upgrades on the CLI also 2021-03-15 17:03:01 +01:00
decentral1se a617629a7a Lowercase that 2021-03-15 16:54:54 +01:00
decentral1se f7ae400eb3 Use makefile for releasing installer 2021-03-15 16:54:33 +01:00
decentral1se 7141d364e1 Zomg lol fix my typo 2021-03-15 10:07:17 +01:00
decentral1se 057ce223f1 Fix link 2021-03-15 10:06:50 +01:00
3wc 7511b25e47 Update installation docs, release new installer
[ci skip]
2021-03-15 10:35:06 +02:00
3wc 62b447d61f Update changelog
[ci skip]
2021-03-14 14:40:57 +02:00
3wc 88d2a75575 Add --dev to installer, to grab git version 2021-03-14 14:39:11 +02:00
3wc 8cb6617a0f Automatically truncate suggested app name
Ref #83
2021-03-14 03:33:33 +02:00
3wc 1a649c56cb Docs & comments 2021-03-14 03:33:22 +02:00
3wc fd655274f8 Bomb out with Bash < 4
Ref #96
2021-03-14 03:24:13 +02:00
decentral1se 946d1a068d Drop current version handling logic for deploy
New logic coming soon.

See https://git.autonomic.zone/coop-cloud/organising/issues/47#issuecomment-4231.
2021-03-13 20:23:53 +01:00
3wc e8651976ca Only load apps once, exciting 2× speed increase 2021-03-13 20:15:54 +02:00
3wc af52ba1fec Clean-up 🧹 2021-03-12 13:13:17 +02:00
3wc 499c08c374 Update CHANGELOG 2021-03-12 13:10:50 +02:00
3wc 08281891a1 Add --type filter to abra <app> ls 2021-03-12 13:10:35 +02:00
decentral1se 5bce042922 First steps to enable abra-hetzner
See https://git.autonomic.zone/coop-cloud/abra/issues/88.
2021-03-10 23:28:59 +01:00
decentral1se 3276c9fe47 Always choose the default IPv4 address for init'ing
Closes https://git.autonomic.zone/coop-cloud/abra/issues/91.
2021-03-10 22:23:38 +01:00
d1admin 040374e781 List volumes/secrets when removing 2021-03-05 12:53:21 +01:00
d1admin 621c8cd5c4 Fix volume/secret deletion logic
- Fix escaping of quotes
- Dont delete things unless options are passed
2021-03-04 19:21:38 +01:00
d1admin 7434b67c34 Fix parens and ignore quote warning 2021-03-04 16:59:36 +01:00
d1admin 17306a753b Support volume and secret removal 2021-03-04 16:55:24 +01:00
d1admin 2e3f4cabd8 Warn if unable to find version 2021-03-04 16:40:35 +01:00
d1admin cf2308cdd7 Point to script 2021-03-04 16:31:18 +01:00
d1admin eec49d6dd1 Guard against length errors in app names
Closes https://git.autonomic.zone/coop-cloud/abra/issues/83.
2021-03-04 16:25:21 +01:00
d1admin d6195ad6d7 Undercore - values in the domain too 2021-03-04 16:19:55 +01:00
d1admin fd04c5a6e9 Support branch selection for app repo clones
Closes https://git.autonomic.zone/coop-cloud/abra/issues/80.
2021-03-04 16:01:56 +01:00
d1admin 1c9d7282b2 Revert "Sort commands listing"
This reverts commit 99ab5bf369.

Woops, they cannot be sorted this way. Ignore me.
2021-03-04 15:50:27 +01:00
d1admin dd9c485c66 Fix wording and wrap 2021-03-04 15:48:53 +01:00
d1admin 99ab5bf369 Sort commands listing 2021-03-04 15:38:09 +01:00
d1admin 25a0afed65 Fix indentation and, document local options only 2021-03-04 15:35:18 +01:00
d1admin 44e22db11b Add change log entry 2021-03-04 13:27:32 +01:00
d1admin 3321010089 Add change log README entry 2021-03-04 13:24:13 +01:00
d1admin e04c4626f2 Update change log 2021-03-04 13:22:57 +01:00
d1admin 65ce949e03 Bail out if versions match
Closes https://git.autonomic.zone/coop-cloud/abra/issues/87.
2021-03-04 13:20:58 +01:00
d1admin 5931cbd791 Only throw away error but keep stdout 2021-03-04 13:20:45 +01:00
d1admin 0bbff91722 Add service rollback
Closes https://git.autonomic.zone/coop-cloud/abra/issues/76.
2021-03-04 13:10:51 +01:00
d1admin 7f5e753dfd Re-factor version output logic and fix bug
It was showing empty strings for deployed versions it could not find.
Now, it will not change messages to output if it can not detect the
versions deployed (containers dont have the tag).
2021-03-04 13:10:00 +01:00
d1admin d3776f4424 Warn on secret storage after generation
Closes https://git.autonomic.zone/coop-cloud/abra/issues/75.
2021-03-03 17:20:24 +01:00
d1admin 544c4e86ba Capture output for version checking
Closes https://git.autonomic.zone/coop-cloud/abra/issues/85.
2021-03-03 16:56:47 +01:00
d1admin 516309b478 Show command to run 2021-03-03 16:54:01 +01:00
d1admin dfd7e29a30 Give spacing to blocks 2021-03-03 16:51:25 +01:00
d1admin bb30fa28da Fix wording 2021-03-03 16:50:59 +01:00
d1admin 044de5824b Drop whitespace 2021-03-03 16:49:58 +01:00
3wc 00cdce7bd2 Update CHANGELOG
[ci skip]
2021-03-02 20:34:35 +02:00
3wc f163d4b0fa Add script to auto-generate app catalogue 2021-03-02 20:33:14 +02:00
d1admin e0032fb74a Add change log entry 2021-03-02 13:07:00 +01:00
d1admin 152dfe9349 Support basic version checking
Closes https://git.autonomic.zone/coop-cloud/abra/issues/82.
2021-03-02 13:06:56 +01:00
d1admin 5a95ae97a0 Add changelog entry 2021-03-02 12:06:14 +01:00
d1admin 98e674b8e8 Add version and digest showing
See https://git.autonomic.zone/coop-cloud/abra/issues/82.
2021-03-02 12:04:25 +01:00
d1admin b655cf20be Make README less vague
Closes https://git.autonomic.zone/coop-cloud/abra/issues/79.
2021-03-02 11:53:55 +01:00
d1admin 5bc702bf96 Propagate new version around 2021-03-01 11:44:58 +01:00
d1admin 4bd842db66 Mark new release of abra 2021-03-01 11:41:44 +01:00
d1admin a8f7faddb9 Fix typo 2021-03-01 11:38:31 +01:00
3wc e5b2a426f0 Add shared backup helpers 2021-02-24 17:03:28 +02:00
3wc 29b22fe162 Display subcommand help with -h/--help
Closes #78
2021-02-11 15:02:37 +02:00
d1admin c082645da0 Remove test commit 2021-02-09 08:35:52 +01:00
decentral1se 156d5d8fba Merge pull request 'abra-commands.sh → abra.sh, make configs type-level' (#77) from move-configs into main
Reviewed-on: https://git.autonomic.zone/coop-cloud/abra/pulls/77
2021-02-09 08:31:45 +01:00
3wc d2cdb11fcc Tweak INFO messages, update CHANGELOG 2021-02-08 13:43:39 +02:00
3wc cef06a82a6 abra-commands.sh → abra.sh 2021-01-30 16:10:40 +02:00
3wc 9a630a0440 Prevent accidental cd when loading app vars 2021-01-30 16:10:40 +02:00
3wc 1c6651b18b Fix secret generate help
[ci skip]
2021-01-30 16:10:13 +02:00
3wc 5f7df4694f Test failed drone build 2021-01-29 14:02:26 +02:00
3wc 7feeab24ec Add RocketChat notifications for failed builds 2021-01-29 13:44:55 +02:00
3wc 1a6688cfbf Merge branch 'debug_logging' into main 2021-01-24 21:51:59 +02:00
3wc f90e1d154c Add container IDs to debug log 2021-01-24 19:11:22 +02:00
3wc 6cc265e931 Tweak info/debug output 2021-01-24 19:11:22 +02:00
3wc 854ae23f60 Initial --verbose / --debug 2021-01-24 19:11:22 +02:00
3wc 43e7672725 Prevent accidental cd when loading app vars 2021-01-24 19:11:11 +02:00
d1admin 4e913c426d Follow same style 2021-01-09 20:07:16 +01:00
3wc 8a08de51e4 Alert on missing secrets, re-add . → _ STACK_NAME 2021-01-09 15:07:39 +02:00
3wordchant 1c7a51bce1 Merge pull request 'App backup & restore' (#72) from backup_restore into main
Reviewed-on: https://git.autonomic.zone/coop-cloud/abra/pulls/72
2021-01-09 12:36:50 +01:00
3wc 5d84cef63c app restore 2021-01-09 13:36:32 +02:00
3wc eda5198904 Add scaffolding for per-app backup commands
See #70
2021-01-09 13:36:32 +02:00
3wc a4a3dccd66 Add global --skip-check / --skip-update commands 2021-01-09 13:36:32 +02:00
3wc a2d249e3a1 Fix abra app ls with * server 2021-01-02 00:34:44 +02:00
d1admin dc83baea12 Remove tab 2021-01-01 22:44:21 +01:00
3wc d6b4a4744f Fix abra app ls for app → type 2021-01-01 23:00:20 +02:00
d1admin 6ba2657dc1 Use short hash convention 2021-01-01 18:59:19 +01:00
d1admin ac6b805cbf Follow parens convention 2021-01-01 18:57:39 +01:00
decentral1se d4e52a9de3 Merge pull request 'Make secret generation more robust' (#73) from simplify-secret-logic into main
Reviewed-on: https://git.autonomic.zone/coop-cloud/abra/pulls/73
2021-01-01 18:55:47 +01:00
d1admin aa59c1ad43 Make secret generation more robust
Closes https://git.autonomic.zone/coop-cloud/abra/issues/68.
2021-01-01 18:54:41 +01:00
decentral1se 36f1d679ae Merge pull request 'Merging auto functionality into generate command' (#64) from app-auto-merge into main
Reviewed-on: https://git.autonomic.zone/coop-cloud/abra/pulls/64
2021-01-01 17:32:30 +01:00
d1admin f5c8ee1136 Merge auto/generate and use --secrets
Closes https://git.autonomic.zone/coop-cloud/abra/issues/58.
2021-01-01 17:31:21 +01:00
3wc 9a17817cc8 Tiny tweak to abra help advice 2021-01-01 17:57:36 +02:00
3wc b2e3292453 Add version subcommand to CHANGELOG 2021-01-01 17:55:45 +02:00
d1admin 13fafb5929 Fix typo and link to commit 2021-01-01 14:18:30 +01:00
3wc 114f99ae2e Update CHANGELOG 2021-01-01 15:11:04 +02:00
3wc fff4b10a41 Gettin some help_
See #50
2021-01-01 15:09:49 +02:00
d1admin ab1353603d Use homebrewed image for CI too 2021-01-01 13:49:19 +01:00
d1admin 1600b6277f Use the One True Way for testing
Closes https://git.autonomic.zone/coop-cloud/abra/issues/71.
2021-01-01 13:48:14 +01:00
3wordchant b79e35f982 Merge pull request 'Add per-subcommand help using abra help <subcommand>' (#61) from command_help_2 into main
Reviewed-on: https://git.autonomic.zone/coop-cloud/abra/pulls/61
2021-01-01 13:13:29 +01:00
3wordchant 703dbe0a0f Merge branch 'main' into command_help_2 2021-01-01 13:09:57 +01:00
d1admin 7abb0191e1 Use upstream CI image
See https://git.autonomic.zone/coop-cloud/docker-dind-bats-kcov.
2021-01-01 12:26:50 +01:00
3wc 886ae5b7f2 Fix help function names 2021-01-01 01:46:09 +02:00
3wc 5411c85793 Don't require weird app_ls format for abra help 2021-01-01 01:42:17 +02:00
3wc caa315e361 Add per-subcommand help 2021-01-01 01:13:17 +02:00
3wc e6b24fe65c Change -v option to version subcommand 2021-01-01 01:10:03 +02:00
3wc 81782bb5f0 make test → test_local, test_docker, test_dind
test_local: run `bats tests/`. Requires `bats`.

test_docker: run docker and install bats in it.

Both of these options require that your local Docker daemon is running
in swarm mode.

test_dind: starts a Docker daemon and runs tests in that. Requires sudo
2020-12-31 22:10:52 +02:00
d1admin 406b9e374e Only output length if using it
Closes https://git.autonomic.zone/coop-cloud/abra/issues/67.
2020-12-31 18:16:01 +01:00
d1admin ce0e0e893c Use plain usage only when erroring out
Closes https://git.autonomic.zone/coop-cloud/abra/issues/65.
2020-12-31 18:12:01 +01:00
d1admin fac45f276e Add entry 2020-12-31 18:11:22 +01:00
d1admin 44d3ac3a1c Support pwqgen/pwgen checking
Closes https://git.autonomic.zone/coop-cloud/abra/issues/66.
2020-12-31 18:10:13 +01:00
d1admin 5da9f26076 Remove old function
Follow on from 3936d6afc0.
2020-12-31 18:06:20 +01:00
d1admin 4e99cf1ded Add log entry 2020-12-31 16:57:10 +01:00
d1admin 55324524ca Don't cut since export ... isn't in the env file
Closes https://git.autonomic.zone/coop-cloud/abra/issues/69.
2020-12-31 16:55:33 +01:00
d1admin b6928959cb Fix test target path 2020-12-31 13:50:10 +01:00
3wc 8ddb290683 Further update to tests for #47 2020-12-31 14:34:52 +02:00
d1admin 2cb1134a54 Use _ now 2020-12-31 13:26:25 +01:00
d1admin c4b1ac482e Update change log 2020-12-31 13:22:26 +01:00
3wc 29cc392dff Prompt on app .. config if $EDITOR is un-set
Closes #41
2020-12-31 12:52:44 +02:00
3wc 8839bd4595 Fix server bash completion
Ref #45
2020-12-31 11:47:14 +02:00
3wc 0179f600f5 Change -v option to version subcommand 2020-12-31 11:47:12 +02:00
decentral1se 15f0233351 Merge pull request '<domain> → <app>, APP → TYPE' (#60) from domain_to_app into main
Reviewed-on: https://git.autonomic.zone/coop-cloud/abra/pulls/60
2020-12-30 22:22:11 +01:00
3wc bbaacb7b9f Update tests for app-name 2020-12-30 21:56:52 +02:00
3wc 6761574d74 Add "app name" concept, and rename APP → TYPE
Ref #47
2020-12-30 21:26:17 +02:00
3wc 8384af8b95 First foray into <domain> → <app> 2020-12-30 13:47:41 +02:00
3wc b9e97688d6 ... and update docopt 2020-12-30 13:43:17 +02:00
3wc 1055805c8d Merge duplicate run commands
Closes #57
2020-12-30 13:33:57 +02:00
decentral1se 678906cb39 Merge pull request 'Use set -a/+a and docker env file formats' (#55) from use-set-a into main
Reviewed-on: https://git.autonomic.zone/coop-cloud/abra/pulls/55
2020-12-30 11:23:30 +01:00
d1admin 11c50ae98d Use set -a/+a and docker env file formats
Closes https://git.autonomic.zone/coop-cloud/abra/issues/40.
2020-12-30 11:22:58 +01:00
decentral1se e911ab246b Merge pull request 'Merge logs/multilogs and avoid multitail external' (#56) from merge-logging into main
Reviewed-on: https://git.autonomic.zone/coop-cloud/abra/pulls/56
2020-12-30 11:21:46 +01:00
decentral1se 34775f306b Merge branch 'main' into merge-logging 2020-12-30 11:21:03 +01:00
d1admin 375a4dd29d Re-add require_app_latest for custom commands
Revision of 67cce192df.

See https://git.autonomic.zone/coop-cloud/abra/pulls/54#issuecomment-2300.
2020-12-30 11:19:55 +01:00
d1admin 2b951e9f54 Mark minor 2020-12-30 11:18:32 +01:00
3wc 01184c313a Add missing CHANGELOG entries
Closes #46
2020-12-30 00:43:52 +02:00
d1admin 3936d6afc0 Merge logs/multilogs and avoid multitail external 2020-12-29 23:22:46 +01:00
d1admin 407744827f Add change log entry for #42
See https://git.autonomic.zone/coop-cloud/abra/issues/42.
2020-12-29 17:11:30 +01:00
decentral1se b634b4c668 Merge pull request 'Make sure to git pull latest changes on ~/.abra/apps side' (#54) from latest-checks into main
Reviewed-on: https://git.autonomic.zone/coop-cloud/abra/pulls/54
2020-12-29 17:10:16 +01:00
d1admin 67cce192df Don't do more cloning that necessary 2020-12-29 17:06:49 +01:00
d1admin 3a9e141b24 Pull latest changes
Closes https://git.autonomic.zone/coop-cloud/abra/issues/42.
2020-12-29 17:06:32 +01:00
d1admin ebfe7ca4e8 Suppress output of clone and do better logging 2020-12-29 17:05:30 +01:00
d1admin fff2fbe819 Prepare function name for new functionality 2020-12-29 17:05:00 +01:00
d1admin f213c3df5f Follow convention and show type of message 2020-12-29 17:04:17 +01:00
d1admin 9b1be33018 Mark as quote and not as entry 2020-12-29 15:11:15 +01:00
d1admin 6ecf4f287a Add missing ) 2020-12-29 15:10:57 +01:00
d1admin e1d6ff8b73 Add docs link 2020-12-29 15:10:05 +01:00
d1admin 07d4815a74 Start 0.5.0 change log 2020-12-29 15:08:42 +01:00
decentral1se 33315f6b43 Merge pull request '<app> -> <type>' (#53) from app-goes-to-type into main
Reviewed-on: https://git.autonomic.zone/coop-cloud/abra/pulls/53
2020-12-29 15:03:43 +01:00
d1admin f017324431 <app> -> <type>
Closes https://git.autonomic.zone/coop-cloud/abra/issues/48.
2020-12-29 14:56:50 +01:00
decentral1se 4339c91cf3 Merge pull request 'Remove abra server use' (#52) from server-use-remove into main
Reviewed-on: https://git.autonomic.zone/coop-cloud/abra/pulls/52
2020-12-29 14:26:39 +01:00
d1admin 592f515ec4 Remove abra server use 2020-12-29 14:24:50 +01:00
3wc 77ba5652b2 Run "check" during "deploy", + "--skip-check" 2020-12-29 11:15:14 +02:00
3wc fdf6334ed3 Use temporary ABRA_DIr in tests 2020-12-27 21:53:44 +02:00
3wc 73e5e64b9a Bump version 2020-12-27 21:45:37 +02:00
d1admin 3f9b4477cd Swap args around 2020-12-27 13:11:22 +01:00
3wc 412729aac9 Fix calling logs with no arguments
Closes #31
2020-12-27 12:23:16 +02:00
3wc 8022a2cb41 Add cheeky network command 2020-12-27 12:07:10 +02:00
3wc 35182ed260 Get both .. volume .. and custom commands working 2020-12-27 12:07:10 +02:00
3wc d90c6ef361 Add --no-tty option to app run 2020-12-27 12:07:10 +02:00
3wc 40ca8dfe93 Add --server filter to app list..
.. and add `server <host> apps` as an alias.
2020-12-27 12:07:10 +02:00
3wc 7f009f927b Fix app ls --status
Closes #35
2020-12-27 12:06:39 +02:00
3wc e222f4152b Pin kcov version 2020-12-24 01:07:01 +02:00
3wc 893150cdd9 Pin shellcheck version 2020-12-24 00:55:57 +02:00
3wc 61126f16e1 Update installer 2020-12-24 00:50:50 +02:00
3wc 63c982c550 Fix custom commands loading 2020-12-17 13:38:30 +02:00
3wc 0d202eedfc Reënable custom per-stack commands 2020-12-14 12:33:22 +02:00
3wc cb2d563ea0 Fix status header 2020-11-18 01:06:07 +02:00
3wc 059968a8cf Fix abra app ls --status 2020-11-18 00:40:26 +02:00
3wc c57069e0af Use ABRA_DIR instead of hard-coding .abra 2020-11-18 00:39:41 +02:00
3wc 946a527baa Add app ... ps command, and some comments 2020-11-16 17:29:35 +02:00
d1admin 89d530a553 Use printf instead of echo like docker docs do 2020-11-08 16:40:45 +01:00
d1admin 25fe6808ad Use one line and strip newlines on generation 2020-11-08 16:34:26 +01:00
d1admin 28b2d8ecc7 Strip whitespace 2020-11-08 16:34:21 +01:00
3wc fd735a1310 Add app <domain> check to sniff out missing vars
Re #36
2020-11-06 23:14:26 +02:00
3wc e307286db7 Add --status option to app ls
Closes #35
2020-11-06 22:35:48 +02:00
d1admin dd570e224b Ignore those build failures 2020-11-05 16:02:21 +01:00
d1admin f495ec0d94 Appease shellcheck 2020-11-05 16:00:50 +01:00
decentral1se b75bce531b Merge pull request 'PASSWORD/KEY distinction+match for secret generation' (#33) from new-pass-keys-generation into main
Reviewed-on: https://git.autonomic.zone/coop-cloud/abra/pulls/33
2020-11-05 15:57:39 +01:00
decentral1se 0d62f01d5a Merge branch 'main' into new-pass-keys-generation 2020-11-05 15:57:32 +01:00
d1admin eec55896a4 Implement password/key handling 2020-11-05 15:56:31 +01:00
d1admin c2a56c6c09 Drop this broken catch-all
Closes https://git.autonomic.zone/coop-cloud/abra/issues/29.
2020-11-01 21:15:06 +01:00
d1admin 74dfd75fb1 Use same [] style once more 2020-11-01 20:55:47 +01:00
d1admin e8c0efa91e Add --auto to app new for generation
Step one of https://git.autonomic.zone/coop-cloud/abra/issues/32.
2020-11-01 20:54:43 +01:00
d1admin 6bae48431c Use same style as other [] options 2020-11-01 20:41:58 +01:00
3wc 543072ab37 Add zsh completion, remove compat hacks from bash
Ref #5
2020-11-01 21:09:33 +02:00
d1admin 57e3a34133 Make sure to clone on deploy as well 2020-11-01 17:08:44 +01:00
d1admin 33a49a8457 Trim whitespace 2020-11-01 17:05:13 +01:00
d1admin 5806e40c1c Add require_app_clone 2020-11-01 17:05:10 +01:00
3wc 0d24a8e5cc Tryna fix Drone CI 2020-11-01 16:52:36 +02:00
3wc e01f06423e Whoops! (Actual compopt fix
Ref #5
2020-11-01 15:22:45 +02:00
3wc 80f06ba0e1 Support >2 COMPOSE_FILEs 2020-10-31 17:51:56 +02:00
3wc f8306b282d Split tests into separate files, add secret tests 2020-10-31 17:06:54 +02:00
3wc 2bdfe8baa8 Handle missing compopt
Ref #5
2020-10-30 21:40:59 +02:00
3wc b226396764 Consistent y/n confirmation prompts 2020-10-30 20:57:13 +02:00
3wc a3cd6d2281 Add git and bash to Drone test env 2020-10-30 18:32:01 +02:00
3wc 50651aeea1 Mollify shellcheck 2020-10-30 18:30:53 +02:00
3wc 57e24eaf0a Always run tests on local Docker 2020-10-30 18:30:43 +02:00
3wc 50ca4f8029 Tighten up options for secret commands 2020-10-30 18:24:22 +02:00
3wc 0433da3689 Fix subcommand -> function mapper 2020-10-30 18:17:06 +02:00
3wc 3e0b9e9475 Working completion app and server 2020-10-30 17:52:47 +02:00
3wc c6b841de6c First stab 2020-10-30 17:52:47 +02:00
3wc 2daffc8694 Use bash select, tweak args 2020-10-30 17:52:46 +02:00
d1admin b15a214049 WIP: app secret rm 2020-10-30 02:09:59 +01:00
d1admin 8d7194fcce Add quotes 2020-10-30 00:39:24 +01:00
d1admin 188dc56dd1 Also run via sh 2020-10-30 00:34:37 +01:00
d1admin dbfe6f8097 Add initial CLI parsing for secret deletion 2020-10-30 00:32:03 +01:00
d1admin 26b994ab84 Add --pass for new apps command
See https://git.autonomic.zone/coop-cloud/abra/issues/25.
2020-10-30 00:04:53 +01:00
3wc dba3c391bd Handle missing tput gracefully 2020-10-29 19:40:19 +02:00
3wc 5a72ed0cfb Make config more chill, small gardening 2020-10-29 19:39:51 +02:00
3wc b5d84d5e0d Test for git, container, and test app new ... 2020-10-29 19:38:42 +02:00
3wc e3983c2440 Run bats in dind container, add git & bash 2020-10-29 19:38:15 +02:00
3wc 85b8a4f459 Add config command 2020-10-29 19:35:59 +02:00
3wc 6e38dc35e5 Fix run subcommand 2020-10-29 19:35:57 +02:00
d1admin 841e4fc61a Explode if can't clone the app 2020-10-28 18:24:28 +01:00
d1admin c1d12eacc5 Fix typo 2020-10-28 18:24:15 +01:00
d1admin 3b730d314b Use basename when listing servers for abra new 2020-10-28 18:05:53 +01:00
d1admin 5df3a9fffb Account for 0 or 1 servers 2020-10-28 17:33:48 +01:00
d1admin 3e3fe0e349 Create under servers directory 2020-10-28 17:32:38 +01:00
d1admin db766f4aec Don't explode if you already have the context 2020-10-28 17:32:30 +01:00
3wc 8909a46d8c Add --force option to app .. delete 2020-10-28 01:52:27 +02:00
3wc 8d139d4d28 Add undeploy/delete commands
Ref #4
2020-10-28 01:40:35 +02:00
3wc b6b80298c2 Add .. secret auto command 2020-10-28 00:29:40 +02:00
3wc b1e8ac4498 Local server list, more tidying 2020-10-27 23:46:03 +02:00
3wc c5785089d6 Tidy up CLI commands, add app list 2020-10-27 21:38:20 +02:00
3wc bec3272a41 .abra dir, abra new
Closes #24

Ref #21
2020-10-27 17:58:34 +02:00
d1admin dc3b772b72 Use server naming 2020-10-26 18:40:04 +01:00
d1admin 07d4f8cbdf Remove abra.yml / yq stuff for now 2020-10-26 18:36:28 +01:00
d1admin 37c59a53ef Drop interactive flags 2020-10-26 18:31:03 +01:00
d1admin 87a54594f6 Fix typo, clean up after 2020-10-26 18:25:40 +01:00
d1admin e2e76edaaf Remove old test file 2020-10-26 18:19:41 +01:00
d1admin ab7772e8f7 Shuffle that 2020-10-26 18:16:54 +01:00
d1admin 7304612f5f Use actual plugin 2020-10-26 18:14:41 +01:00
d1admin 5a07f08ab3 Use new syntax 2020-10-26 18:13:23 +01:00
d1admin 1e158cce55 Use cwd for coverage 2020-10-26 18:09:39 +01:00
d1admin 2102193df1 Fix syntax 2020-10-26 18:05:27 +01:00
d1admin 1b4258f1ea Add shellcheck target, drop default 2020-10-26 18:04:14 +01:00
d1admin cc060b8546 Fix error code 2020-10-26 18:02:24 +01:00
d1admin dce46603bf Add codecov target too 2020-10-26 18:01:12 +01:00
d1admin 64d04a29ab Run full kcov 2020-10-26 18:01:00 +01:00
d1admin db7eb30447 Drop curl 2020-10-26 17:50:06 +01:00
d1admin f648b251bf Remove yq target and newline test target 2020-10-26 17:49:21 +01:00
d1admin 86a790bbe4 Make tests run locally isolated 2020-10-26 17:48:19 +01:00
d1admin b423d61fce Remove install targets 2020-10-26 17:48:12 +01:00
d1admin f03c509552 Use shellcheck image 2020-10-26 17:40:02 +01:00
d1admin e6458e5e60 Update parser once more 2020-10-26 17:30:37 +01:00
d1admin 9580199616 Remove extra secrets help 2020-10-26 17:19:32 +01:00
3wc b59e902d18 Fix logs, cp, `multilogs
Ref #18
2020-10-26 16:55:43 +02:00
3wc c50b1d8760 Goodbye, parse_subcommand! 👋
Ref #18
2020-10-26 16:08:15 +02:00
3wc b49b510c43 Load custom commands in a function 2020-10-26 13:46:54 +02:00
3wc b68bfdfb43 Regnerate Docopt 2020-10-26 13:35:58 +02:00
3wc 983e4af08c Use docopts variables
Ref #18
2020-10-26 13:34:49 +02:00
3wc 6cf7cf843a Update server subcommands 2020-10-26 13:34:30 +02:00
3wc d37d8a0c66 Add default --tail option to abra logs 2020-10-26 13:33:56 +02:00
d1admin da33064a5f Add codecov badge 2020-10-26 11:49:06 +01:00
d1admin be2fffd858 Run plain bin 2020-10-26 11:40:41 +01:00
d1admin 38f0c92bc7 Try avoid error exit code 2020-10-26 11:38:32 +01:00
d1admin c1fbb75657 Wait a sec, collect, not just report 2020-10-26 11:37:13 +01:00
d1admin 33fd0eeb8d Point codecov at report folder 2020-10-26 11:36:12 +01:00
d1admin 5955505752 Get reports only 2020-10-26 11:36:04 +01:00
d1admin e022fe2310 Use kcov image 2020-10-26 11:28:19 +01:00
d1admin 02fa9025dc Install kcov directly now 2020-10-26 11:26:28 +01:00
d1admin 2250713c05 Split up codecov steps 2020-10-26 11:25:55 +01:00
d1admin 2b77cfebf9 Disable failing test for now 2020-10-26 11:25:39 +01:00
d1admin aff01e6741 Install kcov holding package 2020-10-26 11:24:05 +01:00
d1admin 0eb5d14ad8 Drop unit tests temporarily 2020-10-26 11:21:58 +01:00
d1admin 6d6208a63c Add code coverage CI 2020-10-26 11:20:17 +01:00
d1admin 1e059ffe7f Add coverage target 2020-10-26 11:20:09 +01:00
d1admin eb12f2392c Ignore coverage folder 2020-10-26 11:19:38 +01:00
d1admin a13e58c6c0 Finally work out subcommands 2020-10-26 10:05:03 +01:00
d1admin 44c41830a7 Finalise docopt parser integration 2020-10-26 09:46:14 +01:00
d1admin ae6c2c26ae A first stab at docopt-sh integration 2020-10-25 21:41:17 +01:00
d1admin dad72c820a Re-jig names and add test target in Makefile 2020-10-25 21:31:41 +01:00
d1admin eec9a8ba1a Add link to site 2020-10-25 21:22:29 +01:00
d1admin bf44270b3d Make CI zippier with alpine 2020-10-25 21:19:14 +01:00
d1admin 4bf1dbd7eb Spacing and naming 2020-10-25 21:17:01 +01:00
d1admin 4ca1026c2c Don't test against dir that doesnt exist 2020-10-25 21:16:42 +01:00
d1admin 891b2cc6c9 Add place holders for change log 2020-10-25 21:13:33 +01:00
d1admin 4de7f24d8e Start moving installer scripts over 2020-10-25 21:08:08 +01:00
d1admin ad1063a0cc Trim down README for now 2020-10-25 21:08:03 +01:00
3wc 4cfe143326 Install docker using convenience script 2020-10-23 05:13:06 +02:00
3wc 16cc5d9cf7 Fix tests 2020-10-23 05:04:35 +02:00
3wc 9d22797dc8 Attempt to test using bats / drone 2020-10-23 05:03:01 +02:00
3wc 16a09887e6 Rename abra context to abra server 2020-10-23 05:02:39 +02:00
3wc b7757b51b1 Fix abra run if there are 2 containers up 2020-10-23 03:58:55 +02:00
3wordchant 3c7c7694bf Merge pull request 'Adapt --help output to Click-like format' (#20) from new-help into main
Reviewed-on: https://git.autonomic.zone/autonomic-cooperative/abra/pulls/20
2020-10-23 03:56:50 +02:00
d1admin f15dfd9f5f First stab at new help 2020-10-22 21:16:30 +02:00
3wc 66dcaedfd0 Add stack and volume as shortcuts to docker 2020-10-06 20:27:50 +02:00
3wc 6598aabc37 Work even if a local path contains $SERVICE 2020-10-01 01:08:29 +02:00
3wc 6759e6a175 Show multiple COMPOSE_FILEs better
Fixes #16
2020-09-29 00:17:26 +02:00
3wc 8735362580 Load custom commands from $ABRA_STACK_DIR too 2020-09-28 15:02:18 +02:00
3wc 7223dca951 Re-enable running commands as another user
Fixes #14
2020-09-27 23:45:39 +02:00
3wc dd9444b036 Add context init command to set up remote swarm 2020-09-27 13:26:27 +02:00
3wc 81e24b6f72 Remove debugging from secret_generate 2020-09-27 13:26:15 +02:00
d1admin 16292df5f6 Fix installer and mark new patch 2020-09-27 08:20:36 +02:00
d1admin 92c91ddbb0 Add log entry 2020-09-27 08:17:00 +02:00
d1admin cff9b13f60 Remove quote 2020-09-27 08:04:22 +02:00
d1admin 0444991636 Fix ticks 2020-09-27 08:04:03 +02:00
d1admin 28ba33b18e Fix indentation 2020-09-27 08:03:39 +02:00
3wc 77eb83b128 Add credit to sub_multilogs 2020-09-25 00:48:46 +02:00
3wc ff7fcf2201 Combined logging
Closes #8
2020-09-25 00:36:23 +02:00
3wc b0d525a980 Tweak usage text 2020-09-24 21:19:40 +02:00
3wc aa1ffd5d8a Add success() method & secret insert subcommand 2020-09-24 21:17:08 +02:00
3wc 5627e67bf7 Bail if we can't load the specified $ABRA_ENV 2020-09-24 14:47:00 +02:00
3wc 29343369f3 Further tidy-up (+ add warning()) 2020-09-24 14:47:00 +02:00
3wc 427ed97678 Update README and tweak default STACK_DIR 2020-09-24 14:47:00 +02:00
3wc b01fee3c86 Add -e, -c and ABRA_STACK_DIR options..
..and tidy up a little
2020-09-24 14:46:57 +02:00
d1admin 949246821f Clarify versioning 2020-09-24 09:19:58 +02:00
d1admin 60f2892acd Fix CI 2020-09-24 09:18:31 +02:00
d1admin 0268685cfa Declare minor bump 2020-09-24 09:09:46 +02:00
d1admin dd4f31d9a1 Switch over to scripts.d 2020-09-24 09:03:25 +02:00
d1admin 8a19bb059c Clarify repo 2020-09-22 15:00:23 +02:00
d1admin 3a1f4e7bf6 Bump to next version 2020-09-22 14:18:41 +02:00
d1admin a065f5f2a6 Rough and ready upgrading 2020-09-22 14:17:12 +02:00
d1admin 3d47cf97c0 Update notes about installer and tags 2020-09-22 14:02:59 +02:00
d1admin e052aa2b27 Add note about installer scripts repo 2020-09-22 13:58:17 +02:00
d1admin 9660f32b84 Check also the installer script 2020-09-22 13:55:44 +02:00
d1admin 32cef2af68 Add interactive flags for future 2020-09-22 13:55:26 +02:00
d1admin e2e1e07803 Add installer script (first stab) 2020-09-22 13:37:33 +02:00
d1admin 15bdd11599 Jiggle README and add CHANGELOG 2020-09-22 13:05:23 +02:00
3wc f4798bf3bd Context set-up, Subsubcommands, COMPOSE->{_FILE} 2020-09-22 00:08:13 +02:00
3wc efd43752d6 Add mysqldump example 2020-09-22 00:05:28 +02:00
3wc 46de298b7f Appease shellcheck 2020-09-19 12:02:55 +02:00
3wc 5a646abb12 Respect COMPOSE var to make optional services easier 2020-09-18 23:42:01 +02:00
3wc 4658e3484c Add abra cp command to push & pull files 2020-09-14 19:16:34 +02:00
d1admin f0a17bfd87 Add yq key checking for abra.yml 2020-09-14 00:16:46 +02:00
d1admin 6fdb0d64af Un-bork quoting to allow splitting
Closes https://git.autonomic.zone/autonomic-cooperative/abra/issues/12.
2020-09-13 23:36:35 +02:00
d1admin 67dafbb274 Remove bad quoting 2020-09-13 23:33:55 +02:00
3wc dfe588d7f8 Don't try and show $DOMAIN if undefined
Fixes #11
2020-09-13 17:43:12 +02:00
d1admin 09bd22d320 Re-force the symlink 2020-09-13 09:32:39 +02:00
d1admin c7e5e3cc98 Appease shellcheck 2020-09-13 09:31:43 +02:00
3wc 76986927eb Allow passing arguments to logs 2020-09-12 14:03:11 +02:00
3wc b67d45e14d Load per-stack commands from abra-commands.sh..
.. and add a new `abra run_args` command to allow adding arguments to
`docker exec`.
2020-09-11 18:04:47 +02:00
3wc 8da3e7dfca Move -a processing, another shellcheck fix 2020-09-11 16:04:48 +02:00
3wc 69e00bafd6 Fix shellcheck error? 2020-09-11 13:07:01 +02:00
3wc ea79ad809a Allow overriding the STACK_NAME..
..to be able to run e.g. `abra -a traefik logs traefik`
2020-09-11 12:58:59 +02:00
3wc 348e4ca255 Error if .envrc is blocked
Closes #6
2020-09-11 12:58:11 +02:00
3wc ebb763518a deploy status, install globally, rename run 2020-09-09 01:55:03 +02:00
3wc 86785b6aea Custom command to generate passwords 2020-09-08 20:10:37 +02:00
3wc 73768b8014 Add install instructions 2020-09-08 20:10:26 +02:00
d1admin df9dd65d46 Add missing PHONY def 2020-09-08 09:21:13 +02:00
d1admin 91395a5f8e Add stuff for fooling around with yq 2020-09-08 09:19:03 +02:00
d1admin 9c89834d20 Add attempt at logs command 2020-09-08 09:10:37 +02:00
d1admin c29ce5cdda Appease shellcheck 2020-09-08 08:58:49 +02:00
d1admin 0f6a6018bd Add installation of shellcheck 2020-09-08 08:55:57 +02:00
d1admin 0be39305b9 Fix target branch 2020-09-08 08:54:54 +02:00
d1admin 5b09ebe6c1 Add CI badge 2020-09-08 08:54:19 +02:00
d1admin 4ba071a2ca Formatter says use # now 2020-09-08 08:54:07 +02:00
d1admin 1682f6a0dd Add WIP Drone config 2020-09-08 08:53:03 +02:00
d1admin 59e54504fe Format out some whitespaces 2020-09-08 08:49:27 +02:00
d1admin ca25739a69 Add PHONY marker for re-running targets 2020-09-08 08:49:08 +02:00
d1admin e3109575da Drop some whitespace 2020-09-08 08:49:02 +02:00
d1admin 55b5ac0e4c Add default target 2020-09-08 08:48:53 +02:00
d1admin 64a7ead6d5 Help Gitea to render markdown 2020-09-08 08:47:52 +02:00
d1admin b0ec8049a2 Add missing = 2020-09-08 08:47:44 +02:00
3wc a8a83f9e06 Initial import 2020-09-07 23:29:29 +02:00
116 changed files with 2472 additions and 12573 deletions
-40
View File
@@ -1,40 +0,0 @@
{{ range .Versions }}
<a name="{{ .Tag.Name }}"></a>
## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }}
> {{ datetime "2006-01-02" .Tag.Date }}
{{ range .CommitGroups -}}
### {{ .Title }}
{{ range .Commits -}}
* {{ .Subject }}
{{ end }}
{{ end -}}
{{- if .RevertCommits -}}
### Reverts
{{ range .RevertCommits -}}
* {{ .Revert.Header }}
{{ end }}
{{ end -}}
{{- if .MergeCommits -}}
### Pull Requests
{{ range .MergeCommits -}}
* {{ .Header }}
{{ end }}
{{ end -}}
{{- if .NoteGroups -}}
{{ range .NoteGroups -}}
### {{ .Title }}
{{ range .Notes }}
{{ .Body }}
{{ end }}
{{ end -}}
{{ end -}}
{{ end -}}
-27
View File
@@ -1,27 +0,0 @@
style: github
template: CHANGELOG.tpl.md
info:
title: CHANGELOG
repository_url: https://git.autonomic.zone:2222/coop-cloud/go-abra
options:
commits:
# filters:
# Type:
# - feat
# - fix
# - perf
# - refactor
commit_groups:
# title_maps:
# feat: Features
# fix: Bug Fixes
# perf: Performance Improvements
# refactor: Code Refactoring
header:
pattern: "^(\\w*)\\:\\s(.*)$"
pattern_maps:
- Type
- Subject
notes:
keywords:
- BREAKING CHANGE
+29 -60
View File
@@ -1,74 +1,43 @@
---
kind: pipeline
name: coopcloud.tech/abra
name: linters
steps:
- name: make check
image: golang:1.17
- name: run shellcheck
image: koalaman/shellcheck-alpine:v0.7.1
commands:
- make check
- shellcheck abra
- name: make static
image: golang:1.17
ignore: true # until we decide we all want this check
environment:
STATIC_CHECK_URL: honnef.co/go/tools/cmd/staticcheck
STATIC_CHECK_VERSION: v0.2.0
- name: run unit tests
image: decentral1se/docker-dind-bats-kcov
commands:
- go install $STATIC_CHECK_URL@$STATIC_CHECK_VERSION
- make static
- bats tests
- name: make build
image: golang:1.17
- name: collect code coverage
failure: ignore # until we fix this
image: decentral1se/docker-dind-bats-kcov
commands:
- make build
- kcov . bats tests || true
- name: make test
image: golang:1.17
commands:
- make test
- name: notify on failure
image: plugins/matrix
- name: send code coverage report to codecov
failure: ignore # until we fix this
image: plugins/codecov
settings:
homeserver: https://matrix.autonomic.zone
roomid: "IFazIpLtxiScqbHqoa:autonomic.zone"
userid: "@autono-bot:autonomic.zone"
accesstoken:
from_secret: autono_bot_access_token
depends_on:
- make check
- make build
- make test
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}}"
when:
status:
- failure
- name: fetch
image: docker:git
commands:
- git fetch --tags
depends_on:
- make check
- make build
- make test
when:
event: tag
- name: release
image: golang:1.17
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: {}
trigger:
branch:
- main
-6
View File
@@ -1,6 +0,0 @@
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
@@ -1,8 +0,0 @@
---
name: "Do not use this issue tracker"
about: "Do not use this issue tracker"
title: "Do not use this issue tracker"
labels: []
---
Please report your issue on [`coop-cloud/organising`](https://git.coopcloud.tech/coop-cloud/organising)
+2 -5
View File
@@ -1,5 +1,2 @@
abra
.vscode/
vendor/
.envrc
dist/
coverage/
/.venv
-40
View File
@@ -1,40 +0,0 @@
---
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
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
dir: cmd/abra
goos:
- linux
- darwin
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:"
- "^chore:"
- "^docs:"
- "^refactor:"
- "^style:"
- "^test:"
- "^tests:"
+82
View File
@@ -0,0 +1,82 @@
# 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
-38
View File
@@ -1,38 +0,0 @@
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: run test install build clean format check static
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)
static:
@staticcheck $(ABRA)
test:
@go test ./... -cover
loc:
@find . -name "*.go" | xargs wc -l
+36 -83
View File
@@ -1,104 +1,57 @@
# abra
> https://coopcloud.tech
[![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 🎩🐇
`abra` is a command-line tool for managing your own [Co-op Cloud](https://coopcloud.tech). It can provision new servers, create applications, deploy them, run backup and restore operations and a whole lot of other things. It is the go-to tool for day-to-day operations when managing a Co-op Cloud instance.
`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.
## Change log
See [CHANGELOG.md](./CHANGELOG.md).
## Documentation
> [docs.cloud.autonomic.zone](https://docs.cloud.autonomic.zone/)
## Install
### Arch-based Linux Distros
[abra (coming-soon)](https://aur.archlinux.org/packages/abra/) or for the latest version on git [abra-git](https://aur.archlinux.org/packages/abra-git/)
Install the latest stable release:
```sh
yay -S abra-git # or abra
curl https://install.abra.autonomic.zone | bash
```
### Debian-based Linux Distros
**Coming Soon**
### Homebrew
**Coming Soon**
### Build from source
or the bleeding-edge development version:
```sh
git clone https://git.coopcloud.tech/coop-cloud/abra
cd abra
go env -w GOPRIVATE=coopcloud.tech
make install
curl https://install.abra.autonomic.zone | bash -s -- --dev
```
The abra binary will be in `$GOPATH/bin`.
The source for this script is [here](./installer/installer).
## Autocompletion
## Update
**bash**
Run `abra upgrade` to automatically download and install the latest release
version.
Copy `scripts/autocomplete/bash` into `/etc/bash_completion.d/` and rename
it to abra.
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
```
sudo cp scripts/autocomplete/bash /etc/bash_completion.d/abra
source /etc/bash_completion.d/abra
```
**(fi)zsh**
(fi)zsh doesn't have an autocompletion folder by default but you can create one, then copy `scripts/autocomplete/zsh` into it and add a couple lines to your `~/.zshrc` or `~/.fizsh/.fizshrc`
```
sudo mkdir /etc/zsh/completion.d/
sudo cp scripts/autocomplete/zsh /etc/zsh/completion.d/abra
echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc
```
(replace .zshrc with ~/.fizsh/.fizshrc if you use fizsh)
## Hacking
Install direnv, run `cp .envrc.sample .envrc`, then run `direnv allow` in this directory. This will set coopcloud repos as private due to [this bug.](https://git.coopcloud.tech/coop-cloud/coopcloud.tech/issues/20#issuecomment-8201). Or you can run `go env -w GOPRIVATE=coopcloud.tech` but I'm not sure how persistent this is.
Install [Go >= 1.16](https://golang.org/doc/install) and then:
- `make build` to build
- `./abra` to run commands
- `make test` will run tests
Our [Drone CI configuration](.drone.yml) runs a number of sanity on each pushed commit. See the [Makefile](./Makefile) for more handy targets.
Please use the [conventional commit format](https://www.conventionalcommits.org/en/v1.0.0/) for your commits so we can automate our change log.
## Versioning
We use [goreleaser](https://goreleaser.com) to help us automate releases. We use [semver](https://semver.org) for versioning all releases of the tool. While we are still in the public alpha release phase, we will maintain a `0.y.z-alpha` format. Change logs are generated from our commit logs. We are still working this out and aim to refine our release praxis as we go.
For developers, while using this `-alpha` format, the `y` part is the "major" version part. So, if you make breaking changes, you increment that and _not_ the `x` part. So, if you're on `0.1.0-alpha`, then you'd go to `0.1.1-alpha` for a backwards compatible change and `0.2.0-alpha` for a backwards incompatible change.
## Making a new release
- Change `ABRA_VERSION` to match the new tag in [`scripts`](./scripts/installer/installer) (use [semver](https://semver.org))
- Commit that change (e.g. `git commit -m 'chore: publish next tag 0.3.1-alpha'`)
- Make a new tag (e.g. `git tag 0.y.z-alpha`)
- Push the new tag (e.g. `git push && git push --tags`)
- Wait until the build finishes on [build.coopcloud.tech](https://build.coopcloud.tech/coop-cloud/abra)
- Deploy the new installer script (e.g. `cd ./scripts/installer && make`)
- Check the release worked, (e.g. `abra upgrade; abra version`)
## Fork maintenance
We maintain a fork of [godotenv](https://github.com/Autonomic-Cooperative/godotenv) for two features:
1. multi-line env var support
2. inline comment parsing
You can upgrade the version here by running `go get github.com/Autonomic-Cooperative/godotenv@<commit>` where `<commit>` is the
latest commit you want to pin to. We are aiming to migrate to YAML format for the environment configuration, so this should only
be a temporary thing.
Executable
+1856
View File
File diff suppressed because it is too large Load Diff
+100
View File
@@ -0,0 +1,100 @@
#!/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
Executable
+42
View File
@@ -0,0 +1,42 @@
#!/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"
-38
View File
@@ -1,38 +0,0 @@
package app
import (
"github.com/urfave/cli/v2"
)
// AppCommand defines the `abra app` command and ets subcommands
var AppCommand = &cli.Command{
Name: "app",
Usage: "Manage apps",
Aliases: []string{"a"},
ArgsUsage: "<app>",
Description: `
This command provides all the functionality you need to manage the life cycle
of your apps. From initial deployment, day-2 operations (e.g. backup/restore)
to scaling apps up and spinning them down.
`,
Subcommands: []*cli.Command{
appNewCommand,
appConfigCommand,
appDeployCommand,
appUpgradeCommand,
appUndeployCommand,
appBackupCommand,
appRestoreCommand,
appRemoveCommand,
appCheckCommand,
appListCommand,
appPsCommand,
appLogsCommand,
appCpCommand,
appRunCommand,
appRollbackCommand,
appSecretCommand,
appVolumeCommand,
appVersionCommand,
},
}
-87
View File
@@ -1,87 +0,0 @@
package app
import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var backupAllServices bool
var backupAllServicesFlag = &cli.BoolFlag{
Name: "all",
Value: false,
Destination: &backupAllServices,
Aliases: []string{"a"},
Usage: "Backup all services",
}
var appBackupCommand = &cli.Command{
Name: "backup",
Usage: "Backup an app",
Aliases: []string{"b"},
Flags: []cli.Flag{backupAllServicesFlag},
ArgsUsage: "<service>",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if c.Args().Get(1) != "" && backupAllServices {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<service>' and '--all' together"))
}
abraSh := path.Join(config.ABRA_DIR, "apps", app.Type, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("'%s' does not exist?", abraSh)
}
logrus.Fatal(err)
}
sourceCmd := fmt.Sprintf("source %s", abraSh)
execCmd := "abra_backup"
if !backupAllServices {
serviceName := c.Args().Get(1)
if serviceName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no service(s) target provided"))
}
execCmd = fmt.Sprintf("abra_backup_%s", serviceName)
}
bytes, err := ioutil.ReadFile(abraSh)
if err != nil {
logrus.Fatal(err)
}
if !strings.Contains(string(bytes), execCmd) {
logrus.Fatalf("%s doesn't have a '%s' function", app.Type, execCmd)
}
sourceAndExec := fmt.Sprintf("%s; %s", sourceCmd, execCmd)
cmd := exec.Command("bash", "-c", sourceAndExec)
if err := internal.RunCmd(cmd); err != nil {
logrus.Fatal(err)
}
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}
-64
View File
@@ -1,64 +0,0 @@
package app
import (
"fmt"
"os"
"path"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var appCheckCommand = &cli.Command{
Name: "check",
Usage: "Check if app is configured correctly",
Aliases: []string{"c"},
ArgsUsage: "<service>",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
envSamplePath := path.Join(config.ABRA_DIR, "apps", app.Type, ".env.sample")
if _, err := os.Stat(envSamplePath); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("'%s' does not exist?", envSamplePath)
}
logrus.Fatal(err)
}
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: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}
-55
View File
@@ -1,55 +0,0 @@
package app
import (
"fmt"
"os"
"os/exec"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var appConfigCommand = &cli.Command{
Name: "config",
Aliases: []string{"c"},
Usage: "Edit app config",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
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, app.Path)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
logrus.Fatal(err)
}
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}
-124
View File
@@ -1,124 +0,0 @@
package app
import (
"fmt"
"os"
"strings"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"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/v2"
)
var appCpCommand = &cli.Command{
Name: "cp",
Aliases: []string{"c"},
ArgsUsage: "<src> <dst>",
Usage: "Copy files to/from a running app service",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
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")
}
appFiles, err := config.LoadAppFiles("")
if err != nil {
logrus.Fatal(err)
}
appEnv, err := config.GetApp(appFiles, app.Name)
if err != nil {
logrus.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("%s_%s", appEnv.StackName(), service))
containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
if err != nil {
logrus.Fatal(err)
}
if len(containers) != 1 {
logrus.Fatalf("expected 1 container but got %v", len(containers))
}
container := containers[0]
logrus.Debugf("retrieved '%s' as target container on '%s'", formatter.ShortenID(container.ID), app.Server)
if isToContainer {
if _, err := os.Stat(srcPath); err != nil {
logrus.Fatalf("'%s' does not exist?", srcPath)
}
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
content, err := archive.TarWithOptions(srcPath, toTarOpts)
if err != nil {
logrus.Fatal(err)
}
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(c.Context, container.ID, dstPath, content, copyOpts); err != nil {
logrus.Fatal(err)
}
} else {
content, _, err := cl.CopyFromContainer(c.Context, 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
},
}
-184
View File
@@ -1,184 +0,0 @@
package app
import (
"fmt"
"strings"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/client"
stack "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var appDeployCommand = &cli.Command{
Name: "deploy",
Aliases: []string{"d"},
Usage: "Deploy an app",
Flags: []cli.Flag{
internal.ForceFlag,
},
Description: `
This command deploys a new instance of an app. It does not support changing the
version of an existing deployed app, for this you need to look at the "abra app
upgrade <app>" 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 or your doing some live
hacking.
`,
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(c.Context, cl, stackName)
if err != nil {
logrus.Fatal(err)
}
if isDeployed {
if internal.Force {
logrus.Infof("continuing with deployment due to '--force/-f' being set")
} else {
logrus.Fatalf("'%s' is already deployed", stackName)
}
}
version := deployedVersion
if version == "" {
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type)
if err != nil {
logrus.Fatal(err)
}
if len(versions) > 0 {
version = versions[len(versions)-1]
logrus.Infof("choosing '%s' as version to deploy", version)
if err := recipe.EnsureVersion(app.Type, version); err != nil {
logrus.Fatal(err)
}
} else {
version = "latest commit"
logrus.Warning("no versions detected, using latest commit")
if err := recipe.EnsureLatest(app.Type); err != nil {
logrus.Fatal(err)
}
}
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
logrus.Fatal(err)
}
for k, v := range abraShEnv {
app.Env[k] = v
}
composeFiles, err := config.GetAppComposeFiles(app.Type, 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 := DeployOverview(app, version); err != nil {
logrus.Fatal(err)
}
if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
logrus.Fatal(err)
}
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}
// DeployOverview shows a deployment overview
func DeployOverview(app config.App, version string) error {
tableCol := []string{"server", "compose", "domain", "stack", "version"}
table := abraFormatter.CreateTable(tableCol)
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
}
table.Append([]string{app.Server, deployConfig, app.Domain, app.StackName(), version})
table.Render()
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
}
// NewVersionOverview shows an upgrade or downgrade overview
func NewVersionOverview(app config.App, currentVersion, newVersion string) error {
tableCol := []string{"server", "compose", "domain", "stack", "current version", "to be deployed"}
table := abraFormatter.CreateTable(tableCol)
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
}
table.Append([]string{app.Server, deployConfig, app.Domain, app.StackName(), currentVersion, newVersion})
table.Render()
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
}
-173
View File
@@ -1,173 +0,0 @@
package app
import (
"fmt"
"sort"
"strings"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/tagcmp"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var status bool
var statusFlag = &cli.BoolFlag{
Name: "status",
Aliases: []string{"S"},
Value: false,
Usage: "Show app deployment status",
Destination: &status,
}
var appType string
var typeFlag = &cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
Value: "",
Usage: "Show apps of a specific type",
Destination: &appType,
}
var listAppServer string
var listAppServerFlag = &cli.StringFlag{
Name: "server",
Aliases: []string{"s"},
Value: "",
Usage: "Show apps of a specific server",
Destination: &listAppServer,
}
var appListCommand = &cli.Command{
Name: "list",
Usage: "List all managed apps",
Description: `
This command looks at your local file system listing of apps and servers (e.g.
in ~/.abra/) to generate a report of all your apps.
By passing the "--status/-S" flag, you can query all your servers for the
actual live deployment status. Depending on how many servers you manage, this
can take some time.
`,
Aliases: []string{"ls"},
Flags: []cli.Flag{
statusFlag,
listAppServerFlag,
typeFlag,
},
Action: func(c *cli.Context) error {
appFiles, err := config.LoadAppFiles(listAppServer)
if err != nil {
logrus.Fatal(err)
}
apps, err := config.GetApps(appFiles)
if err != nil {
logrus.Fatal(err)
}
sort.Sort(config.ByServerAndType(apps))
statuses := make(map[string]map[string]string)
tableCol := []string{"Server", "Type", "Domain"}
if status {
tableCol = append(tableCol, "Status", "Version", "Updates")
statuses, err = config.GetAppStatuses(appFiles)
if err != nil {
logrus.Fatal(err)
}
}
table := abraFormatter.CreateTable(tableCol)
table.SetAutoMergeCellsByColumnIndex([]int{0})
var (
versionedAppsCount int
unversionedAppsCount int
onLatestCount int
canUpgradeCount int
)
for _, app := range apps {
var tableRow []string
if app.Type == appType || appType == "" {
// If type flag is set, check for it, if not, Type == ""
tableRow = []string{app.Server, app.Type, app.Domain}
if status {
stackName := app.StackName()
status := "unknown"
version := "unknown"
if statusMeta, ok := statuses[stackName]; ok {
if currentVersion, exists := statusMeta["version"]; exists {
version = currentVersion
}
if statusMeta["status"] != "" {
status = statusMeta["status"]
}
tableRow = append(tableRow, status, version)
versionedAppsCount++
} else {
tableRow = append(tableRow, status, version)
unversionedAppsCount++
}
var newUpdates []string
if version != "unknown" {
updates, err := catalogue.GetRecipeCatalogueVersions(app.Type)
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" {
tableRow = append(tableRow, "unknown")
} else {
tableRow = append(tableRow, "on latest")
onLatestCount++
}
} else {
// FIXME: jeezus golang why do you not have a list reverse function
for i, j := 0, len(newUpdates)-1; i < j; i, j = i+1, j-1 {
newUpdates[i], newUpdates[j] = newUpdates[j], newUpdates[i]
}
tableRow = append(tableRow, strings.Join(newUpdates, "\n"))
canUpgradeCount++
}
}
}
table.Append(tableRow)
}
stats := fmt.Sprintf(
"Total apps: %v | Versioned: %v | Unversioned: %v | On latest: %v | Can upgrade: %v",
len(apps),
versionedAppsCount,
unversionedAppsCount,
onLatestCount,
canUpgradeCount,
)
table.SetCaption(true, stats)
table.Render()
return nil
},
}
-124
View File
@@ -1,124 +0,0 @@
package app
import (
"fmt"
"io"
"os"
"sync"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
// stackLogs lists logs for all stack services
func stackLogs(c *cli.Context, stackName string, client *dockerClient.Client) {
filters := filters.NewArgs()
filters.Add("name", stackName)
serviceOpts := types.ServiceListOptions{Filters: filters}
services, err := client.ServiceList(c.Context, serviceOpts)
if err != nil {
logrus.Fatal(err)
}
var wg sync.WaitGroup
for _, service := range services {
wg.Add(1)
go func(s string) {
logOpts := types.ContainerLogsOptions{
Details: true,
Follow: true,
ShowStderr: true,
ShowStdout: true,
Tail: "20",
Timestamps: true,
}
logs, err := client.ServiceLogs(c.Context, s, logOpts)
if err != nil {
logrus.Fatal(err)
}
// defer after err check as any err returns a nil io.ReadCloser
defer logs.Close()
_, err = io.Copy(os.Stdout, logs)
if err != nil && err != io.EOF {
logrus.Fatal(err)
}
}(service.ID)
}
wg.Wait()
os.Exit(0)
}
var appLogsCommand = &cli.Command{
Name: "logs",
Aliases: []string{"l"},
ArgsUsage: "[<service>]",
Usage: "Tail app logs",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
serviceName := c.Args().Get(1)
if serviceName == "" {
logrus.Debug("tailing logs for all app services")
stackLogs(c, app.StackName(), cl)
}
logrus.Debugf("tailing logs for '%s'", serviceName)
service := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
filters := filters.NewArgs()
filters.Add("name", service)
serviceOpts := types.ServiceListOptions{Filters: filters}
services, err := cl.ServiceList(c.Context, serviceOpts)
if err != nil {
logrus.Fatal(err)
}
if len(services) != 1 {
logrus.Fatalf("expected 1 service but got %v", len(services))
}
logOpts := types.ContainerLogsOptions{
Details: true,
Follow: true,
ShowStderr: true,
ShowStdout: true,
Tail: "20",
Timestamps: true,
}
logs, err := cl.ServiceLogs(c.Context, services[0].ID, logOpts)
if err != nil {
logrus.Fatal(err)
}
// defer after err check as any err returns a nil io.ReadCloser
defer logs.Close()
_, err = io.Copy(os.Stdout, logs)
if err != nil && err != io.EOF {
logrus.Fatal(err)
}
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}
-230
View File
@@ -1,230 +0,0 @@
package app
import (
"fmt"
"path"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/secret"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
type secrets map[string]string
var domain string
var domainFlag = &cli.StringFlag{
Name: "domain",
Aliases: []string{"d"},
Value: "",
Usage: "Choose a domain name",
Destination: &domain,
}
var newAppServer string
var newAppServerFlag = &cli.StringFlag{
Name: "server",
Aliases: []string{"s"},
Value: "",
Usage: "Show apps of a specific server",
Destination: &newAppServer,
}
var newAppName string
var newAppNameFlag = &cli.StringFlag{
Name: "app-name",
Aliases: []string{"a"},
Value: "",
Usage: "Choose an app name",
Destination: &newAppName,
}
var appNewDescription = `
This command takes a recipe and uses it to create a new app. This new app
configuration is stored in your ~/.abra directory under the appropriate server.
This command does not deploy your app for you. You will need to run "abra app
deploy <app>" 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",
Usage: "Create a new app",
Aliases: []string{"n"},
Description: appNewDescription,
Flags: []cli.Flag{
newAppServerFlag,
domainFlag,
newAppNameFlag,
internal.PassFlag,
internal.SecretsFlag,
},
ArgsUsage: "<recipe>",
Action: action,
BashComplete: func(c *cli.Context) {
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for name := range catl {
fmt.Println(name)
}
},
}
// ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
func ensureDomainFlag() error {
if domain == "" {
prompt := &survey.Input{
Message: "Specify app domain",
}
if err := survey.AskOne(prompt, &domain); 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 == "" {
prompt := &survey.Select{
Message: "Select app server:",
Options: servers,
}
if err := survey.AskOne(prompt, &newAppServer); err != nil {
return err
}
}
return nil
}
// ensureServerFlag checks if the AppName flag was used. if not, asks the user for it.
func ensureAppNameFlag() error {
if newAppName == "" {
prompt := &survey.Input{
Message: "Specify app name:",
Default: config.SanitiseAppName(domain),
}
if err := survey.AskOne(prompt, &newAppName); err != nil {
return err
}
}
return nil
}
// createSecrets creates all secrets for a new app.
func createSecrets(sanitisedAppName string) (secrets, error) {
appEnvPath := path.Join(config.ABRA_DIR, "servers", newAppServer, fmt.Sprintf("%s.env", sanitisedAppName))
appEnv, err := config.ReadEnv(appEnvPath)
if err != nil {
return nil, err
}
secretEnvVars := secret.ReadSecretEnvVars(appEnv)
secrets, err := secret.GenerateSecrets(secretEnvVars, sanitisedAppName, newAppServer)
if err != nil {
return nil, err
}
if internal.Pass {
for secretName := range secrets {
secretValue := secrets[secretName]
if err := secret.PassInsertSecret(secretValue, secretName, sanitisedAppName, newAppServer); err != nil {
return nil, err
}
}
}
return secrets, nil
}
// action is the main command-line action for this package
func action(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
if err := config.EnsureAbraDirExists(); err != nil {
logrus.Fatal(err)
}
if err := ensureServerFlag(); err != nil {
logrus.Fatal(err)
}
if err := ensureDomainFlag(); err != nil {
logrus.Fatal(err)
}
if err := ensureAppNameFlag(); err != nil {
logrus.Fatal(err)
}
sanitisedAppName := config.SanitiseAppName(newAppName)
if len(sanitisedAppName) > 45 {
logrus.Fatalf("'%s' cannot be longer than 45 characters", sanitisedAppName)
}
logrus.Debugf("'%s' sanitised as '%s' for new app", newAppName, sanitisedAppName)
if err := config.TemplateAppEnvSample(recipe.Name, newAppName, newAppServer, domain, recipe.Name); err != nil {
logrus.Fatal(err)
}
if internal.Secrets {
secrets, err := createSecrets(sanitisedAppName)
if err != nil {
logrus.Fatal(err)
}
secretCols := []string{"Name", "Value"}
secretTable := abraFormatter.CreateTable(secretCols)
for secret := range secrets {
secretTable.Append([]string{secret, secrets[secret]})
}
if len(secrets) > 0 {
defer secretTable.Render()
}
}
tableCol := []string{"Name", "Domain", "Type", "Server"}
table := abraFormatter.CreateTable(tableCol)
table.Append([]string{sanitisedAppName, domain, recipe.Name, newAppServer})
fmt.Println("")
fmt.Println(fmt.Sprintf("New '%s' created! Here is your new app 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", sanitisedAppName))
fmt.Println("")
fmt.Println("You can deploy this app by running the following:")
fmt.Println(fmt.Sprintf("\n abra app deploy %s", sanitisedAppName))
fmt.Println("")
return nil
}
-99
View File
@@ -1,99 +0,0 @@
package app
import (
"fmt"
"strings"
"time"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var watch bool
var watchFlag = &cli.BoolFlag{
Name: "watch",
Aliases: []string{"w"},
Value: false,
Usage: "Watch status by polling repeatedly",
Destination: &watch,
}
var appPsCommand = &cli.Command{
Name: "ps",
Usage: "Check app status",
Aliases: []string{"p"},
Flags: []cli.Flag{
watchFlag,
},
Action: func(c *cli.Context) error {
if !watch {
showPSOutput(c)
return nil
}
// TODO: how do we make this update in-place in an x-platform way?
for {
showPSOutput(c)
time.Sleep(2 * time.Second)
}
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}
// showPSOutput renders ps output.
func showPSOutput(c *cli.Context) {
app := internal.ValidateApp(c)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
filters := filters.NewArgs()
filters.Add("name", app.StackName())
containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
if err != nil {
logrus.Fatal(err)
}
tableCol := []string{"image", "created", "status", "ports", "names"}
table := abraFormatter.CreateTable(tableCol)
for _, container := range containers {
var containerNames []string
for _, containerName := range container.Names {
trimmed := strings.TrimPrefix(containerName, "/")
containerNames = append(containerNames, trimmed)
}
tableRow := []string{
abraFormatter.RemoveSha(container.Image),
abraFormatter.HumanDuration(container.Created),
container.Status,
formatter.DisplayablePorts(container.Ports),
strings.Join(containerNames, "\n"),
}
table.Append(tableRow)
}
table.Render()
}
-168
View File
@@ -1,168 +0,0 @@
package app
import (
"fmt"
"os"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
// 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",
Value: false,
Destination: &Volumes,
}
var appRemoveCommand = &cli.Command{
Name: "remove",
Usage: "Remove an already undeployed app",
Aliases: []string{"rm"},
Flags: []cli.Flag{
VolumesFlag,
internal.ForceFlag,
},
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if !internal.Force {
response := false
prompt := &survey.Confirm{
Message: fmt.Sprintf("about to delete %s, are you sure?", app.Name),
}
if err := survey.AskOne(prompt, &response); err != nil {
logrus.Fatal(err)
}
if !response {
logrus.Fatal("user aborted app removal")
}
}
appFiles, err := config.LoadAppFiles("")
if err != nil {
logrus.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
if !internal.Force {
// FIXME: only query for app we are interested in, not all of them!
statuses, err := config.GetAppStatuses(appFiles)
if err != nil {
logrus.Fatal(err)
}
if statuses[app.Name]["status"] == "deployed" {
logrus.Fatalf("'%s' is still deployed. Run \"abra app %s undeploy\" or pass --force", app.Name, app.Name)
}
}
fs := filters.NewArgs()
fs.Add("name", app.Name)
secretList, err := cl.SecretList(c.Context, 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 {
secretsPrompt := &survey.MultiSelect{
Message: "which secrets do you want to remove?",
Options: secretNames,
Default: secretNames,
}
if err := survey.AskOne(secretsPrompt, &secretNamesToRemove); err != nil {
logrus.Fatal(err)
}
}
for _, name := range secretNamesToRemove {
err := cl.SecretRemove(c.Context, secrets[name])
if err != nil {
logrus.Fatal(err)
}
logrus.Info(fmt.Sprintf("secret: %s removed", name))
}
} else {
logrus.Info("no secrets to remove")
}
volumeListOKBody, err := cl.VolumeList(c.Context, 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 {
volumesPrompt := &survey.MultiSelect{
Message: "which volumes do you want to remove?",
Options: vols,
Default: vols,
}
if err := survey.AskOne(volumesPrompt, &removeVols); err != nil {
logrus.Fatal(err)
}
}
for _, vol := range removeVols {
err := cl.VolumeRemove(c.Context, 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 {
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: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}
-79
View File
@@ -1,79 +0,0 @@
package app
import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var restoreAllServices bool
var restoreAllServicesFlag = &cli.BoolFlag{
Name: "all",
Value: false,
Destination: &restoreAllServices,
Aliases: []string{"a"},
Usage: "Restore all services",
}
var appRestoreCommand = &cli.Command{
Name: "restore",
Usage: "Restore an app from a backup",
Aliases: []string{"r"},
Flags: []cli.Flag{restoreAllServicesFlag},
ArgsUsage: "<service> [<backup file>]",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if c.Args().Len() > 1 && restoreAllServices {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use <service>/<backup file> and '--all' together"))
}
abraSh := path.Join(config.ABRA_DIR, "apps", app.Type, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("'%s' does not exist?", abraSh)
}
logrus.Fatal(err)
}
sourceCmd := fmt.Sprintf("source %s", abraSh)
execCmd := "abra_restore"
if !restoreAllServices {
serviceName := c.Args().Get(1)
if serviceName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no service(s) target provided"))
}
execCmd = fmt.Sprintf("abra_restore_%s", serviceName)
}
bytes, err := ioutil.ReadFile(abraSh)
if err != nil {
logrus.Fatal(err)
}
if !strings.Contains(string(bytes), execCmd) {
logrus.Fatalf("%s doesn't have a '%s' function", app.Type, execCmd)
}
backupFile := c.Args().Get(2)
if backupFile != "" {
execCmd = fmt.Sprintf("%s %s", execCmd, backupFile)
}
sourceAndExec := fmt.Sprintf("%s; %s", sourceCmd, execCmd)
cmd := exec.Command("bash", "-c", sourceAndExec)
if err := internal.RunCmd(cmd); err != nil {
logrus.Fatal(err)
}
return nil
},
}
-158
View File
@@ -1,158 +0,0 @@
package app
import (
"fmt"
"coopcloud.tech/abra/pkg/catalogue"
stack "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"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/v2"
)
var appRollbackCommand = &cli.Command{
Name: "rollback",
Usage: "Roll an app back to a previous version",
Aliases: []string{"r", "downgrade"},
ArgsUsage: "<app>",
Flags: []cli.Flag{
internal.ForceFlag,
},
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 or your doing
some live hacking.
This action could be destructive, please ensure you have a copy of your app
data beforehand - see "abra app backup <app>" for more.
`,
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
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(c.Context, cl, stackName)
if err != nil {
logrus.Fatal(err)
}
if deployedVersion == "" {
logrus.Fatalf("failed to determine version of deployed '%s'", app.Name)
}
if !isDeployed {
logrus.Fatalf("'%s' is not deployed?", app.Name)
}
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type)
if err != nil {
logrus.Fatal(err)
}
var availableDowngrades []string
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.Fatal("no available downgrades, you're on latest")
}
// FIXME: jeezus golang why do you not have a list reverse function
for i, j := 0, len(availableDowngrades)-1; i < j; i, j = i+1, j-1 {
availableDowngrades[i], availableDowngrades[j] = availableDowngrades[j], availableDowngrades[i]
}
var chosenDowngrade string
if !internal.Force {
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.Force {
chosenDowngrade = availableDowngrades[0]
logrus.Debugf("choosing '%s' as version to downgrade to", chosenDowngrade)
}
if err := recipe.EnsureVersion(app.Type, chosenDowngrade); err != nil {
logrus.Fatal(err)
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
logrus.Fatal(err)
}
for k, v := range abraShEnv {
app.Env[k] = v
}
composeFiles, err := config.GetAppComposeFiles(app.Type, 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 := NewVersionOverview(app, deployedVersion, chosenDowngrade); err != nil {
logrus.Fatal(err)
}
}
if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
logrus.Fatal(err)
}
return nil
},
}
-123
View File
@@ -1,123 +0,0 @@
package app
import (
"errors"
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/client/container"
"coopcloud.tech/abra/pkg/config"
"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/v2"
)
var user string
var userFlag = &cli.StringFlag{
Name: "user",
Value: "",
Destination: &user,
}
var noTTY bool
var noTTYFlag = &cli.BoolFlag{
Name: "no-tty",
Value: false,
Destination: &noTTY,
}
var appRunCommand = &cli.Command{
Name: "run",
Flags: []cli.Flag{
noTTYFlag,
userFlag,
},
Aliases: []string{"r"},
ArgsUsage: "<service> <args>...",
Usage: "Run a command in a service container",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if c.Args().Len() < 2 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <service> provided?"))
}
if c.Args().Len() < 3 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <args> provided?"))
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
serviceName := c.Args().Get(1)
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("%s_%s", app.StackName(), serviceName))
containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
if err != nil {
logrus.Fatal(err)
}
if len(containers) > 1 {
logrus.Fatalf("expected 1 container but got %d", len(containers))
}
cmd := c.Args().Slice()[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: an absolutely monumental hack to instantiate another command-line
// client withing our command-line client so that we pass something down
// the tubes that satisfies the necessary interface requirements. We should
// refactor our vendored container code to not require all this cruft. For
// now, It Works.
dcli, err := command.NewDockerCli()
if err != nil {
logrus.Fatal(err)
}
if err := container.RunExec(dcli, cl, containers[0].ID, &execCreateOpts); err != nil {
logrus.Fatal(err)
}
return nil
},
BashComplete: func(c *cli.Context) {
switch c.NArg() {
case 0:
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
for _, a := range appNames {
fmt.Println(a)
}
case 1:
appName := c.Args().First()
serviceNames, err := config.GetAppServiceNames(appName)
if err != nil {
logrus.Warn(err)
}
for _, s := range serviceNames {
fmt.Println(s)
}
}
},
}
-260
View File
@@ -1,260 +0,0 @@
package app
import (
"errors"
"fmt"
"os"
"strconv"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/secret"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var allSecrets bool
var allSecretsFlag = &cli.BoolFlag{
Name: "all",
Aliases: []string{"A"},
Value: false,
Destination: &allSecrets,
Usage: "Generate all secrets",
}
var appSecretGenerateCommand = &cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate secrets",
ArgsUsage: "<secret> <version>",
Flags: []cli.Flag{allSecretsFlag, internal.PassFlag},
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if c.Args().Len() == 1 && !allSecrets {
err := errors.New("missing arguments <secret>/<version> or '--all'")
internal.ShowSubcommandHelpAndError(c, err)
}
if c.Args().Get(1) != "" && allSecrets {
err := errors.New("cannot use '<secret> <version>' and '--all' together")
internal.ShowSubcommandHelpAndError(c, err)
}
secretsToCreate := make(map[string]string)
secretEnvVars := secret.ReadSecretEnvVars(app.Env)
if allSecrets {
secretsToCreate = secretEnvVars
} else {
secretName := c.Args().Get(1)
secretVersion := c.Args().Get(2)
matches := false
for sec := range secretEnvVars {
parsed := secret.ParseSecretEnvVarName(sec)
if secretName == parsed {
secretsToCreate[sec] = secretVersion
}
}
if !matches {
logrus.Fatalf("'%s' doesn't exist in the env config?", secretName)
}
}
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.StackName(), app.Server); err != nil {
logrus.Fatal(err)
}
}
}
if len(secretVals) == 0 {
logrus.Warn("no secrets generated")
os.Exit(1)
}
tableCol := []string{"name", "value"}
table := abraFormatter.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.PassFlag},
ArgsUsage: "<secret> <version> <data>",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if c.Args().Len() != 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)
}
if internal.Pass {
if err := secret.PassInsertSecret(data, name, app.StackName(), app.Server); err != nil {
logrus.Fatal(err)
}
}
return nil
},
}
var appSecretRmCommand = &cli.Command{
Name: "remove",
Usage: "Remove a secret",
Aliases: []string{"rm"},
Flags: []cli.Flag{allSecretsFlag, internal.PassFlag},
ArgsUsage: "<secret>",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if c.Args().Get(1) != "" && allSecrets {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<secret>' and '--all' together"))
}
if c.Args().Get(1) == "" && !allSecrets {
internal.ShowSubcommandHelpAndError(c, errors.New("no secret(s) specified?"))
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
filters := filters.NewArgs()
filters.Add("name", app.StackName())
secretList, err := cl.SecretList(c.Context, types.SecretListOptions{Filters: filters})
if err != nil {
logrus.Fatal(err)
}
secretToRm := c.Args().Get(1)
for _, cont := range secretList {
secretName := cont.Spec.Annotations.Name
parsed := secret.ParseGeneratedSecretName(secretName, app)
if allSecrets {
if err := cl.SecretRemove(c.Context, secretName); err != nil {
logrus.Fatal(err)
}
if internal.Pass {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
logrus.Fatal(err)
}
}
} else {
if parsed == secretToRm {
if err := cl.SecretRemove(c.Context, secretName); err != nil {
logrus.Fatal(err)
}
if internal.Pass {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
logrus.Fatal(err)
}
}
}
}
}
return nil
},
}
var appSecretLsCommand = &cli.Command{
Name: "list",
Usage: "List all secrets",
Aliases: []string{"ls"},
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
secrets := secret.ReadSecretEnvVars(app.Env)
tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"}
table := abraFormatter.CreateTable(tableCol)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
filters := filters.NewArgs()
filters.Add("name", app.StackName())
secretList, err := cl.SecretList(c.Context, 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)
}
table.Render()
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}
var appSecretCommand = &cli.Command{
Name: "secret",
Aliases: []string{"s"},
Usage: "Manage app secrets",
ArgsUsage: "<command>",
Subcommands: []*cli.Command{
appSecretGenerateCommand,
appSecretInsertCommand,
appSecretRmCommand,
appSecretLsCommand,
},
}
-50
View File
@@ -1,50 +0,0 @@
package app
import (
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
stack "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var appUndeployCommand = &cli.Command{
Name: "undeploy",
Aliases: []string{"u"},
Usage: "Undeploy an app",
Description: `
This does not destroy any of the application data. However, you should remain
vigilant, as your swarm installation will consider any previously attached
volumes as eligiblef or pruning once undeployed.
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
rmOpts := stack.Remove{Namespaces: []string{app.StackName()}}
if err := stack.RunRemove(c.Context, cl, rmOpts); err != nil {
logrus.Fatal(err)
}
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}
-157
View File
@@ -1,157 +0,0 @@
package app
import (
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/client"
stack "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var appUpgradeCommand = &cli.Command{
Name: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade an app",
ArgsUsage: "<app>",
Flags: []cli.Flag{
internal.ForceFlag,
},
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 changing the version of running apps, as
opposed to "abra app deploy <app>" 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 or your doing
some live hacking.
This action could be destructive, please ensure you have a copy of your app
data beforehand - see "abra app backup <app>" for more.
`,
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(c.Context, cl, stackName)
if err != nil {
logrus.Fatal(err)
}
if deployedVersion == "" {
logrus.Fatalf("failed to determine version of deployed '%s'", app.Name)
}
if !isDeployed {
logrus.Fatalf("'%s' is not deployed?", app.Name)
}
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type)
if err != nil {
logrus.Fatal(err)
}
var availableUpgrades []string
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 {
logrus.Fatal("no available upgrades, you're on latest")
}
var chosenUpgrade string
if !internal.Force {
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 internal.Force {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
logrus.Debugf("choosing '%s' as version to upgrade to", chosenUpgrade)
}
if err := recipe.EnsureVersion(app.Type, chosenUpgrade); err != nil {
logrus.Fatal(err)
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
logrus.Fatal(err)
}
for k, v := range abraShEnv {
app.Env[k] = v
}
composeFiles, err := config.GetAppComposeFiles(app.Type, 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 := NewVersionOverview(app, deployedVersion, chosenUpgrade); err != nil {
logrus.Fatal(err)
}
}
if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
logrus.Fatal(err)
}
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}
-104
View File
@@ -1,104 +0,0 @@
package app
import (
"fmt"
"strings"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
// getImagePath returns the image name
func getImagePath(image string) (string, error) {
img, err := reference.ParseNormalizedNamed(image)
if err != nil {
return "", err
}
path := reference.Path(img)
if strings.Contains(path, "library") {
path = strings.Split(path, "/")[1]
}
logrus.Debugf("parsed '%s' from '%s'", path, image)
return path, nil
}
var appVersionCommand = &cli.Command{
Name: "version",
Aliases: []string{"v"},
Usage: "Show 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(c.Context, cl, stackName)
if err != nil {
logrus.Fatal(err)
}
if deployedVersion == "" {
logrus.Fatalf("failed to determine version of deployed '%s'", app.Name)
}
if !isDeployed {
logrus.Fatalf("'%s' is not deployed?", app.Name)
}
recipeMeta, err := catalogue.GetRecipeMeta(app.Type)
if err != nil {
logrus.Fatal(err)
}
versionsMeta := make(map[string]catalogue.ServiceMeta)
for _, recipeVersion := range recipeMeta.Versions {
if currentVersion, exists := recipeVersion[deployedVersion]; exists {
versionsMeta = currentVersion
}
}
if len(versionsMeta) == 0 {
logrus.Fatalf("PANIC: could not retrieve deployed version ('%s') from recipe catalogue?", deployedVersion)
}
tableCol := []string{"name", "image", "version", "tag", "digest"}
table := abraFormatter.CreateTable(tableCol)
for serviceName, versionMeta := range versionsMeta {
table.Append([]string{serviceName, versionMeta.Image, deployedVersion, versionMeta.Tag, versionMeta.Digest})
}
table.Render()
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}
-106
View File
@@ -1,106 +0,0 @@
package app
import (
"fmt"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var appVolumeListCommand = &cli.Command{
Name: "list",
Usage: "List volumes associated with an app",
Aliases: []string{"ls"},
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
volumeList, err := client.GetVolumes(c.Context, app.Server, app.Name)
if err != nil {
logrus.Fatal(err)
}
table := abraFormatter.CreateTable([]string{"driver", "volume name"})
var volTable [][]string
for _, volume := range volumeList {
volRow := []string{
volume.Driver,
volume.Name,
}
volTable = append(volTable, volRow)
}
table.AppendBulk(volTable)
table.Render()
return nil
},
}
var appVolumeRemoveCommand = &cli.Command{
Name: "remove",
Usage: "Remove volume(s) associated with an app",
Aliases: []string{"rm"},
Flags: []cli.Flag{
internal.ForceFlag,
},
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
volumeList, err := client.GetVolumes(c.Context, app.Server, app.Name)
if err != nil {
logrus.Fatal(err)
}
volumeNames := client.GetVolumeNames(volumeList)
var volumesToRemove []string
if !internal.Force {
volumesPrompt := &survey.MultiSelect{
Message: "which volumes do you want to remove?",
Options: volumeNames,
Default: volumeNames,
}
if err := survey.AskOne(volumesPrompt, &volumesToRemove); err != nil {
logrus.Fatal(err)
}
} else {
volumesToRemove = volumeNames
}
err = client.RemoveVolumes(c.Context, app.Server, volumesToRemove, internal.Force)
if err != nil {
logrus.Fatal(err)
}
logrus.Info("volumes removed successfully")
return nil
},
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
}
var appVolumeCommand = &cli.Command{
Name: "volume",
Aliases: []string{"vl"},
Usage: "Manage app volumes",
ArgsUsage: "<command>",
Subcommands: []*cli.Command{
appVolumeListCommand,
appVolumeRemoveCommand,
},
}
-17
View File
@@ -1,17 +0,0 @@
package catalogue
import (
"github.com/urfave/cli/v2"
)
// 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,
},
}
-173
View File
@@ -1,173 +0,0 @@
package catalogue
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/git"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
// CatalogueSkipList is all the repos that are not recipes.
var CatalogueSkipList = map[string]bool{
"abra": true,
"abra-bash": true,
"abra-apps": true,
"abra-aur": 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,
"coopcloud.tech": true,
"coturn": true,
"docker-cp-deploy": true,
"docker-dind-bats-kcov": true,
"docs.coopcloud.tech": true,
"example": true,
"gardening": true,
"go-abra": true,
"organising": true,
"pyabra": true,
"radicle-seed-node": true,
"stack-ssh-deploy": true,
"swarm-cronjob": true,
"tagcmp": true,
"tyop": true,
}
var catalogueGenerateCommand = &cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate a new copy of the catalogue",
ArgsUsage: "[<recipe>]",
BashComplete: func(c *cli.Context) {
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for name := range catl {
fmt.Println(name)
}
},
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
repos, err := catalogue.ReadReposMetadata()
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("ensuring '%v' recipe(s) are locally present and up-to-date", len(repos))
retrieveBar := formatter.CreateProgressbar(len(repos), "retrieving recipes...")
ch := make(chan string, len(repos))
for _, repoMeta := range repos {
go func(rm catalogue.RepoMeta) {
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.ABRA_DIR, "apps", rm.Name)
if err := git.Clone(recipeDir, rm.SSHURL); err != nil {
logrus.Fatal(err)
}
if err := git.EnsureUpToDate(recipeDir); err != nil {
logrus.Fatal(err)
}
ch <- rm.Name
retrieveBar.Add(1)
}(repoMeta)
}
for range repos {
<-ch // wait for everything
}
catl := make(catalogue.RecipeCatalogue)
catlBar := formatter.CreateProgressbar(len(repos), "generating catalogue...")
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 := catalogue.GetRecipeVersions(recipeMeta.Name)
if err != nil {
logrus.Fatal(err)
}
catl[recipeMeta.Name] = catalogue.RecipeMeta{
Name: recipeMeta.Name,
Repository: recipeMeta.CloneURL,
Icon: recipeMeta.AvatarURL,
DefaultBranch: recipeMeta.DefaultBranch,
Description: recipeMeta.Description,
Website: recipeMeta.Website,
Versions: versions,
// Category: ..., // FIXME: once we sort out the machine-readable catalogue interface
// Features: ..., // FIXME: once we figure out the machine-readable catalogue interface
}
catlBar.Add(1)
}
recipesJSON, err := json.MarshalIndent(catl, "", " ")
if err != nil {
logrus.Fatal(err)
}
if _, err := os.Stat(config.APPS_JSON); err != nil && os.IsNotExist(err) {
if err := ioutil.WriteFile(config.APPS_JSON, recipesJSON, 0644); err != nil {
logrus.Fatal(err)
}
} else {
if recipeName != "" {
catlFS, err := catalogue.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.APPS_JSON, updatedRecipesJSON, 0644); err != nil {
logrus.Fatal(err)
}
}
}
logrus.Infof("generated new recipe catalogue in '%s'", config.APPS_JSON)
return nil
},
}
-113
View File
@@ -1,113 +0,0 @@
// Package cli provides the interface for the command-line.
package cli
import (
"fmt"
"os"
"path"
"coopcloud.tech/abra/cli/app"
"coopcloud.tech/abra/cli/catalogue"
"coopcloud.tech/abra/cli/recipe"
"coopcloud.tech/abra/cli/server"
"coopcloud.tech/abra/pkg/config"
logrusStack "github.com/Gurpartap/logrus-stack"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
// Verbose stores the variable from VerboseFlag.
var Verbose bool
// VerboseFlag turns on/off verbose logging down to the INFO level.
var VerboseFlag = &cli.BoolFlag{
Name: "verbose",
Aliases: []string{"V"},
Value: false,
Destination: &Verbose,
Usage: "Show INFO messages",
}
// Debug stores the variable from DebugFlag.
var Debug bool
// DebugFlag turns on/off verbose logging down to the DEBUG level.
var DebugFlag = &cli.BoolFlag{
Name: "debug",
Aliases: []string{"d"},
Value: false,
Destination: &Debug,
Usage: "Show DEBUG messages",
}
// RunApp runs CLI abra app.
func RunApp(version, commit string) {
app := &cli.App{
Name: "abra",
Usage: `The Co-op Cloud command-line utility belt 🎩🐇
____ ____ _ _
/ ___|___ ___ _ __ / ___| | ___ _ _ __| |
| | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |
| |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |
\____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|
|_|
`,
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Commands: []*cli.Command{
app.AppCommand,
server.ServerCommand,
recipe.RecipeCommand,
catalogue.CatalogueCommand,
VersionCommand,
UpgradeCommand,
},
Flags: []cli.Flag{
VerboseFlag,
DebugFlag,
},
Authors: []*cli.Author{
{
Name: "Autonomic Co-op",
Email: "helo@autonomic.zone",
},
},
}
app.EnableBashCompletion = true
app.Before = func(c *cli.Context) error {
if Debug {
logrus.SetLevel(logrus.DebugLevel)
logrus.SetFormatter(&logrus.TextFormatter{})
logrus.SetOutput(os.Stderr)
logrus.AddHook(logrusStack.StandardHook())
}
paths := []string{
config.ABRA_DIR,
path.Join(config.ABRA_DIR, "servers"),
path.Join(config.ABRA_DIR, "apps"),
path.Join(config.ABRA_DIR, "vendor"),
}
for _, path := range paths {
if err := os.Mkdir(path, 0755); err != nil {
if !os.IsExist(err) {
logrus.Fatal(err)
}
logrus.Debugf("'%s' already created, moving on...", path)
continue
}
logrus.Debugf("'%s' is missing, creating...", path)
}
logrus.Debugf("abra version '%s', commit '%s'", version, commit)
return nil
}
if err := app.Run(os.Args); err != nil {
logrus.Fatal(err)
}
}
-52
View File
@@ -1,52 +0,0 @@
package formatter
import (
"fmt"
"os"
"strings"
"time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/go-units"
"github.com/olekukonko/tablewriter"
"github.com/schollz/progressbar/v3"
)
func ShortenID(str string) string {
return str[:12]
}
func Truncate(str string) string {
return fmt.Sprintf(`"%s"`, formatter.Ellipsis(str, 19))
}
// RemoveSha remove image sha from a string that are added in some docker outputs
func RemoveSha(str string) string {
return strings.Split(str, "@")[0]
}
// HumanDuration from docker/cli RunningFor() to be accessible outside of the class
func HumanDuration(timestamp int64) string {
date := time.Unix(timestamp, 0)
now := time.Now().UTC()
return units.HumanDuration(now.Sub(date)) + " ago"
}
// CreateTable prepares a table layout for output.
func CreateTable(columns []string) *tablewriter.Table {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader(columns)
return table
}
// 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),
)
}
-39
View File
@@ -1,39 +0,0 @@
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
}
-51
View File
@@ -1,51 +0,0 @@
package internal
import (
"github.com/urfave/cli/v2"
)
// Secrets stores the variable from SecretsFlag
var Secrets bool
// SecretsFlag turns on/off automatically generating secrets
var SecretsFlag = &cli.BoolFlag{
Name: "secrets",
Aliases: []string{"S"},
Value: false,
Usage: "Automatically generate secrets",
Destination: &Secrets,
}
// Pass stores the variable from PassFlag
var Pass bool
// PassFlag turns on/off storing generated secrets in pass
var PassFlag = &cli.BoolFlag{
Name: "pass",
Aliases: []string{"P"},
Value: false,
Usage: "Store the generated secrets in a local pass store",
Destination: &Pass,
}
// Context is temp
var Context string
// ContextFlag is temp
var ContextFlag = &cli.StringFlag{
Name: "context",
Value: "",
Aliases: []string{"c"},
Destination: &Context,
}
// Force force functionality without asking.
var Force bool
// ForceFlag turns on/off force functionality.
var ForceFlag = &cli.BoolFlag{
Name: "force",
Value: false,
Aliases: []string{"f"},
Destination: &Force,
}
-18
View File
@@ -1,18 +0,0 @@
package internal
import (
"os"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
// ShowSubcommandHelpAndError exits the program on error, logs the error to the
// terminal, and shows the help command.
func ShowSubcommandHelpAndError(c *cli.Context, err interface{}) {
if err2 := cli.ShowSubcommandHelp(c); err2 != nil {
logrus.Error(err2)
}
logrus.Error(err)
os.Exit(1)
}
-64
View File
@@ -1,64 +0,0 @@
package internal
import (
"errors"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
// ValidateRecipe ensures the recipe arg is valid.
func ValidateRecipe(c *cli.Context) recipe.Recipe {
recipeName := c.Args().First()
if recipeName == "" {
ShowSubcommandHelpAndError(c, errors.New("no recipe provided"))
}
recipe, err := recipe.Get(recipeName)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("validated '%s' as recipe argument", recipeName)
return recipe
}
// ValidateApp ensures the app name arg is valid.
func ValidateApp(c *cli.Context) config.App {
appName := c.Args().First()
if appName == "" {
ShowSubcommandHelpAndError(c, errors.New("no app provided"))
}
app, err := app.Get(appName)
if err != nil {
logrus.Fatal(err)
}
if err := recipe.EnsureExists(app.Type); 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 == "" {
ShowSubcommandHelpAndError(c, errors.New("no domain provided"))
}
logrus.Debugf("validated '%s' as domain argument", domainName)
return domainName
}
-106
View File
@@ -1,106 +0,0 @@
package recipe
import (
"fmt"
"os"
"strconv"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/tagcmp"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var recipeLintCommand = &cli.Command{
Name: "lint",
Usage: "Lint a recipe",
Aliases: []string{"l"},
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
expectedVersion := false
if recipe.Config.Version == "3.8" {
expectedVersion = true
}
envSampleProvided := false
envSample := fmt.Sprintf("%s/%s/.env.sample", config.APPS_DIR, recipe.Name)
if _, err := os.Stat(envSample); !os.IsNotExist(err) {
envSampleProvided = true
} else if err != nil {
logrus.Fatal(err)
}
serviceNamedApp := false
traefikEnabled := false
healthChecksForAllServices := true
allImagesTagged := true
noUnstableTags := true
semverLikeTags := true
for _, service := range recipe.Config.Services {
if service.Name == "app" {
serviceNamedApp = true
}
for label := range service.Deploy.Labels {
if label == "traefik.enable" {
if service.Deploy.Labels[label] == "true" {
traefikEnabled = true
}
}
}
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
logrus.Fatal(err)
}
if reference.IsNameOnly(img) {
allImagesTagged = false
}
tag := img.(reference.NamedTagged).Tag()
if tag == "latest" {
noUnstableTags = false
}
if !tagcmp.IsParsable(tag) {
semverLikeTags = false
}
if service.HealthCheck == nil {
healthChecksForAllServices = false
}
}
tableCol := []string{"rule", "satisfied"}
table := formatter.CreateTable(tableCol)
table.Append([]string{"compose files have the expected version", strconv.FormatBool(expectedVersion)})
table.Append([]string{"environment configuration is provided", strconv.FormatBool(envSampleProvided)})
table.Append([]string{"recipe contains a service named 'app'", strconv.FormatBool(serviceNamedApp)})
table.Append([]string{"traefik routing enabled on at least one service", strconv.FormatBool(traefikEnabled)})
table.Append([]string{"all services have a healthcheck enabled", strconv.FormatBool(healthChecksForAllServices)})
table.Append([]string{"all images are using a tag", strconv.FormatBool(allImagesTagged)})
table.Append([]string{"no usage of unstable 'latest' tags", strconv.FormatBool(noUnstableTags)})
table.Append([]string{"all tags are using a semver-like format", strconv.FormatBool(semverLikeTags)})
table.Render()
return nil
},
BashComplete: func(c *cli.Context) {
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for name := range catl {
fmt.Println(name)
}
},
}
-39
View File
@@ -1,39 +0,0 @@
package recipe
import (
"fmt"
"sort"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/catalogue"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var recipeListCommand = &cli.Command{
Name: "list",
Usage: "List available recipes",
Aliases: []string{"ls"},
Action: func(c *cli.Context) error {
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err.Error())
}
recipes := catl.Flatten()
sort.Sort(catalogue.ByRecipeName(recipes))
tableCol := []string{"name", "category", "status"}
table := formatter.CreateTable(tableCol)
for _, recipe := range recipes {
status := fmt.Sprintf("%v", recipe.Features.Status)
tableRow := []string{recipe.Name, recipe.Category, status}
table.Append(tableRow)
}
table.Render()
return nil
},
}
-79
View File
@@ -1,79 +0,0 @@
package recipe
import (
"fmt"
"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/v2"
)
var recipeNewCommand = &cli.Command{
Name: "new",
Usage: "Create a new recipe",
Aliases: []string{"n"},
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
directory := path.Join(config.APPS_DIR, recipe.Name)
if _, err := os.Stat(directory); !os.IsNotExist(err) {
logrus.Fatalf("'%s' recipe directory already exists?", directory)
return nil
}
url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL)
if err := git.Clone(directory, url); err != nil {
return err
}
gitRepo := path.Join(config.APPS_DIR, recipe.Name, ".git")
if err := os.RemoveAll(gitRepo); err != nil {
logrus.Fatal(err)
return nil
}
logrus.Debugf("removed git repo in '%s'", gitRepo)
toParse := []string{
path.Join(config.APPS_DIR, recipe.Name, "README.md"),
path.Join(config.APPS_DIR, recipe.Name, ".env.sample"),
path.Join(config.APPS_DIR, recipe.Name, ".drone.yml"),
}
for _, path := range toParse {
file, err := os.OpenFile(path, os.O_RDWR, 0755)
if err != nil {
logrus.Fatal(err)
return nil
}
tpl, err := template.ParseFiles(path)
if err != nil {
logrus.Fatal(err)
return nil
}
// TODO: ask for description and probably other things so that the
// template repository is more "ready" to go than the current best-guess
// mode of templating
if err := tpl.Execute(file, struct {
Name string
Description string
}{recipe.Name, "TODO"}); err != nil {
logrus.Fatal(err)
return nil
}
}
logrus.Infof(
"new recipe '%s' created in %s, happy hacking!\n",
recipe.Name, path.Join(config.APPS_DIR, recipe.Name),
)
return nil
},
}
-51
View File
@@ -1,51 +0,0 @@
package recipe
import (
"github.com/urfave/cli/v2"
)
var Major bool
var MajorFlag = &cli.BoolFlag{
Name: "major",
Value: false,
Aliases: []string{"ma", "x"},
Destination: &Major,
}
var Minor bool
var MinorFlag = &cli.BoolFlag{
Name: "minor",
Value: false,
Aliases: []string{"mi", "y"},
Destination: &Minor,
}
var Patch bool
var PatchFlag = &cli.BoolFlag{
Name: "patch",
Value: false,
Aliases: []string{"p", "z"},
Destination: &Patch,
}
// RecipeCommand defines all recipe related sub-commands.
var RecipeCommand = &cli.Command{
Name: "recipe",
Usage: "Manage recipes",
ArgsUsage: "<recipe>",
Aliases: []string{"r"},
Description: `
A recipe is a blueprint for an app. It is a bunch of configuration files which
describe how to deploy and maintain an app. Recipes are maintained by the Co-op
Cloud community and you can use Abra to read them and create apps for you.
`,
Subcommands: []*cli.Command{
recipeListCommand,
recipeVersionCommand,
recipeReleaseCommand,
recipeNewCommand,
recipeUpgradeCommand,
recipeSyncCommand,
recipeLintCommand,
},
}
-312
View File
@@ -1,312 +0,0 @@
package recipe
import (
"fmt"
"path"
"strconv"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
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/v2"
)
var Push bool
var PushFlag = &cli.BoolFlag{
Name: "push",
Value: false,
Destination: &Push,
}
var Dry bool
var DryFlag = &cli.BoolFlag{
Name: "dry-run",
Value: false,
Aliases: []string{"d"},
Destination: &Dry,
}
var CommitMessage string
var CommitMessageFlag = &cli.StringFlag{
Name: "commit-message",
Usage: "commit message. Implies --commit",
Aliases: []string{"cm"},
Destination: &CommitMessage,
}
var Commit bool
var CommitFlag = &cli.BoolFlag{
Name: "commit",
Usage: "add compose.yml to staging area and commit changes",
Value: false,
Aliases: []string{"c"},
Destination: &Commit,
}
var TagMessage string
var TagMessageFlag = &cli.StringFlag{
Name: "tag-comment",
Usage: "tag comment. If not given, user will be asked for it",
Aliases: []string{"t", "tm"},
Destination: &TagMessage,
}
var recipeReleaseCommand = &cli.Command{
Name: "release",
Usage: "tag a recipe",
Aliases: []string{"rl"},
ArgsUsage: "<recipe> [<tag>]",
Description: `
This command is used to specify a new tag for a recipe. These tags are used to
identify different versions of the recipe and are published on the Co-op Cloud
recipe catalogue.
These tags take the following form:
a.b.c+x.y.z
Where the "a.b.c" part is maintained as a semantic version of the recipe by the
recipe 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 in order to maximise the chances that the nature of recipe
updates are properly communicated.
Abra does its best to read the "a.b.c" version scheme and communicate what
action needs to be taken when performing different operations such as an update
or a rollback of an app.
`,
Flags: []cli.Flag{
DryFlag,
PatchFlag,
MinorFlag,
MajorFlag,
PushFlag,
CommitFlag,
CommitMessageFlag,
TagMessageFlag,
},
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
directory := path.Join(config.APPS_DIR, recipe.Name)
tagstring := c.Args().Get(1)
imagesTmp := getImageVersions(recipe)
mainApp := getMainApp(recipe)
mainAppVersion := imagesTmp[mainApp]
if err := recipePkg.EnsureExists(recipe.Name); err != nil {
logrus.Fatal(err)
}
if mainAppVersion == "" {
logrus.Fatal("main app version is empty?")
}
if tagstring != "" {
if _, err := tagcmp.Parse(tagstring); err != nil {
logrus.Fatal("invalid tag specified")
}
}
if TagMessage == "" {
prompt := &survey.Input{
Message: "tag message",
}
survey.AskOne(prompt, &TagMessage)
}
var createTagOptions git.CreateTagOptions
createTagOptions.Message = TagMessage
if Commit || (CommitMessage != "") {
commitRepo, err := git.PlainOpen(directory)
if err != nil {
logrus.Fatal(err)
}
commitWorktree, err := commitRepo.Worktree()
if err != nil {
logrus.Fatal(err)
}
if CommitMessage == "" {
prompt := &survey.Input{
Message: "commit message",
}
survey.AskOne(prompt, &CommitMessage)
}
err = commitWorktree.AddGlob("compose.**yml")
if err != nil {
logrus.Fatal(err)
}
logrus.Debug("staged compose.**yml for commit")
_, err = commitWorktree.Commit(CommitMessage, &git.CommitOptions{})
if err != nil {
logrus.Fatal(err)
}
logrus.Info("changes commited")
}
repo, err := git.PlainOpen(directory)
if err != nil {
logrus.Fatal(err)
}
head, err := repo.Head()
if err != nil {
logrus.Fatal(err)
}
// bumpType is used to decide what part of the tag should be incremented
bumpType := btoi(Major)*4 + btoi(Minor)*2 + btoi(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.")
}
}
if tagstring != "" {
if bumpType > 0 {
logrus.Warn("user specified a version number and --major/--minor/--patch at the same time! using version number...")
}
tag, err := tagcmp.Parse(tagstring)
if err != nil {
logrus.Fatal(err)
}
if tag.MissingMinor {
tag.Minor = "0"
tag.MissingMinor = false
}
if tag.MissingPatch {
tag.Patch = "0"
tag.MissingPatch = false
}
tagstring = fmt.Sprintf("%s+%s", tag.String(), mainAppVersion)
if Dry {
logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", tagstring, head.Hash()))
return nil
}
repo.CreateTag(tagstring, head.Hash(), &createTagOptions)
logrus.Info(fmt.Sprintf("created tag %s at %s", tagstring, head.Hash()))
if Push {
if err := repo.Push(&git.PushOptions{}); err != nil {
logrus.Fatal(err)
}
logrus.Info(fmt.Sprintf("pushed tag %s to remote", tagstring))
}
return nil
}
// get the latest tag with its hash, name etc
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)
}
fmt.Println(lastGitTag)
newTag := lastGitTag
var newTagString string
if bumpType > 0 {
if Patch {
now, err := strconv.Atoi(newTag.Patch)
if err != nil {
logrus.Fatal(err)
}
newTag.Patch = strconv.Itoa(now + 1)
} else if Minor {
now, err := strconv.Atoi(newTag.Minor)
if err != nil {
logrus.Fatal(err)
}
newTag.Patch = "0"
newTag.Minor = strconv.Itoa(now + 1)
} else if 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)
}
} else {
logrus.Fatal("we don't support automatic tag generation yet - specify a version or use one of: --major --minor --patch")
}
newTag.Metadata = mainAppVersion
newTagString = newTag.String()
if Dry {
logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", newTagString, head.Hash()))
return nil
}
repo.CreateTag(newTagString, head.Hash(), &createTagOptions)
logrus.Info(fmt.Sprintf("created tag %s at %s", newTagString, head.Hash()))
if Push {
if err := repo.Push(&git.PushOptions{}); err != nil {
logrus.Fatal(err)
}
logrus.Info(fmt.Sprintf("pushed tag %s to remote", newTagString))
}
return nil
},
}
func getImageVersions(recipe recipe.Recipe) map[string]string {
var services = make(map[string]string)
for _, service := range recipe.Config.Services {
srv := strings.Split(service.Image, ":")
services[srv[0]] = srv[1]
}
return services
}
func getMainApp(recipe recipe.Recipe) string {
for _, service := range recipe.Config.Services {
name := service.Name
if name == "app" {
return strings.Split(service.Image, ":")[0]
}
}
return ""
}
func btoi(b bool) int {
if b {
return 1
}
return 0
}
-88
View File
@@ -1,88 +0,0 @@
package recipe
import (
"errors"
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var recipeSyncCommand = &cli.Command{
Name: "sync",
Usage: "Ensure recipe version labels are up-to-date",
Aliases: []string{"s"},
ArgsUsage: "<recipe> <version>",
Description: `
This command will generate labels for the main recipe service (i.e. by
convention, typically the service named "app") which corresponds to the
following format:
coop-cloud.${STACK_NAME}.version=<version>
The <version> is determined by the recipe maintainer and is specified on the
command-line. The <recipe> configuration will be updated on the local file
system.
`,
BashComplete: func(c *cli.Context) {
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for name := range catl {
fmt.Println(name)
}
},
Action: func(c *cli.Context) error {
if c.Args().Len() != 2 {
internal.ShowSubcommandHelpAndError(c, errors.New("missing <recipe>/<version> arguments?"))
}
recipe := internal.ValidateRecipe(c)
// TODO: validate with tagcmp when new commits come in
// See https://git.coopcloud.tech/coop-cloud/abra/pulls/109
nextTag := c.Args().Get(1)
mainService := "app"
var services []string
hasAppService := false
for _, service := range recipe.Config.Services {
services = append(services, service.Name)
if service.Name == "app" {
hasAppService = true
logrus.Debugf("detected app service in '%s'", recipe.Name)
}
}
if !hasAppService {
logrus.Warnf("no 'app' service defined in '%s'", recipe.Name)
var chosenService string
prompt := &survey.Select{
Message: fmt.Sprintf("what is the main service name for '%s'?", recipe.Name),
Options: services,
}
if err := survey.AskOne(prompt, &chosenService); err != nil {
logrus.Fatal(err)
}
mainService = chosenService
}
logrus.Debugf("selecting '%s' as the service to sync version labels", mainService)
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag)
if err := recipe.UpdateLabel(mainService, label); err != nil {
logrus.Fatal(err)
}
logrus.Infof("synced label '%s' to service '%s'", label, mainService)
return nil
},
}
-166
View File
@@ -1,166 +0,0 @@
package recipe
import (
"fmt"
"sort"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var recipeUpgradeCommand = &cli.Command{
Name: "upgrade",
Usage: "Upgrade recipe image tags",
Aliases: []string{"u"},
Description: `
This command reads and attempts to parse all image tags within the given
<recipe> configuration and prompt with more recent tags to upgrade to. It will
update the relevant compose file tags on the local file system.
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.
`,
ArgsUsage: "<recipe>",
Flags: []cli.Flag{
PatchFlag,
MinorFlag,
MajorFlag,
},
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
bumpType := btoi(Major)*4 + btoi(Minor)*2 + btoi(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.")
}
}
for _, service := range recipe.Config.Services {
catlVersions, err := catalogue.VersionsOfService(recipe.Name, service.Name)
if err != nil {
logrus.Fatal(err)
}
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
logrus.Fatal(err)
}
image := reference.Path(img)
regVersions, err := client.GetRegistryTags(image)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("retrieved '%s' from remote registry for '%s'", regVersions, image)
if strings.Contains(image, "library") {
// ParseNormalizedNamed prepends 'library' to images like nginx:<tag>,
// postgres:<tag>, i.e. images which do not have a username in the
// first position of the string
image = strings.Split(image, "/")[1]
}
semverLikeTag := true
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
logrus.Debugf("'%s' not considered semver-like", img.(reference.NamedTagged).Tag())
semverLikeTag = false
}
tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag())
if err != nil && semverLikeTag {
logrus.Fatal(err)
}
logrus.Debugf("parsed '%s' for '%s'", tag, service.Name)
var compatible []tagcmp.Tag
for _, regVersion := range regVersions {
other, err := tagcmp.Parse(regVersion.Name)
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 && semverLikeTag {
logrus.Info(fmt.Sprintf("no new versions available for '%s', '%s' is the latest", image, tag))
continue // skip on to the next tag and don't update any compose files
}
var compatibleStrings []string
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
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, tag: %s)", service.Name, tag)
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
tag := img.(reference.NamedTagged).Tag()
logrus.Warning(fmt.Sprintf("unable to determine versioning semantics of '%s', listing all tags", tag))
msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
compatibleStrings = []string{}
for _, regVersion := range regVersions {
compatibleStrings = append(compatibleStrings, regVersion.Name)
}
}
prompt := &survey.Select{
Message: msg,
Options: compatibleStrings,
}
if err := survey.AskOne(prompt, &upgradeTag); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.UpdateTag(image, upgradeTag); err != nil {
logrus.Fatal(err)
}
logrus.Infof("tag upgraded from '%s' to '%s' for '%s'", tag.String(), upgradeTag, image)
}
return nil
},
}
-45
View File
@@ -1,45 +0,0 @@
package recipe
import (
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var recipeVersionCommand = &cli.Command{
Name: "versions",
Usage: "List recipe versions",
Aliases: []string{"v"},
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
catalogue, err := catalogue.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 _, serviceVersion := range recipeMeta.Versions {
for tag, meta := range serviceVersion {
for service, serviceMeta := range meta {
table.Append([]string{tag, service, serviceMeta.Image, serviceMeta.Tag, serviceMeta.Digest})
}
}
}
table.SetAutoMergeCells(true)
table.Render()
return nil
},
}
-138
View File
@@ -1,138 +0,0 @@
package server
import (
"context"
"errors"
"os/user"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/server"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var local bool
var localFlag = &cli.BoolFlag{
Name: "local",
Aliases: []string{"L"},
Value: false,
Usage: "Set up the local server",
Destination: &local,
}
var serverAddCommand = &cli.Command{
Name: "add",
Usage: "Add a new server",
Description: `
This command adds a new server that abra will communicate with, to deploy apps.
If "--local" is passed, then Abra assumes that the current local server is
intended as the target server.
Otherwise, you may specify a remote server. The <domain> argument must be a
publicy accessible domain name which points to your server. You should have SSH
access to this server, Abra will assume port 22 and will use your current
system username to make an initial connection. You can use the <user> and
<port> arguments to adjust this.
For example:
abra server add varia.zone glodemodem 12345
Abra will construct the following SSH connection string then:
ssh://globemodem@varia.zone:12345
All communication between Abra and the server will use this SSH connection.
`,
Aliases: []string{"a"},
Flags: []cli.Flag{
localFlag,
},
ArgsUsage: "<domain> [<user>] [<port>]",
Action: func(c *cli.Context) error {
if c.Args().Len() == 1 && !local {
err := errors.New("missing arguments <domain> or '--local'")
internal.ShowSubcommandHelpAndError(c, err)
}
if c.Args().Get(1) != "" && local {
err := errors.New("cannot use '<domain>' and '--local' together")
internal.ShowSubcommandHelpAndError(c, err)
}
domainName := "default"
if local {
if err := server.CreateServerDir(domainName); err != nil {
logrus.Fatal(err)
}
logrus.Info("local server has been added")
return nil
}
domainName = internal.ValidateDomain(c)
var username string
var port string
username = c.Args().Get(1)
if username == "" {
systemUser, err := user.Current()
if err != nil {
logrus.Fatal(err)
}
username = systemUser.Username
}
port = c.Args().Get(2)
if port == "" {
port = "22"
}
store := client.NewDefaultDockerContextStore()
contexts, err := store.Store.List()
if err != nil {
logrus.Fatal(err)
}
for _, context := range contexts {
if context.Name == domainName {
logrus.Fatalf("server at '%s' already exists?", domainName)
}
}
logrus.Debugf("creating context with domain '%s', username '%s' and port '%s'", domainName, username, port)
if err := client.CreateContext(domainName, username, port); err != nil {
logrus.Fatal(err)
}
ctx := context.Background()
cl, err := client.New(domainName)
if err != nil {
logrus.Fatal(err)
}
if _, err := cl.Info(ctx); err != nil {
if strings.Contains(err.Error(), "command not found") {
logrus.Fatalf("docker is not installed on '%s'?", domainName)
} else {
logrus.Fatalf("unable to make a connection to '%s'?", domainName)
}
logrus.Debug(err)
}
logrus.Debugf("remote connection to '%s' is definitely up", domainName)
if err := server.CreateServerDir(domainName); err != nil {
logrus.Fatal(err)
}
logrus.Infof("server at '%s' has been added", domainName)
return nil
},
}
-81
View File
@@ -1,81 +0,0 @@
package server
import (
"context"
"fmt"
"net"
"time"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var serverInitCommand = &cli.Command{
Name: "init",
Usage: "Initialise server for deploying apps",
Aliases: []string{"i"},
HideHelp: true,
ArgsUsage: "<domain>",
Description: `
Initialise swarm mode on the target <domain>.
This initialisation explicitly chooses the "single host swarm" mode which uses
the default IPv4 address as the advertising address. This can be re-configured
later for more advanced use cases.
`,
Action: func(c *cli.Context) error {
domainName := internal.ValidateDomain(c)
cl, err := client.New(domainName)
if err != nil {
return err
}
// 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),
}
// comrade librehosters DNS resolver https://snopyta.org/service/dns/
return d.DialContext(ctx, "udp", freifunkDNS)
},
}
logrus.Debugf("created DNS resolver via '%s'", freifunkDNS)
ctx := context.Background()
ips, err := resolver.LookupIPAddr(ctx, domainName)
if err != nil {
logrus.Fatal(err)
}
if len(ips) == 0 {
return fmt.Errorf("unable to retrieve ipv4 address for %s", domainName)
}
ipv4 := ips[0].IP.To4().String()
initReq := swarm.InitRequest{
ListenAddr: "0.0.0.0:2377",
AdvertiseAddr: ipv4,
}
if _, err := cl.SwarmInit(ctx, initReq); err != nil {
return err
}
logrus.Debugf("initialised swarm on '%s'", domainName)
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
if _, err := cl.NetworkCreate(ctx, "proxy", netOpts); err != nil {
return err
}
logrus.Debug("swarm overlay network 'proxy' created")
return nil
},
}
-59
View File
@@ -1,59 +0,0 @@
package server
import (
"strings"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var serverListCommand = &cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List managed servers",
ArgsUsage: " ",
HideHelp: true,
Action: func(c *cli.Context) error {
dockerContextStore := client.NewDefaultDockerContextStore()
contexts, err := dockerContextStore.Store.List()
if err != nil {
logrus.Fatal(err)
}
tableColumns := []string{"Name", "Connection"}
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 := client.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 {
row = []string{serverName, endpoint}
}
}
if len(row) == 0 {
if serverName == "default" {
row = []string{serverName, "local"}
} else {
row = []string{serverName, "unknown"}
}
}
table.Append(row)
}
return nil
},
}
-264
View File
@@ -1,264 +0,0 @@
package server
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"time"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"github.com/hetznercloud/hcloud-go/hcloud"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var hetznerCloudType string
var hetznerCloudImage string
var hetznerCloudSSHKeys cli.StringSlice
var hetznerCloudLocation string
var hetznerCloudAPIToken string
var serverNewHetznerCloudCommand = &cli.Command{
Name: "hetzner",
Usage: "Create a new Hetzner virtual server",
ArgsUsage: "<name>",
Description: `
Create a new Hetzner virtual server.
This command uses the uses the Hetzner Cloud API bindings to send a server
creation request. You must already have a Hetzner Cloud account and an account
API token before using this command.
Your token can be loaded from the environment using the HCLOUD_TOKEN
environment variable or otherwise passing the "--env/-e" flag.
`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
Usage: "Server type",
Destination: &hetznerCloudType,
Value: "cx11",
},
&cli.StringFlag{
Name: "image",
Aliases: []string{"i"},
Usage: "Image type",
Value: "debian-10",
Destination: &hetznerCloudImage,
},
&cli.StringSliceFlag{
Name: "ssh-keys",
Aliases: []string{"s"},
Usage: "SSH keys",
Destination: &hetznerCloudSSHKeys,
},
&cli.StringFlag{
Name: "location",
Aliases: []string{"l"},
Usage: "Server location",
Value: "hel1",
Destination: &hetznerCloudLocation,
},
&cli.StringFlag{
Name: "token",
Aliases: []string{"T"},
Usage: "Hetzner Cloud API token",
EnvVars: []string{"HCLOUD_TOKEN"},
Destination: &hetznerCloudAPIToken,
},
},
Action: func(c *cli.Context) error {
name := c.Args().First()
if name == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no name provided"))
}
if hetznerCloudAPIToken == "" {
logrus.Fatal("Hetzner Cloud API token is missing")
}
ctx := context.Background()
client := hcloud.NewClient(hcloud.WithToken(hetznerCloudAPIToken))
logrus.Debugf("successfully created hetzner cloud API client")
var sshKeys []*hcloud.SSHKey
for _, sshKey := range c.StringSlice("ssh-keys") {
sshKey, _, err := client.SSHKey.GetByName(ctx, sshKey)
if err != nil {
logrus.Fatal(err)
}
sshKeys = append(sshKeys, sshKey)
}
serverOpts := hcloud.ServerCreateOpts{
Name: name,
ServerType: &hcloud.ServerType{Name: hetznerCloudType},
Image: &hcloud.Image{Name: hetznerCloudImage},
SSHKeys: sshKeys,
Location: &hcloud.Location{Name: hetznerCloudLocation},
}
res, _, err := client.Server.Create(ctx, serverOpts)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("new server '%s' created", name)
tableColumns := []string{"Name", "IPv4", "Root Password"}
table := formatter.CreateTable(tableColumns)
if len(sshKeys) > 0 {
table.Append([]string{name, res.Server.PublicNet.IPv4.IP.String(), "N/A (using SSH keys)"})
} else {
table.Append([]string{name, res.Server.PublicNet.IPv4.IP.String(), res.RootPassword})
}
table.Render()
return nil
},
}
var capsulInstance string
var capsulType string
var capsulImage string
var capsulSSHKey string
var capsulAPIToken string
var serverNewCapsulCommand = &cli.Command{
Name: "capsul",
Usage: "Create a new Capsul virtual server",
ArgsUsage: "<name>",
Description: `
Create a new Capsul virtual server.
This command uses the uses the Capsul API bindings of your chosen instance to
send a server creation request. You must already have an account on your chosen
Capsul instance before using this command.
Your token can be loaded from the environment using the CAPSUL_TOKEN
environment variable or otherwise passing the "--env/-e" flag.
`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "instance",
Aliases: []string{"I"},
Usage: "Capsul instance",
Destination: &capsulInstance,
Value: "yolo.servers.coop",
},
&cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
Usage: "Server type",
Value: "f1-xs",
Destination: &capsulType,
},
&cli.StringFlag{
Name: "image",
Aliases: []string{"i"},
Usage: "Image type",
Value: "debian10",
Destination: &capsulImage,
},
&cli.StringFlag{
Name: "ssh-key",
Aliases: []string{"s"},
Usage: "SSH key",
Value: "",
Destination: &capsulSSHKey,
},
&cli.StringFlag{
Name: "token",
Aliases: []string{"T"},
Usage: "Capsul instance API token",
EnvVars: []string{"CAPSUL_TOKEN"},
Destination: &capsulAPIToken,
},
},
Action: func(c *cli.Context) error {
name := c.Args().First()
if name == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no name provided"))
}
if capsulAPIToken == "" {
logrus.Fatal("Capsul API token is missing")
}
// yep, the response time is quite slow, something to fix on the Capsul side
client := &http.Client{Timeout: 20 * time.Second}
capsulCreateURL := fmt.Sprintf("https://%s/api/capsul/create", capsulInstance)
logrus.Debugf("using '%s' as capsul create url", capsulCreateURL)
values := map[string]string{
"name": name,
"size": capsulType,
"os": capsulImage,
"ssh_key_0": capsulSSHKey,
}
payload, err := json.Marshal(values)
if err != nil {
logrus.Fatal(err)
}
req, err := http.NewRequest("POST", capsulCreateURL, bytes.NewBuffer(payload))
if err != nil {
logrus.Fatal(err)
}
req.Header = http.Header{
"Content-Type": []string{"application/json"},
"Authorization": []string{capsulAPIToken},
}
res, err := client.Do(req)
if err != nil {
logrus.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
body, err := ioutil.ReadAll(res.Body)
if err != nil {
panic(err)
}
logrus.Fatal(string(body))
}
type capsulCreateResponse struct{ ID string }
var resp capsulCreateResponse
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("capsul created with ID: '%s'", resp.ID)
tableColumns := []string{"Name", "ID"}
table := formatter.CreateTable(tableColumns)
table.Append([]string{name, resp.ID})
table.Render()
return nil
},
}
var serverNewCommand = &cli.Command{
Name: "new",
Aliases: []string{"n"},
Usage: "Create a new server using a 3rd party provider",
Description: `
Use a provider plugin to create a new server which can then be used to house a
new Co-op Cloud installation.
`,
ArgsUsage: "<provider>",
Subcommands: []*cli.Command{
serverNewHetznerCloudCommand,
serverNewCapsulCommand,
},
}
-30
View File
@@ -1,30 +0,0 @@
package server
import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var serverRemoveCommand = &cli.Command{
Name: "remove",
Aliases: []string{"rm"},
Usage: "Remove a server",
Description: `
This does not destroy the actual server. It simply removes it from Abra
internal bookkeeping so that it is not managed any more.
`,
HideHelp: true,
Action: func(c *cli.Context) error {
domainName := internal.ValidateDomain(c)
if err := client.DeleteContext(domainName); err != nil {
logrus.Fatal(err)
}
logrus.Infof("server at '%s' has been forgotten", domainName)
return nil
},
}
-26
View File
@@ -1,26 +0,0 @@
package server
import (
"github.com/urfave/cli/v2"
)
// ServerCommand defines the `abra server` command and its subcommands
var ServerCommand = &cli.Command{
Name: "server",
Aliases: []string{"s"},
Usage: "Manage servers",
Description: `
Manage the lifecycle of a server.
These commands support creating new servers using 3rd party integrations,
initialising existing servers to support Co-op Cloud deployments and managing
the connections to those servers.
`,
Subcommands: []*cli.Command{
serverNewCommand,
serverInitCommand,
serverAddCommand,
serverListCommand,
serverRemoveCommand,
},
}
-23
View File
@@ -1,23 +0,0 @@
package cli
import (
"os/exec"
"coopcloud.tech/abra/cli/internal"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
// UpgradeCommand upgrades abra in-place.
var UpgradeCommand = &cli.Command{
Name: "upgrade",
Usage: "Upgrade abra",
Action: func(c *cli.Context) error {
cmd := exec.Command("bash", "-c", "curl -s https://install.abra.coopcloud.tech | bash")
logrus.Debugf("attempting to run '%s'", cmd)
if err := internal.RunCmd(cmd); err != nil {
logrus.Fatal(err)
}
return nil
},
}
-15
View File
@@ -1,15 +0,0 @@
package cli
import (
"github.com/urfave/cli/v2"
)
// VersionCommand prints the version of abra.
var VersionCommand = &cli.Command{
Name: "version",
Usage: "Print version",
Action: func(c *cli.Context) error {
cli.VersionPrinter(c)
return nil
},
}
-24
View File
@@ -1,24 +0,0 @@
// 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 commit of abra.
var Commit string
func main() {
// If not set in the ld-flags
if Version == "" {
Version = "dev"
}
if Commit == "" {
Commit = " "
}
cli.RunApp(Version, Commit)
}
+52
View File
@@ -0,0 +1,52 @@
#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
@@ -0,0 +1,117 @@
#!/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
-40
View File
@@ -1,40 +0,0 @@
module coopcloud.tech/abra
go 1.16
require (
coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb
github.com/AlecAivazis/survey/v2 v2.3.1
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
github.com/docker/cli v20.10.8+incompatible
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v20.10.8+incompatible
github.com/docker/go-units v0.4.0
github.com/go-git/go-git/v5 v5.4.2
github.com/hetznercloud/hcloud-go v1.32.0
github.com/moby/sys/signal v0.5.0
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.8.3
github.com/schultz-is/passgen v1.0.1
github.com/sirupsen/logrus v1.8.1
github.com/urfave/cli/v2 v2.3.0
gotest.tools/v3 v3.0.3
)
require (
github.com/Microsoft/hcsshim v0.8.21 // indirect
github.com/containerd/containerd v1.5.5 // 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/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/moby/sys/mount v0.2.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/runc v1.0.2 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
)
-1180
View File
File diff suppressed because it is too large Load Diff
@@ -2,7 +2,7 @@
version: "3.8"
services:
app:
abra_installer:
image: "nginx:stable"
configs:
- source: abra_conf
@@ -20,7 +20,7 @@ services:
labels:
- "traefik.enable=true"
- "traefik.http.services.abra-installer.loadbalancer.server.port=80"
- "traefik.http.routers.abra-installer.rule=Host(`install.abra.autonomic.zone`,`install.abra.coopcloud.tech`)"
- "traefik.http.routers.abra-installer.rule=Host(`install.abra.autonomic.zone`)"
- "traefik.http.routers.abra-installer.entrypoints=web-secure"
- "traefik.http.routers.abra-installer.tls.certresolver=production"
+34
View File
@@ -0,0 +1,34 @@
#!/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
@@ -1,6 +1,6 @@
server {
listen 80 default_server;
server_name install.abra.autonomic.zone install.abra.coopcloud.tech;
server_name install.abra.autonomic.zone;
location / {
root /var/www/abra-installer;
+50
View File
@@ -0,0 +1,50 @@
.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
@@ -1,85 +0,0 @@
package app
import (
"context"
"fmt"
"strings"
"coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
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
}
// ParseVersionName parses a $STACK_NAME_$SERVICE_NAME service label.
func ParseServiceName(label string) string {
idx := strings.LastIndex(label, "_")
serviceName := label[idx+1:]
logrus.Debugf("parsed '%s' as service name from '%s'", serviceName, label)
return serviceName
}
-500
View File
@@ -1,500 +0,0 @@
// Package catalogue provides ways of interacting with recipe catalogues which
// are JSON data structures which contain meta information about recipes (e.g.
// what versions of the Nextcloud recipe are available?).
package catalogue
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"strings"
"time"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/web"
"github.com/docker/distribution/reference"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus"
)
// RecipeCatalogueURL is the only current recipe catalogue available.
const RecipeCatalogueURL = "https://apps.coopcloud.tech"
// ReposMetadataURL is the recipe repository metadata
const ReposMetadataURL = "https://git.coopcloud.tech/api/v1/orgs/coop-cloud/repos"
// image represents a recipe container image.
type image struct {
Image string `json:"image"`
Rating string `json:"rating"`
Source string `json:"source"`
URL string `json:"url"`
}
// features represent what top-level features a recipe supports (e.g. does this
// recipe support backups?).
type features struct {
Backups string `json:"backups"`
Email string `json:"email"`
Healthcheck string `json:"healthcheck"`
Image image `json:"image"`
Status int `json:"status"`
Tests string `json:"tests"`
}
// tag represents a git tag.
type tag = string
// service represents a service within a recipe.
type service = string
// ServiceMeta represents meta info associated with a service.
type ServiceMeta struct {
Digest string `json:"digest"`
Image string `json:"image"`
Tag string `json:"tag"`
}
// RecipeVersions are the versions associated with a recipe.
type RecipeVersions []map[tag]map[service]ServiceMeta
// RecipeMeta represents metadata for a recipe in the abra catalogue.
type RecipeMeta struct {
Category string `json:"category"`
DefaultBranch string `json:"default_branch"`
Description string `json:"description"`
Features features `json:"features"`
Icon string `json:"icon"`
Name string `json:"name"`
Repository string `json:"repository"`
Versions RecipeVersions `json:"versions"`
Website string `json:"website"`
}
// LatestVersion returns the latest version of a recipe.
func (r RecipeMeta) LatestVersion() string {
var version string
// apps.json versions are sorted so the last key is latest
latest := r.Versions[len(r.Versions)-1]
for tag := range latest {
version = tag
}
logrus.Debugf("choosing '%s' as latest version of '%s'", version, r.Name)
return version
}
// Name represents a recipe name.
type Name = string
// RecipeCatalogue represents the entire recipe catalogue.
type RecipeCatalogue map[Name]RecipeMeta
// Flatten converts AppCatalogue to slice
func (r RecipeCatalogue) Flatten() []RecipeMeta {
recipes := make([]RecipeMeta, 0, len(r))
for name := range r {
recipes = append(recipes, r[name])
}
return recipes
}
// ByRecipeName sorts recipes by name.
type ByRecipeName []RecipeMeta
func (r ByRecipeName) Len() int { return len(r) }
func (r ByRecipeName) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r ByRecipeName) Less(i, j int) bool {
return strings.ToLower(r[i].Name) < strings.ToLower(r[j].Name)
}
// recipeCatalogueFSIsLatest checks whether the recipe catalogue stored locally
// is up to date.
func recipeCatalogueFSIsLatest() (bool, error) {
httpClient := &http.Client{Timeout: web.Timeout}
res, err := httpClient.Head(RecipeCatalogueURL)
if err != nil {
return false, err
}
lastModified := res.Header["Last-Modified"][0]
parsed, err := time.Parse(time.RFC1123, lastModified)
if err != nil {
return false, err
}
info, err := os.Stat(config.APPS_JSON)
if err != nil {
if os.IsNotExist(err) {
logrus.Debugf("no recipe catalogue found in file system cache")
return false, nil
}
return false, err
}
localModifiedTime := info.ModTime().Unix()
remoteModifiedTime := parsed.Unix()
if localModifiedTime < remoteModifiedTime {
logrus.Debug("file system cached recipe catalogue is out-of-date")
return false, nil
}
logrus.Debug("file system cached recipe catalogue is up-to-date")
return true, nil
}
// ReadRecipeCatalogue reads the recipe catalogue.
func ReadRecipeCatalogue() (RecipeCatalogue, error) {
recipes := make(RecipeCatalogue)
recipeFSIsLatest, err := recipeCatalogueFSIsLatest()
if err != nil {
return nil, err
}
if !recipeFSIsLatest {
logrus.Debugf("reading recipe catalogue from web to get latest")
if err := readRecipeCatalogueWeb(&recipes); err != nil {
return nil, err
}
return recipes, nil
}
logrus.Debugf("reading recipe catalogue from file system cache to get latest")
if err := readRecipeCatalogueFS(&recipes); err != nil {
return nil, err
}
return recipes, nil
}
// readRecipeCatalogueFS reads the catalogue from the file system.
func readRecipeCatalogueFS(target interface{}) error {
recipesJSONFS, err := ioutil.ReadFile(config.APPS_JSON)
if err != nil {
return err
}
if err := json.Unmarshal(recipesJSONFS, &target); err != nil {
return err
}
logrus.Debugf("read recipe catalogue from file system cache in '%s'", config.APPS_JSON)
return nil
}
// readRecipeCatalogueWeb reads the catalogue from the web.
func readRecipeCatalogueWeb(target interface{}) error {
if err := web.ReadJSON(RecipeCatalogueURL, &target); err != nil {
return err
}
recipesJSON, err := json.MarshalIndent(target, "", " ")
if err != nil {
return err
}
if err := ioutil.WriteFile(config.APPS_JSON, recipesJSON, 0644); err != nil {
return err
}
logrus.Debugf("read recipe catalogue from web at '%s'", RecipeCatalogueURL)
return nil
}
// VersionsOfService lists the version of a service.
func VersionsOfService(recipe, serviceName string) ([]string, error) {
catalogue, err := ReadRecipeCatalogue()
if err != nil {
return nil, err
}
rec, ok := catalogue[recipe]
if !ok {
return nil, fmt.Errorf("recipe '%s' does not exist?", recipe)
}
versions := []string{}
alreadySeen := make(map[string]bool)
for _, serviceVersion := range rec.Versions {
for tag := range serviceVersion {
if _, ok := alreadySeen[tag]; !ok {
alreadySeen[tag] = true
versions = append(versions, tag)
}
}
}
logrus.Debugf("detected versions '%s' for '%s'", strings.Join(versions, ", "), recipe)
return versions, nil
}
// GetRecipeMeta retrieves the recipe metadata from the recipe catalogue.
func GetRecipeMeta(recipeName string) (RecipeMeta, error) {
catl, err := ReadRecipeCatalogue()
if err != nil {
return RecipeMeta{}, err
}
recipeMeta, ok := catl[recipeName]
if !ok {
err := fmt.Errorf("recipe '%s' does not exist?", recipeName)
return RecipeMeta{}, err
}
if err := recipe.EnsureExists(recipeName); err != nil {
return RecipeMeta{}, err
}
logrus.Debugf("recipe metadata retrieved for '%s'", recipeName)
return recipeMeta, nil
}
// RepoMeta is a single recipe repo metadata.
type RepoMeta struct {
ID int `json:"id"`
Owner Owner
Name string `json:"name"`
FullName string `json:"full_name"`
Description string `json:"description"`
Empty bool `json:"empty"`
Private bool `json:"private"`
Fork bool `json:"fork"`
Template bool `json:"template"`
Parent interface{} `json:"parent"`
Mirror bool `json:"mirror"`
Size int `json:"size"`
HTMLURL string `json:"html_url"`
SSHURL string `json:"ssh_url"`
CloneURL string `json:"clone_url"`
OriginalURL string `json:"original_url"`
Website string `json:"website"`
StarsCount int `json:"stars_count"`
ForksCount int `json:"forks_count"`
WatchersCount int `json:"watchers_count"`
OpenIssuesCount int `json:"open_issues_count"`
OpenPRCount int `json:"open_pr_counter"`
ReleaseCounter int `json:"release_counter"`
DefaultBranch string `json:"default_branch"`
Archived bool `json:"archived"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Permissions Permissions
HasIssues bool `json:"has_issues"`
InternalTracker InternalTracker
HasWiki bool `json:"has_wiki"`
HasPullRequests bool `json:"has_pull_requests"`
HasProjects bool `json:"has_projects"`
IgnoreWhitespaceConflicts bool `json:"ignore_whitespace_conflicts"`
AllowMergeCommits bool `json:"allow_merge_commits"`
AllowRebase bool `json:"allow_rebase"`
AllowRebaseExplicit bool `json:"allow_rebase_explicit"`
AllowSquashMerge bool `json:"allow_squash_merge"`
AvatarURL string `json:"avatar_url"`
Internal bool `json:"internal"`
MirrorInterval string `json:"mirror_interval"`
}
// Owner is the repo organisation owner metadata.
type Owner struct {
ID int `json:"id"`
Login string `json:"login"`
FullName string `json:"full_name"`
Email string `json:"email"`
AvatarURL string `json:"avatar_url"`
Language string `json:"language"`
IsAdmin bool `json:"is_admin"`
LastLogin string `json:"last_login"`
Created string `json:"created"`
Restricted bool `json:"restricted"`
Username string `json:"username"`
}
// Permissions is perms metadata for a repo.
type Permissions struct {
Admin bool `json:"admin"`
Push bool `json:"push"`
Pull bool `json:"pull"`
}
// InternalTracker is issue tracker metadata for a repo.
type InternalTracker struct {
EnableTimeTracker bool `json:"enable_time_tracker"`
AllowOnlyContributorsToTrackTime bool `json:"allow_only_contributors_to_track_time"`
EnableIssuesDependencies bool `json:"enable_issue_dependencies"`
}
// RepoCatalogue represents all the recipe repo metadata.
type RepoCatalogue map[string]RepoMeta
// ReadReposMetadata retrieves coop-cloud/... repo metadata from Gitea.
func ReadReposMetadata() (RepoCatalogue, error) {
reposMeta := make(RepoCatalogue)
pageIdx := 1
for {
var reposList []RepoMeta
pagedURL := fmt.Sprintf("%s?page=%v", ReposMetadataURL, pageIdx)
logrus.Debugf("fetching repo metadata from '%s'", pagedURL)
if err := web.ReadJSON(pagedURL, &reposList); err != nil {
return reposMeta, err
}
if len(reposList) == 0 {
break
}
for idx, repo := range reposList {
reposMeta[repo.Name] = reposList[idx]
}
pageIdx++
}
return reposMeta, nil
}
// GetRecipeVersions retrieves all recipe versions.
func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
versions := RecipeVersions{}
recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName)
logrus.Debugf("attempting to open git repository in '%s'", recipeDir)
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return versions, err
}
worktree, err := repo.Worktree()
if err != nil {
logrus.Fatal(err)
}
gitTags, err := repo.Tags()
if err != nil {
return versions, err
}
if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
tag := strings.TrimPrefix(string(ref.Name()), "refs/tags/")
logrus.Debugf("processing '%s' for '%s'", tag, recipeName)
checkOutOpts := &git.CheckoutOptions{
Create: false,
Force: true,
Branch: plumbing.ReferenceName(ref.Name()),
}
if err := worktree.Checkout(checkOutOpts); err != nil {
logrus.Debugf("failed to check out '%s' in '%s'", tag, recipeDir)
return err
}
logrus.Debugf("successfully checked out '%s' in '%s'", ref.Name(), recipeDir)
recipe, err := recipe.Get(recipeName)
if err != nil {
return err
}
versionMeta := make(map[string]ServiceMeta)
for _, service := range recipe.Config.Services {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return err
}
path := reference.Path(img)
if strings.Contains(path, "library") {
path = strings.Split(path, "/")[1]
}
digest, err := client.GetTagDigest(img)
if err != nil {
return err
}
versionMeta[service.Name] = ServiceMeta{
Digest: digest,
Image: path,
Tag: img.(reference.NamedTagged).Tag(),
}
logrus.Debugf("collecting digest: '%s', image: '%s', tag: '%s'", digest, path, tag)
}
versions = append(versions, map[string]map[string]ServiceMeta{tag: versionMeta})
return nil
}); err != nil {
return versions, err
}
branch := "master"
if _, err := repo.Branch("master"); err != nil {
if _, err := repo.Branch("main"); err != nil {
logrus.Debugf("failed to select branch in '%s'", recipeDir)
logrus.Fatal(err)
}
branch = "main"
}
refName := fmt.Sprintf("refs/heads/%s", branch)
checkOutOpts := &git.CheckoutOptions{
Create: false,
Force: true,
Branch: plumbing.ReferenceName(refName),
}
if err := worktree.Checkout(checkOutOpts); err != nil {
logrus.Debugf("failed to check out '%s' in '%s'", branch, recipeDir)
logrus.Fatal(err)
}
logrus.Debugf("switched back to '%s' in '%s'", branch, recipeDir)
logrus.Debugf("collected '%s' for '%s'", versions, recipeName)
return versions, nil
}
// GetRecipeCatalogueVersions list the recipe versions listed in the recipe catalogue.
func GetRecipeCatalogueVersions(recipeName string) ([]string, error) {
var versions []string
catl, err := ReadRecipeCatalogue()
if err != nil {
return versions, err
}
if recipeMeta, exists := catl[recipeName]; exists {
for _, versionMeta := range recipeMeta.Versions {
for tag := range versionMeta {
versions = append(versions, tag)
}
}
}
return versions, nil
}
-54
View File
@@ -1,54 +0,0 @@
// Package client provides Docker client initiatialisation functions.
package client
import (
"net/http"
"os"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
// New initiates a new Docker client.
func New(contextName string) (*client.Client, error) {
context, err := GetContext(contextName)
if err != nil {
return nil, err
}
ctxEndpoint, err := GetContextEndpoint(context)
if err != nil {
return nil, err
}
helper := newConnectionHelper(ctxEndpoint)
httpClient := &http.Client{
// No tls, no proxy
Transport: &http.Transport{
DialContext: helper.Dialer,
},
}
var clientOpts []client.Opt
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 {
logrus.Fatalf("unable to create Docker client: %s", err)
}
logrus.Debugf("created client for '%s'", contextName)
return cl, nil
}
-45
View File
@@ -1,45 +0,0 @@
package client
import (
"github.com/docker/cli/cli/connhelper"
"github.com/docker/cli/cli/context/docker"
dCliContextStore "github.com/docker/cli/cli/context/store"
dClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
func newConnectionHelper(daemonURL string) *connhelper.ConnectionHelper {
helper, err := connhelper.GetConnectionHelper(daemonURL)
if err != nil {
logrus.Fatal(err)
}
return helper
}
func getDockerEndpoint(host string) (docker.Endpoint, error) {
skipTLSVerify := false
ep := docker.Endpoint{
EndpointMeta: docker.EndpointMeta{
Host: host,
SkipTLSVerify: skipTLSVerify,
},
}
// try to resolve a docker client, validating the configuration
opts, err := ep.ClientOpts()
if err != nil {
return docker.Endpoint{}, err
}
if _, err := dClient.NewClientWithOpts(opts...); err != nil {
return docker.Endpoint{}, err
}
return ep, nil
}
func getDockerEndpointMetadataAndTLS(host string) (docker.EndpointMeta, *dCliContextStore.EndpointTLSData, error) {
ep, err := getDockerEndpoint(host)
if err != nil {
return docker.EndpointMeta{}, nil, err
}
return ep.EndpointMeta, ep.TLSData.ToStoreTLSData(), nil
}
-191
View File
@@ -1,191 +0,0 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2013-2017 Docker, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-7
View File
@@ -1,7 +0,0 @@
# github.com/docker/cli/cli/command/container
Due to this literally just being copy-pasted from the lib, the Apache license
will be posted in this folder. Small edits to the source code have been to
function names and parts we don't need deleted.
Same vibe as [../convert](../convert).
-130
View File
@@ -1,130 +0,0 @@
package container
import (
"context"
"errors"
"fmt"
"io"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
apiclient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
func RunExec(dockerCli command.Cli, client *apiclient.Client, containerID string, execConfig *types.ExecConfig) error {
ctx := context.Background()
// We need to check the tty _before_ we do the ContainerExecCreate, because
// otherwise if we error out we will leak execIDs on the server (and
// there's no easy way to clean those up). But also in order to make "not
// exist" errors take precedence we do a dummy inspect first.
if _, err := client.ContainerInspect(ctx, containerID); err != nil {
return err
}
if !execConfig.Detach {
if err := dockerCli.In().CheckTty(execConfig.AttachStdin, execConfig.Tty); err != nil {
return err
}
}
response, err := client.ContainerExecCreate(ctx, containerID, *execConfig)
if err != nil {
return err
}
execID := response.ID
if execID == "" {
return errors.New("exec ID empty")
}
if execConfig.Detach {
execStartCheck := types.ExecStartCheck{
Detach: execConfig.Detach,
Tty: execConfig.Tty,
}
return client.ContainerExecStart(ctx, execID, execStartCheck)
}
return interactiveExec(ctx, dockerCli, client, execConfig, execID)
}
func interactiveExec(ctx context.Context, dockerCli command.Cli, client *apiclient.Client,
execConfig *types.ExecConfig, execID string) error {
// Interactive exec requested.
var (
out, stderr io.Writer
in io.ReadCloser
)
if execConfig.AttachStdin {
in = dockerCli.In()
}
if execConfig.AttachStdout {
out = dockerCli.Out()
}
if execConfig.AttachStderr {
if execConfig.Tty {
stderr = dockerCli.Out()
} else {
stderr = dockerCli.Err()
}
}
execStartCheck := types.ExecStartCheck{
Tty: execConfig.Tty,
}
resp, err := client.ContainerExecAttach(ctx, execID, execStartCheck)
if err != nil {
return err
}
defer resp.Close()
errCh := make(chan error, 1)
go func() {
defer close(errCh)
errCh <- func() error {
streamer := hijackedIOStreamer{
streams: dockerCli,
inputStream: in,
outputStream: out,
errorStream: stderr,
resp: resp,
tty: execConfig.Tty,
detachKeys: execConfig.DetachKeys,
}
return streamer.stream(ctx)
}()
}()
if execConfig.Tty && dockerCli.In().IsTerminal() {
if err := MonitorTtySize(ctx, client, dockerCli, execID, true); err != nil {
fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err)
}
}
if err := <-errCh; err != nil {
logrus.Debugf("Error hijack: %s", err)
return err
}
return getExecExitStatus(ctx, client, execID)
}
func getExecExitStatus(ctx context.Context, client apiclient.ContainerAPIClient, execID string) error {
resp, err := client.ContainerExecInspect(ctx, execID)
if err != nil {
// If we can't connect, then the daemon probably died.
if !apiclient.IsErrConnectionFailed(err) {
return err
}
return cli.StatusError{StatusCode: -1}
}
status := resp.ExitCode
if status != 0 {
return cli.StatusError{StatusCode: status}
}
return nil
}
-208
View File
@@ -1,208 +0,0 @@
package container
import (
"context"
"fmt"
"io"
"runtime"
"sync"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/stdcopy"
"github.com/moby/term"
"github.com/sirupsen/logrus"
)
// The default escape key sequence: ctrl-p, ctrl-q
// TODO: This could be moved to `pkg/term`.
var defaultEscapeKeys = []byte{16, 17}
// A hijackedIOStreamer handles copying input to and output from streams to the
// connection.
type hijackedIOStreamer struct {
streams command.Streams
inputStream io.ReadCloser
outputStream io.Writer
errorStream io.Writer
resp types.HijackedResponse
tty bool
detachKeys string
}
// stream handles setting up the IO and then begins streaming stdin/stdout
// to/from the hijacked connection, blocking until it is either done reading
// output, the user inputs the detach key sequence when in TTY mode, or when
// the given context is cancelled.
func (h *hijackedIOStreamer) stream(ctx context.Context) error {
restoreInput, err := h.setupInput()
if err != nil {
return fmt.Errorf("unable to setup input stream: %s", err)
}
defer restoreInput()
outputDone := h.beginOutputStream(restoreInput)
inputDone, detached := h.beginInputStream(restoreInput)
select {
case err := <-outputDone:
return err
case <-inputDone:
// Input stream has closed.
if h.outputStream != nil || h.errorStream != nil {
// Wait for output to complete streaming.
select {
case err := <-outputDone:
return err
case <-ctx.Done():
return ctx.Err()
}
}
return nil
case err := <-detached:
// Got a detach key sequence.
return err
case <-ctx.Done():
return ctx.Err()
}
}
func (h *hijackedIOStreamer) setupInput() (restore func(), err error) {
if h.inputStream == nil || !h.tty {
// No need to setup input TTY.
// The restore func is a nop.
return func() {}, nil
}
if err := setRawTerminal(h.streams); err != nil {
return nil, fmt.Errorf("unable to set IO streams as raw terminal: %s", err)
}
// Use sync.Once so we may call restore multiple times but ensure we
// only restore the terminal once.
var restoreOnce sync.Once
restore = func() {
restoreOnce.Do(func() {
restoreTerminal(h.streams, h.inputStream)
})
}
// Wrap the input to detect detach escape sequence.
// Use default escape keys if an invalid sequence is given.
escapeKeys := defaultEscapeKeys
if h.detachKeys != "" {
customEscapeKeys, err := term.ToBytes(h.detachKeys)
if err != nil {
logrus.Warnf("invalid detach escape keys, using default: %s", err)
} else {
escapeKeys = customEscapeKeys
}
}
h.inputStream = ioutils.NewReadCloserWrapper(term.NewEscapeProxy(h.inputStream, escapeKeys), h.inputStream.Close)
return restore, nil
}
func (h *hijackedIOStreamer) beginOutputStream(restoreInput func()) <-chan error {
if h.outputStream == nil && h.errorStream == nil {
// There is no need to copy output.
return nil
}
outputDone := make(chan error)
go func() {
var err error
// When TTY is ON, use regular copy
if h.outputStream != nil && h.tty {
_, err = io.Copy(h.outputStream, h.resp.Reader)
// We should restore the terminal as soon as possible
// once the connection ends so any following print
// messages will be in normal type.
restoreInput()
} else {
_, err = stdcopy.StdCopy(h.outputStream, h.errorStream, h.resp.Reader)
}
logrus.Debug("[hijack] End of stdout")
if err != nil {
logrus.Debugf("Error receiveStdout: %s", err)
}
outputDone <- err
}()
return outputDone
}
func (h *hijackedIOStreamer) beginInputStream(restoreInput func()) (doneC <-chan struct{}, detachedC <-chan error) {
inputDone := make(chan struct{})
detached := make(chan error)
go func() {
if h.inputStream != nil {
_, err := io.Copy(h.resp.Conn, h.inputStream)
// We should restore the terminal as soon as possible
// once the connection ends so any following print
// messages will be in normal type.
restoreInput()
logrus.Debug("[hijack] End of stdin")
if _, ok := err.(term.EscapeError); ok {
detached <- err
return
}
if err != nil {
// This error will also occur on the receive
// side (from stdout) where it will be
// propagated back to the caller.
logrus.Debugf("Error sendStdin: %s", err)
}
}
if err := h.resp.CloseWrite(); err != nil {
logrus.Debugf("Couldn't send EOF: %s", err)
}
close(inputDone)
}()
return inputDone, detached
}
func setRawTerminal(streams command.Streams) error {
if err := streams.In().SetRawTerminal(); err != nil {
return err
}
return streams.Out().SetRawTerminal()
}
// nolint: unparam
func restoreTerminal(streams command.Streams, in io.Closer) error {
streams.In().RestoreTerminal()
streams.Out().RestoreTerminal()
// WARNING: DO NOT REMOVE THE OS CHECKS !!!
// For some reason this Close call blocks on darwin..
// As the client exits right after, simply discard the close
// until we find a better solution.
//
// This can also cause the client on Windows to get stuck in Win32 CloseHandle()
// in some cases. See https://github.com/docker/docker/issues/28267#issuecomment-288237442
// Tracked internally at Microsoft by VSO #11352156. In the
// Windows case, you hit this if you are using the native/v2 console,
// not the "legacy" console, and you start the client in a new window. eg
// `start docker run --rm -it microsoft/nanoserver cmd /s /c echo foobar`
// will hang. Remove start, and it won't repro.
if in != nil && runtime.GOOS != "darwin" && runtime.GOOS != "windows" {
return in.Close()
}
return nil
}
-98
View File
@@ -1,98 +0,0 @@
package container
import (
"context"
"fmt"
"os"
gosignal "os/signal"
"runtime"
"time"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
apiclient "github.com/docker/docker/client"
"github.com/moby/sys/signal"
"github.com/sirupsen/logrus"
)
// resizeTtyTo resizes tty to specific height and width
func resizeTtyTo(ctx context.Context, client client.ContainerAPIClient, id string, height, width uint, isExec bool) error {
if height == 0 && width == 0 {
return nil
}
options := types.ResizeOptions{
Height: height,
Width: width,
}
var err error
if isExec {
err = client.ContainerExecResize(ctx, id, options)
} else {
err = client.ContainerResize(ctx, id, options)
}
if err != nil {
logrus.Debugf("Error resize: %s\r", err)
}
return err
}
// resizeTty is to resize the tty with cli out's tty size
func resizeTty(ctx context.Context, client *apiclient.Client, cli command.Cli, id string, isExec bool) error {
height, width := cli.Out().GetTtySize()
return resizeTtyTo(ctx, client, id, height, width, isExec)
}
// initTtySize is to init the tty's size to the same as the window, if there is an error, it will retry 5 times.
func initTtySize(ctx context.Context, client *apiclient.Client, cli command.Cli, id string, isExec bool, resizeTtyFunc func(ctx context.Context, client *apiclient.Client, cli command.Cli, id string, isExec bool) error) {
rttyFunc := resizeTtyFunc
if rttyFunc == nil {
rttyFunc = resizeTty
}
if err := rttyFunc(ctx, client, cli, id, isExec); err != nil {
go func() {
var err error
for retry := 0; retry < 5; retry++ {
time.Sleep(10 * time.Millisecond)
if err = rttyFunc(ctx, client, cli, id, isExec); err == nil {
break
}
}
if err != nil {
fmt.Fprintln(cli.Err(), "failed to resize tty, using default size")
}
}()
}
}
// MonitorTtySize updates the container tty size when the terminal tty changes size
func MonitorTtySize(ctx context.Context, client *apiclient.Client, cli command.Cli, id string, isExec bool) error {
initTtySize(ctx, client, cli, id, isExec, resizeTty)
if runtime.GOOS == "windows" {
go func() {
prevH, prevW := cli.Out().GetTtySize()
for {
time.Sleep(time.Millisecond * 250)
h, w := cli.Out().GetTtySize()
if prevW != w || prevH != h {
resizeTty(ctx, client, cli, id, isExec)
}
prevH = h
prevW = w
}
}()
} else {
sigchan := make(chan os.Signal, 1)
gosignal.Notify(sigchan, signal.SIGWINCH)
go func() {
for range sigchan {
resizeTty(ctx, client, cli, id, isExec)
}
}()
}
return nil
}
-128
View File
@@ -1,128 +0,0 @@
package client
import (
"errors"
"fmt"
command "github.com/docker/cli/cli/command"
dConfig "github.com/docker/cli/cli/config"
context "github.com/docker/cli/cli/context"
"github.com/docker/cli/cli/context/docker"
contextStore "github.com/docker/cli/cli/context/store"
"github.com/moby/term"
"github.com/sirupsen/logrus"
)
type Context = contextStore.Metadata
func CreateContext(contextName string, user string, port string) error {
host := contextName
if user != "" {
host = fmt.Sprintf("%s@%s", user, host)
}
if port != "" {
host = fmt.Sprintf("%s:%s", host, port)
}
host = fmt.Sprintf("ssh://%s", host)
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 := NewDefaultDockerContextStore()
contextMetadata := contextStore.Metadata{
Endpoints: make(map[string]interface{}),
Name: name,
}
contextTLSData := contextStore.ContextTLSData{
Endpoints: make(map[string]contextStore.EndpointTLSData),
}
dockerEP, dockerTLS, err := 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
}
// remove any context that might be loaded
// TODO: Check if the context we are removing is the active one rather than doing it all the time
cfg := dConfig.LoadDefaultConfigFile(nil)
cfg.CurrentContext = ""
if err := cfg.Save(); err != nil {
return err
}
return NewDefaultDockerContextStore().Remove(name)
}
func GetContext(contextName string) (contextStore.Metadata, error) {
ctx, err := NewDefaultDockerContextStore().GetMetadata(contextName)
if err != nil {
return contextStore.Metadata{}, err
}
return ctx, nil
}
func GetContextEndpoint(ctx contextStore.Metadata) (string, error) {
// safe to use docker key hardcoded since abra doesn't use k8s... yet...
endpointmeta, ok := ctx.Endpoints["docker"].(context.EndpointMetaBase)
if !ok {
err := errors.New("context lacks Docker endpoint")
return "", err
}
return endpointmeta.Host, nil
}
func newContextStore(dir string, config contextStore.Config) contextStore.Store {
return contextStore.New(dir, config)
}
func NewDefaultDockerContextStore() *command.ContextStoreWithDefault {
// Grabbing the stderr from Docker commands
// Much easier to fit this into the code we are using to replicate docker cli commands
_, _, stderr := term.StdStreams()
// TODO: Look into custom docker configs in case users want that
dockerConfig := dConfig.LoadDefaultConfigFile(stderr)
contextDir := dConfig.ContextStoreDir()
storeConfig := command.DefaultContextStoreConfig()
store := newContextStore(contextDir, storeConfig)
dockerContextStore := &command.ContextStoreWithDefault{
Store: store,
Resolver: func() (*command.DefaultContext, error) {
// nil for the Opts because it works without it and its a cli thing
return command.ResolveDefaultContext(nil, dockerConfig, storeConfig, stderr)
},
}
return dockerContextStore
}
-52
View File
@@ -1,52 +0,0 @@
package client_test
import (
"testing"
"coopcloud.tech/abra/pkg/client"
dContext "github.com/docker/cli/cli/context"
dCliContextStore "github.com/docker/cli/cli/context/store"
)
type TestContext struct {
context dCliContextStore.Metadata
expected_endpoint string
}
func dockerContext(host, key string) TestContext {
dockerContext := dCliContextStore.Metadata{
Name: "foo",
Metadata: nil,
Endpoints: map[string]interface{}{
key: dContext.EndpointMetaBase{
Host: host,
SkipTLSVerify: false,
},
},
}
return TestContext{
context: dockerContext,
expected_endpoint: host,
}
}
func TestGetContextEndpoint(t *testing.T) {
var testDockerContexts = []TestContext{
dockerContext("ssh://foobar", "docker"),
dockerContext("ssh://foobar", "k8"),
}
for _, context := range testDockerContexts {
endpoint, err := client.GetContextEndpoint(context.context)
if err != nil {
if err.Error() != "context lacks Docker endpoint" {
t.Error(err)
}
} else {
if endpoint != context.expected_endpoint {
t.Errorf("did not get correct context endpoint. Expected: %s, received: %s", context.expected_endpoint, endpoint)
}
}
}
}
-191
View File
@@ -1,191 +0,0 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2013-2017 Docker, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-10
View File
@@ -1,10 +0,0 @@
# github.com/docker/cli/cli/compose/convert
DISCLAIMER: This is like the entire `github.com/docker/cli/cli/compose/convert`
package. This should be an easy import but importing it creates DEPENDENCY
HELL. I tried for an hour to fix it but it would work. TRY TO FIX AT YOUR OWN
RISK!!!
Due to this literally just being copy-pasted from the lib, the Apache license
will be posted in this folder. Small edits to the source code have been to
function names and parts we don't need deleted.
-199
View File
@@ -1,199 +0,0 @@
package convert
import (
"io/ioutil"
"strings"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types"
networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm"
)
const (
// LabelNamespace is the label used to track stack resources
LabelNamespace = "com.docker.stack.namespace"
)
// Namespace mangles names by prepending the name
type Namespace struct {
name string
}
// Scope prepends the namespace to a name
func (n Namespace) Scope(name string) string {
return n.name + "_" + name
}
// Descope returns the name without the namespace prefix
func (n Namespace) Descope(name string) string {
return strings.TrimPrefix(name, n.name+"_")
}
// Name returns the name of the namespace
func (n Namespace) Name() string {
return n.name
}
// NewNamespace returns a new Namespace for scoping of names
func NewNamespace(name string) Namespace {
return Namespace{name: name}
}
// AddStackLabel returns labels with the namespace label added
func AddStackLabel(namespace Namespace, labels map[string]string) map[string]string {
if labels == nil {
labels = make(map[string]string)
}
labels[LabelNamespace] = namespace.name
return labels
}
type networkMap map[string]composetypes.NetworkConfig
// Networks from the compose-file type to the engine API type
func Networks(namespace Namespace, networks networkMap, servicesNetworks map[string]struct{}) (map[string]types.NetworkCreate, []string) {
if networks == nil {
networks = make(map[string]composetypes.NetworkConfig)
}
externalNetworks := []string{}
result := make(map[string]types.NetworkCreate)
for internalName := range servicesNetworks {
network := networks[internalName]
if network.External.External {
externalNetworks = append(externalNetworks, network.Name)
continue
}
createOpts := types.NetworkCreate{
Labels: AddStackLabel(namespace, network.Labels),
Driver: network.Driver,
Options: network.DriverOpts,
Internal: network.Internal,
Attachable: network.Attachable,
}
if network.Ipam.Driver != "" || len(network.Ipam.Config) > 0 {
createOpts.IPAM = &networktypes.IPAM{}
}
if network.Ipam.Driver != "" {
createOpts.IPAM.Driver = network.Ipam.Driver
}
for _, ipamConfig := range network.Ipam.Config {
config := networktypes.IPAMConfig{
Subnet: ipamConfig.Subnet,
}
createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
}
networkName := namespace.Scope(internalName)
if network.Name != "" {
networkName = network.Name
}
result[networkName] = createOpts
}
return result, externalNetworks
}
// Secrets converts secrets from the Compose type to the engine API type
func Secrets(namespace Namespace, secrets map[string]composetypes.SecretConfig) ([]swarm.SecretSpec, error) {
result := []swarm.SecretSpec{}
for name, secret := range secrets {
if secret.External.External {
continue
}
var obj swarmFileObject
var err error
if secret.Driver != "" {
obj = driverObjectConfig(namespace, name, composetypes.FileObjectConfig(secret))
} else {
obj, err = fileObjectConfig(namespace, name, composetypes.FileObjectConfig(secret))
}
if err != nil {
return nil, err
}
spec := swarm.SecretSpec{Annotations: obj.Annotations, Data: obj.Data}
if secret.Driver != "" {
spec.Driver = &swarm.Driver{
Name: secret.Driver,
Options: secret.DriverOpts,
}
}
if secret.TemplateDriver != "" {
spec.Templating = &swarm.Driver{
Name: secret.TemplateDriver,
}
}
result = append(result, spec)
}
return result, nil
}
// Configs converts config objects from the Compose type to the engine API type
func Configs(namespace Namespace, configs map[string]composetypes.ConfigObjConfig) ([]swarm.ConfigSpec, error) {
result := []swarm.ConfigSpec{}
for name, config := range configs {
if config.External.External {
continue
}
obj, err := fileObjectConfig(namespace, name, composetypes.FileObjectConfig(config))
if err != nil {
return nil, err
}
spec := swarm.ConfigSpec{Annotations: obj.Annotations, Data: obj.Data}
if config.TemplateDriver != "" {
spec.Templating = &swarm.Driver{
Name: config.TemplateDriver,
}
}
result = append(result, spec)
}
return result, nil
}
type swarmFileObject struct {
Annotations swarm.Annotations
Data []byte
}
func driverObjectConfig(namespace Namespace, name string, obj composetypes.FileObjectConfig) swarmFileObject {
if obj.Name != "" {
name = obj.Name
} else {
name = namespace.Scope(name)
}
return swarmFileObject{
Annotations: swarm.Annotations{
Name: name,
Labels: AddStackLabel(namespace, obj.Labels),
},
Data: []byte{},
}
}
func fileObjectConfig(namespace Namespace, name string, obj composetypes.FileObjectConfig) (swarmFileObject, error) {
data, err := ioutil.ReadFile(obj.File)
if err != nil {
return swarmFileObject{}, err
}
if obj.Name != "" {
name = obj.Name
} else {
name = namespace.Scope(name)
}
return swarmFileObject{
Annotations: swarm.Annotations{
Name: name,
Labels: AddStackLabel(namespace, obj.Labels),
},
Data: data,
}, nil
}
-171
View File
@@ -1,171 +0,0 @@
package convert
import (
"testing"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/fs"
)
func TestNamespaceScope(t *testing.T) {
scoped := Namespace{name: "foo"}.Scope("bar")
assert.Check(t, is.Equal("foo_bar", scoped))
}
func TestAddStackLabel(t *testing.T) {
labels := map[string]string{
"something": "labeled",
}
actual := AddStackLabel(Namespace{name: "foo"}, labels)
expected := map[string]string{
"something": "labeled",
LabelNamespace: "foo",
}
assert.Check(t, is.DeepEqual(expected, actual))
}
func TestNetworks(t *testing.T) {
namespace := Namespace{name: "foo"}
serviceNetworks := map[string]struct{}{
"normal": {},
"outside": {},
"default": {},
"attachablenet": {},
"named": {},
}
source := networkMap{
"normal": composetypes.NetworkConfig{
Driver: "overlay",
DriverOpts: map[string]string{
"opt": "value",
},
Ipam: composetypes.IPAMConfig{
Driver: "driver",
Config: []*composetypes.IPAMPool{
{
Subnet: "10.0.0.0",
},
},
},
Labels: map[string]string{
"something": "labeled",
},
},
"outside": composetypes.NetworkConfig{
External: composetypes.External{External: true},
Name: "special",
},
"attachablenet": composetypes.NetworkConfig{
Driver: "overlay",
Attachable: true,
},
"named": composetypes.NetworkConfig{
Name: "othername",
},
}
expected := map[string]types.NetworkCreate{
"foo_default": {
Labels: map[string]string{
LabelNamespace: "foo",
},
},
"foo_normal": {
Driver: "overlay",
IPAM: &network.IPAM{
Driver: "driver",
Config: []network.IPAMConfig{
{
Subnet: "10.0.0.0",
},
},
},
Options: map[string]string{
"opt": "value",
},
Labels: map[string]string{
LabelNamespace: "foo",
"something": "labeled",
},
},
"foo_attachablenet": {
Driver: "overlay",
Attachable: true,
Labels: map[string]string{
LabelNamespace: "foo",
},
},
"othername": {
Labels: map[string]string{LabelNamespace: "foo"},
},
}
networks, externals := Networks(namespace, source, serviceNetworks)
assert.DeepEqual(t, expected, networks)
assert.DeepEqual(t, []string{"special"}, externals)
}
func TestSecrets(t *testing.T) {
namespace := Namespace{name: "foo"}
secretText := "this is the first secret"
secretFile := fs.NewFile(t, "convert-secrets", fs.WithContent(secretText))
defer secretFile.Remove()
source := map[string]composetypes.SecretConfig{
"one": {
File: secretFile.Path(),
Labels: map[string]string{"monster": "mash"},
},
"ext": {
External: composetypes.External{
External: true,
},
},
}
specs, err := Secrets(namespace, source)
assert.NilError(t, err)
assert.Assert(t, is.Len(specs, 1))
secret := specs[0]
assert.Check(t, is.Equal("foo_one", secret.Name))
assert.Check(t, is.DeepEqual(map[string]string{
"monster": "mash",
LabelNamespace: "foo",
}, secret.Labels))
assert.Check(t, is.DeepEqual([]byte(secretText), secret.Data))
}
func TestConfigs(t *testing.T) {
namespace := Namespace{name: "foo"}
configText := "this is the first config"
configFile := fs.NewFile(t, "convert-configs", fs.WithContent(configText))
defer configFile.Remove()
source := map[string]composetypes.ConfigObjConfig{
"one": {
File: configFile.Path(),
Labels: map[string]string{"monster": "mash"},
},
"ext": {
External: composetypes.External{
External: true,
},
},
}
specs, err := Configs(namespace, source)
assert.NilError(t, err)
assert.Assert(t, is.Len(specs, 1))
config := specs[0]
assert.Check(t, is.Equal("foo_one", config.Name))
assert.Check(t, is.DeepEqual(map[string]string{
"monster": "mash",
LabelNamespace: "foo",
}, config.Labels))
assert.Check(t, is.DeepEqual([]byte(configText), config.Data))
}
-861
View File
@@ -1,861 +0,0 @@
package convert
import (
"context"
"fmt"
"os"
"sort"
"strings"
"time"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
swarmtypes "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client"
"github.com/docker/go-units"
"github.com/pkg/errors"
)
const (
defaultNetwork = "default"
// LabelImage is the label used to store image name provided in the compose file
LabelImage = "com.docker.stack.image"
)
// ParseSecrets retrieves the secrets with the requested names and fills
// secret IDs into the secret references.
func ParseSecrets(client client.SecretAPIClient, requestedSecrets []*swarmtypes.SecretReference) ([]*swarmtypes.SecretReference, error) {
if len(requestedSecrets) == 0 {
return []*swarmtypes.SecretReference{}, nil
}
secretRefs := make(map[string]*swarmtypes.SecretReference)
ctx := context.Background()
for _, secret := range requestedSecrets {
if _, exists := secretRefs[secret.File.Name]; exists {
return nil, errors.Errorf("duplicate secret target for %s not allowed", secret.SecretName)
}
secretRef := new(swarmtypes.SecretReference)
*secretRef = *secret
secretRefs[secret.File.Name] = secretRef
}
args := filters.NewArgs()
for _, s := range secretRefs {
args.Add("name", s.SecretName)
}
secrets, err := client.SecretList(ctx, types.SecretListOptions{
Filters: args,
})
if err != nil {
return nil, err
}
foundSecrets := make(map[string]string)
for _, secret := range secrets {
foundSecrets[secret.Spec.Annotations.Name] = secret.ID
}
addedSecrets := []*swarmtypes.SecretReference{}
for _, ref := range secretRefs {
id, ok := foundSecrets[ref.SecretName]
if !ok {
return nil, errors.Errorf("secret not found: %s", ref.SecretName)
}
// set the id for the ref to properly assign in swarm
// since swarm needs the ID instead of the name
ref.SecretID = id
addedSecrets = append(addedSecrets, ref)
}
return addedSecrets, nil
}
// ParseConfigs retrieves the configs from the requested names and converts
// them to config references to use with the spec
func ParseConfigs(client client.ConfigAPIClient, requestedConfigs []*swarmtypes.ConfigReference) ([]*swarmtypes.ConfigReference, error) {
if len(requestedConfigs) == 0 {
return []*swarmtypes.ConfigReference{}, nil
}
// the configRefs map has two purposes: it prevents duplication of config
// target filenames, and it it used to get all configs so we can resolve
// their IDs. unfortunately, there are other targets for ConfigReferences,
// besides just a File; specifically, the Runtime target, which is used for
// CredentialSpecs. Therefore, we need to have a list of ConfigReferences
// that are not File targets as well. at this time of writing, the only use
// for Runtime targets is CredentialSpecs. However, to future-proof this
// functionality, we should handle the case where multiple Runtime targets
// are in use for the same Config, and we should deduplicate
// such ConfigReferences, as no matter how many times the Config is used,
// it is only needed to be referenced once.
configRefs := make(map[string]*swarmtypes.ConfigReference)
runtimeRefs := make(map[string]*swarmtypes.ConfigReference)
ctx := context.Background()
for _, config := range requestedConfigs {
// copy the config, so we don't mutate the args
configRef := new(swarmtypes.ConfigReference)
*configRef = *config
if config.Runtime != nil {
// by assigning to a map based on ConfigName, if the same Config
// is required as a Runtime target for multiple purposes, we only
// include it once in the final set of configs.
runtimeRefs[config.ConfigName] = config
// continue, so we skip the logic below for handling file-type
// configs
continue
}
if _, exists := configRefs[config.File.Name]; exists {
return nil, errors.Errorf("duplicate config target for %s not allowed", config.ConfigName)
}
configRefs[config.File.Name] = configRef
}
args := filters.NewArgs()
for _, s := range configRefs {
args.Add("name", s.ConfigName)
}
for _, s := range runtimeRefs {
args.Add("name", s.ConfigName)
}
configs, err := client.ConfigList(ctx, types.ConfigListOptions{
Filters: args,
})
if err != nil {
return nil, err
}
foundConfigs := make(map[string]string)
for _, config := range configs {
foundConfigs[config.Spec.Annotations.Name] = config.ID
}
addedConfigs := []*swarmtypes.ConfigReference{}
for _, ref := range configRefs {
id, ok := foundConfigs[ref.ConfigName]
if !ok {
return nil, errors.Errorf("config not found: %s", ref.ConfigName)
}
// set the id for the ref to properly assign in swarm
// since swarm needs the ID instead of the name
ref.ConfigID = id
addedConfigs = append(addedConfigs, ref)
}
// unfortunately, because the key of configRefs and runtimeRefs is different
// values that may collide, we can't just do some fancy trickery to
// concat maps, we need to do two separate loops
for _, ref := range runtimeRefs {
id, ok := foundConfigs[ref.ConfigName]
if !ok {
return nil, errors.Errorf("config not found: %s", ref.ConfigName)
}
ref.ConfigID = id
addedConfigs = append(addedConfigs, ref)
}
return addedConfigs, nil
}
// Services from compose-file types to engine API types
func Services(
namespace Namespace,
config *composetypes.Config,
client client.CommonAPIClient,
) (map[string]swarm.ServiceSpec, error) {
result := make(map[string]swarm.ServiceSpec)
services := config.Services
volumes := config.Volumes
networks := config.Networks
for _, service := range services {
secrets, err := convertServiceSecrets(client, namespace, service.Secrets, config.Secrets)
if err != nil {
return nil, errors.Wrapf(err, "service %s", service.Name)
}
configs, err := convertServiceConfigObjs(client, namespace, service, config.Configs)
if err != nil {
return nil, errors.Wrapf(err, "service %s", service.Name)
}
serviceSpec, err := Service(client.ClientVersion(), namespace, service, networks, volumes, secrets, configs)
if err != nil {
return nil, errors.Wrapf(err, "service %s", service.Name)
}
result[service.Name] = serviceSpec
}
return result, nil
}
// Service converts a ServiceConfig into a swarm ServiceSpec
func Service(
apiVersion string,
namespace Namespace,
service composetypes.ServiceConfig,
networkConfigs map[string]composetypes.NetworkConfig,
volumes map[string]composetypes.VolumeConfig,
secrets []*swarm.SecretReference,
configs []*swarm.ConfigReference,
) (swarm.ServiceSpec, error) {
name := namespace.Scope(service.Name)
endpoint := convertEndpointSpec(service.Deploy.EndpointMode, service.Ports)
mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas)
if err != nil {
return swarm.ServiceSpec{}, err
}
mounts, err := Volumes(service.Volumes, volumes, namespace)
if err != nil {
return swarm.ServiceSpec{}, err
}
resources, err := convertResources(service.Deploy.Resources)
if err != nil {
return swarm.ServiceSpec{}, err
}
restartPolicy, err := convertRestartPolicy(
service.Restart, service.Deploy.RestartPolicy)
if err != nil {
return swarm.ServiceSpec{}, err
}
healthcheck, err := convertHealthcheck(service.HealthCheck)
if err != nil {
return swarm.ServiceSpec{}, err
}
networks, err := convertServiceNetworks(service.Networks, networkConfigs, namespace, service.Name)
if err != nil {
return swarm.ServiceSpec{}, err
}
dnsConfig := convertDNSConfig(service.DNS, service.DNSSearch)
var privileges swarm.Privileges
privileges.CredentialSpec, err = convertCredentialSpec(
namespace, service.CredentialSpec, configs,
)
if err != nil {
return swarm.ServiceSpec{}, err
}
var logDriver *swarm.Driver
if service.Logging != nil {
logDriver = &swarm.Driver{
Name: service.Logging.Driver,
Options: service.Logging.Options,
}
}
capAdd, capDrop := opts.EffectiveCapAddCapDrop(service.CapAdd, service.CapDrop)
serviceSpec := swarm.ServiceSpec{
Annotations: swarm.Annotations{
Name: name,
Labels: AddStackLabel(namespace, service.Deploy.Labels),
},
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{
Image: service.Image,
Command: service.Entrypoint,
Args: service.Command,
Hostname: service.Hostname,
Hosts: convertExtraHosts(service.ExtraHosts),
DNSConfig: dnsConfig,
Healthcheck: healthcheck,
Env: sortStrings(convertEnvironment(service.Environment)),
Labels: AddStackLabel(namespace, service.Labels),
Dir: service.WorkingDir,
User: service.User,
Mounts: mounts,
StopGracePeriod: composetypes.ConvertDurationPtr(service.StopGracePeriod),
StopSignal: service.StopSignal,
TTY: service.Tty,
OpenStdin: service.StdinOpen,
Secrets: secrets,
Configs: configs,
ReadOnly: service.ReadOnly,
Privileges: &privileges,
Isolation: container.Isolation(service.Isolation),
Init: service.Init,
Sysctls: service.Sysctls,
CapabilityAdd: capAdd,
CapabilityDrop: capDrop,
Ulimits: convertUlimits(service.Ulimits),
},
LogDriver: logDriver,
Resources: resources,
RestartPolicy: restartPolicy,
Placement: &swarm.Placement{
Constraints: service.Deploy.Placement.Constraints,
Preferences: getPlacementPreference(service.Deploy.Placement.Preferences),
MaxReplicas: service.Deploy.Placement.MaxReplicas,
},
},
EndpointSpec: endpoint,
Mode: mode,
UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig),
RollbackConfig: convertUpdateConfig(service.Deploy.RollbackConfig),
}
// add an image label to serviceSpec
serviceSpec.Labels[LabelImage] = service.Image
// ServiceSpec.Networks is deprecated and should not have been used by
// this package. It is possible to update TaskTemplate.Networks, but it
// is not possible to update ServiceSpec.Networks. Unfortunately, we
// can't unconditionally start using TaskTemplate.Networks, because that
// will break with older daemons that don't support migrating from
// ServiceSpec.Networks to TaskTemplate.Networks. So which field to use
// is conditional on daemon version.
if versions.LessThan(apiVersion, "1.29") {
serviceSpec.Networks = networks
} else {
serviceSpec.TaskTemplate.Networks = networks
}
return serviceSpec, nil
}
func getPlacementPreference(preferences []composetypes.PlacementPreferences) []swarm.PlacementPreference {
result := []swarm.PlacementPreference{}
for _, preference := range preferences {
spreadDescriptor := preference.Spread
result = append(result, swarm.PlacementPreference{
Spread: &swarm.SpreadOver{
SpreadDescriptor: spreadDescriptor,
},
})
}
return result
}
func sortStrings(strs []string) []string {
sort.Strings(strs)
return strs
}
func convertServiceNetworks(
networks map[string]*composetypes.ServiceNetworkConfig,
networkConfigs networkMap,
namespace Namespace,
name string,
) ([]swarm.NetworkAttachmentConfig, error) {
if len(networks) == 0 {
networks = map[string]*composetypes.ServiceNetworkConfig{
defaultNetwork: {},
}
}
nets := []swarm.NetworkAttachmentConfig{}
for networkName, network := range networks {
networkConfig, ok := networkConfigs[networkName]
if !ok && networkName != defaultNetwork {
return nil, errors.Errorf("undefined network %q", networkName)
}
var aliases []string
if network != nil {
aliases = network.Aliases
}
target := namespace.Scope(networkName)
if networkConfig.Name != "" {
target = networkConfig.Name
}
netAttachConfig := swarm.NetworkAttachmentConfig{
Target: target,
Aliases: aliases,
}
// Only add default aliases to user defined networks. Other networks do
// not support aliases.
if container.NetworkMode(target).IsUserDefined() {
netAttachConfig.Aliases = append(netAttachConfig.Aliases, name)
}
nets = append(nets, netAttachConfig)
}
sort.Slice(nets, func(i, j int) bool {
return nets[i].Target < nets[j].Target
})
return nets, nil
}
// TODO: fix secrets API so that SecretAPIClient is not required here
func convertServiceSecrets(
client client.SecretAPIClient,
namespace Namespace,
secrets []composetypes.ServiceSecretConfig,
secretSpecs map[string]composetypes.SecretConfig,
) ([]*swarm.SecretReference, error) {
refs := []*swarm.SecretReference{}
lookup := func(key string) (composetypes.FileObjectConfig, error) {
secretSpec, exists := secretSpecs[key]
if !exists {
return composetypes.FileObjectConfig{}, errors.Errorf("undefined secret %q", key)
}
return composetypes.FileObjectConfig(secretSpec), nil
}
for _, secret := range secrets {
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(secret), lookup)
if err != nil {
return nil, err
}
file := swarm.SecretReferenceFileTarget(obj.File)
refs = append(refs, &swarm.SecretReference{
File: &file,
SecretName: obj.Name,
})
}
secrs, err := ParseSecrets(client, refs)
if err != nil {
return nil, err
}
// sort to ensure idempotence (don't restart services just because the entries are in different order)
sort.SliceStable(secrs, func(i, j int) bool { return secrs[i].SecretName < secrs[j].SecretName })
return secrs, err
}
// convertServiceConfigObjs takes an API client, a namespace, a ServiceConfig,
// and a set of compose Config specs, and creates the swarm ConfigReferences
// required by the serivce. Unlike convertServiceSecrets, this takes the whole
// ServiceConfig, because some Configs may be needed as a result of other
// fields (like CredentialSpecs).
//
// TODO: fix configs API so that ConfigsAPIClient is not required here
func convertServiceConfigObjs(
client client.ConfigAPIClient,
namespace Namespace,
service composetypes.ServiceConfig,
configSpecs map[string]composetypes.ConfigObjConfig,
) ([]*swarm.ConfigReference, error) {
refs := []*swarm.ConfigReference{}
lookup := func(key string) (composetypes.FileObjectConfig, error) {
configSpec, exists := configSpecs[key]
if !exists {
return composetypes.FileObjectConfig{}, errors.Errorf("undefined config %q", key)
}
return composetypes.FileObjectConfig(configSpec), nil
}
for _, config := range service.Configs {
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(config), lookup)
if err != nil {
return nil, err
}
file := swarm.ConfigReferenceFileTarget(obj.File)
refs = append(refs, &swarm.ConfigReference{
File: &file,
ConfigName: obj.Name,
})
}
// finally, after converting all of the file objects, create any
// Runtime-type configs that are needed. these are configs that are not
// mounted into the container, but are used in some other way by the
// container runtime. Currently, this only means CredentialSpecs, but in
// the future it may be used for other fields
// grab the CredentialSpec out of the Service
credSpec := service.CredentialSpec
// if the credSpec uses a config, then we should grab the config name, and
// create a config reference for it. A File or Registry-type CredentialSpec
// does not need this operation.
if credSpec.Config != "" {
// look up the config in the configSpecs.
obj, err := lookup(credSpec.Config)
if err != nil {
return nil, err
}
// get the actual correct name.
name := namespace.Scope(credSpec.Config)
if obj.Name != "" {
name = obj.Name
}
// now append a Runtime-type config.
refs = append(refs, &swarm.ConfigReference{
ConfigName: name,
Runtime: &swarm.ConfigReferenceRuntimeTarget{},
})
}
confs, err := ParseConfigs(client, refs)
if err != nil {
return nil, err
}
// sort to ensure idempotence (don't restart services just because the entries are in different order)
sort.SliceStable(confs, func(i, j int) bool { return confs[i].ConfigName < confs[j].ConfigName })
return confs, err
}
type swarmReferenceTarget struct {
Name string
UID string
GID string
Mode os.FileMode
}
type swarmReferenceObject struct {
File swarmReferenceTarget
ID string
Name string
}
func convertFileObject(
namespace Namespace,
config composetypes.FileReferenceConfig,
lookup func(key string) (composetypes.FileObjectConfig, error),
) (swarmReferenceObject, error) {
obj, err := lookup(config.Source)
if err != nil {
return swarmReferenceObject{}, err
}
source := namespace.Scope(config.Source)
if obj.Name != "" {
source = obj.Name
}
target := config.Target
if target == "" {
target = config.Source
}
uid := config.UID
gid := config.GID
if uid == "" {
uid = "0"
}
if gid == "" {
gid = "0"
}
mode := config.Mode
if mode == nil {
mode = uint32Ptr(0444)
}
return swarmReferenceObject{
File: swarmReferenceTarget{
Name: target,
UID: uid,
GID: gid,
Mode: os.FileMode(*mode),
},
Name: source,
}, nil
}
func uint32Ptr(value uint32) *uint32 {
return &value
}
// convertExtraHosts converts <host>:<ip> mappings to SwarmKit notation:
// "IP-address hostname(s)". The original order of mappings is preserved.
func convertExtraHosts(extraHosts composetypes.HostsList) []string {
hosts := []string{}
for _, hostIP := range extraHosts {
if v := strings.SplitN(hostIP, ":", 2); len(v) == 2 {
// Convert to SwarmKit notation: IP-address hostname(s)
hosts = append(hosts, fmt.Sprintf("%s %s", v[1], v[0]))
}
}
return hosts
}
func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) {
if healthcheck == nil {
return nil, nil
}
var (
timeout, interval, startPeriod time.Duration
retries int
)
if healthcheck.Disable {
if len(healthcheck.Test) != 0 {
return nil, errors.Errorf("test and disable can't be set at the same time")
}
return &container.HealthConfig{
Test: []string{"NONE"},
}, nil
}
if healthcheck.Timeout != nil {
timeout = time.Duration(*healthcheck.Timeout)
}
if healthcheck.Interval != nil {
interval = time.Duration(*healthcheck.Interval)
}
if healthcheck.StartPeriod != nil {
startPeriod = time.Duration(*healthcheck.StartPeriod)
}
if healthcheck.Retries != nil {
retries = int(*healthcheck.Retries)
}
return &container.HealthConfig{
Test: healthcheck.Test,
Timeout: timeout,
Interval: interval,
Retries: retries,
StartPeriod: startPeriod,
}, nil
}
func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) {
// TODO: log if restart is being ignored
if source == nil {
policy, err := opts.ParseRestartPolicy(restart)
if err != nil {
return nil, err
}
switch {
case policy.IsNone():
return nil, nil
case policy.IsAlways(), policy.IsUnlessStopped():
return &swarm.RestartPolicy{
Condition: swarm.RestartPolicyConditionAny,
}, nil
case policy.IsOnFailure():
attempts := uint64(policy.MaximumRetryCount)
return &swarm.RestartPolicy{
Condition: swarm.RestartPolicyConditionOnFailure,
MaxAttempts: &attempts,
}, nil
default:
return nil, errors.Errorf("unknown restart policy: %s", restart)
}
}
return &swarm.RestartPolicy{
Condition: swarm.RestartPolicyCondition(source.Condition),
Delay: composetypes.ConvertDurationPtr(source.Delay),
MaxAttempts: source.MaxAttempts,
Window: composetypes.ConvertDurationPtr(source.Window),
}, nil
}
func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig {
if source == nil {
return nil
}
parallel := uint64(1)
if source.Parallelism != nil {
parallel = *source.Parallelism
}
return &swarm.UpdateConfig{
Parallelism: parallel,
Delay: time.Duration(source.Delay),
FailureAction: source.FailureAction,
Monitor: time.Duration(source.Monitor),
MaxFailureRatio: source.MaxFailureRatio,
Order: source.Order,
}
}
func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) {
resources := &swarm.ResourceRequirements{}
var err error
if source.Limits != nil {
var cpus int64
if source.Limits.NanoCPUs != "" {
cpus, err = opts.ParseCPUs(source.Limits.NanoCPUs)
if err != nil {
return nil, err
}
}
resources.Limits = &swarm.Limit{
NanoCPUs: cpus,
MemoryBytes: int64(source.Limits.MemoryBytes),
Pids: source.Limits.Pids,
}
}
if source.Reservations != nil {
var cpus int64
if source.Reservations.NanoCPUs != "" {
cpus, err = opts.ParseCPUs(source.Reservations.NanoCPUs)
if err != nil {
return nil, err
}
}
var generic []swarm.GenericResource
for _, res := range source.Reservations.GenericResources {
var r swarm.GenericResource
if res.DiscreteResourceSpec != nil {
r.DiscreteResourceSpec = &swarm.DiscreteGenericResource{
Kind: res.DiscreteResourceSpec.Kind,
Value: res.DiscreteResourceSpec.Value,
}
}
generic = append(generic, r)
}
resources.Reservations = &swarm.Resources{
NanoCPUs: cpus,
MemoryBytes: int64(source.Reservations.MemoryBytes),
GenericResources: generic,
}
}
return resources, nil
}
func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortConfig) *swarm.EndpointSpec {
portConfigs := []swarm.PortConfig{}
for _, port := range source {
portConfig := swarm.PortConfig{
Protocol: swarm.PortConfigProtocol(port.Protocol),
TargetPort: port.Target,
PublishedPort: port.Published,
PublishMode: swarm.PortConfigPublishMode(port.Mode),
}
portConfigs = append(portConfigs, portConfig)
}
sort.Slice(portConfigs, func(i, j int) bool {
return portConfigs[i].PublishedPort < portConfigs[j].PublishedPort
})
return &swarm.EndpointSpec{
Mode: swarm.ResolutionMode(strings.ToLower(endpointMode)),
Ports: portConfigs,
}
}
func convertEnvironment(source map[string]*string) []string {
var output []string
for name, value := range source {
switch value {
case nil:
output = append(output, name)
default:
output = append(output, fmt.Sprintf("%s=%s", name, *value))
}
}
return output
}
func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) {
serviceMode := swarm.ServiceMode{}
switch mode {
case "global":
if replicas != nil {
return serviceMode, errors.Errorf("replicas can only be used with replicated mode")
}
serviceMode.Global = &swarm.GlobalService{}
case "replicated", "":
serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
default:
return serviceMode, errors.Errorf("Unknown mode: %s", mode)
}
return serviceMode, nil
}
func convertDNSConfig(DNS []string, DNSSearch []string) *swarm.DNSConfig {
if DNS != nil || DNSSearch != nil {
return &swarm.DNSConfig{
Nameservers: DNS,
Search: DNSSearch,
}
}
return nil
}
func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpecConfig, refs []*swarm.ConfigReference) (*swarm.CredentialSpec, error) {
var o []string
// Config was added in API v1.40
if spec.Config != "" {
o = append(o, `"Config"`)
}
if spec.File != "" {
o = append(o, `"File"`)
}
if spec.Registry != "" {
o = append(o, `"Registry"`)
}
l := len(o)
switch {
case l == 0:
return nil, nil
case l == 2:
return nil, errors.Errorf("invalid credential spec: cannot specify both %s and %s", o[0], o[1])
case l > 2:
return nil, errors.Errorf("invalid credential spec: cannot specify both %s, and %s", strings.Join(o[:l-1], ", "), o[l-1])
}
swarmCredSpec := swarm.CredentialSpec(spec)
// if we're using a swarm Config for the credential spec, over-write it
// here with the config ID
if swarmCredSpec.Config != "" {
for _, config := range refs {
if swarmCredSpec.Config == config.ConfigName {
swarmCredSpec.Config = config.ConfigID
return &swarmCredSpec, nil
}
}
// if none of the configs match, try namespacing
for _, config := range refs {
if namespace.Scope(swarmCredSpec.Config) == config.ConfigName {
swarmCredSpec.Config = config.ConfigID
return &swarmCredSpec, nil
}
}
return nil, errors.Errorf("invalid credential spec: spec specifies config %v, but no such config can be found", swarmCredSpec.Config)
}
return &swarmCredSpec, nil
}
func convertUlimits(origUlimits map[string]*composetypes.UlimitsConfig) []*units.Ulimit {
newUlimits := make(map[string]*units.Ulimit)
for name, u := range origUlimits {
if u.Single != 0 {
newUlimits[name] = &units.Ulimit{
Name: name,
Soft: int64(u.Single),
Hard: int64(u.Single),
}
} else {
newUlimits[name] = &units.Ulimit{
Name: name,
Soft: int64(u.Soft),
Hard: int64(u.Hard),
}
}
}
var ulimits []*units.Ulimit
for _, ulimit := range newUlimits {
ulimits = append(ulimits, ulimit)
}
sort.SliceStable(ulimits, func(i, j int) bool {
return ulimits[i].Name < ulimits[j].Name
})
return ulimits
}
-678
View File
@@ -1,678 +0,0 @@
package convert
import (
"context"
"os"
"sort"
"strings"
"testing"
"time"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestConvertRestartPolicyFromNone(t *testing.T) {
policy, err := convertRestartPolicy("no", nil)
assert.NilError(t, err)
assert.Check(t, is.DeepEqual((*swarm.RestartPolicy)(nil), policy))
}
func TestConvertRestartPolicyFromUnknown(t *testing.T) {
_, err := convertRestartPolicy("unknown", nil)
assert.Error(t, err, "unknown restart policy: unknown")
}
func TestConvertRestartPolicyFromAlways(t *testing.T) {
policy, err := convertRestartPolicy("always", nil)
expected := &swarm.RestartPolicy{
Condition: swarm.RestartPolicyConditionAny,
}
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(expected, policy))
}
func TestConvertRestartPolicyFromFailure(t *testing.T) {
policy, err := convertRestartPolicy("on-failure:4", nil)
attempts := uint64(4)
expected := &swarm.RestartPolicy{
Condition: swarm.RestartPolicyConditionOnFailure,
MaxAttempts: &attempts,
}
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(expected, policy))
}
func strPtr(val string) *string {
return &val
}
func TestConvertEnvironment(t *testing.T) {
source := map[string]*string{
"foo": strPtr("bar"),
"key": strPtr("value"),
}
env := convertEnvironment(source)
sort.Strings(env)
assert.Check(t, is.DeepEqual([]string{"foo=bar", "key=value"}, env))
}
func TestConvertExtraHosts(t *testing.T) {
source := composetypes.HostsList{
"zulu:127.0.0.2",
"alpha:127.0.0.1",
"zulu:ff02::1",
}
assert.Check(t, is.DeepEqual([]string{"127.0.0.2 zulu", "127.0.0.1 alpha", "ff02::1 zulu"}, convertExtraHosts(source)))
}
func TestConvertResourcesFull(t *testing.T) {
source := composetypes.Resources{
Limits: &composetypes.ResourceLimit{
NanoCPUs: "0.003",
MemoryBytes: composetypes.UnitBytes(300000000),
},
Reservations: &composetypes.Resource{
NanoCPUs: "0.002",
MemoryBytes: composetypes.UnitBytes(200000000),
},
}
resources, err := convertResources(source)
assert.NilError(t, err)
expected := &swarm.ResourceRequirements{
Limits: &swarm.Limit{
NanoCPUs: 3000000,
MemoryBytes: 300000000,
},
Reservations: &swarm.Resources{
NanoCPUs: 2000000,
MemoryBytes: 200000000,
},
}
assert.Check(t, is.DeepEqual(expected, resources))
}
func TestConvertResourcesOnlyMemory(t *testing.T) {
source := composetypes.Resources{
Limits: &composetypes.ResourceLimit{
MemoryBytes: composetypes.UnitBytes(300000000),
},
Reservations: &composetypes.Resource{
MemoryBytes: composetypes.UnitBytes(200000000),
},
}
resources, err := convertResources(source)
assert.NilError(t, err)
expected := &swarm.ResourceRequirements{
Limits: &swarm.Limit{
MemoryBytes: 300000000,
},
Reservations: &swarm.Resources{
MemoryBytes: 200000000,
},
}
assert.Check(t, is.DeepEqual(expected, resources))
}
func TestConvertHealthcheck(t *testing.T) {
retries := uint64(10)
timeout := composetypes.Duration(30 * time.Second)
interval := composetypes.Duration(2 * time.Millisecond)
source := &composetypes.HealthCheckConfig{
Test: []string{"EXEC", "touch", "/foo"},
Timeout: &timeout,
Interval: &interval,
Retries: &retries,
}
expected := &container.HealthConfig{
Test: source.Test,
Timeout: time.Duration(timeout),
Interval: time.Duration(interval),
Retries: 10,
}
healthcheck, err := convertHealthcheck(source)
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(expected, healthcheck))
}
func TestConvertHealthcheckDisable(t *testing.T) {
source := &composetypes.HealthCheckConfig{Disable: true}
expected := &container.HealthConfig{
Test: []string{"NONE"},
}
healthcheck, err := convertHealthcheck(source)
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(expected, healthcheck))
}
func TestConvertHealthcheckDisableWithTest(t *testing.T) {
source := &composetypes.HealthCheckConfig{
Disable: true,
Test: []string{"EXEC", "touch"},
}
_, err := convertHealthcheck(source)
assert.Error(t, err, "test and disable can't be set at the same time")
}
func TestConvertEndpointSpec(t *testing.T) {
source := []composetypes.ServicePortConfig{
{
Protocol: "udp",
Target: 53,
Published: 1053,
Mode: "host",
},
{
Target: 8080,
Published: 80,
},
}
endpoint := convertEndpointSpec("vip", source)
expected := swarm.EndpointSpec{
Mode: swarm.ResolutionMode(strings.ToLower("vip")),
Ports: []swarm.PortConfig{
{
TargetPort: 8080,
PublishedPort: 80,
},
{
Protocol: "udp",
TargetPort: 53,
PublishedPort: 1053,
PublishMode: "host",
},
},
}
assert.Check(t, is.DeepEqual(expected, *endpoint))
}
func TestConvertServiceNetworksOnlyDefault(t *testing.T) {
networkConfigs := networkMap{}
configs, err := convertServiceNetworks(
nil, networkConfigs, NewNamespace("foo"), "service")
expected := []swarm.NetworkAttachmentConfig{
{
Target: "foo_default",
Aliases: []string{"service"},
},
}
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(expected, configs))
}
func TestConvertServiceNetworks(t *testing.T) {
networkConfigs := networkMap{
"front": composetypes.NetworkConfig{
External: composetypes.External{External: true},
Name: "fronttier",
},
"back": composetypes.NetworkConfig{},
}
networks := map[string]*composetypes.ServiceNetworkConfig{
"front": {
Aliases: []string{"something"},
},
"back": {
Aliases: []string{"other"},
},
}
configs, err := convertServiceNetworks(
networks, networkConfigs, NewNamespace("foo"), "service")
expected := []swarm.NetworkAttachmentConfig{
{
Target: "foo_back",
Aliases: []string{"other", "service"},
},
{
Target: "fronttier",
Aliases: []string{"something", "service"},
},
}
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(expected, configs))
}
func TestConvertServiceNetworksCustomDefault(t *testing.T) {
networkConfigs := networkMap{
"default": composetypes.NetworkConfig{
External: composetypes.External{External: true},
Name: "custom",
},
}
networks := map[string]*composetypes.ServiceNetworkConfig{}
configs, err := convertServiceNetworks(
networks, networkConfigs, NewNamespace("foo"), "service")
expected := []swarm.NetworkAttachmentConfig{
{
Target: "custom",
Aliases: []string{"service"},
},
}
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(expected, configs))
}
func TestConvertDNSConfigEmpty(t *testing.T) {
dnsConfig := convertDNSConfig(nil, nil)
assert.Check(t, is.DeepEqual((*swarm.DNSConfig)(nil), dnsConfig))
}
var (
nameservers = []string{"8.8.8.8", "9.9.9.9"}
search = []string{"dc1.example.com", "dc2.example.com"}
)
func TestConvertDNSConfigAll(t *testing.T) {
dnsConfig := convertDNSConfig(nameservers, search)
assert.Check(t, is.DeepEqual(&swarm.DNSConfig{
Nameservers: nameservers,
Search: search,
}, dnsConfig))
}
func TestConvertDNSConfigNameservers(t *testing.T) {
dnsConfig := convertDNSConfig(nameservers, nil)
assert.Check(t, is.DeepEqual(&swarm.DNSConfig{
Nameservers: nameservers,
Search: nil,
}, dnsConfig))
}
func TestConvertDNSConfigSearch(t *testing.T) {
dnsConfig := convertDNSConfig(nil, search)
assert.Check(t, is.DeepEqual(&swarm.DNSConfig{
Nameservers: nil,
Search: search,
}, dnsConfig))
}
func TestConvertCredentialSpec(t *testing.T) {
tests := []struct {
name string
in composetypes.CredentialSpecConfig
out *swarm.CredentialSpec
configs []*swarm.ConfigReference
expectedErr string
}{
{
name: "empty",
},
{
name: "config-and-file",
in: composetypes.CredentialSpecConfig{Config: "0bt9dmxjvjiqermk6xrop3ekq", File: "somefile.json"},
expectedErr: `invalid credential spec: cannot specify both "Config" and "File"`,
},
{
name: "config-and-registry",
in: composetypes.CredentialSpecConfig{Config: "0bt9dmxjvjiqermk6xrop3ekq", Registry: "testing"},
expectedErr: `invalid credential spec: cannot specify both "Config" and "Registry"`,
},
{
name: "file-and-registry",
in: composetypes.CredentialSpecConfig{File: "somefile.json", Registry: "testing"},
expectedErr: `invalid credential spec: cannot specify both "File" and "Registry"`,
},
{
name: "config-and-file-and-registry",
in: composetypes.CredentialSpecConfig{Config: "0bt9dmxjvjiqermk6xrop3ekq", File: "somefile.json", Registry: "testing"},
expectedErr: `invalid credential spec: cannot specify both "Config", "File", and "Registry"`,
},
{
name: "missing-config-reference",
in: composetypes.CredentialSpecConfig{Config: "missing"},
expectedErr: "invalid credential spec: spec specifies config missing, but no such config can be found",
configs: []*swarm.ConfigReference{
{
ConfigName: "someName",
ConfigID: "missing",
},
},
},
{
name: "namespaced-config",
in: composetypes.CredentialSpecConfig{Config: "name"},
configs: []*swarm.ConfigReference{
{
ConfigName: "namespaced-config_name",
ConfigID: "someID",
},
},
out: &swarm.CredentialSpec{Config: "someID"},
},
{
name: "config",
in: composetypes.CredentialSpecConfig{Config: "someName"},
configs: []*swarm.ConfigReference{
{
ConfigName: "someOtherName",
ConfigID: "someOtherID",
}, {
ConfigName: "someName",
ConfigID: "someID",
},
},
out: &swarm.CredentialSpec{Config: "someID"},
},
{
name: "file",
in: composetypes.CredentialSpecConfig{File: "somefile.json"},
out: &swarm.CredentialSpec{File: "somefile.json"},
},
{
name: "registry",
in: composetypes.CredentialSpecConfig{Registry: "testing"},
out: &swarm.CredentialSpec{Registry: "testing"},
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
namespace := NewNamespace(tc.name)
swarmSpec, err := convertCredentialSpec(namespace, tc.in, tc.configs)
if tc.expectedErr != "" {
assert.Error(t, err, tc.expectedErr)
} else {
assert.NilError(t, err)
}
assert.DeepEqual(t, swarmSpec, tc.out)
})
}
}
func TestConvertUpdateConfigOrder(t *testing.T) {
// test default behavior
updateConfig := convertUpdateConfig(&composetypes.UpdateConfig{})
assert.Check(t, is.Equal("", updateConfig.Order))
// test start-first
updateConfig = convertUpdateConfig(&composetypes.UpdateConfig{
Order: "start-first",
})
assert.Check(t, is.Equal(updateConfig.Order, "start-first"))
// test stop-first
updateConfig = convertUpdateConfig(&composetypes.UpdateConfig{
Order: "stop-first",
})
assert.Check(t, is.Equal(updateConfig.Order, "stop-first"))
}
func TestConvertFileObject(t *testing.T) {
namespace := NewNamespace("testing")
config := composetypes.FileReferenceConfig{
Source: "source",
Target: "target",
UID: "user",
GID: "group",
Mode: uint32Ptr(0644),
}
swarmRef, err := convertFileObject(namespace, config, lookupConfig)
assert.NilError(t, err)
expected := swarmReferenceObject{
Name: "testing_source",
File: swarmReferenceTarget{
Name: config.Target,
UID: config.UID,
GID: config.GID,
Mode: os.FileMode(0644),
},
}
assert.Check(t, is.DeepEqual(expected, swarmRef))
}
func lookupConfig(key string) (composetypes.FileObjectConfig, error) {
if key != "source" {
return composetypes.FileObjectConfig{}, errors.New("bad key")
}
return composetypes.FileObjectConfig{}, nil
}
func TestConvertFileObjectDefaults(t *testing.T) {
namespace := NewNamespace("testing")
config := composetypes.FileReferenceConfig{Source: "source"}
swarmRef, err := convertFileObject(namespace, config, lookupConfig)
assert.NilError(t, err)
expected := swarmReferenceObject{
Name: "testing_source",
File: swarmReferenceTarget{
Name: config.Source,
UID: "0",
GID: "0",
Mode: os.FileMode(0444),
},
}
assert.Check(t, is.DeepEqual(expected, swarmRef))
}
func TestServiceConvertsIsolation(t *testing.T) {
src := composetypes.ServiceConfig{
Isolation: "hyperv",
}
result, err := Service("1.35", Namespace{name: "foo"}, src, nil, nil, nil, nil)
assert.NilError(t, err)
assert.Check(t, is.Equal(container.IsolationHyperV, result.TaskTemplate.ContainerSpec.Isolation))
}
func TestConvertServiceSecrets(t *testing.T) {
namespace := Namespace{name: "foo"}
secrets := []composetypes.ServiceSecretConfig{
{Source: "foo_secret"},
{Source: "bar_secret"},
}
secretSpecs := map[string]composetypes.SecretConfig{
"foo_secret": {
Name: "foo_secret",
},
"bar_secret": {
Name: "bar_secret",
},
}
client := &fakeClient{
secretListFunc: func(opts types.SecretListOptions) ([]swarm.Secret, error) {
assert.Check(t, is.Contains(opts.Filters.Get("name"), "foo_secret"))
assert.Check(t, is.Contains(opts.Filters.Get("name"), "bar_secret"))
return []swarm.Secret{
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "foo_secret"}}},
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "bar_secret"}}},
}, nil
},
}
refs, err := convertServiceSecrets(client, namespace, secrets, secretSpecs)
assert.NilError(t, err)
expected := []*swarm.SecretReference{
{
SecretName: "bar_secret",
File: &swarm.SecretReferenceFileTarget{
Name: "bar_secret",
UID: "0",
GID: "0",
Mode: 0444,
},
},
{
SecretName: "foo_secret",
File: &swarm.SecretReferenceFileTarget{
Name: "foo_secret",
UID: "0",
GID: "0",
Mode: 0444,
},
},
}
assert.DeepEqual(t, expected, refs)
}
func TestConvertServiceConfigs(t *testing.T) {
namespace := Namespace{name: "foo"}
service := composetypes.ServiceConfig{
Configs: []composetypes.ServiceConfigObjConfig{
{Source: "foo_config"},
{Source: "bar_config"},
},
CredentialSpec: composetypes.CredentialSpecConfig{
Config: "baz_config",
},
}
configSpecs := map[string]composetypes.ConfigObjConfig{
"foo_config": {
Name: "foo_config",
},
"bar_config": {
Name: "bar_config",
},
"baz_config": {
Name: "baz_config",
},
}
client := &fakeClient{
configListFunc: func(opts types.ConfigListOptions) ([]swarm.Config, error) {
assert.Check(t, is.Contains(opts.Filters.Get("name"), "foo_config"))
assert.Check(t, is.Contains(opts.Filters.Get("name"), "bar_config"))
assert.Check(t, is.Contains(opts.Filters.Get("name"), "baz_config"))
return []swarm.Config{
{Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "foo_config"}}},
{Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "bar_config"}}},
{Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "baz_config"}}},
}, nil
},
}
refs, err := convertServiceConfigObjs(client, namespace, service, configSpecs)
assert.NilError(t, err)
expected := []*swarm.ConfigReference{
{
ConfigName: "bar_config",
File: &swarm.ConfigReferenceFileTarget{
Name: "bar_config",
UID: "0",
GID: "0",
Mode: 0444,
},
},
{
ConfigName: "baz_config",
Runtime: &swarm.ConfigReferenceRuntimeTarget{},
},
{
ConfigName: "foo_config",
File: &swarm.ConfigReferenceFileTarget{
Name: "foo_config",
UID: "0",
GID: "0",
Mode: 0444,
},
},
}
assert.DeepEqual(t, expected, refs)
}
type fakeClient struct {
client.Client
secretListFunc func(types.SecretListOptions) ([]swarm.Secret, error)
configListFunc func(types.ConfigListOptions) ([]swarm.Config, error)
}
func (c *fakeClient) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) {
if c.secretListFunc != nil {
return c.secretListFunc(options)
}
return []swarm.Secret{}, nil
}
func (c *fakeClient) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
if c.configListFunc != nil {
return c.configListFunc(options)
}
return []swarm.Config{}, nil
}
func TestConvertUpdateConfigParallelism(t *testing.T) {
parallel := uint64(4)
// test default behavior
updateConfig := convertUpdateConfig(&composetypes.UpdateConfig{})
assert.Check(t, is.Equal(uint64(1), updateConfig.Parallelism))
// Non default value
updateConfig = convertUpdateConfig(&composetypes.UpdateConfig{
Parallelism: &parallel,
})
assert.Check(t, is.Equal(parallel, updateConfig.Parallelism))
}
func TestConvertServiceCapAddAndCapDrop(t *testing.T) {
tests := []struct {
title string
in, out composetypes.ServiceConfig
}{
{
title: "default behavior",
},
{
title: "some values",
in: composetypes.ServiceConfig{
CapAdd: []string{"SYS_NICE", "CAP_NET_ADMIN"},
CapDrop: []string{"CHOWN", "CAP_NET_ADMIN", "DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER"},
},
out: composetypes.ServiceConfig{
CapAdd: []string{"CAP_NET_ADMIN", "CAP_SYS_NICE"},
CapDrop: []string{"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID"},
},
},
{
title: "adding ALL capabilities",
in: composetypes.ServiceConfig{
CapAdd: []string{"ALL", "CAP_NET_ADMIN"},
CapDrop: []string{"CHOWN", "CAP_NET_ADMIN", "DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER"},
},
out: composetypes.ServiceConfig{
CapAdd: []string{"ALL"},
CapDrop: []string{"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID", "CAP_NET_ADMIN"},
},
},
{
title: "dropping ALL capabilities",
in: composetypes.ServiceConfig{
CapAdd: []string{"CHOWN", "CAP_NET_ADMIN", "DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER"},
CapDrop: []string{"ALL", "CAP_NET_ADMIN", "CAP_FOO"},
},
out: composetypes.ServiceConfig{
CapAdd: []string{"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID", "CAP_NET_ADMIN"},
CapDrop: []string{"ALL"},
},
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.title, func(t *testing.T) {
result, err := Service("1.41", Namespace{name: "foo"}, tc.in, nil, nil, nil, nil)
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityAdd, tc.out.CapAdd))
assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityDrop, tc.out.CapDrop))
})
}
}
-162
View File
@@ -1,162 +0,0 @@
package convert
import (
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types/mount"
"github.com/pkg/errors"
)
type volumes map[string]composetypes.VolumeConfig
// Volumes from compose-file types to engine api types
func Volumes(serviceVolumes []composetypes.ServiceVolumeConfig, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
var mounts []mount.Mount
for _, volumeConfig := range serviceVolumes {
mount, err := convertVolumeToMount(volumeConfig, stackVolumes, namespace)
if err != nil {
return nil, err
}
mounts = append(mounts, mount)
}
return mounts, nil
}
func createMountFromVolume(volume composetypes.ServiceVolumeConfig) mount.Mount {
return mount.Mount{
Type: mount.Type(volume.Type),
Target: volume.Target,
ReadOnly: volume.ReadOnly,
Source: volume.Source,
Consistency: mount.Consistency(volume.Consistency),
}
}
func handleVolumeToMount(
volume composetypes.ServiceVolumeConfig,
stackVolumes volumes,
namespace Namespace,
) (mount.Mount, error) {
result := createMountFromVolume(volume)
if volume.Tmpfs != nil {
return mount.Mount{}, errors.New("tmpfs options are incompatible with type volume")
}
if volume.Bind != nil {
return mount.Mount{}, errors.New("bind options are incompatible with type volume")
}
// Anonymous volumes
if volume.Source == "" {
return result, nil
}
stackVolume, exists := stackVolumes[volume.Source]
if !exists {
return mount.Mount{}, errors.Errorf("undefined volume %q", volume.Source)
}
result.Source = namespace.Scope(volume.Source)
result.VolumeOptions = &mount.VolumeOptions{}
if volume.Volume != nil {
result.VolumeOptions.NoCopy = volume.Volume.NoCopy
}
if stackVolume.Name != "" {
result.Source = stackVolume.Name
}
// External named volumes
if stackVolume.External.External {
return result, nil
}
result.VolumeOptions.Labels = AddStackLabel(namespace, stackVolume.Labels)
if stackVolume.Driver != "" || stackVolume.DriverOpts != nil {
result.VolumeOptions.DriverConfig = &mount.Driver{
Name: stackVolume.Driver,
Options: stackVolume.DriverOpts,
}
}
return result, nil
}
func handleBindToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
result := createMountFromVolume(volume)
if volume.Source == "" {
return mount.Mount{}, errors.New("invalid bind source, source cannot be empty")
}
if volume.Volume != nil {
return mount.Mount{}, errors.New("volume options are incompatible with type bind")
}
if volume.Tmpfs != nil {
return mount.Mount{}, errors.New("tmpfs options are incompatible with type bind")
}
if volume.Bind != nil {
result.BindOptions = &mount.BindOptions{
Propagation: mount.Propagation(volume.Bind.Propagation),
}
}
return result, nil
}
func handleTmpfsToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
result := createMountFromVolume(volume)
if volume.Source != "" {
return mount.Mount{}, errors.New("invalid tmpfs source, source must be empty")
}
if volume.Bind != nil {
return mount.Mount{}, errors.New("bind options are incompatible with type tmpfs")
}
if volume.Volume != nil {
return mount.Mount{}, errors.New("volume options are incompatible with type tmpfs")
}
if volume.Tmpfs != nil {
result.TmpfsOptions = &mount.TmpfsOptions{
SizeBytes: volume.Tmpfs.Size,
}
}
return result, nil
}
func handleNpipeToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
result := createMountFromVolume(volume)
if volume.Source == "" {
return mount.Mount{}, errors.New("invalid npipe source, source cannot be empty")
}
if volume.Volume != nil {
return mount.Mount{}, errors.New("volume options are incompatible with type npipe")
}
if volume.Tmpfs != nil {
return mount.Mount{}, errors.New("tmpfs options are incompatible with type npipe")
}
if volume.Bind != nil {
result.BindOptions = &mount.BindOptions{
Propagation: mount.Propagation(volume.Bind.Propagation),
}
}
return result, nil
}
func convertVolumeToMount(
volume composetypes.ServiceVolumeConfig,
stackVolumes volumes,
namespace Namespace,
) (mount.Mount, error) {
switch volume.Type {
case "volume", "":
return handleVolumeToMount(volume, stackVolumes, namespace)
case "bind":
return handleBindToMount(volume)
case "tmpfs":
return handleTmpfsToMount(volume)
case "npipe":
return handleNpipeToMount(volume)
}
return mount.Mount{}, errors.New("volume type must be volume, bind, tmpfs or npipe")
}
-361
View File
@@ -1,361 +0,0 @@
package convert
import (
"testing"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types/mount"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestConvertVolumeToMountAnonymousVolume(t *testing.T) {
config := composetypes.ServiceVolumeConfig{
Type: "volume",
Target: "/foo/bar",
}
expected := mount.Mount{
Type: mount.TypeVolume,
Target: "/foo/bar",
}
mount, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(expected, mount))
}
func TestConvertVolumeToMountAnonymousBind(t *testing.T) {
config := composetypes.ServiceVolumeConfig{
Type: "bind",
Target: "/foo/bar",
Bind: &composetypes.ServiceVolumeBind{
Propagation: "slave",
},
}
_, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
assert.Error(t, err, "invalid bind source, source cannot be empty")
}
func TestConvertVolumeToMountUnapprovedType(t *testing.T) {
config := composetypes.ServiceVolumeConfig{
Type: "foo",
Target: "/foo/bar",
}
_, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
assert.Error(t, err, "volume type must be volume, bind, tmpfs or npipe")
}
func TestConvertVolumeToMountConflictingOptionsBindInVolume(t *testing.T) {
namespace := NewNamespace("foo")
config := composetypes.ServiceVolumeConfig{
Type: "volume",
Source: "foo",
Target: "/target",
Bind: &composetypes.ServiceVolumeBind{
Propagation: "slave",
},
}
_, err := convertVolumeToMount(config, volumes{}, namespace)
assert.Error(t, err, "bind options are incompatible with type volume")
}
func TestConvertVolumeToMountConflictingOptionsTmpfsInVolume(t *testing.T) {
namespace := NewNamespace("foo")
config := composetypes.ServiceVolumeConfig{
Type: "volume",
Source: "foo",
Target: "/target",
Tmpfs: &composetypes.ServiceVolumeTmpfs{
Size: 1000,
},
}
_, err := convertVolumeToMount(config, volumes{}, namespace)
assert.Error(t, err, "tmpfs options are incompatible with type volume")
}
func TestConvertVolumeToMountConflictingOptionsVolumeInBind(t *testing.T) {
namespace := NewNamespace("foo")
config := composetypes.ServiceVolumeConfig{
Type: "bind",
Source: "/foo",
Target: "/target",
Volume: &composetypes.ServiceVolumeVolume{
NoCopy: true,
},
}
_, err := convertVolumeToMount(config, volumes{}, namespace)
assert.Error(t, err, "volume options are incompatible with type bind")
}
func TestConvertVolumeToMountConflictingOptionsTmpfsInBind(t *testing.T) {
namespace := NewNamespace("foo")
config := composetypes.ServiceVolumeConfig{
Type: "bind",
Source: "/foo",
Target: "/target",
Tmpfs: &composetypes.ServiceVolumeTmpfs{
Size: 1000,
},
}
_, err := convertVolumeToMount(config, volumes{}, namespace)
assert.Error(t, err, "tmpfs options are incompatible with type bind")
}
func TestConvertVolumeToMountConflictingOptionsBindInTmpfs(t *testing.T) {
namespace := NewNamespace("foo")
config := composetypes.ServiceVolumeConfig{
Type: "tmpfs",
Target: "/target",
Bind: &composetypes.ServiceVolumeBind{
Propagation: "slave",
},
}
_, err := convertVolumeToMount(config, volumes{}, namespace)
assert.Error(t, err, "bind options are incompatible with type tmpfs")
}
func TestConvertVolumeToMountConflictingOptionsVolumeInTmpfs(t *testing.T) {
namespace := NewNamespace("foo")
config := composetypes.ServiceVolumeConfig{
Type: "tmpfs",
Target: "/target",
Volume: &composetypes.ServiceVolumeVolume{
NoCopy: true,
},
}
_, err := convertVolumeToMount(config, volumes{}, namespace)
assert.Error(t, err, "volume options are incompatible with type tmpfs")
}
func TestConvertVolumeToMountNamedVolume(t *testing.T) {
stackVolumes := volumes{
"normal": composetypes.VolumeConfig{
Driver: "glusterfs",
DriverOpts: map[string]string{
"opt": "value",
},
Labels: map[string]string{
"something": "labeled",
},
},
}
namespace := NewNamespace("foo")
expected := mount.Mount{
Type: mount.TypeVolume,
Source: "foo_normal",
Target: "/foo",
ReadOnly: true,
VolumeOptions: &mount.VolumeOptions{
Labels: map[string]string{
LabelNamespace: "foo",
"something": "labeled",
},
DriverConfig: &mount.Driver{
Name: "glusterfs",
Options: map[string]string{
"opt": "value",
},
},
NoCopy: true,
},
}
config := composetypes.ServiceVolumeConfig{
Type: "volume",
Source: "normal",
Target: "/foo",
ReadOnly: true,
Volume: &composetypes.ServiceVolumeVolume{
NoCopy: true,
},
}
mount, err := convertVolumeToMount(config, stackVolumes, namespace)
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(expected, mount))
}
func TestConvertVolumeToMountNamedVolumeWithNameCustomizd(t *testing.T) {
stackVolumes := volumes{
"normal": composetypes.VolumeConfig{
Name: "user_specified_name",
Driver: "vsphere",
DriverOpts: map[string]string{
"opt": "value",
},
Labels: map[string]string{
"something": "labeled",
},
},
}
namespace := NewNamespace("foo")
expected := mount.Mount{
Type: mount.TypeVolume,
Source: "user_specified_name",
Target: "/foo",
ReadOnly: true,
VolumeOptions: &mount.VolumeOptions{
Labels: map[string]string{
LabelNamespace: "foo",
"something": "labeled",
},
DriverConfig: &mount.Driver{
Name: "vsphere",
Options: map[string]string{
"opt": "value",
},
},
NoCopy: true,
},
}
config := composetypes.ServiceVolumeConfig{
Type: "volume",
Source: "normal",
Target: "/foo",
ReadOnly: true,
Volume: &composetypes.ServiceVolumeVolume{
NoCopy: true,
},
}
mount, err := convertVolumeToMount(config, stackVolumes, namespace)
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(expected, mount))
}
func TestConvertVolumeToMountNamedVolumeExternal(t *testing.T) {
stackVolumes := volumes{
"outside": composetypes.VolumeConfig{
Name: "special",
External: composetypes.External{External: true},
},
}
namespace := NewNamespace("foo")
expected := mount.Mount{
Type: mount.TypeVolume,
Source: "special",
Target: "/foo",
VolumeOptions: &mount.VolumeOptions{NoCopy: false},
}
config := composetypes.ServiceVolumeConfig{
Type: "volume",
Source: "outside",
Target: "/foo",
}
mount, err := convertVolumeToMount(config, stackVolumes, namespace)
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(expected, mount))
}
func TestConvertVolumeToMountNamedVolumeExternalNoCopy(t *testing.T) {
stackVolumes := volumes{
"outside": composetypes.VolumeConfig{
Name: "special",
External: composetypes.External{External: true},
},
}
namespace := NewNamespace("foo")
expected := mount.Mount{
Type: mount.TypeVolume,
Source: "special",
Target: "/foo",
VolumeOptions: &mount.VolumeOptions{
NoCopy: true,
},
}
config := composetypes.ServiceVolumeConfig{
Type: "volume",
Source: "outside",
Target: "/foo",
Volume: &composetypes.ServiceVolumeVolume{
NoCopy: true,
},
}
mount, err := convertVolumeToMount(config, stackVolumes, namespace)
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(expected, mount))
}
func TestConvertVolumeToMountBind(t *testing.T) {
stackVolumes := volumes{}
namespace := NewNamespace("foo")
expected := mount.Mount{
Type: mount.TypeBind,
Source: "/bar",
Target: "/foo",
ReadOnly: true,
BindOptions: &mount.BindOptions{Propagation: mount.PropagationShared},
}
config := composetypes.ServiceVolumeConfig{
Type: "bind",
Source: "/bar",
Target: "/foo",
ReadOnly: true,
Bind: &composetypes.ServiceVolumeBind{Propagation: "shared"},
}
mount, err := convertVolumeToMount(config, stackVolumes, namespace)
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(expected, mount))
}
func TestConvertVolumeToMountVolumeDoesNotExist(t *testing.T) {
namespace := NewNamespace("foo")
config := composetypes.ServiceVolumeConfig{
Type: "volume",
Source: "unknown",
Target: "/foo",
ReadOnly: true,
}
_, err := convertVolumeToMount(config, volumes{}, namespace)
assert.Error(t, err, "undefined volume \"unknown\"")
}
func TestConvertTmpfsToMountVolume(t *testing.T) {
config := composetypes.ServiceVolumeConfig{
Type: "tmpfs",
Target: "/foo/bar",
Tmpfs: &composetypes.ServiceVolumeTmpfs{
Size: 1000,
},
}
expected := mount.Mount{
Type: mount.TypeTmpfs,
Target: "/foo/bar",
TmpfsOptions: &mount.TmpfsOptions{SizeBytes: 1000},
}
mount, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(expected, mount))
}
func TestConvertTmpfsToMountVolumeWithSource(t *testing.T) {
config := composetypes.ServiceVolumeConfig{
Type: "tmpfs",
Source: "/bar",
Target: "/foo/bar",
Tmpfs: &composetypes.ServiceVolumeTmpfs{
Size: 1000,
},
}
_, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
assert.Error(t, err, "invalid tmpfs source, source must be empty")
}
func TestConvertVolumeToMountAnonymousNpipe(t *testing.T) {
config := composetypes.ServiceVolumeConfig{
Type: "npipe",
Source: `\\.\pipe\foo`,
Target: `\\.\pipe\foo`,
}
expected := mount.Mount{
Type: mount.TypeNamedPipe,
Source: `\\.\pipe\foo`,
Target: `\\.\pipe\foo`,
}
mount, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(expected, mount))
}
-170
View File
@@ -1,170 +0,0 @@
package client
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"coopcloud.tech/abra/pkg/web"
"github.com/docker/distribution/reference"
)
type RawTag struct {
Layer string
Name string
}
type RawTags []RawTag
var registryURL = "https://registry.hub.docker.com/v1/repositories/%s/tags"
func GetRegistryTags(image string) (RawTags, error) {
var tags RawTags
tagsUrl := fmt.Sprintf(registryURL, image)
if err := web.ReadJSON(tagsUrl, &tags); err != nil {
return tags, err
}
return tags, nil
}
// getRegv2Token retrieves a registry v2 authentication token.
func getRegv2Token(image reference.Named) (string, error) {
img := reference.Path(image)
authTokenURL := fmt.Sprintf("https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull", img)
req, err := http.NewRequest("GET", authTokenURL, nil)
if err != nil {
return "", err
}
client := &http.Client{Timeout: web.Timeout}
res, err := client.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
_, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", nil
}
tokenRes := struct {
Token string
Expiry string
Issued string
}{}
if err := json.Unmarshal(body, &tokenRes); err != nil {
return "", err
}
return tokenRes.Token, nil
}
// GetTagDigest retrieves an image digest from a v2 registry
func GetTagDigest(image reference.Named) (string, error) {
img := reference.Path(image)
tag := image.(reference.NamedTagged).Tag()
manifestURL := fmt.Sprintf("https://index.docker.io/v2/%s/manifests/%s", img, tag)
req, err := http.NewRequest("GET", manifestURL, nil)
if err != nil {
return "", err
}
token, err := getRegv2Token(image)
if err != nil {
return "", err
}
req.Header = http.Header{
"Accept": []string{
"application/vnd.docker.distribution.manifest.v2+json",
"application/vnd.docker.distribution.manifest.list.v2+json",
},
"Authorization": []string{fmt.Sprintf("Bearer %s", token)},
}
client := &http.Client{Timeout: web.Timeout}
res, err := client.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
_, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
registryResT1 := struct {
SchemaVersion int
MediaType string
Manifests []struct {
MediaType string
Size int
Digest string
Platform struct {
Architecture string
Os string
}
}
}{}
registryResT2 := struct {
SchemaVersion int
MediaType string
Config struct {
MediaType string
Size int
Digest string
}
Layers []struct {
MediaType string
Size int
Digest string
}
}{}
if err := json.Unmarshal(body, &registryResT1); err != nil {
return "", err
}
var digest string
for _, manifest := range registryResT1.Manifests {
if string(manifest.Platform.Architecture) == "amd64" {
digest = strings.Split(manifest.Digest, ":")[1][:7]
}
}
if digest == "" {
if err := json.Unmarshal(body, &registryResT2); err != nil {
return "", err
}
digest = strings.Split(registryResT2.Config.Digest, ":")[1][:7]
}
if digest == "" {
return "", fmt.Errorf("Unable to retrieve amd64 digest for '%s'", image)
}
return digest, nil
}
-25
View File
@@ -1,25 +0,0 @@
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
}
-131
View File
@@ -1,131 +0,0 @@
package stack
import (
"fmt"
"io/ioutil"
"path/filepath"
"sort"
"strings"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/schema"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/sirupsen/logrus"
)
// LoadComposefile parse the composefile specified in the cli and returns its Config and version.
func LoadComposefile(opts Deploy, appEnv map[string]string) (*composetypes.Config, error) {
configDetails, err := getConfigDetails(opts.Composefiles, appEnv)
if err != nil {
return nil, err
}
dicts := getDictsFrom(configDetails.ConfigFiles)
config, err := loader.Load(configDetails)
if err != nil {
if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok {
return nil, fmt.Errorf("compose file contains unsupported options:\n\n%s",
propertyWarnings(fpe.Properties))
}
return nil, err
}
unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
if len(unsupportedProperties) > 0 {
logrus.Warnf("Ignoring unsupported options: %s\n\n",
strings.Join(unsupportedProperties, ", "))
}
deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
if len(deprecatedProperties) > 0 {
logrus.Warnf("Ignoring deprecated options:\n\n%s\n\n",
propertyWarnings(deprecatedProperties))
}
return config, nil
}
func getDictsFrom(configFiles []composetypes.ConfigFile) []map[string]interface{} {
dicts := []map[string]interface{}{}
for _, configFile := range configFiles {
dicts = append(dicts, configFile.Config)
}
return dicts
}
func propertyWarnings(properties map[string]string) string {
var msgs []string
for name, description := range properties {
msgs = append(msgs, fmt.Sprintf("%s: %s", name, description))
}
sort.Strings(msgs)
return strings.Join(msgs, "\n\n")
}
func getConfigDetails(composefiles []string, appEnv map[string]string) (composetypes.ConfigDetails, error) {
var details composetypes.ConfigDetails
absPath, err := filepath.Abs(composefiles[0])
if err != nil {
return details, err
}
details.WorkingDir = filepath.Dir(absPath)
details.ConfigFiles, err = loadConfigFiles(composefiles)
if err != nil {
return details, err
}
// Take the first file version (2 files can't have different version)
details.Version = schema.Version(details.ConfigFiles[0].Config)
details.Environment = appEnv
return details, err
}
func buildEnvironment(env []string) (map[string]string, error) {
result := make(map[string]string, len(env))
for _, s := range env {
// if value is empty, s is like "K=", not "K".
if !strings.Contains(s, "=") {
return result, fmt.Errorf("unexpected environment %q", s)
}
kv := strings.SplitN(s, "=", 2)
result[kv[0]] = kv[1]
}
return result, nil
}
func loadConfigFiles(filenames []string) ([]composetypes.ConfigFile, error) {
var configFiles []composetypes.ConfigFile
for _, filename := range filenames {
configFile, err := loadConfigFile(filename)
if err != nil {
return configFiles, err
}
configFiles = append(configFiles, *configFile)
}
return configFiles, nil
}
func loadConfigFile(filename string) (*composetypes.ConfigFile, error) {
var bytes []byte
var err error
bytes, err = ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
config, err := loader.ParseYAML(bytes)
if err != nil {
return nil, err
}
return &composetypes.ConfigFile{
Filename: filename,
Config: config,
}, nil
}
-15
View File
@@ -1,15 +0,0 @@
package stack
// Deploy holds docker stack deploy options
type Deploy struct {
Composefiles []string
Namespace string
ResolveImage string
SendRegistryAuth bool
Prune bool
}
// Remove holds docker stack remove options
type Remove struct {
Namespaces []string
}
-138
View File
@@ -1,138 +0,0 @@
package stack
import (
"context"
"fmt"
"sort"
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions"
apiclient "github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// RunRemove is the swarm implementation of docker stack remove
func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error {
var errs []string
for _, namespace := range opts.Namespaces {
services, err := GetStackServices(ctx, client, namespace)
if err != nil {
return err
}
networks, err := getStackNetworks(ctx, client, namespace)
if err != nil {
return err
}
var secrets []swarm.Secret
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.25") {
secrets, err = getStackSecrets(ctx, client, namespace)
if err != nil {
return err
}
}
var configs []swarm.Config
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.30") {
configs, err = getStackConfigs(ctx, client, namespace)
if err != nil {
return err
}
}
if len(services)+len(networks)+len(secrets)+len(configs) == 0 {
logrus.Warning(fmt.Errorf("nothing found in stack: %s", namespace))
continue
}
hasError := removeServices(ctx, client, services)
hasError = removeSecrets(ctx, client, secrets) || hasError
hasError = removeConfigs(ctx, client, configs) || hasError
hasError = removeNetworks(ctx, client, networks) || hasError
if hasError {
errs = append(errs, fmt.Sprintf("failed to remove some resources from stack: %s", namespace))
}
}
if len(errs) > 0 {
return errors.Errorf(strings.Join(errs, "\n"))
}
return nil
}
func sortServiceByName(services []swarm.Service) func(i, j int) bool {
return func(i, j int) bool {
return services[i].Spec.Name < services[j].Spec.Name
}
}
func removeServices(
ctx context.Context,
client *apiclient.Client,
services []swarm.Service,
) bool {
var hasError bool
sort.Slice(services, sortServiceByName(services))
for _, service := range services {
logrus.Infof("removing service %s\n", service.Spec.Name)
if err := client.ServiceRemove(ctx, service.ID); err != nil {
hasError = true
logrus.Fatalf("failed to remove service %s: %s", service.ID, err)
}
}
return hasError
}
func removeNetworks(
ctx context.Context,
client *apiclient.Client,
networks []types.NetworkResource,
) bool {
var hasError bool
for _, network := range networks {
logrus.Infof("removing network %s\n", network.Name)
if err := client.NetworkRemove(ctx, network.ID); err != nil {
hasError = true
logrus.Fatalf("failed to remove network %s: %s", network.ID, err)
}
}
return hasError
}
func removeSecrets(
ctx context.Context,
client *apiclient.Client,
secrets []swarm.Secret,
) bool {
var hasError bool
for _, secret := range secrets {
logrus.Infof("Removing secret %s\n", secret.Spec.Name)
if err := client.SecretRemove(ctx, secret.ID); err != nil {
hasError = true
logrus.Fatalf("Failed to remove secret %s: %s", secret.ID, err)
}
}
return hasError
}
func removeConfigs(
ctx context.Context,
client *apiclient.Client,
configs []swarm.Config,
) bool {
var hasError bool
for _, config := range configs {
logrus.Infof("removing config %s\n", config.Spec.Name)
if err := client.ConfigRemove(ctx, config.ID); err != nil {
hasError = true
logrus.Fatalf("failed to remove config %s: %s", config.ID, err)
}
}
return hasError
}
-439
View File
@@ -1,439 +0,0 @@
package stack
import (
"context"
"fmt"
"strings"
abraClient "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/client/convert"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client"
dockerclient "github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// Resolve image constants
const (
defaultNetworkDriver = "overlay"
ResolveImageAlways = "always"
ResolveImageChanged = "changed"
ResolveImageNever = "never"
)
type StackStatus struct {
Services []swarm.Service
Err error
}
func getStackFilter(namespace string) filters.Args {
filter := filters.NewArgs()
filter.Add("label", convert.LabelNamespace+"="+namespace)
return filter
}
func getStackServiceFilter(namespace string) filters.Args {
return getStackFilter(namespace)
}
func getAllStacksFilter() filters.Args {
filter := filters.NewArgs()
filter.Add("label", convert.LabelNamespace)
return filter
}
func GetStackServices(ctx context.Context, dockerclient client.APIClient, namespace string) ([]swarm.Service, error) {
return dockerclient.ServiceList(ctx, types.ServiceListOptions{Filters: getStackServiceFilter(namespace)})
}
// GetDeployedServicesByLabel filters services by label
func GetDeployedServicesByLabel(contextName string, label string) StackStatus {
cl, err := abraClient.New(contextName)
if err != nil {
if strings.Contains(err.Error(), "does not exist") {
// No local context found, bail out gracefully
return StackStatus{[]swarm.Service{}, nil}
}
return StackStatus{[]swarm.Service{}, err}
}
ctx := context.Background()
filters := filters.NewArgs()
filters.Add("label", label)
services, err := cl.ServiceList(ctx, types.ServiceListOptions{Filters: filters})
if err != nil {
return StackStatus{[]swarm.Service{}, err}
}
return StackStatus{services, nil}
}
func GetAllDeployedServices(contextName string) StackStatus {
cl, err := abraClient.New(contextName)
if err != nil {
if strings.Contains(err.Error(), "does not exist") {
// No local context found, bail out gracefully
return StackStatus{[]swarm.Service{}, nil}
}
return StackStatus{[]swarm.Service{}, err}
}
ctx := context.Background()
services, err := cl.ServiceList(ctx, types.ServiceListOptions{Filters: getAllStacksFilter()})
if err != nil {
return StackStatus{[]swarm.Service{}, err}
}
return StackStatus{services, nil}
}
// GetDeployedServicesByName filters services by name
func GetDeployedServicesByName(ctx context.Context, cl *dockerclient.Client, stackName, serviceName string) StackStatus {
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("%s_%s", stackName, serviceName))
services, err := cl.ServiceList(ctx, types.ServiceListOptions{Filters: filters})
if err != nil {
return StackStatus{[]swarm.Service{}, err}
}
return StackStatus{services, nil}
}
// IsDeployed chekcks whether an appp is deployed or not.
func IsDeployed(ctx context.Context, cl *dockerclient.Client, stackName string) (bool, string, error) {
version := ""
isDeployed := false
filter := filters.NewArgs()
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
services, err := cl.ServiceList(ctx, types.ServiceListOptions{Filters: filter})
if err != nil {
return false, version, err
}
if len(services) > 0 {
for _, service := range services {
labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName)
if deployedVersion, ok := service.Spec.Labels[labelKey]; ok {
version = deployedVersion
break
}
}
logrus.Debugf("'%s' has been detected as deployed with version '%s'", stackName, version)
return true, version, nil
}
logrus.Debugf("'%s' has been detected as not deployed", stackName)
return isDeployed, version, nil
}
// pruneServices removes services that are no longer referenced in the source
func pruneServices(ctx context.Context, cl *dockerclient.Client, namespace convert.Namespace, services map[string]struct{}) {
oldServices, err := GetStackServices(ctx, cl, namespace.Name())
if err != nil {
logrus.Infof("Failed to list services: %s\n", err)
}
pruneServices := []swarm.Service{}
for _, service := range oldServices {
if _, exists := services[namespace.Descope(service.Spec.Name)]; !exists {
pruneServices = append(pruneServices, service)
}
}
removeServices(ctx, cl, pruneServices)
}
// RunDeploy is the swarm implementation of docker stack deploy
func RunDeploy(cl *dockerclient.Client, opts Deploy, cfg *composetypes.Config) error {
ctx := context.Background()
if err := validateResolveImageFlag(&opts); err != nil {
return err
}
// client side image resolution should not be done when the supported
// server version is older than 1.30
if versions.LessThan(cl.ClientVersion(), "1.30") {
opts.ResolveImage = ResolveImageNever
}
return deployCompose(ctx, cl, opts, cfg)
}
// validateResolveImageFlag validates the opts.resolveImage command line option
func validateResolveImageFlag(opts *Deploy) error {
switch opts.ResolveImage {
case ResolveImageAlways, ResolveImageChanged, ResolveImageNever:
return nil
default:
return errors.Errorf("Invalid option %s for flag --resolve-image", opts.ResolveImage)
}
}
func deployCompose(ctx context.Context, cl *dockerclient.Client, opts Deploy, config *composetypes.Config) error {
namespace := convert.NewNamespace(opts.Namespace)
if opts.Prune {
services := map[string]struct{}{}
for _, service := range config.Services {
services[service.Name] = struct{}{}
}
pruneServices(ctx, cl, namespace, services)
}
serviceNetworks := getServicesDeclaredNetworks(config.Services)
networks, externalNetworks := convert.Networks(namespace, config.Networks, serviceNetworks)
if err := validateExternalNetworks(ctx, cl, externalNetworks); err != nil {
return err
}
if err := createNetworks(ctx, cl, namespace, networks); err != nil {
return err
}
secrets, err := convert.Secrets(namespace, config.Secrets)
if err != nil {
return err
}
if err := createSecrets(ctx, cl, secrets); err != nil {
return err
}
configs, err := convert.Configs(namespace, config.Configs)
if err != nil {
return err
}
if err := createConfigs(ctx, cl, configs); err != nil {
return err
}
services, err := convert.Services(namespace, config, cl)
if err != nil {
return err
}
return deployServices(ctx, cl, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
}
func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
serviceNetworks := map[string]struct{}{}
for _, serviceConfig := range serviceConfigs {
if len(serviceConfig.Networks) == 0 {
serviceNetworks["default"] = struct{}{}
continue
}
for network := range serviceConfig.Networks {
serviceNetworks[network] = struct{}{}
}
}
return serviceNetworks
}
func validateExternalNetworks(ctx context.Context, client dockerclient.NetworkAPIClient, externalNetworks []string) error {
for _, networkName := range externalNetworks {
if !container.NetworkMode(networkName).IsUserDefined() {
// Networks that are not user defined always exist on all nodes as
// local-scoped networks, so there's no need to inspect them.
continue
}
network, err := client.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{})
switch {
case dockerclient.IsErrNotFound(err):
return errors.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed", networkName)
case err != nil:
return err
case network.Scope != "swarm":
return errors.Errorf("network %q is declared as external, but it is not in the right scope: %q instead of \"swarm\"", networkName, network.Scope)
}
}
return nil
}
func createSecrets(ctx context.Context, cl *dockerclient.Client, secrets []swarm.SecretSpec) error {
for _, secretSpec := range secrets {
secret, _, err := cl.SecretInspectWithRaw(ctx, secretSpec.Name)
switch {
case err == nil:
// secret already exists, then we update that
if err := cl.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec); err != nil {
return errors.Wrapf(err, "failed to update secret %s", secretSpec.Name)
}
case dockerclient.IsErrNotFound(err):
// secret does not exist, then we create a new one.
logrus.Infof("Creating secret %s\n", secretSpec.Name)
if _, err := cl.SecretCreate(ctx, secretSpec); err != nil {
return errors.Wrapf(err, "failed to create secret %s", secretSpec.Name)
}
default:
return err
}
}
return nil
}
func createConfigs(ctx context.Context, cl *dockerclient.Client, configs []swarm.ConfigSpec) error {
for _, configSpec := range configs {
config, _, err := cl.ConfigInspectWithRaw(ctx, configSpec.Name)
switch {
case err == nil:
// config already exists, then we update that
if err := cl.ConfigUpdate(ctx, config.ID, config.Meta.Version, configSpec); err != nil {
return errors.Wrapf(err, "failed to update config %s", configSpec.Name)
}
case dockerclient.IsErrNotFound(err):
// config does not exist, then we create a new one.
logrus.Infof("Creating config %s\n", configSpec.Name)
if _, err := cl.ConfigCreate(ctx, configSpec); err != nil {
return errors.Wrapf(err, "failed to create config %s", configSpec.Name)
}
default:
return err
}
}
return nil
}
func createNetworks(ctx context.Context, cl *dockerclient.Client, namespace convert.Namespace, networks map[string]types.NetworkCreate) error {
existingNetworks, err := getStackNetworks(ctx, cl, namespace.Name())
if err != nil {
return err
}
existingNetworkMap := make(map[string]types.NetworkResource)
for _, network := range existingNetworks {
existingNetworkMap[network.Name] = network
}
for name, createOpts := range networks {
if _, exists := existingNetworkMap[name]; exists {
continue
}
if createOpts.Driver == "" {
createOpts.Driver = defaultNetworkDriver
}
logrus.Infof("Creating network %s\n", name)
if _, err := cl.NetworkCreate(ctx, name, createOpts); err != nil {
return errors.Wrapf(err, "failed to create network %s", name)
}
}
return nil
}
func deployServices(
ctx context.Context,
cl *dockerclient.Client,
services map[string]swarm.ServiceSpec,
namespace convert.Namespace,
sendAuth bool,
resolveImage string) error {
existingServices, err := GetStackServices(ctx, cl, namespace.Name())
if err != nil {
return err
}
existingServiceMap := make(map[string]swarm.Service)
for _, service := range existingServices {
existingServiceMap[service.Spec.Name] = service
}
for internalName, serviceSpec := range services {
var (
name = namespace.Scope(internalName)
image = serviceSpec.TaskTemplate.ContainerSpec.Image
encodedAuth string
)
// FIXME: disable for now as not sure how to avoid having a `dockerCli`
// instance here and would rather not copy/pasta that entire module in
// right now for something that we don't even support right now. Will skip
// this for now.
if sendAuth {
// Retrieve encoded auth token from the image reference
// encodedAuth, err = command.RetrieveAuthTokenFromImage(ctx, dockerCli, image)
// if err != nil {
// return err
// }
}
if service, exists := existingServiceMap[name]; exists {
logrus.Infof("Updating service %s (id: %s)\n", name, service.ID)
updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}
switch resolveImage {
case ResolveImageAlways:
// image should be updated by the server using QueryRegistry
updateOpts.QueryRegistry = true
case ResolveImageChanged:
if image != service.Spec.Labels[convert.LabelImage] {
// Query the registry to resolve digest for the updated image
updateOpts.QueryRegistry = true
} else {
// image has not changed; update the serviceSpec with the
// existing information that was set by QueryRegistry on the
// previous deploy. Otherwise this will trigger an incorrect
// service update.
serviceSpec.TaskTemplate.ContainerSpec.Image = service.Spec.TaskTemplate.ContainerSpec.Image
}
default:
if image == service.Spec.Labels[convert.LabelImage] {
// image has not changed; update the serviceSpec with the
// existing information that was set by QueryRegistry on the
// previous deploy. Otherwise this will trigger an incorrect
// service update.
serviceSpec.TaskTemplate.ContainerSpec.Image = service.Spec.TaskTemplate.ContainerSpec.Image
}
}
// Stack deploy does not have a `--force` option. Preserve existing
// ForceUpdate value so that tasks are not re-deployed if not updated.
// TODO move this to API client?
serviceSpec.TaskTemplate.ForceUpdate = service.Spec.TaskTemplate.ForceUpdate
response, err := cl.ServiceUpdate(ctx, service.ID, service.Version, serviceSpec, updateOpts)
if err != nil {
return errors.Wrapf(err, "failed to update service %s", name)
}
for _, warning := range response.Warnings {
logrus.Warn(warning)
}
} else {
logrus.Infof("Creating service %s\n", name)
createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth}
// query registry if flag disabling it was not set
if resolveImage == ResolveImageAlways || resolveImage == ResolveImageChanged {
createOpts.QueryRegistry = true
}
if _, err := cl.ServiceCreate(ctx, serviceSpec, createOpts); err != nil {
return errors.Wrapf(err, "failed to create service %s", name)
}
}
}
return nil
}
func getStackNetworks(ctx context.Context, dockerclient client.APIClient, namespace string) ([]types.NetworkResource, error) {
return dockerclient.NetworkList(ctx, types.NetworkListOptions{Filters: getStackFilter(namespace)})
}
func getStackSecrets(ctx context.Context, dockerclient client.APIClient, namespace string) ([]swarm.Secret, error) {
return dockerclient.SecretList(ctx, types.SecretListOptions{Filters: getStackFilter(namespace)})
}
func getStackConfigs(ctx context.Context, dockerclient client.APIClient, namespace string) ([]swarm.Config, error) {
return dockerclient.ConfigList(ctx, types.ConfigListOptions{Filters: getStackFilter(namespace)})
}
-51
View File
@@ -1,51 +0,0 @@
package client
import (
"context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
)
func GetVolumes(ctx context.Context, server string, appName string) ([]*types.Volume, error) {
cl, err := New(server)
if err != nil {
return nil, err
}
fs := filters.NewArgs()
fs.Add("name", appName)
volumeListOKBody, err := cl.VolumeList(ctx, fs)
volumeList := volumeListOKBody.Volumes
if err != nil {
logrus.Fatal(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
}
-140
View File
@@ -1,140 +0,0 @@
package compose
import (
"fmt"
"io/ioutil"
"path"
"path/filepath"
"strings"
"coopcloud.tech/abra/pkg/client/stack"
loader "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
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) error {
composeFiles, err := filepath.Glob(pattern)
if err != nil {
return 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.ABRA_DIR, "apps", recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath)
if err != nil {
return err
}
compose, err := loader.LoadComposefile(opts, sampleEnv)
if err != nil {
return 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 err
}
composeImage := reference.Path(img)
if strings.Contains(composeImage, "library") {
// ParseNormalizedNamed prepends 'library' to images like nginx:<tag>,
// postgres:<tag>, i.e. images which do not have a username in the
// first position of the string
composeImage = strings.Split(composeImage, "/")[1]
}
composeTag := img.(reference.NamedTagged).Tag()
logrus.Debugf("parsed '%s' from '%s'", composeTag, service.Image)
if image == composeImage {
bytes, err := ioutil.ReadFile(composeFile)
if err != nil {
return 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), 0644); err != nil {
return err
}
}
}
}
return 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.ABRA_DIR, "apps", 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
}
for oldLabel, value := range service.Deploy.Labels {
if strings.HasPrefix(oldLabel, "coop-cloud") {
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)
logrus.Debugf("updating '%s' to '%s' in '%s'", old, label, compose.Filename)
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0644); err != nil {
return err
}
}
}
}
return nil
}
-370
View File
@@ -1,370 +0,0 @@
package config
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/client/convert"
loader "coopcloud.tech/abra/pkg/client/stack"
stack "coopcloud.tech/abra/pkg/client/stack"
composetypes "github.com/docker/cli/cli/compose/types"
"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
Type string
Domain string
Env AppEnv
Server string
Path string
}
// StackName gets what the docker safe stack name is for the app
func (a App) StackName() string {
if _, exists := a.Env["STACK_NAME"]; exists {
return a.Env["STACK_NAME"]
}
stackName := SanitiseAppName(a.Name)
a.Env["STACK_NAME"] = stackName
return stackName
}
// SORTING TYPES
// 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)
}
// ByServerAndType sort a slice of Apps
type ByServerAndType []App
func (a ByServerAndType) Len() int { return len(a) }
func (a ByServerAndType) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByServerAndType) Less(i, j int) bool {
if a[i].Server == a[j].Server {
return strings.ToLower(a[i].Type) < strings.ToLower(a[j].Type)
}
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
}
// ByType sort a slice of Apps
type ByType []App
func (a ByType) Len() int { return len(a) }
func (a ByType) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByType) Less(i, j int) bool {
return strings.ToLower(a[i].Type) < strings.ToLower(a[j].Type)
}
// 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) {
// Checking for type as it is required - apps wont work without it
domain := env["DOMAIN"]
apptype, ok := env["TYPE"]
if !ok {
return App{}, errors.New("missing TYPE variable")
}
return App{
Name: name,
Domain: domain,
Type: apptype,
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(ABRA_SERVER_FOLDER)
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(ABRA_SERVER_FOLDER, server)
files, err := getAllFilesInDirectory(serverDir)
if err != nil {
return nil, err
}
for _, file := range files {
appName := strings.TrimSuffix(file.Name(), ".env")
appFilePath := path.Join(ABRA_SERVER_FOLDER, 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.Type, app.Env)
if err != nil {
return serviceNames, err
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := GetAppComposeConfig(app.Type, 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(appType, appName, server, domain, recipe string) error {
envSamplePath := path.Join(ABRA_DIR, "apps", appType, ".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); err == nil {
return fmt.Errorf("%s already exists?", appEnvPath)
}
envSample = []byte(strings.Replace(string(envSample), fmt.Sprintf("%s.example.com", recipe), domain, -1))
envSample = []byte(strings.Replace(string(envSample), "example.com", domain, -1))
err = ioutil.WriteFile(appEnvPath, envSample, 0755)
if err != nil {
return err
}
logrus.Debugf("copied '%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 {
//FIXME: we only need to check containers with the version label not
// every single container and then skip when we see no label perf gains
// to be had here
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", APPS_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", APPS_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
}
-37
View File
@@ -1,37 +0,0 @@
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) {
// TODO: Test failures as well as successes
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)
}
}
-167
View File
@@ -1,167 +0,0 @@
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 ABRA_SERVER_FOLDER = path.Join(ABRA_DIR, "servers")
var APPS_JSON = path.Join(ABRA_DIR, "apps.json")
var APPS_DIR = path.Join(ABRA_DIR, "apps")
var REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
// GetServers retrieves all servers.
func GetServers() ([]string, error) {
var servers []string
servers, err := getAllFoldersInDirectory(ABRA_SERVER_FOLDER)
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(ABRA_SERVER_FOLDER)
if err != nil {
return nil, err
}
logrus.Debugf("read '%s' from '%s'", strings.Join(serverNames, ","), ABRA_SERVER_FOLDER)
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
}
// EnsureAbraDirExists checks for the abra config folder and throws error if not
func EnsureAbraDirExists() error {
if _, err := os.Stat(ABRA_DIR); os.IsNotExist(err) {
logrus.Debugf("'%s' does not exist, creating it", ABRA_DIR)
if err := os.Mkdir(ABRA_DIR, 0777); err != nil {
return err
}
}
return 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
@@ -1,84 +0,0 @@
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",
"TYPE": "ecloud",
}
var expectedApp = App{
Name: appName,
Type: expectedAppEnv["TYPE"],
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 TYPE=%s; Got: DOMAIN=%s TYPE=%s",
expectedAppEnv["DOMAIN"],
expectedAppEnv["TYPE"],
env["DOMAIN"],
env["TYPE"],
)
}
}
-93
View File
@@ -1,93 +0,0 @@
package git
import (
"fmt"
"os"
"strings"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"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 {
return err
}
}
logrus.Debugf("'%s' has been git cloned successfully", dir)
} else {
logrus.Debugf("'%s' already exists, doing nothing", dir)
}
return nil
}
// EnsureUpToDate ensures that a git repo on disk has the latest changes (git-fetch).
func EnsureUpToDate(dir string) error {
repo, err := git.PlainOpen(dir)
if err != nil {
return err
}
branch := "master"
if _, err := repo.Branch("master"); err != nil {
if _, err := repo.Branch("main"); err != nil {
logrus.Debugf("failed to select branch in '%s'", dir)
return err
}
branch = "main"
}
logrus.Debugf("choosing '%s' as main git branch in '%s'", branch, dir)
worktree, err := repo.Worktree()
if err != nil {
return err
}
refName := fmt.Sprintf("refs/heads/%s", branch)
checkOutOpts := &git.CheckoutOptions{
Create: false,
Force: true,
Branch: plumbing.ReferenceName(refName),
}
if err := worktree.Checkout(checkOutOpts); err != nil {
logrus.Debugf("failed to check out '%s' in '%s'", refName, dir)
return err
}
logrus.Debugf("successfully checked out '%s' in '%s'", branch, dir)
remote, err := repo.Remote("origin")
if err != nil {
return err
}
fetchOpts := &git.FetchOptions{
RemoteName: "origin",
RefSpecs: []config.RefSpec{"refs/heads/*:refs/remotes/origin/*"},
Force: true,
}
if err := remote.Fetch(fetchOpts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") {
return err
}
}
logrus.Debugf("successfully fetched all changes in '%s'", dir)
return nil
}
-197
View File
@@ -1,197 +0,0 @@
package recipe
import (
"fmt"
"path"
"path/filepath"
"strings"
"coopcloud.tech/abra/pkg/client/stack"
loader "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/compose"
"coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus"
)
// Recipe represents a recipe.
type Recipe struct {
Name string
Config *composetypes.Config
}
// UpdateLabel updates a recipe label
func (r Recipe) UpdateLabel(serviceName, label string) error {
pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, r.Name)
if err := compose.UpdateLabel(pattern, serviceName, label, r.Name); err != nil {
return err
}
return nil
}
// UpdateTag updates a recipe tag
func (r Recipe) UpdateTag(image, tag string) error {
pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, r.Name)
if err := compose.UpdateTag(pattern, image, tag, r.Name); err != nil {
return err
}
return nil
}
// Tags list the recipe tags
func (r Recipe) Tags() ([]string, error) {
var tags []string
recipeDir := path.Join(config.ABRA_DIR, "apps", r.Name)
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return tags, err
}
gitTags, err := repo.Tags()
if err != nil {
return tags, err
}
if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
tags = append(tags, strings.TrimPrefix(string(ref.Name()), "refs/tags/"))
return nil
}); err != nil {
return tags, err
}
logrus.Debugf("detected '%s' as tags for recipe '%s'", strings.Join(tags, ", "), r.Name)
return tags, nil
}
// Get retrieves a recipe.
func Get(recipeName string) (Recipe, error) {
if err := EnsureExists(recipeName); err != nil {
return Recipe{}, err
}
pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, recipeName)
composeFiles, err := filepath.Glob(pattern)
if err != nil {
return Recipe{}, err
}
envSamplePath := path.Join(config.ABRA_DIR, "apps", recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath)
if err != nil {
logrus.Fatal(err)
}
opts := stack.Deploy{Composefiles: composeFiles}
config, err := loader.LoadComposefile(opts, sampleEnv)
if err != nil {
return Recipe{}, err
}
return Recipe{Name: recipeName, Config: config}, nil
}
// EnsureExists checks whether a recipe has been cloned locally or not.
func EnsureExists(recipe string) error {
recipeDir := path.Join(config.ABRA_DIR, "apps", strings.ToLower(recipe))
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, recipe)
if err := gitPkg.Clone(recipeDir, url); err != nil {
return err
}
return nil
}
// EnsureVersion checks whether a specific version exists for a recipe.
func EnsureVersion(recipeName, version string) error {
recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName)
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return err
}
tags, err := repo.Tags()
if err != nil {
return nil
}
var parsedTags []string
var tagRef plumbing.ReferenceName
if err := tags.ForEach(func(ref *plumbing.Reference) (err error) {
parsedTags = append(parsedTags, ref.Name().Short())
if ref.Name().Short() == version {
tagRef = ref.Name()
}
return nil
}); err != nil {
return err
}
logrus.Debugf("read '%s' as tags for recipe '%s'", strings.Join(parsedTags, ", "), recipeName)
if tagRef.String() == "" {
return fmt.Errorf("%s is not available?", version)
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
opts := &git.CheckoutOptions{
Branch: tagRef,
Create: false,
Force: true,
}
if err := worktree.Checkout(opts); err != nil {
return err
}
logrus.Debugf("successfully checked '%s' out to '%s' in '%s'", recipeName, tagRef.Short(), recipeDir)
return nil
}
// EnsureLatest makes sure the latest commit is checkout on for a local recipe repository.
func EnsureLatest(recipeName string) error {
recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName)
logrus.Debugf("attempting to open git repository in '%s'", recipeDir)
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return err
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
branch := "master"
if _, err := repo.Branch("master"); err != nil {
if _, err := repo.Branch("main"); err != nil {
logrus.Debugf("failed to select branch in '%s'", path.Join(config.APPS_DIR, recipeName))
return err
}
branch = "main"
}
refName := fmt.Sprintf("refs/heads/%s", branch)
checkOutOpts := &git.CheckoutOptions{
Create: false,
Force: true,
Branch: plumbing.ReferenceName(refName),
}
if err := worktree.Checkout(checkOutOpts); err != nil {
logrus.Debugf("failed to check out '%s' in '%s'", branch, recipeDir)
return err
}
return nil
}
-53
View File
@@ -1,53 +0,0 @@
package secret
import (
"errors"
"fmt"
"os/exec"
"github.com/sirupsen/logrus"
)
// PassInsertSecret inserts a secret into a pass store.
func PassInsertSecret(secretValue, secretName, appName, server string) error {
if _, err := exec.LookPath("pass"); err != nil {
return errors.New("pass command not found on $PATH, is it installed?")
}
cmd := fmt.Sprintf(
"echo %s | pass insert hosts/%s/%s/%s -m",
secretValue, server, appName, secretName,
)
logrus.Debugf("attempting to run '%s'", cmd)
if err := exec.Command("bash", "-c", cmd).Run(); err != nil {
return err
}
logrus.Infof("'%s' inserted into pass store", secretName)
return nil
}
// PassRmSecret deletes a secret from a pass store.
func PassRmSecret(secretName, appName, server string) error {
if _, err := exec.LookPath("pass"); err != nil {
return errors.New("pass command not found on $PATH, is it installed?")
}
cmd := fmt.Sprintf(
"pass rm --force hosts/%s/%s/%s",
server, appName, secretName,
)
logrus.Debugf("attempting to run '%s'", cmd)
if err := exec.Command("bash", "-c", cmd).Run(); err != nil {
return err
}
logrus.Infof("'%s' removed from pass store", secretName)
return nil
}
-172
View File
@@ -1,172 +0,0 @@
// Package secret provides functionality for generating and storing secrets
// both in a remote swarm and locally within supported storage such as pass
// stores.
package secret
import (
"fmt"
"regexp"
"strconv"
"strings"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/schultz-is/passgen"
"github.com/sirupsen/logrus"
)
// secretValue represents a parsed `SECRET_FOO=v1 # length=bar` env var config
// secret definition.
type secretValue struct {
Version string
Length int
}
// GeneratePasswords generates passwords.
func GeneratePasswords(count, length uint) ([]string, error) {
passwords, err := passgen.GeneratePasswords(
count,
length,
passgen.AlphabetDefault,
)
if err != nil {
return nil, err
}
logrus.Debugf("generated '%s'", strings.Join(passwords, ", "))
return passwords, nil
}
// GeneratePassphrases generates human readable and rememberable passphrases.
func GeneratePassphrases(count uint) ([]string, error) {
passphrases, err := passgen.GeneratePassphrases(
count,
passgen.PassphraseWordCountDefault,
rune('-'),
passgen.PassphraseCasingDefault,
passgen.WordListDefault,
)
if err != nil {
return nil, err
}
logrus.Debugf("generated '%s'", strings.Join(passphrases, ", "))
return passphrases, nil
}
// ReadSecretEnvVars reads secret env vars from an app env var config.
func ReadSecretEnvVars(appEnv config.AppEnv) map[string]string {
secretEnvVars := make(map[string]string)
for envVar := range appEnv {
regex := regexp.MustCompile(`^SECRET.*VERSION.*`)
if string(regex.Find([]byte(envVar))) != "" {
secretEnvVars[envVar] = appEnv[envVar]
}
}
logrus.Debugf("read '%s' as secrets from '%s'", secretEnvVars, appEnv)
return secretEnvVars
}
// TODO: should probably go in the config/app package?
func ParseSecretEnvVarName(secretEnvVar string) string {
withoutPrefix := strings.TrimPrefix(secretEnvVar, "SECRET_")
withoutSuffix := strings.TrimSuffix(withoutPrefix, "_VERSION")
name := strings.ToLower(withoutSuffix)
logrus.Debugf("parsed '%s' as name from '%s'", name, secretEnvVar)
return name
}
// TODO: should probably go in the config/app package?
func ParseGeneratedSecretName(secret string, appEnv config.App) string {
name := fmt.Sprintf("%s_", appEnv.StackName())
withoutAppName := strings.TrimPrefix(secret, name)
idx := strings.LastIndex(withoutAppName, "_")
parsed := withoutAppName[:idx]
logrus.Debugf("parsed '%s' as name from '%s'", parsed, secret)
return parsed
}
// TODO: should probably go in the config/app package?
func ParseSecretEnvVarValue(secret string) (secretValue, error) {
values := strings.Split(secret, "#")
if len(values) == 0 {
return secretValue{}, fmt.Errorf("unable to parse '%s'", secret)
}
if len(values) == 1 {
return secretValue{Version: values[0], Length: 0}, nil
}
split := strings.Split(values[1], "=")
parsed := split[len(split)-1]
stripped := strings.ReplaceAll(parsed, " ", "")
length, err := strconv.Atoi(stripped)
if err != nil {
return secretValue{}, err
}
version := strings.ReplaceAll(values[0], " ", "")
logrus.Debugf("parsed version '%s' and length '%v' from '%s'", version, length, secret)
return secretValue{Version: version, Length: length}, nil
}
// GenerateSecrets generates secrets locally and sends them to a remote server for storage.
func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (map[string]string, error) {
secrets := make(map[string]string)
ch := make(chan error, len(secretEnvVars))
for secretEnvVar := range secretEnvVars {
go func(s string) {
secretName := ParseSecretEnvVarName(s)
secretValue, err := ParseSecretEnvVarValue(secretEnvVars[s])
if err != nil {
ch <- err
return
}
secretRemoteName := fmt.Sprintf("%s_%s_%s", appName, secretName, secretValue.Version)
logrus.Debugf("attempting to generate and store '%s' on '%s'", secretRemoteName, server)
if secretValue.Length > 0 {
passwords, err := GeneratePasswords(1, uint(secretValue.Length))
if err != nil {
ch <- err
return
}
if err := client.StoreSecret(secretRemoteName, passwords[0], server); err != nil {
ch <- err
return
}
secrets[secretName] = passwords[0]
} else {
passphrases, err := GeneratePassphrases(1)
if err != nil {
ch <- err
return
}
if err := client.StoreSecret(secretRemoteName, passphrases[0], server); err != nil {
ch <- err
}
secrets[secretName] = passphrases[0]
}
ch <- nil
}(secretEnvVar)
}
for range secretEnvVars {
err := <-ch
if err != nil {
return nil, err
}
}
logrus.Debugf("generated and stored '%s' on '%s'", secrets, server)
return secrets, nil
}

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