Compare commits

...

96 Commits

Author SHA1 Message Date
a170e26e27 fix: drop copy/pasta, keep timeouts 2021-10-21 15:42:50 +02:00
03b1882b81 chore: publish new tag 2021-10-21 15:17:34 +02:00
2fcdaca75f fix: dont duplicate info output 2021-10-21 15:13:24 +02:00
c5f44cf340 feat: show undploy overview 2021-10-21 15:10:43 +02:00
7a5ad65178 fix: load timeout before other opts 2021-10-21 15:06:03 +02:00
6d4ee3de0d fix: force flag works for upgrade 2021-10-21 11:44:47 +02:00
63318fb6ff fix: handle chaos mode correctly for deploy
Closes coop-cloud/organising#210.
2021-10-21 10:19:30 +02:00
07ffa08a07 chore: remove unused files 2021-10-20 21:04:09 +02:00
0e5e7490b3 docs: some rewording and clarifying 2021-10-20 17:52:54 +02:00
640032b8fe fix: remove duplicate version command
We can use --version/-v instead.
2021-10-20 17:48:50 +02:00
39babea963 docs: remove that missing feature [ci skip] 2021-10-20 17:36:41 +02:00
07613f5163 fix: devendor capsul code
Closes coop-cloud/organising#155.
2021-10-20 17:34:01 +02:00
7f1d9eeaec fix: check if record already exists 2021-10-20 16:56:34 +02:00
02d24104e1 feat: domain CRUD complete with Gandi provider 2021-10-20 16:52:19 +02:00
da8d72620a test: warning not to test cli [ci skip] 2021-10-20 10:15:55 +01:00
96ccadc70f refactor: move making app struct to construct func
makes the code cleaner and easier to grab the app struct for testing
2021-10-20 09:45:38 +01:00
8703370785 WIP: domain create 2021-10-20 00:05:57 +02:00
7d8c53299d docs: more domain command docs hacking 2021-10-20 00:05:49 +02:00
0110aceb1f docs: rewording 2021-10-19 23:03:12 +02:00
aec1e4520d fix: handle missing containers
Closes coop-cloud/organising#198.
2021-10-19 22:50:43 +02:00
74bcb99c70 fix: use this weird default
Closes coop-cloud/organising#207.
2021-10-19 22:43:43 +02:00
dd4f2b48ec fix: explode when wrong provider chosen 2021-10-19 10:19:31 +02:00
7f3f41ede4 docs: dns list docs 2021-10-18 22:20:11 +02:00
597b4b586e WIP: domain listing with Gandi
Rethinking the interface already.
2021-10-18 22:16:29 +02:00
7ea3df45d4 WIP: dns support via libdns 2021-10-18 20:35:43 +02:00
5941ed9728 fix: handle exceptions 2021-10-18 20:35:32 +02:00
d1e42752e2 fix: set connection timeouts + clean up bad contexts
Closes coop-cloud/organising#205.
2021-10-18 10:48:43 +02:00
9dfbd21c61 fix: parse args correctly for validation 2021-10-18 09:43:32 +02:00
9526d1fde6 fix: ensure we have version checked out on deploy 2021-10-18 09:30:43 +02:00
62cc7ef92d feat: upgrade/downgrade support chaos mode 2021-10-18 08:57:25 +02:00
c5a7a831d2 docs: chaos mode flag docs 2021-10-18 08:35:59 +02:00
4aae186f5f chore: squash formatting issue 2021-10-18 08:27:39 +02:00
2f9b11f389 feat: support deploying with chaos mode 2021-10-18 08:14:06 +02:00
6d42e72f16 fix: allow for client creation on default context
See coop-cloud/organising#206.
2021-10-17 23:50:44 +02:00
5be190e110 fix: check that docker is installed on local add 2021-10-17 23:50:28 +02:00
c1390f232e fix: show "local" instead of "default" 2021-10-17 23:50:12 +02:00
3wc
95e19f03c4 fix: make release not crash on missing images 2021-10-16 18:57:21 +02:00
dc040a0b38 chore: change test context names 2021-10-16 13:26:03 +02:00
e6e2e5214f test: add tests for pkg/client/client.go 2021-10-16 13:04:57 +02:00
61452b5f32 docs: add README.md to document testing 2021-10-16 12:26:43 +02:00
78460ac0ba test: increatse client/context.go coverage to 90% 2021-10-16 11:41:41 +02:00
0615c3f745 fix: support downgrade/upgrade for unknown versions 2021-10-15 09:58:45 +02:00
3wc
e820e0219d docs: how to enable bash autocomplete from source 2021-10-14 22:37:32 +02:00
75fb9a2774 chore: publish new version 2021-10-14 13:31:18 +02:00
0d500b636d feat: more info on version changing deployments 2021-10-14 13:30:33 +02:00
5dd97cace0 docs: expand deploy/upgrade/downgrade docs 2021-10-14 12:26:07 +02:00
ae32b1eed2 fix: standardise checkout options 2021-10-14 12:17:58 +02:00
113bdf9e86 feat: add stats to app list 2021-10-14 12:02:12 +02:00
d4d4da19b7 feat: first steps towards watchable ps output
See coop-cloud/organising#178.
2021-10-14 11:51:40 +02:00
454ee696d6 fix: make ps a bit more useful and less verbose 2021-10-14 11:36:03 +02:00
ca16c002ba docs: add more description for versions command 2021-10-14 11:32:32 +02:00
91cc8b00b3 fix: avoid alias conflict 2021-10-14 11:32:25 +02:00
d0828c4d8d fix: teach app version command to read new versions 2021-10-14 11:29:57 +02:00
b69aed3bcf feat: add rollback command
Closes coop-cloud/organising#127.
2021-10-14 01:52:55 +02:00
875255fd8c feat: add upgrade command 2021-10-14 01:23:04 +02:00
2dca602c0b fix: error handling in deploy 2021-10-14 01:22:54 +02:00
1dca8a1067 chore: set 1.16 as requirement now
Closes coop-cloud/organising#201.
2021-10-13 16:55:58 +02:00
37022bf0c8 feat: make deploy only deploy
See coop-cloud/organising#127.
2021-10-13 16:51:04 +02:00
eb5b35d47f build: change sed flags in installer for mac os compatibility 2021-10-13 16:36:07 +02:00
ece1130797 build: add automatic os and architecture detection to installer script 2021-10-13 15:51:19 +02:00
c266316f7e build: remove python3 dependency from installer 2021-10-13 15:08:00 +02:00
d804276cf2 feat: add pre-deploy overview 2021-10-12 13:25:23 +02:00
4235e06943 chore: new 0.1.8-alpha release 2021-10-12 11:19:26 +02:00
a9af0b3627 fix: let gofmt do its magic 2021-10-12 10:34:10 +02:00
3wc
a0b4886eba WIP: default to compose.yml instead of all of 'em 2021-10-12 10:25:37 +02:00
84489495dc fix: load STACK_NAME if not present 2021-10-12 09:03:48 +02:00
a8683dc38a refactor: better formatting 2021-10-12 08:59:14 +02:00
e2128ea5b6 fix: check key existance correctly 2021-10-12 08:55:42 +02:00
ca3c5fef0f refactor: better wording [ci skip] 2021-10-12 08:49:38 +02:00
4a01e411be refactor: handle STACK_NAME override in one place 2021-10-12 01:14:14 +02:00
777d49ac1d fix: handle STACK_NAME for the ps command 2021-10-12 01:11:34 +02:00
deb7d21158 fix: dont loop over dead tags
Closes coop-cloud/organising#195.
2021-10-12 00:56:52 +02:00
6db1fdcfba refactor!: recipe upgrade: use new tagcmp version 2021-10-11 14:43:06 +00:00
44dc0edf7b refactor: use ; trick for inline checking [ci skip] 2021-10-11 13:48:25 +02:00
36ff50312c fix!: use annotated tags with recipe release 2021-10-11 10:45:00 +02:00
ff4b978876 fix: only list new versions
Closes coop-cloud/organising#192.
2021-10-11 01:17:52 +02:00
b68547b2c2 fix: dont overwrite generated catalogue
Closes coop-cloud/organising#190.
2021-10-11 01:06:51 +02:00
0140f96ca1 fix: make sure to clone recipe
Closes coop-cloud/organising#193.
2021-10-11 00:34:23 +02:00
3wc
1cb45113db fix: default linux binary in installer, add context
Closes coop-cloud/organising#184
2021-10-09 21:45:28 +02:00
c764243f3a fix: manage multiple version showing edge cases 2021-10-08 10:50:48 +02:00
dde8afcd43 feat: support version/upgrade listing
Closes coop-cloud/organising#130.
2021-10-08 09:51:47 +02:00
98ffc210e1 fix: show descending orders on releases [ci skip] 2021-10-06 09:13:07 +02:00
7c0d883135 chore: new release 2021-10-06 08:48:23 +02:00
e78ced41fb fix: use freifunk DNS resolver
Closes coop-cloud/organising#180.
2021-10-06 08:47:01 +02:00
e9113500d8 feat: allow to override STACK_NAME 2021-10-05 20:40:16 +02:00
7368cabc49 fix: format output correctly 2021-10-05 20:24:52 +02:00
f75e264811 fix: ensure dirs are created
Also use debug logging for help.

