Compare commits

...

117 Commits

Author SHA1 Message Date
4235e06943 chore: new 0.1.8-alpha release 2021-10-12 11:19:26 +02:00
a9af0b3627 fix: let gofmt do its magic 2021-10-12 10:34:10 +02:00
3wc
a0b4886eba WIP: default to compose.yml instead of all of 'em 2021-10-12 10:25:37 +02:00
84489495dc fix: load STACK_NAME if not present 2021-10-12 09:03:48 +02:00
a8683dc38a refactor: better formatting 2021-10-12 08:59:14 +02:00
e2128ea5b6 fix: check key existance correctly 2021-10-12 08:55:42 +02:00
ca3c5fef0f refactor: better wording [ci skip] 2021-10-12 08:49:38 +02:00
4a01e411be refactor: handle STACK_NAME override in one place 2021-10-12 01:14:14 +02:00
777d49ac1d fix: handle STACK_NAME for the ps command 2021-10-12 01:11:34 +02:00
deb7d21158 fix: dont loop over dead tags
Closes coop-cloud/organising#195.
2021-10-12 00:56:52 +02:00
6db1fdcfba refactor!: recipe upgrade: use new tagcmp version 2021-10-11 14:43:06 +00:00
44dc0edf7b refactor: use ; trick for inline checking [ci skip] 2021-10-11 13:48:25 +02:00
36ff50312c fix!: use annotated tags with recipe release 2021-10-11 10:45:00 +02:00
ff4b978876 fix: only list new versions
Closes coop-cloud/organising#192.
2021-10-11 01:17:52 +02:00
b68547b2c2 fix: dont overwrite generated catalogue
Closes coop-cloud/organising#190.
2021-10-11 01:06:51 +02:00
0140f96ca1 fix: make sure to clone recipe
Closes coop-cloud/organising#193.
2021-10-11 00:34:23 +02:00
3wc
1cb45113db fix: default linux binary in installer, add context
Closes coop-cloud/organising#184
2021-10-09 21:45:28 +02:00
c764243f3a fix: manage multiple version showing edge cases 2021-10-08 10:50:48 +02:00
dde8afcd43 feat: support version/upgrade listing
Closes coop-cloud/organising#130.
2021-10-08 09:51:47 +02:00
98ffc210e1 fix: show descending orders on releases [ci skip] 2021-10-06 09:13:07 +02:00
7c0d883135 chore: new release 2021-10-06 08:48:23 +02:00
e78ced41fb fix: use freifunk DNS resolver
Closes coop-cloud/organising#180.
2021-10-06 08:47:01 +02:00
e9113500d8 feat: allow to override STACK_NAME 2021-10-05 20:40:16 +02:00
7368cabc49 fix: format output correctly 2021-10-05 20:24:52 +02:00
f75e264811 fix: ensure dirs are created
Also use debug logging for help.

