forked from toolshed/abra
Compare commits
107 Commits
0.1.5-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 | |||
75fb9a2774 | |||
0d500b636d | |||
5dd97cace0 | |||
ae32b1eed2 | |||
113bdf9e86 | |||
d4d4da19b7 | |||
454ee696d6 | |||
ca16c002ba | |||
91cc8b00b3 | |||
d0828c4d8d | |||
b69aed3bcf | |||
875255fd8c | |||
2dca602c0b | |||
1dca8a1067 | |||
37022bf0c8 | |||
eb5b35d47f
|
|||
ece1130797
|
|||
c266316f7e
|
|||
d804276cf2 | |||
4235e06943 | |||
a9af0b3627 | |||
a0b4886eba | |||
84489495dc | |||
a8683dc38a | |||
e2128ea5b6 | |||
ca3c5fef0f | |||
4a01e411be | |||
777d49ac1d | |||
deb7d21158 | |||
6db1fdcfba | |||
44dc0edf7b | |||
36ff50312c
|
|||
ff4b978876 | |||
b68547b2c2 | |||
0140f96ca1 | |||
1cb45113db | |||
c764243f3a | |||
dde8afcd43 | |||
98ffc210e1 | |||
7c0d883135 | |||
e78ced41fb | |||
e9113500d8 | |||
7368cabc49 | |||
f75e264811 | |||
8bfd76fd04 | |||
1cb5e3509d
|
|||
3cd2399cca | |||
11c4651a3b | |||
49f90674f2 | |||
74a70edb03 | |||
6fc5c31347 | |||
c616907b71 | |||
a58cea3e0a | |||
700f89425a | |||
8cc0a350e6 | |||
46e67fa420 | |||
cacbb5a0f1 | |||
e7046a15aa | |||
c1fd97c427 | |||
2f218bd99f | |||
48290aa316 | |||
db5cbfa992 | |||
4c11e813e8 | |||
6ae75e013a
|
@ -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
|
||||
|
@ -14,12 +14,12 @@ builds:
|
||||
dir: cmd/abra
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
ldflags:
|
||||
- "-X 'main.Commit={{ .Commit }}'"
|
||||
- "-X 'main.Version={{ .Version }}'"
|
||||
archives:
|
||||
- replacements:
|
||||
linux: Linux
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
format: binary
|
||||
@ -28,8 +28,13 @@ checksum:
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
sort: desc
|
||||
filters:
|
||||
exclude:
|
||||
- "^WIP:"
|
||||
- "^chore:"
|
||||
- "^docs:"
|
||||
- "^refactor:"
|
||||
- "^style:"
|
||||
- "^test:"
|
||||
- "^tests:"
|
||||
|
@ -50,6 +50,13 @@ sudo cp scripts/autocomplete/bash /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 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
|
||||
var AppCommand = &cli.Command{
|
||||
Name: "app",
|
||||
Usage: "Manage apps",
|
||||
Usage: "Manage deployed apps",
|
||||
Aliases: []string{"a"},
|
||||
ArgsUsage: "<app>",
|
||||
Description: `
|
||||
@ -19,6 +19,7 @@ to scaling apps up and spinning them down.
|
||||
appNewCommand,
|
||||
appConfigCommand,
|
||||
appDeployCommand,
|
||||
appUpgradeCommand,
|
||||
appUndeployCommand,
|
||||
appBackupCommand,
|
||||
appRestoreCommand,
|
||||
|
@ -2,11 +2,16 @@ 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"
|
||||
)
|
||||
@ -15,14 +20,85 @@ var appDeployCommand = &cli.Command{
|
||||
Name: "deploy",
|
||||
Aliases: []string{"d"},
|
||||
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
|
||||
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.
|
||||
|
||||
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.Warnf("'%s' already deployed but continuing (--force)", stackName)
|
||||
} else if internal.Chaos {
|
||||
logrus.Warnf("'%s' already deployed but continuing (--chaos)", stackName)
|
||||
} else {
|
||||
logrus.Fatalf("'%s' is already deployed", stackName)
|
||||
}
|
||||
}
|
||||
|
||||
version := deployedVersion
|
||||
if version == "" && !internal.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 == "" && !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")
|
||||
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
|
||||
if err != nil {
|
||||
@ -31,7 +107,6 @@ var appDeployCommand = &cli.Command{
|
||||
for k, v := range abraShEnv {
|
||||
app.Env[k] = v
|
||||
}
|
||||
app.Env["STACK_NAME"] = app.StackName()
|
||||
|
||||
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
|
||||
if err != nil {
|
||||
@ -39,7 +114,7 @@ var appDeployCommand = &cli.Command{
|
||||
}
|
||||
deployOpts := stack.Deploy{
|
||||
Composefiles: composeFiles,
|
||||
Namespace: app.StackName(),
|
||||
Namespace: stackName,
|
||||
Prune: false,
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
}
|
||||
@ -48,6 +123,10 @@ var appDeployCommand = &cli.Command{
|
||||
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)
|
||||
}
|
||||
@ -67,3 +146,71 @@ var appDeployCommand = &cli.Command{
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
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()
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
abraFormatter "coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@ -65,10 +69,10 @@ can take some time.
|
||||
}
|
||||
sort.Sort(config.ByServerAndType(apps))
|
||||
|
||||
statuses := map[string]string{}
|
||||
statuses := make(map[string]map[string]string)
|
||||
tableCol := []string{"Server", "Type", "Domain"}
|
||||
if status {
|
||||
tableCol = append(tableCol, "Status")
|
||||
tableCol = append(tableCol, "Status", "Version", "Updates")
|
||||
statuses, err = config.GetAppStatuses(appFiles)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
@ -78,22 +82,90 @@ can take some time.
|
||||
table := abraFormatter.CreateTable(tableCol)
|
||||
table.SetAutoMergeCellsByColumnIndex([]int{0})
|
||||
|
||||
var (
|
||||
versionedAppsCount int
|
||||
unversionedAppsCount int
|
||||
onLatestCount int
|
||||
canUpgradeCount int
|
||||
)
|
||||
|
||||
for _, app := range apps {
|
||||
var tableRow []string
|
||||
if app.Type == appType || appType == "" {
|
||||
// If type flag is set, check for it, if not, Type == ""
|
||||
tableRow = []string{app.Server, app.Type, app.Domain}
|
||||
if status {
|
||||
if status, ok := statuses[app.StackName()]; ok {
|
||||
tableRow = append(tableRow, status)
|
||||
stackName := app.StackName()
|
||||
status := "unknown"
|
||||
version := "unknown"
|
||||
if statusMeta, ok := statuses[stackName]; ok {
|
||||
if currentVersion, exists := statusMeta["version"]; exists {
|
||||
version = currentVersion
|
||||
}
|
||||
if statusMeta["status"] != "" {
|
||||
status = statusMeta["status"]
|
||||
}
|
||||
tableRow = append(tableRow, status, version)
|
||||
versionedAppsCount++
|
||||
} else {
|
||||
tableRow = append(tableRow, status, version)
|
||||
unversionedAppsCount++
|
||||
}
|
||||
|
||||
var newUpdates []string
|
||||
if version != "unknown" {
|
||||
updates, err := catalogue.GetRecipeCatalogueVersions(app.Type)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
parsedVersion, err := tagcmp.Parse(version)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
for _, update := range updates {
|
||||
parsedUpdate, err := tagcmp.Parse(update)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if update != version && parsedUpdate.IsGreaterThan(parsedVersion) {
|
||||
newUpdates = append(newUpdates, update)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(newUpdates) == 0 {
|
||||
if version == "unknown" {
|
||||
tableRow = append(tableRow, "unknown")
|
||||
} else {
|
||||
tableRow = append(tableRow, "on latest")
|
||||
onLatestCount++
|
||||
}
|
||||
} else {
|
||||
// FIXME: jeezus golang why do you not have a list reverse function
|
||||
for i, j := 0, len(newUpdates)-1; i < j; i, j = i+1, j-1 {
|
||||
newUpdates[i], newUpdates[j] = newUpdates[j], newUpdates[i]
|
||||
}
|
||||
tableRow = append(tableRow, strings.Join(newUpdates, "\n"))
|
||||
canUpgradeCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
table.Append(tableRow)
|
||||
}
|
||||
|
||||
stats := fmt.Sprintf(
|
||||
"Total apps: %v | Versioned: %v | Unversioned: %v | On latest: %v | Can upgrade: %v",
|
||||
len(apps),
|
||||
versionedAppsCount,
|
||||
unversionedAppsCount,
|
||||
onLatestCount,
|
||||
canUpgradeCount,
|
||||
)
|
||||
|
||||
table.SetCaption(true, stats)
|
||||
table.Render()
|
||||
|
||||
return nil
|
||||
|
@ -210,6 +210,10 @@ func action(c *cli.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
if newAppServer == "default" {
|
||||
newAppServer = "local"
|
||||
}
|
||||
|
||||
tableCol := []string{"Name", "Domain", "Type", "Server"}
|
||||
table := abraFormatter.CreateTable(tableCol)
|
||||
table.Append([]string{sanitisedAppName, domain, recipe.Name, newAppServer})
|
||||
|
@ -3,6 +3,7 @@ package app
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
abraFormatter "coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
@ -15,11 +16,50 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var watch bool
|
||||
var watchFlag = &cli.BoolFlag{
|
||||
Name: "watch",
|
||||
Aliases: []string{"w"},
|
||||
Value: false,
|
||||
Usage: "Watch status by polling repeatedly",
|
||||
Destination: &watch,
|
||||
}
|
||||
|
||||
var appPsCommand = &cli.Command{
|
||||
Name: "ps",
|
||||
Usage: "Check app status",
|
||||
Aliases: []string{"p"},
|
||||
Flags: []cli.Flag{
|
||||
watchFlag,
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if !watch {
|
||||
showPSOutput(c)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: how do we make this update in-place in an x-platform way?
|
||||
for {
|
||||
showPSOutput(c)
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
},
|
||||
BashComplete: func(c *cli.Context) {
|
||||
appNames, err := config.GetAppNames()
|
||||
if err != nil {
|
||||
logrus.Warn(err)
|
||||
}
|
||||
if c.NArg() > 0 {
|
||||
return
|
||||
}
|
||||
for _, a := range appNames {
|
||||
fmt.Println(a)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// showPSOutput renders ps output.
|
||||
func showPSOutput(c *cli.Context) {
|
||||
app := internal.ValidateApp(c)
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
@ -35,35 +75,25 @@ var appPsCommand = &cli.Command{
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
tableCol := []string{"id", "image", "command", "created", "status", "ports", "names"}
|
||||
tableCol := []string{"image", "created", "status", "ports", "names"}
|
||||
table := abraFormatter.CreateTable(tableCol)
|
||||
|
||||
for _, container := range containers {
|
||||
var containerNames []string
|
||||
for _, containerName := range container.Names {
|
||||
trimmed := strings.TrimPrefix(containerName, "/")
|
||||
containerNames = append(containerNames, trimmed)
|
||||
}
|
||||
|
||||
tableRow := []string{
|
||||
abraFormatter.ShortenID(container.ID),
|
||||
abraFormatter.RemoveSha(container.Image),
|
||||
abraFormatter.Truncate(container.Command),
|
||||
abraFormatter.HumanDuration(container.Created),
|
||||
container.Status,
|
||||
formatter.DisplayablePorts(container.Ports),
|
||||
strings.Join(container.Names, ", "),
|
||||
strings.Join(containerNames, "\n"),
|
||||
}
|
||||
table.Append(tableRow)
|
||||
}
|
||||
|
||||
table.Render()
|
||||
return nil
|
||||
},
|
||||
BashComplete: func(c *cli.Context) {
|
||||
appNames, err := config.GetAppNames()
|
||||
if err != nil {
|
||||
logrus.Warn(err)
|
||||
}
|
||||
if c.NArg() > 0 {
|
||||
return
|
||||
}
|
||||
for _, a := range appNames {
|
||||
fmt.Println(a)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ var appRemoveCommand = &cli.Command{
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if statuses[app.Name] == "deployed" {
|
||||
if statuses[app.Name]["status"] == "deployed" {
|
||||
logrus.Fatalf("'%s' is still deployed. Run \"abra app %s undeploy\" or pass --force", app.Name, app.Name)
|
||||
}
|
||||
}
|
||||
|
@ -3,14 +3,15 @@ package app
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"context"
|
||||
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
stack "coopcloud.tech/abra/pkg/client/stack"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/tagcmp"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@ -18,8 +19,25 @@ import (
|
||||
var appRollbackCommand = &cli.Command{
|
||||
Name: "rollback",
|
||||
Usage: "Roll an app back to a previous version",
|
||||
Aliases: []string{"r"},
|
||||
ArgsUsage: "[<version>]",
|
||||
Aliases: []string{"r", "downgrade"},
|
||||
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.
|
||||
|
||||
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()
|
||||
if err != nil {
|
||||
@ -34,48 +52,125 @@ var appRollbackCommand = &cli.Command{
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
stackName := app.StackName()
|
||||
|
||||
ctx := context.Background()
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
recipeMeta, err := catalogue.GetRecipeMeta(app.Type)
|
||||
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 len(recipeMeta.Versions) == 0 {
|
||||
logrus.Fatalf("no catalogue versions available for '%s'", app.Type)
|
||||
}
|
||||
|
||||
deployedVersions, isDeployed, err := appPkg.DeployedVersions(ctx, cl, app)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if !isDeployed {
|
||||
logrus.Fatalf("'%s' is not deployed?", app.Name)
|
||||
}
|
||||
if _, exists := deployedVersions["app"]; !exists {
|
||||
logrus.Fatalf("no versioned 'app' service for '%s', cannot determine version", app.Name)
|
||||
|
||||
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
version := c.Args().Get(1)
|
||||
if version == "" {
|
||||
// TODO:
|
||||
// using deployedVersions["app"], get index+1 version from catalogue
|
||||
// otherwise bail out saying there is nothing to rollback to
|
||||
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 {
|
||||
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
|
||||
for i, j := 0, len(availableDowngrades)-1; i < j; i, j = i+1, j-1 {
|
||||
availableDowngrades[i], availableDowngrades[j] = availableDowngrades[j], availableDowngrades[i]
|
||||
}
|
||||
|
||||
var chosenDowngrade string
|
||||
if !internal.Chaos {
|
||||
if internal.Force {
|
||||
chosenDowngrade = availableDowngrades[0]
|
||||
logrus.Debugf("choosing '%s' as version to downgrade to (--force)", chosenDowngrade)
|
||||
} else {
|
||||
// TODO
|
||||
// ensure this version is listed in the catalogue
|
||||
// ensure this version is "older" (lower down in the list)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
// display table of existing state and expected state and prompt
|
||||
// run the deployment with this target version!
|
||||
if !internal.Chaos {
|
||||
if err := recipe.EnsureVersion(app.Type, chosenDowngrade); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Fatal("command not implemented yet, coming soon TM")
|
||||
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")
|
||||
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 !internal.Force {
|
||||
if err := NewVersionOverview(app, deployedVersion, chosenDowngrade); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
|
@ -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) > 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:]
|
||||
|
@ -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 := 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)
|
||||
|
179
cli/app/upgrade.go
Normal file
179
cli/app/upgrade.go
Normal file
@ -0,0 +1,179 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var appUpgradeCommand = &cli.Command{
|
||||
Name: "upgrade",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "Upgrade an app",
|
||||
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
|
||||
new upgrade to an existing app.
|
||||
|
||||
This command specifically supports changing the version of running apps, as
|
||||
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.
|
||||
|
||||
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)
|
||||
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?", app.Name)
|
||||
}
|
||||
|
||||
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if len(versions) == 0 && !internal.Chaos {
|
||||
logrus.Fatalf("no versions available '%s' in recipe catalogue?", app.Type)
|
||||
}
|
||||
|
||||
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 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.Chaos {
|
||||
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")
|
||||
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 := NewVersionOverview(app, deployedVersion, chosenUpgrade); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
BashComplete: func(c *cli.Context) {
|
||||
appNames, err := config.GetAppNames()
|
||||
if err != nil {
|
||||
logrus.Warn(err)
|
||||
}
|
||||
if c.NArg() > 0 {
|
||||
return
|
||||
}
|
||||
for _, a := range appNames {
|
||||
fmt.Println(a)
|
||||
}
|
||||
},
|
||||
}
|
@ -2,12 +2,12 @@ package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
abraFormatter "coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/client/stack"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"github.com/docker/distribution/reference"
|
||||
@ -32,65 +32,61 @@ func getImagePath(image string) (string, error) {
|
||||
var appVersionCommand = &cli.Command{
|
||||
Name: "version",
|
||||
Aliases: []string{"v"},
|
||||
Usage: "Show version of all services in app",
|
||||
Usage: "Show app versions",
|
||||
Description: `
|
||||
This command shows all information about versioning related to a deployed app.
|
||||
This includes the individual image names, tags and digests. But also the Co-op
|
||||
Cloud recipe version.
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
stackName := app.StackName()
|
||||
|
||||
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
opts := stack.Deploy{Composefiles: composeFiles}
|
||||
compose, err := config.GetAppComposeConfig(app.Type, opts, app.Env)
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
ch := make(chan stack.StackStatus, len(compose.Services))
|
||||
for _, service := range compose.Services {
|
||||
label := fmt.Sprintf("coop-cloud.%s.%s.version", app.StackName(), service.Name)
|
||||
go func(s string, l string) {
|
||||
ch <- stack.GetDeployedServicesByLabel(s, l)
|
||||
}(app.Server, label)
|
||||
logrus.Debugf("checking whether '%s' is already deployed", stackName)
|
||||
|
||||
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
tableCol := []string{"name", "image", "version", "digest"}
|
||||
if deployedVersion == "" {
|
||||
logrus.Fatalf("failed to determine version of deployed '%s'", app.Name)
|
||||
}
|
||||
|
||||
if !isDeployed {
|
||||
logrus.Fatalf("'%s' is not deployed?", app.Name)
|
||||
}
|
||||
|
||||
recipeMeta, err := catalogue.GetRecipeMeta(app.Type)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
versionsMeta := make(map[string]catalogue.ServiceMeta)
|
||||
for _, recipeVersion := range recipeMeta.Versions {
|
||||
if currentVersion, exists := recipeVersion[deployedVersion]; exists {
|
||||
versionsMeta = currentVersion
|
||||
}
|
||||
}
|
||||
|
||||
if len(versionsMeta) == 0 {
|
||||
logrus.Fatalf("PANIC: could not retrieve deployed version ('%s') from recipe catalogue?", deployedVersion)
|
||||
}
|
||||
|
||||
tableCol := []string{"name", "image", "version", "tag", "digest"}
|
||||
table := abraFormatter.CreateTable(tableCol)
|
||||
|
||||
statuses := make(map[string]stack.StackStatus)
|
||||
for range compose.Services {
|
||||
status := <-ch
|
||||
if len(status.Services) > 0 {
|
||||
serviceName := appPkg.ParseServiceName(status.Services[0].Spec.Name)
|
||||
statuses[serviceName] = status
|
||||
}
|
||||
}
|
||||
|
||||
sort.SliceStable(compose.Services, func(i, j int) bool {
|
||||
return compose.Services[i].Name < compose.Services[j].Name
|
||||
})
|
||||
|
||||
for _, service := range compose.Services {
|
||||
if status, ok := statuses[service.Name]; ok {
|
||||
statusService := status.Services[0]
|
||||
label := fmt.Sprintf("coop-cloud.%s.%s.version", app.StackName(), service.Name)
|
||||
version, digest := appPkg.ParseVersionLabel(statusService.Spec.Labels[label])
|
||||
image, err := getImagePath(statusService.Spec.Labels["com.docker.stack.image"])
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
table.Append([]string{service.Name, image, version, digest})
|
||||
continue
|
||||
}
|
||||
|
||||
image, err := getImagePath(service.Image)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
table.Append([]string{service.Name, image, "?", "?"})
|
||||
for serviceName, versionMeta := range versionsMeta {
|
||||
table.Append([]string{serviceName, versionMeta.Image, deployedVersion, versionMeta.Tag, versionMeta.Digest})
|
||||
}
|
||||
|
||||
table.Render()
|
||||
|
||||
return nil
|
||||
},
|
||||
BashComplete: func(c *cli.Context) {
|
||||
|
@ -96,7 +96,7 @@ var appVolumeRemoveCommand = &cli.Command{
|
||||
|
||||
var appVolumeCommand = &cli.Command{
|
||||
Name: "volume",
|
||||
Aliases: []string{"v"},
|
||||
Aliases: []string{"vl"},
|
||||
Usage: "Manage app volumes",
|
||||
ArgsUsage: "<command>",
|
||||
Subcommands: []*cli.Command{
|
||||
|
@ -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",
|
||||
|
@ -2,7 +2,9 @@ package catalogue
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"coopcloud.tech/abra/cli/formatter"
|
||||
@ -49,7 +51,18 @@ var catalogueGenerateCommand = &cli.Command{
|
||||
Aliases: []string{"g"},
|
||||
Usage: "Generate a new copy of the catalogue",
|
||||
ArgsUsage: "[<recipe>]",
|
||||
BashComplete: func(c *cli.Context) {},
|
||||
BashComplete: func(c *cli.Context) {
|
||||
catl, err := catalogue.ReadRecipeCatalogue()
|
||||
if err != nil {
|
||||
logrus.Warn(err)
|
||||
}
|
||||
if c.NArg() > 0 {
|
||||
return
|
||||
}
|
||||
for name := range catl {
|
||||
fmt.Println(name)
|
||||
}
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
recipeName := c.Args().First()
|
||||
|
||||
@ -60,18 +73,18 @@ var catalogueGenerateCommand = &cli.Command{
|
||||
|
||||
logrus.Debugf("ensuring '%v' recipe(s) are locally present and up-to-date", len(repos))
|
||||
|
||||
bar := formatter.CreateProgressbar(len(repos), "retrieving recipes...")
|
||||
retrieveBar := formatter.CreateProgressbar(len(repos), "retrieving recipes...")
|
||||
ch := make(chan string, len(repos))
|
||||
for _, repoMeta := range repos {
|
||||
go func(rm catalogue.RepoMeta) {
|
||||
if recipeName != "" && recipeName != rm.Name {
|
||||
ch <- rm.Name
|
||||
bar.Add(1)
|
||||
retrieveBar.Add(1)
|
||||
return
|
||||
}
|
||||
if _, exists := CatalogueSkipList[rm.Name]; exists {
|
||||
ch <- rm.Name
|
||||
bar.Add(1)
|
||||
retrieveBar.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
@ -86,7 +99,7 @@ var catalogueGenerateCommand = &cli.Command{
|
||||
}
|
||||
|
||||
ch <- rm.Name
|
||||
bar.Add(1)
|
||||
retrieveBar.Add(1)
|
||||
}(repoMeta)
|
||||
}
|
||||
|
||||
@ -95,14 +108,23 @@ var catalogueGenerateCommand = &cli.Command{
|
||||
}
|
||||
|
||||
catl := make(catalogue.RecipeCatalogue)
|
||||
catlBar := formatter.CreateProgressbar(len(repos), "generating catalogue...")
|
||||
for _, recipeMeta := range repos {
|
||||
if recipeName != "" && recipeName != recipeMeta.Name {
|
||||
catlBar.Add(1)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, exists := CatalogueSkipList[recipeMeta.Name]; exists {
|
||||
catlBar.Add(1)
|
||||
continue
|
||||
}
|
||||
|
||||
versions, err := catalogue.GetRecipeVersions(recipeMeta.Name)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
catl[recipeMeta.Name] = catalogue.RecipeMeta{
|
||||
Name: recipeMeta.Name,
|
||||
Repository: recipeMeta.CloneURL,
|
||||
@ -110,10 +132,11 @@ var catalogueGenerateCommand = &cli.Command{
|
||||
DefaultBranch: recipeMeta.DefaultBranch,
|
||||
Description: recipeMeta.Description,
|
||||
Website: recipeMeta.Website,
|
||||
// Versions: ..., // FIXME: once the new versions work goes down
|
||||
Versions: versions,
|
||||
// Category: ..., // FIXME: once we sort out the machine-readable catalogue interface
|
||||
// Features: ..., // FIXME: once we figure out the machine-readable catalogue interface
|
||||
}
|
||||
catlBar.Add(1)
|
||||
}
|
||||
|
||||
recipesJSON, err := json.MarshalIndent(catl, "", " ")
|
||||
@ -121,11 +144,29 @@ var catalogueGenerateCommand = &cli.Command{
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(config.APPS_JSON); err != nil && os.IsNotExist(err) {
|
||||
if err := ioutil.WriteFile(config.APPS_JSON, recipesJSON, 0644); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
if recipeName != "" {
|
||||
catlFS, err := catalogue.ReadRecipeCatalogue()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
catlFS[recipeName] = catl[recipeName]
|
||||
|
||||
logrus.Debugf("generated new recipe catalogue in '%s'", config.APPS_JSON)
|
||||
updatedRecipesJSON, err := json.MarshalIndent(catlFS, "", " ")
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile(config.APPS_JSON, updatedRecipesJSON, 0644); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Infof("generated new recipe catalogue in '%s'", config.APPS_JSON)
|
||||
|
||||
return nil
|
||||
},
|
||||
|
37
cli/cli.go
37
cli/cli.go
@ -4,11 +4,14 @@ package cli
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"coopcloud.tech/abra/cli/app"
|
||||
"coopcloud.tech/abra/cli/catalogue"
|
||||
"coopcloud.tech/abra/cli/domain"
|
||||
"coopcloud.tech/abra/cli/recipe"
|
||||
"coopcloud.tech/abra/cli/server"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
logrusStack "github.com/Gurpartap/logrus-stack"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
@ -38,8 +41,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 🎩🐇
|
||||
@ -57,7 +59,7 @@ func RunApp(version, commit string) {
|
||||
server.ServerCommand,
|
||||
recipe.RecipeCommand,
|
||||
catalogue.CatalogueCommand,
|
||||
VersionCommand,
|
||||
domain.DomainCommand,
|
||||
UpgradeCommand,
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
@ -81,10 +83,35 @@ func RunApp(version, commit string) {
|
||||
logrus.SetOutput(os.Stderr)
|
||||
logrus.AddHook(logrusStack.StandardHook())
|
||||
}
|
||||
return nil
|
||||
|
||||
paths := []string{
|
||||
config.ABRA_DIR,
|
||||
path.Join(config.ABRA_DIR, "servers"),
|
||||
path.Join(config.ABRA_DIR, "apps"),
|
||||
path.Join(config.ABRA_DIR, "vendor"),
|
||||
}
|
||||
|
||||
logrus.Debugf("Flying abra version '%s', commit '%s', enjoy the ride", version, commit)
|
||||
for _, path := range paths {
|
||||
if err := os.Mkdir(path, 0755); err != nil {
|
||||
if !os.IsExist(err) {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Debugf("'%s' already created, moving on...", path)
|
||||
continue
|
||||
}
|
||||
logrus.Debugf("'%s' is missing, creating...", path)
|
||||
}
|
||||
|
||||
logrus.Debugf("abra version '%s', commit '%s'", version, commit)
|
||||
|
||||
return nil
|
||||
}
|
||||
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)
|
||||
|
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"},
|
||||
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)
|
||||
// }
|
@ -41,6 +41,10 @@ func ValidateApp(c *cli.Context) config.App {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := recipe.EnsureExists(app.Type); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Debugf("validated '%s' as app argument", appName)
|
||||
|
||||
return app
|
||||
|
@ -4,10 +4,34 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var Major bool
|
||||
var MajorFlag = &cli.BoolFlag{
|
||||
Name: "major",
|
||||
Value: false,
|
||||
Aliases: []string{"ma", "x"},
|
||||
Destination: &Major,
|
||||
}
|
||||
|
||||
var Minor bool
|
||||
var MinorFlag = &cli.BoolFlag{
|
||||
Name: "minor",
|
||||
Value: false,
|
||||
Aliases: []string{"mi", "y"},
|
||||
Destination: &Minor,
|
||||
}
|
||||
|
||||
var Patch bool
|
||||
var PatchFlag = &cli.BoolFlag{
|
||||
Name: "patch",
|
||||
Value: false,
|
||||
Aliases: []string{"p", "z"},
|
||||
Destination: &Patch,
|
||||
}
|
||||
|
||||
// RecipeCommand defines all recipe related sub-commands.
|
||||
var RecipeCommand = &cli.Command{
|
||||
Name: "recipe",
|
||||
Usage: "Manage recipes",
|
||||
Usage: "Manage recipes (for maintainers)",
|
||||
ArgsUsage: "<recipe>",
|
||||
Aliases: []string{"r"},
|
||||
Description: `
|
||||
|
@ -9,11 +9,11 @@ import (
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@ -33,34 +33,10 @@ var DryFlag = &cli.BoolFlag{
|
||||
Destination: &Dry,
|
||||
}
|
||||
|
||||
var Major bool
|
||||
var MajorFlag = &cli.BoolFlag{
|
||||
Name: "major",
|
||||
Value: false,
|
||||
Aliases: []string{"ma", "x"},
|
||||
Destination: &Major,
|
||||
}
|
||||
|
||||
var Minor bool
|
||||
var MinorFlag = &cli.BoolFlag{
|
||||
Name: "minor",
|
||||
Value: false,
|
||||
Aliases: []string{"mi", "y"},
|
||||
Destination: &Minor,
|
||||
}
|
||||
|
||||
var Patch bool
|
||||
var PatchFlag = &cli.BoolFlag{
|
||||
Name: "patch",
|
||||
Value: false,
|
||||
Aliases: []string{"p", "z"},
|
||||
Destination: &Patch,
|
||||
}
|
||||
|
||||
var CommitMessage string
|
||||
var CommitMessageFlag = &cli.StringFlag{
|
||||
Name: "commit-message",
|
||||
Usage: "commit message",
|
||||
Usage: "commit message. Implies --commit",
|
||||
Aliases: []string{"cm"},
|
||||
Destination: &CommitMessage,
|
||||
}
|
||||
@ -68,11 +44,20 @@ var CommitMessageFlag = &cli.StringFlag{
|
||||
var Commit bool
|
||||
var CommitFlag = &cli.BoolFlag{
|
||||
Name: "commit",
|
||||
Usage: "add compose.yml to staging area and commit changes",
|
||||
Value: false,
|
||||
Aliases: []string{"c"},
|
||||
Destination: &Commit,
|
||||
}
|
||||
|
||||
var TagMessage string
|
||||
var TagMessageFlag = &cli.StringFlag{
|
||||
Name: "tag-comment",
|
||||
Usage: "tag comment. If not given, user will be asked for it",
|
||||
Aliases: []string{"t", "tm"},
|
||||
Destination: &TagMessage,
|
||||
}
|
||||
|
||||
var recipeReleaseCommand = &cli.Command{
|
||||
Name: "release",
|
||||
Usage: "tag a recipe",
|
||||
@ -107,6 +92,7 @@ or a rollback of an app.
|
||||
PushFlag,
|
||||
CommitFlag,
|
||||
CommitMessageFlag,
|
||||
TagMessageFlag,
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
recipe := internal.ValidateRecipe(c)
|
||||
@ -115,17 +101,33 @@ or a rollback of an app.
|
||||
imagesTmp := getImageVersions(recipe)
|
||||
mainApp := getMainApp(recipe)
|
||||
mainAppVersion := imagesTmp[mainApp]
|
||||
|
||||
if err := recipePkg.EnsureExists(recipe.Name); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if mainAppVersion == "" {
|
||||
logrus.Fatal("main app version is empty?")
|
||||
}
|
||||
|
||||
if tagstring != "" {
|
||||
_, err := tagcmp.Parse(tagstring)
|
||||
if err != nil {
|
||||
if _, err := tagcmp.Parse(tagstring); err != nil {
|
||||
logrus.Fatal("invalid tag specified")
|
||||
}
|
||||
}
|
||||
|
||||
if TagMessage == "" {
|
||||
prompt := &survey.Input{
|
||||
Message: "tag message",
|
||||
}
|
||||
if err := survey.AskOne(prompt, &TagMessage); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var createTagOptions git.CreateTagOptions
|
||||
createTagOptions.Message = TagMessage
|
||||
|
||||
if Commit || (CommitMessage != "") {
|
||||
commitRepo, err := git.PlainOpen(directory)
|
||||
if err != nil {
|
||||
@ -140,8 +142,16 @@ 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 {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Debug("staged compose.**yml for commit")
|
||||
|
||||
_, err = commitWorktree.Commit(CommitMessage, &git.CommitOptions{})
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
@ -189,9 +199,7 @@ or a rollback of an app.
|
||||
return nil
|
||||
}
|
||||
|
||||
repo.CreateTag(tagstring, head.Hash(), nil) /* &git.CreateTagOptions{
|
||||
Message: tag,
|
||||
})*/
|
||||
repo.CreateTag(tagstring, head.Hash(), &createTagOptions)
|
||||
logrus.Info(fmt.Sprintf("created tag %s at %s", tagstring, head.Hash()))
|
||||
if Push {
|
||||
if err := repo.Push(&git.PushOptions{}); err != nil {
|
||||
@ -204,27 +212,33 @@ or a rollback of an app.
|
||||
}
|
||||
|
||||
// get the latest tag with its hash, name etc
|
||||
var lastGitTag *object.Tag
|
||||
var lastGitTag tagcmp.Tag
|
||||
iter, err := repo.Tags()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if err := iter.ForEach(func(ref *plumbing.Reference) error {
|
||||
obj, err := repo.TagObject(ref.Hash())
|
||||
if err == nil {
|
||||
lastGitTag = obj
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
}
|
||||
tagcmpTag, err := tagcmp.Parse(obj.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if (lastGitTag == tagcmp.Tag{}) {
|
||||
lastGitTag = tagcmpTag
|
||||
} else if tagcmpTag.IsGreaterThan(lastGitTag) {
|
||||
lastGitTag = tagcmpTag
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
newTag, err := tagcmp.Parse(lastGitTag.Name)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(lastGitTag)
|
||||
|
||||
newTag := lastGitTag
|
||||
var newTagString string
|
||||
if bumpType > 0 {
|
||||
if Patch {
|
||||
@ -238,28 +252,28 @@ or a rollback of an app.
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
newTag.Patch = "0"
|
||||
newTag.Minor = strconv.Itoa(now + 1)
|
||||
} else if Major {
|
||||
now, err := strconv.Atoi(newTag.Major)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
newTag.Patch = "0"
|
||||
newTag.Minor = "0"
|
||||
newTag.Major = strconv.Itoa(now + 1)
|
||||
}
|
||||
newTagString = newTag.String()
|
||||
} else {
|
||||
logrus.Fatal("we don't support automatic tag generation yet - specify a version or use one of: --major --minor --patch")
|
||||
}
|
||||
|
||||
newTagString = fmt.Sprintf("%s+%s", newTagString, mainAppVersion)
|
||||
newTag.Metadata = mainAppVersion
|
||||
newTagString = newTag.String()
|
||||
if Dry {
|
||||
logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", newTagString, head.Hash()))
|
||||
return nil
|
||||
}
|
||||
|
||||
repo.CreateTag(newTagString, head.Hash(), nil) /* &git.CreateTagOptions{
|
||||
Message: tag,
|
||||
})*/
|
||||
repo.CreateTag(newTagString, head.Hash(), &createTagOptions)
|
||||
logrus.Info(fmt.Sprintf("created tag %s at %s", newTagString, head.Hash()))
|
||||
if Push {
|
||||
if err := repo.Push(&git.PushOptions{}); err != nil {
|
||||
@ -277,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,64 +1,87 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"github.com/docker/distribution/reference"
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var recipeSyncCommand = &cli.Command{
|
||||
Name: "sync",
|
||||
Usage: "Generate new recipe labels",
|
||||
Usage: "Ensure recipe version labels are up-to-date",
|
||||
Aliases: []string{"s"},
|
||||
ArgsUsage: "<recipe> <version>",
|
||||
Description: `
|
||||
This command will generate labels for each service which correspond to the
|
||||
This command will generate labels for the main recipe service (i.e. by
|
||||
convention, typically the service named "app") which corresponds to the
|
||||
following format:
|
||||
|
||||
coop-cloud.${STACK_NAME}.${SERVICE_NAME}.version=${IMAGE_TAG}-${IMAGE_DIGEST}
|
||||
coop-cloud.${STACK_NAME}.version=<version>
|
||||
|
||||
The <recipe> configuration will be updated on the local file system. These
|
||||
labels are consumed by abra in other command invocations and used to determine
|
||||
the versioning metadata of up-and-running containers are.
|
||||
The <version> is determined by the recipe maintainer and is specified on the
|
||||
command-line. The <recipe> configuration will be updated on the local file
|
||||
system.
|
||||
`,
|
||||
ArgsUsage: "<recipe>",
|
||||
BashComplete: func(c *cli.Context) {
|
||||
catl, err := catalogue.ReadRecipeCatalogue()
|
||||
if err != nil {
|
||||
logrus.Warn(err)
|
||||
}
|
||||
if c.NArg() > 0 {
|
||||
return
|
||||
}
|
||||
for name := range catl {
|
||||
fmt.Println(name)
|
||||
}
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.Args().Len() != 2 {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("missing <recipe>/<version> arguments?"))
|
||||
}
|
||||
|
||||
recipe := internal.ValidateRecipe(c)
|
||||
|
||||
// TODO: validate with tagcmp when new commits come in
|
||||
// See https://git.coopcloud.tech/coop-cloud/abra/pulls/109
|
||||
nextTag := c.Args().Get(1)
|
||||
|
||||
mainService := "app"
|
||||
var services []string
|
||||
hasAppService := false
|
||||
for _, service := range recipe.Config.Services {
|
||||
services = append(services, service.Name)
|
||||
if service.Name == "app" {
|
||||
hasAppService = true
|
||||
logrus.Debugf("detected app service in '%s'", recipe.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasAppService {
|
||||
logrus.Fatal(fmt.Sprintf("no 'app' service defined in '%s'", recipe.Name))
|
||||
logrus.Warnf("no 'app' service defined in '%s'", recipe.Name)
|
||||
var chosenService string
|
||||
prompt := &survey.Select{
|
||||
Message: fmt.Sprintf("what is the main service name for '%s'?", recipe.Name),
|
||||
Options: services,
|
||||
}
|
||||
|
||||
for _, service := range recipe.Config.Services {
|
||||
img, err := reference.ParseNormalizedNamed(service.Image)
|
||||
if err != nil {
|
||||
if err := survey.AskOne(prompt, &chosenService); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Debugf("detected image '%s' for service '%s'", img, service.Name)
|
||||
mainService = chosenService
|
||||
}
|
||||
|
||||
digest, err := client.GetTagDigest(img)
|
||||
if err != nil {
|
||||
logrus.Debugf("selecting '%s' as the service to sync version labels", mainService)
|
||||
|
||||
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag)
|
||||
if err := recipe.UpdateLabel(mainService, label); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Debugf("retrieved digest '%s' for '%s'", digest, img)
|
||||
|
||||
tag := img.(reference.NamedTagged).Tag()
|
||||
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.%s.version=%s-%s", service.Name, tag, digest)
|
||||
if err := recipe.UpdateLabel(service.Name, label); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Debugf("added label '%s' to service '%s'", label, service.Name)
|
||||
}
|
||||
logrus.Infof("synced label '%s' to service '%s'", label, mainService)
|
||||
|
||||
return nil
|
||||
},
|
||||
|
@ -27,14 +27,24 @@ update the relevant compose file tags on the local file system.
|
||||
Some image tags cannot be parsed because they do not follow some sort of
|
||||
semver-like convention. In this case, all possible tags will be listed and it
|
||||
is up to the end-user to decide.
|
||||
|
||||
This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync
|
||||
<recipe>".
|
||||
`,
|
||||
ArgsUsage: "<recipe>",
|
||||
Flags: []cli.Flag{
|
||||
PatchFlag,
|
||||
MinorFlag,
|
||||
MajorFlag,
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
recipe := internal.ValidateRecipe(c)
|
||||
|
||||
bumpType := btoi(Major)*4 + btoi(Minor)*2 + btoi(Patch)
|
||||
if bumpType != 0 {
|
||||
// a bitwise check if the number is a power of 2
|
||||
if (bumpType & (bumpType - 1)) != 0 {
|
||||
logrus.Fatal("you can only use one of: --major, --minor, --patch.")
|
||||
}
|
||||
}
|
||||
|
||||
for _, service := range recipe.Config.Services {
|
||||
catlVersions, err := catalogue.VersionsOfService(recipe.Name, service.Name)
|
||||
if err != nil {
|
||||
@ -107,7 +117,24 @@ This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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()
|
||||
@ -119,7 +146,6 @@ This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync
|
||||
}
|
||||
}
|
||||
|
||||
var upgradeTag string
|
||||
prompt := &survey.Select{
|
||||
Message: msg,
|
||||
Options: compatibleStrings,
|
||||
@ -127,11 +153,12 @@ This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync
|
||||
if err := survey.AskOne(prompt, &upgradeTag); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := recipe.UpdateTag(image, upgradeTag); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
logrus.Debugf("tag updated from '%s' to '%s' for '%s'", image, upgradeTag, recipe.Name)
|
||||
logrus.Infof("tag upgraded from '%s' to '%s' for '%s'", tag.String(), upgradeTag, image)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -3,14 +3,13 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/server"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@ -30,10 +29,14 @@ var serverAddCommand = &cli.Command{
|
||||
Description: `
|
||||
This command adds a new server that abra will communicate with, to deploy apps.
|
||||
|
||||
The <domain> argument must be a publicy accessible domain name which points to
|
||||
your server. You should have SSH access to this server, Abra will assume port
|
||||
22 and will use your current system username to make an initial connection. You
|
||||
can use the <user> and <port> arguments to adjust this.
|
||||
If "--local" is passed, then Abra assumes that the current local server is
|
||||
intended as the target server.
|
||||
|
||||
Otherwise, you may specify a remote server. The <domain> argument must be a
|
||||
publicy accessible domain name which points to your server. You should have SSH
|
||||
access to this server, Abra will assume port 22 and will use your current
|
||||
system username to make an initial connection. You can use the <user> and
|
||||
<port> arguments to adjust this.
|
||||
|
||||
For example:
|
||||
|
||||
@ -44,9 +47,6 @@ Abra will construct the following SSH connection string then:
|
||||
ssh://globemodem@varia.zone:12345
|
||||
|
||||
All communication between Abra and the server will use this SSH connection.
|
||||
|
||||
NOTE: If you specify --local, none of the above applies 🙃
|
||||
|
||||
`,
|
||||
Aliases: []string{"a"},
|
||||
Flags: []cli.Flag{
|
||||
@ -54,12 +54,13 @@ NOTE: If you specify --local, none of the above applies 🙃
|
||||
},
|
||||
ArgsUsage: "<domain> [<user>] [<port>]",
|
||||
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'")
|
||||
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")
|
||||
internal.ShowSubcommandHelpAndError(c, err)
|
||||
}
|
||||
@ -67,7 +68,13 @@ NOTE: If you specify --local, none of the above applies 🙃
|
||||
domainName := "default"
|
||||
|
||||
if local {
|
||||
os.Mkdir(path.Join(config.ABRA_DIR, "servers", domainName), 0755)
|
||||
if err := server.CreateServerDir(domainName); err != nil {
|
||||
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")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -111,6 +118,10 @@ NOTE: If you specify --local, none of the above applies 🙃
|
||||
ctx := context.Background()
|
||||
cl, err := client.New(domainName)
|
||||
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)
|
||||
}
|
||||
|
||||
@ -118,15 +129,22 @@ NOTE: If you specify --local, none of the above applies 🙃
|
||||
if strings.Contains(err.Error(), "command not found") {
|
||||
logrus.Fatalf("docker is not installed on '%s'?", domainName)
|
||||
} 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.Debug(err)
|
||||
}
|
||||
|
||||
logrus.Debugf("remote connection to '%s' is definitely up", domainName)
|
||||
logrus.Infof("server at '%s' has been added", domainName)
|
||||
|
||||
os.Mkdir(path.Join(config.ABRA_DIR, "servers", domainName), 0755)
|
||||
if err := server.CreateServerDir(domainName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Infof("server at '%s' has been added", domainName)
|
||||
|
||||
return nil
|
||||
},
|
||||
|
@ -35,6 +35,9 @@ later for more advanced use cases.
|
||||
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) {
|
||||
@ -42,10 +45,10 @@ later for more advanced use cases.
|
||||
Timeout: time.Millisecond * time.Duration(10000),
|
||||
}
|
||||
// comrade librehosters DNS resolver https://snopyta.org/service/dns/
|
||||
return d.DialContext(ctx, "udp", "95.216.24.230:53")
|
||||
return d.DialContext(ctx, "udp", freifunkDNS)
|
||||
},
|
||||
}
|
||||
logrus.Debugf("created DNS resolver via 95.216.24.230")
|
||||
logrus.Debugf("created DNS resolver via '%s'", freifunkDNS)
|
||||
|
||||
ctx := context.Background()
|
||||
ips, err := resolver.LookupIPAddr(ctx, domainName)
|
||||
|
@ -45,7 +45,11 @@ var serverListCommand = &cli.Command{
|
||||
}
|
||||
}
|
||||
if len(row) == 0 {
|
||||
row = []string{serverName, "UNKNOWN"}
|
||||
if serverName == "default" {
|
||||
row = []string{serverName, "local"}
|
||||
} else {
|
||||
row = []string{serverName, "unknown"}
|
||||
}
|
||||
}
|
||||
table.Append(row)
|
||||
}
|
||||
|
@ -1,17 +1,13 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"coopcloud.tech/abra/cli/formatter"
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/libcapsul"
|
||||
"github.com/hetznercloud/hcloud-go/hcloud"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
@ -182,8 +178,8 @@ environment variable or otherwise passing the "--env/-e" flag.
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
name := c.Args().First()
|
||||
if name == "" {
|
||||
capsulName := c.Args().First()
|
||||
if capsulName == "" {
|
||||
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")
|
||||
}
|
||||
|
||||
// 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)
|
||||
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 {
|
||||
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"}
|
||||
table := formatter.CreateTable(tableColumns)
|
||||
table.Append([]string{name, resp.ID})
|
||||
table.Append([]string{capsulName, resp.ID})
|
||||
table.Render()
|
||||
|
||||
return nil
|
||||
|
@ -8,7 +8,7 @@ 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.
|
||||
|
||||
|
@ -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
|
||||
},
|
||||
}
|
67
go.mod
67
go.mod
@ -1,9 +1,9 @@
|
||||
module coopcloud.tech/abra
|
||||
|
||||
go 1.17
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
coopcloud.tech/tagcmp v0.0.0-20210906102006-2a8edd82d75d
|
||||
coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb
|
||||
github.com/AlecAivazis/survey/v2 v2.3.1
|
||||
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4
|
||||
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
|
||||
@ -25,75 +25,20 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.17 // indirect
|
||||
coopcloud.tech/libcapsul v0.0.0-20211020153234-f1386b5cf79d
|
||||
github.com/Microsoft/hcsshim v0.8.21 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect
|
||||
github.com/acomagu/bufpipe v1.0.3 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/containerd/cgroups v1.0.1 // indirect
|
||||
github.com/containerd/containerd v1.5.5 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
||||
github.com/fvbommel/sortorder v1.0.2 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.3.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
|
||||
github.com/golang/protobuf v1.5.0 // indirect
|
||||
github.com/google/go-cmp v0.5.5 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
|
||||
github.com/mattn/go-colorable v0.1.2 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||
github.com/miekg/pkcs11 v1.0.3 // indirect
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.1.2 // 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/mountinfo v0.4.1 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||
github.com/opencontainers/runc v1.0.2 // indirect
|
||||
github.com/prometheus/client_golang v1.11.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.26.0 // indirect
|
||||
github.com/prometheus/procfs v0.6.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/spf13/cobra v1.0.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.0 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
go.opencensus.io v0.22.3 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
|
||||
golang.org/x/net v0.0.0-20210326060303-6b1517762897 // indirect
|
||||
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 // indirect
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
|
||||
golang.org/x/text v0.3.4 // indirect
|
||||
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect
|
||||
google.golang.org/grpc v1.33.2 // indirect
|
||||
google.golang.org/protobuf v1.26.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0
|
||||
)
|
||||
|
11
go.sum
11
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-20210906102006-2a8edd82d75d h1:5jeUIiToqQ7vTlLeycdGp4Ezurd6/RTNl5K38usHtoo=
|
||||
coopcloud.tech/tagcmp v0.0.0-20210906102006-2a8edd82d75d/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
|
||||
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/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=
|
||||
@ -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/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=
|
||||
|
@ -9,12 +9,17 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/web"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -49,13 +54,16 @@ type tag = string
|
||||
// service represents a service within a recipe.
|
||||
type service = string
|
||||
|
||||
// serviceMeta represents meta info associated with a service.
|
||||
type serviceMeta struct {
|
||||
// ServiceMeta represents meta info associated with a service.
|
||||
type ServiceMeta struct {
|
||||
Digest string `json:"digest"`
|
||||
Image string `json:"image"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
// RecipeVersions are the versions associated with a recipe.
|
||||
type RecipeVersions []map[tag]map[service]ServiceMeta
|
||||
|
||||
// RecipeMeta represents metadata for a recipe in the abra catalogue.
|
||||
type RecipeMeta struct {
|
||||
Category string `json:"category"`
|
||||
@ -65,7 +73,7 @@ type RecipeMeta struct {
|
||||
Icon string `json:"icon"`
|
||||
Name string `json:"name"`
|
||||
Repository string `json:"repository"`
|
||||
Versions []map[tag]map[service]serviceMeta `json:"versions"`
|
||||
Versions RecipeVersions `json:"versions"`
|
||||
Website string `json:"website"`
|
||||
}
|
||||
|
||||
@ -365,3 +373,128 @@ func ReadReposMetadata() (RepoCatalogue, error) {
|
||||
|
||||
return reposMeta, nil
|
||||
}
|
||||
|
||||
// GetRecipeVersions retrieves all recipe versions.
|
||||
func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
|
||||
versions := RecipeVersions{}
|
||||
|
||||
recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName)
|
||||
|
||||
logrus.Debugf("attempting to open git repository in '%s'", recipeDir)
|
||||
|
||||
repo, err := git.PlainOpen(recipeDir)
|
||||
if err != nil {
|
||||
return versions, err
|
||||
}
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
gitTags, err := repo.Tags()
|
||||
if err != nil {
|
||||
return versions, err
|
||||
}
|
||||
|
||||
if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
|
||||
tag := strings.TrimPrefix(string(ref.Name()), "refs/tags/")
|
||||
|
||||
logrus.Debugf("processing '%s' for '%s'", tag, recipeName)
|
||||
|
||||
checkOutOpts := &git.CheckoutOptions{
|
||||
Create: false,
|
||||
Force: true,
|
||||
Branch: plumbing.ReferenceName(ref.Name()),
|
||||
}
|
||||
if err := worktree.Checkout(checkOutOpts); err != nil {
|
||||
logrus.Debugf("failed to check out '%s' in '%s'", tag, recipeDir)
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debugf("successfully checked out '%s' in '%s'", ref.Name(), recipeDir)
|
||||
|
||||
recipe, err := recipe.Get(recipeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
versionMeta := make(map[string]ServiceMeta)
|
||||
for _, service := range recipe.Config.Services {
|
||||
|
||||
img, err := reference.ParseNormalizedNamed(service.Image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := reference.Path(img)
|
||||
if strings.Contains(path, "library") {
|
||||
path = strings.Split(path, "/")[1]
|
||||
}
|
||||
|
||||
digest, err := client.GetTagDigest(img)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
versionMeta[service.Name] = ServiceMeta{
|
||||
Digest: digest,
|
||||
Image: path,
|
||||
Tag: img.(reference.NamedTagged).Tag(),
|
||||
}
|
||||
|
||||
logrus.Debugf("collecting digest: '%s', image: '%s', tag: '%s'", digest, path, tag)
|
||||
}
|
||||
|
||||
versions = append(versions, map[string]map[string]ServiceMeta{tag: versionMeta})
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return versions, err
|
||||
}
|
||||
|
||||
branch := "master"
|
||||
if _, err := repo.Branch("master"); err != nil {
|
||||
if _, err := repo.Branch("main"); err != nil {
|
||||
logrus.Debugf("failed to select branch in '%s'", recipeDir)
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
branch = "main"
|
||||
}
|
||||
|
||||
refName := fmt.Sprintf("refs/heads/%s", branch)
|
||||
checkOutOpts := &git.CheckoutOptions{
|
||||
Create: false,
|
||||
Force: true,
|
||||
Branch: plumbing.ReferenceName(refName),
|
||||
}
|
||||
if err := worktree.Checkout(checkOutOpts); err != nil {
|
||||
logrus.Debugf("failed to check out '%s' in '%s'", branch, recipeDir)
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Debugf("switched back to '%s' in '%s'", branch, recipeDir)
|
||||
logrus.Debugf("collected '%s' for '%s'", versions, recipeName)
|
||||
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
// GetRecipeCatalogueVersions list the recipe versions listed in the recipe catalogue.
|
||||
func GetRecipeCatalogueVersions(recipeName string) ([]string, error) {
|
||||
var versions []string
|
||||
|
||||
catl, err := ReadRecipeCatalogue()
|
||||
if err != nil {
|
||||
return versions, err
|
||||
}
|
||||
|
||||
if recipeMeta, exists := catl[recipeName]; exists {
|
||||
for _, versionMeta := range recipeMeta.Versions {
|
||||
for tag := range versionMeta {
|
||||
versions = append(versions, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return versions, nil
|
||||
}
|
||||
|
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 (
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// New initiates a new Docker client.
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -29,12 +36,12 @@ func New(contextName string) (*client.Client, error) {
|
||||
},
|
||||
}
|
||||
|
||||
var clientOpts []client.Opt
|
||||
clientOpts = append(clientOpts,
|
||||
client.WithHTTPClient(httpClient),
|
||||
client.WithHost(helper.Host),
|
||||
client.WithDialContext(helper.Dialer),
|
||||
)
|
||||
}
|
||||
|
||||
version := os.Getenv("DOCKER_API_VERSION")
|
||||
if version != "" {
|
||||
@ -45,10 +52,18 @@ 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)
|
||||
|
||||
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"
|
||||
"github.com/docker/cli/cli/context/docker"
|
||||
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/sirupsen/logrus"
|
||||
)
|
||||
@ -94,7 +97,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 +110,47 @@ 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
|
||||
}
|
||||
|
||||
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) {
|
||||
var testDockerContexts = []TestContext{
|
||||
dockerContext("ssh://foobar", "docker"),
|
||||
|
@ -2,6 +2,7 @@ package stack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
abraClient "coopcloud.tech/abra/pkg/client"
|
||||
@ -92,6 +93,50 @@ func GetAllDeployedServices(contextName string) StackStatus {
|
||||
return StackStatus{services, nil}
|
||||
}
|
||||
|
||||
// GetDeployedServicesByName filters services by name
|
||||
func GetDeployedServicesByName(ctx context.Context, cl *dockerclient.Client, stackName, serviceName string) StackStatus {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", fmt.Sprintf("%s_%s", stackName, serviceName))
|
||||
|
||||
services, err := cl.ServiceList(ctx, types.ServiceListOptions{Filters: filters})
|
||||
if err != nil {
|
||||
return StackStatus{[]swarm.Service{}, err}
|
||||
}
|
||||
|
||||
return StackStatus{services, nil}
|
||||
}
|
||||
|
||||
// IsDeployed chekcks whether an appp is deployed or not.
|
||||
func IsDeployed(ctx context.Context, cl *dockerclient.Client, stackName string) (bool, string, error) {
|
||||
version := ""
|
||||
isDeployed := false
|
||||
|
||||
filter := filters.NewArgs()
|
||||
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
|
||||
|
||||
services, err := cl.ServiceList(ctx, types.ServiceListOptions{Filters: filter})
|
||||
if err != nil {
|
||||
return false, version, err
|
||||
}
|
||||
|
||||
if len(services) > 0 {
|
||||
for _, service := range services {
|
||||
labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName)
|
||||
if deployedVersion, ok := service.Spec.Labels[labelKey]; ok {
|
||||
version = deployedVersion
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Debugf("'%s' has been detected as deployed with version '%s'", stackName, version)
|
||||
|
||||
return true, version, nil
|
||||
}
|
||||
|
||||
logrus.Debugf("'%s' has been detected as not deployed", stackName)
|
||||
return isDeployed, version, nil
|
||||
}
|
||||
|
||||
// pruneServices removes services that are no longer referenced in the source
|
||||
func pruneServices(ctx context.Context, cl *dockerclient.Client, namespace convert.Namespace, services map[string]struct{}) {
|
||||
oldServices, err := GetStackServices(ctx, cl, namespace.Name())
|
||||
|
@ -3,18 +3,20 @@ package compose
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/client/stack"
|
||||
loader "coopcloud.tech/abra/pkg/client/stack"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// UpdateTag updates an image tag in-place on file system local compose files.
|
||||
func UpdateTag(pattern, image, tag string) error {
|
||||
func UpdateTag(pattern, image, tag, recipeName string) error {
|
||||
composeFiles, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -24,8 +26,14 @@ func UpdateTag(pattern, image, tag string) error {
|
||||
|
||||
for _, composeFile := range composeFiles {
|
||||
opts := stack.Deploy{Composefiles: []string{composeFile}}
|
||||
emptyEnv := make(map[string]string)
|
||||
compose, err := loader.LoadComposefile(opts, emptyEnv)
|
||||
|
||||
envSamplePath := path.Join(config.ABRA_DIR, "apps", recipeName, ".env.sample")
|
||||
sampleEnv, err := config.ReadEnv(envSamplePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
compose, err := loader.LoadComposefile(opts, sampleEnv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -74,7 +82,7 @@ func UpdateTag(pattern, image, tag string) error {
|
||||
}
|
||||
|
||||
// UpdateLabel updates a label in-place on file system local compose files.
|
||||
func UpdateLabel(pattern, serviceName, label string) error {
|
||||
func UpdateLabel(pattern, serviceName, label, recipeName string) error {
|
||||
composeFiles, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -84,8 +92,14 @@ func UpdateLabel(pattern, serviceName, label string) error {
|
||||
|
||||
for _, composeFile := range composeFiles {
|
||||
opts := stack.Deploy{Composefiles: []string{composeFile}}
|
||||
emptyEnv := make(map[string]string)
|
||||
compose, err := loader.LoadComposefile(opts, emptyEnv)
|
||||
|
||||
envSamplePath := path.Join(config.ABRA_DIR, "apps", recipeName, ".env.sample")
|
||||
sampleEnv, err := config.ReadEnv(envSamplePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
compose, err := loader.LoadComposefile(opts, sampleEnv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -110,7 +124,7 @@ func UpdateLabel(pattern, serviceName, label string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
old := fmt.Sprintf("coop-cloud.${STACK_NAME}.%s.version=%s", service.Name, value)
|
||||
old := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", value)
|
||||
replacedBytes := strings.Replace(string(bytes), old, label, -1)
|
||||
|
||||
logrus.Debugf("updating '%s' to '%s' in '%s'", old, label, compose.Filename)
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/formatter"
|
||||
@ -46,7 +45,12 @@ type App struct {
|
||||
|
||||
// StackName gets what the docker safe stack name is for the app
|
||||
func (a App) StackName() string {
|
||||
return SanitiseAppName(a.Name)
|
||||
if _, exists := a.Env["STACK_NAME"]; exists {
|
||||
return a.Env["STACK_NAME"]
|
||||
}
|
||||
stackName := SanitiseAppName(a.Name)
|
||||
a.Env["STACK_NAME"] = stackName
|
||||
return stackName
|
||||
}
|
||||
|
||||
// SORTING TYPES
|
||||
@ -139,7 +143,7 @@ func LoadAppFiles(servers ...string) (AppFiles, error) {
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Debugf("collecting metadata from '%v' servers: '%s'", len(servers), servers)
|
||||
logrus.Debugf("collecting metadata from '%v' servers: '%s'", len(servers), strings.Join(servers, ", "))
|
||||
|
||||
for _, server := range servers {
|
||||
serverDir := path.Join(ABRA_SERVER_FOLDER, server)
|
||||
@ -276,8 +280,8 @@ func SanitiseAppName(name string) string {
|
||||
}
|
||||
|
||||
// GetAppStatuses queries servers to check the deployment status of given apps
|
||||
func GetAppStatuses(appFiles AppFiles) (map[string]string, error) {
|
||||
statuses := map[string]string{}
|
||||
func GetAppStatuses(appFiles AppFiles) (map[string]map[string]string, error) {
|
||||
statuses := make(map[string]map[string]string)
|
||||
|
||||
var unique []string
|
||||
servers := make(map[string]struct{})
|
||||
@ -300,10 +304,24 @@ func GetAppStatuses(appFiles AppFiles) (map[string]string, error) {
|
||||
for range servers {
|
||||
status := <-ch
|
||||
for _, service := range status.Services {
|
||||
result := make(map[string]string)
|
||||
name := service.Spec.Labels[convert.LabelNamespace]
|
||||
|
||||
if _, ok := statuses[name]; !ok {
|
||||
statuses[name] = "deployed"
|
||||
result["status"] = "deployed"
|
||||
}
|
||||
|
||||
labelKey := fmt.Sprintf("coop-cloud.%s.version", name)
|
||||
if version, ok := service.Spec.Labels[labelKey]; ok {
|
||||
result["version"] = version
|
||||
} else {
|
||||
//FIXME: we only need to check containers with the version label not
|
||||
// every single container and then skip when we see no label perf gains
|
||||
// to be had here
|
||||
continue
|
||||
}
|
||||
|
||||
statuses[name] = result
|
||||
}
|
||||
}
|
||||
|
||||
@ -315,20 +333,18 @@ func GetAppStatuses(appFiles AppFiles) (map[string]string, error) {
|
||||
// GetAppComposeFiles gets the list of compose files for an app which should be
|
||||
// merged into a composetypes.Config while respecting the COMPOSE_FILE env var.
|
||||
func GetAppComposeFiles(recipe string, appEnv AppEnv) ([]string, error) {
|
||||
var composeFiles []string
|
||||
|
||||
if _, ok := appEnv["COMPOSE_FILE"]; !ok {
|
||||
logrus.Debug("no COMPOSE_FILE detected, loading all compose files")
|
||||
pattern := fmt.Sprintf("%s/%s/compose**yml", APPS_DIR, recipe)
|
||||
composeFiles, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return composeFiles, err
|
||||
}
|
||||
logrus.Debug("no COMPOSE_FILE detected, loading compose.yml")
|
||||
path := fmt.Sprintf("%s/%s/compose.yml", APPS_DIR, recipe)
|
||||
composeFiles = append(composeFiles, path)
|
||||
return composeFiles, nil
|
||||
}
|
||||
|
||||
var composeFiles []string
|
||||
composeFileEnvVar := appEnv["COMPOSE_FILE"]
|
||||
envVars := strings.Split(composeFileEnvVar, ":")
|
||||
logrus.Debugf("COMPOSE_FILE detected ('%s'), loading '%s'", composeFileEnvVar, envVars)
|
||||
logrus.Debugf("COMPOSE_FILE detected ('%s'), loading '%s'", composeFileEnvVar, strings.Join(envVars, ", "))
|
||||
for _, file := range strings.Split(composeFileEnvVar, ":") {
|
||||
path := fmt.Sprintf("%s/%s/%s", APPS_DIR, recipe, file)
|
||||
composeFiles = append(composeFiles, path)
|
||||
|
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
|
||||
}
|
@ -62,7 +62,6 @@ func EnsureUpToDate(dir string) error {
|
||||
checkOutOpts := &git.CheckoutOptions{
|
||||
Create: false,
|
||||
Force: true,
|
||||
Keep: false,
|
||||
Branch: plumbing.ReferenceName(refName),
|
||||
}
|
||||
if err := worktree.Checkout(checkOutOpts); err != 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
|
||||
}
|
@ -26,7 +26,7 @@ type Recipe struct {
|
||||
// UpdateLabel updates a recipe label
|
||||
func (r Recipe) UpdateLabel(serviceName, label string) error {
|
||||
pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, r.Name)
|
||||
if err := compose.UpdateLabel(pattern, serviceName, label); err != nil {
|
||||
if err := compose.UpdateLabel(pattern, serviceName, label, r.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@ -35,12 +35,39 @@ func (r Recipe) UpdateLabel(serviceName, label string) error {
|
||||
// UpdateTag updates a recipe tag
|
||||
func (r Recipe) UpdateTag(image, tag string) error {
|
||||
pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, r.Name)
|
||||
if err := compose.UpdateTag(pattern, image, tag); err != nil {
|
||||
if err := compose.UpdateTag(pattern, image, tag, r.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tags list the recipe tags
|
||||
func (r Recipe) Tags() ([]string, error) {
|
||||
var tags []string
|
||||
|
||||
recipeDir := path.Join(config.ABRA_DIR, "apps", r.Name)
|
||||
repo, err := git.PlainOpen(recipeDir)
|
||||
if err != nil {
|
||||
return tags, err
|
||||
}
|
||||
|
||||
gitTags, err := repo.Tags()
|
||||
if err != nil {
|
||||
return tags, err
|
||||
}
|
||||
|
||||
if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
|
||||
tags = append(tags, strings.TrimPrefix(string(ref.Name()), "refs/tags/"))
|
||||
return nil
|
||||
}); err != nil {
|
||||
return tags, err
|
||||
}
|
||||
|
||||
logrus.Debugf("detected '%s' as tags for recipe '%s'", strings.Join(tags, ", "), r.Name)
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// Get retrieves a recipe.
|
||||
func Get(recipeName string) (Recipe, error) {
|
||||
if err := EnsureExists(recipeName); err != nil {
|
||||
@ -82,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
|
||||
@ -92,10 +128,10 @@ func EnsureVersion(recipeName, version string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
logrus.Debugf("read '%s' as tags for recipe '%s'", tags, recipeName)
|
||||
|
||||
var parsedTags []string
|
||||
var tagRef plumbing.ReferenceName
|
||||
if err := tags.ForEach(func(ref *plumbing.Reference) (err error) {
|
||||
parsedTags = append(parsedTags, ref.Name().Short())
|
||||
if ref.Name().Short() == version {
|
||||
tagRef = ref.Name()
|
||||
}
|
||||
@ -104,6 +140,8 @@ func EnsureVersion(recipeName, version string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debugf("read '%s' as tags for recipe '%s'", strings.Join(parsedTags, ", "), recipeName)
|
||||
|
||||
if tagRef.String() == "" {
|
||||
return fmt.Errorf("%s is not available?", version)
|
||||
}
|
||||
@ -113,12 +151,79 @@ func EnsureVersion(recipeName, version string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
opts := &git.CheckoutOptions{Branch: tagRef, Keep: true}
|
||||
opts := &git.CheckoutOptions{
|
||||
Branch: tagRef,
|
||||
Create: false,
|
||||
Force: true,
|
||||
}
|
||||
if err := worktree.Checkout(opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debugf("successfully checked '%s' out to '%s' in '%s'", recipeName, tagRef, recipeDir)
|
||||
logrus.Debugf("successfully checked '%s' out to '%s' in '%s'", recipeName, tagRef.Short(), recipeDir)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureLatest makes sure the latest commit is checkout on for a local recipe repository.
|
||||
func EnsureLatest(recipeName string) error {
|
||||
recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName)
|
||||
|
||||
logrus.Debugf("attempting to open git repository in '%s'", recipeDir)
|
||||
|
||||
repo, err := git.PlainOpen(recipeDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
branch := "master"
|
||||
if _, err := repo.Branch("master"); err != nil {
|
||||
if _, err := repo.Branch("main"); err != nil {
|
||||
logrus.Debugf("failed to select branch in '%s'", path.Join(config.APPS_DIR, recipeName))
|
||||
return err
|
||||
}
|
||||
branch = "main"
|
||||
}
|
||||
|
||||
refName := fmt.Sprintf("refs/heads/%s", branch)
|
||||
checkOutOpts := &git.CheckoutOptions{
|
||||
Create: false,
|
||||
Force: true,
|
||||
Branch: plumbing.ReferenceName(refName),
|
||||
}
|
||||
|
||||
if err := worktree.Checkout(checkOutOpts); err != nil {
|
||||
logrus.Debugf("failed to check out '%s' in '%s'", branch, recipeDir)
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
24
pkg/server/server.go
Normal file
24
pkg/server/server.go
Normal file
@ -0,0 +1,24 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// CreateServerDir creates a server directory under ~/.abra.
|
||||
func CreateServerDir(serverName string) error {
|
||||
serverPath := path.Join(config.ABRA_DIR, "servers", serverName)
|
||||
|
||||
if err := os.Mkdir(serverPath, 0755); err != nil {
|
||||
if !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("'%s' already exists, moving on...", serverPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ABRA_VERSION="0.1.5-alpha"
|
||||
ABRA_VERSION="0.2.1-alpha"
|
||||
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION"
|
||||
|
||||
function show_banner {
|
||||
@ -35,13 +35,11 @@ function install_abra_release {
|
||||
fi
|
||||
|
||||
# FIXME: support different architectures
|
||||
release_url=$(curl -s "$ABRA_RELEASE_URL" |
|
||||
python3 -c "import sys, json; \
|
||||
payload = json.load(sys.stdin); \
|
||||
url = [a['browser_download_url'] for a in payload['assets'] if 'x86_64' in a['name']][0]; \
|
||||
print(url)")
|
||||
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)
|
||||
|
||||
echo "downloading $ABRA_VERSION x86_64 binary release for abra..."
|
||||
echo "downloading $ABRA_VERSION $PLATFORM binary release for abra..."
|
||||
curl --progress-bar "$release_url" --output "$HOME/.local/bin/abra"
|
||||
chmod +x "$HOME/.local/bin/abra"
|
||||
|
||||
|
@ -3,5 +3,5 @@ STACK := abra_installer_script
|
||||
default: deploy
|
||||
|
||||
deploy:
|
||||
@docker stack rm $(STACK) && \
|
||||
docker stack deploy -c compose.yml $(STACK)
|
||||
@DOCKER_CONTEXT=swarm.autonomic.zone docker stack rm $(STACK) && \
|
||||
DOCKER_CONTEXT=swarm.autonomic.zone docker stack deploy -c compose.yml $(STACK)
|
||||
|
0
tests/resources/test_folder/cruft.txt
Normal file
0
tests/resources/test_folder/cruft.txt
Normal file
Reference in New Issue
Block a user