Compare commits

..

82 Commits

Author SHA1 Message Date
ed859c0243 NOT WORKING: WIP on adding server-wide logs functionality 2021-12-03 10:33:41 +01:00
236d0f5892 feat: pick stdout/stderr for abra app logs 2021-11-30 15:47:58 +01:00
6c87d501e6 fix(installer): drop double echo
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-11-30 12:07:40 +01:00
930c29f4a2 fix: switch order of command
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-26 22:24:55 +01:00
1d6c3e98e4 fix: only query deployed app
Closes coop-cloud/organising#266.
2021-11-26 22:24:41 +01:00
a90f3b7463 fix: easier logs
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#270.
2021-11-26 22:14:29 +01:00
962f566228 fix: go on with missing tag
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#264.
2021-11-26 21:34:21 +01:00
9896c57399 chore: drop ' in messages [ci skip] 2021-11-26 21:34:10 +01:00
748d607ddc fix: better converge output
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#263.
2021-11-26 21:24:15 +01:00
3901258a96 fix: better message for existing swarm
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#259.
2021-11-26 21:07:49 +01:00
4347083f98 docs: better message [ci skip] 2021-11-26 21:04:58 +01:00
4641a942d8 chore: drop comment [ci skip] 2021-11-26 21:02:29 +01:00
3wc
759a00eeb3 fix: less fussy catalogue generation
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-24 13:48:17 +02:00
3wc
d1526fad21 fix: skip drone-abra and recipes in catalogue 2021-11-24 13:48:17 +02:00
6ef15e0a26 fix: remove fish from autocomplete
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-11-24 12:11:35 +01:00
dd0f328a65 fix: dont throw away changes
All checks were successful
continuous-integration/drone/push Build is passing
Part of coop-cloud/organising#226.
2021-11-22 21:11:59 +01:00
aea5cc69c3 fix: include ignored files
Part of coop-cloud/organising#226.
2021-11-22 21:11:59 +01:00
3wc
b02475eca5 Merge branch 'catalogue-metadata'
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-22 20:41:34 +02:00
3wc
d0a30f6b7b refactor: code style / error handling improvements
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-22 20:37:12 +02:00
3wc
8635922b9f fix: don't clobber recipe changes during generate
Closes #255
2021-11-22 20:37:12 +02:00
3wc
9d62fff074 feat: recipe generate: load category and features 2021-11-22 20:37:12 +02:00
711c4e5ee8 fix: warn on invalid envs for catalogue generation
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#256.
2021-11-22 18:38:59 +01:00
cb32e88cde fix: support retryable http clients
All checks were successful
continuous-integration/drone/push Build is passing
Closes coop-cloud/organising#257.
2021-11-22 18:28:18 +01:00
a18729bf98 fix: ensure changes are check for
All checks were successful
continuous-integration/drone/push Build is passing
Part of coop-cloud/organising#255.
2021-11-22 17:49:31 +01:00
dbf84b7640 fix: validate this recipe
Part of coop-cloud/organising#255.
2021-11-22 17:49:14 +01:00
3wc
75db249053 fix: don't include traefik-cert-dumper in catalogue
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-22 16:15:51 +02:00
fdf4fc6737 fix: ensure validation takes place
All checks were successful
continuous-integration/drone/push Build is passing
Part of coop-cloud/organising#243 (comment).
2021-11-21 15:00:04 +01:00
ef6a9abba9 fix: ensure clean slate for re-deploy
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-21 14:42:38 +01:00
ce57d5ed54 fix: merge messages 2021-11-21 14:42:22 +01:00
3b01b1bb2e docs: explain docker context also
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-21 14:11:27 +01:00
fbdb792795 fix: add app name to ps output + docs
All checks were successful
continuous-integration/drone/push Build is passing
Part of coop-cloud/organising#252.
2021-11-21 14:07:19 +01:00
900f40f07a fix: add app name to list output
Part of coop-cloud/organising#252.
2021-11-21 13:43:21 +01:00
ecd2a63f0a fix: counts apps + drop versions meta without -S 2021-11-21 13:40:23 +01:00
304b70639f fix: only check catalogue once
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-19 15:50:29 +01:00
d821975aa2 fix: dont check servers so many times 2021-11-19 15:50:17 +01:00
1b836dbab6 fix: better borked ssh config message
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/organising#243.
2021-11-19 15:29:54 +01:00
fc51cf7775 docs: improve wording [ci skip] 2021-11-19 15:29:54 +01:00
a7ebcd8950 chore: bump for new RC
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-18 21:18:40 +01:00
e589709cb0 fix: attempt to include IdentityFile if available
All checks were successful
continuous-integration/drone/push Build is passing
This is part of trying to debug:

    coop-cloud/organising#250

And also part of:

    coop-cloud/docs.coopcloud.tech#27

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

Argh, reverted this by accident, heres another one!
2021-11-09 18:25:28 +01:00
db10c7b849 feat: run wizard mode on recipe upgrade [ci skip] 2021-11-09 18:06:06 +01:00
d38f82ebe7 docs: drop recipe [ci skip] 2021-11-09 18:05:53 +01:00
59031595ea Revert "test: remove broken tests for client"
This reverts commit 17a5f1529a.
2021-11-09 17:58:31 +01:00
6f26b51f3e fix: only check host keys on requested hosts
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/organising#242.
2021-11-09 17:44:13 +01:00
45 changed files with 1252 additions and 273 deletions

View File