Closes coop-cloud/organising#183.
Closes coop-cloud/organising#183.
2021-10-05 20:24:41 +02:00
8bfd76fd04 feat: generate versions for catalogue also
Closes coop-cloud/organising#179.
2021-10-05 20:14:00 +02:00
1cb5e3509d fix: add compose.yml before commiting with recipe release; reset parts of tag according to semver when releasing 2021-10-05 16:36:15 +02:00
3cd2399cca fix: ignore WIP stuff and sort [ci skip] 2021-10-05 11:56:26 +02:00
11c4651a3b fix: don't crash when there is a more serious upgrade available 2021-10-05 09:55:25 +00:00
49f90674f2 fix: --major/minor/patch is the most serious upgrade you want to do 2021-10-05 09:55:25 +00:00
74a70edb03 feat: upgrade an app with no user input with --minor/major/patch flag 2021-10-05 09:55:25 +00:00
6fc5c31347 WIP: #172 upgrade --major/minor/patch placeholder 2021-10-05 09:55:25 +00:00
c616907b71 feat: teach recipe sync to understand new versions
Closes coop-cloud/organising#177.
2021-10-05 10:28:09 +02:00
a58cea3e0a docs: dont assume that yet [ci skip] 2021-10-02 23:30:18 +02:00
700f89425a chore: publish new release 2021-10-02 23:01:25 +02:00
8cc0a350e6 fix: pass sample env when loading recipe
Closes coop-cloud/organising#176.
2021-10-02 23:00:09 +02:00
46e67fa420 feat: support darwin builds 2021-10-02 22:53:07 +02:00
cacbb5a0f1 docs: remove extra change log items 2021-10-02 22:51:27 +02:00
e7046a15aa docs: keep it all lowercase 2021-10-02 22:51:12 +02:00
c1fd97c427 fix: handle new local server is listing 2021-10-02 22:40:08 +02:00
2f218bd99f fix: ensure ~/.abra is created
Also make that debug message less cringe.
2021-10-02 22:37:30 +02:00
48290aa316 fix: make server path creation more robust 2021-10-02 22:30:08 +02:00
db5cbfa992 docs: reword this local flag usage 2021-10-02 22:14:01 +02:00
4c11e813e8 test: ensure .env reading tests work 2021-10-02 22:10:00 +02:00
6ae75e013a refactor: move Major, Minor and Patch to recipe.go 2021-10-01 19:49:18 +02:00
09f49cdc76 chore: fix tests 2021-10-01 12:57:34 +02:00
22118b88e4 chore: appease formatter 2021-10-01 12:56:04 +02:00
e6db064149 chore: publish next tag 0.1.5-alpha 2021-10-01 12:32:41 +02:00
3wc
3688ea9d69 feat: support local server with --local 2021-10-01 11:59:17 +02:00
3wc
7c4cdc530c fix: don't crash if no abra.sh 2021-10-01 11:40:19 +02:00
3wc
49781c7e3f fix: ignore "env" files which don't end in .env 2021-10-01 11:40:19 +02:00
10b15d65b4 docs: use same style log messages [ci skip] 2021-09-29 22:37:16 +02:00
1c5d6d6357 docs: attempt some cmd docs 2021-09-29 22:36:43 +02:00
75bdd59585 Merge pull request 'feat: add a flag to commit your changes before creating a tag' (#102) from knoflook/abra:recipe-release into main
Reviewed-on: coop-cloud/abra#102
2021-09-29 20:24:55 +00:00
96bb145981 feat: check and sanitize user-specified tag 2021-09-29 16:25:39 +02:00
c4c76f4848 feat: add a flag to commit your changes before creating a tag 2021-09-29 16:08:02 +02:00
2076c566bb Merge pull request 'feat: tag recipes with abra' (#99) from knoflook/abra:recipe-release into main
Reviewed-on: coop-cloud/abra#99
2021-09-29 12:39:35 +00:00
62f6327b66 refactor: use usual naming style [ci skip] 2021-09-28 21:28:46 +02:00
6f9120b59c chore: run mod tidy 2021-09-28 21:27:31 +02:00
8c617a9f12 Merge pull request 'feat: print stack traces for errors when debugging' (#101) from knoflook/abra:main into main
Reviewed-on: coop-cloud/abra#101
2021-09-28 19:26:56 +00:00
857d12d23c feat: print stack traces for errors when debugging 2021-09-27 12:24:02 +02:00
22c4d0d864 style: remove doubled debug message 2021-09-24 11:05:49 +02:00
e700e44363 feat: add main apps version as a semver build metadata when releasing 2021-09-24 10:48:09 +02:00
9faefd2592 feat: push the new tag with --push 2021-09-23 18:52:21 +02:00
cd179175f5 refactor: dont' create the same objects twice 2021-09-23 18:32:58 +02:00
c0f92ca13d feat: support --major/-x --minor/-y --patch/-z for tag calculation 2021-09-23 18:27:19 +02:00
48d28c8dd1 feat: tag recipes with abra 2021-09-22 16:03:56 +02:00
e840328e44 chore: publish next release 2021-09-22 09:04:19 +02:00
6f43778691 fix: better UI/UX for app creation
Closes coop-cloud/organising#145.
2021-09-22 08:59:00 +02:00
9783563fa6 fix: drop version checking while churning 2021-09-22 08:47:49 +02:00
1392afc015 fix: give better error message on server create 2021-09-22 08:19:28 +02:00
886009975d fix: order args correctly 2021-09-22 08:19:14 +02:00
b1147cd136 feat: add x-platform progress bars for long loads
Closes coop-cloud/organising#150.
2021-09-22 07:48:17 +02:00
95a9013658 fix: use appFiles to determine server list 2021-09-20 22:43:30 +02:00
bd1bf3b0d6 chore: remove new line [ci skip] 2021-09-20 19:18:49 +02:00
7b349732ac fix: fix name and doc exceptions for catalogue generation 2021-09-20 16:53:49 +02:00
a8ce64a9db fix: ignore abra-bash for catalogue generation 2021-09-20 16:53:38 +02:00
96aa74a977 WIP: gather more meta for catalogue generation 2021-09-20 16:48:27 +02:00
700f022790 WIP: use repo metadata not existing catalogue 2021-09-20 09:38:51 +02:00
d188327b17 WIP: generating new apps.json 2021-09-17 08:04:16 +02:00
fdd46a4d98 chore: run formatter 2021-09-17 07:38:38 +02:00
e00920643e WIP: implement async recipe cloning
See coop-cloud/organising#159.
2021-09-16 16:28:11 +02:00
3wc
754fe81e01 feat: add templating during .. app new
Closes coop-cloud/organising#168
2021-09-16 15:09:35 +02:00
bece2e8351 fix: recovering debug logging [ci skip]
Follows 31edbbd32e.
2021-09-16 13:10:17 +02:00
e47d7029d7 refactor: S1005 gosimple 2021-09-16 12:01:47 +01:00
31edbbd32e fix: git metadata not removed in merge 2021-09-16 11:35:18 +01:00
0a1c73bf00 refactor: use cli context vs creating new one 2021-09-16 11:21:38 +01:00
a74a8bc21b docs: finish release docs off [ci skip] 2021-09-16 09:52:03 +02:00
357cc0593a chore: bump installer for new version 2021-09-16 09:49:48 +02:00
8e111dc32f fix: use correct debug function 2021-09-16 09:48:28 +02:00
20ecdb8061 fix: log which compose files are being loaded
See coop-cloud/organising#167.
2021-09-16 09:45:02 +02:00
f87aad4688 fix: list all servers
Closes coop-cloud/organising#166.
2021-09-16 09:26:12 +02:00
6794236b77 feat: support service completion
Closes coop-cloud/organising#165.
2021-09-16 09:10:05 +02:00
6c9bb89a10 refactor: use our usual initialisation 2021-09-16 09:09:51 +02:00
66aeeee768 fix: completion doesn't fail silently now
Closes coop-cloud/organising#161.
2021-09-16 08:45:38 +02:00
6c115926e3 fix: load sample env for new apps
Closes coop-cloud/organising#170.
2021-09-16 08:40:48 +02:00
b6fe86f2ad fix: use correct args for debug log inputs 2021-09-14 16:14:09 +02:00
d290a4ec0b WIP: the beginning of catalogue generation
See coop-cloud/organising#159.
2021-09-14 16:00:15 +02:00
f93563588a docs: add template 2021-09-11 12:20:27 +02:00
59c55c0a2f fix: add complete for app run command 2021-09-11 11:51:25 +02:00
9fcdc45851 feat: debug logging
Closes coop-cloud/organising#164.
2021-09-11 11:45:26 +02:00
27d665c3be refactor: move autocomplete into scripts folder 2021-09-10 23:45:28 +02:00
bc5fc0b0cb refactor: shorter names for autocomplete files 2021-09-10 23:44:32 +02:00
99160967a8 refactor: domainName as arg and doc strings
See coop-cloud/organising#163.
2021-09-10 15:04:01 +02:00
683ef0c3de fix: make more server new command more robust
See coop-cloud/organising#163.
2021-09-10 14:49:25 +02:00
3c3d8dc0e7 WIP: add first run at app rollback command
See coop-cloud/organising#146.
2021-09-10 11:49:29 +02:00
855e9ea26d fix: dont output secrets table if nothing there
See coop-cloud/organising#162.
2021-09-10 10:36:46 +02:00
50d663ff6e fix: use correct var for storing server var
See coop-cloud/organising#162.
2021-09-10 10:36:39 +02:00
39ad6e8aa8 fix: use recipeName instead of recipe.Name
This provides a correctly formatted recipe name for machine reading
(i.e. with `-` and such) instead of the more human readable version
(i.e. with spaces).

Closes coop-cloud/organising#162.
2021-09-10 09:56:58 +02:00
f39c8cbe21 fix: use our godotenv fork 2021-09-09 21:26:10 +02:00
e114b2a939 Merge pull request 'feat: auto-complete app and recipe names' (#89) from knoflook/abra:main into main
Reviewed-on: coop-cloud/abra#89
2021-09-08 12:16:41 +00:00
511619722f feat: autocomplete recipe names for more abra commands 2021-09-08 13:59:55 +02:00
cf2653fef8 refactor: drop unused function, rename GetAppsNames 2021-09-08 13:43:55 +02:00
5ba40ad883 feat: include service tags
Closes coop-cloud/abra#92.
2021-09-08 10:15:46 +02:00
2e0c16d198 docs: retire TODO.md, use issues [ci skip] 2021-09-07 19:18:13 +02:00
4c216fdf40 feat: auto-complete app and recipe names 2021-09-07 16:57:39 +02:00
5f50c7960c Update 'TODO.md' 2021-09-07 13:34:45 +00:00
66 changed files with 2148 additions and 422 deletions

8
.gitea/ISSUE_TEMPLATE.md Normal file
View File

@ -0,0 +1,8 @@
---
name: "Do not use this issue tracker"
about: "Do not use this issue tracker"
title: "Do not use this issue tracker"
labels: []
---
Please report your issue on [`coop-cloud/organising`](https://git.coopcloud.tech/coop-cloud/organising)

View File

@ -14,12 +14,12 @@ builds:
dir: cmd/abra dir: cmd/abra
goos: goos:
- linux - linux
- darwin
ldflags: ldflags:
- "-X 'main.Commit={{ .Commit }}'" - "-X 'main.Commit={{ .Commit }}'"
- "-X 'main.Version={{ .Version }}'" - "-X 'main.Version={{ .Version }}'"
archives: archives:
- replacements: - replacements:
linux: Linux
386: i386 386: i386
amd64: x86_64 amd64: x86_64
format: binary format: binary
@ -28,8 +28,13 @@ checksum:
snapshot: snapshot:
name_template: "{{ incpatch .Version }}-next" name_template: "{{ incpatch .Version }}-next"
changelog: changelog:
sort: asc sort: desc
filters: filters:
exclude: exclude:
- "^WIP:"
- "^chore:"
- "^docs:" - "^docs:"
- "^refactor:"
- "^style:"
- "^test:" - "^test:"
- "^tests:"

View File

@ -42,21 +42,24 @@ The abra binary will be in `$GOPATH/bin`.
**bash** **bash**
Copy `autocomplete/bash_autocomplete` into `/etc/bash_completion.d/` and rename Copy `scripts/autocomplete/bash` into `/etc/bash_completion.d/` and rename
it to abra. it to abra.
``` ```
sudo cp autocomplete/bash_autocomplete /etc/bash_completion.d/abra sudo cp scripts/autocomplete/bash /etc/bash_completion.d/abra
source /etc/bash_completion.d/abra source /etc/bash_completion.d/abra
``` ```
**(fi)zsh** **(fi)zsh**
(fi)zsh doesn't have an autocompletion folder by default but you can create one, then copy `zsh_autocomplete` into it and add a couple lines to your `~/.zshrc` or `~/.fizsh/.fizshrc` (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 mkdir /etc/zsh/completion.d/
sudo cp autocomplete/zsh_autocomplete /etc/zsh/completion.d/abra 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 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) (replace .zshrc with ~/.fizsh/.fizshrc if you use fizsh)
## Hacking ## Hacking
@ -86,6 +89,7 @@ For developers, while using this `-alpha` format, the `y` part is the "major" ve
- Make a new tag (e.g. `git tag 0.y.z-alpha`) - Make a new tag (e.g. `git tag 0.y.z-alpha`)
- Push the new tag (e.g. `git push && git push --tags`) - 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) - 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`) - Check the release worked, (e.g. `abra upgrade; abra version`)
## Fork maintenance ## Fork maintenance

67
TODO.md
View File

@ -1,67 +0,0 @@
# TODO
## Bash feature parity
- [ ] Commands
- [x] `abra server`
- [x] `ls`
- [x] `add`
- [x] `new`
- [x] `capsul`
- [x] `hetzner`
- [x] `rm`
- [x] `init`
- [ ] `abra app`
- [x] `ls`
- [x] `new`
- [x] `backup`
- [x] `deploy`
- [x] `check`
- [x] `version`
- [x] `config`
- [x] `cp`
- [x] `logs`
- [x] `ps`
- [x] `restore`
- [x] `rm`
- [x] `run`
- [ ] `rollback`
- [x] `secret`
- [x] `generate`
- [x] `insert`
- [x] `rm`
- [x] `ls`
- [x] `undeploy`
- [ ] `volume`
- [x] `ls` (WIP: knoflook)
- [ ] `rm` (WIP: knoflook)
- [x] `abra recipe`
- [x] `ls`
- [x] `create`
- [x] `upgrade`
- [x] `sync`
- [x] `versions`
- [x] `lint`
- [x] `upgrade`
- [x] `version`
## Next phase
- [ ] Polishing UI/UX and testing
- [ ] Refactoring and code organisation
- [ ] Automated builds for releasing
## New features
- [ ] Commands
- [ ] `abra server`
- [ ] `dns`
- [ ] `gandi`
- [ ] `abra recipe`
- [ ] "TBD apps.json generating command" (see [#40](https://git.coopcloud.tech/coop-cloud/go-abra/issues/40))
- [ ] Package manager integration
- [x] AUR
- [ ] Debian
- [ ] Ubuntu
- [ ] Fedora
- [ ] Homebrew

View File

@ -66,13 +66,22 @@ var appBackupCommand = &cli.Command{
sourceAndExec := fmt.Sprintf("%s; %s", sourceCmd, execCmd) sourceAndExec := fmt.Sprintf("%s; %s", sourceCmd, execCmd)
cmd := exec.Command("bash", "-c", sourceAndExec) cmd := exec.Command("bash", "-c", sourceAndExec)
output, err := cmd.Output() if err := internal.RunCmd(cmd); err != nil {
if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
fmt.Print(string(output))
return nil 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)
}
},
} }

View File

@ -1,6 +1,7 @@
package app package app
import ( import (
"fmt"
"os" "os"
"path" "path"
"strings" "strings"
@ -44,8 +45,20 @@ var appCheckCommand = &cli.Command{
logrus.Fatalf("%s is missing %s", app.Path, missingEnvVars) logrus.Fatalf("%s is missing %s", app.Path, missingEnvVars)
} }
logrus.Info("All necessary environment variables defined") logrus.Infof("all necessary environment variables defined for '%s'", app.Name)
return nil 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)
}
},
} }

View File

@ -1,10 +1,12 @@
package app package app
import ( import (
"fmt"
"os" "os"
"os/exec" "os/exec"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -38,4 +40,16 @@ var appConfigCommand = &cli.Command{
return nil 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)
}
},
} }

View File

@ -1,11 +1,11 @@
package app package app
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"strings" "strings"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
@ -55,11 +55,13 @@ var appCpCommand = &cli.Command{
service = parsedSrc[0] service = parsedSrc[0]
srcPath = parsedSrc[1] srcPath = parsedSrc[1]
dstPath = dst dstPath = dst
logrus.Debugf("assuming transfer is coming FROM the container")
} else if len(parsedDst) == 2 { } else if len(parsedDst) == 2 {
service = parsedDst[0] service = parsedDst[0]
dstPath = parsedDst[1] dstPath = parsedDst[1]
srcPath = src srcPath = src
isToContainer = true // <src> <container:dst> isToContainer = true // <src> <container:dst>
logrus.Debugf("assuming transfer is going TO the container")
} }
appFiles, err := config.LoadAppFiles("") appFiles, err := config.LoadAppFiles("")
@ -72,7 +74,6 @@ var appCpCommand = &cli.Command{
logrus.Fatal(err) logrus.Fatal(err)
} }
ctx := context.Background()
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -80,7 +81,7 @@ var appCpCommand = &cli.Command{
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("%s_%s", appEnv.StackName(), service)) filters.Add("name", fmt.Sprintf("%s_%s", appEnv.StackName(), service))
containers, err := cl.ContainerList(ctx, types.ContainerListOptions{Filters: filters}) containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -90,6 +91,8 @@ var appCpCommand = &cli.Command{
} }
container := containers[0] container := containers[0]
logrus.Debugf("retrieved '%s' as target container on '%s'", formatter.ShortenID(container.ID), app.Server)
if isToContainer { if isToContainer {
if _, err := os.Stat(srcPath); err != nil { if _, err := os.Stat(srcPath); err != nil {
logrus.Fatalf("'%s' does not exist?", srcPath) logrus.Fatalf("'%s' does not exist?", srcPath)
@ -102,11 +105,11 @@ var appCpCommand = &cli.Command{
} }
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false} copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(ctx, container.ID, dstPath, content, copyOpts); err != nil { if err := cl.CopyToContainer(c.Context, container.ID, dstPath, content, copyOpts); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} else { } else {
content, _, err := cl.CopyFromContainer(ctx, container.ID, srcPath) content, _, err := cl.CopyFromContainer(c.Context, container.ID, srcPath)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -31,7 +31,6 @@ var appDeployCommand = &cli.Command{
for k, v := range abraShEnv { for k, v := range abraShEnv {
app.Env[k] = v app.Env[k] = v
} }
app.Env["STACK_NAME"] = app.StackName()
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env) composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
if err != nil { if err != nil {
@ -54,4 +53,16 @@ var appDeployCommand = &cli.Command{
return nil 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)
}
},
} }

View File

@ -2,9 +2,12 @@ package app
import ( import (
"sort" "sort"
"strings"
abraFormatter "coopcloud.tech/abra/cli/formatter" abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/tagcmp"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -65,10 +68,10 @@ can take some time.
} }
sort.Sort(config.ByServerAndType(apps)) sort.Sort(config.ByServerAndType(apps))
statuses := map[string]string{} statuses := make(map[string]map[string]string)
tableCol := []string{"Server", "Type", "Domain"} tableCol := []string{"Server", "Type", "Domain"}
if status { if status {
tableCol = append(tableCol, "Status") tableCol = append(tableCol, "Status", "Version", "Updates")
statuses, err = config.GetAppStatuses(appFiles) statuses, err = config.GetAppStatuses(appFiles)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -84,10 +87,57 @@ can take some time.
// If type flag is set, check for it, if not, Type == "" // If type flag is set, check for it, if not, Type == ""
tableRow = []string{app.Server, app.Type, app.Domain} tableRow = []string{app.Server, app.Type, app.Domain}
if status { if status {
if status, ok := statuses[app.StackName()]; ok { stackName := app.StackName()
tableRow = append(tableRow, status) 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)
} else { } else {
tableRow = append(tableRow, "unknown") tableRow = append(tableRow, status, version)
}
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")
}
} 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"))
} }
} }
} }
@ -95,6 +145,7 @@ can take some time.
} }
table.Render() table.Render()
return nil return nil
}, },
} }

View File

@ -1,7 +1,6 @@
package app package app
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -9,6 +8,7 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
@ -17,12 +17,11 @@ import (
) )
// stackLogs lists logs for all stack services // stackLogs lists logs for all stack services
func stackLogs(stackName string, client *dockerClient.Client) { func stackLogs(c *cli.Context, stackName string, client *dockerClient.Client) {
ctx := context.Background()
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", stackName) filters.Add("name", stackName)
serviceOpts := types.ServiceListOptions{Filters: filters} serviceOpts := types.ServiceListOptions{Filters: filters}
services, err := client.ServiceList(ctx, serviceOpts) services, err := client.ServiceList(c.Context, serviceOpts)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -39,7 +38,7 @@ func stackLogs(stackName string, client *dockerClient.Client) {
Tail: "20", Tail: "20",
Timestamps: true, Timestamps: true,
} }
logs, err := client.ServiceLogs(ctx, s, logOpts) logs, err := client.ServiceLogs(c.Context, s, logOpts)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -64,7 +63,6 @@ var appLogsCommand = &cli.Command{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
ctx := context.Background()
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -72,14 +70,16 @@ var appLogsCommand = &cli.Command{
serviceName := c.Args().Get(1) serviceName := c.Args().Get(1)
if serviceName == "" { if serviceName == "" {
stackLogs(app.StackName(), cl) 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) service := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", service) filters.Add("name", service)
serviceOpts := types.ServiceListOptions{Filters: filters} serviceOpts := types.ServiceListOptions{Filters: filters}
services, err := cl.ServiceList(ctx, serviceOpts) services, err := cl.ServiceList(c.Context, serviceOpts)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -95,7 +95,7 @@ var appLogsCommand = &cli.Command{
Tail: "20", Tail: "20",
Timestamps: true, Timestamps: true,
} }
logs, err := cl.ServiceLogs(ctx, services[0].ID, logOpts) logs, err := cl.ServiceLogs(c.Context, services[0].ID, logOpts)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -109,4 +109,16 @@ var appLogsCommand = &cli.Command{
return nil 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)
}
},
} }

View File

@ -8,7 +8,6 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue" "coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -32,7 +31,7 @@ var newAppServerFlag = &cli.StringFlag{
Aliases: []string{"s"}, Aliases: []string{"s"},
Value: "", Value: "",
Usage: "Show apps of a specific server", Usage: "Show apps of a specific server",
Destination: &listAppServer, Destination: &newAppServer,
} }
var newAppName string var newAppName string
@ -78,25 +77,18 @@ var appNewCommand = &cli.Command{
}, },
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Action: action, Action: action,
} BashComplete: func(c *cli.Context) {
catl, err := catalogue.ReadRecipeCatalogue()
// getRecipeMeta retrieves the recipe metadata from the recipe catalogue. if err != nil {
func getRecipeMeta(recipeName string) (catalogue.RecipeMeta, error) { logrus.Warn(err)
catl, err := catalogue.ReadRecipeCatalogue() }
if err != nil { if c.NArg() > 0 {
return catalogue.RecipeMeta{}, err return
} }
for name := range catl {
recipeMeta, ok := catl[recipeName] fmt.Println(name)
if !ok { }
err := fmt.Errorf("recipe '%s' does not exist?", recipeName) },
return catalogue.RecipeMeta{}, err
}
if err := recipePkg.EnsureExists(recipeMeta.Name); err != nil {
return catalogue.RecipeMeta{}, err
}
return recipeMeta, nil
} }
// ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/ // ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
@ -114,11 +106,11 @@ func ensureDomainFlag() error {
// ensureServerFlag checks if the server flag was used. if not, asks the user for it. // ensureServerFlag checks if the server flag was used. if not, asks the user for it.
func ensureServerFlag() error { func ensureServerFlag() error {
appFiles, err := config.LoadAppFiles(newAppServer) servers, err := config.GetServers()
if err != nil { if err != nil {
return err return err
} }
servers := appFiles.GetServers()
if newAppServer == "" { if newAppServer == "" {
prompt := &survey.Select{ prompt := &survey.Select{
Message: "Select app server:", Message: "Select app server:",
@ -128,6 +120,7 @@ func ensureServerFlag() error {
return err return err
} }
} }
return nil return nil
} }
@ -178,16 +171,6 @@ func action(c *cli.Context) error {
logrus.Fatal(err) logrus.Fatal(err)
} }
recipeMeta, err := getRecipeMeta(recipe.Name)
if err != nil {
logrus.Fatal(err)
}
latestVersion := recipeMeta.LatestVersion()
if err := recipePkg.EnsureVersion(recipe.Name, latestVersion); err != nil {
logrus.Fatal(err)
}
if err := ensureServerFlag(); err != nil { if err := ensureServerFlag(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -204,8 +187,9 @@ func action(c *cli.Context) error {
if len(sanitisedAppName) > 45 { if len(sanitisedAppName) > 45 {
logrus.Fatalf("'%s' cannot be longer than 45 characters", sanitisedAppName) logrus.Fatalf("'%s' cannot be longer than 45 characters", sanitisedAppName)
} }
logrus.Debugf("'%s' sanitised as '%s' for new app", newAppName, sanitisedAppName)
if err := config.CopyAppEnvSample(recipe.Name, newAppName, newAppServer); err != nil { if err := config.TemplateAppEnvSample(recipe.Name, newAppName, newAppServer, domain, recipe.Name); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -220,13 +204,27 @@ func action(c *cli.Context) error {
for secret := range secrets { for secret := range secrets {
secretTable.Append([]string{secret, secrets[secret]}) secretTable.Append([]string{secret, secrets[secret]})
} }
defer secretTable.Render()
if len(secrets) > 0 {
defer secretTable.Render()
}
} }
tableCol := []string{"Name", "Domain", "Type", "Server"} tableCol := []string{"Name", "Domain", "Type", "Server"}
table := abraFormatter.CreateTable(tableCol) table := abraFormatter.CreateTable(tableCol)
table.Append([]string{sanitisedAppName, domain, recipe.Name, newAppServer}) table.Append([]string{sanitisedAppName, domain, recipe.Name, newAppServer})
defer table.Render()
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 return nil
} }

View File

@ -1,12 +1,13 @@
package app package app
import ( import (
"context" "fmt"
"strings" "strings"
abraFormatter "coopcloud.tech/abra/cli/formatter" abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
@ -21,7 +22,6 @@ var appPsCommand = &cli.Command{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
ctx := context.Background()
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -30,12 +30,12 @@ var appPsCommand = &cli.Command{
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", app.StackName()) filters.Add("name", app.StackName())
containers, err := cl.ContainerList(ctx, types.ContainerListOptions{Filters: filters}) containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
tableCol := []string{"ID", "Image", "Command", "Created", "Status", "Ports", "Names"} tableCol := []string{"id", "image", "command", "created", "status", "ports", "names"}
table := abraFormatter.CreateTable(tableCol) table := abraFormatter.CreateTable(tableCol)
for _, container := range containers { for _, container := range containers {
@ -46,7 +46,7 @@ var appPsCommand = &cli.Command{
abraFormatter.HumanDuration(container.Created), abraFormatter.HumanDuration(container.Created),
container.Status, container.Status,
formatter.DisplayablePorts(container.Ports), formatter.DisplayablePorts(container.Ports),
strings.Join(container.Names, ","), strings.Join(container.Names, ", "),
} }
table.Append(tableRow) table.Append(tableRow)
} }
@ -54,4 +54,16 @@ var appPsCommand = &cli.Command{
table.Render() table.Render()
return nil 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)
}
},
} }

View File

@ -1,7 +1,6 @@
package app package app
import ( import (
"context"
"fmt" "fmt"
"os" "os"
@ -39,13 +38,13 @@ var appRemoveCommand = &cli.Command{
if !internal.Force { if !internal.Force {
response := false response := false
prompt := &survey.Confirm{ prompt := &survey.Confirm{
Message: fmt.Sprintf("About to delete %s, are you sure", app.Name), Message: fmt.Sprintf("about to delete %s, are you sure?", app.Name),
} }
if err := survey.AskOne(prompt, &response); err != nil { if err := survey.AskOne(prompt, &response); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !response { if !response {
logrus.Fatal("User aborted app removal") logrus.Fatal("user aborted app removal")
} }
} }
@ -54,7 +53,6 @@ var appRemoveCommand = &cli.Command{
logrus.Fatal(err) logrus.Fatal(err)
} }
ctx := context.Background()
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -65,14 +63,14 @@ var appRemoveCommand = &cli.Command{
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if statuses[app.Name] == "deployed" { if statuses[app.Name]["status"] == "deployed" {
logrus.Fatalf("'%s' is still deployed. Run \"abra app %s undeploy\" or pass --force", app.Name, app.Name) logrus.Fatalf("'%s' is still deployed. Run \"abra app %s undeploy\" or pass --force", app.Name, app.Name)
} }
} }
fs := filters.NewArgs() fs := filters.NewArgs()
fs.Add("name", app.Name) fs.Add("name", app.Name)
secretList, err := cl.SecretList(ctx, types.SecretListOptions{Filters: fs}) secretList, err := cl.SecretList(c.Context, types.SecretListOptions{Filters: fs})
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -89,7 +87,7 @@ var appRemoveCommand = &cli.Command{
var secretNamesToRemove []string var secretNamesToRemove []string
if !internal.Force { if !internal.Force {
secretsPrompt := &survey.MultiSelect{ secretsPrompt := &survey.MultiSelect{
Message: "Which secrets do you want to remove?", Message: "which secrets do you want to remove?",
Options: secretNames, Options: secretNames,
Default: secretNames, Default: secretNames,
} }
@ -99,17 +97,17 @@ var appRemoveCommand = &cli.Command{
} }
for _, name := range secretNamesToRemove { for _, name := range secretNamesToRemove {
err := cl.SecretRemove(ctx, secrets[name]) err := cl.SecretRemove(c.Context, secrets[name])
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Info(fmt.Sprintf("Secret: %s removed", name)) logrus.Info(fmt.Sprintf("secret: %s removed", name))
} }
} else { } else {
logrus.Info("No secrets to remove") logrus.Info("no secrets to remove")
} }
volumeListOKBody, err := cl.VolumeList(ctx, fs) volumeListOKBody, err := cl.VolumeList(c.Context, fs)
volumeList := volumeListOKBody.Volumes volumeList := volumeListOKBody.Volumes
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -125,7 +123,7 @@ var appRemoveCommand = &cli.Command{
var removeVols []string var removeVols []string
if !internal.Force { if !internal.Force {
volumesPrompt := &survey.MultiSelect{ volumesPrompt := &survey.MultiSelect{
Message: "Which volumes do you want to remove?", Message: "which volumes do you want to remove?",
Options: vols, Options: vols,
Default: vols, Default: vols,
} }
@ -134,25 +132,37 @@ var appRemoveCommand = &cli.Command{
} }
} }
for _, vol := range removeVols { for _, vol := range removeVols {
err := cl.VolumeRemove(ctx, vol, internal.Force) // last argument is for force removing err := cl.VolumeRemove(c.Context, vol, internal.Force) // last argument is for force removing
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Info(fmt.Sprintf("Volume %s removed", vol)) logrus.Info(fmt.Sprintf("volume %s removed", vol))
} }
} else { } else {
logrus.Info("No volumes were removed") logrus.Info("no volumes were removed")
} }
} else { } else {
logrus.Info("No volumes to remove") logrus.Info("no volumes to remove")
} }
err = os.Remove(app.Path) err = os.Remove(app.Path)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Info(fmt.Sprintf("File: %s removed", app.Path)) logrus.Info(fmt.Sprintf("file: %s removed", app.Path))
return nil 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)
}
},
} }

View File

@ -70,13 +70,10 @@ var appRestoreCommand = &cli.Command{
sourceAndExec := fmt.Sprintf("%s; %s", sourceCmd, execCmd) sourceAndExec := fmt.Sprintf("%s; %s", sourceCmd, execCmd)
cmd := exec.Command("bash", "-c", sourceAndExec) cmd := exec.Command("bash", "-c", sourceAndExec)
output, err := cmd.Output() if err := internal.RunCmd(cmd); err != nil {
if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
fmt.Print(string(output))
return nil return nil
}, },
} }

View File

@ -1,10 +1,82 @@
package app package app
import "github.com/urfave/cli/v2" import (
"fmt"
"context"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var appRollbackCommand = &cli.Command{ var appRollbackCommand = &cli.Command{
Name: "rollback", Name: "rollback",
Usage: "Roll an app back to a previous version", Usage: "Roll an app back to a previous version",
Aliases: []string{"b"}, Aliases: []string{"r"},
ArgsUsage: "[<version>]", ArgsUsage: "[<version>]",
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
ctx := context.Background()
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
recipeMeta, err := catalogue.GetRecipeMeta(app.Type)
if err != nil {
logrus.Fatal(err)
}
if len(recipeMeta.Versions) == 0 {
logrus.Fatalf("no catalogue versions available for '%s'", app.Type)
}
deployedVersions, isDeployed, err := appPkg.DeployedVersions(ctx, cl, app)
if err != nil {
logrus.Fatal(err)
}
if !isDeployed {
logrus.Fatalf("'%s' is not deployed?", app.Name)
}
if _, exists := deployedVersions["app"]; !exists {
logrus.Fatalf("no versioned 'app' service for '%s', cannot determine version", app.Name)
}
version := c.Args().Get(1)
if version == "" {
// TODO:
// using deployedVersions["app"], get index+1 version from catalogue
// otherwise bail out saying there is nothing to rollback to
} else {
// TODO
// ensure this version is listed in the catalogue
// ensure this version is "older" (lower down in the list)
}
// TODO
// display table of existing state and expected state and prompt
// run the deployment with this target version!
logrus.Fatal("command not implemented yet, coming soon TM")
return nil
},
} }

View File

@ -1,13 +1,13 @@
package app package app
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/client/container" "coopcloud.tech/abra/pkg/client/container"
"coopcloud.tech/abra/pkg/config"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
@ -42,10 +42,13 @@ var appRunCommand = &cli.Command{
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if c.Args().Len() < 2 { if c.Args().Len() < 2 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <service> provided")) internal.ShowSubcommandHelpAndError(c, errors.New("no <service> provided?"))
}
if c.Args().Len() < 3 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <args> provided?"))
} }
ctx := context.Background()
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -55,7 +58,7 @@ var appRunCommand = &cli.Command{
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("%s_%s", app.StackName(), serviceName)) filters.Add("name", fmt.Sprintf("%s_%s", app.StackName(), serviceName))
containers, err := cl.ContainerList(ctx, types.ContainerListOptions{Filters: filters}) containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -96,4 +99,25 @@ var appRunCommand = &cli.Command{
return nil 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)
}
}
},
} }

View File

@ -1,7 +1,6 @@
package app package app
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@ -10,6 +9,7 @@ import (
abraFormatter "coopcloud.tech/abra/cli/formatter" abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
@ -82,13 +82,13 @@ var appSecretGenerateCommand = &cli.Command{
os.Exit(1) os.Exit(1)
} }
tableCol := []string{"Name", "Value"} tableCol := []string{"name", "value"}
table := abraFormatter.CreateTable(tableCol) table := abraFormatter.CreateTable(tableCol)
for name, val := range secretVals { for name, val := range secretVals {
table.Append([]string{name, val}) table.Append([]string{name, val})
} }
table.Render() table.Render()
logrus.Warn("these secrets are not shown again, please take note of them *now*") logrus.Warn("generated secrets are not shown again, please take note of them *now*")
return nil return nil
}, },
@ -143,7 +143,6 @@ var appSecretRmCommand = &cli.Command{
internal.ShowSubcommandHelpAndError(c, errors.New("no secret(s) specified?")) internal.ShowSubcommandHelpAndError(c, errors.New("no secret(s) specified?"))
} }
ctx := context.Background()
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -151,7 +150,7 @@ var appSecretRmCommand = &cli.Command{
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", app.StackName()) filters.Add("name", app.StackName())
secretList, err := cl.SecretList(ctx, types.SecretListOptions{Filters: filters}) secretList, err := cl.SecretList(c.Context, types.SecretListOptions{Filters: filters})
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -161,7 +160,7 @@ var appSecretRmCommand = &cli.Command{
secretName := cont.Spec.Annotations.Name secretName := cont.Spec.Annotations.Name
parsed := secret.ParseGeneratedSecretName(secretName, app) parsed := secret.ParseGeneratedSecretName(secretName, app)
if allSecrets { if allSecrets {
if err := cl.SecretRemove(ctx, secretName); err != nil { if err := cl.SecretRemove(c.Context, secretName); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if internal.Pass { if internal.Pass {
@ -171,7 +170,7 @@ var appSecretRmCommand = &cli.Command{
} }
} else { } else {
if parsed == secretToRm { if parsed == secretToRm {
if err := cl.SecretRemove(ctx, secretName); err != nil { if err := cl.SecretRemove(c.Context, secretName); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if internal.Pass { if internal.Pass {
@ -198,7 +197,6 @@ var appSecretLsCommand = &cli.Command{
tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"} tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"}
table := abraFormatter.CreateTable(tableCol) table := abraFormatter.CreateTable(tableCol)
ctx := context.Background()
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -206,7 +204,7 @@ var appSecretLsCommand = &cli.Command{
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", app.StackName()) filters.Add("name", app.StackName())
secretList, err := cl.SecretList(ctx, types.SecretListOptions{Filters: filters}) secretList, err := cl.SecretList(c.Context, types.SecretListOptions{Filters: filters})
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -234,6 +232,18 @@ var appSecretLsCommand = &cli.Command{
table.Render() table.Render()
return nil 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{ var appSecretCommand = &cli.Command{

View File

@ -1,11 +1,12 @@
package app package app
import ( import (
"context" "fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
stack "coopcloud.tech/abra/pkg/client/stack" stack "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -22,17 +23,28 @@ volumes as eligiblef or pruning once undeployed.
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
ctx := context.Background()
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
rmOpts := stack.Remove{Namespaces: []string{app.StackName()}} rmOpts := stack.Remove{Namespaces: []string{app.StackName()}}
if err := stack.RunRemove(ctx, cl, rmOpts); err != nil { if err := stack.RunRemove(c.Context, cl, rmOpts); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
return nil 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)
}
},
} }

View File

@ -7,6 +7,7 @@ import (
abraFormatter "coopcloud.tech/abra/cli/formatter" abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client/stack" "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
@ -24,22 +25,10 @@ func getImagePath(image string) (string, error) {
if strings.Contains(path, "library") { if strings.Contains(path, "library") {
path = strings.Split(path, "/")[1] path = strings.Split(path, "/")[1]
} }
logrus.Debugf("parsed '%s' from '%s'", path, image)
return path, nil return path, nil
} }
// parseVersionLabel parses a $STACK_NAME_$SERVICE_NAME service label
func parseServiceName(label string) string {
idx := strings.LastIndex(label, "_")
return label[idx+1:]
}
// parseVersionLabel parses a $VERSION-$DIGEST service label
func parseVersionLabel(label string) (string, string) {
// versions may look like v4.2-abcd or v4.2-alpine-abcd
idx := strings.LastIndex(label, "-")
return label[:idx], label[idx+1:]
}
var appVersionCommand = &cli.Command{ var appVersionCommand = &cli.Command{
Name: "version", Name: "version",
Aliases: []string{"v"}, Aliases: []string{"v"},
@ -65,14 +54,14 @@ var appVersionCommand = &cli.Command{
}(app.Server, label) }(app.Server, label)
} }
tableCol := []string{"Name", "Image", "Version", "Digest"} tableCol := []string{"name", "image", "version", "digest"}
table := abraFormatter.CreateTable(tableCol) table := abraFormatter.CreateTable(tableCol)
statuses := make(map[string]stack.StackStatus) statuses := make(map[string]stack.StackStatus)
for range compose.Services { for range compose.Services {
status := <-ch status := <-ch
if len(status.Services) > 0 { if len(status.Services) > 0 {
serviceName := parseServiceName(status.Services[0].Spec.Name) serviceName := appPkg.ParseServiceName(status.Services[0].Spec.Name)
statuses[serviceName] = status statuses[serviceName] = status
} }
} }
@ -85,7 +74,7 @@ var appVersionCommand = &cli.Command{
if status, ok := statuses[service.Name]; ok { if status, ok := statuses[service.Name]; ok {
statusService := status.Services[0] statusService := status.Services[0]
label := fmt.Sprintf("coop-cloud.%s.%s.version", app.StackName(), service.Name) label := fmt.Sprintf("coop-cloud.%s.%s.version", app.StackName(), service.Name)
version, digest := parseVersionLabel(statusService.Spec.Labels[label]) version, digest := appPkg.ParseVersionLabel(statusService.Spec.Labels[label])
image, err := getImagePath(statusService.Spec.Labels["com.docker.stack.image"]) image, err := getImagePath(statusService.Spec.Labels["com.docker.stack.image"])
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -104,4 +93,16 @@ var appVersionCommand = &cli.Command{
table.Render() table.Render()
return nil 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)
}
},
} }

View File

@ -1,11 +1,12 @@
package app package app
import ( import (
"context" "fmt"
abraFormatter "coopcloud.tech/abra/cli/formatter" abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -13,18 +14,17 @@ import (
var appVolumeListCommand = &cli.Command{ var appVolumeListCommand = &cli.Command{
Name: "list", Name: "list",
Usage: "list volumes associated with an app", Usage: "List volumes associated with an app",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
ctx := context.Background() volumeList, err := client.GetVolumes(c.Context, app.Server, app.Name)
volumeList, err := client.GetVolumes(ctx, app.Server, app.Name)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
table := abraFormatter.CreateTable([]string{"DRIVER", "VOLUME NAME"}) table := abraFormatter.CreateTable([]string{"driver", "volume name"})
var volTable [][]string var volTable [][]string
for _, volume := range volumeList { for _, volume := range volumeList {
volRow := []string{ volRow := []string{
@ -43,7 +43,7 @@ var appVolumeListCommand = &cli.Command{
var appVolumeRemoveCommand = &cli.Command{ var appVolumeRemoveCommand = &cli.Command{
Name: "remove", Name: "remove",
Usage: "remove volume(s) associated with an app", Usage: "Remove volume(s) associated with an app",
Aliases: []string{"rm"}, Aliases: []string{"rm"},
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.ForceFlag, internal.ForceFlag,
@ -51,8 +51,7 @@ var appVolumeRemoveCommand = &cli.Command{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
ctx := context.Background() volumeList, err := client.GetVolumes(c.Context, app.Server, app.Name)
volumeList, err := client.GetVolumes(ctx, app.Server, app.Name)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -61,7 +60,7 @@ var appVolumeRemoveCommand = &cli.Command{
var volumesToRemove []string var volumesToRemove []string
if !internal.Force { if !internal.Force {
volumesPrompt := &survey.MultiSelect{ volumesPrompt := &survey.MultiSelect{
Message: "Which volumes do you want to remove?", Message: "which volumes do you want to remove?",
Options: volumeNames, Options: volumeNames,
Default: volumeNames, Default: volumeNames,
} }
@ -72,15 +71,27 @@ var appVolumeRemoveCommand = &cli.Command{
volumesToRemove = volumeNames volumesToRemove = volumeNames
} }
err = client.RemoveVolumes(ctx, app.Server, volumesToRemove, internal.Force) err = client.RemoveVolumes(c.Context, app.Server, volumesToRemove, internal.Force)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Info("Volumes removed successfully.") logrus.Info("volumes removed successfully")
return nil 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{ var appVolumeCommand = &cli.Command{

View File

@ -0,0 +1,17 @@
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
cli/catalogue/generate.go Normal file
View File

@ -0,0 +1,173 @@
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
},
}

View File

@ -4,10 +4,14 @@ package cli
import ( import (
"fmt" "fmt"
"os" "os"
"path"
"coopcloud.tech/abra/cli/app" "coopcloud.tech/abra/cli/app"
"coopcloud.tech/abra/cli/catalogue"
"coopcloud.tech/abra/cli/recipe" "coopcloud.tech/abra/cli/recipe"
"coopcloud.tech/abra/cli/server" "coopcloud.tech/abra/cli/server"
"coopcloud.tech/abra/pkg/config"
logrusStack "github.com/Gurpartap/logrus-stack"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -54,6 +58,7 @@ func RunApp(version, commit string) {
app.AppCommand, app.AppCommand,
server.ServerCommand, server.ServerCommand,
recipe.RecipeCommand, recipe.RecipeCommand,
catalogue.CatalogueCommand,
VersionCommand, VersionCommand,
UpgradeCommand, UpgradeCommand,
}, },
@ -62,14 +67,46 @@ func RunApp(version, commit string) {
DebugFlag, DebugFlag,
}, },
Authors: []*cli.Author{ Authors: []*cli.Author{
&cli.Author{ {
Name: "Autonomic Co-op", Name: "Autonomic Co-op",
Email: "helo@autonomic.zone", Email: "helo@autonomic.zone",
}, },
}, },
} }
app.EnableBashCompletion = true 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 { if err := app.Run(os.Args); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -9,6 +9,7 @@ import (
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/docker/go-units" "github.com/docker/go-units"
"github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter"
"github.com/schollz/progressbar/v3"
) )
func ShortenID(str string) string { func ShortenID(str string) string {
@ -31,8 +32,21 @@ func HumanDuration(timestamp int64) string {
return units.HumanDuration(now.Sub(date)) + " ago" return units.HumanDuration(now.Sub(date)) + " ago"
} }
// CreateTable prepares a table layout for output.
func CreateTable(columns []string) *tablewriter.Table { func CreateTable(columns []string) *tablewriter.Table {
table := tablewriter.NewWriter(os.Stdout) table := tablewriter.NewWriter(os.Stdout)
table.SetHeader(columns) table.SetHeader(columns)
return table 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),
)
}

View File

@ -6,6 +6,7 @@ import (
"os/exec" "os/exec"
) )
// RunCmd runs a shell command and streams stdout/stderr in real-time.
func RunCmd(cmd *exec.Cmd) error { func RunCmd(cmd *exec.Cmd) error {
r, err := cmd.StdoutPipe() r, err := cmd.StdoutPipe()
if err != nil { if err != nil {

View File

@ -2,7 +2,6 @@ package internal
import ( import (
"errors" "errors"
"os"
"coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
@ -22,9 +21,10 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
recipe, err := recipe.Get(recipeName) recipe, err := recipe.Get(recipeName)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
os.Exit(1)
} }
logrus.Debugf("validated '%s' as recipe argument", recipeName)
return recipe return recipe
} }
@ -39,8 +39,26 @@ func ValidateApp(c *cli.Context) config.App {
app, err := app.Get(appName) app, err := app.Get(appName)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
os.Exit(1)
} }
if err := recipe.EnsureExists(app.Type); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("validated '%s' as app argument", appName)
return app 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
}

View File

@ -7,6 +7,7 @@ import (
"coopcloud.tech/abra/cli/formatter" "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
@ -76,18 +77,30 @@ var recipeLintCommand = &cli.Command{
} }
} }
tableCol := []string{"Rule", "Satisfied"} tableCol := []string{"rule", "satisfied"}
table := formatter.CreateTable(tableCol) table := formatter.CreateTable(tableCol)
table.Append([]string{"Compose files have the expected version", strconv.FormatBool(expectedVersion)}) 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{"environment configuration is provided", strconv.FormatBool(envSampleProvided)})
table.Append([]string{"Recipe contains a service named 'app'", strconv.FormatBool(serviceNamedApp)}) 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{"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 services have a healthcheck enabled", strconv.FormatBool(healthChecksForAllServices)})
table.Append([]string{"All images are using a tag", strconv.FormatBool(allImagesTagged)}) 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{"no usage of unstable 'latest' tags", strconv.FormatBool(noUnstableTags)})
table.Append([]string{"All tags are using a semver-like format", strconv.FormatBool(semverLikeTags)}) table.Append([]string{"all tags are using a semver-like format", strconv.FormatBool(semverLikeTags)})
table.Render() table.Render()
return nil 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)
}
},
} }

View File

@ -23,7 +23,7 @@ var recipeListCommand = &cli.Command{
recipes := catl.Flatten() recipes := catl.Flatten()
sort.Sort(catalogue.ByRecipeName(recipes)) sort.Sort(catalogue.ByRecipeName(recipes))
tableCol := []string{"Name", "Category", "Status"} tableCol := []string{"name", "category", "status"}
table := formatter.CreateTable(tableCol) table := formatter.CreateTable(tableCol)
for _, recipe := range recipes { for _, recipe := range recipes {

View File

@ -8,7 +8,7 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"github.com/go-git/go-git/v5" "coopcloud.tech/abra/pkg/git"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -28,10 +28,8 @@ var recipeNewCommand = &cli.Command{
} }
url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL) url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL)
_, err := git.PlainClone(directory, false, &git.CloneOptions{URL: url, Tags: git.AllTags}) if err := git.Clone(directory, url); err != nil {
if err != nil { return err
logrus.Fatal(err)
return nil
} }
gitRepo := path.Join(config.APPS_DIR, recipe.Name, ".git") gitRepo := path.Join(config.APPS_DIR, recipe.Name, ".git")
@ -39,6 +37,7 @@ var recipeNewCommand = &cli.Command{
logrus.Fatal(err) logrus.Fatal(err)
return nil return nil
} }
logrus.Debugf("removed git repo in '%s'", gitRepo)
toParse := []string{ toParse := []string{
path.Join(config.APPS_DIR, recipe.Name, "README.md"), path.Join(config.APPS_DIR, recipe.Name, "README.md"),
@ -71,7 +70,7 @@ var recipeNewCommand = &cli.Command{
} }
logrus.Infof( logrus.Infof(
"New recipe '%s' created in %s, happy hacking!\n", "new recipe '%s' created in %s, happy hacking!\n",
recipe.Name, path.Join(config.APPS_DIR, recipe.Name), recipe.Name, path.Join(config.APPS_DIR, recipe.Name),
) )

View File

@ -4,6 +4,30 @@ import (
"github.com/urfave/cli/v2" "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. // RecipeCommand defines all recipe related sub-commands.
var RecipeCommand = &cli.Command{ var RecipeCommand = &cli.Command{
Name: "recipe", Name: "recipe",
@ -18,6 +42,7 @@ Cloud community and you can use Abra to read them and create apps for you.
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
recipeListCommand, recipeListCommand,
recipeVersionCommand, recipeVersionCommand,
recipeReleaseCommand,
recipeNewCommand, recipeNewCommand,
recipeUpgradeCommand, recipeUpgradeCommand,
recipeSyncCommand, recipeSyncCommand,

312
cli/recipe/release.go Normal file
View File

@ -0,0 +1,312 @@
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
}

View File

@ -1,62 +1,88 @@
package recipe package recipe
import ( import (
"errors"
"fmt" "fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/catalogue"
"github.com/docker/distribution/reference" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
var recipeSyncCommand = &cli.Command{ var recipeSyncCommand = &cli.Command{
Name: "sync", Name: "sync",
Usage: "Generate new recipe labels", Usage: "Ensure recipe version labels are up-to-date",
Aliases: []string{"s"}, Aliases: []string{"s"},
ArgsUsage: "<recipe> <version>",
Description: ` Description: `
This command will generate labels for each service which correspond to the 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: following format:
coop-cloud.${STACK_NAME}.${SERVICE_NAME}.version=${IMAGE_TAG}-${IMAGE_DIGEST} coop-cloud.${STACK_NAME}.version=<version>
The <recipe> configuration will be updated on the local file system. These The <version> is determined by the recipe maintainer and is specified on the
labels are consumed by abra in other command invocations and used to determine command-line. The <recipe> configuration will be updated on the local file
the versioning metadata of up-and-running containers are. system.
`, `,
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 { Action: func(c *cli.Context) error {
if c.Args().Len() != 2 {
internal.ShowSubcommandHelpAndError(c, errors.New("missing <recipe>/<version> arguments?"))
}
recipe := internal.ValidateRecipe(c) 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 hasAppService := false
for _, service := range recipe.Config.Services { for _, service := range recipe.Config.Services {
services = append(services, service.Name)
if service.Name == "app" { if service.Name == "app" {
hasAppService = true hasAppService = true
logrus.Debugf("detected app service in '%s'", recipe.Name)
} }
} }
if !hasAppService { if !hasAppService {
logrus.Fatal(fmt.Sprintf("No 'app' service defined in '%s', cannot proceed", recipe.Name)) 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
} }
for _, service := range recipe.Config.Services { logrus.Debugf("selecting '%s' as the service to sync version labels", mainService)
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
logrus.Fatal(err)
}
digest, err := client.GetTagDigest(img) label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag)
if err != nil { if err := recipe.UpdateLabel(mainService, label); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
}
tag := img.(reference.NamedTagged).Tag()
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.%s.version=%s-%s", service.Name, tag, digest)
if err := recipe.UpdateLabel(service.Name, label); err != nil {
logrus.Fatal(err)
}
} }
logrus.Infof("synced label '%s' to service '%s'", label, mainService)
return nil return nil
}, },
} }

View File

@ -27,14 +27,24 @@ 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 Some image tags cannot be parsed because they do not follow some sort of
semver-like convention. In this case, all possible tags will be listed and it semver-like convention. In this case, all possible tags will be listed and it
is up to the end-user to decide. is up to the end-user to decide.
This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync
<recipe>".
`, `,
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Flags: []cli.Flag{
PatchFlag,
MinorFlag,
MajorFlag,
},
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c) 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 { for _, service := range recipe.Config.Services {
catlVersions, err := catalogue.VersionsOfService(recipe.Name, service.Name) catlVersions, err := catalogue.VersionsOfService(recipe.Name, service.Name)
if err != nil { if err != nil {
@ -51,6 +61,7 @@ This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("retrieved '%s' from remote registry for '%s'", regVersions, image)
if strings.Contains(image, "library") { if strings.Contains(image, "library") {
// ParseNormalizedNamed prepends 'library' to images like nginx:<tag>, // ParseNormalizedNamed prepends 'library' to images like nginx:<tag>,
@ -61,6 +72,7 @@ This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync
semverLikeTag := true semverLikeTag := true
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) { if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
logrus.Debugf("'%s' not considered semver-like", img.(reference.NamedTagged).Tag())
semverLikeTag = false semverLikeTag = false
} }
@ -68,6 +80,7 @@ This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync
if err != nil && semverLikeTag { if err != nil && semverLikeTag {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("parsed '%s' for '%s'", tag, service.Name)
var compatible []tagcmp.Tag var compatible []tagcmp.Tag
for _, regVersion := range regVersions { for _, regVersion := range regVersions {
@ -81,10 +94,12 @@ This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync
} }
} }
logrus.Debugf("detected potential upgradable tags '%s' for '%s'", compatible, service.Name)
sort.Sort(tagcmp.ByTagDesc(compatible)) sort.Sort(tagcmp.ByTagDesc(compatible))
if len(compatible) == 0 && semverLikeTag { if len(compatible) == 0 && semverLikeTag {
logrus.Info(fmt.Sprintf("No new versions available for '%s', '%s' is the latest", image, tag)) 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 continue // skip on to the next tag and don't update any compose files
} }
@ -101,29 +116,49 @@ This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync
} }
} }
msg := fmt.Sprintf("Which tag would you like to upgrade to? (service: %s, tag: %s)", service.Name, tag) logrus.Debugf("detected compatible upgradable tags '%s' for '%s'", compatibleStrings, service.Name)
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("Which tag would you like to upgrade to? (service: %s, tag: %s)", service.Name, tag)
compatibleStrings = []string{}
for _, regVersion := range regVersions {
compatibleStrings = append(compatibleStrings, regVersion.Name)
}
}
var upgradeTag string var upgradeTag string
prompt := &survey.Select{ if bumpType != 0 {
Message: msg, for _, upTag := range compatible {
Options: compatibleStrings, upElement, err := tag.UpgradeDelta(upTag)
} if err != nil {
if err := survey.AskOne(prompt, &upgradeTag); err != nil { return err
logrus.Fatal(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 { if err := recipe.UpdateTag(image, upgradeTag); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Infof("tag upgraded from '%s' to '%s' for '%s'", tag.String(), upgradeTag, image)
} }
return nil return nil

View File

@ -26,13 +26,13 @@ var recipeVersionCommand = &cli.Command{
logrus.Fatalf("'%s' recipe doesn't exist?", recipe.Name) logrus.Fatalf("'%s' recipe doesn't exist?", recipe.Name)
} }
tableCol := []string{"Version", "Service", "Image", "Digest"} tableCol := []string{"Version", "Service", "Image", "Tag", "Digest"}
table := formatter.CreateTable(tableCol) table := formatter.CreateTable(tableCol)
for _, serviceVersion := range recipeMeta.Versions { for _, serviceVersion := range recipeMeta.Versions {
for tag, meta := range serviceVersion { for tag, meta := range serviceVersion {
for service, serviceMeta := range meta { for service, serviceMeta := range meta {
table.Append([]string{tag, service, serviceMeta.Image, serviceMeta.Digest}) table.Append([]string{tag, service, serviceMeta.Image, serviceMeta.Tag, serviceMeta.Digest})
} }
} }
} }

View File

@ -1,29 +1,138 @@
package server package server
import ( import (
"fmt" "context"
"errors"
"os/user"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/server"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "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{ var serverAddCommand = &cli.Command{
Name: "add", Name: "add",
Usage: "Add a new server, reachable on <server>.", Usage: "Add a new server",
Aliases: []string{"a"}, Description: `
ArgsUsage: "<server> [<user>] [<port>]", This command adds a new server that abra will communicate with, to deploy apps.
Description: "[<user>], [<port>] SSH connection details",
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 { Action: func(c *cli.Context) error {
argLen := c.Args().Len() if c.Args().Len() == 1 && !local {
args := c.Args().Slice() err := errors.New("missing arguments <domain> or '--local'")
if argLen < 3 { internal.ShowSubcommandHelpAndError(c, err)
args = append(args, make([]string, 3-argLen)...)
} }
if err := client.CreateContext(args[0], args[1], args[2]); err != nil {
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) logrus.Fatal(err)
} }
fmt.Println(args[0])
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 return nil
}, },
} }

