forked from toolshed/abra
Compare commits
43 Commits
0.2.0-alph
...
0.2.1-alph
Author | SHA1 | Date | |
---|---|---|---|
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/
|
vendor/
|
||||||
.envrc
|
.envrc
|
||||||
dist/
|
dist/
|
||||||
|
*fmtcoverage.html
|
||||||
|
@ -50,6 +50,13 @@ sudo cp scripts/autocomplete/bash /etc/bash_completion.d/abra
|
|||||||
source /etc/bash_completion.d/abra
|
source /etc/bash_completion.d/abra
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In development, you can source the script in your git checkout, just make sure
|
||||||
|
to set `PROG=abra`, otherwise it'll add completion to the wrong command:
|
||||||
|
|
||||||
|
```
|
||||||
|
PROG=abra source /path/to/abra/scripts/autocomplete/bash
|
||||||
|
```
|
||||||
|
|
||||||
**(fi)zsh**
|
**(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`
|
(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`
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
// AppCommand defines the `abra app` command and ets subcommands
|
// AppCommand defines the `abra app` command and ets subcommands
|
||||||
var AppCommand = &cli.Command{
|
var AppCommand = &cli.Command{
|
||||||
Name: "app",
|
Name: "app",
|
||||||
Usage: "Manage apps",
|
Usage: "Manage deployed apps",
|
||||||
Aliases: []string{"a"},
|
Aliases: []string{"a"},
|
||||||
ArgsUsage: "<app>",
|
ArgsUsage: "<app>",
|
||||||
Description: `
|
Description: `
|
||||||
|
@ -22,6 +22,7 @@ var appDeployCommand = &cli.Command{
|
|||||||
Usage: "Deploy an app",
|
Usage: "Deploy an app",
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
internal.ForceFlag,
|
internal.ForceFlag,
|
||||||
|
internal.ChaosFlag,
|
||||||
},
|
},
|
||||||
Description: `
|
Description: `
|
||||||
This command deploys a new instance of an app. It does not support changing the
|
This command deploys a new instance of an app. It does not support changing the
|
||||||
@ -29,8 +30,11 @@ version of an existing deployed app, for this you need to look at the "abra app
|
|||||||
upgrade <app>" command.
|
upgrade <app>" command.
|
||||||
|
|
||||||
You may pass "--force" to re-deploy the same version again. This can be useful
|
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
|
if the container runtime has gotten into a weird state.
|
||||||
hacking.
|
|
||||||
|
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 {
|
Action: func(c *cli.Context) error {
|
||||||
app := internal.ValidateApp(c)
|
app := internal.ValidateApp(c)
|
||||||
@ -50,33 +54,51 @@ hacking.
|
|||||||
|
|
||||||
if isDeployed {
|
if isDeployed {
|
||||||
if internal.Force {
|
if internal.Force {
|
||||||
logrus.Infof("continuing with deployment due to '--force/-f' being set")
|
logrus.Warnf("'%s' already deployed but continuing (--force)", stackName)
|
||||||
|
} else if internal.Chaos {
|
||||||
|
logrus.Warnf("'%s' already deployed but continuing (--chaos)", stackName)
|
||||||
} else {
|
} else {
|
||||||
logrus.Fatalf("'%s' is already deployed", stackName)
|
logrus.Fatalf("'%s' is already deployed", stackName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
version := deployedVersion
|
version := deployedVersion
|
||||||
if version == "" {
|
if version == "" && !internal.Chaos {
|
||||||
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type)
|
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
if len(versions) > 0 {
|
if len(versions) > 0 {
|
||||||
version = versions[len(versions)-1]
|
version = versions[len(versions)-1]
|
||||||
logrus.Infof("choosing '%s' as version to deploy", version)
|
logrus.Debugf("choosing '%s' as version to deploy", version)
|
||||||
if err := recipe.EnsureVersion(app.Type, version); err != nil {
|
if err := recipe.EnsureVersion(app.Type, version); err != nil {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
version = "latest commit"
|
version = "latest commit"
|
||||||
logrus.Warning("no versions detected, using latest commit")
|
logrus.Warn("no versions detected, using latest commit")
|
||||||
if err := recipe.EnsureLatest(app.Type); err != nil {
|
if err := recipe.EnsureLatest(app.Type); err != nil {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if version == "" && !internal.Chaos {
|
||||||
|
logrus.Debugf("choosing '%s' as version to deploy", version)
|
||||||
|
if err := recipe.EnsureVersion(app.Type, version); err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if internal.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")
|
abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh")
|
||||||
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
|
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -101,7 +123,7 @@ hacking.
|
|||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := DeployOverview(app, version); err != nil {
|
if err := DeployOverview(app, version, "continue with deployment?"); err != nil {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +148,7 @@ hacking.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeployOverview shows a deployment overview
|
// DeployOverview shows a deployment overview
|
||||||
func DeployOverview(app config.App, version string) error {
|
func DeployOverview(app config.App, version, message string) error {
|
||||||
tableCol := []string{"server", "compose", "domain", "stack", "version"}
|
tableCol := []string{"server", "compose", "domain", "stack", "version"}
|
||||||
table := abraFormatter.CreateTable(tableCol)
|
table := abraFormatter.CreateTable(tableCol)
|
||||||
|
|
||||||
@ -135,12 +157,17 @@ func DeployOverview(app config.App, version string) error {
|
|||||||
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
|
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
table.Append([]string{app.Server, deployConfig, app.Domain, app.StackName(), version})
|
server := app.Server
|
||||||
|
if app.Server == "default" {
|
||||||
|
server = "local"
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Append([]string{server, deployConfig, app.Domain, app.StackName(), version})
|
||||||
table.Render()
|
table.Render()
|
||||||
|
|
||||||
response := false
|
response := false
|
||||||
prompt := &survey.Confirm{
|
prompt := &survey.Confirm{
|
||||||
Message: "continue with deployment?",
|
Message: message,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := survey.AskOne(prompt, &response); err != nil {
|
if err := survey.AskOne(prompt, &response); err != nil {
|
||||||
@ -164,7 +191,12 @@ func NewVersionOverview(app config.App, currentVersion, newVersion string) error
|
|||||||
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
|
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
table.Append([]string{app.Server, deployConfig, app.Domain, app.StackName(), currentVersion, newVersion})
|
server := app.Server
|
||||||
|
if app.Server == "default" {
|
||||||
|
server = "local"
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Append([]string{server, deployConfig, app.Domain, app.StackName(), currentVersion, newVersion})
|
||||||
table.Render()
|
table.Render()
|
||||||
|
|
||||||
response := false
|
response := false
|
||||||
|
@ -210,6 +210,10 @@ func action(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if newAppServer == "default" {
|
||||||
|
newAppServer = "local"
|
||||||
|
}
|
||||||
|
|
||||||
tableCol := []string{"Name", "Domain", "Type", "Server"}
|
tableCol := []string{"Name", "Domain", "Type", "Server"}
|
||||||
table := abraFormatter.CreateTable(tableCol)
|
table := abraFormatter.CreateTable(tableCol)
|
||||||
table.Append([]string{sanitisedAppName, domain, recipe.Name, newAppServer})
|
table.Append([]string{sanitisedAppName, domain, recipe.Name, newAppServer})
|
||||||
|
@ -23,16 +23,20 @@ var appRollbackCommand = &cli.Command{
|
|||||||
ArgsUsage: "<app>",
|
ArgsUsage: "<app>",
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
internal.ForceFlag,
|
internal.ForceFlag,
|
||||||
|
internal.ChaosFlag,
|
||||||
},
|
},
|
||||||
Description: `
|
Description: `
|
||||||
This command rolls an app back to a previous version if one exists.
|
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
|
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
|
useful if the container runtime has gotten into a weird state.
|
||||||
some live hacking.
|
|
||||||
|
|
||||||
This action could be destructive, please ensure you have a copy of your app
|
This action could be destructive, please ensure you have a copy of your app
|
||||||
data beforehand - see "abra app backup <app>" for more.
|
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) {
|
BashComplete: func(c *cli.Context) {
|
||||||
appNames, err := config.GetAppNames()
|
appNames, err := config.GetAppNames()
|
||||||
@ -62,10 +66,6 @@ data beforehand - see "abra app backup <app>" for more.
|
|||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if deployedVersion == "" {
|
|
||||||
logrus.Fatalf("failed to determine version of deployed '%s'", app.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isDeployed {
|
if !isDeployed {
|
||||||
logrus.Fatalf("'%s' is not deployed?", app.Name)
|
logrus.Fatalf("'%s' is not deployed?", app.Name)
|
||||||
}
|
}
|
||||||
@ -76,6 +76,13 @@ data beforehand - see "abra app backup <app>" for more.
|
|||||||
}
|
}
|
||||||
|
|
||||||
var availableDowngrades []string
|
var availableDowngrades []string
|
||||||
|
if deployedVersion == "" {
|
||||||
|
deployedVersion = "unknown"
|
||||||
|
availableDowngrades = versions
|
||||||
|
logrus.Warnf("failed to determine version of deployed '%s'", app.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if deployedVersion != "unknown" && !internal.Chaos {
|
||||||
for _, version := range versions {
|
for _, version := range versions {
|
||||||
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
|
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -93,6 +100,7 @@ data beforehand - see "abra app backup <app>" for more.
|
|||||||
if len(availableDowngrades) == 0 {
|
if len(availableDowngrades) == 0 {
|
||||||
logrus.Fatal("no available downgrades, you're on latest")
|
logrus.Fatal("no available downgrades, you're on latest")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: jeezus golang why do you not have a list reverse function
|
// FIXME: jeezus golang why do you not have a list reverse function
|
||||||
for i, j := 0, len(availableDowngrades)-1; i < j; i, j = i+1, j-1 {
|
for i, j := 0, len(availableDowngrades)-1; i < j; i, j = i+1, j-1 {
|
||||||
@ -100,7 +108,11 @@ data beforehand - see "abra app backup <app>" for more.
|
|||||||
}
|
}
|
||||||
|
|
||||||
var chosenDowngrade string
|
var chosenDowngrade string
|
||||||
if !internal.Force {
|
if !internal.Chaos {
|
||||||
|
if internal.Force {
|
||||||
|
chosenDowngrade = availableDowngrades[0]
|
||||||
|
logrus.Debugf("choosing '%s' as version to downgrade to (--force)", chosenDowngrade)
|
||||||
|
} else {
|
||||||
prompt := &survey.Select{
|
prompt := &survey.Select{
|
||||||
Message: fmt.Sprintf("Please select a downgrade (current version: '%s'):", deployedVersion),
|
Message: fmt.Sprintf("Please select a downgrade (current version: '%s'):", deployedVersion),
|
||||||
Options: availableDowngrades,
|
Options: availableDowngrades,
|
||||||
@ -109,15 +121,22 @@ data beforehand - see "abra app backup <app>" for more.
|
|||||||
return err
|
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 {
|
if err := recipe.EnsureVersion(app.Type, chosenDowngrade); err != nil {
|
||||||
logrus.Fatal(err)
|
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")
|
abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh")
|
||||||
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
|
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
|
||||||
|
@ -55,15 +55,20 @@ var appRunCommand = &cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
serviceName := c.Args().Get(1)
|
serviceName := c.Args().Get(1)
|
||||||
|
stackAndServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
|
||||||
filters := filters.NewArgs()
|
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})
|
containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
if len(containers) > 1 {
|
|
||||||
logrus.Fatalf("expected 1 container but got %d", len(containers))
|
switch len(containers) {
|
||||||
|
case 0:
|
||||||
|
logrus.Fatalf("no containers matching '%s' found?", stackAndServiceName)
|
||||||
|
case 1:
|
||||||
|
logrus.Fatalf("expected 1 container matching '%s' but got %d", stackAndServiceName, len(containers))
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := c.Args().Slice()[2:]
|
cmd := c.Args().Slice()[2:]
|
||||||
|
@ -22,12 +22,28 @@ volumes as eligiblef or pruning once undeployed.
|
|||||||
`,
|
`,
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
app := internal.ValidateApp(c)
|
app := internal.ValidateApp(c)
|
||||||
|
stackName := app.StackName()
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
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 := DeployOverview(app, deployedVersion, "continue with undeploy?"); err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
rmOpts := stack.Remove{Namespaces: []string{app.StackName()}}
|
rmOpts := stack.Remove{Namespaces: []string{app.StackName()}}
|
||||||
if err := stack.RunRemove(c.Context, cl, rmOpts); err != nil {
|
if err := stack.RunRemove(c.Context, cl, rmOpts); err != nil {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
|
@ -22,6 +22,7 @@ var appUpgradeCommand = &cli.Command{
|
|||||||
ArgsUsage: "<app>",
|
ArgsUsage: "<app>",
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
internal.ForceFlag,
|
internal.ForceFlag,
|
||||||
|
internal.ChaosFlag,
|
||||||
},
|
},
|
||||||
Description: `
|
Description: `
|
||||||
This command supports upgrading an app. You can use it to choose and roll out a
|
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.
|
deployed app.
|
||||||
|
|
||||||
You may pass "--force/-f" to upgrade to the same version again. This can be
|
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
|
useful if the container runtime has gotten into a weird state.
|
||||||
some live hacking.
|
|
||||||
|
|
||||||
This action could be destructive, please ensure you have a copy of your app
|
This action could be destructive, please ensure you have a copy of your app
|
||||||
data beforehand - see "abra app backup <app>" for more.
|
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 {
|
Action: func(c *cli.Context) error {
|
||||||
app := internal.ValidateApp(c)
|
app := internal.ValidateApp(c)
|
||||||
@ -54,10 +58,6 @@ data beforehand - see "abra app backup <app>" for more.
|
|||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if deployedVersion == "" {
|
|
||||||
logrus.Fatalf("failed to determine version of deployed '%s'", app.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isDeployed {
|
if !isDeployed {
|
||||||
logrus.Fatalf("'%s' is not deployed?", app.Name)
|
logrus.Fatalf("'%s' is not deployed?", app.Name)
|
||||||
}
|
}
|
||||||
@ -67,7 +67,18 @@ data beforehand - see "abra app backup <app>" for more.
|
|||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(versions) == 0 && !internal.Chaos {
|
||||||
|
logrus.Fatalf("no versions available '%s' in recipe catalogue?", app.Type)
|
||||||
|
}
|
||||||
|
|
||||||
var availableUpgrades []string
|
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 {
|
for _, version := range versions {
|
||||||
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
|
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -82,12 +93,18 @@ data beforehand - see "abra app backup <app>" for more.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(availableUpgrades) == 0 {
|
if len(availableUpgrades) == 0 && !internal.Force {
|
||||||
logrus.Fatal("no available upgrades, you're on latest")
|
logrus.Fatal("no available upgrades, you're on latest")
|
||||||
|
availableUpgrades = versions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var chosenUpgrade string
|
var chosenUpgrade string
|
||||||
if !internal.Force {
|
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{
|
prompt := &survey.Select{
|
||||||
Message: fmt.Sprintf("Please select an upgrade (current version: '%s'):", deployedVersion),
|
Message: fmt.Sprintf("Please select an upgrade (current version: '%s'):", deployedVersion),
|
||||||
Options: availableUpgrades,
|
Options: availableUpgrades,
|
||||||
@ -96,15 +113,22 @@ data beforehand - see "abra app backup <app>" for more.
|
|||||||
return err
|
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 {
|
if err := recipe.EnsureVersion(app.Type, chosenUpgrade); err != nil {
|
||||||
logrus.Fatal(err)
|
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")
|
abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh")
|
||||||
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
|
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
|
||||||
@ -130,11 +154,9 @@ data beforehand - see "abra app backup <app>" for more.
|
|||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !internal.Force {
|
|
||||||
if err := NewVersionOverview(app, deployedVersion, chosenUpgrade); err != nil {
|
if err := NewVersionOverview(app, deployedVersion, chosenUpgrade); err != nil {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
|
if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
// CatalogueCommand defines the `abra catalogue` command and sub-commands.
|
// CatalogueCommand defines the `abra catalogue` command and sub-commands.
|
||||||
var CatalogueCommand = &cli.Command{
|
var CatalogueCommand = &cli.Command{
|
||||||
Name: "catalogue",
|
Name: "catalogue",
|
||||||
Usage: "Manage the recipe catalogue",
|
Usage: "Manage the recipe catalogue (for maintainers)",
|
||||||
Aliases: []string{"c"},
|
Aliases: []string{"c"},
|
||||||
ArgsUsage: "<recipe>",
|
ArgsUsage: "<recipe>",
|
||||||
Description: "This command helps recipe packagers interact with the recipe catalogue",
|
Description: "This command helps recipe packagers interact with the recipe catalogue",
|
||||||
|
12
cli/cli.go
12
cli/cli.go
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"coopcloud.tech/abra/cli/app"
|
"coopcloud.tech/abra/cli/app"
|
||||||
"coopcloud.tech/abra/cli/catalogue"
|
"coopcloud.tech/abra/cli/catalogue"
|
||||||
|
"coopcloud.tech/abra/cli/domain"
|
||||||
"coopcloud.tech/abra/cli/recipe"
|
"coopcloud.tech/abra/cli/recipe"
|
||||||
"coopcloud.tech/abra/cli/server"
|
"coopcloud.tech/abra/cli/server"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
@ -40,8 +41,7 @@ var DebugFlag = &cli.BoolFlag{
|
|||||||
Usage: "Show DEBUG messages",
|
Usage: "Show DEBUG messages",
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunApp runs CLI abra app.
|
func newAbraApp(version, commit string) *cli.App {
|
||||||
func RunApp(version, commit string) {
|
|
||||||
app := &cli.App{
|
app := &cli.App{
|
||||||
Name: "abra",
|
Name: "abra",
|
||||||
Usage: `The Co-op Cloud command-line utility belt 🎩🐇
|
Usage: `The Co-op Cloud command-line utility belt 🎩🐇
|
||||||
@ -59,7 +59,7 @@ func RunApp(version, commit string) {
|
|||||||
server.ServerCommand,
|
server.ServerCommand,
|
||||||
recipe.RecipeCommand,
|
recipe.RecipeCommand,
|
||||||
catalogue.CatalogueCommand,
|
catalogue.CatalogueCommand,
|
||||||
VersionCommand,
|
domain.DomainCommand,
|
||||||
UpgradeCommand,
|
UpgradeCommand,
|
||||||
},
|
},
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
@ -106,6 +106,12 @@ func RunApp(version, commit string) {
|
|||||||
|
|
||||||
return nil
|
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 {
|
if err := app.Run(os.Args); err != nil {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
|
150
cli/domain/create.go
Normal file
150
cli/domain/create.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DomainCreateCommand lists domains.
|
||||||
|
var DomainCreateCommand = &cli.Command{
|
||||||
|
Name: "create",
|
||||||
|
Usage: "Create a new domain record",
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
ArgsUsage: "<zone> <type> <name> <value> [<ttl>] [<priority>]",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
internal.DNSProviderFlag,
|
||||||
|
},
|
||||||
|
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 domain create -p gandi foo.com A myapp 192.168.178.44
|
||||||
|
|
||||||
|
Which means you can then deploy an app against "myapp.foo.com" successfully.
|
||||||
|
`,
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
zone := c.Args().First()
|
||||||
|
if zone == "" {
|
||||||
|
internal.ShowSubcommandHelpAndError(c, errors.New("no zone provided"))
|
||||||
|
}
|
||||||
|
|
||||||
|
recordType := c.Args().Get(1)
|
||||||
|
recordName := c.Args().Get(2)
|
||||||
|
recordValue := c.Args().Get(3)
|
||||||
|
recordTTL := c.Args().Get(4)
|
||||||
|
recordPriority := c.Args().Get(5)
|
||||||
|
|
||||||
|
if recordType == "" {
|
||||||
|
internal.ShowSubcommandHelpAndError(c, errors.New("no type provided"))
|
||||||
|
}
|
||||||
|
if recordName == "" {
|
||||||
|
internal.ShowSubcommandHelpAndError(c, errors.New("no name provided"))
|
||||||
|
}
|
||||||
|
if recordValue == "" {
|
||||||
|
internal.ShowSubcommandHelpAndError(c, errors.New("no value provided"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if recordTTL == "" {
|
||||||
|
recordTTL = "86400"
|
||||||
|
}
|
||||||
|
|
||||||
|
if recordType == "MX" || recordType == "SRV" || recordType == "URI" {
|
||||||
|
if recordPriority == "" {
|
||||||
|
logrus.Fatal("record priority must be set when using MX/SRV/URI records")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
ttl, err := strconv.Atoi(recordTTL)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
record := libdns.Record{
|
||||||
|
Type: recordType,
|
||||||
|
Name: recordName,
|
||||||
|
Value: recordValue,
|
||||||
|
TTL: time.Duration(ttl),
|
||||||
|
}
|
||||||
|
|
||||||
|
if recordType == "MX" || recordType == "SRV" || recordType == "URI" {
|
||||||
|
priority, err := strconv.Atoi(recordPriority)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
record.Priority = priority
|
||||||
|
}
|
||||||
|
|
||||||
|
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 no record 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/domain/domain.go
Normal file
38
cli/domain/domain.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DomainCommand supports managing DNS entries.
|
||||||
|
var DomainCommand = &cli.Command{
|
||||||
|
Name: "domain",
|
||||||
|
Usage: "Manage domains via 3rd party providers",
|
||||||
|
Aliases: []string{"d"},
|
||||||
|
ArgsUsage: "<domain>",
|
||||||
|
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.
|
||||||
|
|
||||||
|
Any new provider 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{
|
||||||
|
DomainListCommand,
|
||||||
|
DomainCreateCommand,
|
||||||
|
DomainRemoveCommand,
|
||||||
|
},
|
||||||
|
}
|
76
cli/domain/list.go
Normal file
76
cli/domain/list.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DomainListCommand lists domains.
|
||||||
|
var DomainListCommand = &cli.Command{
|
||||||
|
Name: "list",
|
||||||
|
Usage: "List domain name records for a zone",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
ArgsUsage: "<zone>",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
internal.DNSProviderFlag,
|
||||||
|
},
|
||||||
|
Description: `
|
||||||
|
This command lists all domain name records managed by a 3rd party provider.
|
||||||
|
|
||||||
|
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 {
|
||||||
|
zone := c.Args().First()
|
||||||
|
if zone == "" {
|
||||||
|
internal.ShowSubcommandHelpAndError(c, errors.New("no zone provided"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
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
|
||||||
|
},
|
||||||
|
}
|
127
cli/domain/remove.go
Normal file
127
cli/domain/remove.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DomainRemoveCommand lists domains.
|
||||||
|
var DomainRemoveCommand = &cli.Command{
|
||||||
|
Name: "remove",
|
||||||
|
Usage: "Remove domain name records for a zone",
|
||||||
|
Aliases: []string{"rm"},
|
||||||
|
ArgsUsage: "<zone> <type> <name>",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
internal.DNSProviderFlag,
|
||||||
|
},
|
||||||
|
Description: `
|
||||||
|
This command removes 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.
|
||||||
|
|
||||||
|
The "<id>" can be retrieved by running "abra domain list <zone>". This can be
|
||||||
|
used then in this command for deletion of the record.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
abra domain remove -p gandi foo.com A myapp
|
||||||
|
|
||||||
|
Which means that the domain name record of type A for myapp.foo.com will be deleted.
|
||||||
|
`,
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
zone := c.Args().First()
|
||||||
|
if zone == "" {
|
||||||
|
internal.ShowSubcommandHelpAndError(c, errors.New("no zone provided"))
|
||||||
|
}
|
||||||
|
|
||||||
|
recordType := c.Args().Get(1)
|
||||||
|
recordName := c.Args().Get(2)
|
||||||
|
|
||||||
|
if recordType == "" {
|
||||||
|
internal.ShowSubcommandHelpAndError(c, errors.New("no type provided"))
|
||||||
|
}
|
||||||
|
if recordName == "" {
|
||||||
|
internal.ShowSubcommandHelpAndError(c, errors.New("no name provided"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
var toDelete libdns.Record
|
||||||
|
for _, record := range records {
|
||||||
|
if record.Type == recordType && record.Name == recordName {
|
||||||
|
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
|
||||||
|
},
|
||||||
|
}
|
@ -49,3 +49,28 @@ var ForceFlag = &cli.BoolFlag{
|
|||||||
Aliases: []string{"f"},
|
Aliases: []string{"f"},
|
||||||
Destination: &Force,
|
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,
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
// }
|
@ -31,7 +31,7 @@ var PatchFlag = &cli.BoolFlag{
|
|||||||
// RecipeCommand defines all recipe related sub-commands.
|
// RecipeCommand defines all recipe related sub-commands.
|
||||||
var RecipeCommand = &cli.Command{
|
var RecipeCommand = &cli.Command{
|
||||||
Name: "recipe",
|
Name: "recipe",
|
||||||
Usage: "Manage recipes",
|
Usage: "Manage recipes (for maintainers)",
|
||||||
ArgsUsage: "<recipe>",
|
ArgsUsage: "<recipe>",
|
||||||
Aliases: []string{"r"},
|
Aliases: []string{"r"},
|
||||||
Description: `
|
Description: `
|
||||||
|
@ -120,7 +120,9 @@ or a rollback of an app.
|
|||||||
prompt := &survey.Input{
|
prompt := &survey.Input{
|
||||||
Message: "tag message",
|
Message: "tag message",
|
||||||
}
|
}
|
||||||
survey.AskOne(prompt, &TagMessage)
|
if err := survey.AskOne(prompt, &TagMessage); err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var createTagOptions git.CreateTagOptions
|
var createTagOptions git.CreateTagOptions
|
||||||
@ -140,7 +142,9 @@ or a rollback of an app.
|
|||||||
prompt := &survey.Input{
|
prompt := &survey.Input{
|
||||||
Message: "commit message",
|
Message: "commit message",
|
||||||
}
|
}
|
||||||
survey.AskOne(prompt, &CommitMessage)
|
if err := survey.AskOne(prompt, &CommitMessage); err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
err = commitWorktree.AddGlob("compose.**yml")
|
err = commitWorktree.AddGlob("compose.**yml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -287,6 +291,9 @@ func getImageVersions(recipe recipe.Recipe) map[string]string {
|
|||||||
|
|
||||||
var services = make(map[string]string)
|
var services = make(map[string]string)
|
||||||
for _, service := range recipe.Config.Services {
|
for _, service := range recipe.Config.Services {
|
||||||
|
if service.Image == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
srv := strings.Split(service.Image, ":")
|
srv := strings.Split(service.Image, ":")
|
||||||
services[srv[0]] = srv[1]
|
services[srv[0]] = srv[1]
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package server
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -53,12 +54,13 @@ All communication between Abra and the server will use this SSH connection.
|
|||||||
},
|
},
|
||||||
ArgsUsage: "<domain> [<user>] [<port>]",
|
ArgsUsage: "<domain> [<user>] [<port>]",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
if c.Args().Len() == 1 && !local {
|
|
||||||
|
if c.Args().Len() == 2 && !local {
|
||||||
err := errors.New("missing arguments <domain> or '--local'")
|
err := errors.New("missing arguments <domain> or '--local'")
|
||||||
internal.ShowSubcommandHelpAndError(c, err)
|
internal.ShowSubcommandHelpAndError(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Args().Get(1) != "" && local {
|
if c.Args().Get(2) != "" && local {
|
||||||
err := errors.New("cannot use '<domain>' and '--local' together")
|
err := errors.New("cannot use '<domain>' and '--local' together")
|
||||||
internal.ShowSubcommandHelpAndError(c, err)
|
internal.ShowSubcommandHelpAndError(c, err)
|
||||||
}
|
}
|
||||||
@ -69,6 +71,9 @@ All communication between Abra and the server will use this SSH connection.
|
|||||||
if err := server.CreateServerDir(domainName); err != nil {
|
if err := server.CreateServerDir(domainName); err != nil {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if _, err := exec.LookPath("docker"); err != nil {
|
||||||
|
return errors.New("docker command not found on $PATH, is it installed?")
|
||||||
|
}
|
||||||
logrus.Info("local server has been added")
|
logrus.Info("local server has been added")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -113,6 +118,10 @@ All communication between Abra and the server will use this SSH connection.
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
cl, err := client.New(domainName)
|
cl, err := client.New(domainName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logrus.Warn("cleaning up context due to connection failure")
|
||||||
|
if err := client.DeleteContext(domainName); err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,6 +129,10 @@ All communication between Abra and the server will use this SSH connection.
|
|||||||
if strings.Contains(err.Error(), "command not found") {
|
if strings.Contains(err.Error(), "command not found") {
|
||||||
logrus.Fatalf("docker is not installed on '%s'?", domainName)
|
logrus.Fatalf("docker is not installed on '%s'?", domainName)
|
||||||
} else {
|
} else {
|
||||||
|
logrus.Warn("cleaning up context due to connection failure")
|
||||||
|
if err := client.DeleteContext(domainName); err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
logrus.Fatalf("unable to make a connection to '%s'?", domainName)
|
logrus.Fatalf("unable to make a connection to '%s'?", domainName)
|
||||||
}
|
}
|
||||||
logrus.Debug(err)
|
logrus.Debug(err)
|
||||||
|
@ -1,17 +1,13 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/formatter"
|
"coopcloud.tech/abra/cli/formatter"
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
|
"coopcloud.tech/libcapsul"
|
||||||
"github.com/hetznercloud/hcloud-go/hcloud"
|
"github.com/hetznercloud/hcloud-go/hcloud"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
@ -182,8 +178,8 @@ environment variable or otherwise passing the "--env/-e" flag.
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
name := c.Args().First()
|
capsulName := c.Args().First()
|
||||||
if name == "" {
|
if capsulName == "" {
|
||||||
internal.ShowSubcommandHelpAndError(c, errors.New("no name provided"))
|
internal.ShowSubcommandHelpAndError(c, errors.New("no name provided"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,57 +187,17 @@ environment variable or otherwise passing the "--env/-e" flag.
|
|||||||
logrus.Fatal("Capsul API token is missing")
|
logrus.Fatal("Capsul API token is missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
// yep, the response time is quite slow, something to fix on the Capsul side
|
|
||||||
client := &http.Client{Timeout: 20 * time.Second}
|
|
||||||
|
|
||||||
capsulCreateURL := fmt.Sprintf("https://%s/api/capsul/create", capsulInstance)
|
capsulCreateURL := fmt.Sprintf("https://%s/api/capsul/create", capsulInstance)
|
||||||
logrus.Debugf("using '%s' as capsul create url", capsulCreateURL)
|
|
||||||
values := map[string]string{
|
|
||||||
"name": name,
|
|
||||||
"size": capsulType,
|
|
||||||
"os": capsulImage,
|
|
||||||
"ssh_key_0": capsulSSHKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
payload, err := json.Marshal(values)
|
capsulClient := libcapsul.New(capsulCreateURL, capsulAPIToken)
|
||||||
|
resp, err := capsulClient.Create(capsulName, capsulType, capsulImage, capsulSSHKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", capsulCreateURL, bytes.NewBuffer(payload))
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header = http.Header{
|
|
||||||
"Content-Type": []string{"application/json"},
|
|
||||||
"Authorization": []string{capsulAPIToken},
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
logrus.Fatal(string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
type capsulCreateResponse struct{ ID string }
|
|
||||||
var resp capsulCreateResponse
|
|
||||||
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
logrus.Debugf("capsul created with ID: '%s'", resp.ID)
|
|
||||||
|
|
||||||
tableColumns := []string{"Name", "ID"}
|
tableColumns := []string{"Name", "ID"}
|
||||||
table := formatter.CreateTable(tableColumns)
|
table := formatter.CreateTable(tableColumns)
|
||||||
table.Append([]string{name, resp.ID})
|
table.Append([]string{capsulName, resp.ID})
|
||||||
table.Render()
|
table.Render()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
var ServerCommand = &cli.Command{
|
var ServerCommand = &cli.Command{
|
||||||
Name: "server",
|
Name: "server",
|
||||||
Aliases: []string{"s"},
|
Aliases: []string{"s"},
|
||||||
Usage: "Manage servers",
|
Usage: "Manage servers via 3rd party providers",
|
||||||
Description: `
|
Description: `
|
||||||
Manage the lifecycle of a server.
|
Manage the lifecycle of a server.
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
},
|
|
||||||
}
|
|
4
go.mod
4
go.mod
@ -25,6 +25,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
coopcloud.tech/libcapsul v0.0.0-20211020153234-f1386b5cf79d
|
||||||
github.com/Microsoft/hcsshim v0.8.21 // indirect
|
github.com/Microsoft/hcsshim v0.8.21 // indirect
|
||||||
github.com/containerd/containerd v1.5.5 // indirect
|
github.com/containerd/containerd v1.5.5 // indirect
|
||||||
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
||||||
@ -32,9 +33,12 @@ require (
|
|||||||
github.com/fvbommel/sortorder v1.0.2 // indirect
|
github.com/fvbommel/sortorder v1.0.2 // indirect
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
github.com/gorilla/mux v1.8.0 // indirect
|
github.com/gorilla/mux v1.8.0 // indirect
|
||||||
|
github.com/libdns/gandi v1.0.2
|
||||||
|
github.com/libdns/libdns v0.2.1
|
||||||
github.com/moby/sys/mount v0.2.0 // indirect
|
github.com/moby/sys/mount v0.2.0 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/opencontainers/runc v1.0.2 // indirect
|
github.com/opencontainers/runc v1.0.2 // indirect
|
||||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0
|
||||||
)
|
)
|
||||||
|
7
go.sum
7
go.sum
@ -21,6 +21,8 @@ cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIA
|
|||||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||||
|
coopcloud.tech/libcapsul v0.0.0-20211020153234-f1386b5cf79d h1:5A69AFx2BP5J43Y9SaB9LlAIMLr2SWqbzfgjUh8sgKM=
|
||||||
|
coopcloud.tech/libcapsul v0.0.0-20211020153234-f1386b5cf79d/go.mod h1:HEQ9pSJRsDKabMxPfYCCzpVpAreLoC4Gh4SkVyOaKvk=
|
||||||
coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb h1:Jf+Dnna2kXcNQvcA5JMp6d2Uyvg2pIVJfip9+X5FrH0=
|
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/tagcmp v0.0.0-20211011140827-4f27c74467eb/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
@ -510,6 +512,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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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/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.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
4
pkg/client/README.md
Normal file
4
pkg/client/README.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
IMPORTANT POINT ABOUT CONTEXTS
|
||||||
|
|
||||||
|
Please use context names starting with `testContext` for testing purposes to ensure that no data is lost. such as `testContext`, `testContext2`, `testContextFail` etc
|
||||||
|
|
@ -4,13 +4,20 @@ package client
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/connhelper"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// New initiates a new Docker client.
|
// New initiates a new Docker client.
|
||||||
func New(contextName string) (*client.Client, error) {
|
func New(contextName string) (*client.Client, error) {
|
||||||
|
var clientOpts []client.Opt
|
||||||
|
|
||||||
|
clientOpts = append(clientOpts, client.WithTimeout(3*time.Second))
|
||||||
|
|
||||||
|
if contextName != "default" {
|
||||||
context, err := GetContext(contextName)
|
context, err := GetContext(contextName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -29,12 +36,12 @@ func New(contextName string) (*client.Client, error) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var clientOpts []client.Opt
|
|
||||||
clientOpts = append(clientOpts,
|
clientOpts = append(clientOpts,
|
||||||
client.WithHTTPClient(httpClient),
|
client.WithHTTPClient(httpClient),
|
||||||
client.WithHost(helper.Host),
|
client.WithHost(helper.Host),
|
||||||
client.WithDialContext(helper.Dialer),
|
client.WithDialContext(helper.Dialer),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
version := os.Getenv("DOCKER_API_VERSION")
|
version := os.Getenv("DOCKER_API_VERSION")
|
||||||
if version != "" {
|
if version != "" {
|
||||||
@ -45,10 +52,18 @@ func New(contextName string) (*client.Client, error) {
|
|||||||
|
|
||||||
cl, err := client.NewClientWithOpts(clientOpts...)
|
cl, err := client.NewClientWithOpts(clientOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("unable to create Docker client: %s", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("created client for '%s'", contextName)
|
logrus.Debugf("created client for '%s'", contextName)
|
||||||
|
|
||||||
return cl, nil
|
return cl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newConnectionHelper(daemonURL string) *connhelper.ConnectionHelper {
|
||||||
|
helper, err := connhelper.GetConnectionHelper(daemonURL)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
return helper
|
||||||
|
}
|
||||||
|
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
|
|
||||||
}
|
|
@ -9,6 +9,9 @@ import (
|
|||||||
context "github.com/docker/cli/cli/context"
|
context "github.com/docker/cli/cli/context"
|
||||||
"github.com/docker/cli/cli/context/docker"
|
"github.com/docker/cli/cli/context/docker"
|
||||||
contextStore "github.com/docker/cli/cli/context/store"
|
contextStore "github.com/docker/cli/cli/context/store"
|
||||||
|
dCliContextStore "github.com/docker/cli/cli/context/store"
|
||||||
|
cliflags "github.com/docker/cli/cli/flags"
|
||||||
|
dClient "github.com/docker/docker/client"
|
||||||
"github.com/moby/term"
|
"github.com/moby/term"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@ -94,7 +97,6 @@ func GetContext(contextName string) (contextStore.Metadata, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetContextEndpoint(ctx contextStore.Metadata) (string, 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)
|
endpointmeta, ok := ctx.Endpoints["docker"].(context.EndpointMetaBase)
|
||||||
if !ok {
|
if !ok {
|
||||||
err := errors.New("context lacks Docker endpoint")
|
err := errors.New("context lacks Docker endpoint")
|
||||||
@ -108,21 +110,47 @@ func newContextStore(dir string, config contextStore.Config) contextStore.Store
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultDockerContextStore() *command.ContextStoreWithDefault {
|
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()
|
_, _, stderr := term.StdStreams()
|
||||||
// TODO: Look into custom docker configs in case users want that
|
|
||||||
dockerConfig := dConfig.LoadDefaultConfigFile(stderr)
|
dockerConfig := dConfig.LoadDefaultConfigFile(stderr)
|
||||||
contextDir := dConfig.ContextStoreDir()
|
contextDir := dConfig.ContextStoreDir()
|
||||||
storeConfig := command.DefaultContextStoreConfig()
|
storeConfig := command.DefaultContextStoreConfig()
|
||||||
store := newContextStore(contextDir, storeConfig)
|
store := newContextStore(contextDir, storeConfig)
|
||||||
|
|
||||||
|
opts := &cliflags.CommonOptions{Context: "default"}
|
||||||
|
|
||||||
dockerContextStore := &command.ContextStoreWithDefault{
|
dockerContextStore := &command.ContextStoreWithDefault{
|
||||||
Store: store,
|
Store: store,
|
||||||
Resolver: func() (*command.DefaultContext, error) {
|
Resolver: func() (*command.DefaultContext, error) {
|
||||||
// nil for the Opts because it works without it and its a cli thing
|
return command.ResolveDefaultContext(opts, dockerConfig, storeConfig, stderr)
|
||||||
return command.ResolveDefaultContext(nil, dockerConfig, storeConfig, stderr)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return dockerContextStore
|
return dockerContextStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
@ -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) {
|
func TestGetContextEndpoint(t *testing.T) {
|
||||||
var testDockerContexts = []TestContext{
|
var testDockerContexts = []TestContext{
|
||||||
dockerContext("ssh://foobar", "docker"),
|
dockerContext("ssh://foobar", "docker"),
|
||||||
|
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
|
||||||
|
}
|
48
pkg/git/read.go
Normal file
48
pkg/git/read.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"coopcloud.tech/abra/pkg/config"
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
return status.IsClean(), nil
|
||||||
|
}
|
@ -109,6 +109,15 @@ func EnsureExists(recipe string) error {
|
|||||||
func EnsureVersion(recipeName, version string) error {
|
func EnsureVersion(recipeName, version string) error {
|
||||||
recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName)
|
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)
|
repo, err := git.PlainOpen(recipeDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -195,3 +204,26 @@ func EnsureLatest(recipeName string) error {
|
|||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
ABRA_VERSION="0.2.0-alpha"
|
ABRA_VERSION="0.2.1-alpha"
|
||||||
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION"
|
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION"
|
||||||
|
|
||||||
function show_banner {
|
function show_banner {
|
||||||
|
Reference in New Issue
Block a user