@ -9,6 +9,20 @@ The Co-op Cloud utility belt 🎩🐇
`abra` is a command-line tool for managing your own [Co-op Cloud](https://coopcloud.tech). It can provision new servers, create apps, deploy them, run backup and restore operations and a whole lot of other things. Please see [docs.coopcloud.tech](https://docs.coopcloud.tech) for more extensive documentation. `abra` is a command-line tool for managing your own [Co-op Cloud](https://coopcloud.tech). It can provision new servers, create apps, deploy them, run backup and restore operations and a whole lot of other things. Please see [docs.coopcloud.tech](https://docs.coopcloud.tech) for more extensive documentation.
## Quick install
```bash
curl https://install.abra.autonomic.zone | bash
```
Or using the latest release candidate (extra experimental!):
```bash
curl https://install.abra.autonomic.zone | bash -s -- --rc
```
Source for this script is in [scripts/installer/installer](./scripts/installer/installer).
## Hacking ## Hacking
### Getting started ### Getting started

View File

@ -18,6 +18,7 @@ to scaling apps up and spinning them down.
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
appNewCommand, appNewCommand,
appConfigCommand, appConfigCommand,
appRestartCommand,
appDeployCommand, appDeployCommand,
appUpgradeCommand, appUpgradeCommand,
appUndeployCommand, appUndeployCommand,

View File

@ -5,13 +5,8 @@ import (
"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/config" "coopcloud.tech/abra/pkg/config"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/archive"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -21,6 +16,18 @@ var appCpCommand = &cli.Command{
Aliases: []string{"c"}, Aliases: []string{"c"},
ArgsUsage: "<src> <dst>", ArgsUsage: "<src> <dst>",
Usage: "Copy files to/from a running app service", Usage: "Copy files to/from a running app service",
Description: `
This command supports copying files to and from any app service file system.
If you want to copy a myfile.txt to the root of the app service:
abra app cp <app> myfile.txt app:/
And if you want to copy that file back to your current working directory locally:
abra app cp <app> app:/myfile.txt .
`,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
@ -64,61 +71,28 @@ var appCpCommand = &cli.Command{
logrus.Debugf("assuming transfer is going TO the container") logrus.Debugf("assuming transfer is going TO the container")
} }
appFiles, err := config.LoadAppFiles("") if !isToContainer {
if _, err := os.Stat(dstPath); os.IsNotExist(err) {
logrus.Fatalf("%s does not exist locally?", dstPath)
}
}
err := internal.ConfigureAndCp(c, app, srcPath, dstPath, service, isToContainer)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
appEnv, err := config.GetApp(appFiles, app.Name)
if err != nil {
logrus.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("%s_%s", appEnv.StackName(), service))
containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
if err != nil {
logrus.Fatal(err)
}
if len(containers) != 1 {
logrus.Fatalf("expected 1 container but got %v", len(containers))
}
container := containers[0]
logrus.Debugf("retrieved '%s' as target container on '%s'", formatter.ShortenID(container.ID), app.Server)
if isToContainer {
if _, err := os.Stat(srcPath); err != nil {
logrus.Fatalf("'%s' does not exist?", srcPath)
}
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
content, err := archive.TarWithOptions(srcPath, toTarOpts)
if err != nil {
logrus.Fatal(err)
}
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(c.Context, container.ID, dstPath, content, copyOpts); err != nil {
logrus.Fatal(err)
}
} else {
content, _, err := cl.CopyFromContainer(c.Context, container.ID, srcPath)
if err != nil {
logrus.Fatal(err)
}
defer content.Close()
fromTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
if err := archive.Untar(content, dstPath, fromTarOpts); err != nil {
logrus.Fatal(err)
}
}
return nil 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

@ -6,8 +6,10 @@ import (
"strings" "strings"
abraFormatter "coopcloud.tech/abra/cli/formatter" abraFormatter "coopcloud.tech/abra/cli/formatter"
"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"
"coopcloud.tech/abra/pkg/ssh"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -69,8 +71,18 @@ can take some time.
} }
sort.Sort(config.ByServerAndType(apps)) sort.Sort(config.ByServerAndType(apps))
alreadySeen := make(map[string]bool)
for _, app := range apps {
if _, ok := alreadySeen[app.Server]; !ok {
if err := ssh.EnsureHostKey(app.Server); err != nil {
logrus.Fatal(fmt.Sprintf(internal.SSHFailMsg, app.Server))
}
alreadySeen[app.Server] = true
}
}
statuses := make(map[string]map[string]string) statuses := make(map[string]map[string]string)
tableCol := []string{"Server", "Type", "Domain"} tableCol := []string{"Server", "Type", "App Name", "Domain"}
if status { if status {
tableCol = append(tableCol, "Status", "Version", "Updates") tableCol = append(tableCol, "Status", "Version", "Updates")
statuses, err = config.GetAppStatuses(appFiles) statuses, err = config.GetAppStatuses(appFiles)
@ -89,11 +101,19 @@ can take some time.
canUpgradeCount int canUpgradeCount int
) )
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
var appsCount int
for _, app := range apps { for _, app := range apps {
var tableRow []string var tableRow []string
if app.Type == appType || appType == "" { if app.Type == appType || appType == "" {
appsCount++
// 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.StackName(), app.Domain}
if status { if status {
stackName := app.StackName() stackName := app.StackName()
status := "unknown" status := "unknown"
@ -114,7 +134,8 @@ can take some time.
var newUpdates []string var newUpdates []string
if version != "unknown" { if version != "unknown" {
updates, err := catalogue.GetRecipeCatalogueVersions(app.Type)
updates, err := catalogue.GetRecipeCatalogueVersions(app.Type, catl)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -156,14 +177,19 @@ can take some time.
table.Append(tableRow) table.Append(tableRow)
} }
stats := fmt.Sprintf( var stats string
"Total apps: %v | Versioned: %v | Unversioned: %v | On latest: %v | Can upgrade: %v", if status {
len(apps), stats = fmt.Sprintf(
versionedAppsCount, "Total apps: %v | Versioned: %v | Unversioned: %v | On latest: %v | Can upgrade: %v",
unversionedAppsCount, appsCount,
onLatestCount, versionedAppsCount,
canUpgradeCount, unversionedAppsCount,
) onLatestCount,
canUpgradeCount,
)
} else {
stats = fmt.Sprintf("Total apps: %v", appsCount)
}
table.SetCaption(true, stats) table.SetCaption(true, stats)
table.Render() table.Render()

View File

@ -4,74 +4,54 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"sync"
"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"
"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"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
// stackLogs lists logs for all stack services
func stackLogs(c *cli.Context, stackName string, client *dockerClient.Client) {
filters := filters.NewArgs()
filters.Add("name", stackName)
serviceOpts := types.ServiceListOptions{Filters: filters}
services, err := client.ServiceList(c.Context, serviceOpts)
if err != nil {
logrus.Fatal(err)
}
var wg sync.WaitGroup
for _, service := range services {
wg.Add(1)
go func(s string) {
logOpts := types.ContainerLogsOptions{
Details: true,
Follow: true,
ShowStderr: true,
ShowStdout: true,
Tail: "20",
Timestamps: true,
}
logs, err := client.ServiceLogs(c.Context, s, logOpts)
if err != nil {
logrus.Fatal(err)
}
// defer after err check as any err returns a nil io.ReadCloser
defer logs.Close()
_, err = io.Copy(os.Stdout, logs)
if err != nil && err != io.EOF {
logrus.Fatal(err)
}
}(service.ID)
}
wg.Wait()
os.Exit(0)
}
var appLogsCommand = &cli.Command{ var appLogsCommand = &cli.Command{
Name: "logs", Name: "logs",
Aliases: []string{"l"}, Aliases: []string{"l"},
ArgsUsage: "[<service>]", ArgsUsage: "[<service>]",
Usage: "Tail app logs", Usage: "Tail app logs",
Flags: []cli.Flag{
internal.StderrFlag,
internal.StdoutFlag,
internal.HealthcheckFlag,
},
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if !internal.Stderr && !internal.Stdout && !internal.Healthcheck {
internal.Stderr = true
internal.Stdout = true
internal.Healthcheck = true
}
logrus.Debugf("flags parsed. --stderr: %t, --stdout: %t, --healthcheck: %t", internal.Stderr, internal.Stdout, internal.Healthcheck)
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logOpts := types.ContainerLogsOptions{
Details: false,
Follow: true,
ShowStderr: internal.Stderr,
ShowStdout: internal.Stdout,
Tail: "20",
Timestamps: true,
}
serviceName := c.Args().Get(1) serviceName := c.Args().Get(1)
if serviceName == "" { if serviceName == "" {
logrus.Debug("tailing logs for all app services") logrus.Debug("tailing logs for all app services")
stackLogs(c, app.StackName(), cl) internal.StackLogs(c, app.StackName(), logOpts, cl)
} }
logrus.Debugf("tailing logs for '%s'", serviceName) logrus.Debugf("tailing logs for '%s'", serviceName)
@ -87,14 +67,6 @@ var appLogsCommand = &cli.Command{
logrus.Fatalf("expected 1 service but got %v", len(services)) logrus.Fatalf("expected 1 service but got %v", len(services))
} }
logOpts := types.ContainerLogsOptions{
Details: true,
Follow: true,
ShowStderr: true,
ShowStdout: true,
Tail: "20",
Timestamps: true,
}
logs, err := cl.ServiceLogs(c.Context, services[0].ID, logOpts) logs, err := cl.ServiceLogs(c.Context, services[0].ID, logOpts)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)

View File

@ -26,9 +26,10 @@ var watchFlag = &cli.BoolFlag{
} }
var appPsCommand = &cli.Command{ var appPsCommand = &cli.Command{
Name: "ps", Name: "ps",
Usage: "Check app status", Usage: "Check app status",
Aliases: []string{"p"}, Description: "This command shows a more detailed status output of a specific deployed app.",
Aliases: []string{"p"},
Flags: []cli.Flag{ Flags: []cli.Flag{
watchFlag, watchFlag,
}, },
@ -75,7 +76,7 @@ func showPSOutput(c *cli.Context) {
logrus.Fatal(err) logrus.Fatal(err)
} }
tableCol := []string{"image", "created", "status", "ports", "names"} tableCol := []string{"image", "created", "status", "ports", "app name", "services"}
table := abraFormatter.CreateTable(tableCol) table := abraFormatter.CreateTable(tableCol)
for _, container := range containers { for _, container := range containers {
@ -90,6 +91,7 @@ func showPSOutput(c *cli.Context) {
abraFormatter.HumanDuration(container.Created), abraFormatter.HumanDuration(container.Created),
container.Status, container.Status,
formatter.DisplayablePorts(container.Ports), formatter.DisplayablePorts(container.Ports),
app.StackName(),
strings.Join(containerNames, "\n"), strings.Join(containerNames, "\n"),
} }
table.Append(tableRow) table.Append(tableRow)

View File

@ -7,6 +7,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" "coopcloud.tech/abra/pkg/config"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"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"
@ -48,23 +49,18 @@ var appRemoveCommand = &cli.Command{
} }
} }
appFiles, err := config.LoadAppFiles("")
if err != nil {
logrus.Fatal(err)
}
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Force { if !internal.Force {
// FIXME: only query for app we are interested in, not all of them! isDeployed, _, err := stack.IsDeployed(c.Context, cl, app.StackName())
statuses, err := config.GetAppStatuses(appFiles)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if statuses[app.Name]["status"] == "deployed" { if isDeployed {
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 undeploy %s \" or pass --force", app.Name, app.Name)
} }
} }

72
cli/app/restart.go Normal file
View File

@ -0,0 +1,72 @@
package app
import (
"errors"
"fmt"
"time"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var appRestartCommand = &cli.Command{
Name: "restart",
Usage: "Restart an app",
Aliases: []string{"R"},
ArgsUsage: "<service>",
Description: `This command restarts a service within a deployed app.`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
serviceName := c.Args().Get(1)
if serviceName == "" {
err := errors.New("missing service?")
internal.ShowSubcommandHelpAndError(c, err)
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
serviceFilter := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
filters := filters.NewArgs()
filters.Add("name", serviceFilter)
containerOpts := types.ContainerListOptions{Filters: filters}
containers, err := cl.ContainerList(c.Context, containerOpts)
if err != nil {
logrus.Fatal(err)
}
if len(containers) != 1 {
logrus.Fatalf("expected 1 service but got %v", len(containers))
}
logrus.Debugf("attempting to restart %s", serviceFilter)
timeout := 30 * time.Second
if err := cl.ContainerRestart(c.Context, containers[0].ID, &timeout); err != nil {
logrus.Fatal(err)
}
logrus.Infof("%s service restarted", serviceFilter)
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,7 +70,12 @@ recipes.
logrus.Fatalf("'%s' is not deployed?", app.Name) logrus.Fatalf("'%s' is not deployed?", app.Name)
} }
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type) catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type, catl)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -62,7 +62,12 @@ recipes.
logrus.Fatalf("'%s' is not deployed?", app.Name) logrus.Fatalf("'%s' is not deployed?", app.Name)
} }
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type) catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type, catl)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -56,7 +56,7 @@ Example:
Supported shells are as follows: Supported shells are as follows:
fish fizsh
zsh zsh
bash bash
`, `,
@ -69,16 +69,16 @@ Supported shells are as follows:
} }
supportedShells := map[string]bool{ supportedShells := map[string]bool{
"bash": true, "bash": true,
"zsh": true, "zsh": true,
"fish": true, "fizsh": true,
} }
if _, ok := supportedShells[shellType]; !ok { if _, ok := supportedShells[shellType]; !ok {
logrus.Fatalf("%s is not a supported shell right now, sorry", shellType) logrus.Fatalf("%s is not a supported shell right now, sorry", shellType)
} }
if shellType == "fish" { if shellType == "fizsh" {
shellType = "zsh" // handled the same on the autocompletion side shellType = "zsh" // handled the same on the autocompletion side
} }

View File