View File

@ -2,7 +2,6 @@ package server
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net" "net"
"time" "time"
@ -20,25 +19,25 @@ var serverInitCommand = &cli.Command{
Usage: "Initialise server for deploying apps", Usage: "Initialise server for deploying apps",
Aliases: []string{"i"}, Aliases: []string{"i"},
HideHelp: true, HideHelp: true,
ArgsUsage: "<server>", ArgsUsage: "<domain>",
Description: ` Description: `
Initialise swarm mode on the target <server>. Initialise swarm mode on the target <domain>.
This initialisation explicitly chooses the "single host swarm" mode which uses This initialisation explicitly chooses the "single host swarm" mode which uses
the default IPv4 address as the advertising address. This can be re-configured the default IPv4 address as the advertising address. This can be re-configured
later for more advanced use cases. later for more advanced use cases.
`, `,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
server := c.Args().First() domainName := internal.ValidateDomain(c)
if server == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no server provided"))
}
cl, err := client.New(server) cl, err := client.New(domainName)
if err != nil { if err != nil {
return err return err
} }
// https://www.privacy-handbuch.de/handbuch_93d.htm
freifunkDNS := "5.1.66.255:53"
resolver := &net.Resolver{ resolver := &net.Resolver{
PreferGo: false, PreferGo: false,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) { Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
@ -46,18 +45,19 @@ later for more advanced use cases.
Timeout: time.Millisecond * time.Duration(10000), Timeout: time.Millisecond * time.Duration(10000),
} }
// comrade librehosters DNS resolver https://snopyta.org/service/dns/ // comrade librehosters DNS resolver https://snopyta.org/service/dns/
return d.DialContext(ctx, "udp", "95.216.24.230:53") return d.DialContext(ctx, "udp", freifunkDNS)
}, },
} }
logrus.Debugf("created DNS resolver via '%s'", freifunkDNS)
ctx := context.Background() ctx := context.Background()
ips, err := resolver.LookupIPAddr(ctx, server) ips, err := resolver.LookupIPAddr(ctx, domainName)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if len(ips) == 0 { if len(ips) == 0 {
return fmt.Errorf("unable to retrieve ipv4 address for %s", server) return fmt.Errorf("unable to retrieve ipv4 address for %s", domainName)
} }
ipv4 := ips[0].IP.To4().String() ipv4 := ips[0].IP.To4().String()
@ -68,11 +68,13 @@ later for more advanced use cases.
if _, err := cl.SwarmInit(ctx, initReq); err != nil { if _, err := cl.SwarmInit(ctx, initReq); err != nil {
return err return err
} }
logrus.Debugf("initialised swarm on '%s'", domainName)
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"} netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
if _, err := cl.NetworkCreate(ctx, "proxy", netOpts); err != nil { if _, err := cl.NetworkCreate(ctx, "proxy", netOpts); err != nil {
return err return err
} }
logrus.Debug("swarm overlay network 'proxy' created")
return nil return nil
}, },