Closes coop-cloud/organising#183.
Closes coop-cloud/organising#183.
2021-10-05 20:24:41 +02:00
8bfd76fd04 feat: generate versions for catalogue also
Closes coop-cloud/organising#179.
2021-10-05 20:14:00 +02:00
1cb5e3509d fix: add compose.yml before commiting with recipe release; reset parts of tag according to semver when releasing 2021-10-05 16:36:15 +02:00
3cd2399cca fix: ignore WIP stuff and sort [ci skip] 2021-10-05 11:56:26 +02:00
11c4651a3b fix: don't crash when there is a more serious upgrade available 2021-10-05 09:55:25 +00:00
49f90674f2 fix: --major/minor/patch is the most serious upgrade you want to do 2021-10-05 09:55:25 +00:00
74a70edb03 feat: upgrade an app with no user input with --minor/major/patch flag 2021-10-05 09:55:25 +00:00
6fc5c31347 WIP: #172 upgrade --major/minor/patch placeholder 2021-10-05 09:55:25 +00:00
c616907b71 feat: teach recipe sync to understand new versions
Closes coop-cloud/organising#177.
2021-10-05 10:28:09 +02:00
a58cea3e0a docs: dont assume that yet [ci skip] 2021-10-02 23:30:18 +02:00
55 changed files with 2007 additions and 538 deletions

View File

@ -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 -}}

View File

@ -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
View File

@ -3,3 +3,4 @@ abra
vendor/ vendor/
.envrc .envrc
dist/ dist/
*fmtcoverage.html

View File

@ -28,12 +28,13 @@ checksum:
snapshot: snapshot:
name_template: "{{ incpatch .Version }}-next" name_template: "{{ incpatch .Version }}-next"
changelog: changelog:
sort: asc sort: desc
filters: filters:
exclude: exclude:
- "^WIP:"
- "^chore:"
- "^docs:" - "^docs:"
- "^refactor:" - "^refactor:"
- "^style:" - "^style:"
- "^test:" - "^test:"
- "^tests:" - "^tests:"
- "^chore:"

View File

@ -50,6 +50,13 @@ sudo cp scripts/autocomplete/bash /etc/bash_completion.d/abra
source /etc/bash_completion.d/abra source /etc/bash_completion.d/abra
``` ```
In development, you can source the script in your git checkout, just make sure
to set `PROG=abra`, otherwise it'll add completion to the wrong command:
```
PROG=abra source /path/to/abra/scripts/autocomplete/bash
```
**(fi)zsh** **(fi)zsh**
(fi)zsh doesn't have an autocompletion folder by default but you can create one, then copy `scripts/autocomplete/zsh` into it and add a couple lines to your `~/.zshrc` or `~/.fizsh/.fizshrc` (fi)zsh doesn't have an autocompletion folder by default but you can create one, then copy `scripts/autocomplete/zsh` into it and add a couple lines to your `~/.zshrc` or `~/.fizsh/.fizshrc`

View File

