forked from toolshed/abra
Compare commits
55 Commits
0.3.0-alph
...
0.3.1-alph
Author | SHA1 | Date | |
---|---|---|---|
a7ebcd8950 | |||
e589709cb0 | |||
56c3e070f5 | |||
cc37615d83 | |||
0b37f63248 | |||
9c3a06a7d9 | |||
cdef8b5ea5 | |||
cba261b18c | |||
1f6e4fa4a3 | |||
4a245c3e02 | |||
299faa1adf
|
|||
704e773a16 | |||
7143d09fd4 | |||
4e76d49c80 | |||
c9dff0c3bd | |||
e77e72a9e6 | |||
af6f759c92 | |||
034295332c | |||
dac2489e6d | |||
7bdc1946a2 | |||
2439643895 | |||
0876f677d1 | |||
31dafb3ae4 | |||
915083b426 | |||
486a1717e7 | |||
9122c0a9b8 | |||
85ff04202f | |||
ecba4e01f1 | |||
751b187df6 | |||
f74261dbe6 | |||
2600a8137c | |||
b6a6163eff | |||
c25b2b17df
|
|||
713308e0b8 | |||
fcbf41ee95 | |||
5add4ccc1b
|
|||
9220a8c09b
|
|||
f78a04109c | |||
b67ad02f87 | |||
215431696e | |||
cd361237e7 | |||
db10c7b849 | |||
d38f82ebe7 | |||
59031595ea | |||
6f26b51f3e | |||
17a5f1529a
|
|||
2ba6445daa | |||
edb427a7ae | |||
3dc186e231 | |||
1467ae5007 | |||
2b9395be1a | |||
a539033b55 | |||
63d9703d9d | |||
f9726b6643 | |||
4a0761926c |
@ -35,3 +35,4 @@ changelog:
|
||||
- "^style:"
|
||||
- "^test:"
|
||||
- "^tests:"
|
||||
- "^Revert"
|
||||
|
2
Makefile
2
Makefile
@ -32,7 +32,7 @@ static:
|
||||
@staticcheck $(ABRA)
|
||||
|
||||
test:
|
||||
@go test ./... -cover
|
||||
@go test ./... -cover -v
|
||||
|
||||
loc:
|
||||
@find . -name "*.go" | xargs wc -l
|
||||
|
14
README.md
14
README.md
@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
### Getting started
|
||||
|
@ -18,6 +18,7 @@ to scaling apps up and spinning them down.
|
||||
Subcommands: []*cli.Command{
|
||||
appNewCommand,
|
||||
appConfigCommand,
|
||||
appRestartCommand,
|
||||
appDeployCommand,
|
||||
appUpgradeCommand,
|
||||
appUndeployCommand,
|
||||
|
@ -5,13 +5,8 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@ -21,6 +16,18 @@ var appCpCommand = &cli.Command{
|
||||
Aliases: []string{"c"},
|
||||
ArgsUsage: "<src> <dst>",
|
||||
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 {
|
||||
app := internal.ValidateApp(c)
|
||||
|
||||
@ -64,61 +71,28 @@ var appCpCommand = &cli.Command{
|
||||
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 {
|
||||
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
|
||||
|
||||
},
|
||||
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)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
abraFormatter "coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/ssh"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
@ -69,6 +70,12 @@ can take some time.
|
||||
}
|
||||
sort.Sort(config.ByServerAndType(apps))
|
||||
|
||||
for _, app := range apps {
|
||||
if err := ssh.EnsureHostKey(app.Server); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
statuses := make(map[string]map[string]string)
|
||||
tableCol := []string{"Server", "Type", "Domain"}
|
||||
if status {
|
||||
|
72
cli/app/restart.go
Normal file
72
cli/app/restart.go
Normal 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)
|
||||
}
|
||||
},
|
||||
}
|
@ -8,10 +8,13 @@ import (
|
||||
"path"
|
||||
|
||||
"coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/git"
|
||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||
"coopcloud.tech/abra/pkg/limit"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@ -19,9 +22,9 @@ import (
|
||||
// CatalogueSkipList is all the repos that are not recipes.
|
||||
var CatalogueSkipList = map[string]bool{
|
||||
"abra": true,
|
||||
"abra-bash": true,
|
||||
"abra-apps": true,
|
||||
"abra-aur": true,
|
||||
"abra-bash": true,
|
||||
"abra-capsul": true,
|
||||
"abra-gandi": true,
|
||||
"abra-hetzner": true,
|
||||
@ -31,6 +34,7 @@ var CatalogueSkipList = map[string]bool{
|
||||
"auto-mirror": true,
|
||||
"backup-bot": true,
|
||||
"backup-bot-two": true,
|
||||
"comrade-renovate-bot": true,
|
||||
"coopcloud.tech": true,
|
||||
"coturn": true,
|
||||
"docker-cp-deploy": true,
|
||||
@ -48,26 +52,52 @@ var CatalogueSkipList = map[string]bool{
|
||||
"tyop": true,
|
||||
}
|
||||
|
||||
var commit bool
|
||||
var commitFlag = &cli.BoolFlag{
|
||||
Name: "commit",
|
||||
Usage: "Commits new generated catalogue changes",
|
||||
Value: false,
|
||||
Aliases: []string{"c"},
|
||||
Destination: &commit,
|
||||
}
|
||||
|
||||
var catalogueGenerateCommand = &cli.Command{
|
||||
Name: "generate",
|
||||
Aliases: []string{"g"},
|
||||
Usage: "Generate a new copy of the catalogue",
|
||||
ArgsUsage: "[<recipe>]",
|
||||
BashComplete: func(c *cli.Context) {
|
||||
catl, err := catalogue.ReadRecipeCatalogue()
|
||||
if err != nil {
|
||||
logrus.Warn(err)
|
||||
}
|
||||
if c.NArg() > 0 {
|
||||
return
|
||||
}
|
||||
for name := range catl {
|
||||
fmt.Println(name)
|
||||
}
|
||||
Flags: []cli.Flag{
|
||||
internal.PushFlag,
|
||||
commitFlag,
|
||||
internal.CommitMessageFlag,
|
||||
},
|
||||
Description: `
|
||||
This command generates a new copy of the recipe catalogue which can be found on:
|
||||
|
||||
https://recipes.coopcloud.tech
|
||||
|
||||
It polls the entire git.coopcloud.tech/coop-cloud/... recipe repository
|
||||
listing, parses README and tags to produce recipe metadata and produces a
|
||||
apps.json file which is placed in your ~/.abra/catalogue/recipes.json.
|
||||
|
||||
It is possible to generate new metadata for a single recipe by passing
|
||||
<recipe>. The existing local catalogue will be updated, not overwritten.
|
||||
|
||||
A new catalogue copy can be published to the recipes repository by passing the
|
||||
"--commit" and "--push" flags. The recipes repository is available here:
|
||||
|
||||
https://git.coopcloud.tech/coop-cloud/recipes
|
||||
|
||||
`,
|
||||
ArgsUsage: "[<recipe>]",
|
||||
Action: func(c *cli.Context) error {
|
||||
recipeName := c.Args().First()
|
||||
|
||||
catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
|
||||
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "recipes")
|
||||
if err := gitPkg.Clone(catalogueDir, url); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repos, err := catalogue.ReadReposMetadata()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
@ -96,11 +126,11 @@ var catalogueGenerateCommand = &cli.Command{
|
||||
|
||||
recipeDir := path.Join(config.ABRA_DIR, "apps", rm.Name)
|
||||
|
||||
if err := git.Clone(recipeDir, rm.SSHURL); err != nil {
|
||||
if err := gitPkg.Clone(recipeDir, rm.SSHURL); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := git.EnsureUpToDate(recipeDir); err != nil {
|
||||
if err := gitPkg.EnsureUpToDate(recipeDir); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
@ -139,8 +169,8 @@ var catalogueGenerateCommand = &cli.Command{
|
||||
Description: recipeMeta.Description,
|
||||
Website: recipeMeta.Website,
|
||||
Versions: versions,
|
||||
// Category: ..., // FIXME: once we sort out the machine-readable catalogue interface
|
||||
// Features: ..., // FIXME: once we figure out the machine-readable catalogue interface
|
||||
// Category: ..., // FIXME: parse & load
|
||||
// Features: ..., // FIXME: parse & load
|
||||
}
|
||||
catlBar.Add(1)
|
||||
}
|
||||
@ -172,8 +202,61 @@ var catalogueGenerateCommand = &cli.Command{
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Infof("generated new recipe catalogue in '%s'", config.APPS_JSON)
|
||||
cataloguePath := path.Join(config.ABRA_DIR, "catalogue", "recipes.json")
|
||||
logrus.Infof("generated new recipe catalogue in %s", cataloguePath)
|
||||
|
||||
if commit {
|
||||
repoPath := path.Join(config.ABRA_DIR, "catalogue")
|
||||
commitRepo, err := git.PlainOpen(repoPath)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
commitWorktree, err := commitRepo.Worktree()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if internal.CommitMessage == "" {
|
||||
prompt := &survey.Input{
|
||||
Message: "commit message",
|
||||
Default: "chore: publish new catalogue changes",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &internal.CommitMessage); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
err = commitWorktree.AddGlob("**.json")
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Debug("staged **.json for commit")
|
||||
|
||||
_, err = commitWorktree.Commit(internal.CommitMessage, &git.CommitOptions{})
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Info("changes commited")
|
||||
|
||||
if err := commitRepo.Push(&git.PushOptions{}); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Info("changes pushed")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
BashComplete: func(c *cli.Context) {
|
||||
catl, err := catalogue.ReadRecipeCatalogue()
|
||||
if err != nil {
|
||||
logrus.Warn(err)
|
||||
}
|
||||
if c.NArg() > 0 {
|
||||
return
|
||||
}
|
||||
for name := range catl {
|
||||
fmt.Println(name)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
16
cli/cli.go
16
cli/cli.go
@ -30,18 +30,6 @@ var VerboseFlag = &cli.BoolFlag{
|
||||
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 {
|
||||
app := &cli.App{
|
||||
Name: "abra",
|
||||
@ -66,7 +54,7 @@ func newAbraApp(version, commit string) *cli.App {
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
VerboseFlag,
|
||||
DebugFlag,
|
||||
internal.DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
},
|
||||
Authors: []*cli.Author{
|
||||
@ -80,7 +68,7 @@ func newAbraApp(version, commit string) *cli.App {
|
||||
app.EnableBashCompletion = true
|
||||
|
||||
app.Before = func(c *cli.Context) error {
|
||||
if Debug {
|
||||
if internal.Debug {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
logrus.SetFormatter(&logrus.TextFormatter{})
|
||||
logrus.SetOutput(os.Stderr)
|
||||
|
@ -20,6 +20,10 @@ func Truncate(str string) string {
|
||||
return fmt.Sprintf(`"%s"`, formatter.Ellipsis(str, 19))
|
||||
}
|
||||
|
||||
func SmallSHA(hash string) string {
|
||||
return hash[:8]
|
||||
}
|
||||
|
||||
// RemoveSha remove image sha from a string that are added in some docker outputs
|
||||
func RemoveSha(str string) string {
|
||||
return strings.Split(str, "@")[0]
|
||||
|
@ -259,3 +259,15 @@ var HetznerCloudAPITokenFlag = &cli.StringFlag{
|
||||
EnvVars: []string{"HCLOUD_TOKEN"},
|
||||
Destination: &HetznerCloudAPIToken,
|
||||
}
|
||||
|
||||
// Debug stores the variable from DebugFlag.
|
||||
var Debug bool
|
||||
|
||||
// DebugFlag turns on/off verbose logging down to the DEBUG level.
|
||||
var DebugFlag = &cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Aliases: []string{"d"},
|
||||
Value: false,
|
||||
Destination: &Debug,
|
||||
Usage: "Show DEBUG messages",
|
||||
}
|
||||
|
74
cli/internal/copy.go
Normal file
74
cli/internal/copy.go
Normal 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
|
||||
}
|
@ -8,6 +8,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/dns"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
@ -107,6 +108,16 @@ func DeployAction(c *cli.Context) error {
|
||||
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 {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/secret"
|
||||
"coopcloud.tech/abra/pkg/ssh"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
@ -163,6 +164,10 @@ func NewAction(c *cli.Context) error {
|
||||
}
|
||||
|
||||
if Secrets {
|
||||
if err := ssh.EnsureHostKey(NewAppServer); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
secrets, err := createSecrets(sanitisedAppName)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
|
152
cli/internal/recipe.go
Normal file
152
cli/internal/recipe.go
Normal file
@ -0,0 +1,152 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var Major bool
|
||||
var MajorFlag = &cli.BoolFlag{
|
||||
Name: "major",
|
||||
Usage: "Increase the major part of the version",
|
||||
Value: false,
|
||||
Aliases: []string{"ma", "x"},
|
||||
Destination: &Major,
|
||||
}
|
||||
|
||||
var Minor bool
|
||||
var MinorFlag = &cli.BoolFlag{
|
||||
Name: "minor",
|
||||
Usage: "Increase the minor part of the version",
|
||||
Value: false,
|
||||
Aliases: []string{"mi", "y"},
|
||||
Destination: &Minor,
|
||||
}
|
||||
|
||||
var Patch bool
|
||||
var PatchFlag = &cli.BoolFlag{
|
||||
Name: "patch",
|
||||
Usage: "Increase the patch part of the version",
|
||||
Value: false,
|
||||
Aliases: []string{"p", "z"},
|
||||
Destination: &Patch,
|
||||
}
|
||||
|
||||
var Dry bool
|
||||
var DryFlag = &cli.BoolFlag{
|
||||
Name: "dry-run",
|
||||
Usage: "No changes are made, only reports changes that would be made",
|
||||
Value: false,
|
||||
Aliases: []string{"d"},
|
||||
Destination: &Dry,
|
||||
}
|
||||
|
||||
var Push bool
|
||||
var PushFlag = &cli.BoolFlag{
|
||||
Name: "push",
|
||||
Usage: "Git push changes",
|
||||
Value: false,
|
||||
Aliases: []string{"P"},
|
||||
Destination: &Push,
|
||||
}
|
||||
|
||||
var CommitMessage string
|
||||
var CommitMessageFlag = &cli.StringFlag{
|
||||
Name: "commit-message",
|
||||
Usage: "Commit message (implies --commit)",
|
||||
Aliases: []string{"cm"},
|
||||
Destination: &CommitMessage,
|
||||
}
|
||||
|
||||
var Commit bool
|
||||
var CommitFlag = &cli.BoolFlag{
|
||||
Name: "commit",
|
||||
Usage: "Commits compose.**yml file changes to recipe repository",
|
||||
Value: false,
|
||||
Aliases: []string{"c"},
|
||||
Destination: &Commit,
|
||||
}
|
||||
|
||||
var TagMessage string
|
||||
var TagMessageFlag = &cli.StringFlag{
|
||||
Name: "tag-comment",
|
||||
Usage: "Description for release tag",
|
||||
Aliases: []string{"t", "tm"},
|
||||
Destination: &TagMessage,
|
||||
}
|
||||
|
||||
// PromptBumpType prompts for version bump type
|
||||
func PromptBumpType(tagString string) error {
|
||||
if (!Major && !Minor && !Patch) && tagString == "" {
|
||||
fmt.Printf(`
|
||||
semver cheat sheet (more via semver.org):
|
||||
major: new features/bug fixes, backwards incompatible
|
||||
minor: new features/bug fixes, backwards compatible
|
||||
patch: bug fixes, backwards compatible
|
||||
|
||||
`)
|
||||
var chosenBumpType string
|
||||
prompt := &survey.Select{
|
||||
Message: fmt.Sprintf("select recipe version increment type"),
|
||||
Options: []string{"major", "minor", "patch"},
|
||||
}
|
||||
if err := survey.AskOne(prompt, &chosenBumpType); err != nil {
|
||||
return err
|
||||
}
|
||||
SetBumpType(chosenBumpType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBumpType figures out which bump type is specified
|
||||
func GetBumpType() string {
|
||||
var bumpType string
|
||||
|
||||
if Major {
|
||||
bumpType = "major"
|
||||
} else if Minor {
|
||||
bumpType = "minor"
|
||||
} else if Patch {
|
||||
bumpType = "patch"
|
||||
} else {
|
||||
logrus.Fatal("no version bump type specififed?")
|
||||
}
|
||||
|
||||
return bumpType
|
||||
}
|
||||
|
||||
// SetBumpType figures out which bump type is specified
|
||||
func SetBumpType(bumpType string) {
|
||||
if bumpType == "major" {
|
||||
Major = true
|
||||
} else if bumpType == "minor" {
|
||||
Minor = true
|
||||
} else if bumpType == "patch" {
|
||||
Patch = true
|
||||
} else {
|
||||
logrus.Fatal("no version bump type specififed?")
|
||||
}
|
||||
}
|
||||
|
||||
// GetMainApp retrieves the main 'app' image name
|
||||
func GetMainApp(recipe recipe.Recipe) string {
|
||||
var app string
|
||||
|
||||
for _, service := range recipe.Config.Services {
|
||||
name := service.Name
|
||||
if name == "app" {
|
||||
app = strings.Split(service.Image, ":")[0]
|
||||
}
|
||||
}
|
||||
|
||||
if app == "" {
|
||||
logrus.Fatalf("%s has no main 'app' service?", recipe.Name)
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
@ -8,6 +8,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/ssh"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
@ -98,6 +99,10 @@ func ValidateApp(c *cli.Context) config.App {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := ssh.EnsureHostKey(app.Server); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Debugf("validated '%s' as app argument", appName)
|
||||
|
||||
return app
|
||||
|
@ -63,7 +63,14 @@ var recipeLintCommand = &cli.Command{
|
||||
allImagesTagged = false
|
||||
}
|
||||
|
||||
tag := img.(reference.NamedTagged).Tag()
|
||||
var tag string
|
||||
switch img.(type) {
|
||||
case reference.NamedTagged:
|
||||
tag = img.(reference.NamedTagged).Tag()
|
||||
case reference.Named:
|
||||
noUnstableTags = false
|
||||
}
|
||||
|
||||
if tag == "latest" {
|
||||
noUnstableTags = false
|
||||
}
|
||||
|
@ -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(
|
||||
"new recipe '%s' created in %s, happy hacking!\n",
|
||||
recipeName, path.Join(config.APPS_DIR, recipeName),
|
||||
|
@ -4,30 +4,6 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var Major bool
|
||||
var MajorFlag = &cli.BoolFlag{
|
||||
Name: "major",
|
||||
Value: false,
|
||||
Aliases: []string{"ma", "x"},
|
||||
Destination: &Major,
|
||||
}
|
||||
|
||||
var Minor bool
|
||||
var MinorFlag = &cli.BoolFlag{
|
||||
Name: "minor",
|
||||
Value: false,
|
||||
Aliases: []string{"mi", "y"},
|
||||
Destination: &Minor,
|
||||
}
|
||||
|
||||
var Patch bool
|
||||
var PatchFlag = &cli.BoolFlag{
|
||||
Name: "patch",
|
||||
Value: false,
|
||||
Aliases: []string{"p", "z"},
|
||||
Destination: &Patch,
|
||||
}
|
||||
|
||||
// RecipeCommand defines all recipe related sub-commands.
|
||||
var RecipeCommand = &cli.Command{
|
||||
Name: "recipe",
|
||||
|
@ -6,63 +6,25 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
abraFormatter "coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var Push bool
|
||||
var PushFlag = &cli.BoolFlag{
|
||||
Name: "push",
|
||||
Value: false,
|
||||
Destination: &Push,
|
||||
}
|
||||
|
||||
var Dry bool
|
||||
var DryFlag = &cli.BoolFlag{
|
||||
Name: "dry-run",
|
||||
Value: false,
|
||||
Aliases: []string{"d"},
|
||||
Destination: &Dry,
|
||||
}
|
||||
|
||||
var CommitMessage string
|
||||
var CommitMessageFlag = &cli.StringFlag{
|
||||
Name: "commit-message",
|
||||
Usage: "commit message. Implies --commit",
|
||||
Aliases: []string{"cm"},
|
||||
Destination: &CommitMessage,
|
||||
}
|
||||
|
||||
var Commit bool
|
||||
var CommitFlag = &cli.BoolFlag{
|
||||
Name: "commit",
|
||||
Usage: "add compose.yml to staging area and commit changes",
|
||||
Value: false,
|
||||
Aliases: []string{"c"},
|
||||
Destination: &Commit,
|
||||
}
|
||||
|
||||
var TagMessage string
|
||||
var TagMessageFlag = &cli.StringFlag{
|
||||
Name: "tag-comment",
|
||||
Usage: "tag comment. If not given, user will be asked for it",
|
||||
Aliases: []string{"t", "tm"},
|
||||
Destination: &TagMessage,
|
||||
}
|
||||
|
||||
var recipeReleaseCommand = &cli.Command{
|
||||
Name: "release",
|
||||
Usage: "tag a recipe",
|
||||
Usage: "Release a new recipe version",
|
||||
Aliases: []string{"rl"},
|
||||
ArgsUsage: "<recipe> [<tag>]",
|
||||
ArgsUsage: "<recipe> [<version>]",
|
||||
Description: `
|
||||
This command is used to specify a new tag for a recipe. These tags are used to
|
||||
identify different versions of the recipe and are published on the Co-op Cloud
|
||||
@ -83,23 +45,32 @@ updates are properly communicated.
|
||||
Abra does its best to read the "a.b.c" version scheme and communicate what
|
||||
action needs to be taken when performing different operations such as an update
|
||||
or a rollback of an app.
|
||||
|
||||
You may invoke this command in "wizard" mode and be prompted for input:
|
||||
|
||||
abra recipe release gitea
|
||||
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
DryFlag,
|
||||
PatchFlag,
|
||||
MinorFlag,
|
||||
MajorFlag,
|
||||
PushFlag,
|
||||
CommitFlag,
|
||||
CommitMessageFlag,
|
||||
TagMessageFlag,
|
||||
internal.DryFlag,
|
||||
internal.MajorFlag,
|
||||
internal.MinorFlag,
|
||||
internal.PatchFlag,
|
||||
internal.PushFlag,
|
||||
internal.CommitFlag,
|
||||
internal.CommitMessageFlag,
|
||||
internal.TagMessageFlag,
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
recipe := internal.ValidateRecipe(c)
|
||||
recipe := internal.ValidateRecipeWithPrompt(c)
|
||||
directory := path.Join(config.APPS_DIR, recipe.Name)
|
||||
tagstring := c.Args().Get(1)
|
||||
imagesTmp := getImageVersions(recipe)
|
||||
mainApp := getMainApp(recipe)
|
||||
tagString := c.Args().Get(1)
|
||||
mainApp := internal.GetMainApp(recipe)
|
||||
|
||||
imagesTmp, err := getImageVersions(recipe)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
mainAppVersion := imagesTmp[mainApp]
|
||||
|
||||
if err := recipePkg.EnsureExists(recipe.Name); err != nil {
|
||||
@ -107,28 +78,68 @@ or a rollback of an app.
|
||||
}
|
||||
|
||||
if mainAppVersion == "" {
|
||||
logrus.Fatal("main app version is empty?")
|
||||
logrus.Fatalf("main 'app' service version for %s is empty?", recipe.Name)
|
||||
}
|
||||
|
||||
if tagstring != "" {
|
||||
if _, err := tagcmp.Parse(tagstring); err != nil {
|
||||
if tagString != "" {
|
||||
if _, err := tagcmp.Parse(tagString); err != nil {
|
||||
logrus.Fatal("invalid tag specified")
|
||||
}
|
||||
}
|
||||
|
||||
if TagMessage == "" {
|
||||
if (!internal.Major && !internal.Minor && !internal.Patch) && tagString != "" {
|
||||
logrus.Fatal("please specify <version> or bump type (--major/--minor/--patch)")
|
||||
}
|
||||
|
||||
if (internal.Major || internal.Minor || internal.Patch) && tagString != "" {
|
||||
logrus.Fatal("cannot specify tag and bump type at the same time")
|
||||
}
|
||||
|
||||
// bumpType is used to decide what part of the tag should be incremented
|
||||
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
|
||||
if bumpType != 0 {
|
||||
// a bitwise check if the number is a power of 2
|
||||
if (bumpType & (bumpType - 1)) != 0 {
|
||||
logrus.Fatal("you can only use one of: --major, --minor, --patch.")
|
||||
}
|
||||
}
|
||||
|
||||
if err := internal.PromptBumpType(tagString); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if internal.TagMessage == "" {
|
||||
prompt := &survey.Input{
|
||||
Message: "tag message",
|
||||
Default: fmt.Sprintf("chore: publish new %s version", internal.GetBumpType()),
|
||||
}
|
||||
if err := survey.AskOne(prompt, &TagMessage); err != nil {
|
||||
if err := survey.AskOne(prompt, &internal.TagMessage); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var createTagOptions git.CreateTagOptions
|
||||
createTagOptions.Message = TagMessage
|
||||
createTagOptions.Message = internal.TagMessage
|
||||
|
||||
if Commit || (CommitMessage != "") {
|
||||
if !internal.Commit {
|
||||
prompt := &survey.Confirm{
|
||||
Message: "git commit changes also?",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &internal.Commit); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !internal.Push {
|
||||
prompt := &survey.Confirm{
|
||||
Message: "git push changes also?",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &internal.Push); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if internal.Commit || internal.CommitMessage != "" {
|
||||
commitRepo, err := git.PlainOpen(directory)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
@ -138,25 +149,31 @@ or a rollback of an app.
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if CommitMessage == "" {
|
||||
if internal.CommitMessage == "" {
|
||||
prompt := &survey.Input{
|
||||
Message: "commit message",
|
||||
Default: fmt.Sprintf("chore: publish new %s version", internal.GetBumpType()),
|
||||
}
|
||||
if err := survey.AskOne(prompt, &CommitMessage); err != nil {
|
||||
if err := survey.AskOne(prompt, &internal.CommitMessage); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
err = commitWorktree.AddGlob("compose.**yml")
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Debug("staged compose.**yml for commit")
|
||||
|
||||
_, err = commitWorktree.Commit(CommitMessage, &git.CommitOptions{})
|
||||
if !internal.Dry {
|
||||
_, err = commitWorktree.Commit(internal.CommitMessage, &git.CommitOptions{})
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Info("changes commited")
|
||||
} else {
|
||||
logrus.Info("dry run only: NOT committing changes")
|
||||
}
|
||||
}
|
||||
|
||||
repo, err := git.PlainOpen(directory)
|
||||
@ -168,20 +185,8 @@ or a rollback of an app.
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
// bumpType is used to decide what part of the tag should be incremented
|
||||
bumpType := btoi(Major)*4 + btoi(Minor)*2 + btoi(Patch)
|
||||
if bumpType != 0 {
|
||||
// a bitwise check if the number is a power of 2
|
||||
if (bumpType & (bumpType - 1)) != 0 {
|
||||
logrus.Fatal("you can only use one of: --major, --minor, --patch.")
|
||||
}
|
||||
}
|
||||
|
||||
if tagstring != "" {
|
||||
if bumpType > 0 {
|
||||
logrus.Warn("user specified a version number and --major/--minor/--patch at the same time! using version number...")
|
||||
}
|
||||
tag, err := tagcmp.Parse(tagstring)
|
||||
if tagString != "" {
|
||||
tag, err := tagcmp.Parse(tagString)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@ -193,19 +198,23 @@ or a rollback of an app.
|
||||
tag.Patch = "0"
|
||||
tag.MissingPatch = false
|
||||
}
|
||||
tagstring = fmt.Sprintf("%s+%s", tag.String(), mainAppVersion)
|
||||
if Dry {
|
||||
logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", tagstring, head.Hash()))
|
||||
tagString = fmt.Sprintf("%s+%s", tag.String(), mainAppVersion)
|
||||
if internal.Dry {
|
||||
hash := abraFormatter.SmallSHA(head.Hash().String())
|
||||
logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", tagString, hash))
|
||||
return nil
|
||||
}
|
||||
|
||||
repo.CreateTag(tagstring, head.Hash(), &createTagOptions)
|
||||
logrus.Info(fmt.Sprintf("created tag %s at %s", tagstring, head.Hash()))
|
||||
if Push {
|
||||
repo.CreateTag(tagString, head.Hash(), &createTagOptions)
|
||||
hash := abraFormatter.SmallSHA(head.Hash().String())
|
||||
logrus.Info(fmt.Sprintf("created tag %s at %s", tagString, hash))
|
||||
if internal.Push && !internal.Dry {
|
||||
if err := repo.Push(&git.PushOptions{}); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Info(fmt.Sprintf("pushed tag %s to remote", tagstring))
|
||||
logrus.Info(fmt.Sprintf("pushed tag %s to remote", tagString))
|
||||
} else {
|
||||
logrus.Info("dry run only: NOT pushing changes")
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -236,25 +245,23 @@ or a rollback of an app.
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(lastGitTag)
|
||||
|
||||
newTag := lastGitTag
|
||||
var newTagString string
|
||||
var newtagString string
|
||||
if bumpType > 0 {
|
||||
if Patch {
|
||||
if internal.Patch {
|
||||
now, err := strconv.Atoi(newTag.Patch)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
newTag.Patch = strconv.Itoa(now + 1)
|
||||
} else if Minor {
|
||||
} else if internal.Minor {
|
||||
now, err := strconv.Atoi(newTag.Minor)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
newTag.Patch = "0"
|
||||
newTag.Minor = strconv.Itoa(now + 1)
|
||||
} else if Major {
|
||||
} else if internal.Major {
|
||||
now, err := strconv.Atoi(newTag.Major)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
@ -263,57 +270,70 @@ or a rollback of an app.
|
||||
newTag.Minor = "0"
|
||||
newTag.Major = strconv.Itoa(now + 1)
|
||||
}
|
||||
} else {
|
||||
logrus.Fatal("we don't support automatic tag generation yet - specify a version or use one of: --major --minor --patch")
|
||||
}
|
||||
|
||||
newTag.Metadata = mainAppVersion
|
||||
newTagString = newTag.String()
|
||||
if Dry {
|
||||
logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", newTagString, head.Hash()))
|
||||
newtagString = newTag.String()
|
||||
if internal.Dry {
|
||||
hash := abraFormatter.SmallSHA(head.Hash().String())
|
||||
logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", newtagString, hash))
|
||||
return nil
|
||||
}
|
||||
|
||||
repo.CreateTag(newTagString, head.Hash(), &createTagOptions)
|
||||
logrus.Info(fmt.Sprintf("created tag %s at %s", newTagString, head.Hash()))
|
||||
if Push {
|
||||
repo.CreateTag(newtagString, head.Hash(), &createTagOptions)
|
||||
hash := abraFormatter.SmallSHA(head.Hash().String())
|
||||
logrus.Info(fmt.Sprintf("created tag %s at %s", newtagString, hash))
|
||||
if internal.Push && !internal.Dry {
|
||||
if err := repo.Push(&git.PushOptions{}); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Info(fmt.Sprintf("pushed tag %s to remote", newTagString))
|
||||
logrus.Info(fmt.Sprintf("pushed tag %s to remote", newtagString))
|
||||
} else {
|
||||
logrus.Info("gry run only: NOT pushing changes")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
func getImageVersions(recipe recipe.Recipe) map[string]string {
|
||||
|
||||
// getImageVersions retrieves image versions for a recipe
|
||||
func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
|
||||
var services = make(map[string]string)
|
||||
|
||||
for _, service := range recipe.Config.Services {
|
||||
if service.Image == "" {
|
||||
continue
|
||||
}
|
||||
srv := strings.Split(service.Image, ":")
|
||||
services[srv[0]] = srv[1]
|
||||
|
||||
img, err := reference.ParseNormalizedNamed(service.Image)
|
||||
if err != nil {
|
||||
return services, err
|
||||
}
|
||||
|
||||
return services
|
||||
path := reference.Path(img)
|
||||
if strings.Contains(path, "library") {
|
||||
path = strings.Split(path, "/")[1]
|
||||
}
|
||||
|
||||
func getMainApp(recipe recipe.Recipe) string {
|
||||
for _, service := range recipe.Config.Services {
|
||||
name := service.Name
|
||||
if name == "app" {
|
||||
return strings.Split(service.Image, ":")[0]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
var tag string
|
||||
switch img.(type) {
|
||||
case reference.NamedTagged:
|
||||
tag = img.(reference.NamedTagged).Tag()
|
||||
case reference.Named:
|
||||
logrus.Fatalf("%s service is missing image tag?", path)
|
||||
}
|
||||
|
||||
services[path] = tag
|
||||
}
|
||||
|
||||
return services, nil
|
||||
}
|
||||
|
||||
// btoi converts a boolean value into an integer
|
||||
func btoi(b bool) int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
@ -1,12 +1,17 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@ -15,18 +20,167 @@ var recipeSyncCommand = &cli.Command{
|
||||
Name: "sync",
|
||||
Usage: "Ensure recipe version labels are up-to-date",
|
||||
Aliases: []string{"s"},
|
||||
ArgsUsage: "<recipe> <version>",
|
||||
ArgsUsage: "<recipe> [<version>]",
|
||||
Flags: []cli.Flag{
|
||||
internal.DryFlag,
|
||||
internal.MajorFlag,
|
||||
internal.MinorFlag,
|
||||
internal.PatchFlag,
|
||||
},
|
||||
Description: `
|
||||
This command will generate labels for the main recipe service (i.e. by
|
||||
convention, typically the service named "app") which corresponds to the
|
||||
following format:
|
||||
convention, the service named "app") which corresponds to the following format:
|
||||
|
||||
coop-cloud.${STACK_NAME}.version=<version>
|
||||
|
||||
The <version> is determined by the recipe maintainer and is specified on the
|
||||
command-line. The <recipe> configuration will be updated on the local file
|
||||
system.
|
||||
|
||||
You may invoke this command in "wizard" mode and be prompted for input:
|
||||
|
||||
abra recipe sync
|
||||
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
recipe := internal.ValidateRecipeWithPrompt(c)
|
||||
|
||||
mainApp := internal.GetMainApp(recipe)
|
||||
|
||||
imagesTmp, err := getImageVersions(recipe)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
mainAppVersion := imagesTmp[mainApp]
|
||||
|
||||
tags, err := recipe.Tags()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
nextTag := c.Args().Get(1)
|
||||
if len(tags) == 0 && nextTag == "" {
|
||||
logrus.Warnf("no tags found for %s", recipe.Name)
|
||||
var chosenVersion string
|
||||
edPrompt := &survey.Select{
|
||||
Message: "which version do you want to begin with?",
|
||||
Options: []string{"0.1.0", "1.0.0"},
|
||||
}
|
||||
if err := survey.AskOne(edPrompt, &chosenVersion); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
nextTag = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion)
|
||||
}
|
||||
|
||||
if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
|
||||
if err := internal.PromptBumpType(""); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if nextTag == "" {
|
||||
recipeDir := path.Join(config.APPS_DIR, recipe.Name)
|
||||
repo, err := git.PlainOpen(recipeDir)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
var lastGitTag tagcmp.Tag
|
||||
iter, err := repo.Tags()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if err := iter.ForEach(func(ref *plumbing.Reference) error {
|
||||
obj, err := repo.TagObject(ref.Hash())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tagcmpTag, err := tagcmp.Parse(obj.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if (lastGitTag == tagcmp.Tag{}) {
|
||||
lastGitTag = tagcmpTag
|
||||
} else if tagcmpTag.IsGreaterThan(lastGitTag) {
|
||||
lastGitTag = tagcmpTag
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
// bumpType is used to decide what part of the tag should be incremented
|
||||
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
|
||||
if bumpType != 0 {
|
||||
// a bitwise check if the number is a power of 2
|
||||
if (bumpType & (bumpType - 1)) != 0 {
|
||||
logrus.Fatal("you can only use one of: --major, --minor, --patch.")
|
||||
}
|
||||
}
|
||||
|
||||
newTag := lastGitTag
|
||||
if bumpType > 0 {
|
||||
if internal.Patch {
|
||||
now, err := strconv.Atoi(newTag.Patch)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
newTag.Patch = strconv.Itoa(now + 1)
|
||||
} else if internal.Minor {
|
||||
now, err := strconv.Atoi(newTag.Minor)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
newTag.Patch = "0"
|
||||
newTag.Minor = strconv.Itoa(now + 1)
|
||||
} else if internal.Major {
|
||||
now, err := strconv.Atoi(newTag.Major)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
newTag.Patch = "0"
|
||||
newTag.Minor = "0"
|
||||
newTag.Major = strconv.Itoa(now + 1)
|
||||
}
|
||||
}
|
||||
|
||||
newTag.Metadata = mainAppVersion
|
||||
logrus.Debugf("choosing %s as new version for %s", newTag.String(), recipe.Name)
|
||||
nextTag = newTag.String()
|
||||
}
|
||||
|
||||
if _, err := tagcmp.Parse(nextTag); err != nil {
|
||||
logrus.Fatalf("invalid version %s specified", nextTag)
|
||||
}
|
||||
|
||||
mainService := "app"
|
||||
var services []string
|
||||
hasAppService := false
|
||||
for _, service := range recipe.Config.Services {
|
||||
services = append(services, service.Name)
|
||||
if service.Name == "app" {
|
||||
hasAppService = true
|
||||
logrus.Debugf("detected app service in %s", recipe.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasAppService {
|
||||
logrus.Fatalf("%s has no main 'app' service?", recipe.Name)
|
||||
}
|
||||
|
||||
logrus.Debugf("selecting %s as the service to sync version label", mainService)
|
||||
|
||||
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag)
|
||||
if !internal.Dry {
|
||||
if err := recipe.UpdateLabel(mainService, label); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Infof("synced label '%s' to service '%s'", label, mainService)
|
||||
} else {
|
||||
logrus.Infof("dry run only: NOT syncing label %s for recipe %s", nextTag, recipe.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
BashComplete: func(c *cli.Context) {
|
||||
catl, err := catalogue.ReadRecipeCatalogue()
|
||||
if err != nil {
|
||||
@ -39,50 +193,4 @@ system.
|
||||
fmt.Println(name)
|
||||
}
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.Args().Len() != 2 {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("missing <recipe>/<version> arguments?"))
|
||||
}
|
||||
|
||||
recipe := internal.ValidateRecipe(c)
|
||||
|
||||
// TODO: validate with tagcmp when new commits come in
|
||||
// See https://git.coopcloud.tech/coop-cloud/abra/pulls/109
|
||||
nextTag := c.Args().Get(1)
|
||||
|
||||
mainService := "app"
|
||||
var services []string
|
||||
hasAppService := false
|
||||
for _, service := range recipe.Config.Services {
|
||||
services = append(services, service.Name)
|
||||
if service.Name == "app" {
|
||||
hasAppService = true
|
||||
logrus.Debugf("detected app service in '%s'", recipe.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasAppService {
|
||||
logrus.Warnf("no 'app' service defined in '%s'", recipe.Name)
|
||||
var chosenService string
|
||||
prompt := &survey.Select{
|
||||
Message: fmt.Sprintf("what is the main service name for '%s'?", recipe.Name),
|
||||
Options: services,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &chosenService); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
mainService = chosenService
|
||||
}
|
||||
|
||||
logrus.Debugf("selecting '%s' as the service to sync version labels", mainService)
|
||||
|
||||
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag)
|
||||
if err := recipe.UpdateLabel(mainService, label); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Infof("synced label '%s' to service '%s'", label, mainService)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
@ -36,17 +36,22 @@ update the relevant compose file tags on the local file system.
|
||||
Some image tags cannot be parsed because they do not follow some sort of
|
||||
semver-like convention. In this case, all possible tags will be listed and it
|
||||
is up to the end-user to decide.
|
||||
|
||||
You may invoke this command in "wizard" mode and be prompted for input:
|
||||
|
||||
abra recipe upgrade
|
||||
|
||||
`,
|
||||
ArgsUsage: "<recipe>",
|
||||
Flags: []cli.Flag{
|
||||
PatchFlag,
|
||||
MinorFlag,
|
||||
MajorFlag,
|
||||
internal.PatchFlag,
|
||||
internal.MinorFlag,
|
||||
internal.MajorFlag,
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
recipe := internal.ValidateRecipe(c)
|
||||
recipe := internal.ValidateRecipeWithPrompt(c)
|
||||
|
||||
bumpType := btoi(Major)*4 + btoi(Minor)*2 + btoi(Patch)
|
||||
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
|
||||
if bumpType != 0 {
|
||||
// a bitwise check if the number is a power of 2
|
||||
if (bumpType & (bumpType - 1)) != 0 {
|
||||
@ -179,11 +184,11 @@ is up to the end-user to decide.
|
||||
if contains {
|
||||
logrus.Infof("Upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString)
|
||||
} else {
|
||||
logrus.Infof("service %s, image %s pinned to %s. No compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString)
|
||||
logrus.Infof("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
logrus.Fatalf("Service %s is at version %s, but pinned to %s. Please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String())
|
||||
logrus.Fatalf("Service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String())
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
|
@ -1,29 +1,27 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
abraFormatter "coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
contextPkg "coopcloud.tech/abra/pkg/context"
|
||||
"coopcloud.tech/abra/pkg/dns"
|
||||
"coopcloud.tech/abra/pkg/server"
|
||||
"coopcloud.tech/abra/pkg/ssh"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/sfreiberg/simplessh"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@ -166,7 +164,7 @@ func newLocalServer(c *cli.Context, domainName string) error {
|
||||
}
|
||||
|
||||
func newContext(c *cli.Context, domainName, username, port string) error {
|
||||
store := client.NewDefaultDockerContextStore()
|
||||
store := contextPkg.NewDefaultDockerContextStore()
|
||||
contexts, err := store.Store.List()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -196,7 +194,7 @@ func newClient(c *cli.Context, domainName string) (*dockerClient.Client, error)
|
||||
return cl, nil
|
||||
}
|
||||
|
||||
func installDocker(c *cli.Context, cl *dockerClient.Client, sshCl *simplessh.Client, domainName string) error {
|
||||
func installDocker(c *cli.Context, cl *dockerClient.Client, sshCl *ssh.Client, domainName string) error {
|
||||
result, err := sshCl.Exec("which docker")
|
||||
if err != nil && string(result) != "" {
|
||||
return err
|
||||
@ -268,33 +266,11 @@ func initSwarmLocal(c *cli.Context, cl *dockerClient.Client, domainName string)
|
||||
}
|
||||
|
||||
func initSwarm(c *cli.Context, cl *dockerClient.Client, domainName string) error {
|
||||
// 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)
|
||||
|
||||
ips, err := resolver.LookupIPAddr(c.Context, domainName)
|
||||
ipv4, err := dns.EnsureIPv4(domainName)
|
||||
if err != nil {
|
||||
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{
|
||||
ListenAddr: "0.0.0.0:2377",
|
||||
AdvertiseAddr: ipv4,
|
||||
|
@ -4,8 +4,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/context"
|
||||
"github.com/docker/cli/cli/connhelper/ssh"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
@ -18,7 +18,7 @@ var serverListCommand = &cli.Command{
|
||||
ArgsUsage: " ",
|
||||
HideHelp: true,
|
||||
Action: func(c *cli.Context) error {
|
||||
dockerContextStore := client.NewDefaultDockerContextStore()
|
||||
dockerContextStore := context.NewDefaultDockerContextStore()
|
||||
contexts, err := dockerContextStore.Store.List()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
@ -36,7 +36,7 @@ var serverListCommand = &cli.Command{
|
||||
for _, serverName := range serverNames {
|
||||
var row []string
|
||||
for _, ctx := range contexts {
|
||||
endpoint, err := client.GetContextEndpoint(ctx)
|
||||
endpoint, err := context.GetContextEndpoint(ctx)
|
||||
if err != nil && strings.Contains(err.Error(), "does not exist") {
|
||||
// No local context found, we can continue safely
|
||||
continue
|
||||
|
@ -8,12 +8,24 @@ import (
|
||||
"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.
|
||||
var UpgradeCommand = &cli.Command{
|
||||
Name: "upgrade",
|
||||
Usage: "Upgrade abra",
|
||||
Flags: []cli.Flag{RCFlag},
|
||||
Action: func(c *cli.Context) error {
|
||||
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)
|
||||
if err := internal.RunCmd(cmd); err != nil {
|
||||
logrus.Fatal(err)
|
||||
|
19
go.mod
19
go.mod
@ -4,16 +4,16 @@ go 1.16
|
||||
|
||||
require (
|
||||
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/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/docker v20.10.8+incompatible
|
||||
github.com/docker/docker v20.10.11+incompatible
|
||||
github.com/docker/go-units v0.4.0
|
||||
github.com/go-git/go-git/v5 v5.4.2
|
||||
github.com/hetznercloud/hcloud-go v1.32.0
|
||||
github.com/moby/sys/signal v0.5.0
|
||||
github.com/hetznercloud/hcloud-go v1.33.1
|
||||
github.com/moby/sys/signal v0.6.0
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/pkg/errors v0.9.1
|
||||
@ -28,21 +28,20 @@ require (
|
||||
coopcloud.tech/libcapsul v0.0.0-20211022074848-c35e78fe3f3e
|
||||
github.com/Microsoft/hcsshim v0.8.21 // indirect
|
||||
github.com/containerd/containerd v1.5.5 // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
||||
github.com/fvbommel/sortorder v1.0.2 // indirect
|
||||
github.com/gliderlabs/ssh v0.3.3
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351
|
||||
github.com/kevinburke/ssh_config v1.1.0
|
||||
github.com/libdns/gandi v1.0.2
|
||||
github.com/libdns/libdns v0.2.1
|
||||
github.com/moby/sys/mount v0.2.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/runc v1.0.2 // indirect
|
||||
github.com/pkg/sftp v1.13.4 // indirect
|
||||
github.com/sfreiberg/simplessh v0.0.0-20180301191542-495cbb862a9c
|
||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359
|
||||
)
|
||||
|
43
go.sum
43
go.sum
@ -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/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.1 h1:lzkuHA60pER7L4eYL8qQJor4bUWlJe4V0gqAT19tdOA=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.1/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.2 h1:TqTB+aDDCLYhf9/bD2TwSO8u8jDSmMUd2SUVO4gCnU8=
|
||||
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/go.mod h1:oZRCMMRS318l07ei4DTqbZoOawfJlJ4yyo8juk2v4Rk=
|
||||
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-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/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-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/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=
|
||||
@ -253,22 +254,20 @@ github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7h
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
|
||||
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
|
||||
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/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/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.8+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v20.10.11+incompatible h1:tXU1ezXcruZQRrMP8RN2z9N91h+6egZTS1gsPsKantc=
|
||||
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 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/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v20.10.8+incompatible h1:RVqD337BgQicVCzYrrlhLDWhq6OAD2PJDUg2LsEUvKM=
|
||||
github.com/docker/docker v20.10.8+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v20.10.11+incompatible h1:OqzI/g/W54LczvhnccGqniFoQghHx3pklbLuhfXpqGo=
|
||||
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/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
|
||||
@ -316,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/ghodss/yaml v0.0.0-20150909031657-73d445a93680/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.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/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
|
||||
github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
|
||||
@ -450,8 +450,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
||||
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/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hetznercloud/hcloud-go v1.32.0 h1:7zyN2V7hMlhm3HZdxOarmOtvzKvkcYKjM0hcwYMQZz0=
|
||||
github.com/hetznercloud/hcloud-go v1.32.0/go.mod h1:XX/TQub3ge0yWR2yHWmnDVIrB+MQbda1pHxkUmDlUME=
|
||||
github.com/hetznercloud/hcloud-go v1.33.1 h1:W1HdO2bRLTKU4WsyqAasDSpt54fYO4WNckWYfH5AuCQ=
|
||||
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/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
@ -490,8 +490,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/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/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 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.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
@ -501,8 +502,6 @@ github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdY
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
@ -566,8 +565,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.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/signal v0.5.0 h1:MzpEFrMxugDynb1gkTIThU1O3wEmrAkOY+G9dHcHnCc=
|
||||
github.com/moby/sys/signal v0.5.0/go.mod h1:JwObcMnOrUy2VTP5swPKWwywH0Mbgk8Y5qua9iwtIRM=
|
||||
github.com/moby/sys/signal v0.6.0 h1:aDpY94H8VlhTGa9sNYUFCFsMZIUh5wm0B6XkIoJj/iY=
|
||||
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/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=
|
||||
@ -643,8 +642,6 @@ github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.4 h1:Lb0RYJCmgUcBgZosfoi9Y9sbl6+LJgOIgk/2Y4YjMFg=
|
||||
github.com/pkg/sftp v1.13.4/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
@ -699,8 +696,6 @@ github.com/schultz-is/passgen v1.0.1/go.mod h1:NnqzT2aSfvyheNQvBtlLUa0YlPFLDj60J
|
||||
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sfreiberg/simplessh v0.0.0-20180301191542-495cbb862a9c h1:7Q+2oF0uBoLEV+j13E3/xUkPkI7f+sFNPZOPo2jmrWk=
|
||||
github.com/sfreiberg/simplessh v0.0.0-20180301191542-495cbb862a9c/go.mod h1:sB7d6wQapoRM+qx5MgQYB6JVHtel4YHRr0NXXCkXiwQ=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
@ -827,7 +822,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-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-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
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/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@ -974,15 +969,15 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-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-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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
contextPkg "coopcloud.tech/abra/pkg/context"
|
||||
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -21,7 +22,7 @@ func New(contextName string) (*client.Client, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctxEndpoint, err := GetContextEndpoint(context)
|
||||
ctxEndpoint, err := contextPkg.GetContextEndpoint(context)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1,46 +0,0 @@
|
||||
package client_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
)
|
||||
|
||||
// use at the start to ensure testContext[0, 1, ..., amnt-1] exist and
|
||||
// testContextFail[0, 1, ..., failAmnt-1] don't exist
|
||||
func ensureTestState(amnt, failAmnt int) error {
|
||||
for i := 0; i < amnt; i++ {
|
||||
err := client.CreateContext(fmt.Sprintf("testContext%d", i), "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for i := 0; i < failAmnt; i++ {
|
||||
if _, er := client.GetContext(fmt.Sprintf("testContextFail%d", i)); er == nil {
|
||||
err := client.DeleteContext(fmt.Sprintf("testContextFail%d", i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
err := ensureTestState(1, 1)
|
||||
if err != nil {
|
||||
t.Errorf("Couldn't ensure existence/nonexistence of contexts: %s", err)
|
||||
}
|
||||
contextName := "testContext0"
|
||||
_, err = client.New(contextName)
|
||||
if err != nil {
|
||||
t.Errorf("couldn't initialise a new client with context %s: %s", contextName, err)
|
||||
}
|
||||
contextName = "testContextFail0"
|
||||
_, err = client.New(contextName)
|
||||
if err == nil {
|
||||
t.Errorf("client.New(\"testContextFail0\") should have failed but didn't return an error")
|
||||
}
|
||||
|
||||
}
|
@ -4,14 +4,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"coopcloud.tech/abra/pkg/context"
|
||||
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
|
||||
command "github.com/docker/cli/cli/command"
|
||||
dConfig "github.com/docker/cli/cli/config"
|
||||
context "github.com/docker/cli/cli/context"
|
||||
"github.com/docker/cli/cli/context/docker"
|
||||
contextStore "github.com/docker/cli/cli/context/store"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
"github.com/moby/term"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -35,7 +32,7 @@ func CreateContext(contextName string, user string, port string) error {
|
||||
|
||||
// createContext interacts with Docker Context to create a Docker context config
|
||||
func createContext(name string, host string) error {
|
||||
s := NewDefaultDockerContextStore()
|
||||
s := context.NewDefaultDockerContextStore()
|
||||
contextMetadata := contextStore.Metadata{
|
||||
Endpoints: make(map[string]interface{}),
|
||||
Name: name,
|
||||
@ -83,46 +80,14 @@ func DeleteContext(name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return NewDefaultDockerContextStore().Remove(name)
|
||||
return context.NewDefaultDockerContextStore().Remove(name)
|
||||
}
|
||||
|
||||
func GetContext(contextName string) (contextStore.Metadata, error) {
|
||||
ctx, err := NewDefaultDockerContextStore().GetMetadata(contextName)
|
||||
ctx, err := context.NewDefaultDockerContextStore().GetMetadata(contextName)
|
||||
if err != nil {
|
||||
return contextStore.Metadata{}, err
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func GetContextEndpoint(ctx contextStore.Metadata) (string, error) {
|
||||
endpointmeta, ok := ctx.Endpoints["docker"].(context.EndpointMetaBase)
|
||||
if !ok {
|
||||
err := errors.New("context lacks Docker endpoint")
|
||||
return "", err
|
||||
}
|
||||
return endpointmeta.Host, nil
|
||||
}
|
||||
|
||||
func newContextStore(dir string, config contextStore.Config) contextStore.Store {
|
||||
return contextStore.New(dir, config)
|
||||
}
|
||||
|
||||
func NewDefaultDockerContextStore() *command.ContextStoreWithDefault {
|
||||
_, _, stderr := term.StdStreams()
|
||||
dockerConfig := dConfig.LoadDefaultConfigFile(stderr)
|
||||
contextDir := dConfig.ContextStoreDir()
|
||||
storeConfig := command.DefaultContextStoreConfig()
|
||||
store := newContextStore(contextDir, storeConfig)
|
||||
|
||||
opts := &cliflags.CommonOptions{Context: "default"}
|
||||
|
||||
dockerContextStore := &command.ContextStoreWithDefault{
|
||||
Store: store,
|
||||
Resolver: func() (*command.DefaultContext, error) {
|
||||
return command.ResolveDefaultContext(opts, dockerConfig, storeConfig, stderr)
|
||||
},
|
||||
}
|
||||
|
||||
return dockerContextStore
|
||||
}
|
||||
|
@ -1,80 +0,0 @@
|
||||
package client_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
dContext "github.com/docker/cli/cli/context"
|
||||
dCliContextStore "github.com/docker/cli/cli/context/store"
|
||||
)
|
||||
|
||||
type TestContext struct {
|
||||
context dCliContextStore.Metadata
|
||||
expected_endpoint string
|
||||
}
|
||||
|
||||
func dockerContext(host, key string) TestContext {
|
||||
dockerContext := dCliContextStore.Metadata{
|
||||
Name: "foo",
|
||||
Metadata: nil,
|
||||
Endpoints: map[string]interface{}{
|
||||
key: dContext.EndpointMetaBase{
|
||||
Host: host,
|
||||
SkipTLSVerify: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
return TestContext{
|
||||
context: dockerContext,
|
||||
expected_endpoint: host,
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateContext(t *testing.T) {
|
||||
err := client.CreateContext("testContext0", "wronguser", "wrongport")
|
||||
if err == nil {
|
||||
t.Error("client.CreateContext(\"testContextCreate\", \"wronguser\", \"wrongport\") should have failed but didn't return an error")
|
||||
}
|
||||
err = client.CreateContext("testContext0", "", "")
|
||||
if err != nil {
|
||||
t.Errorf("Couldn't create context: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteContext(t *testing.T) {
|
||||
ensureTestState(1, 1)
|
||||
err := client.DeleteContext("default")
|
||||
if err == nil {
|
||||
t.Errorf("client.DeleteContext(\"default\") should have failed but didn't return an error")
|
||||
}
|
||||
|
||||
err = client.DeleteContext("testContext0")
|
||||
if err != nil {
|
||||
t.Errorf("client.DeleteContext(\"testContext0\") failed: %s", err)
|
||||
}
|
||||
err = client.DeleteContext("testContextFail0")
|
||||
if err == nil {
|
||||
t.Errorf("client.DeleteContext(\"testContextFail0\") should have failed (attempt to delete non-existent context) but didn't return an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetContextEndpoint(t *testing.T) {
|
||||
var testDockerContexts = []TestContext{
|
||||
dockerContext("ssh://foobar", "docker"),
|
||||
dockerContext("ssh://foobar", "k8"),
|
||||
}
|
||||
for _, context := range testDockerContexts {
|
||||
endpoint, err := client.GetContextEndpoint(context.context)
|
||||
if err != nil {
|
||||
if err.Error() != "context lacks Docker endpoint" {
|
||||
t.Error(err)
|
||||
}
|
||||
} else {
|
||||
if endpoint != context.expected_endpoint {
|
||||
t.Errorf("did not get correct context endpoint. Expected: %s, received: %s", context.expected_endpoint, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -117,8 +117,11 @@ func UpdateLabel(pattern, serviceName, label, recipeName string) error {
|
||||
continue
|
||||
}
|
||||
|
||||
discovered := false
|
||||
for oldLabel, value := range service.Deploy.Labels {
|
||||
if strings.HasPrefix(oldLabel, "coop-cloud") {
|
||||
discovered = true
|
||||
|
||||
bytes, err := ioutil.ReadFile(composeFile)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -127,13 +130,19 @@ func UpdateLabel(pattern, serviceName, label, recipeName string) error {
|
||||
old := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", value)
|
||||
replacedBytes := strings.Replace(string(bytes), old, label, -1)
|
||||
|
||||
logrus.Debugf("updating '%s' to '%s' in '%s'", old, label, compose.Filename)
|
||||
logrus.Debugf("updating %s to %s in %s", old, label, compose.Filename)
|
||||
|
||||
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !discovered {
|
||||
logrus.Warn("no existing label found, cannot continue...")
|
||||
logrus.Fatalf("add '%s' manually, automagic insertion not supported yet", label)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
44
pkg/context/context.go
Normal file
44
pkg/context/context.go
Normal file
@ -0,0 +1,44 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
dConfig "github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/context"
|
||||
contextStore "github.com/docker/cli/cli/context/store"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
"github.com/moby/term"
|
||||
)
|
||||
|
||||
func NewDefaultDockerContextStore() *command.ContextStoreWithDefault {
|
||||
_, _, stderr := term.StdStreams()
|
||||
dockerConfig := dConfig.LoadDefaultConfigFile(stderr)
|
||||
contextDir := dConfig.ContextStoreDir()
|
||||
storeConfig := command.DefaultContextStoreConfig()
|
||||
store := newContextStore(contextDir, storeConfig)
|
||||
|
||||
opts := &cliflags.CommonOptions{Context: "default"}
|
||||
|
||||
dockerContextStore := &command.ContextStoreWithDefault{
|
||||
Store: store,
|
||||
Resolver: func() (*command.DefaultContext, error) {
|
||||
return command.ResolveDefaultContext(opts, dockerConfig, storeConfig, stderr)
|
||||
},
|
||||
}
|
||||
|
||||
return dockerContextStore
|
||||
}
|
||||
|
||||
func GetContextEndpoint(ctx contextStore.Metadata) (string, error) {
|
||||
endpointmeta, ok := ctx.Endpoints["docker"].(context.EndpointMetaBase)
|
||||
if !ok {
|
||||
err := errors.New("context lacks Docker endpoint")
|
||||
return "", err
|
||||
}
|
||||
return endpointmeta.Host, nil
|
||||
}
|
||||
|
||||
func newContextStore(dir string, config contextStore.Config) contextStore.Store {
|
||||
return contextStore.New(dir, config)
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -26,3 +29,68 @@ func NewToken(provider, providerTokenEnvVar string) (string, error) {
|
||||
|
||||
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
|
||||
}
|
||||
|
14
pkg/git/common.go
Normal file
14
pkg/git/common.go
Normal 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
38
pkg/git/init.go
Normal 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
|
||||
}
|
@ -2,6 +2,7 @@ package recipe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@ -95,13 +96,22 @@ func Get(recipeName string) (Recipe, error) {
|
||||
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 {
|
||||
recipeDir := path.Join(config.ABRA_DIR, "apps", strings.ToLower(recipe))
|
||||
|
||||
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 nil
|
||||
}
|
||||
|
||||
@ -118,6 +128,10 @@ func EnsureVersion(recipeName, version string) error {
|
||||
return fmt.Errorf("'%s' has locally unstaged changes", recipeName)
|
||||
}
|
||||
|
||||
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repo, err := git.PlainOpen(recipeDir)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -178,6 +192,10 @@ func EnsureLatest(recipeName string) error {
|
||||
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)
|
||||
|
||||
repo, err := git.PlainOpen(recipeDir)
|
||||
|
486
pkg/ssh/ssh.go
486
pkg/ssh/ssh.go
@ -3,18 +3,35 @@ package ssh
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"coopcloud.tech/abra/pkg/context"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
dockerSSHPkg "github.com/docker/cli/cli/connhelper/ssh"
|
||||
sshPkg "github.com/gliderlabs/ssh"
|
||||
"github.com/kevinburke/ssh_config"
|
||||
"github.com/sfreiberg/simplessh"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/agent"
|
||||
"golang.org/x/crypto/ssh/knownhosts"
|
||||
)
|
||||
|
||||
var KnownHostsPath = filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts")
|
||||
|
||||
type Client struct {
|
||||
SSHClient *ssh.Client
|
||||
}
|
||||
|
||||
// HostConfig is a SSH host config.
|
||||
type HostConfig struct {
|
||||
Host string
|
||||
@ -23,61 +40,39 @@ type HostConfig struct {
|
||||
User string
|
||||
}
|
||||
|
||||
// GetHostConfig retrieves a ~/.ssh/config config for a host.
|
||||
func GetHostConfig(hostname, username, port string) (HostConfig, error) {
|
||||
var hostConfig HostConfig
|
||||
|
||||
var host, idf string
|
||||
|
||||
if host = ssh_config.Get(hostname, "Hostname"); host == "" {
|
||||
logrus.Debugf("no hostname found in SSH config, assuming %s", hostname)
|
||||
host = hostname
|
||||
}
|
||||
|
||||
if username == "" {
|
||||
if username = ssh_config.Get(hostname, "User"); username == "" {
|
||||
systemUser, err := user.Current()
|
||||
// Exec cmd on the remote host and return stderr and stdout
|
||||
func (c *Client) Exec(cmd string) ([]byte, error) {
|
||||
session, err := c.SSHClient.NewSession()
|
||||
if err != nil {
|
||||
return hostConfig, err
|
||||
}
|
||||
logrus.Debugf("no username found in SSH config or passed on command-line, assuming %s", username)
|
||||
username = systemUser.Username
|
||||
return nil, err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
return session.CombinedOutput(cmd)
|
||||
}
|
||||
|
||||
if port == "" {
|
||||
if port = ssh_config.Get(hostname, "Port"); port == "" {
|
||||
logrus.Debugf("no port found in SSH config or passed on command-line, assuming 22")
|
||||
port = "22"
|
||||
}
|
||||
}
|
||||
|
||||
idf = ssh_config.Get(hostname, "IdentityFile")
|
||||
|
||||
hostConfig.Host = host
|
||||
if idf != "" {
|
||||
hostConfig.IdentityFile = idf
|
||||
}
|
||||
hostConfig.Port = port
|
||||
hostConfig.User = username
|
||||
|
||||
logrus.Debugf("constructed SSH config %s for %s", hostConfig, hostname)
|
||||
|
||||
return hostConfig, nil
|
||||
// Close the underlying SSH connection
|
||||
func (c *Client) Close() error {
|
||||
return c.SSHClient.Close()
|
||||
}
|
||||
|
||||
// New creates a new SSH client connection.
|
||||
func New(domainName, sshAuth, username, port string) (*simplessh.Client, error) {
|
||||
var client *simplessh.Client
|
||||
func New(domainName, sshAuth, username, port string) (*Client, error) {
|
||||
var client *Client
|
||||
|
||||
hostConfig, err := GetHostConfig(domainName, username, port)
|
||||
ctxConnDetails, err := GetContextConnDetails(domainName)
|
||||
if err != nil {
|
||||
return client, err
|
||||
return client, nil
|
||||
}
|
||||
|
||||
if sshAuth == "identity-file" {
|
||||
var err error
|
||||
client, err = simplessh.ConnectWithAgentTimeout(hostConfig.Host, hostConfig.User, 5*time.Second)
|
||||
client, err = connectWithAgentTimeout(
|
||||
ctxConnDetails.Host,
|
||||
ctxConnDetails.User,
|
||||
ctxConnDetails.Port,
|
||||
5*time.Second,
|
||||
)
|
||||
if err != nil {
|
||||
return client, err
|
||||
}
|
||||
@ -91,7 +86,13 @@ func New(domainName, sshAuth, username, port string) (*simplessh.Client, error)
|
||||
}
|
||||
|
||||
var err error
|
||||
client, err = simplessh.ConnectWithPasswordTimeout(hostConfig.Host, hostConfig.User, password, 5*time.Second)
|
||||
client, err = connectWithPasswordTimeout(
|
||||
ctxConnDetails.Host,
|
||||
ctxConnDetails.User,
|
||||
ctxConnDetails.Port,
|
||||
password,
|
||||
5*time.Second,
|
||||
)
|
||||
if err != nil {
|
||||
return client, err
|
||||
}
|
||||
@ -100,8 +101,7 @@ func New(domainName, sshAuth, username, port string) (*simplessh.Client, error)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// sudoWriter supports sudo command handling.
|
||||
// https://github.com/sfreiberg/simplessh/blob/master/simplessh.go
|
||||
// sudoWriter supports sudo command handling
|
||||
type sudoWriter struct {
|
||||
b bytes.Buffer
|
||||
pw string
|
||||
@ -109,8 +109,7 @@ type sudoWriter struct {
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
// Write satisfies the write interface for sudoWriter.
|
||||
// https://github.com/sfreiberg/simplessh/blob/master/simplessh.go
|
||||
// Write satisfies the write interface for sudoWriter
|
||||
func (w *sudoWriter) Write(p []byte) (int, error) {
|
||||
if string(p) == "sudo_password" {
|
||||
w.stdin.Write([]byte(w.pw + "\n"))
|
||||
@ -124,9 +123,8 @@ func (w *sudoWriter) Write(p []byte) (int, error) {
|
||||
return w.b.Write(p)
|
||||
}
|
||||
|
||||
// RunSudoCmd runs SSH commands and streams output.
|
||||
// https://github.com/sfreiberg/simplessh/blob/master/simplessh.go
|
||||
func RunSudoCmd(cmd, passwd string, cl *simplessh.Client) error {
|
||||
// RunSudoCmd runs SSH commands and streams output
|
||||
func RunSudoCmd(cmd, passwd string, cl *Client) error {
|
||||
session, err := cl.SSHClient.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -170,9 +168,8 @@ func RunSudoCmd(cmd, passwd string, cl *simplessh.Client) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Exec runs a command on a remote and streams output.
|
||||
// https://github.com/sfreiberg/simplessh/blob/master/simplessh.go
|
||||
func Exec(cmd string, cl *simplessh.Client) error {
|
||||
// Exec runs a command on a remote and streams output
|
||||
func Exec(cmd string, cl *Client) error {
|
||||
session, err := cl.SSHClient.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -224,3 +221,384 @@ func Exec(cmd string, cl *simplessh.Client) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureKnowHostsFiles ensures that ~/.ssh/known_hosts is created
|
||||
func EnsureKnowHostsFiles() error {
|
||||
if _, err := os.Stat(KnownHostsPath); os.IsNotExist(err) {
|
||||
logrus.Debugf("missing %s, creating now", KnownHostsPath)
|
||||
file, err := os.OpenFile(KnownHostsPath, os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetHostKey checks if a host key is registered in the ~/.ssh/known_hosts file
|
||||
func GetHostKey(hostname string) (bool, sshPkg.PublicKey, error) {
|
||||
var hostKey sshPkg.PublicKey
|
||||
|
||||
ctxConnDetails, err := GetContextConnDetails(hostname)
|
||||
if err != nil {
|
||||
return false, hostKey, err
|
||||
}
|
||||
|
||||
if err := EnsureKnowHostsFiles(); err != nil {
|
||||
return false, hostKey, err
|
||||
}
|
||||
|
||||
file, err := os.Open(KnownHostsPath)
|
||||
if err != nil {
|
||||
return false, hostKey, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
fields := strings.Split(scanner.Text(), " ")
|
||||
if len(fields) != 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
hostnameAndPort := fmt.Sprintf("%s:%s", ctxConnDetails.Host, ctxConnDetails.Port)
|
||||
hashed := knownhosts.Normalize(hostnameAndPort)
|
||||
|
||||
if strings.Contains(fields[0], hashed) {
|
||||
var err error
|
||||
hostKey, _, _, _, err = ssh.ParseAuthorizedKey(scanner.Bytes())
|
||||
if err != nil {
|
||||
return false, hostKey, fmt.Errorf("error parsing server SSH host key %q: %v", fields[2], err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hostKey != nil {
|
||||
logrus.Debugf("server SSH host key present in ~/.ssh/known_hosts for %s", hostname)
|
||||
return true, hostKey, nil
|
||||
}
|
||||
|
||||
return false, hostKey, nil
|
||||
}
|
||||
|
||||
// InsertHostKey adds a new host key to the ~/.ssh/known_hosts file
|
||||
func InsertHostKey(hostname string, remote net.Addr, pubKey ssh.PublicKey) error {
|
||||
file, err := os.OpenFile(KnownHostsPath, os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hashedHostname := knownhosts.Normalize(hostname)
|
||||
lineHostname := knownhosts.Line([]string{hashedHostname}, pubKey)
|
||||
_, err = file.WriteString(fmt.Sprintf("%s\n", lineHostname))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hashedRemote := knownhosts.Normalize(remote.String())
|
||||
lineRemote := knownhosts.Line([]string{hashedRemote}, pubKey)
|
||||
_, err = file.WriteString(fmt.Sprintf("%s\n", lineRemote))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debugf("SSH host key generated: %s", lineHostname)
|
||||
logrus.Debugf("SSH host key generated: %s", lineRemote)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HostKeyAddCallback ensures server ssh host keys are handled
|
||||
func HostKeyAddCallback(hostnameAndPort string, remote net.Addr, pubKey ssh.PublicKey) error {
|
||||
exists, _, err := GetHostKey(hostnameAndPort)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exists {
|
||||
hostname := strings.Split(hostnameAndPort, ":")[0]
|
||||
logrus.Debugf("server SSH host key found for %s, moving on", hostname)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !exists {
|
||||
hostname := strings.Split(hostnameAndPort, ":")[0]
|
||||
parsedPubKey := FingerprintSHA256(pubKey)
|
||||
|
||||
fmt.Printf(fmt.Sprintf(`
|
||||
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
|
||||
you want to connect to. Please take a moment to validate the following SSH host
|
||||
key, it is important.
|
||||
|
||||
Host: %s
|
||||
Fingerprint: %s
|
||||
|
||||
If this is confusing to you, you can read the article below and learn how to
|
||||
validate this fingerprint safely. Thanks to the comrades at cyberia.club for
|
||||
writing this extensive guide <3
|
||||
|
||||
https://sequentialread.com/understanding-the-secure-shell-protocol-ssh/
|
||||
|
||||
`, hostname, parsedPubKey))
|
||||
|
||||
response := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: "are you sure you trust this host key?",
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !response {
|
||||
logrus.Fatal("exiting as requested")
|
||||
}
|
||||
|
||||
logrus.Debugf("attempting to insert server SSH host key for %s, %s", hostnameAndPort, remote)
|
||||
|
||||
if err := InsertHostKey(hostnameAndPort, remote, pubKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("successfully added server SSH host key for %s", hostname)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// connect makes the SSH connection
|
||||
func connect(username, host, port string, authMethod ssh.AuthMethod, timeout time.Duration) (*Client, error) {
|
||||
config := &ssh.ClientConfig{
|
||||
User: username,
|
||||
Auth: []ssh.AuthMethod{authMethod},
|
||||
HostKeyCallback: HostKeyAddCallback, // the main reason why we fork
|
||||
}
|
||||
|
||||
hostnameAndPort := fmt.Sprintf("%s:%s", host, port)
|
||||
|
||||
logrus.Debugf("tcp dialing %s", hostnameAndPort)
|
||||
|
||||
var conn net.Conn
|
||||
var err error
|
||||
conn, err = net.DialTimeout("tcp", hostnameAndPort, timeout)
|
||||
if err != nil {
|
||||
logrus.Debugf("tcp dialing %s failed, trying via ~/.ssh/config", hostnameAndPort)
|
||||
hostConfig, err := GetHostConfig(host, username, port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err = net.DialTimeout("tcp", fmt.Sprintf("%s:%s", hostConfig.Host, hostConfig.Port), timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
sshConn, chans, reqs, err := ssh.NewClientConn(conn, hostnameAndPort, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := ssh.NewClient(sshConn, chans, reqs)
|
||||
c := &Client{SSHClient: client}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
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"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func connectWithPasswordTimeout(host, username, port, pass string, timeout time.Duration) (*Client, error) {
|
||||
authMethod := ssh.Password(pass)
|
||||
|
||||
return connect(username, host, port, authMethod, timeout)
|
||||
}
|
||||
|
||||
// EnsureHostKey ensures that a host key trusted and added to the ~/.ssh/known_hosts file
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctxConnDetails, err := GetContextConnDetails(hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = connectWithAgentTimeout(
|
||||
ctxConnDetails.Host,
|
||||
ctxConnDetails.User,
|
||||
ctxConnDetails.Port,
|
||||
5*time.Second,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FingerprintSHA256 generates the SHA256 fingerprint for a server SSH host key
|
||||
func FingerprintSHA256(key ssh.PublicKey) string {
|
||||
hash := sha256.Sum256(key.Marshal())
|
||||
b64hash := base64.StdEncoding.EncodeToString(hash[:])
|
||||
trimmed := strings.TrimRight(b64hash, "=")
|
||||
return fmt.Sprintf("SHA256:%s", trimmed)
|
||||
}
|
||||
|
||||
// GetContextConnDetails retrieves SSH connection details from a docker context endpoint
|
||||
func GetContextConnDetails(serverName string) (*dockerSSHPkg.Spec, error) {
|
||||
dockerContextStore := context.NewDefaultDockerContextStore()
|
||||
contexts, err := dockerContextStore.Store.List()
|
||||
if err != nil {
|
||||
return &dockerSSHPkg.Spec{}, err
|
||||
}
|
||||
|
||||
if strings.Contains(serverName, ":") {
|
||||
serverName = strings.Split(serverName, ":")[0]
|
||||
}
|
||||
|
||||
for _, ctx := range contexts {
|
||||
endpoint, err := context.GetContextEndpoint(ctx)
|
||||
if err != nil && strings.Contains(err.Error(), "does not exist") {
|
||||
// No local context found, we can continue safely
|
||||
continue
|
||||
}
|
||||
if ctx.Name == serverName {
|
||||
ctxConnDetails, err := dockerSSHPkg.ParseURL(endpoint)
|
||||
if err != nil {
|
||||
return &dockerSSHPkg.Spec{}, err
|
||||
}
|
||||
logrus.Debugf("found context connection details %v for %s", ctxConnDetails, serverName)
|
||||
return ctxConnDetails, nil
|
||||
}
|
||||
}
|
||||
|
||||
hostConfig, err := GetHostConfig(serverName, "", "")
|
||||
if err != nil {
|
||||
return &dockerSSHPkg.Spec{}, err
|
||||
}
|
||||
|
||||
logrus.Debugf("couldn't find a docker context matching %s", serverName)
|
||||
logrus.Debugf("searching ~/.ssh/config for a Host entry for %s", serverName)
|
||||
|
||||
connDetails := &dockerSSHPkg.Spec{
|
||||
Host: hostConfig.Host,
|
||||
User: hostConfig.User,
|
||||
Port: hostConfig.Port,
|
||||
}
|
||||
|
||||
logrus.Debugf("using %v from ~/.ssh/config for connection details", connDetails)
|
||||
|
||||
return connDetails, nil
|
||||
}
|
||||
|
||||
// GetHostConfig retrieves a ~/.ssh/config config for a host.
|
||||
func GetHostConfig(hostname, username, port string) (HostConfig, error) {
|
||||
var hostConfig HostConfig
|
||||
|
||||
var host, idf string
|
||||
|
||||
if host = ssh_config.Get(hostname, "Hostname"); host == "" {
|
||||
logrus.Debugf("no hostname found in SSH config, assuming %s", hostname)
|
||||
host = hostname
|
||||
}
|
||||
|
||||
if username == "" {
|
||||
if username = ssh_config.Get(hostname, "User"); username == "" {
|
||||
systemUser, err := user.Current()
|
||||
if err != nil {
|
||||
return hostConfig, err
|
||||
}
|
||||
logrus.Debugf("no username found in SSH config or passed on command-line, assuming %s", username)
|
||||
username = systemUser.Username
|
||||
}
|
||||
}
|
||||
|
||||
if port == "" {
|
||||
if port = ssh_config.Get(hostname, "Port"); port == "" {
|
||||
logrus.Debugf("no port found in SSH config or passed on command-line, assuming 22")
|
||||
port = "22"
|
||||
}
|
||||
}
|
||||
|
||||
idf = ssh_config.Get(hostname, "IdentityFile")
|
||||
if idf != "" {
|
||||
var err error
|
||||
idf, err = identityFileAbsPath(idf)
|
||||
if err != nil {
|
||||
return hostConfig, err
|
||||
}
|
||||
hostConfig.IdentityFile = idf
|
||||
}
|
||||
|
||||
hostConfig.Host = host
|
||||
hostConfig.Port = port
|
||||
hostConfig.User = username
|
||||
|
||||
logrus.Debugf("constructed SSH config %s for %s", hostConfig, hostname)
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -2,9 +2,11 @@ package commandconn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
|
||||
sshPkg "coopcloud.tech/abra/pkg/ssh"
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/docker/cli/cli/connhelper/ssh"
|
||||
"github.com/docker/cli/cli/context/docker"
|
||||
@ -23,19 +25,38 @@ func GetConnectionHelper(daemonURL string) (*connhelper.ConnectionHelper, error)
|
||||
}
|
||||
|
||||
func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.ConnectionHelper, error) {
|
||||
u, err := url.Parse(daemonURL)
|
||||
url, err := url.Parse(daemonURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch scheme := u.Scheme; scheme {
|
||||
switch scheme := url.Scheme; scheme {
|
||||
case "ssh":
|
||||
sp, err := ssh.ParseURL(daemonURL)
|
||||
ctxConnDetails, err := ssh.ParseURL(daemonURL)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "ssh host connection is not valid")
|
||||
}
|
||||
|
||||
if err := sshPkg.EnsureHostKey(ctxConnDetails.Host); err != nil {
|
||||
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{
|
||||
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return New(ctx, "ssh", append(sshFlags, sp.Args("docker", "system", "dial-stdio")...)...)
|
||||
return New(ctx, "ssh", append(sshFlags, ctxConnDetails.Args("docker", "system", "dial-stdio")...)...)
|
||||
},
|
||||
Host: "http://docker.example.com",
|
||||
}, nil
|
||||
|
@ -3,10 +3,14 @@ package stack // https://github.com/docker/cli/blob/master/cli/command/stack/swa
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
abraClient "coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||
"github.com/docker/cli/cli/command/service/progress"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
@ -346,6 +350,7 @@ func deployServices(
|
||||
existingServiceMap[service.Spec.Name] = service
|
||||
}
|
||||
|
||||
var serviceIDs []string
|
||||
for internalName, serviceSpec := range services {
|
||||
var (
|
||||
name = namespace.Scope(internalName)
|
||||
@ -405,6 +410,8 @@ func deployServices(
|
||||
return errors.Wrapf(err, "failed to update service %s", name)
|
||||
}
|
||||
|
||||
serviceIDs = append(serviceIDs, service.ID)
|
||||
|
||||
for _, warning := range response.Warnings {
|
||||
logrus.Warn(warning)
|
||||
}
|
||||
@ -418,11 +425,35 @@ func deployServices(
|
||||
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)
|
||||
}
|
||||
|
||||
serviceIDs = append(serviceIDs, serviceCreateResponse.ID)
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Infof("waiting for services to converge: %s", strings.Join(serviceIDs, ", "))
|
||||
|
||||
ch := make(chan error, len(serviceIDs))
|
||||
for _, serviceID := range serviceIDs {
|
||||
logrus.Debugf("waiting on %s to converge", serviceID)
|
||||
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
|
||||
}
|
||||
|
||||
@ -437,3 +468,25 @@ func getStackSecrets(ctx context.Context, dockerclient client.APIClient, namespa
|
||||
func getStackConfigs(ctx context.Context, dockerclient client.APIClient, namespace string) ([]swarm.Config, error) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
3
renovate.json
Normal file
3
renovate.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||
}
|
@ -2,6 +2,15 @@
|
||||
|
||||
ABRA_VERSION="0.3.0-alpha"
|
||||
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 {
|
||||
echo ""
|
||||
@ -35,6 +44,7 @@ function install_abra_release {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# FIXME: support different architectures
|
||||
PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m)
|
||||
FILENAME="abra_"$ABRA_VERSION"_"$PLATFORM""
|
||||
@ -79,6 +89,7 @@ function install_abra_release {
|
||||
echo "abra installed to $HOME/.local/bin/abra"
|
||||
}
|
||||
|
||||
|
||||
function run_installation {
|
||||
show_banner
|
||||
install_abra_release
|
||||
|
Reference in New Issue
Block a user