View File

@ -13,7 +13,7 @@ import (
var serverListCommand = &cli.Command{ var serverListCommand = &cli.Command{
Name: "list", Name: "list",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Usage: "List locally-defined servers.", Usage: "List managed servers",
ArgsUsage: " ", ArgsUsage: " ",
HideHelp: true, HideHelp: true,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
@ -22,6 +22,7 @@ var serverListCommand = &cli.Command{
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
tableColumns := []string{"Name", "Connection"} tableColumns := []string{"Name", "Connection"}
table := formatter.CreateTable(tableColumns) table := formatter.CreateTable(tableColumns)
defer table.Render() defer table.Render()
@ -30,8 +31,8 @@ var serverListCommand = &cli.Command{
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
for _, serverName := range serverNames {
for _, serverName := range serverNames {
var row []string var row []string
for _, ctx := range contexts { for _, ctx := range contexts {
endpoint, err := client.GetContextEndpoint(ctx) endpoint, err := client.GetContextEndpoint(ctx)
@ -44,12 +45,15 @@ var serverListCommand = &cli.Command{
} }
} }
if len(row) == 0 { if len(row) == 0 {
row = []string{serverName, "UNKNOWN"} if serverName == "default" {
row = []string{serverName, "local"}
} else {
row = []string{serverName, "unknown"}
}
} }
table.Append(row) table.Append(row)
} }
return nil
return nil
}, },
} }