@ -22,9 +22,9 @@ import (
// CatalogueSkipList is all the repos that are not recipes. // CatalogueSkipList is all the repos that are not recipes.
var CatalogueSkipList = map[string]bool{ var CatalogueSkipList = map[string]bool{
"abra": true, "abra": true,
"abra-bash": true,
"abra-apps": true, "abra-apps": true,
"abra-aur": true, "abra-aur": true,
"abra-bash": true,
"abra-capsul": true, "abra-capsul": true,
"abra-gandi": true, "abra-gandi": true,
"abra-hetzner": true, "abra-hetzner": true,
@ -34,20 +34,24 @@ var CatalogueSkipList = map[string]bool{
"auto-mirror": true, "auto-mirror": true,
"backup-bot": true, "backup-bot": true,
"backup-bot-two": true, "backup-bot-two": true,
"comrade-renovate-bot": true,
"coopcloud.tech": true, "coopcloud.tech": true,
"coturn": true, "coturn": true,
"docker-cp-deploy": true, "docker-cp-deploy": true,
"docker-dind-bats-kcov": true, "docker-dind-bats-kcov": true,
"docs.coopcloud.tech": true, "docs.coopcloud.tech": true,
"drone-abra": true,
"example": true, "example": true,
"gardening": true, "gardening": true,
"go-abra": true, "go-abra": true,
"organising": true, "organising": true,
"pyabra": true, "pyabra": true,
"radicle-seed-node": true, "radicle-seed-node": true,
"recipes": true,
"stack-ssh-deploy": true, "stack-ssh-deploy": true,
"swarm-cronjob": true, "swarm-cronjob": true,
"tagcmp": true, "tagcmp": true,
"traefik-cert-dumper": true,
"tyop": true, "tyop": true,
} }
@ -90,6 +94,9 @@ A new catalogue copy can be published to the recipes repository by passing the
ArgsUsage: "[<recipe>]", ArgsUsage: "[<recipe>]",
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipeName := c.Args().First() recipeName := c.Args().First()
if recipeName != "" {
internal.ValidateRecipe(c)
}
catalogueDir := path.Join(config.ABRA_DIR, "catalogue") catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "recipes") url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "recipes")
@ -129,6 +136,15 @@ A new catalogue copy can be published to the recipes repository by passing the
logrus.Fatal(err) logrus.Fatal(err)
} }
isClean, err := gitPkg.IsClean(rm.Name)
if err != nil {
logrus.Fatal(err)
}
if !isClean {
logrus.Fatalf("'%s' has locally unstaged changes", rm.Name)
}
if err := gitPkg.EnsureUpToDate(recipeDir); err != nil { if err := gitPkg.EnsureUpToDate(recipeDir); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -160,6 +176,11 @@ A new catalogue copy can be published to the recipes repository by passing the
logrus.Fatal(err) logrus.Fatal(err)
} }
features, category, err := catalogue.GetRecipeFeaturesAndCategory(recipeMeta.Name)
if err != nil {
logrus.Warn(err)
}
catl[recipeMeta.Name] = catalogue.RecipeMeta{ catl[recipeMeta.Name] = catalogue.RecipeMeta{
Name: recipeMeta.Name, Name: recipeMeta.Name,
Repository: recipeMeta.CloneURL, Repository: recipeMeta.CloneURL,
@ -168,8 +189,8 @@ A new catalogue copy can be published to the recipes repository by passing the
Description: recipeMeta.Description, Description: recipeMeta.Description,
Website: recipeMeta.Website, Website: recipeMeta.Website,
Versions: versions, Versions: versions,
// Category: ..., // FIXME: parse & load Category: category,
// Features: ..., // FIXME: parse & load Features: features,
} }
catlBar.Add(1) catlBar.Add(1)
} }

View File