@ -7,7 +7,7 @@ import (
// AppCommand defines the `abra app` command and ets subcommands // AppCommand defines the `abra app` command and ets subcommands
var AppCommand = &cli.Command{ var AppCommand = &cli.Command{
Name: "app", Name: "app",
Usage: "Manage apps", Usage: "Manage deployed apps",
Aliases: []string{"a"}, Aliases: []string{"a"},
ArgsUsage: "<app>", ArgsUsage: "<app>",
Description: ` Description: `
@ -19,6 +19,7 @@ to scaling apps up and spinning them down.
appNewCommand, appNewCommand,
appConfigCommand, appConfigCommand,
appDeployCommand, appDeployCommand,
appUpgradeCommand,
appUndeployCommand, appUndeployCommand,
appBackupCommand, appBackupCommand,
appRestoreCommand, appRestoreCommand,

View File

@ -2,11 +2,16 @@ package app
import ( import (
"fmt" "fmt"
"strings"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
stack "coopcloud.tech/abra/pkg/client/stack" stack "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -15,14 +20,85 @@ var appDeployCommand = &cli.Command{
Name: "deploy", Name: "deploy",
Aliases: []string{"d"}, Aliases: []string{"d"},
Usage: "Deploy an app", 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 { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
stackName := app.StackName()
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("checking whether '%s' is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
if err != nil {
logrus.Fatal(err)
}
if isDeployed {
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") abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath) abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil { if err != nil {
@ -31,7 +107,6 @@ var appDeployCommand = &cli.Command{
for k, v := range abraShEnv { for k, v := range abraShEnv {
app.Env[k] = v app.Env[k] = v
} }
app.Env["STACK_NAME"] = app.StackName()
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env) composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
if err != nil { if err != nil {
@ -39,7 +114,7 @@ var appDeployCommand = &cli.Command{
} }
deployOpts := stack.Deploy{ deployOpts := stack.Deploy{
Composefiles: composeFiles, Composefiles: composeFiles,
Namespace: app.StackName(), Namespace: stackName,
Prune: false, Prune: false,
ResolveImage: stack.ResolveImageAlways, ResolveImage: stack.ResolveImageAlways,
} }
@ -48,6 +123,10 @@ var appDeployCommand = &cli.Command{
logrus.Fatal(err) 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 { if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
logrus.Fatal(err) 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
}

View File

@ -1,10 +1,14 @@
package app package app
import ( import (
"fmt"
"sort" "sort"
"strings"
abraFormatter "coopcloud.tech/abra/cli/formatter" abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/tagcmp"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -65,10 +69,10 @@ can take some time.
} }
sort.Sort(config.ByServerAndType(apps)) sort.Sort(config.ByServerAndType(apps))
statuses := map[string]string{} statuses := make(map[string]map[string]string)
tableCol := []string{"Server", "Type", "Domain"} tableCol := []string{"Server", "Type", "Domain"}
if status { if status {
tableCol = append(tableCol, "Status") tableCol = append(tableCol, "Status", "Version", "Updates")
statuses, err = config.GetAppStatuses(appFiles) statuses, err = config.GetAppStatuses(appFiles)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -78,22 +82,90 @@ can take some time.
table := abraFormatter.CreateTable(tableCol) table := abraFormatter.CreateTable(tableCol)
table.SetAutoMergeCellsByColumnIndex([]int{0}) table.SetAutoMergeCellsByColumnIndex([]int{0})
var (
versionedAppsCount int
unversionedAppsCount int
onLatestCount int
canUpgradeCount int
)
for _, app := range apps { for _, app := range apps {
var tableRow []string var tableRow []string
if app.Type == appType || appType == "" { if app.Type == appType || appType == "" {
// If type flag is set, check for it, if not, Type == "" // If type flag is set, check for it, if not, Type == ""
tableRow = []string{app.Server, app.Type, app.Domain} tableRow = []string{app.Server, app.Type, app.Domain}
if status { if status {
if status, ok := statuses[app.StackName()]; ok { stackName := app.StackName()
tableRow = append(tableRow, status) 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 { } else {
tableRow = append(tableRow, "unknown") 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) 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() table.Render()
return nil return nil

View File

@ -210,6 +210,10 @@ func action(c *cli.Context) error {
} }
} }
if newAppServer == "default" {
newAppServer = "local"
}
tableCol := []string{"Name", "Domain", "Type", "Server"} tableCol := []string{"Name", "Domain", "Type", "Server"}
table := abraFormatter.CreateTable(tableCol) table := abraFormatter.CreateTable(tableCol)
table.Append([]string{sanitisedAppName, domain, recipe.Name, newAppServer}) table.Append([]string{sanitisedAppName, domain, recipe.Name, newAppServer})

View File

@ -3,6 +3,7 @@ package app
import ( import (
"fmt" "fmt"
"strings" "strings"
"time"
abraFormatter "coopcloud.tech/abra/cli/formatter" abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
@ -15,44 +16,33 @@ import (
"github.com/urfave/cli/v2" "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{ var appPsCommand = &cli.Command{
Name: "ps", Name: "ps",
Usage: "Check app status", Usage: "Check app status",
Aliases: []string{"p"}, Aliases: []string{"p"},
Flags: []cli.Flag{
watchFlag,
},
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) if !watch {
showPSOutput(c)
cl, err := client.New(app.Server) return nil
if err != nil {
logrus.Fatal(err)
} }
filters := filters.NewArgs() // TODO: how do we make this update in-place in an x-platform way?
filters.Add("name", app.StackName()) for {
showPSOutput(c)
containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters}) time.Sleep(2 * time.Second)
if err != nil {
logrus.Fatal(err)
} }
tableCol := []string{"id", "image", "command", "created", "status", "ports", "names"}
table := abraFormatter.CreateTable(tableCol)
for _, container := range containers {
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, ", "),
}
table.Append(tableRow)
}
table.Render()
return nil
}, },
BashComplete: func(c *cli.Context) { BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames() appNames, err := config.GetAppNames()
@ -67,3 +57,43 @@ var appPsCommand = &cli.Command{
} }
}, },
} }
// showPSOutput renders ps output.
func showPSOutput(c *cli.Context) {
app := internal.ValidateApp(c)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
filters := filters.NewArgs()
filters.Add("name", app.StackName())
containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
if err != nil {
logrus.Fatal(err)
}
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.RemoveSha(container.Image),
abraFormatter.HumanDuration(container.Created),
container.Status,
formatter.DisplayablePorts(container.Ports),
strings.Join(containerNames, "\n"),
}
table.Append(tableRow)
}
table.Render()
}

View File

@ -63,7 +63,7 @@ var appRemoveCommand = &cli.Command{
if err != nil { if err != nil {
logrus.Fatal(err) 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) logrus.Fatalf("'%s' is still deployed. Run \"abra app %s undeploy\" or pass --force", app.Name, app.Name)
} }
} }

View File

@ -3,14 +3,15 @@ package app
import ( import (
"fmt" "fmt"
"context" "coopcloud.tech/abra/pkg/catalogue"
stack "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
"coopcloud.tech/abra/cli/internal" "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"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -18,8 +19,25 @@ import (
var appRollbackCommand = &cli.Command{ var appRollbackCommand = &cli.Command{
Name: "rollback", Name: "rollback",
Usage: "Roll an app back to a previous version", Usage: "Roll an app back to a previous version",
Aliases: []string{"r"}, Aliases: []string{"r", "downgrade"},
ArgsUsage: "[<version>]", 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) { BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames() appNames, err := config.GetAppNames()
if err != nil { if err != nil {
@ -34,48 +52,125 @@ var appRollbackCommand = &cli.Command{
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
stackName := app.StackName()
ctx := context.Background()
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) 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 { if err != nil {
logrus.Fatal(err) 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 { if !isDeployed {
logrus.Fatalf("'%s' is not deployed?", app.Name) 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) var availableDowngrades []string
if version == "" { if deployedVersion == "" {
// TODO: deployedVersion = "unknown"
// using deployedVersions["app"], get index+1 version from catalogue availableDowngrades = versions
// otherwise bail out saying there is nothing to rollback to logrus.Warnf("failed to determine version of deployed '%s'", app.Name)
} else {
// TODO
// ensure this version is listed in the catalogue
// ensure this version is "older" (lower down in the list)
} }
// TODO if deployedVersion != "unknown" && !internal.Chaos {
// display table of existing state and expected state and prompt for _, version := range versions {
// run the deployment with this target version! 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)
}
}
logrus.Fatal("command not implemented yet, coming soon TM") 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 {
prompt := &survey.Select{
Message: fmt.Sprintf("Please select a downgrade (current version: '%s'):", deployedVersion),
Options: availableDowngrades,
}
if err := survey.AskOne(prompt, &chosenDowngrade); err != nil {
return err
}
}
}
if !internal.Chaos {
if err := recipe.EnsureVersion(app.Type, chosenDowngrade); err != nil {
logrus.Fatal(err)
}
}
if internal.Chaos {
logrus.Warn("chaos mode engaged")
var err error
chosenDowngrade, err = recipe.ChaosVersion(app.Type)
if err != nil {
logrus.Fatal(err)
}
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh")
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 return nil
}, },

View File

@ -55,15 +55,20 @@ var appRunCommand = &cli.Command{
} }
serviceName := c.Args().Get(1) serviceName := c.Args().Get(1)
stackAndServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("%s_%s", app.StackName(), serviceName)) filters.Add("name", stackAndServiceName)
containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters}) containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if len(containers) > 1 {
logrus.Fatalf("expected 1 container but got %d", len(containers)) switch len(containers) {
case 0:
logrus.Fatalf("no containers matching '%s' found?", stackAndServiceName)
case 1:
logrus.Fatalf("expected 1 container matching '%s' but got %d", stackAndServiceName, len(containers))
} }
cmd := c.Args().Slice()[2:] cmd := c.Args().Slice()[2:]

View File

@ -22,12 +22,28 @@ volumes as eligiblef or pruning once undeployed.
`, `,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
stackName := app.StackName()
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("checking whether '%s' is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
if err != nil {
logrus.Fatal(err)
}
if !isDeployed {
logrus.Fatalf("'%s' is not deployed?", stackName)
}
if err := DeployOverview(app, deployedVersion, "continue with undeploy?"); err != nil {
logrus.Fatal(err)
}
rmOpts := stack.Remove{Namespaces: []string{app.StackName()}} rmOpts := stack.Remove{Namespaces: []string{app.StackName()}}
if err := stack.RunRemove(c.Context, cl, rmOpts); err != nil { if err := stack.RunRemove(c.Context, cl, rmOpts); err != nil {
logrus.Fatal(err) logrus.Fatal(err)

179
cli/app/upgrade.go Normal file
View 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)
}
},
}

View File

@ -2,12 +2,12 @@ package app
import ( import (
"fmt" "fmt"
"sort"
"strings" "strings"
abraFormatter "coopcloud.tech/abra/cli/formatter" abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal" "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/client/stack"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
@ -32,65 +32,61 @@ func getImagePath(image string) (string, error) {
var appVersionCommand = &cli.Command{ var appVersionCommand = &cli.Command{
Name: "version", Name: "version",
Aliases: []string{"v"}, 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 { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
stackName := app.StackName()
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env) cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := config.GetAppComposeConfig(app.Type, opts, app.Env)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
ch := make(chan stack.StackStatus, len(compose.Services)) logrus.Debugf("checking whether '%s' is already deployed", stackName)
for _, service := range compose.Services {
label := fmt.Sprintf("coop-cloud.%s.%s.version", app.StackName(), service.Name) isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
go func(s string, l string) { if err != nil {
ch <- stack.GetDeployedServicesByLabel(s, l) logrus.Fatal(err)
}(app.Server, label)
} }
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) table := abraFormatter.CreateTable(tableCol)
statuses := make(map[string]stack.StackStatus) for serviceName, versionMeta := range versionsMeta {
for range compose.Services { table.Append([]string{serviceName, versionMeta.Image, deployedVersion, versionMeta.Tag, versionMeta.Digest})
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, "?", "?"})
} }
table.Render() table.Render()
return nil return nil
}, },
BashComplete: func(c *cli.Context) { BashComplete: func(c *cli.Context) {

View File

@ -96,7 +96,7 @@ var appVolumeRemoveCommand = &cli.Command{
var appVolumeCommand = &cli.Command{ var appVolumeCommand = &cli.Command{
Name: "volume", Name: "volume",
Aliases: []string{"v"}, Aliases: []string{"vl"},
Usage: "Manage app volumes", Usage: "Manage app volumes",
ArgsUsage: "<command>", ArgsUsage: "<command>",
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{

View File

@ -7,7 +7,7 @@ import (
// CatalogueCommand defines the `abra catalogue` command and sub-commands. // CatalogueCommand defines the `abra catalogue` command and sub-commands.
var CatalogueCommand = &cli.Command{ var CatalogueCommand = &cli.Command{
Name: "catalogue", Name: "catalogue",
Usage: "Manage the recipe catalogue", Usage: "Manage the recipe catalogue (for maintainers)",
Aliases: []string{"c"}, Aliases: []string{"c"},
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Description: "This command helps recipe packagers interact with the recipe catalogue", Description: "This command helps recipe packagers interact with the recipe catalogue",

View File

@ -2,7 +2,9 @@ package catalogue
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io/ioutil" "io/ioutil"
"os"
"path" "path"
"coopcloud.tech/abra/cli/formatter" "coopcloud.tech/abra/cli/formatter"
@ -45,11 +47,22 @@ var CatalogueSkipList = map[string]bool{
} }
var catalogueGenerateCommand = &cli.Command{ var catalogueGenerateCommand = &cli.Command{
Name: "generate", Name: "generate",
Aliases: []string{"g"}, Aliases: []string{"g"},
Usage: "Generate a new copy of the catalogue", Usage: "Generate a new copy of the catalogue",
ArgsUsage: "[<recipe>]", 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 { Action: func(c *cli.Context) error {
recipeName := c.Args().First() 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)) 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)) ch := make(chan string, len(repos))
for _, repoMeta := range repos { for _, repoMeta := range repos {
go func(rm catalogue.RepoMeta) { go func(rm catalogue.RepoMeta) {
if recipeName != "" && recipeName != rm.Name { if recipeName != "" && recipeName != rm.Name {
ch <- rm.Name ch <- rm.Name
bar.Add(1) retrieveBar.Add(1)
return return
} }
if _, exists := CatalogueSkipList[rm.Name]; exists { if _, exists := CatalogueSkipList[rm.Name]; exists {
ch <- rm.Name ch <- rm.Name
bar.Add(1) retrieveBar.Add(1)
return return
} }
@ -86,7 +99,7 @@ var catalogueGenerateCommand = &cli.Command{
} }
ch <- rm.Name ch <- rm.Name
bar.Add(1) retrieveBar.Add(1)
}(repoMeta) }(repoMeta)
} }
@ -95,14 +108,23 @@ var catalogueGenerateCommand = &cli.Command{
} }
catl := make(catalogue.RecipeCatalogue) catl := make(catalogue.RecipeCatalogue)
catlBar := formatter.CreateProgressbar(len(repos), "generating catalogue...")
for _, recipeMeta := range repos { for _, recipeMeta := range repos {
if recipeName != "" && recipeName != recipeMeta.Name { if recipeName != "" && recipeName != recipeMeta.Name {
catlBar.Add(1)
continue continue
} }
if _, exists := CatalogueSkipList[recipeMeta.Name]; exists { if _, exists := CatalogueSkipList[recipeMeta.Name]; exists {
catlBar.Add(1)
continue continue
} }
versions, err := catalogue.GetRecipeVersions(recipeMeta.Name)
if err != nil {
logrus.Fatal(err)
}
catl[recipeMeta.Name] = catalogue.RecipeMeta{ catl[recipeMeta.Name] = catalogue.RecipeMeta{
Name: recipeMeta.Name, Name: recipeMeta.Name,
Repository: recipeMeta.CloneURL, Repository: recipeMeta.CloneURL,
@ -110,10 +132,11 @@ var catalogueGenerateCommand = &cli.Command{
DefaultBranch: recipeMeta.DefaultBranch, DefaultBranch: recipeMeta.DefaultBranch,
Description: recipeMeta.Description, Description: recipeMeta.Description,
Website: recipeMeta.Website, 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 // Category: ..., // FIXME: once we sort out the machine-readable catalogue interface
// Features: ..., // FIXME: once we figure 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, "", " ") recipesJSON, err := json.MarshalIndent(catl, "", " ")
@ -121,11 +144,29 @@ var catalogueGenerateCommand = &cli.Command{
logrus.Fatal(err) logrus.Fatal(err)
} }
if err := ioutil.WriteFile(config.APPS_JSON, recipesJSON, 0644); err != nil { if _, err := os.Stat(config.APPS_JSON); err != nil && os.IsNotExist(err) {
logrus.Fatal(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]
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.Debugf("generated new recipe catalogue in '%s'", config.APPS_JSON) logrus.Infof("generated new recipe catalogue in '%s'", config.APPS_JSON)
return nil return nil
}, },

View File

@ -4,9 +4,11 @@ package cli
import ( import (
"fmt" "fmt"
"os" "os"
"path"
"coopcloud.tech/abra/cli/app" "coopcloud.tech/abra/cli/app"
"coopcloud.tech/abra/cli/catalogue" "coopcloud.tech/abra/cli/catalogue"
"coopcloud.tech/abra/cli/domain"
"coopcloud.tech/abra/cli/recipe" "coopcloud.tech/abra/cli/recipe"
"coopcloud.tech/abra/cli/server" "coopcloud.tech/abra/cli/server"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
@ -39,8 +41,7 @@ var DebugFlag = &cli.BoolFlag{
Usage: "Show DEBUG messages", Usage: "Show DEBUG messages",
} }
// RunApp runs CLI abra app. func newAbraApp(version, commit string) *cli.App {
func RunApp(version, commit string) {
app := &cli.App{ app := &cli.App{
Name: "abra", Name: "abra",
Usage: `The Co-op Cloud command-line utility belt 🎩🐇 Usage: `The Co-op Cloud command-line utility belt 🎩🐇
@ -58,7 +59,7 @@ func RunApp(version, commit string) {
server.ServerCommand, server.ServerCommand,
recipe.RecipeCommand, recipe.RecipeCommand,
catalogue.CatalogueCommand, catalogue.CatalogueCommand,
VersionCommand, domain.DomainCommand,
UpgradeCommand, UpgradeCommand,
}, },
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -82,16 +83,35 @@ func RunApp(version, commit string) {
logrus.SetOutput(os.Stderr) logrus.SetOutput(os.Stderr)
logrus.AddHook(logrusStack.StandardHook()) logrus.AddHook(logrusStack.StandardHook())
} }
paths := []string{
config.ABRA_DIR,
path.Join(config.ABRA_DIR, "servers"),
path.Join(config.ABRA_DIR, "apps"),
path.Join(config.ABRA_DIR, "vendor"),
}
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 nil
} }
return app
}
if err := os.Mkdir(config.ABRA_DIR, 0755); err != nil { // RunApp runs CLI abra app.
if !os.IsExist(err) { func RunApp(version, commit string) {
logrus.Fatal(err) app := newAbraApp(version, commit)
}
}
logrus.Debugf("abra version '%s', commit '%s'", version, commit)
if err := app.Run(os.Args); err != nil { if err := app.Run(os.Args); err != nil {
logrus.Fatal(err) logrus.Fatal(err)

150
cli/domain/create.go Normal file
View 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
View 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
View 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
View 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
},
}

View File

@ -49,3 +49,28 @@ var ForceFlag = &cli.BoolFlag{
Aliases: []string{"f"}, Aliases: []string{"f"},
Destination: &Force, Destination: &Force,
} }
// Chaos engages chaos mode.
var Chaos bool
// ChaosFlag turns on/off chaos functionality.
var ChaosFlag = &cli.BoolFlag{
Name: "chaos",
Value: false,
Aliases: []string{"ch"},
Usage: "Deploy uncommitted recipes changes. Use with care!",
Destination: &Chaos,
}
// DNSProvider specifies a DNS provider.
var DNSProvider string
// DNSProviderFlag selects a DNS provider.
var DNSProviderFlag = &cli.StringFlag{
Name: "provider",
Value: "",
Aliases: []string{"p"},
Usage: "DNS provider",
Destination: &DNSProvider,
Required: true,
}

View 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)
// }

View File

@ -41,6 +41,10 @@ func ValidateApp(c *cli.Context) config.App {
logrus.Fatal(err) logrus.Fatal(err)
} }
if err := recipe.EnsureExists(app.Type); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("validated '%s' as app argument", appName) logrus.Debugf("validated '%s' as app argument", appName)
return app return app

View File

@ -31,7 +31,7 @@ var PatchFlag = &cli.BoolFlag{
// RecipeCommand defines all recipe related sub-commands. // RecipeCommand defines all recipe related sub-commands.
var RecipeCommand = &cli.Command{ var RecipeCommand = &cli.Command{
Name: "recipe", Name: "recipe",
Usage: "Manage recipes", Usage: "Manage recipes (for maintainers)",
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Aliases: []string{"r"}, Aliases: []string{"r"},
Description: ` Description: `

View File

@ -9,11 +9,11 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -36,7 +36,7 @@ var DryFlag = &cli.BoolFlag{
var CommitMessage string var CommitMessage string
var CommitMessageFlag = &cli.StringFlag{ var CommitMessageFlag = &cli.StringFlag{
Name: "commit-message", Name: "commit-message",
Usage: "commit message", Usage: "commit message. Implies --commit",
Aliases: []string{"cm"}, Aliases: []string{"cm"},
Destination: &CommitMessage, Destination: &CommitMessage,
} }
@ -44,11 +44,20 @@ var CommitMessageFlag = &cli.StringFlag{
var Commit bool var Commit bool
var CommitFlag = &cli.BoolFlag{ var CommitFlag = &cli.BoolFlag{
Name: "commit", Name: "commit",
Usage: "add compose.yml to staging area and commit changes",
Value: false, Value: false,
Aliases: []string{"c"}, Aliases: []string{"c"},
Destination: &Commit, 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{ var recipeReleaseCommand = &cli.Command{
Name: "release", Name: "release",
Usage: "tag a recipe", Usage: "tag a recipe",
@ -83,6 +92,7 @@ or a rollback of an app.
PushFlag, PushFlag,
CommitFlag, CommitFlag,
CommitMessageFlag, CommitMessageFlag,
TagMessageFlag,
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c) recipe := internal.ValidateRecipe(c)
@ -91,17 +101,33 @@ or a rollback of an app.
imagesTmp := getImageVersions(recipe) imagesTmp := getImageVersions(recipe)
mainApp := getMainApp(recipe) mainApp := getMainApp(recipe)
mainAppVersion := imagesTmp[mainApp] mainAppVersion := imagesTmp[mainApp]
if err := recipePkg.EnsureExists(recipe.Name); err != nil {
logrus.Fatal(err)
}
if mainAppVersion == "" { if mainAppVersion == "" {
logrus.Fatal("main app version is empty?") logrus.Fatal("main app version is empty?")
} }
if tagstring != "" { if tagstring != "" {
_, err := tagcmp.Parse(tagstring) if _, err := tagcmp.Parse(tagstring); err != nil {
if err != nil {
logrus.Fatal("invalid tag specified") 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 != "") { if Commit || (CommitMessage != "") {
commitRepo, err := git.PlainOpen(directory) commitRepo, err := git.PlainOpen(directory)
if err != nil { if err != nil {
@ -116,8 +142,16 @@ or a rollback of an app.
prompt := &survey.Input{ prompt := &survey.Input{
Message: "commit message", Message: "commit message",
} }
survey.AskOne(prompt, &CommitMessage) if err := survey.AskOne(prompt, &CommitMessage); err != nil {
logrus.Fatal(err)
}
} }
err = commitWorktree.AddGlob("compose.**yml")
if err != nil {
logrus.Fatal(err)
}
logrus.Debug("staged compose.**yml for commit")
_, err = commitWorktree.Commit(CommitMessage, &git.CommitOptions{}) _, err = commitWorktree.Commit(CommitMessage, &git.CommitOptions{})
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -165,9 +199,7 @@ or a rollback of an app.
return nil return nil
} }
repo.CreateTag(tagstring, head.Hash(), nil) /* &git.CreateTagOptions{ repo.CreateTag(tagstring, head.Hash(), &createTagOptions)
Message: tag,
})*/
logrus.Info(fmt.Sprintf("created tag %s at %s", tagstring, head.Hash())) logrus.Info(fmt.Sprintf("created tag %s at %s", tagstring, head.Hash()))
if Push { if Push {
if err := repo.Push(&git.PushOptions{}); err != nil { if err := repo.Push(&git.PushOptions{}); err != nil {
@ -180,27 +212,33 @@ or a rollback of an app.
} }
// get the latest tag with its hash, name etc // get the latest tag with its hash, name etc
var lastGitTag *object.Tag var lastGitTag tagcmp.Tag
iter, err := repo.Tags() iter, err := repo.Tags()
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if err := iter.ForEach(func(ref *plumbing.Reference) error { if err := iter.ForEach(func(ref *plumbing.Reference) error {
obj, err := repo.TagObject(ref.Hash()) obj, err := repo.TagObject(ref.Hash())
if err == nil { if err != nil {
lastGitTag = obj return err
return 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 { }); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
newTag, err := tagcmp.Parse(lastGitTag.Name)
if err != nil {
logrus.Fatal(err)
}
fmt.Println(lastGitTag)
newTag := lastGitTag
var newTagString string var newTagString string
if bumpType > 0 { if bumpType > 0 {
if Patch { if Patch {
@ -214,28 +252,28 @@ or a rollback of an app.
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
newTag.Patch = "0"
newTag.Minor = strconv.Itoa(now + 1) newTag.Minor = strconv.Itoa(now + 1)
} else if Major { } else if Major {
now, err := strconv.Atoi(newTag.Major) now, err := strconv.Atoi(newTag.Major)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
newTag.Patch = "0"
newTag.Minor = "0"
newTag.Major = strconv.Itoa(now + 1) newTag.Major = strconv.Itoa(now + 1)
} }
newTagString = newTag.String()
} else { } else {
logrus.Fatal("we don't support automatic tag generation yet - specify a version or use one of: --major --minor --patch") logrus.Fatal("we don't support automatic tag generation yet - specify a version or use one of: --major --minor --patch")
} }
newTag.Metadata = mainAppVersion
newTagString = fmt.Sprintf("%s+%s", newTagString, mainAppVersion) newTagString = newTag.String()
if Dry { if Dry {
logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", newTagString, head.Hash())) logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", newTagString, head.Hash()))
return nil return nil
} }
repo.CreateTag(newTagString, head.Hash(), nil) /* &git.CreateTagOptions{ repo.CreateTag(newTagString, head.Hash(), &createTagOptions)
Message: tag,
})*/
logrus.Info(fmt.Sprintf("created tag %s at %s", newTagString, head.Hash())) logrus.Info(fmt.Sprintf("created tag %s at %s", newTagString, head.Hash()))
if Push { if Push {
if err := repo.Push(&git.PushOptions{}); err != nil { if err := repo.Push(&git.PushOptions{}); err != nil {
@ -253,6 +291,9 @@ func getImageVersions(recipe recipe.Recipe) map[string]string {
var services = make(map[string]string) var services = make(map[string]string)
for _, service := range recipe.Config.Services { for _, service := range recipe.Config.Services {
if service.Image == "" {
continue
}
srv := strings.Split(service.Image, ":") srv := strings.Split(service.Image, ":")
services[srv[0]] = srv[1] services[srv[0]] = srv[1]
} }

View File

@ -1,65 +1,88 @@
package recipe package recipe
import ( import (
"errors"
"fmt" "fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/catalogue"
"github.com/docker/distribution/reference" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
var recipeSyncCommand = &cli.Command{ var recipeSyncCommand = &cli.Command{
Name: "sync", Name: "sync",
Usage: "Generate new recipe labels", Usage: "Ensure recipe version labels are up-to-date",
Aliases: []string{"s"}, Aliases: []string{"s"},
ArgsUsage: "<recipe> <version>",
Description: ` 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: 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 The <version> is determined by the recipe maintainer and is specified on the
labels are consumed by abra in other command invocations and used to determine command-line. The <recipe> configuration will be updated on the local file
the versioning metadata of up-and-running containers are. 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 { Action: func(c *cli.Context) error {
if c.Args().Len() != 2 {
internal.ShowSubcommandHelpAndError(c, errors.New("missing <recipe>/<version> arguments?"))
}
recipe := internal.ValidateRecipe(c) 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 hasAppService := false
for _, service := range recipe.Config.Services { for _, service := range recipe.Config.Services {
services = append(services, service.Name)
if service.Name == "app" { if service.Name == "app" {
hasAppService = true hasAppService = true
logrus.Debugf("detected app service in '%s'", recipe.Name)
} }
} }
if !hasAppService { 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,
}
if err := survey.AskOne(prompt, &chosenService); err != nil {
logrus.Fatal(err)
}
mainService = chosenService
} }
for _, service := range recipe.Config.Services { logrus.Debugf("selecting '%s' as the service to sync version labels", mainService)
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("detected image '%s' for service '%s'", img, service.Name)
digest, err := client.GetTagDigest(img) label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag)
if err != nil { if err := recipe.UpdateLabel(mainService, label); err != nil {
logrus.Fatal(err) 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 return nil
}, },
} }

View File

@ -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 Some image tags cannot be parsed because they do not follow some sort of
semver-like convention. In this case, all possible tags will be listed and it semver-like convention. In this case, all possible tags will be listed and it
is up to the end-user to decide. is up to the end-user to decide.
This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync
<recipe>".
`, `,
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Flags: []cli.Flag{
PatchFlag,
MinorFlag,
MajorFlag,
},
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c) 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 { for _, service := range recipe.Config.Services {
catlVersions, err := catalogue.VersionsOfService(recipe.Name, service.Name) catlVersions, err := catalogue.VersionsOfService(recipe.Name, service.Name)
if err != nil { if err != nil {
@ -107,31 +117,48 @@ 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) logrus.Debugf("detected compatible upgradable tags '%s' for '%s'", compatibleStrings, service.Name)
msg := fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
tag := img.(reference.NamedTagged).Tag()
logrus.Warning(fmt.Sprintf("unable to determine versioning semantics of '%s', listing all tags", tag))
msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
compatibleStrings = []string{}
for _, regVersion := range regVersions {
compatibleStrings = append(compatibleStrings, regVersion.Name)
}
}
var upgradeTag string var upgradeTag string
prompt := &survey.Select{ if bumpType != 0 {
Message: msg, for _, upTag := range compatible {
Options: compatibleStrings, upElement, err := tag.UpgradeDelta(upTag)
} if err != nil {
if err := survey.AskOne(prompt, &upgradeTag); err != nil { return err
logrus.Fatal(err) }
delta := upElement.UpgradeType()
if delta <= bumpType {
upgradeTag = upTag.String()
break
}
}
if upgradeTag == "" {
logrus.Warnf("not upgrading from '%s' to '%s' for '%s', because the upgrade type is more serious than what user wants.", tag.String(), compatible[0].String(), image)
continue
}
} else {
msg := fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
tag := img.(reference.NamedTagged).Tag()
logrus.Warning(fmt.Sprintf("unable to determine versioning semantics of '%s', listing all tags", tag))
msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
compatibleStrings = []string{}
for _, regVersion := range regVersions {
compatibleStrings = append(compatibleStrings, regVersion.Name)
}
}
prompt := &survey.Select{
Message: msg,
Options: compatibleStrings,
}
if err := survey.AskOne(prompt, &upgradeTag); err != nil {
logrus.Fatal(err)
}
} }
if err := recipe.UpdateTag(image, upgradeTag); err != nil { if err := recipe.UpdateTag(image, upgradeTag); err != nil {
logrus.Fatal(err) 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 return nil

View File

@ -3,6 +3,7 @@ package server
import ( import (
"context" "context"
"errors" "errors"
"os/exec"
"os/user" "os/user"
"strings" "strings"
@ -53,12 +54,13 @@ All communication between Abra and the server will use this SSH connection.
}, },
ArgsUsage: "<domain> [<user>] [<port>]", ArgsUsage: "<domain> [<user>] [<port>]",
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if c.Args().Len() == 1 && !local {
if c.Args().Len() == 2 && !local {
err := errors.New("missing arguments <domain> or '--local'") err := errors.New("missing arguments <domain> or '--local'")
internal.ShowSubcommandHelpAndError(c, err) internal.ShowSubcommandHelpAndError(c, err)
} }
if c.Args().Get(1) != "" && local { if c.Args().Get(2) != "" && local {
err := errors.New("cannot use '<domain>' and '--local' together") err := errors.New("cannot use '<domain>' and '--local' together")
internal.ShowSubcommandHelpAndError(c, err) internal.ShowSubcommandHelpAndError(c, err)
} }
@ -69,6 +71,9 @@ All communication between Abra and the server will use this SSH connection.
if err := server.CreateServerDir(domainName); err != nil { if err := server.CreateServerDir(domainName); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if _, err := exec.LookPath("docker"); err != nil {
return errors.New("docker command not found on $PATH, is it installed?")
}
logrus.Info("local server has been added") logrus.Info("local server has been added")
return nil return nil
} }
@ -113,6 +118,10 @@ All communication between Abra and the server will use this SSH connection.
ctx := context.Background() ctx := context.Background()
cl, err := client.New(domainName) cl, err := client.New(domainName)
if err != nil { if err != nil {
logrus.Warn("cleaning up context due to connection failure")
if err := client.DeleteContext(domainName); err != nil {
logrus.Fatal(err)
}
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -120,6 +129,10 @@ All communication between Abra and the server will use this SSH connection.
if strings.Contains(err.Error(), "command not found") { if strings.Contains(err.Error(), "command not found") {
logrus.Fatalf("docker is not installed on '%s'?", domainName) logrus.Fatalf("docker is not installed on '%s'?", domainName)
} else { } else {
logrus.Warn("cleaning up context due to connection failure")
if err := client.DeleteContext(domainName); err != nil {
logrus.Fatal(err)
}
logrus.Fatalf("unable to make a connection to '%s'?", domainName) logrus.Fatalf("unable to make a connection to '%s'?", domainName)
} }
logrus.Debug(err) logrus.Debug(err)

View File

@ -35,6 +35,9 @@ later for more advanced use cases.
return err return err
} }
// https://www.privacy-handbuch.de/handbuch_93d.htm
freifunkDNS := "5.1.66.255:53"
resolver := &net.Resolver{ resolver := &net.Resolver{
PreferGo: false, PreferGo: false,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) { 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), Timeout: time.Millisecond * time.Duration(10000),
} }
// comrade librehosters DNS resolver https://snopyta.org/service/dns/ // 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() ctx := context.Background()
ips, err := resolver.LookupIPAddr(ctx, domainName) ips, err := resolver.LookupIPAddr(ctx, domainName)

View File

@ -1,17 +1,13 @@
package server package server
import ( import (
"bytes"
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"net/http"
"time"
"coopcloud.tech/abra/cli/formatter" "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/libcapsul"
"github.com/hetznercloud/hcloud-go/hcloud" "github.com/hetznercloud/hcloud-go/hcloud"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -182,8 +178,8 @@ environment variable or otherwise passing the "--env/-e" flag.
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
name := c.Args().First() capsulName := c.Args().First()
if name == "" { if capsulName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no name provided")) internal.ShowSubcommandHelpAndError(c, errors.New("no name provided"))
} }
@ -191,57 +187,17 @@ environment variable or otherwise passing the "--env/-e" flag.
logrus.Fatal("Capsul API token is missing") logrus.Fatal("Capsul API token is missing")
} }
// yep, the response time is quite slow, something to fix on the Capsul side
client := &http.Client{Timeout: 20 * time.Second}
capsulCreateURL := fmt.Sprintf("https://%s/api/capsul/create", capsulInstance) capsulCreateURL := fmt.Sprintf("https://%s/api/capsul/create", capsulInstance)
logrus.Debugf("using '%s' as capsul create url", capsulCreateURL)
values := map[string]string{
"name": name,
"size": capsulType,
"os": capsulImage,
"ssh_key_0": capsulSSHKey,
}
payload, err := json.Marshal(values) capsulClient := libcapsul.New(capsulCreateURL, capsulAPIToken)
resp, err := capsulClient.Create(capsulName, capsulType, capsulImage, capsulSSHKey)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
req, err := http.NewRequest("POST", capsulCreateURL, bytes.NewBuffer(payload))
if err != nil {
logrus.Fatal(err)
}
req.Header = http.Header{
"Content-Type": []string{"application/json"},
"Authorization": []string{capsulAPIToken},
}
res, err := client.Do(req)
if err != nil {
logrus.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
body, err := ioutil.ReadAll(res.Body)
if err != nil {
panic(err)
}
logrus.Fatal(string(body))
}
type capsulCreateResponse struct{ ID string }
var resp capsulCreateResponse
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("capsul created with ID: '%s'", resp.ID)
tableColumns := []string{"Name", "ID"} tableColumns := []string{"Name", "ID"}
table := formatter.CreateTable(tableColumns) table := formatter.CreateTable(tableColumns)
table.Append([]string{name, resp.ID}) table.Append([]string{capsulName, resp.ID})
table.Render() table.Render()
return nil return nil

View File

@ -8,7 +8,7 @@ import (
var ServerCommand = &cli.Command{ var ServerCommand = &cli.Command{
Name: "server", Name: "server",
Aliases: []string{"s"}, Aliases: []string{"s"},
Usage: "Manage servers", Usage: "Manage servers via 3rd party providers",
Description: ` Description: `
Manage the lifecycle of a server. Manage the lifecycle of a server.

View File

@ -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
View File

@ -1,9 +1,9 @@
module coopcloud.tech/abra module coopcloud.tech/abra
go 1.17 go 1.16
require ( 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/AlecAivazis/survey/v2 v2.3.1
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4 github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
@ -25,75 +25,20 @@ require (
) )
require ( require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect coopcloud.tech/libcapsul v0.0.0-20211020153234-f1386b5cf79d
github.com/Microsoft/go-winio v0.4.17 // indirect
github.com/Microsoft/hcsshim v0.8.21 // indirect 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/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/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/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/fvbommel/sortorder v1.0.2 // indirect github.com/fvbommel/sortorder v1.0.2 // indirect
github.com/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/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/mux v1.8.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect github.com/libdns/gandi v1.0.2
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/libdns/libdns v0.2.1
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/moby/sys/mount v0.2.0 // indirect 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/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/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/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 github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.opencensus.io v0.22.3 // indirect golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0
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
) )

11
go.sum
View File

@ -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.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
coopcloud.tech/tagcmp v0.0.0-20210906102006-2a8edd82d75d h1:5jeUIiToqQ7vTlLeycdGp4Ezurd6/RTNl5K38usHtoo= coopcloud.tech/libcapsul v0.0.0-20211020153234-f1386b5cf79d h1:5A69AFx2BP5J43Y9SaB9LlAIMLr2SWqbzfgjUh8sgKM=
coopcloud.tech/tagcmp v0.0.0-20210906102006-2a8edd82d75d/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ= 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= 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 h1:lzkuHA60pER7L4eYL8qQJor4bUWlJe4V0gqAT19tdOA=
github.com/AlecAivazis/survey/v2 v2.3.1/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/libdns/gandi v1.0.2 h1:1Ts8UpI1x5PVKpOjKC7Dn4+EObndz9gm6vdZnloHSKQ=
github.com/libdns/gandi v1.0.2/go.mod h1:hxpbQKcQFgQrTS5lV4tAgn6QoL6HcCnoBJaW5nOW4Sk=
github.com/libdns/libdns v0.1.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=

View File

@ -9,12 +9,17 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os" "os"
"path"
"strings" "strings"
"time" "time"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/web" "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" "github.com/sirupsen/logrus"
) )
@ -49,24 +54,27 @@ type tag = string
// service represents a service within a recipe. // service represents a service within a recipe.
type service = string type service = string
// serviceMeta represents meta info associated with a service. // ServiceMeta represents meta info associated with a service.
type serviceMeta struct { type ServiceMeta struct {
Digest string `json:"digest"` Digest string `json:"digest"`
Image string `json:"image"` Image string `json:"image"`
Tag string `json:"tag"` 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. // RecipeMeta represents metadata for a recipe in the abra catalogue.
type RecipeMeta struct { type RecipeMeta struct {
Category string `json:"category"` Category string `json:"category"`
DefaultBranch string `json:"default_branch"` DefaultBranch string `json:"default_branch"`
Description string `json:"description"` Description string `json:"description"`
Features features `json:"features"` Features features `json:"features"`
Icon string `json:"icon"` Icon string `json:"icon"`
Name string `json:"name"` Name string `json:"name"`
Repository string `json:"repository"` Repository string `json:"repository"`
Versions []map[tag]map[service]serviceMeta `json:"versions"` Versions RecipeVersions `json:"versions"`
Website string `json:"website"` Website string `json:"website"`
} }
// LatestVersion returns the latest version of a recipe. // LatestVersion returns the latest version of a recipe.
@ -365,3 +373,128 @@ func ReadReposMetadata() (RepoCatalogue, error) {
return reposMeta, nil 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
View 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

View File

@ -4,37 +4,44 @@ package client
import ( import (
"net/http" "net/http"
"os" "os"
"time"
"github.com/docker/cli/cli/connhelper"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// New initiates a new Docker client. // New initiates a new Docker client.
func New(contextName string) (*client.Client, error) { func New(contextName string) (*client.Client, error) {
context, err := GetContext(contextName)
if err != nil {
return nil, err
}
ctxEndpoint, err := GetContextEndpoint(context)
if err != nil {
return nil, err
}
helper := newConnectionHelper(ctxEndpoint)
httpClient := &http.Client{
// No tls, no proxy
Transport: &http.Transport{
DialContext: helper.Dialer,
},
}
var clientOpts []client.Opt var clientOpts []client.Opt
clientOpts = append(clientOpts,
client.WithHTTPClient(httpClient), clientOpts = append(clientOpts, client.WithTimeout(3*time.Second))
client.WithHost(helper.Host),
client.WithDialContext(helper.Dialer), if contextName != "default" {
) context, err := GetContext(contextName)
if err != nil {
return nil, err
}
ctxEndpoint, err := GetContextEndpoint(context)
if err != nil {
return nil, err
}
helper := newConnectionHelper(ctxEndpoint)
httpClient := &http.Client{
// No tls, no proxy
Transport: &http.Transport{
DialContext: helper.Dialer,
},
}
clientOpts = append(clientOpts,
client.WithHTTPClient(httpClient),
client.WithHost(helper.Host),
client.WithDialContext(helper.Dialer),
)
}
version := os.Getenv("DOCKER_API_VERSION") version := os.Getenv("DOCKER_API_VERSION")
if version != "" { if version != "" {
@ -45,10 +52,18 @@ func New(contextName string) (*client.Client, error) {
cl, err := client.NewClientWithOpts(clientOpts...) cl, err := client.NewClientWithOpts(clientOpts...)
if err != nil { if err != nil {
logrus.Fatalf("unable to create Docker client: %s", err) return nil, err
} }
logrus.Debugf("created client for '%s'", contextName) logrus.Debugf("created client for '%s'", contextName)
return cl, nil return cl, nil
} }
func newConnectionHelper(daemonURL string) *connhelper.ConnectionHelper {
helper, err := connhelper.GetConnectionHelper(daemonURL)
if err != nil {
logrus.Fatal(err)
}
return helper
}

46
pkg/client/client_test.go Normal file
View 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")
}
}

View File

@ -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
}

View File

@ -9,6 +9,9 @@ import (
context "github.com/docker/cli/cli/context" context "github.com/docker/cli/cli/context"
"github.com/docker/cli/cli/context/docker" "github.com/docker/cli/cli/context/docker"
contextStore "github.com/docker/cli/cli/context/store" contextStore "github.com/docker/cli/cli/context/store"
dCliContextStore "github.com/docker/cli/cli/context/store"
cliflags "github.com/docker/cli/cli/flags"
dClient "github.com/docker/docker/client"
"github.com/moby/term" "github.com/moby/term"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -94,7 +97,6 @@ func GetContext(contextName string) (contextStore.Metadata, error) {
} }
func GetContextEndpoint(ctx contextStore.Metadata) (string, error) { func GetContextEndpoint(ctx contextStore.Metadata) (string, error) {
// safe to use docker key hardcoded since abra doesn't use k8s... yet...
endpointmeta, ok := ctx.Endpoints["docker"].(context.EndpointMetaBase) endpointmeta, ok := ctx.Endpoints["docker"].(context.EndpointMetaBase)
if !ok { if !ok {
err := errors.New("context lacks Docker endpoint") err := errors.New("context lacks Docker endpoint")
@ -108,21 +110,47 @@ func newContextStore(dir string, config contextStore.Config) contextStore.Store
} }
func NewDefaultDockerContextStore() *command.ContextStoreWithDefault { func NewDefaultDockerContextStore() *command.ContextStoreWithDefault {
// Grabbing the stderr from Docker commands
// Much easier to fit this into the code we are using to replicate docker cli commands
_, _, stderr := term.StdStreams() _, _, stderr := term.StdStreams()
// TODO: Look into custom docker configs in case users want that
dockerConfig := dConfig.LoadDefaultConfigFile(stderr) dockerConfig := dConfig.LoadDefaultConfigFile(stderr)
contextDir := dConfig.ContextStoreDir() contextDir := dConfig.ContextStoreDir()
storeConfig := command.DefaultContextStoreConfig() storeConfig := command.DefaultContextStoreConfig()
store := newContextStore(contextDir, storeConfig) store := newContextStore(contextDir, storeConfig)
opts := &cliflags.CommonOptions{Context: "default"}
dockerContextStore := &command.ContextStoreWithDefault{ dockerContextStore := &command.ContextStoreWithDefault{
Store: store, Store: store,
Resolver: func() (*command.DefaultContext, error) { Resolver: func() (*command.DefaultContext, error) {
// nil for the Opts because it works without it and its a cli thing return command.ResolveDefaultContext(opts, dockerConfig, storeConfig, stderr)
return command.ResolveDefaultContext(nil, dockerConfig, storeConfig, stderr)
}, },
} }
return dockerContextStore return dockerContextStore
} }
func getDockerEndpointMetadataAndTLS(host string) (docker.EndpointMeta, *dCliContextStore.EndpointTLSData, error) {
ep, err := getDockerEndpoint(host)
if err != nil {
return docker.EndpointMeta{}, nil, err
}
return ep.EndpointMeta, ep.TLSData.ToStoreTLSData(), nil
}
func getDockerEndpoint(host string) (docker.Endpoint, error) {
skipTLSVerify := false
ep := docker.Endpoint{
EndpointMeta: docker.EndpointMeta{
Host: host,
SkipTLSVerify: skipTLSVerify,
},
}
// try to resolve a docker client, validating the configuration
opts, err := ep.ClientOpts()
if err != nil {
return docker.Endpoint{}, err
}
if _, err := dClient.NewClientWithOpts(opts...); err != nil {
return docker.Endpoint{}, err
}
return ep, nil
}

View File

@ -30,6 +30,34 @@ func dockerContext(host, key string) TestContext {
} }
} }
func TestCreateContext(t *testing.T) {
err := client.CreateContext("testContext0", "wronguser", "wrongport")
if err == nil {
t.Error("client.CreateContext(\"testContextCreate\", \"wronguser\", \"wrongport\") should have failed but didn't return an error")
}
err = client.CreateContext("testContext0", "", "")
if err != nil {
t.Errorf("Couldn't create context: %s", err)
}
}
func TestDeleteContext(t *testing.T) {
ensureTestState(1, 1)
err := client.DeleteContext("default")
if err == nil {
t.Errorf("client.DeleteContext(\"default\") should have failed but didn't return an error")
}
err = client.DeleteContext("testContext0")
if err != nil {
t.Errorf("client.DeleteContext(\"testContext0\") failed: %s", err)
}
err = client.DeleteContext("testContextFail0")
if err == nil {
t.Errorf("client.DeleteContext(\"testContextFail0\") should have failed (attempt to delete non-existent context) but didn't return an error")
}
}
func TestGetContextEndpoint(t *testing.T) { func TestGetContextEndpoint(t *testing.T) {
var testDockerContexts = []TestContext{ var testDockerContexts = []TestContext{
dockerContext("ssh://foobar", "docker"), dockerContext("ssh://foobar", "docker"),

View File

@ -2,6 +2,7 @@ package stack
import ( import (
"context" "context"
"fmt"
"strings" "strings"
abraClient "coopcloud.tech/abra/pkg/client" abraClient "coopcloud.tech/abra/pkg/client"
@ -92,6 +93,50 @@ func GetAllDeployedServices(contextName string) StackStatus {
return StackStatus{services, nil} 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 // 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{}) { func pruneServices(ctx context.Context, cl *dockerclient.Client, namespace convert.Namespace, services map[string]struct{}) {
oldServices, err := GetStackServices(ctx, cl, namespace.Name()) oldServices, err := GetStackServices(ctx, cl, namespace.Name())

View File

@ -124,7 +124,7 @@ func UpdateLabel(pattern, serviceName, label, recipeName string) error {
return err 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) replacedBytes := strings.Replace(string(bytes), old, label, -1)
logrus.Debugf("updating '%s' to '%s' in '%s'", old, label, compose.Filename) logrus.Debugf("updating '%s' to '%s' in '%s'", old, label, compose.Filename)

View File

@ -6,7 +6,6 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
"path/filepath"
"strings" "strings"
"coopcloud.tech/abra/cli/formatter" "coopcloud.tech/abra/cli/formatter"
@ -46,7 +45,12 @@ type App struct {
// StackName gets what the docker safe stack name is for the app // StackName gets what the docker safe stack name is for the app
func (a App) StackName() string { 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 // 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 { for _, server := range servers {
serverDir := path.Join(ABRA_SERVER_FOLDER, server) 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 // GetAppStatuses queries servers to check the deployment status of given apps
func GetAppStatuses(appFiles AppFiles) (map[string]string, error) { func GetAppStatuses(appFiles AppFiles) (map[string]map[string]string, error) {
statuses := map[string]string{} statuses := make(map[string]map[string]string)
var unique []string var unique []string
servers := make(map[string]struct{}) servers := make(map[string]struct{})
@ -300,10 +304,24 @@ func GetAppStatuses(appFiles AppFiles) (map[string]string, error) {
for range servers { for range servers {
status := <-ch status := <-ch
for _, service := range status.Services { for _, service := range status.Services {
result := make(map[string]string)
name := service.Spec.Labels[convert.LabelNamespace] name := service.Spec.Labels[convert.LabelNamespace]
if _, ok := statuses[name]; !ok { 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 // 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. // merged into a composetypes.Config while respecting the COMPOSE_FILE env var.
func GetAppComposeFiles(recipe string, appEnv AppEnv) ([]string, error) { func GetAppComposeFiles(recipe string, appEnv AppEnv) ([]string, error) {
var composeFiles []string
if _, ok := appEnv["COMPOSE_FILE"]; !ok { if _, ok := appEnv["COMPOSE_FILE"]; !ok {
logrus.Debug("no COMPOSE_FILE detected, loading all compose files") logrus.Debug("no COMPOSE_FILE detected, loading compose.yml")
pattern := fmt.Sprintf("%s/%s/compose**yml", APPS_DIR, recipe) path := fmt.Sprintf("%s/%s/compose.yml", APPS_DIR, recipe)
composeFiles, err := filepath.Glob(pattern) composeFiles = append(composeFiles, path)
if err != nil {
return composeFiles, err
}
return composeFiles, nil return composeFiles, nil
} }
var composeFiles []string
composeFileEnvVar := appEnv["COMPOSE_FILE"] composeFileEnvVar := appEnv["COMPOSE_FILE"]
envVars := strings.Split(composeFileEnvVar, ":") 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, ":") { for _, file := range strings.Split(composeFileEnvVar, ":") {
path := fmt.Sprintf("%s/%s/%s", APPS_DIR, recipe, file) path := fmt.Sprintf("%s/%s/%s", APPS_DIR, recipe, file)
composeFiles = append(composeFiles, path) composeFiles = append(composeFiles, path)

28
pkg/dns/common.go Normal file
View 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
View 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
}

View File

@ -62,7 +62,6 @@ func EnsureUpToDate(dir string) error {
checkOutOpts := &git.CheckoutOptions{ checkOutOpts := &git.CheckoutOptions{
Create: false, Create: false,
Force: true, Force: true,
Keep: false,
Branch: plumbing.ReferenceName(refName), Branch: plumbing.ReferenceName(refName),
} }
if err := worktree.Checkout(checkOutOpts); err != nil { if err := worktree.Checkout(checkOutOpts); err != nil {

48
pkg/git/read.go Normal file
View 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
}

View File

@ -41,6 +41,33 @@ func (r Recipe) UpdateTag(image, tag string) error {
return nil 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. // Get retrieves a recipe.
func Get(recipeName string) (Recipe, error) { func Get(recipeName string) (Recipe, error) {
if err := EnsureExists(recipeName); err != nil { if err := EnsureExists(recipeName); err != nil {
@ -82,6 +109,15 @@ func EnsureExists(recipe string) error {
func EnsureVersion(recipeName, version string) error { func EnsureVersion(recipeName, version string) error {
recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName) recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName)
isClean, err := gitPkg.IsClean(recipeName)
if err != nil {
return err
}
if !isClean {
return fmt.Errorf("'%s' has locally unstaged changes", recipeName)
}
repo, err := git.PlainOpen(recipeDir) repo, err := git.PlainOpen(recipeDir)
if err != nil { if err != nil {
return err return err
@ -92,10 +128,10 @@ func EnsureVersion(recipeName, version string) error {
return nil return nil
} }
logrus.Debugf("read '%s' as tags for recipe '%s'", tags, recipeName) var parsedTags []string
var tagRef plumbing.ReferenceName var tagRef plumbing.ReferenceName
if err := tags.ForEach(func(ref *plumbing.Reference) (err error) { if err := tags.ForEach(func(ref *plumbing.Reference) (err error) {
parsedTags = append(parsedTags, ref.Name().Short())
if ref.Name().Short() == version { if ref.Name().Short() == version {
tagRef = ref.Name() tagRef = ref.Name()
} }
@ -104,6 +140,8 @@ func EnsureVersion(recipeName, version string) error {
return err return err
} }
logrus.Debugf("read '%s' as tags for recipe '%s'", strings.Join(parsedTags, ", "), recipeName)
if tagRef.String() == "" { if tagRef.String() == "" {
return fmt.Errorf("%s is not available?", version) return fmt.Errorf("%s is not available?", version)
} }
@ -113,12 +151,79 @@ func EnsureVersion(recipeName, version string) error {
return err 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 { if err := worktree.Checkout(opts); err != nil {
return err 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 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
}

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
ABRA_VERSION="0.1.6-alpha" ABRA_VERSION="0.2.1-alpha"
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION" ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION"
function show_banner { function show_banner {
@ -35,13 +35,11 @@ function install_abra_release {
fi fi
# FIXME: support different architectures # FIXME: support different architectures
release_url=$(curl -s "$ABRA_RELEASE_URL" | PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m)
python3 -c "import sys, json; \ sed_command='s/.*"assets":\[\{[^}]*"name":"abra.*_'"$PLATFORM"'"[^}]*"browser_download_url":"([^"]*)".*\].*/\1/p'
payload = json.load(sys.stdin); \ release_url=$(curl -s $ABRA_RELEASE_URL | sed -En $sed_command)
url = [a['browser_download_url'] for a in payload['assets'] if 'x86_64' in a['name']][0]; \
print(url)")
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" curl --progress-bar "$release_url" --output "$HOME/.local/bin/abra"
chmod +x "$HOME/.local/bin/abra" chmod +x "$HOME/.local/bin/abra"

View File

@ -3,5 +3,5 @@ STACK := abra_installer_script
default: deploy default: deploy
deploy: deploy:
@docker stack rm $(STACK) && \ @DOCKER_CONTEXT=swarm.autonomic.zone docker stack rm $(STACK) && \
docker stack deploy -c compose.yml $(STACK) DOCKER_CONTEXT=swarm.autonomic.zone docker stack deploy -c compose.yml $(STACK)