View File

@ -79,12 +79,14 @@ environment variable or otherwise passing the "--env/-e" flag.
} }
if hetznerCloudAPIToken == "" { if hetznerCloudAPIToken == "" {
logrus.Fatal("Hetzner Cloud API token is missing, cannot continue") logrus.Fatal("Hetzner Cloud API token is missing")
} }
ctx := context.Background() ctx := context.Background()
client := hcloud.NewClient(hcloud.WithToken(hetznerCloudAPIToken)) client := hcloud.NewClient(hcloud.WithToken(hetznerCloudAPIToken))
logrus.Debugf("successfully created hetzner cloud API client")
var sshKeys []*hcloud.SSHKey var sshKeys []*hcloud.SSHKey
for _, sshKey := range c.StringSlice("ssh-keys") { for _, sshKey := range c.StringSlice("ssh-keys") {
sshKey, _, err := client.SSHKey.GetByName(ctx, sshKey) sshKey, _, err := client.SSHKey.GetByName(ctx, sshKey)
@ -106,13 +108,17 @@ environment variable or otherwise passing the "--env/-e" flag.
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("new server '%s' created", name)
tableColumns := []string{"Name", "IPv4", "Root Password"} tableColumns := []string{"Name", "IPv4", "Root Password"}
table := formatter.CreateTable(tableColumns) table := formatter.CreateTable(tableColumns)
if len(sshKeys) > 0 { if len(sshKeys) > 0 {
table.Append([]string{name, res.Server.PublicNet.IPv4.IP.String(), "N/A (using SSH keys)"}) table.Append([]string{name, res.Server.PublicNet.IPv4.IP.String(), "N/A (using SSH keys)"})
} else { } else {
table.Append([]string{name, res.Server.PublicNet.IPv4.IP.String(), res.RootPassword}) table.Append([]string{name, res.Server.PublicNet.IPv4.IP.String(), res.RootPassword})
} }
table.Render() table.Render()
return nil return nil
@ -182,13 +188,14 @@ environment variable or otherwise passing the "--env/-e" flag.
} }
if capsulAPIToken == "" { if capsulAPIToken == "" {
logrus.Fatal("Capsul API token is missing, cannot continue") logrus.Fatal("Capsul API token is missing")
} }
// yep, the response time is quite slow, something to fix Capsul side // yep, the response time is quite slow, something to fix on the Capsul side
client := &http.Client{Timeout: 20 * time.Second} client := &http.Client{Timeout: 20 * time.Second}
capsulCreateURL := fmt.Sprintf("https://%s/api/capsul/create", capsulInstance) capsulCreateURL := fmt.Sprintf("https://%s/api/capsul/create", capsulInstance)
logrus.Debugf("using '%s' as capsul create url", capsulCreateURL)
values := map[string]string{ values := map[string]string{
"name": name, "name": name,
"size": capsulType, "size": capsulType,
@ -230,6 +237,7 @@ environment variable or otherwise passing the "--env/-e" flag.
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil { if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("capsul created with ID: '%s'", resp.ID)
tableColumns := []string{"Name", "ID"} tableColumns := []string{"Name", "ID"}
table := formatter.CreateTable(tableColumns) table := formatter.CreateTable(tableColumns)
@ -241,10 +249,14 @@ environment variable or otherwise passing the "--env/-e" flag.
} }
var serverNewCommand = &cli.Command{ var serverNewCommand = &cli.Command{
Name: "new", Name: "new",
Usage: "Create a new server using a 3rd party provider", Aliases: []string{"n"},
Description: "Use a provider plugin to create a new server which can then be used to house a new Co-op Cloud installation.", Usage: "Create a new server using a 3rd party provider",
ArgsUsage: "<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{ Subcommands: []*cli.Command{
serverNewHetznerCloudCommand, serverNewHetznerCloudCommand,
serverNewCapsulCommand, serverNewCapsulCommand,

View File

@ -1,8 +1,6 @@
package server package server
import ( import (
"errors"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -19,13 +17,14 @@ internal bookkeeping so that it is not managed any more.
`, `,
HideHelp: true, HideHelp: true,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
server := c.Args().First() domainName := internal.ValidateDomain(c)
if server == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no server provided")) if err := client.DeleteContext(domainName); err != nil {
}
if err := client.DeleteContext(server); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Infof("server at '%s' has been forgotten", domainName)
return nil return nil
}, },
} }

View File

