Compare commits
123 Commits
0.2.0-alph
...
0.3.0-alph
Author | SHA1 | Date | |
---|---|---|---|
de7054fd74 | |||
0e0e2db755 | |||
04e24022f5 | |||
c227972c12 | |||
911f22233f | |||
7d8e2d9dd1 | |||
f041083604 | |||
f57ae1e904 | |||
49a87cae2e | |||
f0de18a7f0 | |||
1caef09cd2 | |||
e4e606efb0 | |||
08aca28d9d | |||
f02ea7ca0d | |||
3d3c4b3aae | |||
e37b49201f | |||
ede5a59562 | |||
fc2deda1f6 | |||
c76601c9ce | |||
7f176d8e2f | |||
9b704b002b | |||
ab02c5f0dd | |||
f2b02e39a7 | |||
31f6bd06a5 | |||
bd92c52eed | |||
0486091768 | |||
3b77607f36 | |||
f833ccb864 | |||
7022f42711 | |||
c76bd25c1d | |||
a6b5ac3410 | |||
71225d2099
|
|||
5d59d12d75
|
|||
d56400eea8 | |||
b3496ad286 | |||
066b2b9373 | |||
aec11bda28 | |||
9a513a0700 | |||
9f3ab0de9e | |||
e26afb97af | |||
960e47437c | |||
8e3f90a7f3 | |||
1d7cb0d9b6 | |||
4d2a2d42fb | |||
bdae61ed51 | |||
766e3008f6 | |||
383f857f4a
|
|||
3d46ce6db2 | |||
9e0d77d5c6 | |||
f9e2d24550 | |||
8772217f41 | |||
a7970132c2 | |||
2d091a6b00 | |||
147687d7ce | |||
9a0e12258a | |||
1396f15c78 | |||
2e2560dea7 | |||
c789a70653 | |||
8f55330210 | |||
d54a45bef7 | |||
fdc0246f1d | |||
a394618965 | |||
8cd9f2700f | |||
b72fa28ddb | |||
313e3beb1e | |||
94c7f59113 | |||
5ae06bbd42 | |||
9f9248b987 | |||
2bb4a9c063 | |||
0c8dba0681 | |||
a491332c1c | |||
6a75ffc051 | |||
5261d1a033 | |||
a458a5d9f7 | |||
5ce2419354 | |||
963f8dcc73 | |||
dc04cf5ff7 | |||
80921c9f55 | |||
8b15f2de5b | |||
cdb76e7276 | |||
a170e26e27 | |||
03b1882b81 | |||
2fcdaca75f | |||
c5f44cf340 | |||
7a5ad65178 | |||
6d4ee3de0d | |||
63318fb6ff | |||
07ffa08a07 | |||
0e5e7490b3 | |||
640032b8fe | |||
39babea963 | |||
07613f5163 | |||
7f1d9eeaec | |||
02d24104e1 | |||
da8d72620a
|
|||
96ccadc70f
|
|||
8703370785 | |||
7d8c53299d | |||
0110aceb1f | |||
aec1e4520d | |||
74bcb99c70 | |||
dd4f2b48ec | |||
7f3f41ede4 | |||
597b4b586e | |||
7ea3df45d4 | |||
5941ed9728 | |||
d1e42752e2 | |||
9dfbd21c61 | |||
9526d1fde6 | |||
62cc7ef92d | |||
c5a7a831d2 | |||
4aae186f5f | |||
2f9b11f389 | |||
6d42e72f16 | |||
5be190e110 | |||
c1390f232e | |||
95e19f03c4 | |||
dc040a0b38
|
|||
e6e2e5214f
|
|||
61452b5f32
|
|||
78460ac0ba
|
|||
0615c3f745 | |||
e820e0219d |
@ -1,40 +0,0 @@
|
||||
{{ range .Versions }}
|
||||
<a name="{{ .Tag.Name }}"></a>
|
||||
## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }}
|
||||
|
||||
> {{ datetime "2006-01-02" .Tag.Date }}
|
||||
|
||||
{{ range .CommitGroups -}}
|
||||
### {{ .Title }}
|
||||
|
||||
{{ range .Commits -}}
|
||||
* {{ .Subject }}
|
||||
{{ end }}
|
||||
{{ end -}}
|
||||
|
||||
{{- if .RevertCommits -}}
|
||||
### Reverts
|
||||
|
||||
{{ range .RevertCommits -}}
|
||||
* {{ .Revert.Header }}
|
||||
{{ end }}
|
||||
{{ end -}}
|
||||
|
||||
{{- if .MergeCommits -}}
|
||||
### Pull Requests
|
||||
|
||||
{{ range .MergeCommits -}}
|
||||
* {{ .Header }}
|
||||
{{ end }}
|
||||
{{ end -}}
|
||||
|
||||
{{- if .NoteGroups -}}
|
||||
{{ range .NoteGroups -}}
|
||||
### {{ .Title }}
|
||||
|
||||
{{ range .Notes }}
|
||||
{{ .Body }}
|
||||
{{ end }}
|
||||
{{ end -}}
|
||||
{{ end -}}
|
||||
{{ end -}}
|
@ -1,27 +0,0 @@
|
||||
style: github
|
||||
template: CHANGELOG.tpl.md
|
||||
info:
|
||||
title: CHANGELOG
|
||||
repository_url: https://git.autonomic.zone:2222/coop-cloud/go-abra
|
||||
options:
|
||||
commits:
|
||||
# filters:
|
||||
# Type:
|
||||
# - feat
|
||||
# - fix
|
||||
# - perf
|
||||
# - refactor
|
||||
commit_groups:
|
||||
# title_maps:
|
||||
# feat: Features
|
||||
# fix: Bug Fixes
|
||||
# perf: Performance Improvements
|
||||
# refactor: Code Refactoring
|
||||
header:
|
||||
pattern: "^(\\w*)\\:\\s(.*)$"
|
||||
pattern_maps:
|
||||
- Type
|
||||
- Subject
|
||||
notes:
|
||||
keywords:
|
||||
- BREAKING CHANGE
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ abra
|
||||
vendor/
|
||||
.envrc
|
||||
dist/
|
||||
*fmtcoverage.html
|
||||
|
@ -32,9 +32,6 @@ changelog:
|
||||
filters:
|
||||
exclude:
|
||||
- "^WIP:"
|
||||
- "^chore:"
|
||||
- "^docs:"
|
||||
- "^refactor:"
|
||||
- "^style:"
|
||||
- "^test:"
|
||||
- "^tests:"
|
||||
|
7
Makefile
7
Makefile
@ -36,3 +36,10 @@ test:
|
||||
|
||||
loc:
|
||||
@find . -name "*.go" | xargs wc -l
|
||||
|
||||
loc-author:
|
||||
@git ls-files -z | \
|
||||
xargs -0rn 1 -P "$$(nproc)" -I{} sh -c 'git blame -w -M -C -C --line-porcelain -- {} | grep -I --line-buffered "^author "' | \
|
||||
sort -f | \
|
||||
uniq -ic | \
|
||||
sort -n
|
||||
|
79
README.md
79
README.md
@ -7,92 +7,45 @@
|
||||
|
||||
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 applications, deploy them, run backup and restore operations and a whole lot of other things. It is the go-to tool for day-to-day operations when managing a Co-op Cloud instance.
|
||||
|
||||
## Install
|
||||
|
||||
### Arch-based Linux Distros
|
||||
|
||||
[abra (coming-soon)](https://aur.archlinux.org/packages/abra/) or for the latest version on git [abra-git](https://aur.archlinux.org/packages/abra-git/)
|
||||
|
||||
```sh
|
||||
yay -S abra-git # or abra
|
||||
```
|
||||
|
||||
### Debian-based Linux Distros
|
||||
|
||||
**Coming Soon**
|
||||
|
||||
### Homebrew
|
||||
|
||||
**Coming Soon**
|
||||
|
||||
### Build from source
|
||||
|
||||
```sh
|
||||
git clone https://git.coopcloud.tech/coop-cloud/abra
|
||||
cd abra
|
||||
go env -w GOPRIVATE=coopcloud.tech
|
||||
make install
|
||||
```
|
||||
|
||||
The abra binary will be in `$GOPATH/bin`.
|
||||
|
||||
## Autocompletion
|
||||
|
||||
**bash**
|
||||
|
||||
Copy `scripts/autocomplete/bash` into `/etc/bash_completion.d/` and rename
|
||||
it to abra.
|
||||
|
||||
```
|
||||
sudo cp scripts/autocomplete/bash /etc/bash_completion.d/abra
|
||||
source /etc/bash_completion.d/abra
|
||||
```
|
||||
|
||||
**(fi)zsh**
|
||||
|
||||
(fi)zsh doesn't have an autocompletion folder by default but you can create one, then copy `scripts/autocomplete/zsh` into it and add a couple lines to your `~/.zshrc` or `~/.fizsh/.fizshrc`
|
||||
|
||||
```
|
||||
sudo mkdir /etc/zsh/completion.d/
|
||||
sudo cp scripts/autocomplete/zsh /etc/zsh/completion.d/abra
|
||||
echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc
|
||||
```
|
||||
|
||||
(replace .zshrc with ~/.fizsh/.fizshrc if you use fizsh)
|
||||
`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.
|
||||
|
||||
## Hacking
|
||||
|
||||
Install direnv, run `cp .envrc.sample .envrc`, then run `direnv allow` in this directory. This will set coopcloud repos as private due to [this bug.](https://git.coopcloud.tech/coop-cloud/coopcloud.tech/issues/20#issuecomment-8201). Or you can run `go env -w GOPRIVATE=coopcloud.tech` but I'm not sure how persistent this is.
|
||||
### Getting started
|
||||
|
||||
Install [direnv](https://direnv.net), run `cp .envrc.sample .envrc`, then run `direnv allow` in this directory. This will set coopcloud repos as private due to [this bug.](https://git.coopcloud.tech/coop-cloud/coopcloud.tech/issues/20#issuecomment-8201). Or you can run `go env -w GOPRIVATE=coopcloud.tech` but I'm not sure how persistent this is.
|
||||
|
||||
Install [Go >= 1.16](https://golang.org/doc/install) and then:
|
||||
|
||||
- `make build` to build
|
||||
- `./abra` to run commands
|
||||
- `make test` will run tests
|
||||
- `make install` will install it to `$GOPATH/bin`
|
||||
- `go get <package>` and `go mod tidy` to add a new dependency
|
||||
|
||||
Our [Drone CI configuration](.drone.yml) runs a number of sanity on each pushed commit. See the [Makefile](./Makefile) for more handy targets.
|
||||
|
||||
Please use the [conventional commit format](https://www.conventionalcommits.org/en/v1.0.0/) for your commits so we can automate our change log.
|
||||
|
||||
## Versioning
|
||||
### Versioning
|
||||
|
||||
We use [goreleaser](https://goreleaser.com) to help us automate releases. We use [semver](https://semver.org) for versioning all releases of the tool. While we are still in the public alpha release phase, we will maintain a `0.y.z-alpha` format. Change logs are generated from our commit logs. We are still working this out and aim to refine our release praxis as we go.
|
||||
|
||||
For developers, while using this `-alpha` format, the `y` part is the "major" version part. So, if you make breaking changes, you increment that and _not_ the `x` part. So, if you're on `0.1.0-alpha`, then you'd go to `0.1.1-alpha` for a backwards compatible change and `0.2.0-alpha` for a backwards incompatible change.
|
||||
|
||||
## Making a new release
|
||||
### Making a new release
|
||||
|
||||
- Change `ABRA_VERSION` to match the new tag in [`scripts`](./scripts/installer/installer) (use [semver](https://semver.org))
|
||||
- Commit that change (e.g. `git commit -m 'chore: publish next tag 0.3.1-alpha'`)
|
||||
- Make a new tag (e.g. `git tag 0.y.z-alpha`)
|
||||
- Commit that change (e.g. `git commit -m 'chore: publish next tag x.y.z-alpha'`)
|
||||
- Make a new tag (e.g. `git tag -a x.y.z-alpha`)
|
||||
- Push the new tag (e.g. `git push && git push --tags`)
|
||||
- Wait until the build finishes on [build.coopcloud.tech](https://build.coopcloud.tech/coop-cloud/abra)
|
||||
- Deploy the new installer script (e.g. `cd ./scripts/installer && make`)
|
||||
- Check the release worked, (e.g. `abra upgrade; abra version`)
|
||||
- Check the release worked, (e.g. `abra upgrade; abra -v`)
|
||||
|
||||
## Fork maintenance
|
||||
### Fork maintenance
|
||||
|
||||
#### `godotenv`
|
||||
|
||||
We maintain a fork of [godotenv](https://github.com/Autonomic-Cooperative/godotenv) for two features:
|
||||
|
||||
@ -102,3 +55,7 @@ We maintain a fork of [godotenv](https://github.com/Autonomic-Cooperative/godote
|
||||
You can upgrade the version here by running `go get github.com/Autonomic-Cooperative/godotenv@<commit>` where `<commit>` is the
|
||||
latest commit you want to pin to. We are aiming to migrate to YAML format for the environment configuration, so this should only
|
||||
be a temporary thing.
|
||||
|
||||
#### `docker/client`
|
||||
|
||||
A number of modules in [pkg/upstream](./pkg/upstream) are copy/pasta'd from the upstream [docker/docker/client](https://pkg.go.dev/github.com/docker/docker/client). We had to do this because upstream are not exposing their API as public.
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
// AppCommand defines the `abra app` command and ets subcommands
|
||||
var AppCommand = &cli.Command{
|
||||
Name: "app",
|
||||
Usage: "Manage apps",
|
||||
Usage: "Manage deployed apps",
|
||||
Aliases: []string{"a"},
|
||||
ArgsUsage: "<app>",
|
||||
Description: `
|
||||
|
@ -1,6 +1,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@ -17,7 +18,21 @@ var appConfigCommand = &cli.Command{
|
||||
Aliases: []string{"c"},
|
||||
Usage: "Edit app config",
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
appName := c.Args().First()
|
||||
|
||||
if appName == "" {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("no app provided"))
|
||||
}
|
||||
|
||||
files, err := config.LoadAppFiles("")
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
appFile, exists := files[appName]
|
||||
if !exists {
|
||||
logrus.Fatalf("cannot find app with name '%s'", appName)
|
||||
}
|
||||
|
||||
ed, ok := os.LookupEnv("EDITOR")
|
||||
if !ok {
|
||||
@ -30,7 +45,7 @@ var appConfigCommand = &cli.Command{
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command(ed, app.Path)
|
||||
cmd := exec.Command(ed, appFile.Path)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
@ -2,16 +2,9 @@ package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
abraFormatter "coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
stack "coopcloud.tech/abra/pkg/client/stack"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@ -22,6 +15,7 @@ var appDeployCommand = &cli.Command{
|
||||
Usage: "Deploy an app",
|
||||
Flags: []cli.Flag{
|
||||
internal.ForceFlag,
|
||||
internal.ChaosFlag,
|
||||
},
|
||||
Description: `
|
||||
This command deploys a new instance of an app. It does not support changing the
|
||||
@ -29,88 +23,13 @@ version of an existing deployed app, for this you need to look at the "abra app
|
||||
upgrade <app>" command.
|
||||
|
||||
You may pass "--force" to re-deploy the same version again. This can be useful
|
||||
if the container runtime has gotten into a weird state or your doing some live
|
||||
hacking.
|
||||
if the container runtime has gotten into a weird state.
|
||||
|
||||
Chas mode ("--chaos") will deploy your local checkout of a recipe as-is,
|
||||
including unstaged changes and can be useful for live hacking and testing new
|
||||
recipes.
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
stackName := app.StackName()
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Debugf("checking whether '%s' is already deployed", stackName)
|
||||
|
||||
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if isDeployed {
|
||||
if internal.Force {
|
||||
logrus.Infof("continuing with deployment due to '--force/-f' being set")
|
||||
} else {
|
||||
logrus.Fatalf("'%s' is already deployed", stackName)
|
||||
}
|
||||
}
|
||||
|
||||
version := deployedVersion
|
||||
if version == "" {
|
||||
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if len(versions) > 0 {
|
||||
version = versions[len(versions)-1]
|
||||
logrus.Infof("choosing '%s' as version to deploy", version)
|
||||
if err := recipe.EnsureVersion(app.Type, version); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
version = "latest commit"
|
||||
logrus.Warning("no versions detected, using latest commit")
|
||||
if err := recipe.EnsureLatest(app.Type); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh")
|
||||
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
for k, v := range abraShEnv {
|
||||
app.Env[k] = v
|
||||
}
|
||||
|
||||
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
deployOpts := stack.Deploy{
|
||||
Composefiles: composeFiles,
|
||||
Namespace: stackName,
|
||||
Prune: false,
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
}
|
||||
compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := DeployOverview(app, version); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Action: internal.DeployAction,
|
||||
BashComplete: func(c *cli.Context) {
|
||||
appNames, err := config.GetAppNames()
|
||||
if err != nil {
|
||||
@ -124,61 +43,3 @@ hacking.
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// DeployOverview shows a deployment overview
|
||||
func DeployOverview(app config.App, version string) error {
|
||||
tableCol := []string{"server", "compose", "domain", "stack", "version"}
|
||||
table := abraFormatter.CreateTable(tableCol)
|
||||
|
||||
deployConfig := "compose.yml"
|
||||
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
|
||||
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
|
||||
}
|
||||
|
||||
table.Append([]string{app.Server, deployConfig, app.Domain, app.StackName(), version})
|
||||
table.Render()
|
||||
|
||||
response := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: "continue with deployment?",
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !response {
|
||||
logrus.Fatal("exiting as requested")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewVersionOverview shows an upgrade or downgrade overview
|
||||
func NewVersionOverview(app config.App, currentVersion, newVersion string) error {
|
||||
tableCol := []string{"server", "compose", "domain", "stack", "current version", "to be deployed"}
|
||||
table := abraFormatter.CreateTable(tableCol)
|
||||
|
||||
deployConfig := "compose.yml"
|
||||
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
|
||||
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
|
||||
}
|
||||
|
||||
table.Append([]string{app.Server, deployConfig, app.Domain, app.StackName(), currentVersion, newVersion})
|
||||
table.Render()
|
||||
|
||||
response := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: "continue with deployment?",
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !response {
|
||||
logrus.Fatal("exiting as requested")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
180
cli/app/new.go
180
cli/app/new.go
@ -2,47 +2,13 @@ package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
abraFormatter "coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/secret"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type secrets map[string]string
|
||||
|
||||
var domain string
|
||||
var domainFlag = &cli.StringFlag{
|
||||
Name: "domain",
|
||||
Aliases: []string{"d"},
|
||||
Value: "",
|
||||
Usage: "Choose a domain name",
|
||||
Destination: &domain,
|
||||
}
|
||||
|
||||
var newAppServer string
|
||||
var newAppServerFlag = &cli.StringFlag{
|
||||
Name: "server",
|
||||
Aliases: []string{"s"},
|
||||
Value: "",
|
||||
Usage: "Show apps of a specific server",
|
||||
Destination: &newAppServer,
|
||||
}
|
||||
|
||||
var newAppName string
|
||||
var newAppNameFlag = &cli.StringFlag{
|
||||
Name: "app-name",
|
||||
Aliases: []string{"a"},
|
||||
Value: "",
|
||||
Usage: "Choose an app name",
|
||||
Destination: &newAppName,
|
||||
}
|
||||
|
||||
var appNewDescription = `
|
||||
This command takes a recipe and uses it to create a new app. This new app
|
||||
configuration is stored in your ~/.abra directory under the appropriate server.
|
||||
@ -69,14 +35,14 @@ var appNewCommand = &cli.Command{
|
||||
Aliases: []string{"n"},
|
||||
Description: appNewDescription,
|
||||
Flags: []cli.Flag{
|
||||
newAppServerFlag,
|
||||
domainFlag,
|
||||
newAppNameFlag,
|
||||
internal.NewAppServerFlag,
|
||||
internal.DomainFlag,
|
||||
internal.NewAppNameFlag,
|
||||
internal.PassFlag,
|
||||
internal.SecretsFlag,
|
||||
},
|
||||
ArgsUsage: "<recipe>",
|
||||
Action: action,
|
||||
Action: internal.NewAction,
|
||||
BashComplete: func(c *cli.Context) {
|
||||
catl, err := catalogue.ReadRecipeCatalogue()
|
||||
if err != nil {
|
||||
@ -90,141 +56,3 @@ var appNewCommand = &cli.Command{
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
|
||||
func ensureDomainFlag() error {
|
||||
if domain == "" {
|
||||
prompt := &survey.Input{
|
||||
Message: "Specify app domain",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &domain); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureServerFlag checks if the server flag was used. if not, asks the user for it.
|
||||
func ensureServerFlag() error {
|
||||
servers, err := config.GetServers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newAppServer == "" {
|
||||
prompt := &survey.Select{
|
||||
Message: "Select app server:",
|
||||
Options: servers,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &newAppServer); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureServerFlag checks if the AppName flag was used. if not, asks the user for it.
|
||||
func ensureAppNameFlag() error {
|
||||
if newAppName == "" {
|
||||
prompt := &survey.Input{
|
||||
Message: "Specify app name:",
|
||||
Default: config.SanitiseAppName(domain),
|
||||
}
|
||||
if err := survey.AskOne(prompt, &newAppName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createSecrets creates all secrets for a new app.
|
||||
func createSecrets(sanitisedAppName string) (secrets, error) {
|
||||
appEnvPath := path.Join(config.ABRA_DIR, "servers", newAppServer, fmt.Sprintf("%s.env", sanitisedAppName))
|
||||
appEnv, err := config.ReadEnv(appEnvPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secretEnvVars := secret.ReadSecretEnvVars(appEnv)
|
||||
secrets, err := secret.GenerateSecrets(secretEnvVars, sanitisedAppName, newAppServer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if internal.Pass {
|
||||
for secretName := range secrets {
|
||||
secretValue := secrets[secretName]
|
||||
if err := secret.PassInsertSecret(secretValue, secretName, sanitisedAppName, newAppServer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
// action is the main command-line action for this package
|
||||
func action(c *cli.Context) error {
|
||||
recipe := internal.ValidateRecipe(c)
|
||||
|
||||
if err := config.EnsureAbraDirExists(); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := ensureServerFlag(); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := ensureDomainFlag(); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := ensureAppNameFlag(); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
sanitisedAppName := config.SanitiseAppName(newAppName)
|
||||
if len(sanitisedAppName) > 45 {
|
||||
logrus.Fatalf("'%s' cannot be longer than 45 characters", sanitisedAppName)
|
||||
}
|
||||
logrus.Debugf("'%s' sanitised as '%s' for new app", newAppName, sanitisedAppName)
|
||||
|
||||
if err := config.TemplateAppEnvSample(recipe.Name, newAppName, newAppServer, domain, recipe.Name); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if internal.Secrets {
|
||||
secrets, err := createSecrets(sanitisedAppName)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
secretCols := []string{"Name", "Value"}
|
||||
secretTable := abraFormatter.CreateTable(secretCols)
|
||||
for secret := range secrets {
|
||||
secretTable.Append([]string{secret, secrets[secret]})
|
||||
}
|
||||
|
||||
if len(secrets) > 0 {
|
||||
defer secretTable.Render()
|
||||
}
|
||||
}
|
||||
|
||||
tableCol := []string{"Name", "Domain", "Type", "Server"}
|
||||
table := abraFormatter.CreateTable(tableCol)
|
||||
table.Append([]string{sanitisedAppName, domain, recipe.Name, newAppServer})
|
||||
|
||||
fmt.Println("")
|
||||
fmt.Println(fmt.Sprintf("New '%s' created! Here is your new app overview:", recipe.Name))
|
||||
fmt.Println("")
|
||||
table.Render()
|
||||
fmt.Println("")
|
||||
fmt.Println("You can configure this app by running the following:")
|
||||
fmt.Println(fmt.Sprintf("\n abra app config %s", sanitisedAppName))
|
||||
fmt.Println("")
|
||||
fmt.Println("You can deploy this app by running the following:")
|
||||
fmt.Println(fmt.Sprintf("\n abra app deploy %s", sanitisedAppName))
|
||||
fmt.Println("")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -4,9 +4,9 @@ import (
|
||||
"fmt"
|
||||
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
stack "coopcloud.tech/abra/pkg/client/stack"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"coopcloud.tech/tagcmp"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
@ -23,16 +23,20 @@ var appRollbackCommand = &cli.Command{
|
||||
ArgsUsage: "<app>",
|
||||
Flags: []cli.Flag{
|
||||
internal.ForceFlag,
|
||||
internal.ChaosFlag,
|
||||
},
|
||||
Description: `
|
||||
This command rolls an app back to a previous version if one exists.
|
||||
|
||||
You may pass "--force/-f" to downgrade to the same version again. This can be
|
||||
useful if the container runtime has gotten into a weird state or your doing
|
||||
some live hacking.
|
||||
useful if the container runtime has gotten into a weird state.
|
||||
|
||||
This action could be destructive, please ensure you have a copy of your app
|
||||
data beforehand - see "abra app backup <app>" for more.
|
||||
|
||||
Chas mode ("--chaos") will deploy your local checkout of a recipe as-is,
|
||||
including unstaged changes and can be useful for live hacking and testing new
|
||||
recipes.
|
||||
`,
|
||||
BashComplete: func(c *cli.Context) {
|
||||
appNames, err := config.GetAppNames()
|
||||
@ -62,10 +66,6 @@ data beforehand - see "abra app backup <app>" for more.
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if deployedVersion == "" {
|
||||
logrus.Fatalf("failed to determine version of deployed '%s'", app.Name)
|
||||
}
|
||||
|
||||
if !isDeployed {
|
||||
logrus.Fatalf("'%s' is not deployed?", app.Name)
|
||||
}
|
||||
@ -76,22 +76,30 @@ data beforehand - see "abra app backup <app>" for more.
|
||||
}
|
||||
|
||||
var availableDowngrades []string
|
||||
for _, version := range versions {
|
||||
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
parsedVersion, err := tagcmp.Parse(version)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if parsedVersion != parsedDeployedVersion && parsedVersion.IsLessThan(parsedDeployedVersion) {
|
||||
availableDowngrades = append(availableDowngrades, version)
|
||||
}
|
||||
if deployedVersion == "" {
|
||||
deployedVersion = "unknown"
|
||||
availableDowngrades = versions
|
||||
logrus.Warnf("failed to determine version of deployed '%s'", app.Name)
|
||||
}
|
||||
|
||||
if len(availableDowngrades) == 0 {
|
||||
logrus.Fatal("no available downgrades, you're on latest")
|
||||
if deployedVersion != "unknown" && !internal.Chaos {
|
||||
for _, version := range versions {
|
||||
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
parsedVersion, err := tagcmp.Parse(version)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if parsedVersion != parsedDeployedVersion && parsedVersion.IsLessThan(parsedDeployedVersion) {
|
||||
availableDowngrades = append(availableDowngrades, version)
|
||||
}
|
||||
}
|
||||
|
||||
if len(availableDowngrades) == 0 {
|
||||
logrus.Fatal("no available downgrades, you're on latest")
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: jeezus golang why do you not have a list reverse function
|
||||
@ -100,23 +108,34 @@ data beforehand - see "abra app backup <app>" for more.
|
||||
}
|
||||
|
||||
var chosenDowngrade string
|
||||
if !internal.Force {
|
||||
prompt := &survey.Select{
|
||||
Message: fmt.Sprintf("Please select a downgrade (current version: '%s'):", deployedVersion),
|
||||
Options: availableDowngrades,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &chosenDowngrade); err != nil {
|
||||
return err
|
||||
if !internal.Chaos {
|
||||
if internal.Force {
|
||||
chosenDowngrade = availableDowngrades[0]
|
||||
logrus.Debugf("choosing '%s' as version to downgrade to (--force)", chosenDowngrade)
|
||||
} else {
|
||||
prompt := &survey.Select{
|
||||
Message: fmt.Sprintf("Please select a downgrade (current version: '%s'):", deployedVersion),
|
||||
Options: availableDowngrades,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &chosenDowngrade); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if internal.Force {
|
||||
chosenDowngrade = availableDowngrades[0]
|
||||
logrus.Debugf("choosing '%s' as version to downgrade to", chosenDowngrade)
|
||||
if !internal.Chaos {
|
||||
if err := recipe.EnsureVersion(app.Type, chosenDowngrade); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := recipe.EnsureVersion(app.Type, chosenDowngrade); err != nil {
|
||||
logrus.Fatal(err)
|
||||
if internal.Chaos {
|
||||
logrus.Warn("chaos mode engaged")
|
||||
var err error
|
||||
chosenDowngrade, err = recipe.ChaosVersion(app.Type)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh")
|
||||
@ -144,7 +163,7 @@ data beforehand - see "abra app backup <app>" for more.
|
||||
}
|
||||
|
||||
if !internal.Force {
|
||||
if err := NewVersionOverview(app, deployedVersion, chosenDowngrade); err != nil {
|
||||
if err := internal.NewVersionOverview(app, deployedVersion, chosenDowngrade); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ import (
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/client/container"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/upstream/container"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
@ -55,15 +55,20 @@ var appRunCommand = &cli.Command{
|
||||
}
|
||||
|
||||
serviceName := c.Args().Get(1)
|
||||
stackAndServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", fmt.Sprintf("%s_%s", app.StackName(), serviceName))
|
||||
filters.Add("name", stackAndServiceName)
|
||||
|
||||
containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if len(containers) == 0 {
|
||||
logrus.Fatalf("no containers matching '%s' found?", stackAndServiceName)
|
||||
}
|
||||
if len(containers) > 1 {
|
||||
logrus.Fatalf("expected 1 container but got %d", len(containers))
|
||||
logrus.Fatalf("expected 1 container matching '%s' but got %d", stackAndServiceName, len(containers))
|
||||
}
|
||||
|
||||
cmd := c.Args().Slice()[2:]
|
||||
|
@ -99,7 +99,19 @@ var appSecretInsertCommand = &cli.Command{
|
||||
Aliases: []string{"i"},
|
||||
Usage: "Insert secret",
|
||||
Flags: []cli.Flag{internal.PassFlag},
|
||||
ArgsUsage: "<secret> <version> <data>",
|
||||
ArgsUsage: "<app> <secret-name> <version> <data>",
|
||||
Description: `
|
||||
This command inserts a secret into an app environment.
|
||||
|
||||
This can be useful when you want to manually generate secrets for an app
|
||||
environment. Typically, you can let Abra generate them for you on app creation
|
||||
(see "abra app new --secrets" for more).
|
||||
|
||||
Example:
|
||||
|
||||
abra app secret insert myapp db_pass v1 mySecretPassword
|
||||
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
|
||||
@ -131,12 +143,20 @@ var appSecretRmCommand = &cli.Command{
|
||||
Usage: "Remove a secret",
|
||||
Aliases: []string{"rm"},
|
||||
Flags: []cli.Flag{allSecretsFlag, internal.PassFlag},
|
||||
ArgsUsage: "<secret>",
|
||||
ArgsUsage: "<app> <secret-name>",
|
||||
Description: `
|
||||
This command removes a secret from an app environment.
|
||||
|
||||
Example:
|
||||
|
||||
abra app secret remove myapp db_pass
|
||||
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
|
||||
if c.Args().Get(1) != "" && allSecrets {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<secret>' and '--all' together"))
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<secret-name>' and '--all' together"))
|
||||
}
|
||||
|
||||
if c.Args().Get(1) == "" && !allSecrets {
|
||||
|
@ -5,8 +5,8 @@ import (
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
stack "coopcloud.tech/abra/pkg/client/stack"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@ -22,12 +22,28 @@ volumes as eligiblef or pruning once undeployed.
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
stackName := app.StackName()
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Debugf("checking whether '%s' is already deployed", stackName)
|
||||
|
||||
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if !isDeployed {
|
||||
logrus.Fatalf("'%s' is not deployed?", stackName)
|
||||
}
|
||||
|
||||
if err := internal.DeployOverview(app, deployedVersion, "continue with undeploy?"); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
rmOpts := stack.Remove{Namespaces: []string{app.StackName()}}
|
||||
if err := stack.RunRemove(c.Context, cl, rmOpts); err != nil {
|
||||
logrus.Fatal(err)
|
||||
|
@ -6,9 +6,9 @@ import (
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
stack "coopcloud.tech/abra/pkg/client/stack"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -22,6 +22,7 @@ var appUpgradeCommand = &cli.Command{
|
||||
ArgsUsage: "<app>",
|
||||
Flags: []cli.Flag{
|
||||
internal.ForceFlag,
|
||||
internal.ChaosFlag,
|
||||
},
|
||||
Description: `
|
||||
This command supports upgrading an app. You can use it to choose and roll out a
|
||||
@ -32,11 +33,14 @@ opposed to "abra app deploy <app>" which will not change the version of a
|
||||
deployed app.
|
||||
|
||||
You may pass "--force/-f" to upgrade to the same version again. This can be
|
||||
useful if the container runtime has gotten into a weird state or your doing
|
||||
some live hacking.
|
||||
useful if the container runtime has gotten into a weird state.
|
||||
|
||||
This action could be destructive, please ensure you have a copy of your app
|
||||
data beforehand - see "abra app backup <app>" for more.
|
||||
|
||||
Chas mode ("--chaos") will deploy your local checkout of a recipe as-is,
|
||||
including unstaged changes and can be useful for live hacking and testing new
|
||||
recipes.
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
@ -54,10 +58,6 @@ data beforehand - see "abra app backup <app>" for more.
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if deployedVersion == "" {
|
||||
logrus.Fatalf("failed to determine version of deployed '%s'", app.Name)
|
||||
}
|
||||
|
||||
if !isDeployed {
|
||||
logrus.Fatalf("'%s' is not deployed?", app.Name)
|
||||
}
|
||||
@ -67,43 +67,67 @@ data beforehand - see "abra app backup <app>" for more.
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
var availableUpgrades []string
|
||||
for _, version := range versions {
|
||||
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
parsedVersion, err := tagcmp.Parse(version)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if parsedVersion.IsGreaterThan(parsedDeployedVersion) {
|
||||
availableUpgrades = append(availableUpgrades, version)
|
||||
}
|
||||
if len(versions) == 0 && !internal.Chaos {
|
||||
logrus.Fatalf("no versions available '%s' in recipe catalogue?", app.Type)
|
||||
}
|
||||
|
||||
if len(availableUpgrades) == 0 {
|
||||
logrus.Fatal("no available upgrades, you're on latest")
|
||||
var availableUpgrades []string
|
||||
if deployedVersion == "" {
|
||||
deployedVersion = "unknown"
|
||||
availableUpgrades = versions
|
||||
logrus.Warnf("failed to determine version of deployed '%s'", app.Name)
|
||||
}
|
||||
|
||||
if deployedVersion != "unknown" && !internal.Chaos {
|
||||
for _, version := range versions {
|
||||
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
parsedVersion, err := tagcmp.Parse(version)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if parsedVersion.IsGreaterThan(parsedDeployedVersion) {
|
||||
availableUpgrades = append(availableUpgrades, version)
|
||||
}
|
||||
}
|
||||
|
||||
if len(availableUpgrades) == 0 && !internal.Force {
|
||||
logrus.Fatal("no available upgrades, you're on latest")
|
||||
availableUpgrades = versions
|
||||
}
|
||||
}
|
||||
|
||||
var chosenUpgrade string
|
||||
if !internal.Force {
|
||||
prompt := &survey.Select{
|
||||
Message: fmt.Sprintf("Please select an upgrade (current version: '%s'):", deployedVersion),
|
||||
Options: availableUpgrades,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &chosenUpgrade); err != nil {
|
||||
return err
|
||||
if len(availableUpgrades) > 0 && !internal.Chaos {
|
||||
if internal.Force {
|
||||
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
|
||||
logrus.Debugf("choosing '%s' as version to upgrade to", chosenUpgrade)
|
||||
} else {
|
||||
prompt := &survey.Select{
|
||||
Message: fmt.Sprintf("Please select an upgrade (current version: '%s'):", deployedVersion),
|
||||
Options: availableUpgrades,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &chosenUpgrade); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if internal.Force {
|
||||
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
|
||||
logrus.Debugf("choosing '%s' as version to upgrade to", chosenUpgrade)
|
||||
if !internal.Chaos {
|
||||
if err := recipe.EnsureVersion(app.Type, chosenUpgrade); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := recipe.EnsureVersion(app.Type, chosenUpgrade); err != nil {
|
||||
logrus.Fatal(err)
|
||||
if internal.Chaos {
|
||||
logrus.Warn("chaos mode engaged")
|
||||
var err error
|
||||
chosenUpgrade, err = recipe.ChaosVersion(app.Type)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh")
|
||||
@ -130,10 +154,8 @@ data beforehand - see "abra app backup <app>" for more.
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if !internal.Force {
|
||||
if err := NewVersionOverview(app, deployedVersion, chosenUpgrade); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if err := internal.NewVersionOverview(app, deployedVersion, chosenUpgrade); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
|
||||
|
@ -8,8 +8,8 @@ import (
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/client/stack"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
121
cli/autocomplete.go
Normal file
121
cli/autocomplete.go
Normal file
@ -0,0 +1,121 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// downloadFile downloads a file brah
|
||||
func downloadFile(filepath string, url string) (err error) {
|
||||
out, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutoCompleteCommand helps people set up auto-complete in their shells
|
||||
var AutoCompleteCommand = &cli.Command{
|
||||
Name: "autocomplete",
|
||||
Usage: "Help set up shell autocompletion",
|
||||
Aliases: []string{"ac"},
|
||||
Description: `
|
||||
This command helps set up autocompletion in your shell by downloading the
|
||||
relevant autocompletion files and laying out what additional information must
|
||||
be loaded.
|
||||
|
||||
Example:
|
||||
|
||||
abra autocomplete bash
|
||||
|
||||
Supported shells are as follows:
|
||||
|
||||
fish
|
||||
zsh
|
||||
bash
|
||||
`,
|
||||
ArgsUsage: "<shell>",
|
||||
Action: func(c *cli.Context) error {
|
||||
shellType := c.Args().First()
|
||||
|
||||
if shellType == "" {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("no shell provided"))
|
||||
}
|
||||
|
||||
supportedShells := map[string]bool{
|
||||
"bash": true,
|
||||
"zsh": true,
|
||||
"fish": true,
|
||||
}
|
||||
|
||||
if _, ok := supportedShells[shellType]; !ok {
|
||||
logrus.Fatalf("%s is not a supported shell right now, sorry", shellType)
|
||||
}
|
||||
|
||||
if shellType == "fish" {
|
||||
shellType = "zsh" // handled the same on the autocompletion side
|
||||
}
|
||||
|
||||
autocompletionDir := path.Join(config.ABRA_DIR, "autocompletion")
|
||||
if err := os.Mkdir(autocompletionDir, 0755); err != nil {
|
||||
if !os.IsExist(err) {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Debugf("'%s' already created, moving on...", autocompletionDir)
|
||||
}
|
||||
|
||||
autocompletionFile := path.Join(config.ABRA_DIR, "autocompletion", shellType)
|
||||
if _, err := os.Stat(autocompletionFile); err != nil && os.IsNotExist(err) {
|
||||
url := fmt.Sprintf("https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/autocomplete/%s", shellType)
|
||||
logrus.Infof("fetching %s", url)
|
||||
if err := downloadFile(autocompletionFile, url); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
switch shellType {
|
||||
case "bash":
|
||||
fmt.Println(fmt.Sprintf(`
|
||||
# Run the following commands to install autocompletion
|
||||
sudo mkdir /etc/bash/completion.d/
|
||||
sudo cp %s /etc/bash_completion.d/abra
|
||||
echo "source /etc/bash/completion.d/abra" >> ~/.bashrc
|
||||
`, autocompletionFile))
|
||||
case "zsh":
|
||||
fmt.Println(fmt.Sprintf(`
|
||||
# Run the following commands to install autocompletion
|
||||
sudo mkdir /etc/zsh/completion.d/
|
||||
sudo cp %s /etc/zsh/completion.d/abra
|
||||
echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc
|
||||
`, autocompletionFile))
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
@ -7,7 +7,7 @@ import (
|
||||
// CatalogueCommand defines the `abra catalogue` command and sub-commands.
|
||||
var CatalogueCommand = &cli.Command{
|
||||
Name: "catalogue",
|
||||
Usage: "Manage the recipe catalogue",
|
||||
Usage: "Manage the recipe catalogue (for maintainers)",
|
||||
Aliases: []string{"c"},
|
||||
ArgsUsage: "<recipe>",
|
||||
Description: "This command helps recipe packagers interact with the recipe catalogue",
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/git"
|
||||
"coopcloud.tech/abra/pkg/limit"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@ -29,6 +30,7 @@ var CatalogueSkipList = map[string]bool{
|
||||
"auto-apps-json": true,
|
||||
"auto-mirror": true,
|
||||
"backup-bot": true,
|
||||
"backup-bot-two": true,
|
||||
"coopcloud.tech": true,
|
||||
"coturn": true,
|
||||
"docker-cp-deploy": true,
|
||||
@ -73,10 +75,14 @@ var catalogueGenerateCommand = &cli.Command{
|
||||
|
||||
logrus.Debugf("ensuring '%v' recipe(s) are locally present and up-to-date", len(repos))
|
||||
|
||||
cloneLimiter := limit.New(10)
|
||||
retrieveBar := formatter.CreateProgressbar(len(repos), "retrieving recipes...")
|
||||
ch := make(chan string, len(repos))
|
||||
for _, repoMeta := range repos {
|
||||
go func(rm catalogue.RepoMeta) {
|
||||
cloneLimiter.Begin()
|
||||
defer cloneLimiter.End()
|
||||
|
||||
if recipeName != "" && recipeName != rm.Name {
|
||||
ch <- rm.Name
|
||||
retrieveBar.Add(1)
|
||||
|
23
cli/cli.go
23
cli/cli.go
@ -8,7 +8,9 @@ import (
|
||||
|
||||
"coopcloud.tech/abra/cli/app"
|
||||
"coopcloud.tech/abra/cli/catalogue"
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/cli/recipe"
|
||||
"coopcloud.tech/abra/cli/record"
|
||||
"coopcloud.tech/abra/cli/server"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
logrusStack "github.com/Gurpartap/logrus-stack"
|
||||
@ -40,8 +42,7 @@ var DebugFlag = &cli.BoolFlag{
|
||||
Usage: "Show DEBUG messages",
|
||||
}
|
||||
|
||||
// RunApp runs CLI abra app.
|
||||
func RunApp(version, commit string) {
|
||||
func newAbraApp(version, commit string) *cli.App {
|
||||
app := &cli.App{
|
||||
Name: "abra",
|
||||
Usage: `The Co-op Cloud command-line utility belt 🎩🐇
|
||||
@ -59,18 +60,20 @@ func RunApp(version, commit string) {
|
||||
server.ServerCommand,
|
||||
recipe.RecipeCommand,
|
||||
catalogue.CatalogueCommand,
|
||||
VersionCommand,
|
||||
record.RecordCommand,
|
||||
UpgradeCommand,
|
||||
AutoCompleteCommand,
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
VerboseFlag,
|
||||
DebugFlag,
|
||||
internal.NoInputFlag,
|
||||
},
|
||||
Authors: []*cli.Author{
|
||||
{
|
||||
Name: "Autonomic Co-op",
|
||||
Email: "helo@autonomic.zone",
|
||||
},
|
||||
{Name: "3wordchant"},
|
||||
{Name: "decentral1se"},
|
||||
{Name: "knoflook"},
|
||||
{Name: "roxxers"},
|
||||
},
|
||||
}
|
||||
|
||||
@ -106,6 +109,12 @@ func RunApp(version, commit string) {
|
||||
|
||||
return nil
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
||||
// RunApp runs CLI abra app.
|
||||
func RunApp(version, commit string) {
|
||||
app := newAbraApp(version, commit)
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
logrus.Fatal(err)
|
||||
|
@ -49,3 +49,213 @@ var ForceFlag = &cli.BoolFlag{
|
||||
Aliases: []string{"f"},
|
||||
Destination: &Force,
|
||||
}
|
||||
|
||||
// Chaos engages chaos mode.
|
||||
var Chaos bool
|
||||
|
||||
// ChaosFlag turns on/off chaos functionality.
|
||||
var ChaosFlag = &cli.BoolFlag{
|
||||
Name: "chaos",
|
||||
Value: false,
|
||||
Aliases: []string{"ch"},
|
||||
Usage: "Deploy uncommitted recipes changes. Use with care!",
|
||||
Destination: &Chaos,
|
||||
}
|
||||
|
||||
// DNSProvider specifies a DNS provider.
|
||||
var DNSProvider string
|
||||
|
||||
// DNSProviderFlag selects a DNS provider.
|
||||
var DNSProviderFlag = &cli.StringFlag{
|
||||
Name: "provider",
|
||||
Value: "",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "DNS provider",
|
||||
Destination: &DNSProvider,
|
||||
}
|
||||
|
||||
var NoInput bool
|
||||
var NoInputFlag = &cli.BoolFlag{
|
||||
Name: "no-input",
|
||||
Value: false,
|
||||
Aliases: []string{"n"},
|
||||
Usage: "Toggle non-interactive mode",
|
||||
Destination: &NoInput,
|
||||
}
|
||||
|
||||
var DNSType string
|
||||
|
||||
var DNSTypeFlag = &cli.StringFlag{
|
||||
Name: "type",
|
||||
Value: "",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "Domain name record type (e.g. A)",
|
||||
Destination: &DNSType,
|
||||
}
|
||||
|
||||
var DNSName string
|
||||
|
||||
var DNSNameFlag = &cli.StringFlag{
|
||||
Name: "name",
|
||||
Value: "",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "Domain name record name (e.g. mysubdomain)",
|
||||
Destination: &DNSName,
|
||||
}
|
||||
|
||||
var DNSValue string
|
||||
|
||||
var DNSValueFlag = &cli.StringFlag{
|
||||
Name: "value",
|
||||
Value: "",
|
||||
Aliases: []string{"v"},
|
||||
Usage: "Domain name record value (e.g. 192.168.1.1)",
|
||||
Destination: &DNSValue,
|
||||
}
|
||||
|
||||
var DNSTTL int
|
||||
|
||||
var DNSTTLFlag = &cli.IntFlag{
|
||||
Name: "ttl",
|
||||
Value: 86400,
|
||||
Aliases: []string{"T"},
|
||||
Usage: "Domain name TTL value)",
|
||||
Destination: &DNSTTL,
|
||||
}
|
||||
|
||||
var DNSPriority int
|
||||
|
||||
var DNSPriorityFlag = &cli.IntFlag{
|
||||
Name: "priority",
|
||||
Value: 10,
|
||||
Aliases: []string{"P"},
|
||||
Usage: "Domain name priority value",
|
||||
Destination: &DNSPriority,
|
||||
}
|
||||
|
||||
var ServerProvider string
|
||||
|
||||
var ServerProviderFlag = &cli.StringFlag{
|
||||
Name: "provider",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "3rd party server provider",
|
||||
Destination: &ServerProvider,
|
||||
}
|
||||
|
||||
var CapsulInstanceURL string
|
||||
|
||||
var CapsulInstanceURLFlag = &cli.StringFlag{
|
||||
Name: "capsul-url",
|
||||
Value: "yolo.servers.coop",
|
||||
Aliases: []string{"cu"},
|
||||
Usage: "capsul instance URL",
|
||||
Destination: &CapsulInstanceURL,
|
||||
}
|
||||
|
||||
var CapsulName string
|
||||
|
||||
var CapsulNameFlag = &cli.StringFlag{
|
||||
Name: "capsul-name",
|
||||
Value: "",
|
||||
Aliases: []string{"cn"},
|
||||
Usage: "capsul name",
|
||||
Destination: &CapsulName,
|
||||
}
|
||||
|
||||
var CapsulType string
|
||||
|
||||
var CapsulTypeFlag = &cli.StringFlag{
|
||||
Name: "capsul-type",
|
||||
Value: "f1-xs",
|
||||
Aliases: []string{"ct"},
|
||||
Usage: "capsul type",
|
||||
Destination: &CapsulType,
|
||||
}
|
||||
|
||||
var CapsulImage string
|
||||
|
||||
var CapsulImageFlag = &cli.StringFlag{
|
||||
Name: "capsul-image",
|
||||
Value: "debian10",
|
||||
Aliases: []string{"ci"},
|
||||
Usage: "capsul image",
|
||||
Destination: &CapsulImage,
|
||||
}
|
||||
|
||||
var CapsulSSHKeys cli.StringSlice
|
||||
|
||||
var CapsulSSHKeysFlag = &cli.StringSliceFlag{
|
||||
Name: "capsul-ssh-keys",
|
||||
Aliases: []string{"cs"},
|
||||
Usage: "capsul SSH key",
|
||||
Destination: &CapsulSSHKeys,
|
||||
}
|
||||
|
||||
var CapsulAPIToken string
|
||||
|
||||
var CapsulAPITokenFlag = &cli.StringFlag{
|
||||
Name: "capsul-token",
|
||||
Aliases: []string{"ca"},
|
||||
Usage: "capsul API token",
|
||||
EnvVars: []string{"CAPSUL_TOKEN"},
|
||||
Destination: &CapsulAPIToken,
|
||||
}
|
||||
|
||||
var HetznerCloudName string
|
||||
|
||||
var HetznerCloudNameFlag = &cli.StringFlag{
|
||||
Name: "hetzner-name",
|
||||
Value: "",
|
||||
Aliases: []string{"hn"},
|
||||
Usage: "hetzner cloud name",
|
||||
Destination: &HetznerCloudName,
|
||||
}
|
||||
|
||||
var HetznerCloudType string
|
||||
|
||||
var HetznerCloudTypeFlag = &cli.StringFlag{
|
||||
Name: "hetzner-type",
|
||||
Aliases: []string{"ht"},
|
||||
Usage: "hetzner cloud type",
|
||||
Destination: &HetznerCloudType,
|
||||
Value: "cx11",
|
||||
}
|
||||
|
||||
var HetznerCloudImage string
|
||||
|
||||
var HetznerCloudImageFlag = &cli.StringFlag{
|
||||
Name: "hetzner-image",
|
||||
Aliases: []string{"hi"},
|
||||
Usage: "hetzner cloud image",
|
||||
Value: "debian-10",
|
||||
Destination: &HetznerCloudImage,
|
||||
}
|
||||
|
||||
var HetznerCloudSSHKeys cli.StringSlice
|
||||
|
||||
var HetznerCloudSSHKeysFlag = &cli.StringSliceFlag{
|
||||
Name: "hetzner-ssh-keys",
|
||||
Aliases: []string{"hs"},
|
||||
Usage: "hetzner cloud SSH keys (e.g. me@foo.com)",
|
||||
Destination: &HetznerCloudSSHKeys,
|
||||
}
|
||||
|
||||
var HetznerCloudLocation string
|
||||
|
||||
var HetznerCloudLocationFlag = &cli.StringFlag{
|
||||
Name: "hetzner-location",
|
||||
Aliases: []string{"hl"},
|
||||
Usage: "hetzner cloud server location",
|
||||
Value: "hel1",
|
||||
Destination: &HetznerCloudLocation,
|
||||
}
|
||||
|
||||
var HetznerCloudAPIToken string
|
||||
|
||||
var HetznerCloudAPITokenFlag = &cli.StringFlag{
|
||||
Name: "hetzner-token",
|
||||
Aliases: []string{"ha"},
|
||||
Usage: "hetzner cloud API token",
|
||||
EnvVars: []string{"HCLOUD_TOKEN"},
|
||||
Destination: &HetznerCloudAPIToken,
|
||||
}
|
||||
|
191
cli/internal/deploy.go
Normal file
191
cli/internal/deploy.go
Normal file
@ -0,0 +1,191 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
abraFormatter "coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// DeployAction is the main command-line action for this package
|
||||
func DeployAction(c *cli.Context) error {
|
||||
app := ValidateApp(c)
|
||||
stackName := app.StackName()
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Debugf("checking whether '%s' is already deployed", stackName)
|
||||
|
||||
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if isDeployed {
|
||||
if Force {
|
||||
logrus.Warnf("'%s' already deployed but continuing (--force)", stackName)
|
||||
} else if Chaos {
|
||||
logrus.Warnf("'%s' already deployed but continuing (--chaos)", stackName)
|
||||
} else {
|
||||
logrus.Fatalf("'%s' is already deployed", stackName)
|
||||
}
|
||||
}
|
||||
|
||||
version := deployedVersion
|
||||
if version == "" && !Chaos {
|
||||
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if len(versions) > 0 {
|
||||
version = versions[len(versions)-1]
|
||||
logrus.Debugf("choosing '%s' as version to deploy", version)
|
||||
if err := recipe.EnsureVersion(app.Type, version); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
version = "latest commit"
|
||||
logrus.Warn("no versions detected, using latest commit")
|
||||
if err := recipe.EnsureLatest(app.Type); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if version == "" && !Chaos {
|
||||
logrus.Debugf("choosing '%s' as version to deploy", version)
|
||||
if err := recipe.EnsureVersion(app.Type, version); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if Chaos {
|
||||
logrus.Warnf("chaos mode engaged")
|
||||
var err error
|
||||
version, err = recipe.ChaosVersion(app.Type)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh")
|
||||
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
for k, v := range abraShEnv {
|
||||
app.Env[k] = v
|
||||
}
|
||||
|
||||
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
deployOpts := stack.Deploy{
|
||||
Composefiles: composeFiles,
|
||||
Namespace: stackName,
|
||||
Prune: false,
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
}
|
||||
compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := DeployOverview(app, version, "continue with deployment?"); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeployOverview shows a deployment overview
|
||||
func DeployOverview(app config.App, version, message string) error {
|
||||
tableCol := []string{"server", "compose", "domain", "stack", "version"}
|
||||
table := abraFormatter.CreateTable(tableCol)
|
||||
|
||||
deployConfig := "compose.yml"
|
||||
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
|
||||
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
|
||||
}
|
||||
|
||||
server := app.Server
|
||||
if app.Server == "default" {
|
||||
server = "local"
|
||||
}
|
||||
|
||||
table.Append([]string{server, deployConfig, app.Domain, app.StackName(), version})
|
||||
table.Render()
|
||||
|
||||
if NoInput {
|
||||
return nil
|
||||
}
|
||||
|
||||
response := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: message,
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !response {
|
||||
logrus.Fatal("exiting as requested")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewVersionOverview shows an upgrade or downgrade overview
|
||||
func NewVersionOverview(app config.App, currentVersion, newVersion string) error {
|
||||
tableCol := []string{"server", "compose", "domain", "stack", "current version", "to be deployed"}
|
||||
table := abraFormatter.CreateTable(tableCol)
|
||||
|
||||
deployConfig := "compose.yml"
|
||||
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
|
||||
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
|
||||
}
|
||||
|
||||
server := app.Server
|
||||
if app.Server == "default" {
|
||||
server = "local"
|
||||
}
|
||||
|
||||
table.Append([]string{server, deployConfig, app.Domain, app.StackName(), currentVersion, newVersion})
|
||||
table.Render()
|
||||
|
||||
if NoInput {
|
||||
return nil
|
||||
}
|
||||
|
||||
response := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: "continue with deployment?",
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !response {
|
||||
logrus.Fatal("exiting as requested")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
38
cli/internal/errors_test.go
Normal file
38
cli/internal/errors_test.go
Normal file
@ -0,0 +1,38 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Testing functions that call os.Exit
|
||||
// https://stackoverflow.com/questions/26225513/how-to-test-os-exit-scenarios-in-go
|
||||
// https://talks.golang.org/2014/testing.slide#23
|
||||
|
||||
var testapp = &cli.App{
|
||||
Name: "abra",
|
||||
Usage: `The Co-op Cloud command-line utility belt 🎩🐇`,
|
||||
}
|
||||
|
||||
// not testing output as that changes. just if it exits with code 1
|
||||
// does not work because of some weird errors on cli's part. Its a hard lib to test effectively.
|
||||
// func TestShowSubcommandHelpAndError(t *testing.T) {
|
||||
// if os.Getenv("HelpAndError") == "1" {
|
||||
// ShowSubcommandHelpAndError(cli.NewContext(testapp, nil, nil), errors.New("Test error"))
|
||||
// return
|
||||
// }
|
||||
// cmd := exec.Command(os.Args[0], "-test.run=TestShowSubcommandHelpAndError")
|
||||
// cmd.Env = append(os.Environ(), "HelpAndError=1")
|
||||
// var out bytes.Buffer
|
||||
// cmd.Stderr = &out
|
||||
// err := cmd.Run()
|
||||
// println(out.String())
|
||||
// if !strings.Contains(out.String(), "Test error") {
|
||||
|
||||
// t.Fatalf("expected command to show the error causing the exit, did not get correct stdout output")
|
||||
// }
|
||||
|
||||
// if e, ok := err.(*exec.ExitError); ok && !e.Success() {
|
||||
// return
|
||||
// }
|
||||
// t.Fatalf("process ran with err %v, want exit status 1", err)
|
||||
// }
|
203
cli/internal/new.go
Normal file
203
cli/internal/new.go
Normal file
@ -0,0 +1,203 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
abraFormatter "coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/secret"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type AppSecrets map[string]string
|
||||
|
||||
var Domain string
|
||||
var DomainFlag = &cli.StringFlag{
|
||||
Name: "domain",
|
||||
Aliases: []string{"d"},
|
||||
Value: "",
|
||||
Usage: "Choose a domain name",
|
||||
Destination: &Domain,
|
||||
}
|
||||
|
||||
var NewAppServer string
|
||||
var NewAppServerFlag = &cli.StringFlag{
|
||||
Name: "server",
|
||||
Aliases: []string{"s"},
|
||||
Value: "",
|
||||
Usage: "Show apps of a specific server",
|
||||
Destination: &NewAppServer,
|
||||
}
|
||||
|
||||
var NewAppName string
|
||||
var NewAppNameFlag = &cli.StringFlag{
|
||||
Name: "app-name",
|
||||
Aliases: []string{"a"},
|
||||
Value: "",
|
||||
Usage: "Choose an app name",
|
||||
Destination: &NewAppName,
|
||||
}
|
||||
|
||||
// RecipeName is used for configuring recipe name programmatically
|
||||
var RecipeName string
|
||||
|
||||
// createSecrets creates all secrets for a new app.
|
||||
func createSecrets(sanitisedAppName string) (AppSecrets, error) {
|
||||
appEnvPath := path.Join(config.ABRA_DIR, "servers", NewAppServer, fmt.Sprintf("%s.env", sanitisedAppName))
|
||||
appEnv, err := config.ReadEnv(appEnvPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secretEnvVars := secret.ReadSecretEnvVars(appEnv)
|
||||
secrets, err := secret.GenerateSecrets(secretEnvVars, sanitisedAppName, NewAppServer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if Pass {
|
||||
for secretName := range secrets {
|
||||
secretValue := secrets[secretName]
|
||||
if err := secret.PassInsertSecret(secretValue, secretName, sanitisedAppName, NewAppServer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
// ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
|
||||
func ensureDomainFlag(recipe recipe.Recipe, server string) error {
|
||||
if Domain == "" && !NoInput {
|
||||
prompt := &survey.Input{
|
||||
Message: "Specify app domain",
|
||||
Default: fmt.Sprintf("%s.%s", recipe.Name, server),
|
||||
}
|
||||
if err := survey.AskOne(prompt, &Domain); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if Domain == "" {
|
||||
return fmt.Errorf("no domain provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureServerFlag checks if the server flag was used. if not, asks the user for it.
|
||||
func ensureServerFlag() error {
|
||||
servers, err := config.GetServers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if NewAppServer == "" && !NoInput {
|
||||
prompt := &survey.Select{
|
||||
Message: "Select app server:",
|
||||
Options: servers,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &NewAppServer); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if NewAppServer == "" {
|
||||
return fmt.Errorf("no server provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureServerFlag checks if the AppName flag was used. if not, asks the user for it.
|
||||
func ensureAppNameFlag() error {
|
||||
if NewAppName == "" && !NoInput {
|
||||
prompt := &survey.Input{
|
||||
Message: "Specify app name:",
|
||||
Default: config.SanitiseAppName(Domain),
|
||||
}
|
||||
if err := survey.AskOne(prompt, &NewAppName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if NewAppName == "" {
|
||||
return fmt.Errorf("no app name provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewAction is the new app creation logic
|
||||
func NewAction(c *cli.Context) error {
|
||||
recipe := ValidateRecipeWithPrompt(c)
|
||||
|
||||
if err := config.EnsureAbraDirExists(); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := ensureServerFlag(); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := ensureDomainFlag(recipe, NewAppServer); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := ensureAppNameFlag(); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
sanitisedAppName := config.SanitiseAppName(NewAppName)
|
||||
if len(sanitisedAppName) > 45 {
|
||||
logrus.Fatalf("'%s' cannot be longer than 45 characters", sanitisedAppName)
|
||||
}
|
||||
logrus.Debugf("'%s' sanitised as '%s' for new app", NewAppName, sanitisedAppName)
|
||||
|
||||
if err := config.TemplateAppEnvSample(recipe.Name, NewAppName, NewAppServer, Domain, recipe.Name); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if Secrets {
|
||||
secrets, err := createSecrets(sanitisedAppName)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
secretCols := []string{"Name", "Value"}
|
||||
secretTable := abraFormatter.CreateTable(secretCols)
|
||||
for secret := range secrets {
|
||||
secretTable.Append([]string{secret, secrets[secret]})
|
||||
}
|
||||
|
||||
if len(secrets) > 0 {
|
||||
defer secretTable.Render()
|
||||
}
|
||||
}
|
||||
|
||||
if NewAppServer == "default" {
|
||||
NewAppServer = "local"
|
||||
}
|
||||
|
||||
tableCol := []string{"Name", "Domain", "Type", "Server"}
|
||||
table := abraFormatter.CreateTable(tableCol)
|
||||
table.Append([]string{sanitisedAppName, Domain, recipe.Name, NewAppServer})
|
||||
|
||||
fmt.Println("")
|
||||
fmt.Println(fmt.Sprintf("A new %s app has been created! Here is an overview:", recipe.Name))
|
||||
fmt.Println("")
|
||||
table.Render()
|
||||
fmt.Println("")
|
||||
fmt.Println("You can configure this app by running the following:")
|
||||
fmt.Println(fmt.Sprintf("\n abra app config %s", sanitisedAppName))
|
||||
fmt.Println("")
|
||||
fmt.Println("You can deploy this app by running the following:")
|
||||
fmt.Println(fmt.Sprintf("\n abra app deploy %s", sanitisedAppName))
|
||||
fmt.Println("")
|
||||
|
||||
return nil
|
||||
}
|
106
cli/internal/record.go
Normal file
106
cli/internal/record.go
Normal file
@ -0,0 +1,106 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// EnsureDNSProvider ensures a DNS provider is chosen.
|
||||
func EnsureDNSProvider() error {
|
||||
if DNSProvider == "" && !NoInput {
|
||||
prompt := &survey.Select{
|
||||
Message: "Select DNS provider",
|
||||
Options: []string{"gandi"},
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &DNSProvider); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if DNSProvider == "" {
|
||||
return fmt.Errorf("missing DNS provider?")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureDNSTypeFlag ensures a DNS type flag is present.
|
||||
func EnsureDNSTypeFlag(c *cli.Context) error {
|
||||
if DNSType == "" && !NoInput {
|
||||
prompt := &survey.Input{
|
||||
Message: "Specify DNS record type",
|
||||
Default: "A",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &DNSType); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if DNSType == "" {
|
||||
ShowSubcommandHelpAndError(c, errors.New("no record type provided"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureDNSNameFlag ensures a DNS name flag is present.
|
||||
func EnsureDNSNameFlag(c *cli.Context) error {
|
||||
if DNSName == "" && !NoInput {
|
||||
prompt := &survey.Input{
|
||||
Message: "Specify DNS record name",
|
||||
Default: "mysubdomain",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &DNSName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if DNSName == "" {
|
||||
ShowSubcommandHelpAndError(c, errors.New("no record name provided"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureDNSValueFlag ensures a DNS value flag is present.
|
||||
func EnsureDNSValueFlag(c *cli.Context) error {
|
||||
if DNSValue == "" && !NoInput {
|
||||
prompt := &survey.Input{
|
||||
Message: "Specify DNS record value",
|
||||
Default: "192.168.1.2",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &DNSValue); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if DNSName == "" {
|
||||
ShowSubcommandHelpAndError(c, errors.New("no record value provided"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureZoneArgument ensures a zone argument is present.
|
||||
func EnsureZoneArgument(c *cli.Context) (string, error) {
|
||||
var zone string
|
||||
if c.Args().First() == "" && !NoInput {
|
||||
prompt := &survey.Input{
|
||||
Message: "Specify a domain name zone",
|
||||
Default: "example.com",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &zone); err != nil {
|
||||
return zone, err
|
||||
}
|
||||
}
|
||||
|
||||
if zone == "" {
|
||||
ShowSubcommandHelpAndError(c, errors.New("no zone value provided"))
|
||||
}
|
||||
|
||||
return zone, nil
|
||||
}
|
208
cli/internal/server.go
Normal file
208
cli/internal/server.go
Normal file
@ -0,0 +1,208 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// EnsureServerProvider ensures a 3rd party server provider is chosen.
|
||||
func EnsureServerProvider() error {
|
||||
if ServerProvider == "" && !NoInput {
|
||||
prompt := &survey.Select{
|
||||
Message: "Select server provider",
|
||||
Options: []string{"capsul", "hetzner-cloud"},
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &ServerProvider); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if ServerProvider == "" {
|
||||
return fmt.Errorf("missing server provider?")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureNewCapsulVPSFlags ensure all flags are present.
|
||||
func EnsureNewCapsulVPSFlags(c *cli.Context) error {
|
||||
if CapsulName == "" && !NoInput {
|
||||
prompt := &survey.Input{
|
||||
Message: "specify capsul name",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &CapsulName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !NoInput {
|
||||
prompt := &survey.Input{
|
||||
Message: "specify capsul instance URL",
|
||||
Default: CapsulInstanceURL,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &CapsulInstanceURL); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !NoInput {
|
||||
prompt := &survey.Input{
|
||||
Message: "specify capsul type",
|
||||
Default: CapsulType,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &CapsulType); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !NoInput {
|
||||
prompt := &survey.Input{
|
||||
Message: "specify capsul image",
|
||||
Default: CapsulImage,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &CapsulImage); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(CapsulSSHKeys.Value()) == 0 && !NoInput {
|
||||
var sshKeys string
|
||||
prompt := &survey.Input{
|
||||
Message: "specify capsul SSH keys (e.g. me@foo.com)",
|
||||
Default: "",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &CapsulSSHKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
CapsulSSHKeys = *cli.NewStringSlice(strings.Split(sshKeys, ",")...)
|
||||
}
|
||||
|
||||
if CapsulAPIToken == "" && !NoInput {
|
||||
token, ok := os.LookupEnv("CAPSUL_TOKEN")
|
||||
if !ok {
|
||||
prompt := &survey.Input{
|
||||
Message: "specify capsul API token",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &CapsulAPIToken); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
CapsulAPIToken = token
|
||||
}
|
||||
}
|
||||
|
||||
if CapsulName == "" {
|
||||
ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul name?"))
|
||||
}
|
||||
if CapsulInstanceURL == "" {
|
||||
ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul instance url?"))
|
||||
}
|
||||
if CapsulType == "" {
|
||||
ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul type?"))
|
||||
}
|
||||
if CapsulImage == "" {
|
||||
ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul image?"))
|
||||
}
|
||||
if len(CapsulSSHKeys.Value()) == 0 {
|
||||
ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul ssh keys?"))
|
||||
}
|
||||
if CapsulAPIToken == "" {
|
||||
ShowSubcommandHelpAndError(c, fmt.Errorf("missing capsul API token?"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureNewHetznerCloudVPSFlags ensure all flags are present.
|
||||
func EnsureNewHetznerCloudVPSFlags(c *cli.Context) error {
|
||||
if HetznerCloudName == "" && !NoInput {
|
||||
prompt := &survey.Input{
|
||||
Message: "specify hetzner cloud VPS name",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &HetznerCloudName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !NoInput {
|
||||
prompt := &survey.Input{
|
||||
Message: "specify hetzner cloud VPS type",
|
||||
Default: HetznerCloudType,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &HetznerCloudType); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !NoInput {
|
||||
prompt := &survey.Input{
|
||||
Message: "specify hetzner cloud VPS image",
|
||||
Default: HetznerCloudImage,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &HetznerCloudImage); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(HetznerCloudSSHKeys.Value()) == 0 && !NoInput {
|
||||
var sshKeys string
|
||||
prompt := &survey.Input{
|
||||
Message: "specify hetzner cloud SSH keys (e.g. me@foo.com)",
|
||||
Default: "",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &sshKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
HetznerCloudSSHKeys = *cli.NewStringSlice(strings.Split(sshKeys, ",")...)
|
||||
}
|
||||
|
||||
if !NoInput {
|
||||
prompt := &survey.Input{
|
||||
Message: "specify hetzner cloud VPS location",
|
||||
Default: HetznerCloudLocation,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &HetznerCloudLocation); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if HetznerCloudAPIToken == "" && !NoInput {
|
||||
token, ok := os.LookupEnv("HCLOUD_TOKEN")
|
||||
if !ok {
|
||||
prompt := &survey.Input{
|
||||
Message: "specify hetzner cloud API token",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &HetznerCloudAPIToken); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
HetznerCloudAPIToken = token
|
||||
}
|
||||
}
|
||||
|
||||
if HetznerCloudName == "" {
|
||||
ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud VPS name?"))
|
||||
}
|
||||
if HetznerCloudType == "" {
|
||||
ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud VPS type?"))
|
||||
}
|
||||
if HetznerCloudImage == "" {
|
||||
ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud image?"))
|
||||
}
|
||||
if len(HetznerCloudSSHKeys.Value()) == 0 {
|
||||
ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud ssh keys?"))
|
||||
}
|
||||
if HetznerCloudLocation == "" {
|
||||
ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud VPS location?"))
|
||||
}
|
||||
if HetznerCloudAPIToken == "" {
|
||||
ShowSubcommandHelpAndError(c, fmt.Errorf("missing hetzner cloud API token?"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -2,14 +2,20 @@ package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// AppName is used for configuring app name programmatically
|
||||
var AppName string
|
||||
|
||||
// ValidateRecipe ensures the recipe arg is valid.
|
||||
func ValidateRecipe(c *cli.Context) recipe.Recipe {
|
||||
recipeName := c.Args().First()
|
||||
@ -28,10 +34,57 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
|
||||
return recipe
|
||||
}
|
||||
|
||||
// ValidateRecipeWithPrompt ensures a recipe argument is present before
|
||||
// validating, asking for input if required.
|
||||
func ValidateRecipeWithPrompt(c *cli.Context) recipe.Recipe {
|
||||
recipeName := c.Args().First()
|
||||
|
||||
if recipeName == "" && !NoInput {
|
||||
catl, err := catalogue.ReadRecipeCatalogue()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
var recipes []string
|
||||
for name := range catl {
|
||||
recipes = append(recipes, name)
|
||||
}
|
||||
prompt := &survey.Select{
|
||||
Message: "Select recipe",
|
||||
Options: recipes,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &recipeName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if RecipeName != "" {
|
||||
recipeName = RecipeName
|
||||
logrus.Debugf("programmatically setting recipe name to %s", recipeName)
|
||||
}
|
||||
|
||||
if recipeName == "" {
|
||||
ShowSubcommandHelpAndError(c, errors.New("no recipe provided"))
|
||||
}
|
||||
|
||||
recipe, err := recipe.Get(recipeName)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Debugf("validated '%s' as recipe argument", recipeName)
|
||||
|
||||
return recipe
|
||||
}
|
||||
|
||||
// ValidateApp ensures the app name arg is valid.
|
||||
func ValidateApp(c *cli.Context) config.App {
|
||||
appName := c.Args().First()
|
||||
|
||||
if AppName != "" {
|
||||
appName = AppName
|
||||
logrus.Debugf("programmatically setting app name to %s", appName)
|
||||
}
|
||||
|
||||
if appName == "" {
|
||||
ShowSubcommandHelpAndError(c, errors.New("no app provided"))
|
||||
}
|
||||
@ -51,14 +104,66 @@ func ValidateApp(c *cli.Context) config.App {
|
||||
}
|
||||
|
||||
// ValidateDomain ensures the domain name arg is valid.
|
||||
func ValidateDomain(c *cli.Context) string {
|
||||
func ValidateDomain(c *cli.Context) (string, error) {
|
||||
domainName := c.Args().First()
|
||||
|
||||
if domainName == "" && !NoInput {
|
||||
prompt := &survey.Input{
|
||||
Message: "Specify a domain name",
|
||||
Default: "example.com",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &domainName); err != nil {
|
||||
return domainName, err
|
||||
}
|
||||
}
|
||||
|
||||
if domainName == "" {
|
||||
ShowSubcommandHelpAndError(c, errors.New("no domain provided"))
|
||||
}
|
||||
|
||||
logrus.Debugf("validated '%s' as domain argument", domainName)
|
||||
|
||||
return domainName
|
||||
return domainName, nil
|
||||
}
|
||||
|
||||
// ValidateSubCmdFlags ensures flag order conforms to correct order
|
||||
func ValidateSubCmdFlags(c *cli.Context) bool {
|
||||
for argIdx, arg := range c.Args().Slice() {
|
||||
if !strings.HasPrefix(arg, "--") {
|
||||
for _, flag := range c.Args().Slice()[argIdx:] {
|
||||
if strings.HasPrefix(flag, "--") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ValidateServer ensures the server name arg is valid.
|
||||
func ValidateServer(c *cli.Context) (string, error) {
|
||||
serverName := c.Args().First()
|
||||
|
||||
serverNames, err := config.ReadServerNames()
|
||||
if err != nil {
|
||||
return serverName, err
|
||||
}
|
||||
|
||||
if serverName == "" && !NoInput {
|
||||
prompt := &survey.Select{
|
||||
Message: "Specify a server name",
|
||||
Options: serverNames,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &serverName); err != nil {
|
||||
return serverName, err
|
||||
}
|
||||
}
|
||||
|
||||
if serverName == "" {
|
||||
ShowSubcommandHelpAndError(c, errors.New("no server provided"))
|
||||
}
|
||||
|
||||
logrus.Debugf("validated '%s' as server argument", serverName)
|
||||
|
||||
return serverName, nil
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
@ -18,43 +19,56 @@ var recipeNewCommand = &cli.Command{
|
||||
Usage: "Create a new recipe",
|
||||
Aliases: []string{"n"},
|
||||
ArgsUsage: "<recipe>",
|
||||
Action: func(c *cli.Context) error {
|
||||
recipe := internal.ValidateRecipe(c)
|
||||
Description: `
|
||||
This command creates a new recipe.
|
||||
|
||||
directory := path.Join(config.APPS_DIR, recipe.Name)
|
||||
Abra uses our built-in example repository which is available here:
|
||||
|
||||
https://git.coopcloud.tech/coop-cloud/example
|
||||
|
||||
Files within the example repository make use of the Golang templating system
|
||||
which Abra uses to inject values into the generated recipe folder (e.g. name of
|
||||
recipe and domain in the sample environment config).
|
||||
|
||||
The new example repository is cloned to ~/.abra/apps/<recipe>.
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
recipeName := c.Args().First()
|
||||
|
||||
if recipeName == "" {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("no recipe provided"))
|
||||
}
|
||||
|
||||
directory := path.Join(config.APPS_DIR, recipeName)
|
||||
if _, err := os.Stat(directory); !os.IsNotExist(err) {
|
||||
logrus.Fatalf("'%s' recipe directory already exists?", directory)
|
||||
return nil
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL)
|
||||
if err := git.Clone(directory, url); err != nil {
|
||||
return err
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
gitRepo := path.Join(config.APPS_DIR, recipe.Name, ".git")
|
||||
gitRepo := path.Join(config.APPS_DIR, recipeName, ".git")
|
||||
if err := os.RemoveAll(gitRepo); err != nil {
|
||||
logrus.Fatal(err)
|
||||
return nil
|
||||
}
|
||||
logrus.Debugf("removed git repo in '%s'", gitRepo)
|
||||
|
||||
toParse := []string{
|
||||
path.Join(config.APPS_DIR, recipe.Name, "README.md"),
|
||||
path.Join(config.APPS_DIR, recipe.Name, ".env.sample"),
|
||||
path.Join(config.APPS_DIR, recipe.Name, ".drone.yml"),
|
||||
path.Join(config.APPS_DIR, recipeName, "README.md"),
|
||||
path.Join(config.APPS_DIR, recipeName, ".env.sample"),
|
||||
path.Join(config.APPS_DIR, recipeName, ".drone.yml"),
|
||||
}
|
||||
for _, path := range toParse {
|
||||
file, err := os.OpenFile(path, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
tpl, err := template.ParseFiles(path)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: ask for description and probably other things so that the
|
||||
@ -63,15 +77,14 @@ var recipeNewCommand = &cli.Command{
|
||||
if err := tpl.Execute(file, struct {
|
||||
Name string
|
||||
Description string
|
||||
}{recipe.Name, "TODO"}); err != nil {
|
||||
}{recipeName, "TODO"}); err != nil {
|
||||
logrus.Fatal(err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Infof(
|
||||
"new recipe '%s' created in %s, happy hacking!\n",
|
||||
recipe.Name, path.Join(config.APPS_DIR, recipe.Name),
|
||||
recipeName, path.Join(config.APPS_DIR, recipeName),
|
||||
)
|
||||
|
||||
return nil
|
||||
|
@ -31,7 +31,7 @@ var PatchFlag = &cli.BoolFlag{
|
||||
// RecipeCommand defines all recipe related sub-commands.
|
||||
var RecipeCommand = &cli.Command{
|
||||
Name: "recipe",
|
||||
Usage: "Manage recipes",
|
||||
Usage: "Manage recipes (for maintainers)",
|
||||
ArgsUsage: "<recipe>",
|
||||
Aliases: []string{"r"},
|
||||
Description: `
|
||||
|
@ -120,7 +120,9 @@ or a rollback of an app.
|
||||
prompt := &survey.Input{
|
||||
Message: "tag message",
|
||||
}
|
||||
survey.AskOne(prompt, &TagMessage)
|
||||
if err := survey.AskOne(prompt, &TagMessage); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var createTagOptions git.CreateTagOptions
|
||||
@ -140,7 +142,9 @@ or a rollback of an app.
|
||||
prompt := &survey.Input{
|
||||
Message: "commit message",
|
||||
}
|
||||
survey.AskOne(prompt, &CommitMessage)
|
||||
if err := survey.AskOne(prompt, &CommitMessage); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
err = commitWorktree.AddGlob("compose.**yml")
|
||||
if err != nil {
|
||||
@ -287,6 +291,9 @@ func getImageVersions(recipe recipe.Recipe) map[string]string {
|
||||
|
||||
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]
|
||||
}
|
||||
|
@ -1,13 +1,17 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/docker/distribution/reference"
|
||||
@ -15,6 +19,11 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type imgPin struct {
|
||||
image string
|
||||
version tagcmp.Tag
|
||||
}
|
||||
|
||||
var recipeUpgradeCommand = &cli.Command{
|
||||
Name: "upgrade",
|
||||
Usage: "Upgrade recipe image tags",
|
||||
@ -45,6 +54,43 @@ is up to the end-user to decide.
|
||||
}
|
||||
}
|
||||
|
||||
// check for versions file and load pinned versions
|
||||
versionsPresent := false
|
||||
recipeDir := path.Join(config.ABRA_DIR, "apps", recipe.Name)
|
||||
versionsPath := path.Join(recipeDir, "versions")
|
||||
var servicePins = make(map[string]imgPin)
|
||||
if _, err := os.Stat(versionsPath); err == nil {
|
||||
logrus.Debugf("found versions file for %s", recipe.Name)
|
||||
file, err := os.Open(versionsPath)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
splitLine := strings.Split(line, " ")
|
||||
if splitLine[0] != "pin" || len(splitLine) != 3 {
|
||||
logrus.Fatalf("malformed version pin specification: %s", line)
|
||||
}
|
||||
pinSlice := strings.Split(splitLine[2], ":")
|
||||
pinTag, err := tagcmp.Parse(pinSlice[1])
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
pin := imgPin{
|
||||
image: pinSlice[0],
|
||||
version: pinTag,
|
||||
}
|
||||
servicePins[splitLine[1]] = pin
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
versionsPresent = true
|
||||
} else {
|
||||
logrus.Debugf("did not find versions file for %s", recipe.Name)
|
||||
}
|
||||
|
||||
for _, service := range recipe.Config.Services {
|
||||
catlVersions, err := catalogue.VersionsOfService(recipe.Name, service.Name)
|
||||
if err != nil {
|
||||
@ -69,7 +115,6 @@ is up to the end-user to decide.
|
||||
// first position of the string
|
||||
image = strings.Split(image, "/")[1]
|
||||
}
|
||||
|
||||
semverLikeTag := true
|
||||
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
|
||||
logrus.Debugf("'%s' not considered semver-like", img.(reference.NamedTagged).Tag())
|
||||
@ -81,7 +126,6 @@ is up to the end-user to decide.
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Debugf("parsed '%s' for '%s'", tag, service.Name)
|
||||
|
||||
var compatible []tagcmp.Tag
|
||||
for _, regVersion := range regVersions {
|
||||
other, err := tagcmp.Parse(regVersion.Name)
|
||||
@ -117,44 +161,69 @@ is up to the end-user to decide.
|
||||
}
|
||||
|
||||
logrus.Debugf("detected compatible upgradable tags '%s' for '%s'", compatibleStrings, service.Name)
|
||||
|
||||
var upgradeTag string
|
||||
if bumpType != 0 {
|
||||
for _, upTag := range compatible {
|
||||
upElement, err := tag.UpgradeDelta(upTag)
|
||||
if err != nil {
|
||||
return err
|
||||
_, ok := servicePins[service.Name]
|
||||
if versionsPresent && ok {
|
||||
pinnedTag := servicePins[service.Name].version
|
||||
if tag.IsLessThan(pinnedTag) {
|
||||
pinnedTagString := pinnedTag.String()
|
||||
contains := false
|
||||
for _, v := range compatible {
|
||||
if pinnedTag.IsUpgradeCompatible(v) {
|
||||
contains = true
|
||||
upgradeTag = v.String()
|
||||
break
|
||||
}
|
||||
}
|
||||
delta := upElement.UpgradeType()
|
||||
if delta <= bumpType {
|
||||
upgradeTag = upTag.String()
|
||||
break
|
||||
if contains {
|
||||
logrus.Infof("Upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString)
|
||||
} else {
|
||||
logrus.Infof("service %s, image %s pinned to %s. No compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if upgradeTag == "" {
|
||||
logrus.Warnf("not upgrading from '%s' to '%s' for '%s', because the upgrade type is more serious than what user wants.", tag.String(), compatible[0].String(), image)
|
||||
} else {
|
||||
logrus.Fatalf("Service %s is at version %s, but pinned to %s. Please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String())
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
msg := fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
|
||||
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
|
||||
tag := img.(reference.NamedTagged).Tag()
|
||||
logrus.Warning(fmt.Sprintf("unable to determine versioning semantics of '%s', listing all tags", tag))
|
||||
msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
|
||||
compatibleStrings = []string{}
|
||||
for _, regVersion := range regVersions {
|
||||
compatibleStrings = append(compatibleStrings, regVersion.Name)
|
||||
if bumpType != 0 {
|
||||
for _, upTag := range compatible {
|
||||
upElement, err := tag.UpgradeDelta(upTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delta := upElement.UpgradeType()
|
||||
if delta <= bumpType {
|
||||
upgradeTag = upTag.String()
|
||||
break
|
||||
}
|
||||
}
|
||||
if upgradeTag == "" {
|
||||
logrus.Warnf("not upgrading from '%s' to '%s' for '%s', because the upgrade type is more serious than what user wants.", tag.String(), compatible[0].String(), image)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
msg := fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
|
||||
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
|
||||
tag := img.(reference.NamedTagged).Tag()
|
||||
logrus.Warning(fmt.Sprintf("unable to determine versioning semantics of '%s', listing all tags", tag))
|
||||
msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
|
||||
compatibleStrings = []string{}
|
||||
for _, regVersion := range regVersions {
|
||||
compatibleStrings = append(compatibleStrings, regVersion.Name)
|
||||
}
|
||||
}
|
||||
|
||||
prompt := &survey.Select{
|
||||
Message: msg,
|
||||
Options: compatibleStrings,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &upgradeTag); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
prompt := &survey.Select{
|
||||
Message: msg,
|
||||
Options: compatibleStrings,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &upgradeTag); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := recipe.UpdateTag(image, upgradeTag); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
79
cli/record/list.go
Normal file
79
cli/record/list.go
Normal file
@ -0,0 +1,79 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
abraFormatter "coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
gandiPkg "coopcloud.tech/abra/pkg/dns/gandi"
|
||||
"github.com/libdns/gandi"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// RecordListCommand lists domains.
|
||||
var RecordListCommand = &cli.Command{
|
||||
Name: "list",
|
||||
Usage: "List domain name records",
|
||||
Aliases: []string{"ls"},
|
||||
ArgsUsage: "<zone>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DNSProviderFlag,
|
||||
},
|
||||
Description: `
|
||||
This command lists all domain name records managed by a 3rd party provider for
|
||||
a specific zone.
|
||||
|
||||
You must specify a zone (e.g. example.com) under which your domain name records
|
||||
are listed. This zone must already be created on your provider account.
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := internal.EnsureDNSProvider(); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
zone, err := internal.EnsureZoneArgument(c)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
var provider gandi.Provider
|
||||
switch internal.DNSProvider {
|
||||
case "gandi":
|
||||
provider, err = gandiPkg.New()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
default:
|
||||
logrus.Fatalf("'%s' is not a supported DNS provider", internal.DNSProvider)
|
||||
}
|
||||
|
||||
records, err := provider.GetRecords(c.Context, zone)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
tableCol := []string{"type", "name", "value", "TTL", "priority"}
|
||||
table := abraFormatter.CreateTable(tableCol)
|
||||
|
||||
for _, record := range records {
|
||||
value := record.Value
|
||||
if len(record.Value) > 30 {
|
||||
value = fmt.Sprintf("%s...", record.Value[:30])
|
||||
}
|
||||
|
||||
table.Append([]string{
|
||||
record.Type,
|
||||
record.Name,
|
||||
value,
|
||||
record.TTL.String(),
|
||||
strconv.Itoa(record.Priority),
|
||||
})
|
||||
}
|
||||
|
||||
table.Render()
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
136
cli/record/new.go
Normal file
136
cli/record/new.go
Normal file
@ -0,0 +1,136 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
abraFormatter "coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
gandiPkg "coopcloud.tech/abra/pkg/dns/gandi"
|
||||
"github.com/libdns/gandi"
|
||||
"github.com/libdns/libdns"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// RecordNewCommand creates a new domain name record.
|
||||
var RecordNewCommand = &cli.Command{
|
||||
Name: "new",
|
||||
Usage: "Create a new domain record",
|
||||
Aliases: []string{"n"},
|
||||
ArgsUsage: "<zone>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DNSProviderFlag,
|
||||
internal.DNSTypeFlag,
|
||||
internal.DNSNameFlag,
|
||||
internal.DNSValueFlag,
|
||||
internal.DNSTTLFlag,
|
||||
internal.DNSPriorityFlag,
|
||||
},
|
||||
Description: `
|
||||
This command creates a new domain name record for a specific zone.
|
||||
|
||||
You must specify a zone (e.g. example.com) under which your domain name records
|
||||
are listed. This zone must already be created on your provider account.
|
||||
|
||||
Example:
|
||||
|
||||
abra record new foo.com -p gandi -t A -n myapp -v 192.168.178.44
|
||||
|
||||
You may also invoke this command in "wizard" mode and be prompted for input
|
||||
|
||||
abra record new
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
zone, err := internal.EnsureZoneArgument(c)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := internal.EnsureDNSProvider(); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
var provider gandi.Provider
|
||||
switch internal.DNSProvider {
|
||||
case "gandi":
|
||||
provider, err = gandiPkg.New()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
default:
|
||||
logrus.Fatalf("'%s' is not a supported DNS provider", internal.DNSProvider)
|
||||
}
|
||||
|
||||
if err := internal.EnsureDNSTypeFlag(c); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := internal.EnsureDNSNameFlag(c); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := internal.EnsureDNSValueFlag(c); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
record := libdns.Record{
|
||||
Type: internal.DNSType,
|
||||
Name: internal.DNSName,
|
||||
Value: internal.DNSValue,
|
||||
TTL: time.Duration(internal.DNSTTL),
|
||||
}
|
||||
|
||||
if internal.DNSType == "MX" || internal.DNSType == "SRV" || internal.DNSType == "URI" {
|
||||
record.Priority = internal.DNSPriority
|
||||
}
|
||||
|
||||
records, err := provider.GetRecords(c.Context, zone)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
for _, existingRecord := range records {
|
||||
if existingRecord.Type == record.Type &&
|
||||
existingRecord.Name == record.Name &&
|
||||
existingRecord.Value == record.Value {
|
||||
logrus.Fatal("provider library reports that this record already exists?")
|
||||
}
|
||||
}
|
||||
|
||||
createdRecords, err := provider.SetRecords(
|
||||
c.Context,
|
||||
zone,
|
||||
[]libdns.Record{record},
|
||||
)
|
||||
|
||||
if len(createdRecords) == 0 {
|
||||
logrus.Fatal("provider library reports that no record was created?")
|
||||
}
|
||||
|
||||
createdRecord := createdRecords[0]
|
||||
|
||||
tableCol := []string{"type", "name", "value", "TTL", "priority"}
|
||||
table := abraFormatter.CreateTable(tableCol)
|
||||
|
||||
value := createdRecord.Value
|
||||
if len(createdRecord.Value) > 30 {
|
||||
value = fmt.Sprintf("%s...", createdRecord.Value[:30])
|
||||
}
|
||||
|
||||
table.Append([]string{
|
||||
createdRecord.Type,
|
||||
createdRecord.Name,
|
||||
value,
|
||||
createdRecord.TTL.String(),
|
||||
strconv.Itoa(createdRecord.Priority),
|
||||
})
|
||||
|
||||
table.Render()
|
||||
|
||||
logrus.Info("record created")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
38
cli/record/record.go
Normal file
38
cli/record/record.go
Normal file
@ -0,0 +1,38 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// RecordCommand supports managing DNS entries.
|
||||
var RecordCommand = &cli.Command{
|
||||
Name: "record",
|
||||
Usage: "Manage domain name records via 3rd party providers",
|
||||
Aliases: []string{"rc"},
|
||||
ArgsUsage: "<record>",
|
||||
Description: `
|
||||
This command supports managing domain name records via 3rd party providers such
|
||||
as Gandi DNS. It supports listing, creating and removing all types of records
|
||||
that you might need for managing Co-op Cloud apps.
|
||||
|
||||
The following providers are supported:
|
||||
|
||||
Gandi DNS https://www.gandi.net
|
||||
|
||||
You need an account with such a provider already. Typically, you need to
|
||||
provide an API token on the Abra command-line when using these commands so that
|
||||
you can authenticate with your provider account.
|
||||
|
||||
New providers can be integrated, we welcome change sets. See the underlying DNS
|
||||
library documentation for more. It supports many existing providers and allows
|
||||
to implement new provider support easily.
|
||||
|
||||
https://pkg.go.dev/github.com/libdns/libdns
|
||||
|
||||
`,
|
||||
Subcommands: []*cli.Command{
|
||||
RecordListCommand,
|
||||
RecordNewCommand,
|
||||
RecordRemoveCommand,
|
||||
},
|
||||
}
|
130
cli/record/remove.go
Normal file
130
cli/record/remove.go
Normal file
@ -0,0 +1,130 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
abraFormatter "coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
gandiPkg "coopcloud.tech/abra/pkg/dns/gandi"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/libdns/gandi"
|
||||
"github.com/libdns/libdns"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// RecordRemoveCommand lists domains.
|
||||
var RecordRemoveCommand = &cli.Command{
|
||||
Name: "remove",
|
||||
Usage: "Remove a domain name record",
|
||||
Aliases: []string{"rm"},
|
||||
ArgsUsage: "<zone>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DNSProviderFlag,
|
||||
internal.DNSTypeFlag,
|
||||
internal.DNSNameFlag,
|
||||
},
|
||||
Description: `
|
||||
This command removes a domain name record for a specific zone.
|
||||
|
||||
It uses the type of record and name to match existing records and choose one
|
||||
for deletion. You must specify a zone (e.g. example.com) under which your
|
||||
domain name records are listed. This zone must already be created on your
|
||||
provider account.
|
||||
|
||||
Example:
|
||||
|
||||
abra record remove foo.com -p gandi -t A -n myapp
|
||||
|
||||
You may also invoke this command in "wizard" mode and be prompted for input
|
||||
|
||||
abra record rm
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
zone, err := internal.EnsureZoneArgument(c)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := internal.EnsureDNSProvider(); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
var provider gandi.Provider
|
||||
switch internal.DNSProvider {
|
||||
case "gandi":
|
||||
provider, err = gandiPkg.New()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
default:
|
||||
logrus.Fatalf("'%s' is not a supported DNS provider", internal.DNSProvider)
|
||||
}
|
||||
|
||||
if err := internal.EnsureDNSTypeFlag(c); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := internal.EnsureDNSNameFlag(c); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
records, err := provider.GetRecords(c.Context, zone)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
var toDelete libdns.Record
|
||||
for _, record := range records {
|
||||
if record.Type == internal.DNSType && record.Name == internal.DNSName {
|
||||
toDelete = record
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (libdns.Record{}) == toDelete {
|
||||
logrus.Fatal("provider library reports no matching record?")
|
||||
}
|
||||
|
||||
tableCol := []string{"type", "name", "value", "TTL", "priority"}
|
||||
table := abraFormatter.CreateTable(tableCol)
|
||||
|
||||
value := toDelete.Value
|
||||
if len(toDelete.Value) > 30 {
|
||||
value = fmt.Sprintf("%s...", toDelete.Value[:30])
|
||||
}
|
||||
|
||||
table.Append([]string{
|
||||
toDelete.Type,
|
||||
toDelete.Name,
|
||||
value,
|
||||
toDelete.TTL.String(),
|
||||
strconv.Itoa(toDelete.Priority),
|
||||
})
|
||||
|
||||
table.Render()
|
||||
|
||||
response := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: "continue with record deletion?",
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !response {
|
||||
logrus.Fatal("exiting as requested")
|
||||
}
|
||||
|
||||
_, err = provider.DeleteRecords(c.Context, zone, []libdns.Record{toDelete})
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Info("record successfully deleted")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
@ -3,33 +3,396 @@ 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"
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
dockerInstallMsg = `
|
||||
A docker installation cannot be found on %s. This is a required system
|
||||
dependency for running Co-op Cloud on your server. If you would like, Abra can
|
||||
attempt to install Docker for you using the upstream non-interactive
|
||||
installation script.
|
||||
|
||||
See the following documentation for more:
|
||||
|
||||
https://docs.docker.com/engine/install/debian/#install-using-the-convenience-script
|
||||
|
||||
N.B Docker doesn't recommend it for production environments but many use it for
|
||||
such purposes. Docker stable is now installed by default by this script. The
|
||||
source for this script can be seen here:
|
||||
|
||||
https://github.com/docker/docker-install
|
||||
|
||||
`
|
||||
)
|
||||
|
||||
var local bool
|
||||
var localFlag = &cli.BoolFlag{
|
||||
Name: "local",
|
||||
Aliases: []string{"L"},
|
||||
Aliases: []string{"l"},
|
||||
Value: false,
|
||||
Usage: "Set up the local server",
|
||||
Usage: "Use local server",
|
||||
Destination: &local,
|
||||
}
|
||||
|
||||
var provision bool
|
||||
var provisionFlag = &cli.BoolFlag{
|
||||
Name: "provision",
|
||||
Aliases: []string{"p"},
|
||||
Value: false,
|
||||
Usage: "Provision server so it can deploy apps",
|
||||
Destination: &provision,
|
||||
}
|
||||
|
||||
var sshAuth string
|
||||
var sshAuthFlag = &cli.StringFlag{
|
||||
Name: "ssh-auth",
|
||||
Aliases: []string{"sh"},
|
||||
Value: "identity-file",
|
||||
Usage: "Select SSH authentication method (identity-file, password)",
|
||||
Destination: &sshAuth,
|
||||
}
|
||||
|
||||
var askSudoPass bool
|
||||
var askSudoPassFlag = &cli.BoolFlag{
|
||||
Name: "ask-sudo-pass",
|
||||
Aliases: []string{"as"},
|
||||
Value: false,
|
||||
Usage: "Ask for sudo password",
|
||||
Destination: &askSudoPass,
|
||||
}
|
||||
|
||||
var traefik bool
|
||||
var traefikFlag = &cli.BoolFlag{
|
||||
Name: "traefik",
|
||||
Aliases: []string{"t"},
|
||||
Value: false,
|
||||
Usage: "Deploy traefik",
|
||||
Destination: &traefik,
|
||||
}
|
||||
|
||||
func cleanUp(domainName string) {
|
||||
logrus.Warnf("cleaning up context for %s", domainName)
|
||||
if err := client.DeleteContext(domainName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Warnf("cleaning up server directory for %s", domainName)
|
||||
if err := os.RemoveAll(filepath.Join(config.ABRA_SERVER_FOLDER, domainName)); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func installDockerLocal(c *cli.Context) error {
|
||||
fmt.Println(fmt.Sprintf(dockerInstallMsg, "this local server"))
|
||||
|
||||
response := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: fmt.Sprintf("attempt install docker on local server?"),
|
||||
}
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
if !response {
|
||||
logrus.Fatal("exiting as requested")
|
||||
}
|
||||
|
||||
cmd := exec.Command("bash", "-c", "curl -s https://get.docker.com | bash")
|
||||
if err := internal.RunCmd(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newLocalServer(c *cli.Context, domainName string) error {
|
||||
if err := createServerDir(domainName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cl, err := newClient(c, domainName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if provision {
|
||||
out, err := exec.Command("which", "docker").Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if string(out) == "" {
|
||||
if err := installDockerLocal(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := initSwarmLocal(c, cl, domainName); err != nil {
|
||||
if !strings.Contains(err.Error(), "proxy already exists") {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if traefik {
|
||||
if err := deployTraefik(c, cl, domainName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Info("local server has been added")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newContext(c *cli.Context, domainName, username, port string) error {
|
||||
store := client.NewDefaultDockerContextStore()
|
||||
contexts, err := store.Store.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, context := range contexts {
|
||||
if context.Name == domainName {
|
||||
logrus.Debugf("context for %s already exists", domainName)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Debugf("creating context with domain %s, username %s and port %s", domainName, username, port)
|
||||
|
||||
if err := client.CreateContext(domainName, username, port); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newClient(c *cli.Context, domainName string) (*dockerClient.Client, error) {
|
||||
cl, err := client.New(domainName)
|
||||
if err != nil {
|
||||
return &dockerClient.Client{}, err
|
||||
}
|
||||
return cl, nil
|
||||
}
|
||||
|
||||
func installDocker(c *cli.Context, cl *dockerClient.Client, sshCl *simplessh.Client, domainName string) error {
|
||||
result, err := sshCl.Exec("which docker")
|
||||
if err != nil && string(result) != "" {
|
||||
return err
|
||||
}
|
||||
|
||||
if string(result) == "" {
|
||||
fmt.Println(fmt.Sprintf(dockerInstallMsg, domainName))
|
||||
|
||||
response := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: fmt.Sprintf("attempt install docker on %s?", domainName),
|
||||
}
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
if !response {
|
||||
logrus.Fatal("exiting as requested")
|
||||
}
|
||||
|
||||
cmd := "curl -s https://get.docker.com | bash"
|
||||
|
||||
var sudoPass string
|
||||
if askSudoPass {
|
||||
prompt := &survey.Password{
|
||||
Message: "sudo password?",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &sudoPass); err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Debugf("running '%s' on %s now with sudo password", cmd, domainName)
|
||||
if err := ssh.RunSudoCmd(cmd, sudoPass, sshCl); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
logrus.Debugf("running '%s' on %s now without sudo password", cmd, domainName)
|
||||
if err := ssh.Exec(cmd, sshCl); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Infof("docker is installed on %s", domainName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initSwarmLocal(c *cli.Context, cl *dockerClient.Client, domainName string) error {
|
||||
initReq := swarm.InitRequest{ListenAddr: "0.0.0.0:2377"}
|
||||
if _, err := cl.SwarmInit(c.Context, initReq); err != nil {
|
||||
if !strings.Contains(err.Error(), "is already part of a swarm") {
|
||||
return err
|
||||
}
|
||||
logrus.Info("swarm mode already initialised on local server")
|
||||
} else {
|
||||
logrus.Infof("initialised swarm mode on local server")
|
||||
}
|
||||
|
||||
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
|
||||
if _, err := cl.NetworkCreate(c.Context, "proxy", netOpts); err != nil {
|
||||
if !strings.Contains(err.Error(), "proxy already exists") {
|
||||
return err
|
||||
}
|
||||
logrus.Info("swarm overlay network already created on local server")
|
||||
} else {
|
||||
logrus.Infof("swarm overlay network created on local server")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initSwarm(c *cli.Context, cl *dockerClient.Client, domainName string) error {
|
||||
// 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)
|
||||
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,
|
||||
}
|
||||
if _, err := cl.SwarmInit(c.Context, initReq); err != nil {
|
||||
if !strings.Contains(err.Error(), "is already part of a swarm") {
|
||||
return err
|
||||
}
|
||||
logrus.Infof("swarm mode already initialised on %s", domainName)
|
||||
} else {
|
||||
logrus.Infof("initialised swarm mode on %s", domainName)
|
||||
}
|
||||
|
||||
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
|
||||
if _, err := cl.NetworkCreate(c.Context, "proxy", netOpts); err != nil {
|
||||
if !strings.Contains(err.Error(), "proxy already exists") {
|
||||
return err
|
||||
}
|
||||
logrus.Infof("swarm overlay network already created on %s", domainName)
|
||||
} else {
|
||||
logrus.Infof("swarm overlay network created on %s", domainName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createServerDir(domainName string) error {
|
||||
if err := server.CreateServerDir(domainName); err != nil {
|
||||
if !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
logrus.Debugf("server dir for %s already created", domainName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deployTraefik(c *cli.Context, cl *dockerClient.Client, domainName string) error {
|
||||
internal.NoInput = true
|
||||
|
||||
internal.RecipeName = "traefik"
|
||||
internal.NewAppServer = domainName
|
||||
internal.Domain = fmt.Sprintf("%s.%s", "traefik", domainName)
|
||||
internal.NewAppName = fmt.Sprintf("%s_%s", "traefik", config.SanitiseAppName(domainName))
|
||||
|
||||
appEnvPath := path.Join(config.ABRA_DIR, "servers", internal.Domain, fmt.Sprintf("%s.env", internal.NewAppName))
|
||||
if _, err := os.Stat(appEnvPath); !os.IsNotExist(err) {
|
||||
fmt.Println(fmt.Sprintf(`
|
||||
You specified "--traefik/-t" and that means that Abra will now try to
|
||||
automatically create a new Traefik app on %s.
|
||||
`, internal.NewAppServer))
|
||||
|
||||
tableCol := []string{"recipe", "domain", "server", "name"}
|
||||
table := abraFormatter.CreateTable(tableCol)
|
||||
table.Append([]string{internal.RecipeName, internal.Domain, internal.NewAppServer, internal.NewAppName})
|
||||
|
||||
if err := internal.NewAction(c); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
logrus.Infof("%s already exists, not creating again", appEnvPath)
|
||||
}
|
||||
|
||||
internal.AppName = internal.NewAppName
|
||||
if err := internal.DeployAction(c); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var serverAddCommand = &cli.Command{
|
||||
Name: "add",
|
||||
Usage: "Add a new server",
|
||||
Usage: "Add a server to your configuration",
|
||||
Description: `
|
||||
This command adds a new server that abra will communicate with, to deploy apps.
|
||||
This command adds a new server to your configuration so that it can be managed
|
||||
by Abra. This can be useful when you already have a server provisioned and want
|
||||
to start running Abra commands against it.
|
||||
|
||||
This command can also provision your server ("--provision/-p") so that it is
|
||||
capable of hosting Co-op Cloud apps. Abra will default to expecting that you
|
||||
have a running ssh-agent and are using SSH keys to connect to your new server.
|
||||
Abra will also read your SSH config (matching "Host" as <domain>). SSH
|
||||
connection details precedence follows as such: command-line > SSH config >
|
||||
guessed defaults.
|
||||
|
||||
If you have no SSH key configured for this host and are instead using password
|
||||
authentication, you may pass "--ssh-auth password" to have Abra ask you for the
|
||||
password. "--ask-sudo-pass" may be passed if you run your provisioning commands
|
||||
via sudo privilege escalation.
|
||||
|
||||
If "--local" is passed, then Abra assumes that the current local server is
|
||||
intended as the target server.
|
||||
intended as the target server. This is useful when you want to have your entire
|
||||
Co-op Cloud config located on the server itself, and not on your local
|
||||
developer machine.
|
||||
|
||||
Example:
|
||||
|
||||
abra server add --local
|
||||
|
||||
Otherwise, you may specify a remote server. The <domain> argument must be a
|
||||
publicy accessible domain name which points to your server. You should have SSH
|
||||
@ -37,102 +400,111 @@ access to this server, Abra will assume port 22 and will use your current
|
||||
system username to make an initial connection. You can use the <user> and
|
||||
<port> arguments to adjust this.
|
||||
|
||||
For example:
|
||||
Example:
|
||||
|
||||
abra server add varia.zone glodemodem 12345
|
||||
abra server add --provision --traefik varia.zone glodemodem 12345
|
||||
|
||||
Abra will construct the following SSH connection string then:
|
||||
Abra will construct the following SSH connection and Docker context:
|
||||
|
||||
ssh://globemodem@varia.zone:12345
|
||||
|
||||
All communication between Abra and the server will use this SSH connection.
|
||||
|
||||
In this example, Abra will run the following operations:
|
||||
|
||||
1. Install Docker
|
||||
2. Initialise Swarm mode
|
||||
3. Deploy Traefik (core web proxy)
|
||||
|
||||
You may omit flags to avoid performing this provisioning logic.
|
||||
`,
|
||||
Aliases: []string{"a"},
|
||||
Flags: []cli.Flag{
|
||||
localFlag,
|
||||
provisionFlag,
|
||||
sshAuthFlag,
|
||||
askSudoPassFlag,
|
||||
traefikFlag,
|
||||
},
|
||||
ArgsUsage: "<domain> [<user>] [<port>]",
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.Args().Len() == 1 && !local {
|
||||
err := errors.New("missing arguments <domain> or '--local'")
|
||||
internal.ShowSubcommandHelpAndError(c, err)
|
||||
}
|
||||
|
||||
if c.Args().Get(1) != "" && local {
|
||||
if c.Args().Len() > 0 && local || !internal.ValidateSubCmdFlags(c) {
|
||||
err := errors.New("cannot use '<domain>' and '--local' together")
|
||||
internal.ShowSubcommandHelpAndError(c, err)
|
||||
}
|
||||
|
||||
domainName := "default"
|
||||
if sshAuth != "password" && sshAuth != "identity-file" {
|
||||
err := errors.New("--ssh-auth only accepts 'identity-file' or 'password'")
|
||||
internal.ShowSubcommandHelpAndError(c, err)
|
||||
}
|
||||
|
||||
if local {
|
||||
if err := server.CreateServerDir(domainName); err != nil {
|
||||
if err := newLocalServer(c, "default"); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Info("local server has been added")
|
||||
return nil
|
||||
}
|
||||
|
||||
domainName = internal.ValidateDomain(c)
|
||||
domainName, err := internal.ValidateDomain(c)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
var username string
|
||||
var port string
|
||||
|
||||
username = c.Args().Get(1)
|
||||
username := c.Args().Get(1)
|
||||
if username == "" {
|
||||
systemUser, err := user.Current()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
return err
|
||||
}
|
||||
username = systemUser.Username
|
||||
}
|
||||
|
||||
port = c.Args().Get(2)
|
||||
port := c.Args().Get(2)
|
||||
if port == "" {
|
||||
port = "22"
|
||||
}
|
||||
|
||||
store := client.NewDefaultDockerContextStore()
|
||||
contexts, err := store.Store.List()
|
||||
if err := createServerDir(domainName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := newContext(c, domainName, username, port); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
cl, err := newClient(c, domainName)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
for _, context := range contexts {
|
||||
if context.Name == domainName {
|
||||
logrus.Fatalf("server at '%s' already exists?", domainName)
|
||||
if provision {
|
||||
logrus.Debugf("attempting to construct SSH client for %s", domainName)
|
||||
sshCl, err := ssh.New(domainName, sshAuth, username, port)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
defer sshCl.Close()
|
||||
logrus.Debugf("successfully created SSH client for %s", domainName)
|
||||
|
||||
if err := installDocker(c, cl, sshCl, domainName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if err := initSwarm(c, cl, domainName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Debugf("creating context with domain '%s', username '%s' and port '%s'", domainName, username, port)
|
||||
|
||||
if err := client.CreateContext(domainName, username, port); err != nil {
|
||||
logrus.Fatal(err)
|
||||
if _, err := cl.Info(c.Context); err != nil {
|
||||
cleanUp(domainName)
|
||||
logrus.Fatalf("couldn't make a remote docker connection to %s? use --provision/-p to attempt to install", domainName)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
cl, err := client.New(domainName)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := cl.Info(ctx); err != nil {
|
||||
if strings.Contains(err.Error(), "command not found") {
|
||||
logrus.Fatalf("docker is not installed on '%s'?", domainName)
|
||||
} else {
|
||||
logrus.Fatalf("unable to make a connection to '%s'?", domainName)
|
||||
if traefik {
|
||||
if err := deployTraefik(c, cl, domainName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Debug(err)
|
||||
}
|
||||
|
||||
logrus.Debugf("remote connection to '%s' is definitely up", domainName)
|
||||
|
||||
if err := server.CreateServerDir(domainName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Infof("server at '%s' has been added", domainName)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
@ -1,81 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var serverInitCommand = &cli.Command{
|
||||
Name: "init",
|
||||
Usage: "Initialise server for deploying apps",
|
||||
Aliases: []string{"i"},
|
||||
HideHelp: true,
|
||||
ArgsUsage: "<domain>",
|
||||
Description: `
|
||||
Initialise swarm mode on the target <domain>.
|
||||
|
||||
This initialisation explicitly chooses the "single host swarm" mode which uses
|
||||
the default IPv4 address as the advertising address. This can be re-configured
|
||||
later for more advanced use cases.
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
domainName := internal.ValidateDomain(c)
|
||||
|
||||
cl, err := client.New(domainName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// https://www.privacy-handbuch.de/handbuch_93d.htm
|
||||
freifunkDNS := "5.1.66.255:53"
|
||||
|
||||
resolver := &net.Resolver{
|
||||
PreferGo: false,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
d := net.Dialer{
|
||||
Timeout: time.Millisecond * time.Duration(10000),
|
||||
}
|
||||
// comrade librehosters DNS resolver https://snopyta.org/service/dns/
|
||||
return d.DialContext(ctx, "udp", freifunkDNS)
|
||||
},
|
||||
}
|
||||
logrus.Debugf("created DNS resolver via '%s'", freifunkDNS)
|
||||
|
||||
ctx := context.Background()
|
||||
ips, err := resolver.LookupIPAddr(ctx, domainName)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if len(ips) == 0 {
|
||||
return fmt.Errorf("unable to retrieve ipv4 address for %s", domainName)
|
||||
}
|
||||
ipv4 := ips[0].IP.To4().String()
|
||||
|
||||
initReq := swarm.InitRequest{
|
||||
ListenAddr: "0.0.0.0:2377",
|
||||
AdvertiseAddr: ipv4,
|
||||
}
|
||||
if _, err := cl.SwarmInit(ctx, initReq); err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Debugf("initialised swarm on '%s'", domainName)
|
||||
|
||||
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
|
||||
if _, err := cl.NetworkCreate(ctx, "proxy", netOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Debug("swarm overlay network 'proxy' created")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
@ -6,6 +6,7 @@ import (
|
||||
"coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"github.com/docker/cli/cli/connhelper/ssh"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@ -23,7 +24,7 @@ var serverListCommand = &cli.Command{
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
tableColumns := []string{"Name", "Connection"}
|
||||
tableColumns := []string{"name", "host", "user", "port"}
|
||||
table := formatter.CreateTable(tableColumns)
|
||||
defer table.Render()
|
||||
|
||||
@ -41,14 +42,18 @@ var serverListCommand = &cli.Command{
|
||||
continue
|
||||
}
|
||||
if ctx.Name == serverName {
|
||||
row = []string{serverName, endpoint}
|
||||
sp, err := ssh.ParseURL(endpoint)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
row = []string{serverName, sp.Host, sp.User, sp.Port}
|
||||
}
|
||||
}
|
||||
if len(row) == 0 {
|
||||
if serverName == "default" {
|
||||
row = []string{serverName, "local"}
|
||||
row = []string{serverName, "local", "n/a", "n/a"}
|
||||
} else {
|
||||
row = []string{serverName, "unknown"}
|
||||
row = []string{serverName, "unknown", "unknown", "unknown"}
|
||||
}
|
||||
}
|
||||
table.Append(row)
|
||||
|
@ -1,251 +1,177 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/libcapsul"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/hetznercloud/hcloud-go/hcloud"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var hetznerCloudType string
|
||||
var hetznerCloudImage string
|
||||
var hetznerCloudSSHKeys cli.StringSlice
|
||||
var hetznerCloudLocation string
|
||||
var hetznerCloudAPIToken string
|
||||
var serverNewHetznerCloudCommand = &cli.Command{
|
||||
Name: "hetzner",
|
||||
Usage: "Create a new Hetzner virtual server",
|
||||
ArgsUsage: "<name>",
|
||||
Description: `
|
||||
Create a new Hetzner virtual server.
|
||||
func newHetznerCloudVPS(c *cli.Context) error {
|
||||
if err := internal.EnsureNewHetznerCloudVPSFlags(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
This command uses the uses the Hetzner Cloud API bindings to send a server
|
||||
creation request. You must already have a Hetzner Cloud account and an account
|
||||
API token before using this command.
|
||||
client := hcloud.NewClient(hcloud.WithToken(internal.HetznerCloudAPIToken))
|
||||
|
||||
Your token can be loaded from the environment using the HCLOUD_TOKEN
|
||||
environment variable or otherwise passing the "--env/-e" flag.
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "Server type",
|
||||
Destination: &hetznerCloudType,
|
||||
Value: "cx11",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "image",
|
||||
Aliases: []string{"i"},
|
||||
Usage: "Image type",
|
||||
Value: "debian-10",
|
||||
Destination: &hetznerCloudImage,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "ssh-keys",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "SSH keys",
|
||||
Destination: &hetznerCloudSSHKeys,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "location",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "Server location",
|
||||
Value: "hel1",
|
||||
Destination: &hetznerCloudLocation,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "token",
|
||||
Aliases: []string{"T"},
|
||||
Usage: "Hetzner Cloud API token",
|
||||
EnvVars: []string{"HCLOUD_TOKEN"},
|
||||
Destination: &hetznerCloudAPIToken,
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
name := c.Args().First()
|
||||
if name == "" {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("no name provided"))
|
||||
var sshKeysRaw []string
|
||||
var sshKeys []*hcloud.SSHKey
|
||||
for _, sshKey := range c.StringSlice("hetzner-ssh-keys") {
|
||||
if sshKey == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if hetznerCloudAPIToken == "" {
|
||||
logrus.Fatal("Hetzner Cloud API token is missing")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
client := hcloud.NewClient(hcloud.WithToken(hetznerCloudAPIToken))
|
||||
|
||||
logrus.Debugf("successfully created hetzner cloud API client")
|
||||
|
||||
var sshKeys []*hcloud.SSHKey
|
||||
for _, sshKey := range c.StringSlice("ssh-keys") {
|
||||
sshKey, _, err := client.SSHKey.GetByName(ctx, sshKey)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
sshKeys = append(sshKeys, sshKey)
|
||||
}
|
||||
|
||||
serverOpts := hcloud.ServerCreateOpts{
|
||||
Name: name,
|
||||
ServerType: &hcloud.ServerType{Name: hetznerCloudType},
|
||||
Image: &hcloud.Image{Name: hetznerCloudImage},
|
||||
SSHKeys: sshKeys,
|
||||
Location: &hcloud.Location{Name: hetznerCloudLocation},
|
||||
}
|
||||
res, _, err := client.Server.Create(ctx, serverOpts)
|
||||
sshKey, _, err := client.SSHKey.GetByName(c.Context, sshKey)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
return err
|
||||
}
|
||||
sshKeys = append(sshKeys, sshKey)
|
||||
sshKeysRaw = append(sshKeysRaw, sshKey.Name)
|
||||
}
|
||||
|
||||
logrus.Debugf("new server '%s' created", name)
|
||||
serverOpts := hcloud.ServerCreateOpts{
|
||||
Name: internal.HetznerCloudName,
|
||||
ServerType: &hcloud.ServerType{Name: internal.HetznerCloudType},
|
||||
Image: &hcloud.Image{Name: internal.HetznerCloudImage},
|
||||
SSHKeys: sshKeys,
|
||||
Location: &hcloud.Location{Name: internal.HetznerCloudLocation},
|
||||
}
|
||||
|
||||
tableColumns := []string{"Name", "IPv4", "Root Password"}
|
||||
table := formatter.CreateTable(tableColumns)
|
||||
tableColumns := []string{"name", "type", "image", "ssh-keys", "location"}
|
||||
table := formatter.CreateTable(tableColumns)
|
||||
table.Append([]string{
|
||||
internal.HetznerCloudName,
|
||||
internal.HetznerCloudType,
|
||||
internal.HetznerCloudImage,
|
||||
strings.Join(sshKeysRaw, "\n"),
|
||||
internal.HetznerCloudLocation,
|
||||
})
|
||||
table.Render()
|
||||
|
||||
if len(sshKeys) > 0 {
|
||||
table.Append([]string{name, res.Server.PublicNet.IPv4.IP.String(), "N/A (using SSH keys)"})
|
||||
} else {
|
||||
table.Append([]string{name, res.Server.PublicNet.IPv4.IP.String(), res.RootPassword})
|
||||
}
|
||||
response := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: "continue with hetzner cloud VPS creation?",
|
||||
}
|
||||
|
||||
table.Render()
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
if !response {
|
||||
logrus.Fatal("exiting as requested")
|
||||
}
|
||||
|
||||
res, _, err := client.Server.Create(c.Context, serverOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var rootPassword string
|
||||
if len(sshKeys) > 0 {
|
||||
rootPassword = "N/A (using SSH keys)"
|
||||
} else {
|
||||
rootPassword = res.RootPassword
|
||||
}
|
||||
|
||||
ip := res.Server.PublicNet.IPv4.IP.String()
|
||||
|
||||
fmt.Println(fmt.Sprintf(`
|
||||
Your new Hetzner Cloud VPS has successfully been created! Here are the details:
|
||||
|
||||
name: %s
|
||||
IP address: %s
|
||||
root password: %s
|
||||
|
||||
You can access this new VPS via SSH using the following command:
|
||||
|
||||
ssh root@%s
|
||||
|
||||
Please note, this server is not managed by Abra yet (i.e. "abra server ls" will
|
||||
not list this server)! You will need to assign a domain name record ("abra
|
||||
record new") and add the server to your Abra configuration ("abra server add")
|
||||
to have a working server that you can deploy Co-op Cloud apps to.
|
||||
`,
|
||||
internal.HetznerCloudName, ip, rootPassword,
|
||||
ip,
|
||||
))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var capsulInstance string
|
||||
var capsulType string
|
||||
var capsulImage string
|
||||
var capsulSSHKey string
|
||||
var capsulAPIToken string
|
||||
var serverNewCapsulCommand = &cli.Command{
|
||||
Name: "capsul",
|
||||
Usage: "Create a new Capsul virtual server",
|
||||
ArgsUsage: "<name>",
|
||||
Description: `
|
||||
Create a new Capsul virtual server.
|
||||
func newCapsulVPS(c *cli.Context) error {
|
||||
if err := internal.EnsureNewCapsulVPSFlags(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
This command uses the uses the Capsul API bindings of your chosen instance to
|
||||
send a server creation request. You must already have an account on your chosen
|
||||
Capsul instance before using this command.
|
||||
capsulCreateURL := fmt.Sprintf("https://%s/api/capsul/create", internal.CapsulInstanceURL)
|
||||
|
||||
Your token can be loaded from the environment using the CAPSUL_TOKEN
|
||||
environment variable or otherwise passing the "--env/-e" flag.
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "instance",
|
||||
Aliases: []string{"I"},
|
||||
Usage: "Capsul instance",
|
||||
Destination: &capsulInstance,
|
||||
Value: "yolo.servers.coop",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "Server type",
|
||||
Value: "f1-xs",
|
||||
Destination: &capsulType,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "image",
|
||||
Aliases: []string{"i"},
|
||||
Usage: "Image type",
|
||||
Value: "debian10",
|
||||
Destination: &capsulImage,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ssh-key",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "SSH key",
|
||||
Value: "",
|
||||
Destination: &capsulSSHKey,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "token",
|
||||
Aliases: []string{"T"},
|
||||
Usage: "Capsul instance API token",
|
||||
EnvVars: []string{"CAPSUL_TOKEN"},
|
||||
Destination: &capsulAPIToken,
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
name := c.Args().First()
|
||||
if name == "" {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("no name provided"))
|
||||
var sshKeys []string
|
||||
for _, sshKey := range c.StringSlice("capsul-ssh-keys") {
|
||||
if sshKey == "" {
|
||||
continue
|
||||
}
|
||||
sshKeys = append(sshKeys, sshKey)
|
||||
}
|
||||
|
||||
if capsulAPIToken == "" {
|
||||
logrus.Fatal("Capsul API token is missing")
|
||||
}
|
||||
tableColumns := []string{"instance", "name", "type", "image", "ssh-keys"}
|
||||
table := formatter.CreateTable(tableColumns)
|
||||
table.Append([]string{
|
||||
internal.CapsulInstanceURL,
|
||||
internal.CapsulName,
|
||||
internal.CapsulType,
|
||||
internal.CapsulImage,
|
||||
strings.Join(sshKeys, "\n"),
|
||||
})
|
||||
table.Render()
|
||||
|
||||
// yep, the response time is quite slow, something to fix on the Capsul side
|
||||
client := &http.Client{Timeout: 20 * time.Second}
|
||||
response := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: "continue with capsul creation?",
|
||||
}
|
||||
|
||||
capsulCreateURL := fmt.Sprintf("https://%s/api/capsul/create", capsulInstance)
|
||||
logrus.Debugf("using '%s' as capsul create url", capsulCreateURL)
|
||||
values := map[string]string{
|
||||
"name": name,
|
||||
"size": capsulType,
|
||||
"os": capsulImage,
|
||||
"ssh_key_0": capsulSSHKey,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(values)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if !response {
|
||||
logrus.Fatal("exiting as requested")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", capsulCreateURL, bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
capsulClient := libcapsul.New(capsulCreateURL, internal.CapsulAPIToken)
|
||||
resp, err := capsulClient.Create(
|
||||
internal.CapsulName,
|
||||
internal.CapsulType,
|
||||
internal.CapsulImage,
|
||||
sshKeys,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header = http.Header{
|
||||
"Content-Type": []string{"application/json"},
|
||||
"Authorization": []string{capsulAPIToken},
|
||||
}
|
||||
fmt.Println(fmt.Sprintf(`
|
||||
Your new Capsul has successfully been created! Here are the details:
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
Capsul name: %s
|
||||
Capsul ID: %v
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
logrus.Fatal(string(body))
|
||||
}
|
||||
You will need to log into your Capsul instance web interface to retrieve the IP
|
||||
address. You can learn all about how to get SSH access to your new Capsul on:
|
||||
|
||||
type capsulCreateResponse struct{ ID string }
|
||||
var resp capsulCreateResponse
|
||||
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Debugf("capsul created with ID: '%s'", resp.ID)
|
||||
%s/about-ssh
|
||||
|
||||
tableColumns := []string{"Name", "ID"}
|
||||
table := formatter.CreateTable(tableColumns)
|
||||
table.Append([]string{name, resp.ID})
|
||||
table.Render()
|
||||
Please note, this server is not managed by Abra yet (i.e. "abra server ls" will
|
||||
not list this server)! You will need to assign a domain name record ("abra
|
||||
record new") and add the server to your Abra configuration ("abra server add")
|
||||
to have a working server that you can deploy Co-op Cloud apps to.
|
||||
`, internal.CapsulName, resp.ID, internal.CapsulInstanceURL))
|
||||
|
||||
return nil
|
||||
},
|
||||
return nil
|
||||
}
|
||||
|
||||
var serverNewCommand = &cli.Command{
|
||||
@ -253,12 +179,58 @@ var serverNewCommand = &cli.Command{
|
||||
Aliases: []string{"n"},
|
||||
Usage: "Create a new server using a 3rd party provider",
|
||||
Description: `
|
||||
Use a provider plugin to create a new server which can then be used to house a
|
||||
new Co-op Cloud installation.
|
||||
This command creates a new server via a 3rd party provider.
|
||||
|
||||
The following providers are supported:
|
||||
|
||||
Capsul https://git.cyberia.club/Cyberia/capsul-flask
|
||||
Hetzner Cloud https://docs.hetzner.com/cloud
|
||||
|
||||
You may invoke this command in "wizard" mode and be prompted for input:
|
||||
|
||||
abra record new
|
||||
|
||||
API tokens are read from the environment if specified, e.g.
|
||||
|
||||
export HCLOUD_TOKEN=...
|
||||
|
||||
Where "$provider_TOKEN" is the expected env var format.
|
||||
`,
|
||||
ArgsUsage: "<provider>",
|
||||
Subcommands: []*cli.Command{
|
||||
serverNewHetznerCloudCommand,
|
||||
serverNewCapsulCommand,
|
||||
Flags: []cli.Flag{
|
||||
internal.ServerProviderFlag,
|
||||
|
||||
// Capsul
|
||||
internal.CapsulInstanceURLFlag,
|
||||
internal.CapsulTypeFlag,
|
||||
internal.CapsulImageFlag,
|
||||
internal.CapsulSSHKeysFlag,
|
||||
internal.CapsulAPITokenFlag,
|
||||
|
||||
// Hetzner
|
||||
internal.HetznerCloudNameFlag,
|
||||
internal.HetznerCloudTypeFlag,
|
||||
internal.HetznerCloudImageFlag,
|
||||
internal.HetznerCloudSSHKeysFlag,
|
||||
internal.HetznerCloudLocationFlag,
|
||||
internal.HetznerCloudAPITokenFlag,
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := internal.EnsureServerProvider(); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
switch internal.ServerProvider {
|
||||
case "capsul":
|
||||
if err := newCapsulVPS(c); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
case "hetzner-cloud":
|
||||
if err := newHetznerCloudVPS(c); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
@ -1,29 +1,158 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/hetznercloud/hcloud-go/hcloud"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var serverRemoveCommand = &cli.Command{
|
||||
Name: "remove",
|
||||
Aliases: []string{"rm"},
|
||||
Usage: "Remove a server",
|
||||
Description: `
|
||||
This does not destroy the actual server. It simply removes it from Abra
|
||||
internal bookkeeping so that it is not managed any more.
|
||||
`,
|
||||
HideHelp: true,
|
||||
Action: func(c *cli.Context) error {
|
||||
domainName := internal.ValidateDomain(c)
|
||||
var rmServer bool
|
||||
var rmServerFlag = &cli.BoolFlag{
|
||||
Name: "server",
|
||||
Aliases: []string{"s"},
|
||||
Value: false,
|
||||
Usage: "remove the actual server also",
|
||||
Destination: &rmServer,
|
||||
}
|
||||
|
||||
if err := client.DeleteContext(domainName); err != nil {
|
||||
func rmHetznerCloudVPS(c *cli.Context) error {
|
||||
if internal.HetznerCloudName == "" && !internal.NoInput {
|
||||
prompt := &survey.Input{
|
||||
Message: "specify hetzner cloud VPS name",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &internal.HetznerCloudName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if internal.HetznerCloudAPIToken == "" && !internal.NoInput {
|
||||
token, ok := os.LookupEnv("HCLOUD_TOKEN")
|
||||
if !ok {
|
||||
prompt := &survey.Input{
|
||||
Message: "specify hetzner cloud API token",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &internal.HetznerCloudAPIToken); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
internal.HetznerCloudAPIToken = token
|
||||
}
|
||||
}
|
||||
|
||||
client := hcloud.NewClient(hcloud.WithToken(internal.HetznerCloudAPIToken))
|
||||
|
||||
server, _, err := client.Server.Get(c.Context, internal.HetznerCloudName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if server == nil {
|
||||
logrus.Fatalf("library provider reports that %s doesn't exist?", internal.HetznerCloudName)
|
||||
}
|
||||
|
||||
fmt.Println(fmt.Sprintf(`
|
||||
You have requested that Abra delete the following server (%s). Please be
|
||||
absolutely sure that this is indeed the server that you would like to have
|
||||
removed. There will be no going back once you confirm, the server will be
|
||||
destroyed.
|
||||
`, server.Name))
|
||||
|
||||
tableColumns := []string{"name", "type", "image", "location"}
|
||||
table := formatter.CreateTable(tableColumns)
|
||||
table.Append([]string{
|
||||
server.Name,
|
||||
server.ServerType.Name,
|
||||
server.Image.Name,
|
||||
server.Datacenter.Name,
|
||||
})
|
||||
table.Render()
|
||||
|
||||
response := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: "continue with hetzner cloud VPS removal?",
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !response {
|
||||
logrus.Fatal("exiting as requested")
|
||||
}
|
||||
|
||||
_, err = client.Server.Delete(c.Context, server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("%s has been deleted from your hetzner cloud account", internal.HetznerCloudName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var serverRemoveCommand = &cli.Command{
|
||||
Name: "remove",
|
||||
Aliases: []string{"rm"},
|
||||
ArgsUsage: "<server>",
|
||||
Usage: "Remove a managed server",
|
||||
Description: `
|
||||
This command removes a server from Abra management.
|
||||
|
||||
Depending on whether you used a 3rd party provider to create this server ("abra
|
||||
server new"), you can also destroy the virtual server as well. Pass
|
||||
"--server/-s" to tell Abra to try to delete this VPS.
|
||||
|
||||
Otherwise, Abra will remove the internal bookkeeping (~/.abra/servers/...) and
|
||||
underlying client connection context. This server will then be lost in time,
|
||||
like tears in rain.
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
rmServerFlag,
|
||||
|
||||
// Hetzner
|
||||
internal.HetznerCloudNameFlag,
|
||||
internal.HetznerCloudAPITokenFlag,
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
serverName, err := internal.ValidateServer(c)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Infof("server at '%s' has been forgotten", domainName)
|
||||
if rmServer {
|
||||
if err := internal.EnsureServerProvider(); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
switch internal.ServerProvider {
|
||||
case "capsul":
|
||||
logrus.Warn("capsul provider does not support automatic removal yet, sorry!")
|
||||
case "hetzner-cloud":
|
||||
if err := rmHetznerCloudVPS(c); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if err := client.DeleteContext(serverName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(filepath.Join(config.ABRA_SERVER_FOLDER, serverName)); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Infof("server at '%s' has been lost in time, like tears in rain", serverName)
|
||||
|
||||
return nil
|
||||
},
|
||||
|
@ -8,17 +8,18 @@ import (
|
||||
var ServerCommand = &cli.Command{
|
||||
Name: "server",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "Manage servers",
|
||||
Usage: "Manage servers via 3rd party providers",
|
||||
Description: `
|
||||
Manage the lifecycle of a server.
|
||||
These commands support creating, managing and removing servers using 3rd party
|
||||
integrations.
|
||||
|
||||
These commands support creating new servers using 3rd party integrations,
|
||||
initialising existing servers to support Co-op Cloud deployments and managing
|
||||
the connections to those servers.
|
||||
Servers can be created from scratch using the "abra server new" command. If you
|
||||
already have a server, you can add it to your configuration using "abra server
|
||||
add". Abra can provision servers so that they are ready to deploy Co-op Cloud
|
||||
apps, see available flags on "server add" for more.
|
||||
`,
|
||||
Subcommands: []*cli.Command{
|
||||
serverNewCommand,
|
||||
serverInitCommand,
|
||||
serverAddCommand,
|
||||
serverListCommand,
|
||||
serverRemoveCommand,
|
||||
|
@ -1,15 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// VersionCommand prints the version of abra.
|
||||
var VersionCommand = &cli.Command{
|
||||
Name: "version",
|
||||
Usage: "Print version",
|
||||
Action: func(c *cli.Context) error {
|
||||
cli.VersionPrinter(c)
|
||||
return nil
|
||||
},
|
||||
}
|
10
go.mod
10
go.mod
@ -3,7 +3,7 @@ module coopcloud.tech/abra
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb
|
||||
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52
|
||||
github.com/AlecAivazis/survey/v2 v2.3.1
|
||||
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4
|
||||
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
|
||||
@ -25,16 +25,24 @@ require (
|
||||
)
|
||||
|
||||
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/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/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
|
||||
)
|
||||
|
21
go.sum
21
go.sum
@ -21,8 +21,10 @@ cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIA
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb h1:Jf+Dnna2kXcNQvcA5JMp6d2Uyvg2pIVJfip9+X5FrH0=
|
||||
coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
|
||||
coopcloud.tech/libcapsul v0.0.0-20211022074848-c35e78fe3f3e h1:o5OZInc5b9esiN4hlfjZY6u0r+qB2iSv/11jnMGuR38=
|
||||
coopcloud.tech/libcapsul v0.0.0-20211022074848-c35e78fe3f3e/go.mod h1:HEQ9pSJRsDKabMxPfYCCzpVpAreLoC4Gh4SkVyOaKvk=
|
||||
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=
|
||||
@ -251,6 +253,8 @@ 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=
|
||||
@ -497,6 +501,8 @@ 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=
|
||||
@ -510,6 +516,11 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/libdns/gandi v1.0.2 h1:1Ts8UpI1x5PVKpOjKC7Dn4+EObndz9gm6vdZnloHSKQ=
|
||||
github.com/libdns/gandi v1.0.2/go.mod h1:hxpbQKcQFgQrTS5lV4tAgn6QoL6HcCnoBJaW5nOW4Sk=
|
||||
github.com/libdns/libdns v0.1.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
||||
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
|
||||
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
||||
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
@ -632,6 +643,8 @@ 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=
|
||||
@ -686,6 +699,8 @@ 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=
|
||||
@ -812,6 +827,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-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=
|
||||
@ -958,6 +974,7 @@ 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=
|
||||
|
@ -5,8 +5,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/client/stack"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
apiclient "github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
@ -4,37 +4,43 @@ package client
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// New initiates a new Docker client.
|
||||
func New(contextName string) (*client.Client, error) {
|
||||
context, err := GetContext(contextName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctxEndpoint, err := GetContextEndpoint(context)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
helper := newConnectionHelper(ctxEndpoint)
|
||||
httpClient := &http.Client{
|
||||
// No tls, no proxy
|
||||
Transport: &http.Transport{
|
||||
DialContext: helper.Dialer,
|
||||
},
|
||||
}
|
||||
|
||||
var clientOpts []client.Opt
|
||||
clientOpts = append(clientOpts,
|
||||
client.WithHTTPClient(httpClient),
|
||||
client.WithHost(helper.Host),
|
||||
client.WithDialContext(helper.Dialer),
|
||||
)
|
||||
|
||||
if contextName != "default" {
|
||||
context, err := GetContext(contextName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctxEndpoint, err := GetContextEndpoint(context)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
helper := commandconnPkg.NewConnectionHelper(ctxEndpoint)
|
||||
httpClient := &http.Client{
|
||||
// No tls, no proxy
|
||||
Transport: &http.Transport{
|
||||
DialContext: helper.Dialer,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
clientOpts = append(clientOpts,
|
||||
client.WithHTTPClient(httpClient),
|
||||
client.WithHost(helper.Host),
|
||||
client.WithDialContext(helper.Dialer),
|
||||
)
|
||||
}
|
||||
|
||||
version := os.Getenv("DOCKER_API_VERSION")
|
||||
if version != "" {
|
||||
@ -45,7 +51,7 @@ func New(contextName string) (*client.Client, error) {
|
||||
|
||||
cl, err := client.NewClientWithOpts(clientOpts...)
|
||||
if err != nil {
|
||||
logrus.Fatalf("unable to create Docker client: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logrus.Debugf("created client for '%s'", contextName)
|
||||
|
46
pkg/client/client_test.go
Normal file
46
pkg/client/client_test.go
Normal file
@ -0,0 +1,46 @@
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/docker/cli/cli/context/docker"
|
||||
dCliContextStore "github.com/docker/cli/cli/context/store"
|
||||
dClient "github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func newConnectionHelper(daemonURL string) *connhelper.ConnectionHelper {
|
||||
helper, err := connhelper.GetConnectionHelper(daemonURL)
|
||||
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
return helper
|
||||
}
|
||||
|
||||
func getDockerEndpoint(host string) (docker.Endpoint, error) {
|
||||
skipTLSVerify := false
|
||||
ep := docker.Endpoint{
|
||||
EndpointMeta: docker.EndpointMeta{
|
||||
Host: host,
|
||||
SkipTLSVerify: skipTLSVerify,
|
||||
},
|
||||
}
|
||||
// try to resolve a docker client, validating the configuration
|
||||
opts, err := ep.ClientOpts()
|
||||
if err != nil {
|
||||
return docker.Endpoint{}, err
|
||||
}
|
||||
if _, err := dClient.NewClientWithOpts(opts...); err != nil {
|
||||
return docker.Endpoint{}, err
|
||||
}
|
||||
return ep, nil
|
||||
}
|
||||
|
||||
func getDockerEndpointMetadataAndTLS(host string) (docker.EndpointMeta, *dCliContextStore.EndpointTLSData, error) {
|
||||
ep, err := getDockerEndpoint(host)
|
||||
if err != nil {
|
||||
return docker.EndpointMeta{}, nil, err
|
||||
}
|
||||
return ep.EndpointMeta, ep.TLSData.ToStoreTLSData(), nil
|
||||
}
|
@ -1,191 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2013-2017 Docker, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
@ -1,7 +0,0 @@
|
||||
# github.com/docker/cli/cli/command/container
|
||||
|
||||
Due to this literally just being copy-pasted from the lib, the Apache license
|
||||
will be posted in this folder. Small edits to the source code have been to
|
||||
function names and parts we don't need deleted.
|
||||
|
||||
Same vibe as [../convert](../convert).
|
@ -4,11 +4,13 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
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"
|
||||
)
|
||||
@ -43,7 +45,7 @@ func createContext(name string, host string) error {
|
||||
Endpoints: make(map[string]contextStore.EndpointTLSData),
|
||||
}
|
||||
|
||||
dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(host)
|
||||
dockerEP, dockerTLS, err := commandconnPkg.GetDockerEndpointMetadataAndTLS(host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -94,7 +96,6 @@ func GetContext(contextName string) (contextStore.Metadata, error) {
|
||||
}
|
||||
|
||||
func GetContextEndpoint(ctx contextStore.Metadata) (string, error) {
|
||||
// safe to use docker key hardcoded since abra doesn't use k8s... yet...
|
||||
endpointmeta, ok := ctx.Endpoints["docker"].(context.EndpointMetaBase)
|
||||
if !ok {
|
||||
err := errors.New("context lacks Docker endpoint")
|
||||
@ -108,21 +109,20 @@ func newContextStore(dir string, config contextStore.Config) contextStore.Store
|
||||
}
|
||||
|
||||
func NewDefaultDockerContextStore() *command.ContextStoreWithDefault {
|
||||
// Grabbing the stderr from Docker commands
|
||||
// Much easier to fit this into the code we are using to replicate docker cli commands
|
||||
_, _, stderr := term.StdStreams()
|
||||
// TODO: Look into custom docker configs in case users want that
|
||||
dockerConfig := dConfig.LoadDefaultConfigFile(stderr)
|
||||
contextDir := dConfig.ContextStoreDir()
|
||||
storeConfig := command.DefaultContextStoreConfig()
|
||||
store := newContextStore(contextDir, storeConfig)
|
||||
|
||||
opts := &cliflags.CommonOptions{Context: "default"}
|
||||
|
||||
dockerContextStore := &command.ContextStoreWithDefault{
|
||||
Store: store,
|
||||
Resolver: func() (*command.DefaultContext, error) {
|
||||
// nil for the Opts because it works without it and its a cli thing
|
||||
return command.ResolveDefaultContext(nil, dockerConfig, storeConfig, stderr)
|
||||
return command.ResolveDefaultContext(opts, dockerConfig, storeConfig, stderr)
|
||||
},
|
||||
}
|
||||
|
||||
return dockerContextStore
|
||||
}
|
||||
|
@ -30,6 +30,34 @@ func dockerContext(host, key string) TestContext {
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
|
@ -1,191 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2013-2017 Docker, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
@ -1,10 +0,0 @@
|
||||
# github.com/docker/cli/cli/compose/convert
|
||||
|
||||
DISCLAIMER: This is like the entire `github.com/docker/cli/cli/compose/convert`
|
||||
package. This should be an easy import but importing it creates DEPENDENCY
|
||||
HELL. I tried for an hour to fix it but it would work. TRY TO FIX AT YOUR OWN
|
||||
RISK!!!
|
||||
|
||||
Due to this literally just being copy-pasted from the lib, the Apache license
|
||||
will be posted in this folder. Small edits to the source code have been to
|
||||
function names and parts we don't need deleted.
|
@ -7,9 +7,9 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/client/stack"
|
||||
loader "coopcloud.tech/abra/pkg/client/stack"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
loader "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
@ -9,9 +9,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/pkg/client/convert"
|
||||
loader "coopcloud.tech/abra/pkg/client/stack"
|
||||
stack "coopcloud.tech/abra/pkg/client/stack"
|
||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||
loader "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
28
pkg/dns/common.go
Normal file
28
pkg/dns/common.go
Normal file
@ -0,0 +1,28 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// NewToken constructs a new DNS provider token.
|
||||
func NewToken(provider, providerTokenEnvVar string) (string, error) {
|
||||
if token, present := os.LookupEnv(providerTokenEnvVar); present {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
logrus.Debugf("no %s in environment, asking via stdin", providerTokenEnvVar)
|
||||
|
||||
var token string
|
||||
prompt := &survey.Input{
|
||||
Message: fmt.Sprintf("%s API token?", provider),
|
||||
}
|
||||
if err := survey.AskOne(prompt, &token); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
15
pkg/dns/gandi/gandi.go
Normal file
15
pkg/dns/gandi/gandi.go
Normal file
@ -0,0 +1,15 @@
|
||||
package gandi
|
||||
|
||||
import (
|
||||
"coopcloud.tech/abra/pkg/dns"
|
||||
"github.com/libdns/gandi"
|
||||
)
|
||||
|
||||
// New constructs a new DNs provider.
|
||||
func New() (gandi.Provider, error) {
|
||||
token, err := dns.NewToken("Gandi", "GANDI_TOKEN")
|
||||
if err != nil {
|
||||
return gandi.Provider{}, err
|
||||
}
|
||||
return gandi.Provider{APIToken: token}, nil
|
||||
}
|
@ -3,6 +3,7 @@ package git
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
@ -24,6 +25,10 @@ func Clone(dir, url string) error {
|
||||
ReferenceName: plumbing.ReferenceName("refs/heads/main"),
|
||||
})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "authentication required") {
|
||||
name := filepath.Base(dir)
|
||||
return fmt.Errorf("unable to clone %s, does %s exist?", name, url)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
55
pkg/git/read.go
Normal file
55
pkg/git/read.go
Normal file
@ -0,0 +1,55 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// GetRecipeHead retrieves latest HEAD metadata.
|
||||
func GetRecipeHead(recipeName string) (*plumbing.Reference, error) {
|
||||
recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName)
|
||||
|
||||
repo, err := git.PlainOpen(recipeDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
head, err := repo.Head()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return head, nil
|
||||
}
|
||||
|
||||
// IsClean checks if a repo has unstaged changes
|
||||
func IsClean(recipeName string) (bool, error) {
|
||||
recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName)
|
||||
|
||||
repo, err := git.PlainOpen(recipeDir)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
status, err := worktree.Status()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if status.String() != "" {
|
||||
logrus.Debugf("discovered git status for %s repository: %s", recipeName, status.String())
|
||||
} else {
|
||||
logrus.Debugf("discovered clean git status for %s repository", recipeName)
|
||||
}
|
||||
|
||||
return status.IsClean(), nil
|
||||
}
|
20
pkg/limit/limit.go
Normal file
20
pkg/limit/limit.go
Normal file
@ -0,0 +1,20 @@
|
||||
package limit // https://github.com/tidwall/limiter
|
||||
|
||||
// Limiter is for limiting the number of concurrent operations. This
|
||||
type Limiter struct{ sem chan struct{} }
|
||||
|
||||
// New returns a new Limiter. The limit param is the maximum number of
|
||||
// concurrent operations.
|
||||
func New(limit int) *Limiter {
|
||||
return &Limiter{make(chan struct{}, limit)}
|
||||
}
|
||||
|
||||
// Begin an operation.
|
||||
func (l *Limiter) Begin() {
|
||||
l.sem <- struct{}{}
|
||||
}
|
||||
|
||||
// End the operation.
|
||||
func (l *Limiter) End() {
|
||||
<-l.sem
|
||||
}
|
@ -6,11 +6,11 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/client/stack"
|
||||
loader "coopcloud.tech/abra/pkg/client/stack"
|
||||
"coopcloud.tech/abra/pkg/compose"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
loader "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
@ -109,6 +109,15 @@ func EnsureExists(recipe string) error {
|
||||
func EnsureVersion(recipeName, version string) error {
|
||||
recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName)
|
||||
|
||||
isClean, err := gitPkg.IsClean(recipeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isClean {
|
||||
return fmt.Errorf("'%s' has locally unstaged changes", recipeName)
|
||||
}
|
||||
|
||||
repo, err := git.PlainOpen(recipeDir)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -160,6 +169,15 @@ func EnsureVersion(recipeName, version string) error {
|
||||
func EnsureLatest(recipeName string) error {
|
||||
recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName)
|
||||
|
||||
isClean, err := gitPkg.IsClean(recipeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isClean {
|
||||
return fmt.Errorf("'%s' has locally unstaged changes", recipeName)
|
||||
}
|
||||
|
||||
logrus.Debugf("attempting to open git repository in '%s'", recipeDir)
|
||||
|
||||
repo, err := git.PlainOpen(recipeDir)
|
||||
@ -195,3 +213,26 @@ func EnsureLatest(recipeName string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChaosVersion constructs a chaos mode recipe version.
|
||||
func ChaosVersion(recipeName string) (string, error) {
|
||||
var version string
|
||||
|
||||
head, err := gitPkg.GetRecipeHead(recipeName)
|
||||
if err != nil {
|
||||
return version, err
|
||||
}
|
||||
|
||||
version = head.String()[:8]
|
||||
|
||||
isClean, err := gitPkg.IsClean(recipeName)
|
||||
if err != nil {
|
||||
return version, err
|
||||
}
|
||||
|
||||
if !isClean {
|
||||
version = fmt.Sprintf("%s + unstaged changes", version)
|
||||
}
|
||||
|
||||
return version, nil
|
||||
}
|
||||
|
@ -17,8 +17,11 @@ func CreateServerDir(serverName string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("'%s' already exists, moving on...", serverPath)
|
||||
logrus.Infof("%s already exists", serverPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
logrus.Infof("successfully created %s", serverPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
226
pkg/ssh/ssh.go
Normal file
226
pkg/ssh/ssh.go
Normal file
@ -0,0 +1,226 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/user"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/kevinburke/ssh_config"
|
||||
"github.com/sfreiberg/simplessh"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// HostConfig is a SSH host config.
|
||||
type HostConfig struct {
|
||||
Host string
|
||||
IdentityFile string
|
||||
Port string
|
||||
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()
|
||||
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")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// New creates a new SSH client connection.
|
||||
func New(domainName, sshAuth, username, port string) (*simplessh.Client, error) {
|
||||
var client *simplessh.Client
|
||||
|
||||
hostConfig, err := GetHostConfig(domainName, username, port)
|
||||
if err != nil {
|
||||
return client, err
|
||||
}
|
||||
|
||||
if sshAuth == "identity-file" {
|
||||
var err error
|
||||
client, err = simplessh.ConnectWithAgentTimeout(hostConfig.Host, hostConfig.User, 5*time.Second)
|
||||
if err != nil {
|
||||
return client, err
|
||||
}
|
||||
} else {
|
||||
password := ""
|
||||
prompt := &survey.Password{
|
||||
Message: "SSH password?",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &password); err != nil {
|
||||
return client, err
|
||||
}
|
||||
|
||||
var err error
|
||||
client, err = simplessh.ConnectWithPasswordTimeout(hostConfig.Host, hostConfig.User, password, 5*time.Second)
|
||||
if err != nil {
|
||||
return client, err
|
||||
}
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// sudoWriter supports sudo command handling.
|
||||
// https://github.com/sfreiberg/simplessh/blob/master/simplessh.go
|
||||
type sudoWriter struct {
|
||||
b bytes.Buffer
|
||||
pw string
|
||||
stdin io.Writer
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
// Write satisfies the write interface for sudoWriter.
|
||||
// https://github.com/sfreiberg/simplessh/blob/master/simplessh.go
|
||||
func (w *sudoWriter) Write(p []byte) (int, error) {
|
||||
if string(p) == "sudo_password" {
|
||||
w.stdin.Write([]byte(w.pw + "\n"))
|
||||
w.pw = ""
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
w.m.Lock()
|
||||
defer w.m.Unlock()
|
||||
|
||||
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 {
|
||||
session, err := cl.SSHClient.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
cmd = "sudo -p " + "sudo_password" + " -S " + cmd
|
||||
|
||||
w := &sudoWriter{
|
||||
pw: passwd,
|
||||
}
|
||||
w.stdin, err = session.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.Stdout = w
|
||||
session.Stderr = w
|
||||
|
||||
done := make(chan struct{})
|
||||
scanner := bufio.NewScanner(session.Stdin)
|
||||
|
||||
go func() {
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
fmt.Println(line)
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
if err := session.Start(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
<-done
|
||||
|
||||
if err := session.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 {
|
||||
session, err := cl.SSHClient.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
stdout, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stderr, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stdoutDone := make(chan struct{})
|
||||
stdoutScanner := bufio.NewScanner(stdout)
|
||||
|
||||
go func() {
|
||||
for stdoutScanner.Scan() {
|
||||
line := stdoutScanner.Text()
|
||||
fmt.Println(line)
|
||||
}
|
||||
stdoutDone <- struct{}{}
|
||||
}()
|
||||
|
||||
stderrDone := make(chan struct{})
|
||||
stderrScanner := bufio.NewScanner(stderr)
|
||||
|
||||
go func() {
|
||||
for stderrScanner.Scan() {
|
||||
line := stderrScanner.Text()
|
||||
fmt.Println(line)
|
||||
}
|
||||
stderrDone <- struct{}{}
|
||||
}()
|
||||
|
||||
if err := session.Start(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
<-stdoutDone
|
||||
<-stderrDone
|
||||
|
||||
if err := session.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
295
pkg/upstream/commandconn/commandconn.go
Normal file
295
pkg/upstream/commandconn/commandconn.go
Normal file
@ -0,0 +1,295 @@
|
||||
// Package commandconn provides a net.Conn implementation that can be used for
|
||||
// proxying (or emulating) stream via a custom command.
|
||||
//
|
||||
// For example, to provide an http.Client that can connect to a Docker daemon
|
||||
// running in a Docker container ("DIND"):
|
||||
//
|
||||
// httpClient := &http.Client{
|
||||
// Transport: &http.Transport{
|
||||
// DialContext: func(ctx context.Context, _network, _addr string) (net.Conn, error) {
|
||||
// return commandconn.New(ctx, "docker", "exec", "-it", containerID, "docker", "system", "dial-stdio")
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
|
||||
package commandconn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
exec "golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
func createSession(cmd *exec.Cmd) {
|
||||
// for supporting ssh connection helper with ProxyCommand
|
||||
// https://github.com/docker/cli/issues/1707
|
||||
cmd.SysProcAttr.Setsid = true
|
||||
}
|
||||
|
||||
// New returns net.Conn
|
||||
func New(ctx context.Context, cmd string, args ...string) (net.Conn, error) {
|
||||
var (
|
||||
c commandConn
|
||||
err error
|
||||
)
|
||||
c.cmd = exec.CommandContext(ctx, cmd, args...)
|
||||
// we assume that args never contains sensitive information
|
||||
logrus.Debugf("commandconn: starting %s with %v", cmd, args)
|
||||
c.cmd.Env = os.Environ()
|
||||
c.cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||
setPdeathsig(c.cmd)
|
||||
createSession(c.cmd)
|
||||
c.stdin, err = c.cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.stdout, err = c.cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.cmd.Stderr = &stderrWriter{
|
||||
stderrMu: &c.stderrMu,
|
||||
stderr: &c.stderr,
|
||||
debugPrefix: fmt.Sprintf("commandconn (%s):", cmd),
|
||||
}
|
||||
c.localAddr = dummyAddr{network: "dummy", s: "dummy-0"}
|
||||
c.remoteAddr = dummyAddr{network: "dummy", s: "dummy-1"}
|
||||
return &c, c.cmd.Start()
|
||||
}
|
||||
|
||||
// commandConn implements net.Conn
|
||||
type commandConn struct {
|
||||
cmd *exec.Cmd
|
||||
cmdExited bool
|
||||
cmdWaitErr error
|
||||
cmdMutex sync.Mutex
|
||||
stdin io.WriteCloser
|
||||
stdout io.ReadCloser
|
||||
stderrMu sync.Mutex
|
||||
stderr bytes.Buffer
|
||||
stdioClosedMu sync.Mutex // for stdinClosed and stdoutClosed
|
||||
stdinClosed bool
|
||||
stdoutClosed bool
|
||||
localAddr net.Addr
|
||||
remoteAddr net.Addr
|
||||
}
|
||||
|
||||
// killIfStdioClosed kills the cmd if both stdin and stdout are closed.
|
||||
func (c *commandConn) killIfStdioClosed() error {
|
||||
c.stdioClosedMu.Lock()
|
||||
stdioClosed := c.stdoutClosed && c.stdinClosed
|
||||
c.stdioClosedMu.Unlock()
|
||||
if !stdioClosed {
|
||||
return nil
|
||||
}
|
||||
return c.kill()
|
||||
}
|
||||
|
||||
// killAndWait tries sending SIGTERM to the process before sending SIGKILL.
|
||||
func killAndWait(cmd *exec.Cmd) error {
|
||||
var werr error
|
||||
if runtime.GOOS != "windows" {
|
||||
werrCh := make(chan error)
|
||||
go func() { werrCh <- cmd.Wait() }()
|
||||
cmd.Process.Signal(syscall.SIGTERM)
|
||||
select {
|
||||
case werr = <-werrCh:
|
||||
case <-time.After(3 * time.Second):
|
||||
cmd.Process.Kill()
|
||||
werr = <-werrCh
|
||||
}
|
||||
} else {
|
||||
cmd.Process.Kill()
|
||||
werr = cmd.Wait()
|
||||
}
|
||||
return werr
|
||||
}
|
||||
|
||||
// kill returns nil if the command terminated, regardless to the exit status.
|
||||
func (c *commandConn) kill() error {
|
||||
var werr error
|
||||
c.cmdMutex.Lock()
|
||||
if c.cmdExited {
|
||||
werr = c.cmdWaitErr
|
||||
} else {
|
||||
werr = killAndWait(c.cmd)
|
||||
c.cmdWaitErr = werr
|
||||
c.cmdExited = true
|
||||
}
|
||||
c.cmdMutex.Unlock()
|
||||
if werr == nil {
|
||||
return nil
|
||||
}
|
||||
wExitErr, ok := werr.(*exec.ExitError)
|
||||
if ok {
|
||||
if wExitErr.ProcessState.Exited() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.Wrapf(werr, "commandconn: failed to wait")
|
||||
}
|
||||
|
||||
func (c *commandConn) onEOF(eof error) error {
|
||||
// when we got EOF, the command is going to be terminated
|
||||
var werr error
|
||||
c.cmdMutex.Lock()
|
||||
if c.cmdExited {
|
||||
werr = c.cmdWaitErr
|
||||
} else {
|
||||
werrCh := make(chan error)
|
||||
go func() { werrCh <- c.cmd.Wait() }()
|
||||
select {
|
||||
case werr = <-werrCh:
|
||||
c.cmdWaitErr = werr
|
||||
c.cmdExited = true
|
||||
case <-time.After(10 * time.Second):
|
||||
c.cmdMutex.Unlock()
|
||||
c.stderrMu.Lock()
|
||||
stderr := c.stderr.String()
|
||||
c.stderrMu.Unlock()
|
||||
return errors.Errorf("command %v did not exit after %v: stderr=%q", c.cmd.Args, eof, stderr)
|
||||
}
|
||||
}
|
||||
c.cmdMutex.Unlock()
|
||||
if werr == nil {
|
||||
return eof
|
||||
}
|
||||
c.stderrMu.Lock()
|
||||
stderr := c.stderr.String()
|
||||
c.stderrMu.Unlock()
|
||||
return errors.Errorf("command %v has exited with %v, please make sure the URL is valid, and Docker 18.09 or later is installed on the remote host: stderr=%s", c.cmd.Args, werr, stderr)
|
||||
}
|
||||
|
||||
func ignorableCloseError(err error) bool {
|
||||
errS := err.Error()
|
||||
ss := []string{
|
||||
os.ErrClosed.Error(),
|
||||
}
|
||||
for _, s := range ss {
|
||||
if strings.Contains(errS, s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *commandConn) CloseRead() error {
|
||||
// NOTE: maybe already closed here
|
||||
if err := c.stdout.Close(); err != nil && !ignorableCloseError(err) {
|
||||
// TODO: muted because https://github.com/docker/compose/issues/8544
|
||||
// logrus.Warnf("commandConn.CloseRead: %v", err)
|
||||
}
|
||||
c.stdioClosedMu.Lock()
|
||||
c.stdoutClosed = true
|
||||
c.stdioClosedMu.Unlock()
|
||||
if err := c.killIfStdioClosed(); err != nil {
|
||||
// TODO: muted because https://github.com/docker/compose/issues/8544
|
||||
// logrus.Warnf("commandConn.CloseRead: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *commandConn) Read(p []byte) (int, error) {
|
||||
n, err := c.stdout.Read(p)
|
||||
if err == io.EOF {
|
||||
err = c.onEOF(err)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (c *commandConn) CloseWrite() error {
|
||||
// NOTE: maybe already closed here
|
||||
if err := c.stdin.Close(); err != nil && !ignorableCloseError(err) {
|
||||
// TODO: muted because https://github.com/docker/compose/issues/8544
|
||||
// logrus.Warnf("commandConn.CloseWrite: %v", err)
|
||||
}
|
||||
c.stdioClosedMu.Lock()
|
||||
c.stdinClosed = true
|
||||
c.stdioClosedMu.Unlock()
|
||||
if err := c.killIfStdioClosed(); err != nil {
|
||||
// TODO: muted because https://github.com/docker/compose/issues/8544
|
||||
// logrus.Warnf("commandConn.CloseWrite: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *commandConn) Write(p []byte) (int, error) {
|
||||
n, err := c.stdin.Write(p)
|
||||
if err == io.EOF {
|
||||
err = c.onEOF(err)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (c *commandConn) Close() error {
|
||||
var err error
|
||||
if err = c.CloseRead(); err != nil {
|
||||
logrus.Warnf("commandConn.Close: CloseRead: %v", err)
|
||||
}
|
||||
if err = c.CloseWrite(); err != nil {
|
||||
// TODO: muted because https://github.com/docker/compose/issues/8544
|
||||
// logrus.Warnf("commandConn.Close: CloseWrite: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *commandConn) LocalAddr() net.Addr {
|
||||
return c.localAddr
|
||||
}
|
||||
func (c *commandConn) RemoteAddr() net.Addr {
|
||||
return c.remoteAddr
|
||||
}
|
||||
func (c *commandConn) SetDeadline(t time.Time) error {
|
||||
logrus.Debugf("unimplemented call: SetDeadline(%v)", t)
|
||||
return nil
|
||||
}
|
||||
func (c *commandConn) SetReadDeadline(t time.Time) error {
|
||||
logrus.Debugf("unimplemented call: SetReadDeadline(%v)", t)
|
||||
return nil
|
||||
}
|
||||
func (c *commandConn) SetWriteDeadline(t time.Time) error {
|
||||
logrus.Debugf("unimplemented call: SetWriteDeadline(%v)", t)
|
||||
return nil
|
||||
}
|
||||
|
||||
type dummyAddr struct {
|
||||
network string
|
||||
s string
|
||||
}
|
||||
|
||||
func (d dummyAddr) Network() string {
|
||||
return d.network
|
||||
}
|
||||
|
||||
func (d dummyAddr) String() string {
|
||||
return d.s
|
||||
}
|
||||
|
||||
type stderrWriter struct {
|
||||
stderrMu *sync.Mutex
|
||||
stderr *bytes.Buffer
|
||||
debugPrefix string
|
||||
}
|
||||
|
||||
func (w *stderrWriter) Write(p []byte) (int, error) {
|
||||
logrus.Debugf("%s%s", w.debugPrefix, string(p))
|
||||
w.stderrMu.Lock()
|
||||
if w.stderr.Len() > 4096 {
|
||||
w.stderr.Reset()
|
||||
}
|
||||
n, err := w.stderr.Write(p)
|
||||
w.stderrMu.Unlock()
|
||||
return n, err
|
||||
}
|
82
pkg/upstream/commandconn/connection.go
Normal file
82
pkg/upstream/commandconn/connection.go
Normal file
@ -0,0 +1,82 @@
|
||||
package commandconn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/url"
|
||||
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/docker/cli/cli/connhelper/ssh"
|
||||
"github.com/docker/cli/cli/context/docker"
|
||||
dCliContextStore "github.com/docker/cli/cli/context/store"
|
||||
dClient "github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// GetConnectionHelper returns Docker-specific connection helper for the given URL.
|
||||
// GetConnectionHelper returns nil without error when no helper is registered for the scheme.
|
||||
//
|
||||
// ssh://<user>@<host> URL requires Docker 18.09 or later on the remote host.
|
||||
func GetConnectionHelper(daemonURL string) (*connhelper.ConnectionHelper, error) {
|
||||
return getConnectionHelper(daemonURL, []string{"-o ConnectTimeout=5"})
|
||||
}
|
||||
|
||||
func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.ConnectionHelper, error) {
|
||||
u, err := url.Parse(daemonURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch scheme := u.Scheme; scheme {
|
||||
case "ssh":
|
||||
sp, err := ssh.ParseURL(daemonURL)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "ssh host connection is not valid")
|
||||
}
|
||||
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")...)...)
|
||||
},
|
||||
Host: "http://docker.example.com",
|
||||
}, nil
|
||||
}
|
||||
// Future version may support plugins via ~/.docker/config.json. e.g. "dind"
|
||||
// See docker/cli#889 for the previous discussion.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func NewConnectionHelper(daemonURL string) *connhelper.ConnectionHelper {
|
||||
helper, err := GetConnectionHelper(daemonURL)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
return helper
|
||||
}
|
||||
|
||||
func getDockerEndpoint(host string) (docker.Endpoint, error) {
|
||||
skipTLSVerify := false
|
||||
ep := docker.Endpoint{
|
||||
EndpointMeta: docker.EndpointMeta{
|
||||
Host: host,
|
||||
SkipTLSVerify: skipTLSVerify,
|
||||
},
|
||||
}
|
||||
// try to resolve a docker client, validating the configuration
|
||||
opts, err := ep.ClientOpts()
|
||||
if err != nil {
|
||||
return docker.Endpoint{}, err
|
||||
}
|
||||
if _, err := dClient.NewClientWithOpts(opts...); err != nil {
|
||||
return docker.Endpoint{}, err
|
||||
}
|
||||
return ep, nil
|
||||
}
|
||||
|
||||
func GetDockerEndpointMetadataAndTLS(host string) (docker.EndpointMeta, *dCliContextStore.EndpointTLSData, error) {
|
||||
ep, err := getDockerEndpoint(host)
|
||||
if err != nil {
|
||||
return docker.EndpointMeta{}, nil, err
|
||||
}
|
||||
return ep.EndpointMeta, ep.TLSData.ToStoreTLSData(), nil
|
||||
}
|
10
pkg/upstream/commandconn/pdeathsig_linux.go
Normal file
10
pkg/upstream/commandconn/pdeathsig_linux.go
Normal file
@ -0,0 +1,10 @@
|
||||
package commandconn
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func setPdeathsig(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr.Pdeathsig = syscall.SIGKILL
|
||||
}
|
11
pkg/upstream/commandconn/pdeathsig_nolinux.go
Normal file
11
pkg/upstream/commandconn/pdeathsig_nolinux.go
Normal file
@ -0,0 +1,11 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
package commandconn
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func setPdeathsig(cmd *exec.Cmd) {
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package container
|
||||
package container // https://github.com/docker/cli/blob/master/cli/command/container/exec.go
|
||||
|
||||
import (
|
||||
"context"
|
@ -1,4 +1,4 @@
|
||||
package container
|
||||
package container // https://github.com/docker/cli/blob/master/cli/command/container/hijack.go
|
||||
|
||||
import (
|
||||
"context"
|
@ -1,4 +1,4 @@
|
||||
package container
|
||||
package container // https://github.com/docker/cli/blob/master/cli/command/container/tty.go
|
||||
|
||||
import (
|
||||
"context"
|
@ -1,4 +1,4 @@
|
||||
package convert
|
||||
package convert // https://github.com/docker/cli/blob/master/cli/command/container/tty.go
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
@ -1,4 +1,4 @@
|
||||
package convert
|
||||
package convert // https://github.com/docker/cli/blob/master/cli/compose/convert/compose_test.go
|
||||
|
||||
import (
|
||||
"testing"
|
@ -1,4 +1,4 @@
|
||||
package convert
|
||||
package convert // https://github.com/docker/cli/blob/master/cli/compose/convert/service.go
|
||||
|
||||
import (
|
||||
"context"
|
@ -1,4 +1,4 @@
|
||||
package convert
|
||||
package convert // https://github.com/docker/cli/blob/master/cli/compose/convert/service_test.go
|
||||
|
||||
import (
|
||||
"context"
|
@ -1,4 +1,4 @@
|
||||
package convert
|
||||
package convert // https://github.com/docker/cli/blob/master/cli/compose/convert/volume.go
|
||||
|
||||
import (
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
@ -1,4 +1,4 @@
|
||||
package convert
|
||||
package convert // https://github.com/docker/cli/blob/master/cli/compose/convert/volume_test.go
|
||||
|
||||
import (
|
||||
"testing"
|
@ -1,4 +1,4 @@
|
||||
package stack
|
||||
package stack // https://github.com/docker/cli/blob/master/cli/command/stack/loader/loader.go
|
||||
|
||||
import (
|
||||
"fmt"
|
@ -1,4 +1,4 @@
|
||||
package stack
|
||||
package stack // https://github.com/docker/cli/blob/master/cli/command/stack/options/opts.go
|
||||
|
||||
// Deploy holds docker stack deploy options
|
||||
type Deploy struct {
|
@ -1,4 +1,4 @@
|
||||
package stack
|
||||
package stack // https://github.com/docker/cli/blob/master/cli/command/stack/remove.go
|
||||
|
||||
import (
|
||||
"context"
|
@ -1,4 +1,4 @@
|
||||
package stack
|
||||
package stack // https://github.com/docker/cli/blob/master/cli/command/stack/swarm/common.go
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
abraClient "coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/client/convert"
|
||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ABRA_VERSION="0.2.0-alpha"
|
||||
ABRA_VERSION="0.3.0-alpha"
|
||||
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION"
|
||||
|
||||
function show_banner {
|
||||
@ -18,6 +18,13 @@ function show_banner {
|
||||
echo ""
|
||||
}
|
||||
|
||||
function print_checksum_error {
|
||||
echo "$(tput setaf 1)ERROR: the checksum of downloaded file doesn't match the checksum in release!!! Either the file was corrupted during download or the file has been changed during transport!$(tput sgr0)"
|
||||
echo "expected checksum: $checksum"
|
||||
echo "checksum of downloaded file: $localsum"
|
||||
echo "abra was NOT installed/upgraded"
|
||||
}
|
||||
|
||||
function install_abra_release {
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
|
||||
@ -28,21 +35,47 @@ function install_abra_release {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! type "curl" > /dev/null 2>&1; then
|
||||
error "'python3' is not installed, cannot proceed..."
|
||||
echo "perhaps try installing manually via the releases URL?"
|
||||
echo "https://git.coopcloud.tech/coop-cloud/abra/releases"
|
||||
fi
|
||||
|
||||
# FIXME: support different architectures
|
||||
PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m)
|
||||
sed_command='s/.*"assets":\[\{[^}]*"name":"abra.*_'"$PLATFORM"'"[^}]*"browser_download_url":"([^"]*)".*\].*/\1/p'
|
||||
release_url=$(curl -s $ABRA_RELEASE_URL | sed -En $sed_command)
|
||||
FILENAME="abra_"$ABRA_VERSION"_"$PLATFORM""
|
||||
sed_command_rel='s/.*"assets":\[\{[^]]*"name":"'$FILENAME'"[^}]*"browser_download_url":"([^"]*)".*\].*/\1/p'
|
||||
sed_command_checksums='s/.*"assets":\[\{[^\]*"name":"checksums.txt"[^}]*"browser_download_url":"([^"]*)".*\].*/\1/p'
|
||||
|
||||
json=$(curl -s $ABRA_RELEASE_URL)
|
||||
release_url=$(echo $json | sed -En $sed_command_rel)
|
||||
checksums_url=$(echo $json | sed -En $sed_command_checksums)
|
||||
|
||||
checksums=$(curl -s $checksums_url)
|
||||
checksum=$(echo "$checksums" | grep "$FILENAME" - | sed -En 's/([0-9a-f]{64})\s+'"$FILENAME"'.*/\1/p')
|
||||
|
||||
echo "downloading $ABRA_VERSION $PLATFORM binary release for abra..."
|
||||
curl --progress-bar "$release_url" --output "$HOME/.local/bin/abra"
|
||||
curl --progress-bar "$release_url" --output "$HOME/.local/bin/.abra-download"
|
||||
localsum=$(sha256sum $HOME/.local/bin/.abra-download | sed -En 's/([0-9a-f]{64})\s+.*/\1/p')
|
||||
echo "checking if checksums match..."
|
||||
if [[ "$localsum" != "$checksum" ]]; then
|
||||
print_checksum_error
|
||||
exit 1
|
||||
fi
|
||||
echo "$(tput setaf 2)check successful!$(tput sgr0)"
|
||||
mv "$HOME/.local/bin/.abra-download" "$HOME/.local/bin/abra"
|
||||
chmod +x "$HOME/.local/bin/abra"
|
||||
|
||||
x=$(echo $PATH | grep $HOME/.local/bin)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "$(tput setaf 3)WARNING: $HOME/.local/bin/ is not in \$PATH! If you want to run abra by just typing "abra" you should add it to your \$PATH! To do that run:$(tput sgr0)"
|
||||
p=$HOME/.local/bin
|
||||
com="echo PATH=\$PATH:$p"
|
||||
if [[ $SHELL =~ "bash" ]]; then
|
||||
echo "echo $com >> $HOME/.bashrc"
|
||||
elif [[ $SHELL =~ "fizsh" ]]; then
|
||||
echo "echo $com >> $HOME/.fizsh/.fizshrc"
|
||||
elif [[ $SHELL =~ "zsh" ]]; then
|
||||
echo "echo $com >> $HOME/.zshrc"
|
||||
else
|
||||
echo "echo $com >> $HOME/.profile"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "abra installed to $HOME/.local/bin/abra"
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user