@ -30,18 +30,6 @@ var VerboseFlag = &cli.BoolFlag{
Usage: "Show INFO messages", Usage: "Show INFO messages",
} }
// Debug stores the variable from DebugFlag.
var Debug bool
// DebugFlag turns on/off verbose logging down to the DEBUG level.
var DebugFlag = &cli.BoolFlag{
Name: "debug",
Aliases: []string{"d"},
Value: false,
Destination: &Debug,
Usage: "Show DEBUG messages",
}
func newAbraApp(version, commit string) *cli.App { func newAbraApp(version, commit string) *cli.App {
app := &cli.App{ app := &cli.App{
Name: "abra", Name: "abra",
@ -66,7 +54,7 @@ func newAbraApp(version, commit string) *cli.App {
}, },
Flags: []cli.Flag{ Flags: []cli.Flag{
VerboseFlag, VerboseFlag,
DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
}, },
Authors: []*cli.Author{ Authors: []*cli.Author{
@ -80,7 +68,7 @@ func newAbraApp(version, commit string) *cli.App {
app.EnableBashCompletion = true app.EnableBashCompletion = true
app.Before = func(c *cli.Context) error { app.Before = func(c *cli.Context) error {
if Debug { if internal.Debug {
logrus.SetLevel(logrus.DebugLevel) logrus.SetLevel(logrus.DebugLevel)
logrus.SetFormatter(&logrus.TextFormatter{}) logrus.SetFormatter(&logrus.TextFormatter{})
logrus.SetOutput(os.Stderr) logrus.SetOutput(os.Stderr)

View File

@ -259,3 +259,60 @@ var HetznerCloudAPITokenFlag = &cli.StringFlag{
EnvVars: []string{"HCLOUD_TOKEN"}, EnvVars: []string{"HCLOUD_TOKEN"},
Destination: &HetznerCloudAPIToken, Destination: &HetznerCloudAPIToken,
} }
// Debug stores the variable from DebugFlag.
var Debug bool
// DebugFlag turns on/off verbose logging down to the DEBUG level.
var DebugFlag = &cli.BoolFlag{
Name: "debug",
Aliases: []string{"d"},
Value: false,
Destination: &Debug,
Usage: "Show DEBUG messages",
}
// SSHFailMsg is a hopefully helpful SSH failure message
var SSHFailMsg = `
Woops, Abra is unable to connect to connect to %s.
Here are a few tips for debugging your local SSH config. Abra uses plain 'ol
SSH to make connections to servers, so if your SSH config is working, Abra is
working.
In the first place, Abra will always try to read your Docker context connection
string for SSH connection details. You can view your server context configs
with the following command. Are they correct?
abra server ls
Is your ssh-agent running? You can start it by running the following command:
eval "$(ssh-agent)"
If your SSH private key loaded? You can check by running the following command:
ssh-add -L
If you are using a non-default public/private key, you can configure this in
your ~/.ssh/config file which Abra will read in order to figure out connection
details:
Host foo.coopcloud.tech
Hostname foo.coopcloud.tech
User bar
Port 12345
IdentityFile ~/.ssh/bar@foo.coopcloud.tech
If you're only using password authentication, you can use the following config:
Host foo.coopcloud.tech
Hostname foo.coopcloud.tech
User bar
Port 12345
PreferredAuthentications=password
PubkeyAuthentication=no
Good luck!
`

74
cli/internal/copy.go Normal file
View File

@ -0,0 +1,74 @@
package internal
import (
"fmt"
"os"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/archive"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
func ConfigureAndCp(c *cli.Context, app config.App, srcPath string, dstPath string, service string, isToContainer bool) error {
appFiles, err := config.LoadAppFiles("")
if err != nil {
logrus.Fatal(err)
}
appEnv, err := config.GetApp(appFiles, app.Name)
if err != nil {
logrus.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("%s_%s", appEnv.StackName(), service))
containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
if err != nil {
logrus.Fatal(err)
}
if len(containers) != 1 {
logrus.Fatalf("expected 1 container but got %v", len(containers))
}
container := containers[0]
logrus.Debugf("retrieved '%s' as target container on '%s'", formatter.ShortenID(container.ID), app.Server)
if isToContainer {
if _, err := os.Stat(srcPath); err != nil {
logrus.Fatalf("'%s' does not exist?", srcPath)
}
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
content, err := archive.TarWithOptions(srcPath, toTarOpts)
if err != nil {
logrus.Fatal(err)
}
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(c.Context, container.ID, dstPath, content, copyOpts); err != nil {
logrus.Fatal(err)
}
} else {
content, _, err := cl.CopyFromContainer(c.Context, container.ID, srcPath)
if err != nil {
logrus.Fatal(err)
}
defer content.Close()
fromTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
if err := archive.Untar(content, dstPath, fromTarOpts); err != nil {
logrus.Fatal(err)
}
}
return nil
}

View File

@ -8,6 +8,7 @@ import (
"coopcloud.tech/abra/pkg/catalogue" "coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/dns"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
@ -25,7 +26,7 @@ func DeployAction(c *cli.Context) error {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("checking whether '%s' is already deployed", stackName) logrus.Debugf("checking whether %s is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName) isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
if err != nil { if err != nil {
@ -33,24 +34,26 @@ func DeployAction(c *cli.Context) error {
} }
if isDeployed { if isDeployed {
if Force { if Force || Chaos {
logrus.Warnf("'%s' already deployed but continuing (--force)", stackName) logrus.Warnf("%s is already deployed but continuing (--force/--chaos)", stackName)
} else if Chaos {
logrus.Warnf("'%s' already deployed but continuing (--chaos)", stackName)
} else { } else {
logrus.Fatalf("'%s' is already deployed", stackName) logrus.Fatalf("%s is already deployed", stackName)
} }
} }
version := deployedVersion version := deployedVersion
if version == "" && !Chaos { if version == "" && !Chaos {
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type) catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type, catl)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if len(versions) > 0 { if len(versions) > 0 {
version = versions[len(versions)-1] version = versions[len(versions)-1]
logrus.Debugf("choosing '%s' as version to deploy", version) logrus.Debugf("choosing %s as version to deploy", version)
if err := recipe.EnsureVersion(app.Type, version); err != nil { if err := recipe.EnsureVersion(app.Type, version); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -64,7 +67,13 @@ func DeployAction(c *cli.Context) error {
} }
if version == "" && !Chaos { if version == "" && !Chaos {
logrus.Debugf("choosing '%s' as version to deploy", version) logrus.Debugf("choosing %s as version to deploy", version)
if err := recipe.EnsureVersion(app.Type, version); err != nil {
logrus.Fatal(err)
}
}
if version != "" && !Chaos {
if err := recipe.EnsureVersion(app.Type, version); err != nil { if err := recipe.EnsureVersion(app.Type, version); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -107,6 +116,16 @@ func DeployAction(c *cli.Context) error {
logrus.Fatal(err) logrus.Fatal(err)
} }
domainName := app.Env["DOMAIN"]
ipv4, err := dns.EnsureIPv4(domainName)
if err != nil || ipv4 == "" {
logrus.Fatalf("could not find an IP address assigned to %s?", domainName)
}
if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
logrus.Fatal(err)
}
if err := stack.RunDeploy(cl, deployOpts, compose); err != nil { if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

68
cli/internal/logs.go Normal file
View File

@ -0,0 +1,68 @@
package internal
import (
"io"
"os"
"sync"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var Stderr bool
var StderrFlag = &cli.BoolFlag{
Name: "stderr",
Aliases: []string{"e"},
Value: false,
Destination: &Stderr,
}
var Stdout bool
var StdoutFlag = &cli.BoolFlag{
Name: "stdout",
Aliases: []string{"o"},
Value: false,
Destination: &Stdout,
}
var Healthcheck bool
var HealthcheckFlag = &cli.BoolFlag{
Name: "healthcheck",
Aliases: []string{"c"},
Value: false,
Destination: &Healthcheck,
}
// StackLogs lists logs for all stack services
func StackLogs(c *cli.Context, stackName string, logOpts types.ContainerLogsOptions, client *dockerClient.Client) {
filters := filters.NewArgs()
filters.Add("name", stackName)
serviceOpts := types.ServiceListOptions{Filters: filters}
services, err := client.ServiceList(c.Context, serviceOpts)
if err != nil {
logrus.Fatal(err)
}
var wg sync.WaitGroup
for _, service := range services {
wg.Add(1)
go func(s string) {
logs, err := client.ServiceLogs(c.Context, s, logOpts)
if err != nil {
logrus.Fatal(err)
}
// defer after err check as any err returns a nil io.ReadCloser
defer logs.Close()
_, err = io.Copy(os.Stdout, logs)
if err != nil && err != io.EOF {
logrus.Fatal(err)
}
}(service.ID)
}
wg.Wait()
os.Exit(0)
}

View File

@ -8,6 +8,7 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
"coopcloud.tech/abra/pkg/ssh"
"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"
@ -163,6 +164,10 @@ func NewAction(c *cli.Context) error {
} }
if Secrets { if Secrets {
if err := ssh.EnsureHostKey(NewAppServer); err != nil {
logrus.Fatal(err)
}
secrets, err := createSecrets(sanitisedAppName) secrets, err := createSecrets(sanitisedAppName)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)

View File

@ -8,6 +8,7 @@ import (
"coopcloud.tech/abra/pkg/catalogue" "coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/ssh"
"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"
@ -26,7 +27,11 @@ 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) if c.Command.Name == "generate" {
logrus.Warn(err)
} else {
logrus.Fatal(err)
}
} }
logrus.Debugf("validated '%s' as recipe argument", recipeName) logrus.Debugf("validated '%s' as recipe argument", recipeName)
@ -98,6 +103,39 @@ func ValidateApp(c *cli.Context) config.App {
logrus.Fatal(err) logrus.Fatal(err)
} }
if err := ssh.EnsureHostKey(app.Server); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("validated '%s' as app argument", appName)
return app
}
// ValidateAppByName ensures the app is valid and takes an app name as an argument, not context.
func ValidateAppByName(c *cli.Context, appName string) config.App {
if AppName != "" {
appName = AppName
logrus.Debugf("programmatically setting app name to %s", appName)
}
if appName == "" {
ShowSubcommandHelpAndError(c, errors.New("no app provided"))
}
app, err := app.Get(appName)
if err != nil {
logrus.Fatal(err)
}
if err := recipe.EnsureExists(app.Type); err != nil {
logrus.Fatal(err)
}
if err := ssh.EnsureHostKey(app.Server); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("validated '%s' as app argument", appName) logrus.Debugf("validated '%s' as app argument", appName)
return app return app

View File

@ -82,6 +82,11 @@ The new example repository is cloned to ~/.abra/apps/<recipe>.
} }
} }
newGitRepo := path.Join(config.APPS_DIR, recipeName)
if err := git.Init(newGitRepo, true); err != nil {
logrus.Fatal(err)
}
logrus.Infof( logrus.Infof(
"new recipe '%s' created in %s, happy hacking!\n", "new recipe '%s' created in %s, happy hacking!\n",
recipeName, path.Join(config.APPS_DIR, recipeName), recipeName, path.Join(config.APPS_DIR, recipeName),

View File

@ -39,7 +39,7 @@ system.
You may invoke this command in "wizard" mode and be prompted for input: You may invoke this command in "wizard" mode and be prompted for input:
abra recipe sync gitea abra recipe sync
`, `,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {

View File

@ -36,6 +36,11 @@ 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.
You may invoke this command in "wizard" mode and be prompted for input:
abra recipe upgrade
`, `,
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -44,7 +49,7 @@ is up to the end-user to decide.
internal.MajorFlag, internal.MajorFlag,
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c) recipe := internal.ValidateRecipeWithPrompt(c)
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch) bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 { if bumpType != 0 {

View File

@ -1,23 +1,21 @@
package server package server
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"net"
"os" "os"
"os/exec" "os/exec"
"os/user" "os/user"
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
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/config"
contextPkg "coopcloud.tech/abra/pkg/context" contextPkg "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/dns"
"coopcloud.tech/abra/pkg/server" "coopcloud.tech/abra/pkg/server"
"coopcloud.tech/abra/pkg/ssh" "coopcloud.tech/abra/pkg/ssh"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
@ -238,7 +236,7 @@ func installDocker(c *cli.Context, cl *dockerClient.Client, sshCl *ssh.Client, d
} }
} }
logrus.Infof("docker is installed on %s", domainName) logrus.Infof("docker is already installed on %s", domainName)
return nil return nil
} }
@ -268,39 +266,18 @@ func initSwarmLocal(c *cli.Context, cl *dockerClient.Client, domainName string)
} }
func initSwarm(c *cli.Context, cl *dockerClient.Client, domainName string) error { func initSwarm(c *cli.Context, cl *dockerClient.Client, domainName string) error {
// comrade librehosters DNS resolver -> https://www.privacy-handbuch.de/handbuch_93d.htm ipv4, err := dns.EnsureIPv4(domainName)
freifunkDNS := "5.1.66.255:53"
resolver := &net.Resolver{
PreferGo: false,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{
Timeout: time.Millisecond * time.Duration(10000),
}
return d.DialContext(ctx, "udp", freifunkDNS)
},
}
logrus.Debugf("created DNS resolver via '%s'", freifunkDNS)
ips, err := resolver.LookupIPAddr(c.Context, domainName)
if err != nil { if err != nil {
return err return err
} }
if len(ips) == 0 {
return fmt.Errorf("unable to retrieve ipv4 address for %s", domainName)
}
ipv4 := ips[0].IP.To4().String()
logrus.Debugf("discovered the following ipv4 addr: %s", ipv4)
initReq := swarm.InitRequest{ initReq := swarm.InitRequest{
ListenAddr: "0.0.0.0:2377", ListenAddr: "0.0.0.0:2377",
AdvertiseAddr: ipv4, AdvertiseAddr: ipv4,
} }
if _, err := cl.SwarmInit(c.Context, initReq); err != nil { if _, err := cl.SwarmInit(c.Context, initReq); err != nil {
if !strings.Contains(err.Error(), "is already part of a swarm") { if !strings.Contains(err.Error(), "is already part of a swarm") ||
!strings.Contains(err.Error(), "must specify a listening address") {
return err return err
} }
logrus.Infof("swarm mode already initialised on %s", domainName) logrus.Infof("swarm mode already initialised on %s", domainName)

101
cli/server/logs.go Normal file
View File

@ -0,0 +1,101 @@
package server
import (
"fmt"
"io"
"os"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var Taillen string
var TaillenFlag = &cli.StringFlag{
Name: "tail",
Aliases: []string{"t"},
Value: "5",
Destination: &Taillen,
Usage: "change how many lines are shown",
}
var serverLogsCommand = &cli.Command{
Name: "logs",
Aliases: []string{"l"},
ArgsUsage: "<server>",
Usage: "show logs from all apps from server",
Flags: []cli.Flag{
TaillenFlag,
internal.StderrFlag,
internal.StdoutFlag,
internal.HealthcheckFlag,
},
Action: func(c *cli.Context) error {
serverName, err := internal.ValidateServer(c)
serviceName := ""
if !internal.Stderr && !internal.Stdout && !internal.Healthcheck {
internal.Stderr = true
internal.Stdout = true
internal.Healthcheck = true
}
logrus.Debugf("flags parsed. --stderr: %t, --stdout: %t, --healthcheck: %t", internal.Stderr, internal.Stdout, internal.Healthcheck)
if err != nil {
logrus.Fatal(err)
}
appMap, err := config.LoadAppFiles(serverName)
if err != nil {
logrus.Fatal(err)
}
logOpts := types.ContainerLogsOptions{
Details: false,
Follow: false,
ShowStderr: internal.Stderr,
ShowStdout: internal.Stdout,
Tail: Taillen,
Timestamps: true,
}
var appFiles []config.App
for appname, _ := range appMap {
app := internal.ValidateAppByName(c, appname)
appFiles = append(appFiles, app)
}
for _, app := range appFiles {
fmt.Println(app)
logrus.Debugf("checking logs for: %s", app.Name)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("tailing logs for all services")
filters := filters.NewArgs()
filters.Add("name", service)
serviceOpts := types.ServiceListOptions{Filters: filters}
services, err := cl.ServiceList(c.Context, serviceOpts)
if err != nil {
logrus.Fatal(err)
}
logs, err := cl.ServiceLogs(c.Context, services[0].ID, logOpts)
if err != nil {
logrus.Fatal(err)
}
logrus.Info(app.StackName())
for {
_, err = io.Copy(os.Stdout, logs)
if err == io.EOF {
break
} else if err != nil {
logrus.Fatal(err)
}
}
logs.Close()
}
return nil
},
}

View File

@ -23,5 +23,6 @@ apps, see available flags on "server add" for more.
serverAddCommand, serverAddCommand,
serverListCommand, serverListCommand,
serverRemoveCommand, serverRemoveCommand,
serverLogsCommand,
}, },
} }

View File

@ -8,12 +8,24 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
var RC bool
var RCFlag = &cli.BoolFlag{
Name: "rc",
Value: false,
Destination: &RC,
Usage: "Insatll the latest Release Candidate",
}
// UpgradeCommand upgrades abra in-place. // UpgradeCommand upgrades abra in-place.
var UpgradeCommand = &cli.Command{ var UpgradeCommand = &cli.Command{
Name: "upgrade", Name: "upgrade",
Usage: "Upgrade abra", Usage: "Upgrade abra",
Flags: []cli.Flag{RCFlag},
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")
if RC {
cmd = exec.Command("bash", "-c", "curl -s https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer | bash -s -- --rc")
}
logrus.Debugf("attempting to run '%s'", cmd) 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)

17
go.mod
View File

@ -4,16 +4,16 @@ go 1.16
require ( require (
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52 coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52
github.com/AlecAivazis/survey/v2 v2.3.1 github.com/AlecAivazis/survey/v2 v2.3.2
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4 github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
github.com/docker/cli v20.10.8+incompatible github.com/docker/cli v20.10.11+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.11+incompatible
github.com/docker/go-units v0.4.0 github.com/docker/go-units v0.4.0
github.com/go-git/go-git/v5 v5.4.2 github.com/go-git/go-git/v5 v5.4.2
github.com/hetznercloud/hcloud-go v1.32.0 github.com/hetznercloud/hcloud-go v1.33.1
github.com/moby/sys/signal v0.5.0 github.com/moby/sys/signal v0.6.0
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
@ -31,10 +31,11 @@ require (
github.com/docker/docker-credential-helpers v0.6.4 // indirect github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // 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/gliderlabs/ssh v0.2.2 github.com/gliderlabs/ssh v0.3.3
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/mux v1.8.0 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 github.com/hashicorp/go-retryablehttp v0.7.0
github.com/kevinburke/ssh_config v1.1.0
github.com/libdns/gandi v1.0.2 github.com/libdns/gandi v1.0.2
github.com/libdns/libdns v0.2.1 github.com/libdns/libdns v0.2.1
github.com/moby/sys/mount v0.2.0 // indirect github.com/moby/sys/mount v0.2.0 // indirect
@ -43,5 +44,5 @@ require (
github.com/theupdateframework/notary v0.7.0 // indirect github.com/theupdateframework/notary v0.7.0 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359
) )

40
go.sum
View File

@ -26,8 +26,8 @@ coopcloud.tech/libcapsul v0.0.0-20211022074848-c35e78fe3f3e/go.mod h1:HEQ9pSJRsD
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52 h1:cyFFOl0tKe+dVHt8saejG8xoff33eQiHxFCVzRpPUjM= coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52 h1:cyFFOl0tKe+dVHt8saejG8xoff33eQiHxFCVzRpPUjM=
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ= coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52/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.2 h1:TqTB+aDDCLYhf9/bD2TwSO8u8jDSmMUd2SUVO4gCnU8=
github.com/AlecAivazis/survey/v2 v2.3.1/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg= github.com/AlecAivazis/survey/v2 v2.3.2/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg=
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4 h1:aYUdiI42a4fWfPoUr25XlaJrFEICv24+o/gWhqYS/jk= github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4 h1:aYUdiI42a4fWfPoUr25XlaJrFEICv24+o/gWhqYS/jk=
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4/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=
@ -88,8 +88,9 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
@ -259,14 +260,14 @@ github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/cli v20.10.8+incompatible h1:/zO/6y9IOpcehE49yMRTV9ea0nBpb8OeqSskXLNfH1E= github.com/docker/cli v20.10.11+incompatible h1:tXU1ezXcruZQRrMP8RN2z9N91h+6egZTS1gsPsKantc=
github.com/docker/cli v20.10.8+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v20.10.11+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v20.10.8+incompatible h1:RVqD337BgQicVCzYrrlhLDWhq6OAD2PJDUg2LsEUvKM= github.com/docker/docker v20.10.11+incompatible h1:OqzI/g/W54LczvhnccGqniFoQghHx3pklbLuhfXpqGo=
github.com/docker/docker v20.10.8+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v20.10.11+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o= github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o=
github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
@ -302,7 +303,6 @@ github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= 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/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 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
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=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
@ -315,8 +315,9 @@ github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui72
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/gliderlabs/ssh v0.3.3 h1:mBQ8NiOgDkINJrZtoizkC3nDNYgSaWtxyem6S2XHBtA=
github.com/gliderlabs/ssh v0.3.3/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
@ -443,14 +444,20 @@ github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4=
github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hetznercloud/hcloud-go v1.32.0 h1:7zyN2V7hMlhm3HZdxOarmOtvzKvkcYKjM0hcwYMQZz0= github.com/hetznercloud/hcloud-go v1.33.1 h1:W1HdO2bRLTKU4WsyqAasDSpt54fYO4WNckWYfH5AuCQ=
github.com/hetznercloud/hcloud-go v1.32.0/go.mod h1:XX/TQub3ge0yWR2yHWmnDVIrB+MQbda1pHxkUmDlUME= github.com/hetznercloud/hcloud-go v1.33.1/go.mod h1:XX/TQub3ge0yWR2yHWmnDVIrB+MQbda1pHxkUmDlUME=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@ -489,8 +496,9 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= 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/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kevinburke/ssh_config v1.1.0 h1:pH/t1WS9NzT8go394IqZeJTMHVm6Cr6ZJ6AQ+mdNo/o=
github.com/kevinburke/ssh_config v1.1.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@ -563,8 +571,8 @@ github.com/moby/sys/mount v0.2.0/go.mod h1:aAivFE2LB3W4bACsUXChRHQ0qKWsetY4Y9V7s
github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
github.com/moby/sys/mountinfo v0.4.1 h1:1O+1cHA1aujwEwwVMa2Xm2l+gIpUHyd3+D+d7LZh1kM= github.com/moby/sys/mountinfo v0.4.1 h1:1O+1cHA1aujwEwwVMa2Xm2l+gIpUHyd3+D+d7LZh1kM=
github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
github.com/moby/sys/signal v0.5.0 h1:MzpEFrMxugDynb1gkTIThU1O3wEmrAkOY+G9dHcHnCc= github.com/moby/sys/signal v0.6.0 h1:aDpY94H8VlhTGa9sNYUFCFsMZIUh5wm0B6XkIoJj/iY=
github.com/moby/sys/signal v0.5.0/go.mod h1:JwObcMnOrUy2VTP5swPKWwywH0Mbgk8Y5qua9iwtIRM= github.com/moby/sys/signal v0.6.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg=
github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ=
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=
@ -820,6 +828,7 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh
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/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= 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/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=
@ -972,8 +981,9 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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/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/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/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/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=

View File

@ -7,7 +7,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http"
"os" "os"
"path" "path"
"strings" "strings"
@ -46,6 +45,7 @@ type features struct {
Image image `json:"image"` Image image `json:"image"`
Status int `json:"status"` Status int `json:"status"`
Tests string `json:"tests"` Tests string `json:"tests"`
SSO string `json:"sso"`
} }
// tag represents a git tag. // tag represents a git tag.
@ -122,7 +122,7 @@ func (r ByRecipeName) Less(i, j int) bool {
// recipeCatalogueFSIsLatest checks whether the recipe catalogue stored locally // recipeCatalogueFSIsLatest checks whether the recipe catalogue stored locally
// is up to date. // is up to date.
func recipeCatalogueFSIsLatest() (bool, error) { func recipeCatalogueFSIsLatest() (bool, error) {
httpClient := &http.Client{Timeout: web.Timeout} httpClient := web.NewHTTPRetryClient()
res, err := httpClient.Head(RecipeCatalogueURL) res, err := httpClient.Head(RecipeCatalogueURL)
if err != nil { if err != nil {
return false, err return false, err
@ -374,6 +374,127 @@ func ReadReposMetadata() (RepoCatalogue, error) {
return reposMeta, nil return reposMeta, nil
} }
func GetStringInBetween(str, start, end string) (result string, err error) {
// GetStringInBetween returns empty string if no start or end string found
s := strings.Index(str, start)
if s == -1 {
return "", fmt.Errorf("marker string '%s' not found", start)
}
s += len(start)
e := strings.Index(str[s:], end)
if e == -1 {
return "", fmt.Errorf("end marker '%s' not found", end)
}
return str[s : s+e], nil
}
func GetImageMetadata(imageRowString string) (image, error) {
img := image{}
imgFields := strings.Split(imageRowString, ",")
for i, elem := range imgFields {
imgFields[i] = strings.TrimSpace(elem)
}
if len(imgFields) < 3 {
logrus.Warnf("image string has incorrect format: %s", imageRowString)
return img, nil
}
img.Rating = imgFields[1]
img.Source = imgFields[2]
imgString := imgFields[0]
imageName, err := GetStringInBetween(imgString, "[", "]")
if err != nil {
logrus.Fatal(err)
}
img.Image = strings.ReplaceAll(imageName, "`", "")
imageURL, err := GetStringInBetween(imgString, "(", ")")
if err != nil {
logrus.Fatal(err)
}
img.URL = imageURL
return img, nil
}
func GetRecipeFeaturesAndCategory(recipeName string) (features, string, error) {
feat := features{}
var category string
readmePath := path.Join(config.ABRA_DIR, "apps", recipeName, "README.md")
logrus.Debugf("attempting to open '%s'", readmePath)
readmeFS, err := ioutil.ReadFile(readmePath)
if err != nil {
return feat, category, err
}
readmeMetadata, err := GetStringInBetween( // Find text between delimiters
string(readmeFS),
"<!-- metadata -->", "<!-- endmetadata -->",
)
if err != nil {
return feat, category, err
}
readmeLines := strings.Split( // Array item from lines
strings.ReplaceAll( // Remove \t tabs
readmeMetadata, "\t", "",
),
"\n")
for _, val := range readmeLines {
if strings.Contains(val, "**Category**") {
category = strings.TrimSpace(
strings.TrimPrefix(val, "* **Category**:"),
)
}
if strings.Contains(val, "**Backups**") {
feat.Backups = strings.TrimSpace(
strings.TrimPrefix(val, "* **Backups**:"),
)
}
if strings.Contains(val, "**Email**") {
feat.Email = strings.TrimSpace(
strings.TrimPrefix(val, "* **Email**:"),
)
}
if strings.Contains(val, "**SSO**") {
feat.SSO = strings.TrimSpace(
strings.TrimPrefix(val, "* **SSO**:"),
)
}
if strings.Contains(val, "**Healthcheck**") {
feat.Healthcheck = strings.TrimSpace(
strings.TrimPrefix(val, "* **Healthcheck**:"),
)
}
if strings.Contains(val, "**Tests**") {
feat.Tests = strings.TrimSpace(
strings.TrimPrefix(val, "* **Tests**:"),
)
}
if strings.Contains(val, "**Image**") {
imageMetadata, err := GetImageMetadata(strings.TrimSpace(
strings.TrimPrefix(val, "* **Image**:"),
))
if err != nil {
continue
}
feat.Image = imageMetadata
}
}
return feat, category, nil
}
// GetRecipeVersions retrieves all recipe versions. // GetRecipeVersions retrieves all recipe versions.
func GetRecipeVersions(recipeName string) (RecipeVersions, error) { func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
versions := RecipeVersions{} versions := RecipeVersions{}
@ -404,7 +525,7 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
checkOutOpts := &git.CheckoutOptions{ checkOutOpts := &git.CheckoutOptions{
Create: false, Create: false,
Force: true, Keep: true,
Branch: plumbing.ReferenceName(ref.Name()), Branch: plumbing.ReferenceName(ref.Name()),
} }
if err := worktree.Checkout(checkOutOpts); err != nil { if err := worktree.Checkout(checkOutOpts); err != nil {
@ -432,9 +553,21 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
path = strings.Split(path, "/")[1] path = strings.Split(path, "/")[1]
} }
var tag string
switch img.(type) {
case reference.NamedTagged:
tag = img.(reference.NamedTagged).Tag()
case reference.Named:
logrus.Warnf("%s service is missing image tag?", path)
continue
}
logrus.Debugf("looking up image: '%s' from '%s'", img, path)
digest, err := client.GetTagDigest(img) digest, err := client.GetTagDigest(img)
if err != nil { if err != nil {
return err logrus.Warn(err)
continue
} }
versionMeta[service.Name] = ServiceMeta{ versionMeta[service.Name] = ServiceMeta{
@ -465,7 +598,7 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
refName := fmt.Sprintf("refs/heads/%s", branch) refName := fmt.Sprintf("refs/heads/%s", branch)
checkOutOpts := &git.CheckoutOptions{ checkOutOpts := &git.CheckoutOptions{
Create: false, Create: false,
Force: true, Keep: true,
Branch: plumbing.ReferenceName(refName), Branch: plumbing.ReferenceName(refName),
} }
if err := worktree.Checkout(checkOutOpts); err != nil { if err := worktree.Checkout(checkOutOpts); err != nil {
@ -480,14 +613,9 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
} }
// GetRecipeCatalogueVersions list the recipe versions listed in the recipe catalogue. // GetRecipeCatalogueVersions list the recipe versions listed in the recipe catalogue.
func GetRecipeCatalogueVersions(recipeName string) ([]string, error) { func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]string, error) {
var versions []string var versions []string
catl, err := ReadRecipeCatalogue()
if err != nil {
return versions, err
}
if recipeMeta, exists := catl[recipeName]; exists { if recipeMeta, exists := catl[recipeName]; exists {
for _, versionMeta := range recipeMeta.Versions { for _, versionMeta := range recipeMeta.Versions {
for tag := range versionMeta { for tag := range versionMeta {

View File

@ -9,6 +9,7 @@ import (
"coopcloud.tech/abra/pkg/web" "coopcloud.tech/abra/pkg/web"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/hashicorp/go-retryablehttp"
) )
type RawTag struct { type RawTag struct {
@ -35,12 +36,12 @@ func GetRegistryTags(image string) (RawTags, error) {
func getRegv2Token(image reference.Named) (string, error) { func getRegv2Token(image reference.Named) (string, error) {
img := reference.Path(image) img := reference.Path(image)
authTokenURL := fmt.Sprintf("https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull", img) authTokenURL := fmt.Sprintf("https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull", img)
req, err := http.NewRequest("GET", authTokenURL, nil) req, err := retryablehttp.NewRequest("GET", authTokenURL, nil)
if err != nil { if err != nil {
return "", err return "", err
} }
client := &http.Client{Timeout: web.Timeout} client := web.NewHTTPRetryClient()
res, err := client.Do(req) res, err := client.Do(req)
if err != nil { if err != nil {
return "", err return "", err
@ -78,7 +79,7 @@ func GetTagDigest(image reference.Named) (string, error) {
tag := image.(reference.NamedTagged).Tag() tag := image.(reference.NamedTagged).Tag()
manifestURL := fmt.Sprintf("https://index.docker.io/v2/%s/manifests/%s", img, tag) manifestURL := fmt.Sprintf("https://index.docker.io/v2/%s/manifests/%s", img, tag)
req, err := http.NewRequest("GET", manifestURL, nil) req, err := retryablehttp.NewRequest("GET", manifestURL, nil)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -96,7 +97,7 @@ func GetTagDigest(image reference.Named) (string, error) {
"Authorization": []string{fmt.Sprintf("Bearer %s", token)}, "Authorization": []string{fmt.Sprintf("Bearer %s", token)},
} }
client := &http.Client{Timeout: web.Timeout} client := web.NewHTTPRetryClient()
res, err := client.Do(req) res, err := client.Do(req)
if err != nil { if err != nil {
return "", err return "", err

View File

@ -9,7 +9,6 @@ import (
"strings" "strings"
"coopcloud.tech/abra/cli/formatter" "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/ssh"
"coopcloud.tech/abra/pkg/upstream/convert" "coopcloud.tech/abra/pkg/upstream/convert"
loader "coopcloud.tech/abra/pkg/upstream/stack" loader "coopcloud.tech/abra/pkg/upstream/stack"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
@ -146,10 +145,6 @@ func LoadAppFiles(servers ...string) (AppFiles, error) {
logrus.Debugf("collecting metadata from '%v' servers: '%s'", len(servers), strings.Join(servers, ", ")) logrus.Debugf("collecting metadata from '%v' servers: '%s'", len(servers), strings.Join(servers, ", "))
if err := EnsureHostKeysAllServers(servers...); err != nil {
return nil, err
}
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)
@ -373,15 +368,3 @@ func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv AppEnv) (*comp
return compose, nil return compose, nil
} }
// EnsureHostKeysAllServers ensures all configured servers have server SSH host keys validated
func EnsureHostKeysAllServers(servers ...string) error {
for _, serverName := range servers {
logrus.Debugf("ensuring server SSH host key available for %s", serverName)
if err := ssh.EnsureHostKey(serverName); err != nil {
return err
}
}
return nil
}

View File

@ -1,8 +1,11 @@
package dns package dns
import ( import (
"context"
"fmt" "fmt"
"net"
"os" "os"
"time"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -26,3 +29,68 @@ func NewToken(provider, providerTokenEnvVar string) (string, error) {
return token, nil return token, nil
} }
// EnsureIPv4 ensures that an ipv4 address is set for a domain name
func EnsureIPv4(domainName string) (string, error) {
var ipv4 string
// comrade librehosters DNS resolver -> https://www.privacy-handbuch.de/handbuch_93d.htm
freifunkDNS := "5.1.66.255:53"
resolver := &net.Resolver{
PreferGo: false,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{
Timeout: time.Millisecond * time.Duration(10000),
}
return d.DialContext(ctx, "udp", freifunkDNS)
},
}
logrus.Debugf("created DNS resolver via '%s'", freifunkDNS)
ctx := context.Background()
ips, err := resolver.LookupIPAddr(ctx, domainName)
if err != nil {
return ipv4, err
}
if len(ips) == 0 {
return ipv4, fmt.Errorf("unable to retrieve ipv4 address for %s", domainName)
}
ipv4 = ips[0].IP.To4().String()
logrus.Debugf("discovered the following ipv4 addr: %s", ipv4)
return ipv4, nil
}
// EnsureDomainsResolveSameIPv4 ensures that domains resolve to the same ipv4 address
func EnsureDomainsResolveSameIPv4(domainName, server string) (string, error) {
var ipv4 string
domainIPv4, err := EnsureIPv4(domainName)
if err != nil {
return ipv4, err
}
if domainIPv4 == "" {
return ipv4, fmt.Errorf("cannot resolve ipv4 for %s?", domainName)
}
serverIPv4, err := EnsureIPv4(server)
if err != nil {
return ipv4, err
}
if serverIPv4 == "" {
return ipv4, fmt.Errorf("cannot resolve ipv4 for %s?", server)
}
if domainIPv4 != serverIPv4 {
err := "app domain %s (%s) does not appear to resolve to app server %s (%s)?"
return ipv4, fmt.Errorf(err, domainName, domainIPv4, server, serverIPv4)
}
return ipv4, nil
}

View File

@ -47,6 +47,16 @@ func EnsureUpToDate(dir string) error {
return err return err
} }
recipeName := filepath.Base(dir)
isClean, err := IsClean(recipeName)
if err != nil {
return err
}
if !isClean {
return fmt.Errorf("'%s' has locally unstaged changes", recipeName)
}
branch := "master" branch := "master"
if _, err := repo.Branch("master"); err != nil { if _, err := repo.Branch("master"); err != nil {
if _, err := repo.Branch("main"); err != nil { if _, err := repo.Branch("main"); err != nil {
@ -66,7 +76,7 @@ func EnsureUpToDate(dir string) error {
refName := fmt.Sprintf("refs/heads/%s", branch) refName := fmt.Sprintf("refs/heads/%s", branch)
checkOutOpts := &git.CheckoutOptions{ checkOutOpts := &git.CheckoutOptions{
Create: false, Create: false,
Force: true, Keep: true,
Branch: plumbing.ReferenceName(refName), Branch: plumbing.ReferenceName(refName),
} }
if err := worktree.Checkout(checkOutOpts); err != nil { if err := worktree.Checkout(checkOutOpts); err != nil {

14
pkg/git/common.go Normal file
View File

@ -0,0 +1,14 @@
package git
import (
"fmt"
"os"
)
// EnsureGitRepo ensures a git repo .git folder exists
func EnsureGitRepo(repoPath string) error {
if _, err := os.Stat(repoPath); os.IsNotExist(err) {
return fmt.Errorf("no .git directory in %s?", repoPath)
}
return nil
}

38
pkg/git/init.go Normal file
View File

@ -0,0 +1,38 @@
package git
import (
"github.com/go-git/go-git/v5"
gitPkg "github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
)
// Init inits a new repo and commits all the stuff if you want
func Init(repoPath string, commit bool) error {
if _, err := gitPkg.PlainInit(repoPath, false); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("initialised new git repo in %s", repoPath)
if commit {
commitRepo, err := git.PlainOpen(repoPath)
if err != nil {
logrus.Fatal(err)
}
commitWorktree, err := commitRepo.Worktree()
if err != nil {
logrus.Fatal(err)
}
if err := commitWorktree.AddGlob("**"); err != nil {
return err
}
if _, err = commitWorktree.Commit("init", &git.CommitOptions{}); err != nil {
return err
}
logrus.Debugf("init committed all files for new git repo in %s", repoPath)
}
return nil
}

View File

@ -1,11 +1,17 @@
package git package git
import ( import (
"io/ioutil"
"os/user"
"path" "path"
"path/filepath"
"strings"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
gitConfigPkg "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -40,6 +46,12 @@ func IsClean(recipeName string) (bool, error) {
return false, err return false, err
} }
patterns, err := GetExcludesFiles()
if err != nil {
return false, err
}
worktree.Excludes = append(patterns, worktree.Excludes...)
status, err := worktree.Status() status, err := worktree.Status()
if err != nil { if err != nil {
return false, err return false, err
@ -53,3 +65,95 @@ func IsClean(recipeName string) (bool, error) {
return status.IsClean(), nil return status.IsClean(), nil
} }
// GetExcludesFiles reads the exlude files from a global git ignore
func GetExcludesFiles() ([]gitignore.Pattern, error) {
var err error
var patterns []gitignore.Pattern
cfg, err := parseGitConfig()
if err != nil {
return patterns, err
}
excludesfile := getExcludesFile(cfg)
patterns, err = parseExcludesFile(excludesfile)
if err != nil {
return patterns, err
}
return patterns, nil
}
func parseGitConfig() (*gitConfigPkg.Config, error) {
cfg := gitConfigPkg.NewConfig()
usr, err := user.Current()
if err != nil {
return nil, err
}
b, err := ioutil.ReadFile(usr.HomeDir + "/.gitconfig")
if err != nil {
return nil, err
}
if err := cfg.Unmarshal(b); err != nil {
return nil, err
}
return cfg, err
}
func getExcludesFile(cfg *gitConfigPkg.Config) string {
for _, sec := range cfg.Raw.Sections {
if sec.Name == "core" {
for _, opt := range sec.Options {
if opt.Key == "excludesfile" {
return opt.Value
}
}
}
}
return ""
}
func parseExcludesFile(excludesfile string) ([]gitignore.Pattern, error) {
excludesfile, err := expandTilde(excludesfile)
if err != nil {
return nil, err
}
data, err := ioutil.ReadFile(excludesfile)
if err != nil {
return nil, err
}
var ps []gitignore.Pattern
for _, s := range strings.Split(string(data), "\n") {
if !strings.HasPrefix(s, "#") && len(strings.TrimSpace(s)) > 0 {
ps = append(ps, gitignore.ParsePattern(s, nil))
}
}
return ps, nil
}
func expandTilde(path string) (string, error) {
if !strings.HasPrefix(path, "~") {
return path, nil
}
var paths []string
u, err := user.Current()
if err != nil {
return "", err
}
for _, p := range strings.Split(path, string(filepath.Separator)) {
if p == "~" {
paths = append(paths, u.HomeDir)
} else {
paths = append(paths, p)
}
}
return "/" + filepath.Join(paths...), nil
}

View File

@ -2,6 +2,7 @@ package recipe
import ( import (
"fmt" "fmt"
"os"
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
@ -83,7 +84,7 @@ func Get(recipeName string) (Recipe, error) {
envSamplePath := path.Join(config.ABRA_DIR, "apps", recipeName, ".env.sample") envSamplePath := path.Join(config.ABRA_DIR, "apps", recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath) sampleEnv, err := config.ReadEnv(envSamplePath)
if err != nil { if err != nil {
logrus.Fatal(err) return Recipe{}, err
} }
opts := stack.Deploy{Composefiles: composeFiles} opts := stack.Deploy{Composefiles: composeFiles}
@ -95,13 +96,22 @@ func Get(recipeName string) (Recipe, error) {
return Recipe{Name: recipeName, Config: config}, nil return Recipe{Name: recipeName, Config: config}, nil
} }
// EnsureExists checks whether a recipe has been cloned locally or not. // EnsureExists ensures that a recipe is locally cloned
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 := gitPkg.Clone(recipeDir, url); err != nil { if _, err := os.Stat(recipeDir); os.IsNotExist(err) {
logrus.Debugf("%s does not exist, attemmpting to clone", recipeDir)
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, recipe)
if err := gitPkg.Clone(recipeDir, url); err != nil {
return err
}
}
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err return err
} }
return nil return nil
} }
@ -118,6 +128,10 @@ func EnsureVersion(recipeName, version string) error {
return fmt.Errorf("'%s' has locally unstaged changes", recipeName) return fmt.Errorf("'%s' has locally unstaged changes", recipeName)
} }
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err
}
repo, err := git.PlainOpen(recipeDir) repo, err := git.PlainOpen(recipeDir)
if err != nil { if err != nil {
return err return err
@ -143,7 +157,8 @@ func EnsureVersion(recipeName, version string) error {
logrus.Debugf("read '%s' as tags for recipe '%s'", strings.Join(parsedTags, ", "), recipeName) logrus.Debugf("read '%s' as tags for recipe '%s'", strings.Join(parsedTags, ", "), recipeName)
if tagRef.String() == "" { if tagRef.String() == "" {
return fmt.Errorf("%s is not available?", version) logrus.Warnf("%s recipe has no local tag: %s? this recipe version is not released?", recipeName, version)
return nil
} }
worktree, err := repo.Worktree() worktree, err := repo.Worktree()
@ -154,7 +169,7 @@ func EnsureVersion(recipeName, version string) error {
opts := &git.CheckoutOptions{ opts := &git.CheckoutOptions{
Branch: tagRef, Branch: tagRef,
Create: false, Create: false,
Force: true, Keep: true,
} }
if err := worktree.Checkout(opts); err != nil { if err := worktree.Checkout(opts); err != nil {
return err return err
@ -178,6 +193,10 @@ func EnsureLatest(recipeName string) error {
return fmt.Errorf("'%s' has locally unstaged changes", recipeName) return fmt.Errorf("'%s' has locally unstaged changes", recipeName)
} }
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err
}
logrus.Debugf("attempting to open git repository in '%s'", recipeDir) logrus.Debugf("attempting to open git repository in '%s'", recipeDir)
repo, err := git.PlainOpen(recipeDir) repo, err := git.PlainOpen(recipeDir)
@ -202,7 +221,7 @@ func EnsureLatest(recipeName string) error {
refName := fmt.Sprintf("refs/heads/%s", branch) refName := fmt.Sprintf("refs/heads/%s", branch)
checkOutOpts := &git.CheckoutOptions{ checkOutOpts := &git.CheckoutOptions{
Create: false, Create: false,
Force: true, Keep: true,
Branch: plumbing.ReferenceName(refName), Branch: plumbing.ReferenceName(refName),
} }

View File

@ -330,9 +330,9 @@ func HostKeyAddCallback(hostnameAndPort string, remote net.Addr, pubKey ssh.Publ
fmt.Printf(fmt.Sprintf(` fmt.Printf(fmt.Sprintf(`
You are attempting to make an SSH connection to a server but there is no entry You are attempting to make an SSH connection to a server but there is no entry
in your ~/.ssh/known_hosts file which confirms that this is indeed the server in your ~/.ssh/known_hosts file which confirms that you have already validated
you want to connect to. Please take a moment to validate the following SSH host that this is indeed the server you want to connect to. Please take a moment to
key, it is important. validate the following SSH host key, it is important.
Host: %s Host: %s
Fingerprint: %s Fingerprint: %s
@ -409,12 +409,31 @@ func connect(username, host, port string, authMethod ssh.AuthMethod, timeout tim
} }
func connectWithAgentTimeout(host, username, port string, timeout time.Duration) (*Client, error) { func connectWithAgentTimeout(host, username, port string, timeout time.Duration) (*Client, error) {
logrus.Debugf("using ssh-agent to make an SSH connection for %s", host)
sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")) sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
authMethod := ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers) agentCl := agent.NewClient(sshAgent)
authMethod := ssh.PublicKeysCallback(agentCl.Signers)
loadedKeys, err := agentCl.List()
if err != nil {
return nil, err
}
var convertedKeys []string
for _, key := range loadedKeys {
convertedKeys = append(convertedKeys, key.String())
}
if len(convertedKeys) > 0 {
logrus.Debugf("ssh-agent has these keys loaded: %s", strings.Join(convertedKeys, ","))
} else {
logrus.Debug("ssh-agent has no keys loaded")
}
return connect(username, host, port, authMethod, timeout) return connect(username, host, port, authMethod, timeout)
} }
@ -427,6 +446,11 @@ func connectWithPasswordTimeout(host, username, port, pass string, timeout time.
// EnsureHostKey ensures that a host key trusted and added to the ~/.ssh/known_hosts file // EnsureHostKey ensures that a host key trusted and added to the ~/.ssh/known_hosts file
func EnsureHostKey(hostname string) error { func EnsureHostKey(hostname string) error {
if hostname == "default" || hostname == "local" {
logrus.Debugf("not checking server SSH host key against local/default target")
return nil
}
exists, _, err := GetHostKey(hostname) exists, _, err := GetHostKey(hostname)
if err != nil { if err != nil {
return err return err
@ -539,11 +563,16 @@ func GetHostConfig(hostname, username, port string) (HostConfig, error) {
} }
idf = ssh_config.Get(hostname, "IdentityFile") idf = ssh_config.Get(hostname, "IdentityFile")
hostConfig.Host = host
if idf != "" { if idf != "" {
var err error
idf, err = identityFileAbsPath(idf)
if err != nil {
return hostConfig, err
}
hostConfig.IdentityFile = idf hostConfig.IdentityFile = idf
} }
hostConfig.Host = host
hostConfig.Port = port hostConfig.Port = port
hostConfig.User = username hostConfig.User = username
@ -551,3 +580,25 @@ func GetHostConfig(hostname, username, port string) (HostConfig, error) {
return hostConfig, nil return hostConfig, nil
} }
func identityFileAbsPath(relPath string) (string, error) {
var err error
var absPath string
if strings.HasPrefix(relPath, "~/") {
systemUser, err := user.Current()
if err != nil {
return absPath, err
}
absPath = filepath.Join(systemUser.HomeDir, relPath[2:])
} else {
absPath, err = filepath.Abs(relPath)
if err != nil {
return absPath, err
}
}
logrus.Debugf("resolved %s to %s to read the ssh identity file", relPath, absPath)
return absPath, nil
}

View File

@ -2,6 +2,7 @@ package commandconn
import ( import (
"context" "context"
"fmt"
"net" "net"
"net/url" "net/url"
@ -34,9 +35,25 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.Conne
if err != nil { if err != nil {
return nil, errors.Wrap(err, "ssh host connection is not valid") return nil, errors.Wrap(err, "ssh host connection is not valid")
} }
if err := sshPkg.EnsureHostKey(ctxConnDetails.Host); err != nil { if err := sshPkg.EnsureHostKey(ctxConnDetails.Host); err != nil {
return nil, err return nil, err
} }
hostConfig, err := sshPkg.GetHostConfig(
ctxConnDetails.Host,
ctxConnDetails.User,
ctxConnDetails.Port,
)
if err != nil {
return nil, err
}
if hostConfig.IdentityFile != "" {
msg := "discovered %s as identity file for %s, using for ssh connection"
logrus.Debugf(msg, hostConfig.IdentityFile, ctxConnDetails.Host)
sshFlags = append(sshFlags, fmt.Sprintf("-o IdentityFile=%s", hostConfig.IdentityFile))
}
return &connhelper.ConnectionHelper{ return &connhelper.ConnectionHelper{
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
return New(ctx, "ssh", append(sshFlags, ctxConnDetails.Args("docker", "system", "dial-stdio")...)...) return New(ctx, "ssh", append(sshFlags, ctxConnDetails.Args("docker", "system", "dial-stdio")...)...)

View File

@ -13,6 +13,11 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// DontSkipValidation ensures validation is done for compose file loading
func DontSkipValidation(opts *loader.Options) {
opts.SkipValidation = false
}
// LoadComposefile parse the composefile specified in the cli and returns its Config and version. // LoadComposefile parse the composefile specified in the cli and returns its Config and version.
func LoadComposefile(opts Deploy, appEnv map[string]string) (*composetypes.Config, error) { func LoadComposefile(opts Deploy, appEnv map[string]string) (*composetypes.Config, error) {
configDetails, err := getConfigDetails(opts.Composefiles, appEnv) configDetails, err := getConfigDetails(opts.Composefiles, appEnv)
@ -21,13 +26,12 @@ func LoadComposefile(opts Deploy, appEnv map[string]string) (*composetypes.Confi
} }
dicts := getDictsFrom(configDetails.ConfigFiles) dicts := getDictsFrom(configDetails.ConfigFiles)
config, err := loader.Load(configDetails) config, err := loader.Load(configDetails, DontSkipValidation)
if err != nil { if err != nil {
if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok { if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok {
return nil, fmt.Errorf("compose file contains unsupported options:\n\n%s", return nil, fmt.Errorf("compose file contains unsupported options:\n\n%s",
propertyWarnings(fpe.Properties)) propertyWarnings(fpe.Properties))
} }
return nil, err return nil, err
} }

View File

@ -3,10 +3,14 @@ package stack // https://github.com/docker/cli/blob/master/cli/command/stack/swa
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"io/ioutil"
"strings" "strings"
"time"
abraClient "coopcloud.tech/abra/pkg/client" abraClient "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/upstream/convert" "coopcloud.tech/abra/pkg/upstream/convert"
"github.com/docker/cli/cli/command/service/progress"
composetypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
@ -346,6 +350,7 @@ func deployServices(
existingServiceMap[service.Spec.Name] = service existingServiceMap[service.Spec.Name] = service
} }
serviceIDs := make(map[string]string)
for internalName, serviceSpec := range services { for internalName, serviceSpec := range services {
var ( var (
name = namespace.Scope(internalName) name = namespace.Scope(internalName)
@ -405,6 +410,8 @@ func deployServices(
return errors.Wrapf(err, "failed to update service %s", name) return errors.Wrapf(err, "failed to update service %s", name)
} }
serviceIDs[service.ID] = name
for _, warning := range response.Warnings { for _, warning := range response.Warnings {
logrus.Warn(warning) logrus.Warn(warning)
} }
@ -418,11 +425,39 @@ func deployServices(
createOpts.QueryRegistry = true createOpts.QueryRegistry = true
} }
if _, err := cl.ServiceCreate(ctx, serviceSpec, createOpts); err != nil { serviceCreateResponse, err := cl.ServiceCreate(ctx, serviceSpec, createOpts)
if err != nil {
return errors.Wrapf(err, "failed to create service %s", name) return errors.Wrapf(err, "failed to create service %s", name)
} }
serviceIDs[serviceCreateResponse.ID] = name
} }
} }
var serviceNames []string
for _, serviceName := range serviceIDs {
serviceNames = append(serviceNames, serviceName)
}
logrus.Infof("waiting for services to converge: %s", strings.Join(serviceNames, ", "))
ch := make(chan error, len(serviceIDs))
for serviceID, serviceName := range serviceIDs {
logrus.Debugf("waiting on %s to converge", serviceName)
go func(s string) {
ch <- waitOnService(ctx, cl, s)
}(serviceID)
}
for _, serviceID := range serviceIDs {
err := <-ch
if err != nil {
return err
}
logrus.Debugf("assuming %s converged successfully", serviceID)
}
logrus.Info("services converged 👌")
return nil return nil
} }
@ -437,3 +472,25 @@ func getStackSecrets(ctx context.Context, dockerclient client.APIClient, namespa
func getStackConfigs(ctx context.Context, dockerclient client.APIClient, namespace string) ([]swarm.Config, error) { func getStackConfigs(ctx context.Context, dockerclient client.APIClient, namespace string) ([]swarm.Config, error) {
return dockerclient.ConfigList(ctx, types.ConfigListOptions{Filters: getStackFilter(namespace)}) return dockerclient.ConfigList(ctx, types.ConfigListOptions{Filters: getStackFilter(namespace)})
} }
// https://github.com/docker/cli/blob/master/cli/command/service/helpers.go
// https://github.com/docker/cli/blob/master/cli/command/service/progress/progress.go
func waitOnService(ctx context.Context, cl *dockerclient.Client, serviceID string) error {
errChan := make(chan error, 1)
pipeReader, pipeWriter := io.Pipe()
go func() {
errChan <- progress.ServiceProgress(ctx, cl, serviceID, pipeWriter)
}()
go io.Copy(ioutil.Discard, pipeReader)
timeout := 60 * time.Second
select {
case err := <-errChan:
return err
case <-time.After(timeout):
return fmt.Errorf("%s has still not converged (%s second timeout)?", serviceID, timeout)
}
}

23
pkg/web/client.go Normal file
View File

@ -0,0 +1,23 @@
package web
import (
"fmt"
"github.com/hashicorp/go-retryablehttp"
"github.com/sirupsen/logrus"
)
type customLeveledLogger struct {
retryablehttp.Logger
}
func (l customLeveledLogger) Printf(msg string, args ...interface{}) {
logrus.Debugf(fmt.Sprintf(msg, args...))
}
// NewHTTPRetryClient instantiates a new http client with retries baked in
func NewHTTPRetryClient() *retryablehttp.Client {
retryClient := retryablehttp.NewClient()
retryClient.Logger = customLeveledLogger{}
return retryClient
}

View File

@ -3,7 +3,6 @@ package web
import ( import (
"encoding/json" "encoding/json"
"net/http"
"time" "time"
) )
@ -13,7 +12,7 @@ const Timeout = 10 * time.Second
// ReadJSON reads JSON and parses it into your chosen interface pointer // ReadJSON reads JSON and parses it into your chosen interface pointer
func ReadJSON(url string, target interface{}) error { func ReadJSON(url string, target interface{}) error {
httpClient := &http.Client{Timeout: Timeout} httpClient := NewHTTPRetryClient()
res, err := httpClient.Get(url) res, err := httpClient.Get(url)
if err != nil { if err != nil {
return err return err

3
renovate.json Normal file
View File

@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

View File

@ -2,6 +2,15 @@
ABRA_VERSION="0.3.0-alpha" ABRA_VERSION="0.3.0-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"
RC_VERSION="0.3.1-alpha-rc2"
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION"
for arg in "$@"; do
if [ "$arg" == "--rc" ]; then
ABRA_VERSION="$RC_VERSION"
ABRA_RELEASE_URL="$RC_VERSION_URL"
fi
done
function show_banner { function show_banner {
echo "" echo ""
@ -35,6 +44,7 @@ function install_abra_release {
exit 1 exit 1
fi fi
# FIXME: support different architectures # FIXME: support different architectures
PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m) PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m)
FILENAME="abra_"$ABRA_VERSION"_"$PLATFORM"" FILENAME="abra_"$ABRA_VERSION"_"$PLATFORM""
@ -66,19 +76,20 @@ function install_abra_release {
p=$HOME/.local/bin p=$HOME/.local/bin
com="echo PATH=\$PATH:$p" com="echo PATH=\$PATH:$p"
if [[ $SHELL =~ "bash" ]]; then if [[ $SHELL =~ "bash" ]]; then
echo "echo $com >> $HOME/.bashrc" echo "$com >> $HOME/.bashrc"
elif [[ $SHELL =~ "fizsh" ]]; then elif [[ $SHELL =~ "fizsh" ]]; then
echo "echo $com >> $HOME/.fizsh/.fizshrc" echo "$com >> $HOME/.fizsh/.fizshrc"
elif [[ $SHELL =~ "zsh" ]]; then elif [[ $SHELL =~ "zsh" ]]; then
echo "echo $com >> $HOME/.zshrc" echo "$com >> $HOME/.zshrc"
else else
echo "echo $com >> $HOME/.profile" echo "$com >> $HOME/.profile"
fi fi
fi fi
echo "abra installed to $HOME/.local/bin/abra" echo "abra installed to $HOME/.local/bin/abra"
} }
function run_installation { function run_installation {
show_banner show_banner
install_abra_release install_abra_release