@ -14,6 +14,7 @@ var UpgradeCommand = &cli.Command{
Usage: "Upgrade abra", Usage: "Upgrade abra",
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
cmd := exec.Command("bash", "-c", "curl -s https://install.abra.coopcloud.tech | bash") 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 { if err := internal.RunCmd(cmd); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

19
go.mod
View File

@ -3,9 +3,10 @@ module coopcloud.tech/abra
go 1.17 go 1.17
require ( require (
coopcloud.tech/tagcmp v0.0.0-20210906102006-2a8edd82d75d coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb
github.com/AlecAivazis/survey/v2 v2.3.1 github.com/AlecAivazis/survey/v2 v2.3.1
github.com/Autonomic-Cooperative/godotenv v1.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/cli v20.10.8+incompatible
github.com/docker/distribution v2.7.1+incompatible github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v20.10.8+incompatible github.com/docker/docker v20.10.8+incompatible
@ -16,6 +17,7 @@ require (
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
github.com/olekukonko/tablewriter v0.0.5 github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.8.3
github.com/schultz-is/passgen v1.0.1 github.com/schultz-is/passgen v1.0.1
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/urfave/cli/v2 v2.3.0 github.com/urfave/cli/v2 v2.3.0
@ -38,6 +40,7 @@ require (
github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-metrics v0.0.1 // indirect
github.com/emirpasic/gods v1.12.0 // indirect github.com/emirpasic/gods v1.12.0 // indirect
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/fvbommel/sortorder v1.0.2 // indirect github.com/fvbommel/sortorder v1.0.2 // indirect
github.com/go-git/gcfg v1.5.0 // indirect github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect github.com/go-git/go-billy/v5 v5.3.1 // indirect
@ -53,11 +56,12 @@ require (
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-isatty v0.0.8 // indirect github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/miekg/pkcs11 v1.0.3 // indirect github.com/miekg/pkcs11 v1.0.3 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/moby/sys/mount v0.2.0 // indirect github.com/moby/sys/mount v0.2.0 // indirect
@ -70,6 +74,7 @@ require (
github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.26.0 // indirect github.com/prometheus/common v0.26.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect github.com/prometheus/procfs v0.6.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/sergi/go-diff v1.1.0 // indirect github.com/sergi/go-diff v1.1.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
@ -81,10 +86,10 @@ require (
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.opencensus.io v0.22.3 // indirect go.opencensus.io v0.22.3 // indirect
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
golang.org/x/net v0.0.0-20210326060303-6b1517762897 // indirect golang.org/x/net v0.0.0-20210326060303-6b1517762897 // indirect
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 // indirect
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
golang.org/x/text v0.3.4 // indirect golang.org/x/text v0.3.4 // indirect
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect
google.golang.org/grpc v1.33.2 // indirect google.golang.org/grpc v1.33.2 // indirect

37
go.sum
View File

@ -21,13 +21,15 @@ cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIA
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
coopcloud.tech/tagcmp v0.0.0-20210906102006-2a8edd82d75d h1:5jeUIiToqQ7vTlLeycdGp4Ezurd6/RTNl5K38usHtoo= coopcloud.tech/tagcmp v0.0.0-20211007122926-aa7b4ff851f6 h1:EV1aomKx6n0jLZ847ejeqroE4K9E+yX5NvEyQufkZVk=
coopcloud.tech/tagcmp v0.0.0-20210906102006-2a8edd82d75d/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ= coopcloud.tech/tagcmp v0.0.0-20211007122926-aa7b4ff851f6/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb h1:Jf+Dnna2kXcNQvcA5JMp6d2Uyvg2pIVJfip9+X5FrH0=
coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/AlecAivazis/survey/v2 v2.3.1 h1:lzkuHA60pER7L4eYL8qQJor4bUWlJe4V0gqAT19tdOA= github.com/AlecAivazis/survey/v2 v2.3.1 h1:lzkuHA60pER7L4eYL8qQJor4bUWlJe4V0gqAT19tdOA=
github.com/AlecAivazis/survey/v2 v2.3.1/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg= github.com/AlecAivazis/survey/v2 v2.3.1/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg=
github.com/Autonomic-Cooperative/godotenv v1.3.1 h1:LxRTdqBgXyBu7sM1kY8RXuYYA8OFmeLKowLGOAT0Yw0= github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4 h1:aYUdiI42a4fWfPoUr25XlaJrFEICv24+o/gWhqYS/jk=
github.com/Autonomic-Cooperative/godotenv v1.3.1/go.mod h1:oZRCMMRS318l07ei4DTqbZoOawfJlJ4yyo8juk2v4Rk= github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4/go.mod h1:oZRCMMRS318l07ei4DTqbZoOawfJlJ4yyo8juk2v4Rk=
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
@ -44,6 +46,8 @@ github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 h1:vdT7QwBhJJEVNFMBNhRSFDRCB6O16T28VhvqRgqFyn8=
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4/go.mod h1:SvXOG8ElV28oAiG9zv91SDe5+9PfIr7PPccpr8YyXNs=
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
@ -295,6 +299,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
@ -479,6 +485,7 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck= github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
@ -518,11 +525,13 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@ -534,6 +543,8 @@ github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WT
github.com/miekg/pkcs11 v1.0.3 h1:iMwmD7I5225wv84WxIG/bmxz9AXjWvTWIbM/TYHvWtw= github.com/miekg/pkcs11 v1.0.3 h1:iMwmD7I5225wv84WxIG/bmxz9AXjWvTWIbM/TYHvWtw=
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
@ -662,12 +673,16 @@ github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/schollz/progressbar/v3 v3.8.3 h1:FnLGl3ewlDUP+YdSwveXBaXs053Mem/du+wr7XSYKl8=
github.com/schollz/progressbar/v3 v3.8.3/go.mod h1:pWnVCjSBZsT2X3nx9HfRdnCDrpbevliMeoEVhStwHko=
github.com/schultz-is/passgen v1.0.1 h1:wUINzqW1Xmmy3yREHR6YTj+83VlFYjj2DIDMHzIi5TQ= github.com/schultz-is/passgen v1.0.1 h1:wUINzqW1Xmmy3yREHR6YTj+83VlFYjj2DIDMHzIi5TQ=
github.com/schultz-is/passgen v1.0.1/go.mod h1:NnqzT2aSfvyheNQvBtlLUa0YlPFLDj60Jw2DZVwqiJk= github.com/schultz-is/passgen v1.0.1/go.mod h1:NnqzT2aSfvyheNQvBtlLUa0YlPFLDj60Jw2DZVwqiJk=
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
@ -798,8 +813,9 @@ golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -947,13 +963,16 @@ golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 h1:xrCZDmdtoloIiooiA9q0OQb9r8HejIHYoHGhGCe1pGg=
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@ -1,7 +1,14 @@
package app package app
import ( import (
"context"
"fmt"
"strings"
"coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
apiclient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
) )
// Get retrieves an app // Get retrieves an app
@ -16,5 +23,63 @@ func Get(appName string) (config.App, error) {
return config.App{}, err return config.App{}, err
} }
logrus.Debugf("retrieved '%s' for '%s'", app, appName)
return app, nil 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
}

View File

@ -9,16 +9,26 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os" "os"
"path"
"strings" "strings"
"time" "time"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/web" "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. // RecipeCatalogueURL is the only current recipe catalogue available.
const RecipeCatalogueURL = "https://apps.coopcloud.tech" 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. // image represents a recipe container image.
type image struct { type image struct {
Image string `json:"image"` Image string `json:"image"`
@ -51,17 +61,20 @@ type serviceMeta struct {
Tag string `json:"tag"` 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. // RecipeMeta represents metadata for a recipe in the abra catalogue.
type RecipeMeta struct { type RecipeMeta struct {
Category string `json:"category"` Category string `json:"category"`
DefaultBranch string `json:"default_branch"` DefaultBranch string `json:"default_branch"`
Description string `json:"description"` Description string `json:"description"`
Features features `json:"features"` Features features `json:"features"`
Icon string `json:"icon"` Icon string `json:"icon"`
Name string `json:"name"` Name string `json:"name"`
Repository string `json:"repository"` Repository string `json:"repository"`
Versions []map[tag]map[service]serviceMeta `json:"versions"` Versions RecipeVersions `json:"versions"`
Website string `json:"website"` Website string `json:"website"`
} }
// LatestVersion returns the latest version of a recipe. // LatestVersion returns the latest version of a recipe.
@ -75,6 +88,8 @@ func (r RecipeMeta) LatestVersion() string {
version = tag version = tag
} }
logrus.Debugf("choosing '%s' as latest version of '%s'", version, r.Name)
return version return version
} }
@ -87,9 +102,11 @@ type RecipeCatalogue map[Name]RecipeMeta
// Flatten converts AppCatalogue to slice // Flatten converts AppCatalogue to slice
func (r RecipeCatalogue) Flatten() []RecipeMeta { func (r RecipeCatalogue) Flatten() []RecipeMeta {
recipes := make([]RecipeMeta, 0, len(r)) recipes := make([]RecipeMeta, 0, len(r))
for name := range r { for name := range r {
recipes = append(recipes, r[name]) recipes = append(recipes, r[name])
} }
return recipes return recipes
} }
@ -116,9 +133,11 @@ func recipeCatalogueFSIsLatest() (bool, error) {
if err != nil { if err != nil {
return false, err return false, err
} }
info, err := os.Stat(config.APPS_JSON) info, err := os.Stat(config.APPS_JSON)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
logrus.Debugf("no recipe catalogue found in file system cache")
return false, nil return false, nil
} }
return false, err return false, err
@ -128,9 +147,12 @@ func recipeCatalogueFSIsLatest() (bool, error) {
remoteModifiedTime := parsed.Unix() remoteModifiedTime := parsed.Unix()
if localModifiedTime < remoteModifiedTime { if localModifiedTime < remoteModifiedTime {
logrus.Debug("file system cached recipe catalogue is out-of-date")
return false, nil return false, nil
} }
logrus.Debug("file system cached recipe catalogue is up-to-date")
return true, nil return true, nil
} }
@ -144,12 +166,14 @@ func ReadRecipeCatalogue() (RecipeCatalogue, error) {
} }
if !recipeFSIsLatest { if !recipeFSIsLatest {
logrus.Debugf("reading recipe catalogue from web to get latest")
if err := readRecipeCatalogueWeb(&recipes); err != nil { if err := readRecipeCatalogueWeb(&recipes); err != nil {
return nil, err return nil, err
} }
return recipes, nil return recipes, nil
} }
logrus.Debugf("reading recipe catalogue from file system cache to get latest")
if err := readRecipeCatalogueFS(&recipes); err != nil { if err := readRecipeCatalogueFS(&recipes); err != nil {
return nil, err return nil, err
} }
@ -163,9 +187,13 @@ func readRecipeCatalogueFS(target interface{}) error {
if err != nil { if err != nil {
return err return err
} }
if err := json.Unmarshal(recipesJSONFS, &target); err != nil { if err := json.Unmarshal(recipesJSONFS, &target); err != nil {
return err return err
} }
logrus.Debugf("read recipe catalogue from file system cache in '%s'", config.APPS_JSON)
return nil return nil
} }
@ -184,6 +212,8 @@ func readRecipeCatalogueWeb(target interface{}) error {
return err return err
} }
logrus.Debugf("read recipe catalogue from web at '%s'", RecipeCatalogueURL)
return nil return nil
} }
@ -210,5 +240,263 @@ func VersionsOfService(recipe, serviceName string) ([]string, error) {
} }
} }
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,
Keep: false,
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,
Keep: false,
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 return versions, nil
} }

View File

@ -48,5 +48,7 @@ func New(contextName string) (*client.Client, error) {
logrus.Fatalf("unable to create Docker client: %s", err) logrus.Fatalf("unable to create Docker client: %s", err)
} }
logrus.Debugf("created client for '%s'", contextName)
return cl, nil return cl, nil
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/docker/cli/cli/context/docker" "github.com/docker/cli/cli/context/docker"
contextStore "github.com/docker/cli/cli/context/store" contextStore "github.com/docker/cli/cli/context/store"
"github.com/moby/term" "github.com/moby/term"
"github.com/sirupsen/logrus"
) )
type Context = contextStore.Metadata type Context = contextStore.Metadata
@ -26,6 +27,7 @@ func CreateContext(contextName string, user string, port string) error {
if err := createContext(contextName, host); err != nil { if err := createContext(contextName, host); err != nil {
return err return err
} }
logrus.Debugf("created the '%s' context", contextName)
return nil return nil
} }
@ -36,13 +38,16 @@ func createContext(name string, host string) error {
Endpoints: make(map[string]interface{}), Endpoints: make(map[string]interface{}),
Name: name, Name: name,
} }
contextTLSData := contextStore.ContextTLSData{ contextTLSData := contextStore.ContextTLSData{
Endpoints: make(map[string]contextStore.EndpointTLSData), Endpoints: make(map[string]contextStore.EndpointTLSData),
} }
dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(host) dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(host)
if err != nil { if err != nil {
return err return err
} }
contextMetadata.Endpoints[docker.DockerEndpoint] = dockerEP contextMetadata.Endpoints[docker.DockerEndpoint] = dockerEP
if dockerTLS != nil { if dockerTLS != nil {
contextTLSData.Endpoints[docker.DockerEndpoint] = *dockerTLS contextTLSData.Endpoints[docker.DockerEndpoint] = *dockerTLS
@ -51,9 +56,11 @@ func createContext(name string, host string) error {
if err := s.CreateOrUpdate(contextMetadata); err != nil { if err := s.CreateOrUpdate(contextMetadata); err != nil {
return err return err
} }
if err := s.ResetTLSMaterial(name, &contextTLSData); err != nil { if err := s.ResetTLSMaterial(name, &contextTLSData); err != nil {
return err return err
} }
return nil return nil
} }
@ -61,6 +68,7 @@ func DeleteContext(name string) error {
if name == "default" { if name == "default" {
return errors.New("context 'default' cannot be removed") return errors.New("context 'default' cannot be removed")
} }
if _, err := GetContext(name); err != nil { if _, err := GetContext(name); err != nil {
return err return err
} }
@ -81,6 +89,7 @@ func GetContext(contextName string) (contextStore.Metadata, error) {
if err != nil { if err != nil {
return contextStore.Metadata{}, err return contextStore.Metadata{}, err
} }
return ctx, nil return ctx, nil
} }

View File

@ -18,7 +18,7 @@ import (
func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error { func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error {
var errs []string var errs []string
for _, namespace := range opts.Namespaces { for _, namespace := range opts.Namespaces {
services, err := getStackServices(ctx, client, namespace) services, err := GetStackServices(ctx, client, namespace)
if err != nil { if err != nil {
return err return err
} }

View File

@ -47,7 +47,7 @@ func getAllStacksFilter() filters.Args {
return filter return filter
} }
func getStackServices(ctx context.Context, dockerclient client.APIClient, namespace string) ([]swarm.Service, error) { func GetStackServices(ctx context.Context, dockerclient client.APIClient, namespace string) ([]swarm.Service, error) {
return dockerclient.ServiceList(ctx, types.ServiceListOptions{Filters: getStackServiceFilter(namespace)}) return dockerclient.ServiceList(ctx, types.ServiceListOptions{Filters: getStackServiceFilter(namespace)})
} }
@ -94,7 +94,7 @@ func GetAllDeployedServices(contextName string) StackStatus {
// pruneServices removes services that are no longer referenced in the source // 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{}) { func pruneServices(ctx context.Context, cl *dockerclient.Client, namespace convert.Namespace, services map[string]struct{}) {
oldServices, err := getStackServices(ctx, cl, namespace.Name()) oldServices, err := GetStackServices(ctx, cl, namespace.Name())
if err != nil { if err != nil {
logrus.Infof("Failed to list services: %s\n", err) logrus.Infof("Failed to list services: %s\n", err)
} }
@ -174,6 +174,7 @@ func deployCompose(ctx context.Context, cl *dockerclient.Client, opts Deploy, co
if err != nil { if err != nil {
return err return err
} }
return deployServices(ctx, cl, services, namespace, opts.SendRegistryAuth, opts.ResolveImage) return deployServices(ctx, cl, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
} }
@ -290,7 +291,7 @@ func deployServices(
namespace convert.Namespace, namespace convert.Namespace,
sendAuth bool, sendAuth bool,
resolveImage string) error { resolveImage string) error {
existingServices, err := getStackServices(ctx, cl, namespace.Name()) existingServices, err := GetStackServices(ctx, cl, namespace.Name())
if err != nil { if err != nil {
return err return err
} }

View File

@ -3,27 +3,37 @@ package compose
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
"coopcloud.tech/abra/pkg/client/stack" "coopcloud.tech/abra/pkg/client/stack"
loader "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" composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// UpdateTag updates an image tag in-place on file system local compose files. // UpdateTag updates an image tag in-place on file system local compose files.
func UpdateTag(pattern, image, tag string) error { func UpdateTag(pattern, image, tag, recipeName string) error {
composeFiles, err := filepath.Glob(pattern) composeFiles, err := filepath.Glob(pattern)
if err != nil { if err != nil {
return err return err
} }
logrus.Debugf("considering '%s' config(s) for tag update", strings.Join(composeFiles, ", "))
for _, composeFile := range composeFiles { for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}} opts := stack.Deploy{Composefiles: []string{composeFile}}
emptyEnv := make(map[string]string)
compose, err := loader.LoadComposefile(opts, emptyEnv) 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 { if err != nil {
return err return err
} }
@ -35,7 +45,7 @@ func UpdateTag(pattern, image, tag string) error {
img, _ := reference.ParseNormalizedNamed(service.Image) img, _ := reference.ParseNormalizedNamed(service.Image)
if err != nil { if err != nil {
logrus.Fatal(err) return err
} }
composeImage := reference.Path(img) composeImage := reference.Path(img)
@ -47,16 +57,20 @@ func UpdateTag(pattern, image, tag string) error {
} }
composeTag := img.(reference.NamedTagged).Tag() composeTag := img.(reference.NamedTagged).Tag()
logrus.Debugf("parsed '%s' from '%s'", composeTag, service.Image)
if image == composeImage { if image == composeImage {
bytes, err := ioutil.ReadFile(composeFile) bytes, err := ioutil.ReadFile(composeFile)
if err != nil { if err != nil {
logrus.Fatal(err) return err
} }
old := fmt.Sprintf("%s:%s", composeImage, composeTag) old := fmt.Sprintf("%s:%s", composeImage, composeTag)
new := fmt.Sprintf("%s:%s", composeImage, tag) new := fmt.Sprintf("%s:%s", composeImage, tag)
replacedBytes := strings.Replace(string(bytes), old, new, -1) 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 { if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0644); err != nil {
return err return err
} }
@ -68,16 +82,24 @@ func UpdateTag(pattern, image, tag string) error {
} }
// UpdateLabel updates a label in-place on file system local compose files. // UpdateLabel updates a label in-place on file system local compose files.
func UpdateLabel(pattern, serviceName, label string) error { func UpdateLabel(pattern, serviceName, label, recipeName string) error {
composeFiles, err := filepath.Glob(pattern) composeFiles, err := filepath.Glob(pattern)
if err != nil { if err != nil {
return err return err
} }
logrus.Debugf("considering '%s' config(s) for label update", strings.Join(composeFiles, ", "))
for _, composeFile := range composeFiles { for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}} opts := stack.Deploy{Composefiles: []string{composeFile}}
emptyEnv := make(map[string]string)
compose, err := loader.LoadComposefile(opts, emptyEnv) 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 { if err != nil {
return err return err
} }
@ -102,8 +124,11 @@ func UpdateLabel(pattern, serviceName, label string) error {
return err return err
} }
old := fmt.Sprintf("coop-cloud.${STACK_NAME}.%s.version=%s", service.Name, value) old := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", value)
replacedBytes := strings.Replace(string(bytes), old, label, -1) 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 { if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0644); err != nil {
return err return err
} }

View File

@ -6,13 +6,14 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
"path/filepath"
"strings" "strings"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/client/convert" "coopcloud.tech/abra/pkg/client/convert"
loader "coopcloud.tech/abra/pkg/client/stack" loader "coopcloud.tech/abra/pkg/client/stack"
stack "coopcloud.tech/abra/pkg/client/stack" stack "coopcloud.tech/abra/pkg/client/stack"
composetypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types"
"github.com/sirupsen/logrus"
) )
// Type aliases to make code hints easier to understand // Type aliases to make code hints easier to understand
@ -44,7 +45,12 @@ type App struct {
// StackName gets what the docker safe stack name is for the app // StackName gets what the docker safe stack name is for the app
func (a App) StackName() string { func (a App) StackName() string {
return SanitiseAppName(a.Name) 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 // SORTING TYPES
@ -93,10 +99,14 @@ func readAppEnvFile(appFile AppFile, name AppName) (App, error) {
if err != nil { if err != nil {
return App{}, fmt.Errorf("env file for '%s' couldn't be read: %s", name, err.Error()) 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) app, err := newApp(env, name, appFile)
if err != nil { if err != nil {
return App{}, fmt.Errorf("env file for '%s' has issues: %s", name, err.Error()) return App{}, fmt.Errorf("env file for '%s' has issues: %s", name, err.Error())
} }
return app, nil return app, nil
} }
@ -108,6 +118,7 @@ func newApp(env AppEnv, name string, appFile AppFile) (App, error) {
if !ok { if !ok {
return App{}, errors.New("missing TYPE variable") return App{}, errors.New("missing TYPE variable")
} }
return App{ return App{
Name: name, Name: name,
Domain: domain, Domain: domain,
@ -131,6 +142,9 @@ func LoadAppFiles(servers ...string) (AppFiles, error) {
} }
} }
} }
logrus.Debugf("collecting metadata from '%v' servers: '%s'", len(servers), strings.Join(servers, ", "))
for _, server := range servers { for _, server := range servers {
serverDir := path.Join(ABRA_SERVER_FOLDER, server) serverDir := path.Join(ABRA_SERVER_FOLDER, server)
files, err := getAllFilesInDirectory(serverDir) files, err := getAllFilesInDirectory(serverDir)
@ -157,16 +171,19 @@ func GetApp(apps AppFiles, name AppName) (App, error) {
if !exists { if !exists {
return App{}, fmt.Errorf("cannot find app with name '%s'", name) return App{}, fmt.Errorf("cannot find app with name '%s'", name)
} }
app, err := readAppEnvFile(appFile, name) app, err := readAppEnvFile(appFile, name)
if err != nil { if err != nil {
return App{}, err return App{}, err
} }
return app, nil return app, nil
} }
// GetApps returns a slice of Apps with their env files read from a given slice of AppFiles // GetApps returns a slice of Apps with their env files read from a given slice of AppFiles
func GetApps(appFiles AppFiles) ([]App, error) { func GetApps(appFiles AppFiles) ([]App, error) {
var apps []App var apps []App
for name := range appFiles { for name := range appFiles {
app, err := GetApp(appFiles, name) app, err := GetApp(appFiles, name)
if err != nil { if err != nil {
@ -174,11 +191,65 @@ func GetApps(appFiles AppFiles) ([]App, error) {
} }
apps = append(apps, app) apps = append(apps, app)
} }
return apps, nil return apps, nil
} }
// CopyAppEnvSample copies the example env file for the app into the users env files // GetAppServiceNames retrieves a list of app service names.
func CopyAppEnvSample(appType, appName, server string) error { 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") envSamplePath := path.Join(ABRA_DIR, "apps", appType, ".env.sample")
envSample, err := ioutil.ReadFile(envSamplePath) envSample, err := ioutil.ReadFile(envSamplePath)
if err != nil { if err != nil {
@ -190,11 +261,16 @@ func CopyAppEnvSample(appType, appName, server string) error {
return fmt.Errorf("%s already exists?", appEnvPath) 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) err = ioutil.WriteFile(appEnvPath, envSample, 0755)
if err != nil { if err != nil {
return err return err
} }
logrus.Debugf("copied '%s' to '%s'", envSamplePath, appEnvPath)
return nil return nil
} }
@ -204,45 +280,78 @@ func SanitiseAppName(name string) string {
} }
// GetAppStatuses queries servers to check the deployment status of given apps // GetAppStatuses queries servers to check the deployment status of given apps
func GetAppStatuses(appFiles AppFiles) (map[string]string, error) { func GetAppStatuses(appFiles AppFiles) (map[string]map[string]string, error) {
servers := appFiles.GetServers() statuses := make(map[string]map[string]string)
ch := make(chan stack.StackStatus, len(servers))
for _, server := range servers { var unique []string
go func(s string) { ch <- stack.GetAllDeployedServices(s) }(server) 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)
} }
statuses := map[string]string{}
for range servers { for range servers {
status := <-ch status := <-ch
for _, service := range status.Services { for _, service := range status.Services {
result := make(map[string]string)
name := service.Spec.Labels[convert.LabelNamespace] name := service.Spec.Labels[convert.LabelNamespace]
if _, ok := statuses[name]; !ok { if _, ok := statuses[name]; !ok {
statuses[name] = "deployed" 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 return statuses, nil
} }
// GetAppComposeFiles gets the list of compose files for an app which should be // 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. // merged into a composetypes.Config while respecting the COMPOSE_FILE env var.
func GetAppComposeFiles(recipe string, appEnv AppEnv) ([]string, error) { func GetAppComposeFiles(recipe string, appEnv AppEnv) ([]string, error) {
var composeFiles []string
if _, ok := appEnv["COMPOSE_FILE"]; !ok { if _, ok := appEnv["COMPOSE_FILE"]; !ok {
pattern := fmt.Sprintf("%s/%s/compose**yml", APPS_DIR, recipe) logrus.Debug("no COMPOSE_FILE detected, loading compose.yml")
composeFiles, err := filepath.Glob(pattern) path := fmt.Sprintf("%s/%s/compose.yml", APPS_DIR, recipe)
if err != nil { composeFiles = append(composeFiles, path)
return composeFiles, err
}
return composeFiles, nil return composeFiles, nil
} }
var composeFiles []string
composeFileEnvVar := appEnv["COMPOSE_FILE"] 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, ":") { for _, file := range strings.Split(composeFileEnvVar, ":") {
path := fmt.Sprintf("%s/%s/%s", APPS_DIR, recipe, file) path := fmt.Sprintf("%s/%s/%s", APPS_DIR, recipe, file)
composeFiles = append(composeFiles, path) composeFiles = append(composeFiles, path)
} }
logrus.Debugf("retrieved '%s' configs for '%s'", strings.Join(composeFiles, ", "), recipe)
return composeFiles, nil return composeFiles, nil
} }
@ -254,5 +363,8 @@ func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv AppEnv) (*comp
if err != nil { if err != nil {
return &composetypes.Config{}, err return &composetypes.Config{}, err
} }
logrus.Debugf("retrieved '%s' for '%s'", compose.Filename, recipe)
return compose, nil return compose, nil
} }

View File

@ -20,45 +20,64 @@ var APPS_JSON = path.Join(ABRA_DIR, "apps.json")
var APPS_DIR = path.Join(ABRA_DIR, "apps") var APPS_DIR = path.Join(ABRA_DIR, "apps")
var REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud" var REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
func (a AppFiles) GetServers() []string { // GetServers retrieves all servers.
var unique []string func GetServers() ([]string, error) {
servers := make(map[string]struct{}) var servers []string
for _, appFile := range a {
if _, ok := servers[appFile.Server]; !ok { servers, err := getAllFoldersInDirectory(ABRA_SERVER_FOLDER)
servers[appFile.Server] = struct{}{} if err != nil {
unique = append(unique, appFile.Server) return servers, err
}
} }
return unique
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) { func ReadEnv(filePath string) (AppEnv, error) {
var envFile AppEnv var envFile AppEnv
envFile, err := godotenv.Read(filePath) envFile, err := godotenv.Read(filePath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
logrus.Debugf("read '%s' from '%s'", envFile, filePath)
return envFile, nil return envFile, nil
} }
// ReadServerNames retrieves all server names.
func ReadServerNames() ([]string, error) { func ReadServerNames() ([]string, error) {
serverNames, err := getAllFoldersInDirectory(ABRA_SERVER_FOLDER) serverNames, err := getAllFoldersInDirectory(ABRA_SERVER_FOLDER)
if err != nil { if err != nil {
return nil, err return nil, err
} }
logrus.Debugf("read '%s' from '%s'", strings.Join(serverNames, ","), ABRA_SERVER_FOLDER)
return serverNames, nil return serverNames, nil
} }
// getAllFilesInDirectory returns filenames of all files in directory // getAllFilesInDirectory returns filenames of all files in directory
func getAllFilesInDirectory(directory string) ([]fs.FileInfo, error) { func getAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
var realFiles []fs.FileInfo var realFiles []fs.FileInfo
files, err := ioutil.ReadDir(directory) files, err := ioutil.ReadDir(directory)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, file := range files { for _, file := range files {
// Follow any symlinks // Follow any symlinks
filePath := path.Join(directory, file.Name()) filePath := path.Join(directory, file.Name())
if filepath.Ext(strings.TrimSpace(filePath)) != ".env" {
continue
}
realPath, err := filepath.EvalSymlinks(filePath) realPath, err := filepath.EvalSymlinks(filePath)
if err != nil { if err != nil {
logrus.Warningf("broken symlink in your abra config folders: '%s'", filePath) logrus.Warningf("broken symlink in your abra config folders: '%s'", filePath)
@ -71,14 +90,15 @@ func getAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
realFiles = append(realFiles, file) realFiles = append(realFiles, file)
} }
} }
} }
return realFiles, nil return realFiles, nil
} }
// getAllFoldersInDirectory returns both folder and symlink paths // getAllFoldersInDirectory returns both folder and symlink paths
func getAllFoldersInDirectory(directory string) ([]string, error) { func getAllFoldersInDirectory(directory string) ([]string, error) {
var folders []string var folders []string
files, err := ioutil.ReadDir(directory) files, err := ioutil.ReadDir(directory)
if err != nil { if err != nil {
return nil, err return nil, err
@ -86,6 +106,7 @@ func getAllFoldersInDirectory(directory string) ([]string, error) {
if len(files) == 0 { if len(files) == 0 {
return nil, fmt.Errorf("directory is empty: '%s'", directory) return nil, fmt.Errorf("directory is empty: '%s'", directory)
} }
for _, file := range files { for _, file := range files {
// Check if file is directory or symlink // Check if file is directory or symlink
if file.IsDir() || file.Mode()&fs.ModeSymlink != 0 { if file.IsDir() || file.Mode()&fs.ModeSymlink != 0 {
@ -99,12 +120,14 @@ func getAllFoldersInDirectory(directory string) ([]string, error) {
} }
} }
} }
return folders, nil return folders, nil
} }
// EnsureAbraDirExists checks for the abra config folder and throws error if not // EnsureAbraDirExists checks for the abra config folder and throws error if not
func EnsureAbraDirExists() error { func EnsureAbraDirExists() error {
if _, err := os.Stat(ABRA_DIR); os.IsNotExist(err) { 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 { if err := os.Mkdir(ABRA_DIR, 0777); err != nil {
return err return err
} }
@ -112,13 +135,14 @@ func EnsureAbraDirExists() error {
return nil return nil
} }
// ReadAbraShEnvVars reads env vars from an abra.sh recipe file.
func ReadAbraShEnvVars(abraSh string) (map[string]string, error) { func ReadAbraShEnvVars(abraSh string) (map[string]string, error) {
envVars := make(map[string]string) envVars := make(map[string]string)
file, err := os.Open(abraSh) file, err := os.Open(abraSh)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return envVars, fmt.Errorf("'%s' does not exist?", abraSh) return envVars, nil
} }
return envVars, err return envVars, err
} }
@ -137,5 +161,7 @@ func ReadAbraShEnvVars(abraSh string) (map[string]string, error) {
} }
} }
logrus.Debugf("read '%s' from '%s'", envVars, abraSh)
return envVars, nil return envVars, nil
} }

View File

@ -13,7 +13,7 @@ var validAbraConf = os.ExpandEnv("$PWD/../../tests/resources/valid_abra_config")
// make sure these are in alphabetical order // make sure these are in alphabetical order
var tFolders = []string{"folder1", "folder2"} var tFolders = []string{"folder1", "folder2"}
var tFiles = []string{"bar", "foo"} var tFiles = []string{"bar.env", "foo.env"}
var appName = "ecloud" var appName = "ecloud"
var serverName = "evil.corp" var serverName = "evil.corp"

94
pkg/git/clone.go Normal file
View File

@ -0,0 +1,94 @@
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,
Keep: false,
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
}

View File

@ -2,7 +2,6 @@ package recipe
import ( import (
"fmt" "fmt"
"os"
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
@ -11,9 +10,11 @@ import (
loader "coopcloud.tech/abra/pkg/client/stack" loader "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/compose" "coopcloud.tech/abra/pkg/compose"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git"
composetypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus"
) )
// Recipe represents a recipe. // Recipe represents a recipe.
@ -25,7 +26,7 @@ type Recipe struct {
// UpdateLabel updates a recipe label // UpdateLabel updates a recipe label
func (r Recipe) UpdateLabel(serviceName, label string) error { func (r Recipe) UpdateLabel(serviceName, label string) error {
pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, r.Name) pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, r.Name)
if err := compose.UpdateLabel(pattern, serviceName, label); err != nil { if err := compose.UpdateLabel(pattern, serviceName, label, r.Name); err != nil {
return err return err
} }
return nil return nil
@ -34,12 +35,39 @@ func (r Recipe) UpdateLabel(serviceName, label string) error {
// UpdateTag updates a recipe tag // UpdateTag updates a recipe tag
func (r Recipe) UpdateTag(image, tag string) error { func (r Recipe) UpdateTag(image, tag string) error {
pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, r.Name) pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, r.Name)
if err := compose.UpdateTag(pattern, image, tag); err != nil { if err := compose.UpdateTag(pattern, image, tag, r.Name); err != nil {
return err return err
} }
return nil 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. // Get retrieves a recipe.
func Get(recipeName string) (Recipe, error) { func Get(recipeName string) (Recipe, error) {
if err := EnsureExists(recipeName); err != nil { if err := EnsureExists(recipeName); err != nil {
@ -52,9 +80,14 @@ func Get(recipeName string) (Recipe, error) {
return Recipe{}, err 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} opts := stack.Deploy{Composefiles: composeFiles}
emptyEnv := make(map[string]string) config, err := loader.LoadComposefile(opts, sampleEnv)
config, err := loader.LoadComposefile(opts, emptyEnv)
if err != nil { if err != nil {
return Recipe{}, err return Recipe{}, err
} }
@ -65,23 +98,10 @@ func Get(recipeName string) (Recipe, error) {
// EnsureExists checks whether a recipe has been cloned locally or not. // EnsureExists checks whether a recipe has been cloned locally or not.
func EnsureExists(recipe string) error { func EnsureExists(recipe string) error {
recipeDir := path.Join(config.ABRA_DIR, "apps", strings.ToLower(recipe)) recipeDir := path.Join(config.ABRA_DIR, "apps", strings.ToLower(recipe))
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, recipe)
if _, err := os.Stat(recipeDir); os.IsNotExist(err) { if err := gitPkg.Clone(recipeDir, url); err != nil {
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, recipe) return err
_, err := git.PlainClone(recipeDir, false, &git.CloneOptions{URL: url, Tags: git.AllTags})
if err != nil {
// try with main branch because Git is being a Git
_, err := git.PlainClone(recipeDir, false, &git.CloneOptions{
URL: url,
Tags: git.AllTags,
ReferenceName: plumbing.ReferenceName("refs/heads/main"),
})
if err != nil {
return err
}
}
} }
return nil return nil
} }
@ -99,6 +119,8 @@ func EnsureVersion(recipeName, version string) error {
return nil return nil
} }
logrus.Debugf("read '%s' as tags for recipe '%s'", tags, recipeName)
var tagRef plumbing.ReferenceName var tagRef plumbing.ReferenceName
if err := tags.ForEach(func(ref *plumbing.Reference) (err error) { if err := tags.ForEach(func(ref *plumbing.Reference) (err error) {
if ref.Name().Short() == version { if ref.Name().Short() == version {
@ -123,5 +145,7 @@ func EnsureVersion(recipeName, version string) error {
return err return err
} }
logrus.Debugf("successfully checked '%s' out to '%s' in '%s'", recipeName, tagRef, recipeDir)
return nil return nil
} }

View File

@ -19,6 +19,8 @@ func PassInsertSecret(secretValue, secretName, appName, server string) error {
secretValue, server, appName, secretName, secretValue, server, appName, secretName,
) )
logrus.Debugf("attempting to run '%s'", cmd)
if err := exec.Command("bash", "-c", cmd).Run(); err != nil { if err := exec.Command("bash", "-c", cmd).Run(); err != nil {
return err return err
} }
@ -39,6 +41,8 @@ func PassRmSecret(secretName, appName, server string) error {
server, appName, secretName, server, appName, secretName,
) )
logrus.Debugf("attempting to run '%s'", cmd)
if err := exec.Command("bash", "-c", cmd).Run(); err != nil { if err := exec.Command("bash", "-c", cmd).Run(); err != nil {
return err return err
} }

View File

@ -12,6 +12,7 @@ import (
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"github.com/schultz-is/passgen" "github.com/schultz-is/passgen"
"github.com/sirupsen/logrus"
) )
// secretValue represents a parsed `SECRET_FOO=v1 # length=bar` env var config // secretValue represents a parsed `SECRET_FOO=v1 # length=bar` env var config
@ -33,6 +34,8 @@ func GeneratePasswords(count, length uint) ([]string, error) {
return nil, err return nil, err
} }
logrus.Debugf("generated '%s'", strings.Join(passwords, ", "))
return passwords, nil return passwords, nil
} }
@ -50,17 +53,24 @@ func GeneratePassphrases(count uint) ([]string, error) {
return nil, err return nil, err
} }
logrus.Debugf("generated '%s'", strings.Join(passphrases, ", "))
return passphrases, nil return passphrases, nil
} }
// ReadSecretEnvVars reads secret env vars from an app env var config.
func ReadSecretEnvVars(appEnv config.AppEnv) map[string]string { func ReadSecretEnvVars(appEnv config.AppEnv) map[string]string {
secretEnvVars := make(map[string]string) secretEnvVars := make(map[string]string)
for envVar := range appEnv { for envVar := range appEnv {
regex := regexp.MustCompile(`^SECRET.*VERSION.*`) regex := regexp.MustCompile(`^SECRET.*VERSION.*`)
if string(regex.Find([]byte(envVar))) != "" { if string(regex.Find([]byte(envVar))) != "" {
secretEnvVars[envVar] = appEnv[envVar] secretEnvVars[envVar] = appEnv[envVar]
} }
} }
logrus.Debugf("read '%s' as secrets from '%s'", secretEnvVars, appEnv)
return secretEnvVars return secretEnvVars
} }
@ -68,7 +78,9 @@ func ReadSecretEnvVars(appEnv config.AppEnv) map[string]string {
func ParseSecretEnvVarName(secretEnvVar string) string { func ParseSecretEnvVarName(secretEnvVar string) string {
withoutPrefix := strings.TrimPrefix(secretEnvVar, "SECRET_") withoutPrefix := strings.TrimPrefix(secretEnvVar, "SECRET_")
withoutSuffix := strings.TrimSuffix(withoutPrefix, "_VERSION") withoutSuffix := strings.TrimSuffix(withoutPrefix, "_VERSION")
return strings.ToLower(withoutSuffix) 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? // TODO: should probably go in the config/app package?
@ -76,7 +88,9 @@ func ParseGeneratedSecretName(secret string, appEnv config.App) string {
name := fmt.Sprintf("%s_", appEnv.StackName()) name := fmt.Sprintf("%s_", appEnv.StackName())
withoutAppName := strings.TrimPrefix(secret, name) withoutAppName := strings.TrimPrefix(secret, name)
idx := strings.LastIndex(withoutAppName, "_") idx := strings.LastIndex(withoutAppName, "_")
return withoutAppName[:idx] parsed := withoutAppName[:idx]
logrus.Debugf("parsed '%s' as name from '%s'", parsed, secret)
return parsed
} }
// TODO: should probably go in the config/app package? // TODO: should probably go in the config/app package?
@ -85,19 +99,23 @@ func ParseSecretEnvVarValue(secret string) (secretValue, error) {
if len(values) == 0 { if len(values) == 0 {
return secretValue{}, fmt.Errorf("unable to parse '%s'", secret) return secretValue{}, fmt.Errorf("unable to parse '%s'", secret)
} }
if len(values) == 1 { if len(values) == 1 {
return secretValue{Version: values[0], Length: 0}, nil return secretValue{Version: values[0], Length: 0}, nil
} else {
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], " ", "")
return secretValue{Version: version, Length: length}, 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. // GenerateSecrets generates secrets locally and sends them to a remote server for storage.
@ -114,6 +132,7 @@ func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (m
return return
} }
secretRemoteName := fmt.Sprintf("%s_%s_%s", appName, secretName, secretValue.Version) secretRemoteName := fmt.Sprintf("%s_%s_%s", appName, secretName, secretValue.Version)
logrus.Debugf("attempting to generate and store '%s' on '%s'", secretRemoteName, server)
if secretValue.Length > 0 { if secretValue.Length > 0 {
passwords, err := GeneratePasswords(1, uint(secretValue.Length)) passwords, err := GeneratePasswords(1, uint(secretValue.Length))
if err != nil { if err != nil {
@ -147,5 +166,7 @@ func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (m
} }
} }
logrus.Debugf("generated and stored '%s' on '%s'", secrets, server)
return secrets, nil return secrets, nil
} }

24
pkg/server/server.go Normal file
View File

@ -0,0 +1,24 @@
package server
import (
"os"
"path"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
)
// CreateServerDir creates a server directory under ~/.abra.
func CreateServerDir(serverName string) error {
serverPath := path.Join(config.ABRA_DIR, "servers", serverName)
if err := os.Mkdir(serverPath, 0755); err != nil {
if !os.IsExist(err) {
return err
}
logrus.Infof("'%s' already exists, moving on...", serverPath)
}
return nil
}

View File

@ -6,4 +6,4 @@ Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock {
Invoke-Expression $other | ForEach-Object { Invoke-Expression $other | ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
} }
} }

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
ABRA_VERSION="0.1.2-alpha" ABRA_VERSION="0.1.8-alpha"
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION" ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION"
function show_banner { function show_banner {
@ -38,7 +38,7 @@ function install_abra_release {
release_url=$(curl -s "$ABRA_RELEASE_URL" | release_url=$(curl -s "$ABRA_RELEASE_URL" |
python3 -c "import sys, json; \ python3 -c "import sys, json; \
payload = json.load(sys.stdin); \ payload = json.load(sys.stdin); \
url = [a['browser_download_url'] for a in payload['assets'] if 'x86_64' in a['name']][0]; \ url = [a['browser_download_url'] for a in payload['assets'] if 'linux_x86_64' in a['name']][0]; \
print(url)") print(url)")
echo "downloading $ABRA_VERSION x86_64 binary release for abra..." echo "downloading $ABRA_VERSION x86_64 binary release for abra..."

View File

@ -3,5 +3,5 @@ STACK := abra_installer_script
default: deploy default: deploy
deploy: deploy:
@docker stack rm $(STACK) && \ @DOCKER_CONTEXT=swarm.autonomic.zone docker stack rm $(STACK) && \
docker stack deploy -c compose.yml $(STACK) DOCKER_CONTEXT=swarm.autonomic.zone docker stack deploy -c compose.yml $(STACK)

View File