Compare commits

..

9 Commits

3646 changed files with 1610 additions and 983409 deletions

View File

@ -71,6 +71,7 @@ steps:
port: 22
command_timeout: 60m
script_stop: true
envs: [ DRONE_SOURCE_BRANCH ]
request_pty: true
script:
- |

View File

@ -1,7 +1,7 @@
# integration test suite
go env -w GOPRIVATE=coopcloud.tech
# export PASSWORD_STORE_DIR=$(pwd)/../../autonomic/passwords/passwords/
# export ABRA_DIR="$HOME/.abra_test"
# export ABRA_TEST_DOMAIN=test.example.com
# export ABRA_CI=1
# release automation
# export GITEA_TOKEN=
# export ABRA_SKIP_TEARDOWN=1 # for faster feedback when developing tests

1
.gitignore vendored
View File

@ -6,3 +6,4 @@
abra
dist/
tests/integration/.bats
vendor/

View File

@ -49,8 +49,6 @@ builds:
- 5
- 6
- 7
gcflags:
- "all=-l -B"
ldflags:
- "-X 'main.Commit={{ .Commit }}'"
- "-X 'main.Version={{ .Version }}'"

View File

@ -5,7 +5,6 @@ GOPATH := $(shell go env GOPATH)
GOVERSION := 1.21
LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
DIST_LDFLAGS := $(LDFLAGS)" -s -w"
GCFLAGS := "all=-l -B"
export GOPRIVATE=coopcloud.tech
@ -13,24 +12,22 @@ export GOPRIVATE=coopcloud.tech
all: format check build-abra test
run-abra:
@go run -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(ABRA)
@go run -ldflags=$(LDFLAGS) $(ABRA)
run-kadabra:
@go run -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(KADABRA)
@go run -ldflags=$(LDFLAGS) $(KADABRA)
install-abra:
@go install -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(ABRA)
@go install -ldflags=$(LDFLAGS) $(ABRA)
install-kadabra:
@go install -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(KADABRA)
install: install-abra install-kadabra
@go install -ldflags=$(LDFLAGS) $(KADABRA)
build-abra:
@go build -v -gcflags=$(GCFLAGS) -ldflags=$(DIST_LDFLAGS) $(ABRA)
@go build -v -ldflags=$(DIST_LDFLAGS) $(ABRA)
build-kadabra:
@go build -v -gcflags=$(GCFLAGS) -ldflags=$(DIST_LDFLAGS) $(KADABRA)
@go build -v -ldflags=$(DIST_LDFLAGS) $(KADABRA)
build: build-abra build-kadabra

View File

@ -1,34 +1,35 @@
package app
import (
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var AppCommand = cli.Command{
Name: "app",
Aliases: []string{"a"},
Usage: "Manage apps",
ArgsUsage: "<domain>",
Subcommands: []cli.Command{
appBackupCommand,
appCheckCommand,
appCmdCommand,
appConfigCommand,
appCpCommand,
appDeployCommand,
appListCommand,
appLogsCommand,
appNewCommand,
appPsCommand,
appRemoveCommand,
appRestartCommand,
appRestoreCommand,
appRollbackCommand,
appRunCommand,
appSecretCommand,
appServicesCommand,
appUndeployCommand,
appUpgradeCommand,
appVolumeCommand,
Name: "app",
Aliases: []string{"a"},
Usage: "Manage apps",
UsageText: "abra app [command] [options] [arguments]",
HideHelpCommand: true,
Commands: []*cli.Command{
&appBackupCommand,
&appCheckCommand,
&appCmdCommand,
&appConfigCommand,
&appCpCommand,
&appDeployCommand,
&appListCommand,
&appLogsCommand,
&appNewCommand,
&appPsCommand,
&appRemoveCommand,
&appRestartCommand,
&appRestoreCommand,
&appRollbackCommand,
&appRunCommand,
&appSecretCommand,
&appServicesCommand,
&appUndeployCommand,
&appUpgradeCommand,
&appVolumeCommand,
},
}

View File

@ -1,32 +1,36 @@
package app
import (
"context"
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var snapshot string
var snapshotFlag = &cli.StringFlag{
Name: "snapshot, s",
Name: "snapshot",
Aliases: []string{"s"},
Usage: "Lists specific snapshot",
Destination: &snapshot,
}
var includePath string
var includePathFlag = &cli.StringFlag{
Name: "path, p",
Name: "path",
Aliases: []string{"p"},
Usage: "Include path",
Destination: &includePath,
}
var resticRepo string
var resticRepoFlag = &cli.StringFlag{
Name: "repo, r",
Name: "repo",
Aliases: []string{"r"},
Usage: "Restic repository",
Destination: &resticRepo,
}
@ -35,16 +39,17 @@ var appBackupListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
snapshotFlag,
includePathFlag,
},
Before: internal.SubCommandBefore,
Usage: "List all backups",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Before: internal.SubCommandBefore,
Usage: "List all backups",
EnableShellCompletion: true,
HideHelpCommand: true,
UsageText: "abra app backup list [options] <domain>",
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
@ -82,16 +87,17 @@ var appBackupDownloadCommand = cli.Command{
Name: "download",
Aliases: []string{"d"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
snapshotFlag,
includePathFlag,
},
Before: internal.SubCommandBefore,
Usage: "Download a backup",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Before: internal.SubCommandBefore,
Usage: "Download a backup",
UsageText: "abra app backup download [options] <domain>",
HideHelpCommand: true,
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)
@ -153,15 +159,16 @@ var appBackupCreateCommand = cli.Command{
Name: "create",
Aliases: []string{"c"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
resticRepoFlag,
},
Before: internal.SubCommandBefore,
Usage: "Create a new backup",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Before: internal.SubCommandBefore,
Usage: "Create a new backup",
HideHelpCommand: true,
UsageText: "abra app backup create [options] <domain>",
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)
@ -211,15 +218,16 @@ var appBackupSnapshotsCommand = cli.Command{
Name: "snapshots",
Aliases: []string{"s"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
snapshotFlag,
},
Before: internal.SubCommandBefore,
Usage: "List backup snapshots",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Before: internal.SubCommandBefore,
Usage: "List backup snapshots",
UsageText: "abra app backup snapshots [options] <domain>",
HideHelpCommand: true,
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)
@ -266,14 +274,15 @@ var appBackupSnapshotsCommand = cli.Command{
}
var appBackupCommand = cli.Command{
Name: "backup",
Aliases: []string{"b"},
Usage: "Manage app backups",
ArgsUsage: "<domain>",
Subcommands: []cli.Command{
appBackupListCommand,
appBackupSnapshotsCommand,
appBackupDownloadCommand,
appBackupCreateCommand,
Name: "backup",
Aliases: []string{"b"},
Usage: "Manage app backups",
UsageText: "abra app backup [command] [options] [arguments]",
HideHelpCommand: true,
Commands: []*cli.Command{
&appBackupListCommand,
&appBackupSnapshotsCommand,
&appBackupDownloadCommand,
&appBackupCreateCommand,
},
}

View File

@ -1,24 +1,21 @@
package app
import (
"fmt"
"context"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/charmbracelet/lipgloss"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var appCheckCommand = cli.Command{
Name: "check",
Aliases: []string{"chk"},
Usage: "Ensure an app is well configured",
Description: `
This command compares env vars in both the app ".env" and recipe ".env.sample"
file.
Description: `Compare env vars in both the app ".env" and recipe ".env.sample" file.
The goal is to ensure that recipe ".env.sample" env vars are defined in your
app ".env" file. Only env var definitions in the ".env.sample" which are
@ -28,36 +25,24 @@ these env vars, then "check" will complain.
Recipe maintainers may or may not provide defaults for env vars within their
recipes regardless of commenting or not (e.g. through the use of
${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
ArgsUsage: "<domain>",
UsageText: "abra app check [options] <domain>",
HideHelpCommand: true,
Flags: []cli.Flag{
internal.DebugFlag,
internal.ChaosFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Before: internal.SubCommandBefore,
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table.
Headers("RECIPE ENV SAMPLE", "APP ENV").
StyleFunc(func(row, col int) lipgloss.Style {
switch {
case col == 1:
return lipgloss.NewStyle().Padding(0, 1, 0, 1).Align(lipgloss.Center)
default:
return lipgloss.NewStyle().Padding(0, 1, 0, 1)
}
})
tableCol := []string{"recipe env sample", "app env"}
table := formatter.CreateTable(tableCol)
envVars, err := appPkg.CheckEnv(app)
if err != nil {
@ -66,15 +51,13 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
for _, envVar := range envVars {
if envVar.Present {
val := []string{envVar.Name, "✅"}
table.Row(val...)
table.Append([]string{envVar.Name, "✅"})
} else {
val := []string{envVar.Name, "❌"}
table.Row(val...)
table.Append([]string{envVar.Name, "❌"})
}
}
fmt.Println(table)
table.Render()
return nil
},

View File

@ -1,6 +1,7 @@
package app
import (
"context"
"errors"
"fmt"
"os"
@ -14,7 +15,7 @@ import (
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var appCmdCommand = cli.Command{
@ -26,47 +27,45 @@ var appCmdCommand = cli.Command{
These commands are bash functions, defined in the abra.sh of the recipe itself.
They can be run within the context of a service (e.g. app) or locally on your
work station by passing "--local". Arguments can be passed into these functions
using the "-- <args>" syntax.
using the "-- <cmd-args>" syntax.
**WARNING**: options must be passed directly after the sub-command "cmd".
EXAMPLE:
abra app cmd --local example.com app create_user -- me@example.com`,
ArgsUsage: "<domain> [<service>] <command> [-- <args>]",
**WARNING**: [options] must be passed directly after the "cmd" sub-command.`,
UsageText: "abra app cmd [options] <domain> [<service>] <cmd> [-- <cmd-args>]",
HideHelpCommand: true,
Flags: []cli.Flag{
internal.DebugFlag,
internal.LocalCmdFlag,
internal.RemoteUserFlag,
internal.TtyFlag,
internal.OfflineFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
Subcommands: []cli.Command{appCmdListCommand},
BashComplete: func(ctx *cli.Context) {
args := ctx.Args()
switch len(args) {
Before: internal.SubCommandBefore,
Commands: []*cli.Command{
&appCmdListCommand,
},
EnableShellCompletion: true,
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
args := cmd.Args()
switch args.Len() {
case 0:
autocomplete.AppNameComplete(ctx)
autocomplete.AppNameComplete(ctx, cmd)
case 1:
autocomplete.ServiceNameComplete(args.Get(0))
case 2:
cmdNameComplete(args.Get(0))
}
},
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
if internal.LocalCmd && internal.RemoteUser != "" {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use --local & --user together"))
internal.ShowSubcommandHelpAndError(cmd, errors.New("cannot use --local & --user together"))
}
hasCmdArgs, parsedCmdArgs := parseCmdArgs(c.Args(), internal.LocalCmd)
hasCmdArgs, parsedCmdArgs := parseCmdArgs(cmd.Args().Slice(), internal.LocalCmd)
if _, err := os.Stat(app.Recipe.AbraShPath); err != nil {
if os.IsNotExist(err) {
@ -76,11 +75,11 @@ EXAMPLE:
}
if internal.LocalCmd {
if !(len(c.Args()) >= 2) {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments"))
if !(cmd.Args().Len() >= 2) {
internal.ShowSubcommandHelpAndError(cmd, errors.New("missing arguments"))
}
cmdName := c.Args().Get(1)
cmdName := cmd.Args().Get(1)
if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
log.Fatal(err)
}
@ -112,13 +111,13 @@ EXAMPLE:
log.Fatal(err)
}
} else {
if !(len(c.Args()) >= 3) {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments"))
if !(cmd.Args().Len() >= 3) {
internal.ShowSubcommandHelpAndError(cmd, errors.New("missing arguments"))
}
targetServiceName := c.Args().Get(1)
targetServiceName := cmd.Args().Get(1)
cmdName := c.Args().Get(2)
cmdName := cmd.Args().Get(2)
if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
log.Fatal(err)
}
@ -195,19 +194,19 @@ func cmdNameComplete(appName string) {
}
var appCmdListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List all available commands",
ArgsUsage: "<domain>",
Name: "list",
Aliases: []string{"ls"},
Usage: "List all available commands",
UsageText: "abra app cmd ls [options] <domain>",
HideHelpCommand: true,
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
internal.ChaosFlag,
},
BashComplete: autocomplete.AppNameComplete,
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Before: internal.SubCommandBefore,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)

View File

@ -1,6 +1,7 @@
package app
import (
"context"
"errors"
"os"
"os/exec"
@ -10,24 +11,23 @@ import (
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var appConfigCommand = cli.Command{
Name: "config",
Aliases: []string{"cfg"},
Usage: "Edit app config",
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
appName := c.Args().First()
Name: "config",
Aliases: []string{"cfg"},
Usage: "Edit app config",
HideHelpCommand: true,
UsageText: "abra app config [options] <domain>",
Before: internal.SubCommandBefore,
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
appName := cmd.Args().First()
if appName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no app provided"))
internal.ShowSubcommandHelpAndError(cmd, errors.New("no app provided"))
}
files, err := appPkg.LoadAppFiles("")
@ -51,11 +51,11 @@ var appConfigCommand = cli.Command{
}
}
cmd := exec.Command(ed, appFile.Path)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
c := exec.Command(ed, appFile.Path)
c.Stdin = os.Stdin
c.Stdout = os.Stdout
c.Stderr = os.Stderr
if err := c.Run(); err != nil {
log.Fatal(err)
}

View File

@ -22,21 +22,17 @@ import (
dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/archive"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var appCpCommand = cli.Command{
Name: "cp",
Aliases: []string{"c"},
ArgsUsage: "<domain> <src> <dst>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
Usage: "Copy files to/from a deployed app service",
Description: `
Copy files to and from any app service file system.
Name: "cp",
Aliases: []string{"c"},
HideHelpCommand: true,
UsageText: "abra app cp [options] <domain> <src> <dst>",
Before: internal.SubCommandBefore,
Usage: "Copy files to/from a deployed app service",
Description: `Copy files to and from any app service file system.
If you want to copy a myfile.txt to the root of the app service:
@ -44,18 +40,17 @@ If you want to copy a myfile.txt to the root of the app service:
And if you want to copy that file back to your current working directory locally:
abra app cp <domain> app:/myfile.txt .
`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
abra app cp <domain> app:/myfile.txt`,
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
src := c.Args().Get(1)
dst := c.Args().Get(2)
src := cmd.Args().Get(1)
dst := cmd.Args().Get(2)
if src == "" {
log.Fatal("missing <src> argument")
}

View File

@ -2,11 +2,9 @@ package app
import (
"context"
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/secret"
@ -17,22 +15,21 @@ import (
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var appDeployCommand = cli.Command{
Name: "deploy",
Aliases: []string{"d"},
Usage: "Deploy an app",
ArgsUsage: "<domain> [<version>]",
Name: "deploy",
Aliases: []string{"d"},
Usage: "Deploy an app",
ArgsUsage: "<domain> [<version>]",
HideHelpCommand: true,
UsageText: "abra app deploy [options] <domain> [<version>]",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
internal.ChaosFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `Deploy an app.
@ -40,35 +37,22 @@ var appDeployCommand = cli.Command{
This command supports chaos operations. Use "--chaos" to deploy your recipe
checkout as-is. Recipe commit hashes are also supported values for
"[<version>]". Please note, "upgrade"/"rollback" do not support chaos
operations.
EXAMPLE:
abra app deploy foo.example.com
abra app deploy foo.example.com 1.2.3+3.2.1
abra app deploy foo.example.com 1e83340e`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
var warnMessages []string
app := internal.ValidateApp(c)
operations.`,
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
stackName := app.StackName()
specificVersion := c.Args().Get(1)
specificVersion := cmd.Args().Get(1)
if specificVersion == "" {
specificVersion = app.Recipe.Version
}
if specificVersion != "" && internal.Chaos {
log.Fatal("cannot use <version> and --chaos together")
}
if specificVersion != "" {
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion)
app.Recipe.Version = specificVersion
}
if specificVersion == "" && app.Recipe.Version != "" && !internal.Chaos {
log.Debugf("retrieved %s as version from env file", app.Recipe.Version)
specificVersion = app.Recipe.Version
}
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -126,7 +110,7 @@ EXAMPLE:
if deployMeta.IsDeployed {
if internal.Force || internal.Chaos {
warnMessages = append(warnMessages, fmt.Sprintf("%s is already deployed", app.Name))
log.Warnf("%s is already deployed but continuing (--force/--chaos)", app.Name)
} else {
log.Fatalf("%s is already deployed", app.Name)
}
@ -150,13 +134,13 @@ EXAMPLE:
log.Fatal(err)
}
version = formatter.SmallSHA(head.String())
warnMessages = append(warnMessages, fmt.Sprintf("no versions detected, using latest commit"))
log.Warn("no versions detected, using latest commit")
}
}
chaosVersion := config.CHAOS_DEFAULT
chaosVersion := "false"
if internal.Chaos {
warnMessages = append(warnMessages, "chaos mode engaged")
log.Warnf("chaos mode engaged")
if isChaosCommit {
chaosVersion = specificVersion
@ -212,12 +196,14 @@ EXAMPLE:
for _, envVar := range envVars {
if !envVar.Present {
warnMessages = append(warnMessages,
fmt.Sprintf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain),
)
log.Warnf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain)
}
}
if err := internal.DeployOverview(app, version, chaosVersion, "continue with deployment?"); err != nil {
log.Fatal(err)
}
if !internal.NoDomainChecks {
domainName, ok := app.Env["DOMAIN"]
if ok {
@ -225,14 +211,10 @@ EXAMPLE:
log.Fatal(err)
}
} else {
warnMessages = append(warnMessages, "skipping domain checks as no DOMAIN=... configured for app")
log.Warn("skipping domain checks as no DOMAIN=... configured for app")
}
} else {
warnMessages = append(warnMessages, "skipping domain checks as requested")
}
if err := internal.DeployOverview(app, warnMessages, version, chaosVersion); err != nil {
log.Fatal(err)
log.Warn("skipping domain checks as requested")
}
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
@ -253,15 +235,12 @@ EXAMPLE:
}
}
app.Recipe.Version = version
if chaosVersion != config.CHAOS_DEFAULT {
app.Recipe.Version = chaosVersion
if app.Recipe.Version != "" && specificVersion != "" && specificVersion != app.Recipe.Version {
err := app.WriteRecipeVersion(specificVersion)
if err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
}
log.Debugf("choosing %s as version to save to env file", app.Recipe.Version)
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
return nil
},
}

View File

@ -1,6 +1,7 @@
package app
import (
"context"
"encoding/json"
"fmt"
"sort"
@ -12,13 +13,14 @@ import (
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/tagcmp"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var (
status bool
statusFlag = &cli.BoolFlag{
Name: "status, S",
Name: "status",
Aliases: []string{"S"},
Usage: "Show app deployment status",
Destination: &status,
}
@ -27,7 +29,8 @@ var (
var (
recipeFilter string
recipeFlag = &cli.StringFlag{
Name: "recipe, r",
Name: "recipe",
Aliases: []string{"r"},
Value: "",
Usage: "Show apps of a specific recipe",
Destination: &recipeFilter,
@ -37,7 +40,8 @@ var (
var (
listAppServer string
listAppServerFlag = &cli.StringFlag{
Name: "server, s",
Name: "server",
Aliases: []string{"s"},
Value: "",
Usage: "Show apps of a specific server",
Destination: &listAppServer,
@ -67,26 +71,24 @@ type serverStatus struct {
}
var appListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List all managed apps",
Description: `
Read the local file system listing of apps and servers (e.g. ~/.abra/) to
generate a report of all your apps.
Name: "list",
Aliases: []string{"ls"},
Usage: "List all managed apps",
HideHelpCommand: true,
UsageText: "abra app list [options]",
Description: `Generate a report of all managed apps.
By passing the "--status/-S" flag, you can query all your servers for the
actual live deployment status. Depending on how many servers you manage, this
can take some time.`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.MachineReadableFlag,
statusFlag,
listAppServerFlag,
recipeFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
Action: func(ctx context.Context, cmd *cli.Command) error {
appFiles, err := appPkg.LoadAppFiles(listAppServer)
if err != nil {
log.Fatal(err)
@ -239,27 +241,15 @@ can take some time.`,
serverStat := allStats[app.Server]
headers := []string{"RECIPE", "DOMAIN"}
tableCol := []string{"recipe", "domain"}
if status {
headers = append(headers, []string{
"STATUS",
"CHAOS",
"VERSION",
"UPGRADE",
"AUTOUPDATE"}...,
)
tableCol = append(tableCol, []string{"status", "chaos", "version", "upgrade", "autoupdate"}...)
}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table := formatter.CreateTable(tableCol)
table.Headers(headers...)
var rows [][]string
for _, appStat := range serverStat.Apps {
row := []string{appStat.Recipe, appStat.Domain}
tableRow := []string{appStat.Recipe, appStat.Domain}
if status {
chaosStatus := appStat.Chaos
if chaosStatus != "unknown" {
@ -271,27 +261,17 @@ can take some time.`,
chaosStatus = appStat.ChaosVersion
}
}
row = append(row, []string{
appStat.Status,
chaosStatus,
appStat.Version,
appStat.Upgrade,
appStat.AutoUpdate}...,
)
tableRow = append(tableRow, []string{appStat.Status, chaosStatus, appStat.Version, appStat.Upgrade, appStat.AutoUpdate}...)
}
rows = append(rows, row)
table.Append(tableRow)
}
table.Rows(rows...)
if len(rows) > 0 {
fmt.Println(table)
if table.NumLines() > 0 {
table.Render()
if status {
fmt.Println(fmt.Sprintf(
"SERVER: %s | TOTAL APPS: %v | VERSIONED: %v | UNVERSIONED: %v | LATEST : %v | UPGRADE: %v",
"server: %s | total apps: %v | versioned: %v | unversioned: %v | latest: %v | upgrade: %v",
app.Server,
serverStat.AppCount,
serverStat.VersionCount,
@ -300,21 +280,19 @@ can take some time.`,
serverStat.UpgradeCount,
))
} else {
log.Infof("SERVER: %s TOTAL APPS: %v", app.Server, serverStat.AppCount)
fmt.Println(fmt.Sprintf("server: %s | total apps: %v", app.Server, serverStat.AppCount))
}
}
if len(allStats) > 1 && len(rows) > 0 {
fmt.Println() // newline separator for multiple servers
}
if len(allStats) > 1 && table.NumLines() > 0 {
fmt.Println() // newline separator for multiple servers
}
alreadySeen[app.Server] = true
}
if len(allStats) > 1 {
totalServers := formatter.BoldStyle.Render("TOTAL SERVERS")
totalApps := formatter.BoldStyle.Render("TOTAL APPS")
log.Infof("%s: %v | %s: %v ", totalServers, totalServersCount, totalApps, totalAppsCount)
fmt.Println(fmt.Sprintf("total servers: %v | total apps: %v ", totalServersCount, totalAppsCount))
}
return nil

View File

@ -19,23 +19,24 @@ import (
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
dockerClient "github.com/docker/docker/client"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var appLogsCommand = cli.Command{
Name: "logs",
Aliases: []string{"l"},
ArgsUsage: "<domain> [<service>]",
Usage: "Tail app logs",
Name: "logs",
Aliases: []string{"l"},
Usage: "Tail app logs",
HideHelpCommand: true,
UsageText: "abra app logs [options] <domain> [<service>]",
Flags: []cli.Flag{
internal.StdErrOnlyFlag,
internal.SinceLogsFlag,
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Before: internal.SubCommandBefore,
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
stackName := app.StackName()
if err := app.Recipe.EnsureExists(); err != nil {
@ -56,7 +57,7 @@ var appLogsCommand = cli.Command{
log.Fatalf("%s is not deployed?", app.Name)
}
serviceName := c.Args().Get(1)
serviceName := cmd.Args().Get(1)
serviceNames := []string{}
if serviceName != "" {
serviceNames = []string{serviceName}

View File

@ -1,27 +1,28 @@
package app
import (
"context"
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/jsontable"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret"
"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/lipgloss/table"
dockerClient "github.com/docker/docker/client"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var appNewDescription = `
Creates a new app from a default recipe. This new app configuration is stored
in your $ABRA_DIR directory under the appropriate server.
var appNewDescription = `Creates a new app from a default recipe.
This new app configuration is stored in your $ABRA_DIR directory under the
appropriate server.
This command does not deploy your app for you. You will need to run "abra app
deploy <domain>" to do so.
@ -29,8 +30,6 @@ deploy <domain>" to do so.
You can see what recipes are available (i.e. values for the <recipe> argument)
by running "abra recipe ls".
Recipe commit hashes are supported values for "[<version>]".
Passing the "--secrets/-S" flag will automatically generate secrets for your
app and store them encrypted at rest on the chosen target server. These
generated secrets are only visible at generation time, so please take care to
@ -46,30 +45,28 @@ var appNewCommand = cli.Command{
Usage: "Create a new app",
Description: appNewDescription,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.NewAppServerFlag,
internal.DomainFlag,
internal.PassFlag,
internal.SecretsFlag,
internal.OfflineFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "[<recipe>] [<version>]",
BashComplete: func(ctx *cli.Context) {
args := ctx.Args()
switch len(args) {
Before: internal.SubCommandBefore,
HideHelpCommand: true,
UsageText: "abra app new [options] [<recipe>] [<version>]",
EnableShellCompletion: true,
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
args := cmd.Args()
switch args.Len() {
case 0:
autocomplete.RecipeNameComplete(ctx)
autocomplete.RecipeNameComplete(ctx, cmd)
case 1:
autocomplete.RecipeVersionComplete(ctx.Args().Get(0))
autocomplete.RecipeVersionComplete(cmd.Args().Get(0))
}
},
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
Action: func(ctx context.Context, cmd *cli.Command) error {
recipe := internal.ValidateRecipe(cmd)
var version string
if !internal.Chaos {
if err := recipe.EnsureIsClean(); err != nil {
log.Fatal(err)
@ -79,13 +76,16 @@ var appNewCommand = cli.Command{
log.Fatal(err)
}
}
if cmd.Args().Get(1) == "" {
var version string
if c.Args().Get(1) == "" {
recipeVersions, err := recipe.GetRecipeVersions()
if err != nil {
log.Fatal(err)
}
// NOTE(d1): determine whether recipe versions exist or not and check
// out the latest version or current HEAD
if len(recipeVersions) > 0 {
latest := recipeVersions[len(recipeVersions)-1]
for tag := range latest {
@ -101,8 +101,7 @@ var appNewCommand = cli.Command{
}
}
} else {
version = c.Args().Get(1)
if _, err := recipe.EnsureVersion(version); err != nil {
if _, err := recipe.EnsureVersion(cmd.Args().Get(1)); err != nil {
log.Fatal(err)
}
}
@ -129,7 +128,7 @@ var appNewCommand = cli.Command{
}
var secrets AppSecrets
var secretsTable *table.Table
var secretTable *jsontable.JSONTable
if internal.Secrets {
sampleEnv, err := recipe.SampleEnv()
if err != nil {
@ -160,16 +159,10 @@ var appNewCommand = cli.Command{
log.Fatal(err)
}
secretsTable, err = formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
headers := []string{"NAME", "VALUE"}
secretsTable.Headers(headers...)
secretCols := []string{"Name", "Value"}
secretTable = formatter.CreateTable(secretCols)
for name, val := range secrets {
secretsTable.Row(name, val)
secretTable.Append([]string{name, val})
}
}
@ -177,20 +170,14 @@ var appNewCommand = cli.Command{
internal.NewAppServer = "local"
}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
headers := []string{"SERVER", "DOMAIN", "RECIPE", "VERSION"}
table.Headers(headers...)
table.Row(internal.NewAppServer, internal.Domain, recipe.Name, version)
tableCol := []string{"server", "recipe", "domain"}
table := formatter.CreateTable(tableCol)
table.Append([]string{internal.NewAppServer, recipe.Name, internal.Domain})
log.Infof("new app '%s' created 🌞", recipe.Name)
fmt.Println("")
fmt.Println(table)
table.Render()
fmt.Println("")
fmt.Println("Configure this app:")
@ -204,23 +191,8 @@ var appNewCommand = cli.Command{
fmt.Println("")
fmt.Println("Generated secrets:")
fmt.Println("")
fmt.Println(secretsTable)
log.Warnf(
"generated secrets %s shown again, please take note of them %s",
formatter.BoldStyle.Render("NOT"),
formatter.BoldStyle.Render("NOW"),
)
}
app, err := app.Get(internal.Domain)
if err != nil {
log.Fatal(err)
}
log.Debugf("choosing %s as version to save to env file", version)
if err := app.WriteRecipeVersion(version, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
secretTable.Render()
log.Warn("generated secrets are not shown again, please take note of them NOW")
}
return nil

View File

@ -9,7 +9,6 @@ import (
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
abraService "coopcloud.tech/abra/pkg/service"
@ -18,26 +17,25 @@ import (
containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var appPsCommand = cli.Command{
Name: "ps",
Aliases: []string{"p"},
Usage: "Check app status",
ArgsUsage: "<domain>",
Description: "Show status of a deployed app.",
Name: "ps",
Aliases: []string{"p"},
Usage: "Check app status",
HideHelpCommand: true,
UsageText: "abra app ps [options] <domain>",
Description: "Show status of a deployed app.",
Flags: []cli.Flag{
internal.MachineReadableFlag,
internal.DebugFlag,
internal.ChaosFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
Before: internal.SubCommandBefore,
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.Ensure(false, false); err != nil {
log.Fatal(err)
}
@ -55,10 +53,15 @@ var appPsCommand = cli.Command{
log.Fatalf("%s is not deployed?", app.Name)
}
chaosVersion := config.CHAOS_DEFAULT
chaosVersion := "false"
statuses, err := appPkg.GetAppStatuses([]appPkg.App{app}, true)
if statusMeta, ok := statuses[app.StackName()]; ok {
if isChaos, exists := statusMeta["chaos"]; exists && isChaos == "true" {
isChaos, exists := statusMeta["chaos"]
if exists && isChaos == "false" {
if _, err := app.Recipe.EnsureVersion(deployMeta.Version); err != nil {
log.Fatal(err)
}
} else {
chaosVersion, err = app.Recipe.ChaosVersion()
if err != nil {
log.Fatal(err)
@ -92,7 +95,7 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
return
}
var rows [][]string
var tablerows [][]string
allContainerStats := make(map[string]map[string]string)
for _, service := range compose.Services {
filters := filters.NewArgs()
@ -132,7 +135,9 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
allContainerStats[containerStats["service"]] = containerStats
row := []string{
tablerow := []string{
deployedVersion,
chaosVersion,
containerStats["service"],
containerStats["image"],
containerStats["created"],
@ -141,37 +146,25 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
containerStats["ports"],
}
rows = append(rows, row)
tablerows = append(tablerows, tablerow)
}
if internal.MachineReadable {
jsonstring, err := json.Marshal(allContainerStats)
if err != nil {
log.Fatal("unable to convert to JSON: %s", err)
log.Fatal(err)
}
fmt.Println(string(jsonstring))
return
}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
tableCol := []string{"version", "chaos", "service", "image", "created", "status", "state", "ports"}
table := formatter.CreateTable(tableCol)
for _, row := range tablerows {
table.Append(row)
}
headers := []string{
"SERVICE",
"IMAGE",
"CREATED",
"STATUS",
"STATE",
"PORTS",
}
table.
Headers(headers...).
Rows(rows...)
fmt.Println(table)
log.Infof("VERSION: %s CHAOS: %s", deployedVersion, chaosVersion)
table.SetAutoMergeCellsByColumnIndex([]int{0, 1})
table.Render()
}

View File

@ -12,16 +12,16 @@ import (
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var appRemoveCommand = cli.Command{
Name: "remove",
Aliases: []string{"rm"},
ArgsUsage: "<domain>",
Usage: "Remove all app data, locally and remotely",
Description: `
This command removes everything related to an app which is already undeployed.
Name: "remove",
Aliases: []string{"rm"},
HideHelpCommand: true,
UsageText: "abra app remove [options] <domain>",
Usage: "Remove all app data, locally and remotely",
Description: `Remove everything related to an app which is already undeployed.
By default, it will prompt for confirmation before proceeding. All secrets,
volumes and the local app env file will be deleted.
@ -39,24 +39,20 @@ To delete everything without prompt, use the "--force/-f" or the "--no-input/n"
flag.`,
Flags: []cli.Flag{
internal.ForceFlag,
internal.DebugFlag,
internal.NoInputFlag,
internal.OfflineFlag,
},
BashComplete: autocomplete.AppNameComplete,
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Before: internal.SubCommandBefore,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if !internal.Force && !internal.NoInput {
log.Warnf("ALERTA ALERTA: this will completely remove %s data and config locally and remotely", app.Name)
response := false
prompt := &survey.Confirm{Message: "are you sure?"}
msg := "ALERTA ALERTA: this will completely remove %s data and configurations locally and remotely, are you sure?"
prompt := &survey.Confirm{Message: fmt.Sprintf(msg, app.Name)}
if err := survey.AskOne(prompt, &response); err != nil {
log.Fatal(err)
}
if !response {
log.Fatal("aborting as requested")
}

View File

@ -12,41 +12,36 @@ import (
"coopcloud.tech/abra/pkg/log"
upstream "coopcloud.tech/abra/pkg/upstream/service"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var appRestartCommand = cli.Command{
Name: "restart",
Aliases: []string{"re"},
Usage: "Restart an app",
ArgsUsage: "<domain> [<service>]",
Name: "restart",
Aliases: []string{"re"},
Usage: "Restart an app",
HideHelpCommand: true,
UsageText: "abra app restart [options] <domain> [<service>]",
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
internal.AllServicesFlag,
},
Before: internal.SubCommandBefore,
Description: `
This command restarts services within a deployed app.
Description: `This command restarts services within a deployed app.
Run "abra app ps <domain>" to see a list of service names.
Pass "--all-services/-a" to restart all services.
EXAMPLE:
abra app restart example.com app`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Pass "--all-services/-a" to restart all services.`,
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.Ensure(false, false); err != nil {
log.Fatal(err)
}
serviceName := c.Args().Get(1)
serviceName := cmd.Args().Get(1)
if serviceName == "" && !internal.AllServices {
err := errors.New("missing <service>")
internal.ShowSubcommandHelpAndError(c, err)
internal.ShowSubcommandHelpAndError(cmd, err)
}
if serviceName != "" && internal.AllServices {

View File

@ -1,36 +1,38 @@
package app
import (
"context"
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var targetPath string
var targetPathFlag = &cli.StringFlag{
Name: "target, t",
Name: "target",
Aliases: []string{"t"},
Usage: "Target path",
Destination: &targetPath,
}
var appRestoreCommand = cli.Command{
Name: "restore",
Aliases: []string{"rs"},
Usage: "Restore an app backup",
ArgsUsage: "<domain> <service>",
Name: "restore",
Aliases: []string{"rs"},
Usage: "Restore an app backup",
HideHelpCommand: true,
UsageText: "abra app restore [options] <domain> <service>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
targetPathFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Before: internal.SubCommandBefore,
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}

View File

@ -6,7 +6,6 @@ import (
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/lint"
stack "coopcloud.tech/abra/pkg/upstream/stack"
@ -16,49 +15,34 @@ import (
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var appRollbackCommand = cli.Command{
Name: "rollback",
Aliases: []string{"rl"},
Usage: "Roll an app back to a previous version",
ArgsUsage: "<domain> [<version>]",
Name: "rollback",
Aliases: []string{"rl"},
Usage: "Roll an app back to a previous version",
HideHelpCommand: true,
UsageText: "abra app rollback [options] <domain> [<version>]",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `
This command rolls an app back to a previous version.
Description: `This command rolls an app back to a previous version.
Unlike "deploy", chaos operations are not supported here. Only recipe versions
are supported values for "[<version>]".
A rollback can be destructive, please ensure you have a copy of your app data
beforehand.
EXAMPLE:
abra app rollback foo.example.com
abra app rollback foo.example.com 1.2.3+3.2.1`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
var warnMessages []string
app := internal.ValidateApp(c)
beforehand.`,
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
stackName := app.StackName()
specificVersion := c.Args().Get(1)
if specificVersion != "" {
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion)
app.Recipe.Version = specificVersion
}
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -91,7 +75,12 @@ EXAMPLE:
var availableDowngrades []string
if deployMeta.Version == "unknown" {
availableDowngrades = versions
warnMessages = append(warnMessages, fmt.Sprintf("failed to determine deployed version of %s", app.Name))
log.Warnf("failed to determine deployed version of %s", app.Name)
}
specificVersion := cmd.Args().Get(1)
if specificVersion == "" {
specificVersion = app.Recipe.Version
}
if specificVersion != "" {
@ -118,7 +107,7 @@ EXAMPLE:
if deployMeta.Version != "unknown" && specificVersion == "" {
if deployMeta.IsChaos {
warnMessages = append(warnMessages, fmt.Sprintf("attempting to rollback a chaos deployment"))
log.Warn("attempting to rollback a chaos deployment")
}
for _, version := range versions {
@ -202,20 +191,13 @@ EXAMPLE:
appPkg.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
appPkg.SetUpdateLabel(compose, stackName, app.Env)
chaosVersion := config.CHAOS_DEFAULT
chaosVersion := "false"
if deployMeta.IsChaos {
chaosVersion = deployMeta.ChaosVersion
}
// NOTE(d1): no release notes implemeneted for rolling back
if err := internal.NewVersionOverview(
app,
warnMessages,
"rollback",
deployMeta.Version,
chaosVersion,
chosenDowngrade,
""); err != nil {
if err := internal.NewVersionOverview(app, deployMeta.Version, chaosVersion, chosenDowngrade, ""); err != nil {
log.Fatal(err)
}
@ -223,10 +205,11 @@ EXAMPLE:
log.Fatal(err)
}
app.Recipe.Version = chosenDowngrade
log.Debugf("choosing %s as version to save to env file", app.Recipe.Version)
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
if app.Recipe.Version != "" {
err := app.WriteRecipeVersion(chosenDowngrade)
if err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
}
return nil

View File

@ -14,19 +14,21 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var user string
var userFlag = &cli.StringFlag{
Name: "user, u",
Name: "user",
Aliases: []string{"u"},
Value: "",
Destination: &user,
}
var noTTY bool
var noTTYFlag = &cli.BoolFlag{
Name: "no-tty, t",
Name: "no-tty",
Aliases: []string{"t"},
Destination: &noTTY,
}
@ -34,23 +36,24 @@ var appRunCommand = cli.Command{
Name: "run",
Aliases: []string{"r"},
Flags: []cli.Flag{
internal.DebugFlag,
noTTYFlag,
userFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> <service> <args>...",
Usage: "Run a command in a service container",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Before: internal.SubCommandBefore,
UsageText: "abra app run [options] <domain> <service> <args>",
Usage: "Run a command in an app service",
HideHelpCommand: true,
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if len(c.Args()) < 2 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <service> provided?"))
if cmd.Args().Len() < 2 {
internal.ShowSubcommandHelpAndError(cmd, errors.New("no <service> provided?"))
}
if len(c.Args()) < 3 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <args> provided?"))
if cmd.Args().Len() < 3 {
internal.ShowSubcommandHelpAndError(cmd, errors.New("no <args> provided?"))
}
cl, err := client.New(app.Server)
@ -58,7 +61,7 @@ var appRunCommand = cli.Command{
log.Fatal(err)
}
serviceName := c.Args().Get(1)
serviceName := cmd.Args().Get(1)
stackAndServiceName := fmt.Sprintf("^%s_%s", app.StackName(), serviceName)
filters := filters.NewArgs()
filters.Add("name", stackAndServiceName)
@ -68,12 +71,12 @@ var appRunCommand = cli.Command{
log.Fatal(err)
}
cmd := c.Args()[2:]
c := cmd.Args().Slice()[2:]
execCreateOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: cmd,
Cmd: c,
Detach: false,
Tty: true,
}

View File

@ -17,13 +17,14 @@ import (
"coopcloud.tech/abra/pkg/secret"
"github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var (
allSecrets bool
allSecretsFlag = &cli.BoolFlag{
Name: "all, a",
Name: "all",
Aliases: []string{"a"},
Destination: &allSecrets,
Usage: "Generate all secrets",
}
@ -32,41 +33,42 @@ var (
var (
rmAllSecrets bool
rmAllSecretsFlag = &cli.BoolFlag{
Name: "all, a",
Name: "all",
Aliases: []string{"a"},
Destination: &rmAllSecrets,
Usage: "Remove all secrets",
}
)
var appSecretGenerateCommand = cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate secrets",
ArgsUsage: "<domain> <secret> <version>",
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate secrets",
UsageText: "abra app secret generate [options] <domain> <secret> <version>",
HideHelpCommand: true,
Flags: []cli.Flag{
internal.DebugFlag,
allSecretsFlag,
internal.PassFlag,
internal.MachineReadableFlag,
internal.OfflineFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Before: internal.SubCommandBefore,
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
if len(c.Args()) == 1 && !allSecrets {
if cmd.Args().Len() == 1 && !allSecrets {
err := errors.New("missing arguments <secret>/<version> or '--all'")
internal.ShowSubcommandHelpAndError(c, err)
internal.ShowSubcommandHelpAndError(cmd, err)
}
if c.Args().Get(1) != "" && allSecrets {
if cmd.Args().Get(1) != "" && allSecrets {
err := errors.New("cannot use '<secret> <version>' and '--all' together")
internal.ShowSubcommandHelpAndError(c, err)
internal.ShowSubcommandHelpAndError(cmd, err)
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
@ -80,8 +82,8 @@ var appSecretGenerateCommand = cli.Command{
}
if !allSecrets {
secretName := c.Args().Get(1)
secretVersion := c.Args().Get(2)
secretName := cmd.Args().Get(1)
secretVersion := cmd.Args().Get(2)
s, ok := secrets[secretName]
if !ok {
log.Fatalf("%s doesn't exist in the env config?", secretName)
@ -115,37 +117,18 @@ var appSecretGenerateCommand = cli.Command{
os.Exit(1)
}
headers := []string{"NAME", "VALUE"}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table.Headers(headers...)
var rows [][]string
tableCol := []string{"name", "value"}
table := formatter.CreateTable(tableCol)
for name, val := range secretVals {
row := []string{name, val}
rows = append(rows, row)
table.Row(row...)
table.Append([]string{name, val})
}
if internal.MachineReadable {
out, err := formatter.ToJSON(headers, rows)
if err != nil {
log.Fatal("unable to render to JSON: %s", err)
}
fmt.Println(out)
return nil
table.JSONRender()
} else {
table.Render()
}
fmt.Println(table)
log.Warnf(
"generated secrets %s shown again, please take note of them %s",
formatter.BoldStyle.Render("NOT"),
formatter.BoldStyle.Render("NOW"),
)
log.Warn("generated secrets are not shown again, please take note of them NOW")
return nil
},
@ -160,31 +143,22 @@ var appSecretInsertCommand = cli.Command{
internal.PassFlag,
internal.FileFlag,
internal.TrimFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> <secret-name> <version> <data>",
BashComplete: autocomplete.AppNameComplete,
Description: `
This command inserts a secret into an app environment.
Before: internal.SubCommandBefore,
UsageText: "abra app secret insert [options] <domain> <secret> <version> <data>",
HideHelpCommand: true,
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Description: `This command inserts a secret into an app environment.
This can be useful when you want to manually generate secrets for an app
environment. Typically, you can let Abra generate them for you on app creation
(see "abra app new --secrets" for more).
(see "abra app new --secrets" for more).`,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
Example:
abra app secret insert myapp db_pass v1 mySecretPassword
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
if len(c.Args()) != 4 {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments?"))
if cmd.Args().Len() != 4 {
internal.ShowSubcommandHelpAndError(cmd, errors.New("missing arguments?"))
}
cl, err := client.New(app.Server)
@ -192,9 +166,9 @@ Example:
log.Fatal(err)
}
name := c.Args().Get(1)
version := c.Args().Get(2)
data := c.Args().Get(3)
name := cmd.Args().Get(1)
version := cmd.Args().Get(2)
data := cmd.Args().Get(3)
if internal.File {
raw, err := os.ReadFile(data)
@ -256,9 +230,10 @@ var appSecretRmCommand = cli.Command{
internal.OfflineFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> [<secret-name>]",
BashComplete: autocomplete.AppNameComplete,
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> [<secret-name>]",
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Description: `
This command removes app secrets.
@ -266,8 +241,8 @@ Example:
abra app secret remove myapp db_pass
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -282,12 +257,12 @@ Example:
log.Fatal(err)
}
if c.Args().Get(1) != "" && rmAllSecrets {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<secret-name>' and '--all' together"))
if cmd.Args().Get(1) != "" && rmAllSecrets {
internal.ShowSubcommandHelpAndError(cmd, errors.New("cannot use '<secret-name>' and '--all' together"))
}
if c.Args().Get(1) == "" && !rmAllSecrets {
internal.ShowSubcommandHelpAndError(c, errors.New("no secret(s) specified?"))
if cmd.Args().Get(1) == "" && !rmAllSecrets {
internal.ShowSubcommandHelpAndError(cmd, errors.New("no secret(s) specified?"))
}
cl, err := client.New(app.Server)
@ -311,7 +286,7 @@ Example:
}
match := false
secretToRm := c.Args().Get(1)
secretToRm := cmd.Args().Get(1)
for secretName, val := range secrets {
secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, val.Version)
if _, ok := remoteSecretNames[secretRemoteName]; ok {
@ -354,11 +329,12 @@ var appSecretLsCommand = cli.Command{
internal.ChaosFlag,
internal.MachineReadableFlag,
},
Before: internal.SubCommandBefore,
Usage: "List all secrets",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Before: internal.SubCommandBefore,
Usage: "List all secrets",
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -368,61 +344,48 @@ var appSecretLsCommand = cli.Command{
log.Fatal(err)
}
headers := []string{"NAME", "VERSION", "GENERATED NAME", "CREATED ON SERVER"}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table.Headers(headers...)
tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"}
table := formatter.CreateTable(tableCol)
secStats, err := secret.PollSecretsStatus(cl, app)
if err != nil {
log.Fatal(err)
}
var rows [][]string
for _, secStat := range secStats {
row := []string{
tableRow := []string{
secStat.LocalName,
secStat.Version,
secStat.RemoteName,
strconv.FormatBool(secStat.CreatedOnRemote),
}
rows = append(rows, row)
table.Row(row...)
table.Append(tableRow)
}
if len(rows) > 0 {
if table.NumLines() > 0 {
if internal.MachineReadable {
out, err := formatter.ToJSON(headers, rows)
if err != nil {
log.Fatal("unable to render to JSON: %s", err)
}
fmt.Println(out)
return nil
table.JSONRender()
} else {
table.Render()
}
fmt.Println(table)
return nil
} else {
log.Warnf("no secrets stored for %s", app.Name)
}
log.Warnf("no secrets stored for %s", app.Name)
return nil
},
}
var appSecretCommand = cli.Command{
Name: "secret",
Aliases: []string{"s"},
Usage: "Manage app secrets",
ArgsUsage: "<domain>",
Subcommands: []cli.Command{
appSecretGenerateCommand,
appSecretInsertCommand,
appSecretRmCommand,
appSecretLsCommand,
Name: "secret",
Aliases: []string{"s"},
Usage: "Manage app secrets",
HideHelpCommand: true,
UsageText: "abra app secret [command] [options] [arguments]",
Commands: []*cli.Command{
&appSecretGenerateCommand,
&appSecretInsertCommand,
&appSecretRmCommand,
&appSecretLsCommand,
},
}

View File

@ -13,21 +13,20 @@ import (
"coopcloud.tech/abra/pkg/service"
stack "coopcloud.tech/abra/pkg/upstream/stack"
containerTypes "github.com/docker/docker/api/types/container"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var appServicesCommand = cli.Command{
Name: "services",
Aliases: []string{"sr"},
Usage: "Display all services of an app",
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Name: "services",
Aliases: []string{"sr"},
Usage: "Display all services of an app",
HideHelpCommand: true,
UsageText: "abra app services [options] <domain>",
Before: internal.SubCommandBefore,
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -56,15 +55,9 @@ var appServicesCommand = cli.Command{
log.Fatal(err)
}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
tableCol := []string{"service name", "image"}
table := formatter.CreateTable(tableCol)
headers := []string{"SERVICE (SHORT)", "SERVICE (LONG)", "IMAGE"}
table.Headers(headers...)
var rows [][]string
for _, container := range containers {
var containerNames []string
for _, containerName := range container.Names {
@ -75,20 +68,14 @@ var appServicesCommand = cli.Command{
serviceShortName := service.ContainerToServiceName(container.Names, app.StackName())
serviceLongName := fmt.Sprintf("%s_%s", app.StackName(), serviceShortName)
row := []string{
serviceShortName,
tableRow := []string{
serviceLongName,
formatter.RemoveSha(container.Image),
}
rows = append(rows, row)
table.Append(tableRow)
}
table.Rows(rows...)
if len(rows) > 0 {
fmt.Println(table)
}
table.Render()
return nil
},

View File

@ -8,19 +8,19 @@ import (
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var prune bool
var pruneFlag = &cli.BoolFlag{
Name: "prune, p",
Name: "prune",
Aliases: []string{"p"},
Destination: &prune,
Usage: "Prunes unused containers, networks, and dangling images for an app",
}
@ -62,27 +62,28 @@ func pruneApp(cl *dockerClient.Client, app appPkg.App) error {
}
var appUndeployCommand = cli.Command{
Name: "undeploy",
Aliases: []string{"un"},
ArgsUsage: "<domain>",
Name: "undeploy",
Aliases: []string{"un"},
UsageText: "abra app undeploy [options] <domain>",
HideHelpCommand: true,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.OfflineFlag,
pruneFlag,
},
Before: internal.SubCommandBefore,
Usage: "Undeploy an app",
BashComplete: autocomplete.AppNameComplete,
Description: `
This does not destroy any of the application data.
Before: internal.SubCommandBefore,
Usage: "Undeploy an app",
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Description: `This does not destroy any of the application data.
However, you should remain vigilant, as your swarm installation will consider
any previously attached volumes as eligible for pruning once undeployed.
Passing "-p/--prune" does not remove those volumes.`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
stackName := app.StackName()
cl, err := client.New(app.Server)
@ -101,12 +102,12 @@ Passing "-p/--prune" does not remove those volumes.`,
log.Fatalf("%s is not deployed?", app.Name)
}
chaosVersion := config.CHAOS_DEFAULT
chaosVersion := "false"
if deployMeta.IsChaos {
chaosVersion = deployMeta.ChaosVersion
}
if err := internal.DeployOverview(app, []string{}, deployMeta.Version, chaosVersion); err != nil {
if err := internal.DeployOverview(app, deployMeta.Version, chaosVersion, "continue with undeploy?"); err != nil {
log.Fatal(err)
}

View File

@ -8,57 +8,41 @@ import (
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var appUpgradeCommand = cli.Command{
Name: "upgrade",
Aliases: []string{"up"},
Usage: "Upgrade an app",
ArgsUsage: "<domain> [<version>]",
UsageText: "abra app upgrade [options] <domain> [<version>]",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
internal.OfflineFlag,
internal.ReleaseNotesFlag,
},
Before: internal.SubCommandBefore,
Description: `
Upgrade an app.
Description: `Upgrade an app.
Unlike "deploy", chaos operations are not supported here. Only recipe versions
are supported values for "[<version>]".
An upgrade can be destructive, please ensure you have a copy of your app data
beforehand.
EXAMPLE:
abra app upgrade foo.example.com
abra app upgrade foo.example.com 1.2.3+3.2.1`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
var warnMessages []string
app := internal.ValidateApp(c)
beforehand.`,
EnableShellCompletion: true,
HideHelpCommand: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
stackName := app.StackName()
specificVersion := c.Args().Get(1)
if specificVersion != "" {
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion)
app.Recipe.Version = specificVersion
}
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -91,9 +75,10 @@ EXAMPLE:
var availableUpgrades []string
if deployMeta.Version == "unknown" {
availableUpgrades = versions
warnMessages = append(warnMessages, fmt.Sprintf("failed to determine deployed version of %s", app.Name))
log.Warnf("failed to determine deployed version of %s", app.Name)
}
specificVersion := cmd.Args().Get(1)
if specificVersion != "" {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
@ -122,7 +107,7 @@ EXAMPLE:
if deployMeta.Version != "unknown" && specificVersion == "" {
if deployMeta.IsChaos {
warnMessages = append(warnMessages, fmt.Sprintf("attempting to upgrade a chaos deployment"))
log.Warn("attempting to upgrade a chaos deployment")
}
for _, version := range versions {
@ -164,7 +149,7 @@ EXAMPLE:
}
if internal.Force && chosenUpgrade == "" {
warnMessages = append(warnMessages, fmt.Sprintf("%s is already upgraded to latest", app.Name))
log.Warnf("%s is already upgraded to latest but continuing (--force)", app.Name)
chosenUpgrade = deployMeta.Version
}
@ -238,9 +223,7 @@ EXAMPLE:
for _, envVar := range envVars {
if !envVar.Present {
warnMessages = append(warnMessages,
fmt.Sprintf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain),
)
log.Warnf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain)
}
}
@ -250,19 +233,12 @@ EXAMPLE:
return nil
}
chaosVersion := config.CHAOS_DEFAULT
chaosVersion := "false"
if deployMeta.IsChaos {
chaosVersion = deployMeta.ChaosVersion
}
if err := internal.NewVersionOverview(
app,
warnMessages,
"upgrade",
deployMeta.Version,
chaosVersion,
chosenUpgrade,
releaseNotes); err != nil {
if err := internal.NewVersionOverview(app, deployMeta.Version, chaosVersion, chosenUpgrade, releaseNotes); err != nil {
log.Fatal(err)
}
@ -284,10 +260,11 @@ EXAMPLE:
}
}
app.Recipe.Version = chosenUpgrade
log.Debugf("choosing %s as version to save to env file", app.Recipe.Version)
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
if app.Recipe.Version != "" {
err := app.WriteRecipeVersion(chosenUpgrade)
if err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
}
return nil

View File

@ -2,7 +2,6 @@ package app
import (
"context"
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
@ -11,22 +10,20 @@ import (
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var appVolumeListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
Usage: "List volumes associated with an app",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Name: "list",
Aliases: []string{"ls"},
UsageText: "abra app volume list [options] <domain>",
Before: internal.SubCommandBefore,
Usage: "List volumes associated with an app",
HideHelpCommand: true,
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
cl, err := client.New(app.Server)
if err != nil {
@ -38,35 +35,26 @@ var appVolumeListCommand = cli.Command{
log.Fatal(err)
}
volumes, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
if err != nil {
log.Fatal(err)
}
headers := []string{"name", "created", "mounted"}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
table := formatter.CreateTable([]string{"name", "created", "mounted"})
var volTable [][]string
for _, volume := range volumeList {
volRow := []string{volume.Name, volume.CreatedAt, volume.Mountpoint}
volTable = append(volTable, volRow)
}
table.Headers(headers...)
table.AppendBulk(volTable)
var rows [][]string
for _, volume := range volumes {
row := []string{volume.Name, volume.CreatedAt, volume.Mountpoint}
rows = append(rows, row)
if table.NumLines() > 0 {
table.Render()
} else {
log.Warnf("no volumes created for %s", app.Name)
}
table.Rows(rows...)
if len(rows) > 0 {
fmt.Println(table)
return nil
}
log.Warnf("no volumes created for %s", app.Name)
return nil
},
}
@ -74,27 +62,28 @@ var appVolumeListCommand = cli.Command{
var appVolumeRemoveCommand = cli.Command{
Name: "remove",
Usage: "Remove volume(s) associated with an app",
Description: `
This command supports removing volumes associated with an app. The app in
question must be undeployed before you try to remove volumes. See "abra app
undeploy <domain>" for more.
Description: `Memove volumes associated with an app.
The app in question must be undeployed before you try to remove volumes. See
"abra app undeploy <domain>" for more.
The command is interactive and will show a multiple select input which allows
you to make a seclection. Use the "?" key to see more help on navigating this
interface.
Passing "--force/-f" will select all volumes for removal. Be careful.`,
ArgsUsage: "<domain>",
Aliases: []string{"rm"},
HideHelpCommand: true,
UsageText: "abra app volume remove [options] <domain>",
Aliases: []string{"rm"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
Before: internal.SubCommandBefore,
EnableShellCompletion: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
cl, err := client.New(app.Server)
if err != nil {
@ -155,12 +144,13 @@ Passing "--force/-f" will select all volumes for removal. Be careful.`,
}
var appVolumeCommand = cli.Command{
Name: "volume",
Aliases: []string{"vl"},
Usage: "Manage app volumes",
ArgsUsage: "<domain>",
Subcommands: []cli.Command{
appVolumeListCommand,
appVolumeRemoveCommand,
Name: "volume",
Aliases: []string{"vl"},
Usage: "Manage app volumes",
UsageText: "abra app volume [command] [options] [arguments]",
HideHelpCommand: true,
Commands: []*cli.Command{
&appVolumeListCommand,
&appVolumeRemoveCommand,
},
}

View File

@ -1,6 +1,7 @@
package catalogue
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
@ -15,25 +16,23 @@ import (
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/go-git/go-git/v5"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var catalogueGenerateCommand = cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate the recipe catalogue",
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate the recipe catalogue",
HideHelpCommand: true,
UsageText: "abra catalogue generate [options] [<recipe>]",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.PublishFlag,
internal.DryFlag,
internal.SkipUpdatesFlag,
internal.ChaosFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `
Generate a new copy of the recipe catalogue.
Description: `Generate a new copy of the recipe catalogue.
It is possible to generate new metadata for a single recipe by passing
<recipe>. The existing local catalogue will be updated, not overwritten.
@ -45,14 +44,14 @@ If you have a Hub account you can have Abra log you in to avoid this. Pass
Push your new release to git.coopcloud.tech with "-p/--publish". This requires
that you have permission to git push to these repositories and have your SSH
keys configured on your account.`,
ArgsUsage: "[<recipe>]",
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
EnableShellCompletion: true,
ShellComplete: autocomplete.RecipeNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
recipeName := cmd.Args().First()
r := recipe.Get(recipeName)
if recipeName != "" {
internal.ValidateRecipe(c)
internal.ValidateRecipe(cmd)
}
if !internal.Chaos {
@ -205,11 +204,12 @@ keys configured on your account.`,
// CatalogueCommand defines the `abra catalogue` command and sub-commands.
var CatalogueCommand = cli.Command{
Name: "catalogue",
Usage: "Manage the recipe catalogue",
Aliases: []string{"c"},
ArgsUsage: "<recipe>",
Subcommands: []cli.Command{
catalogueGenerateCommand,
Name: "catalogue",
Usage: "Manage the recipe catalogue",
Aliases: []string{"c"},
HideHelpCommand: true,
UsageText: "abra catalogue [command] [options] [arguments]",
Commands: []*cli.Command{
&catalogueGenerateCommand,
},
}

View File

@ -2,6 +2,7 @@
package cli
import (
"context"
"errors"
"fmt"
"os"
@ -18,7 +19,7 @@ import (
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/web"
charmLog "github.com/charmbracelet/log"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
// AutoCompleteCommand helps people set up auto-complete in their shells
@ -26,23 +27,15 @@ var AutoCompleteCommand = cli.Command{
Name: "autocomplete",
Aliases: []string{"ac"},
Usage: "Configure shell autocompletion",
Description: `
Set up shell auto-completion.
Description: `Set up shell auto-completion.
Supported shells are: bash, fish, fizsh & zsh.
EXAMPLE:
abra autocomplete bash`,
Supported shells are: bash, fish, fizsh & zsh.`,
ArgsUsage: "<shell>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Action: func(c *cli.Context) error {
shellType := c.Args().First()
Action: func(ctx context.Context, cmd *cli.Command) error {
shellType := cmd.Args().First()
if shellType == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no shell provided"))
internal.ShowSubcommandHelpAndError(cmd, errors.New("no shell provided"))
}
supportedShells := map[string]bool{
@ -80,29 +73,26 @@ EXAMPLE:
switch shellType {
case "bash":
fmt.Println(fmt.Sprintf(`
# run the following commands once to install auto-completion
sudo mkdir -p /etc/bash_completion.d/
# run the following commands to install auto-completion
sudo mkdir /etc/bash_completion.d/
sudo cp %s /etc/bash_completion.d/abra
echo "source /etc/bash_completion.d/abra" >> ~/.bashrc
source /etc/bash_completion.d/abra
# To test, run the following: "abra app <hit tab key>" - you should see command completion!
`, autocompletionFile))
case "zsh":
fmt.Println(fmt.Sprintf(`
# run the following commands to once install auto-completion
sudo mkdir -p /etc/zsh/completion.d/
# run the following commands to install auto-completion
sudo mkdir /etc/zsh/completion.d/
sudo cp %s /etc/zsh/completion.d/abra
echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc
source /etc/zsh/completion.d/abra
# to test, run the following: "abra app <hit tab key>" - you should see command completion!
`, autocompletionFile))
case "fish":
fmt.Println(fmt.Sprintf(`
# run the following commands once to install auto-completion
# run the following commands to install auto-completion
sudo mkdir -p /etc/fish/completions
sudo cp %s /etc/fish/completions/abra
echo "source /etc/fish/completions/abra" >> ~/.config/fish/config.fish
source /etc/fish/completions/abra
# to test, run the following: "abra app <hit tab key>" - you should see command completion!
`, autocompletionFile))
}
@ -116,30 +106,24 @@ var UpgradeCommand = cli.Command{
Name: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade abra",
Description: `
Upgrade abra in-place with the latest stable or release candidate.
Description: `Upgrade abra in-place with the latest stable or release candidate.
Use "-r/--rc" to install the latest release candidate. Please bear in mind that
it may contain absolutely catastrophic deal-breaker bugs. Thank you very much
for the testing efforts 💗
EXAMPLE:
abra upgrade
abra upgrade --rc`,
for the testing efforts 💗`,
Flags: []cli.Flag{internal.RCFlag},
Action: func(c *cli.Context) error {
Action: func(ctx context.Context, cmd *cli.Command) error {
mainURL := "https://install.abra.coopcloud.tech"
cmd := exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash", mainURL))
c := exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash", mainURL))
if internal.RC {
releaseCandidateURL := "https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer"
cmd = exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash -s -- --rc", releaseCandidateURL))
c = exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash -s -- --rc", releaseCandidateURL))
}
log.Debugf("attempting to run %s", cmd)
log.Debugf("attempting to run %s", c)
if err := internal.RunCmd(cmd); err != nil {
if err := internal.RunCmd(c); err != nil {
log.Fatal(err)
}
@ -147,32 +131,32 @@ EXAMPLE:
},
}
func newAbraApp(version, commit string) *cli.App {
app := &cli.App{
Name: "abra",
Usage: `the Co-op Cloud command-line utility belt 🎩🐇
____ ____ _ _
/ ___|___ ___ _ __ / ___| | ___ _ _ __| |
| | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |
| |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |
\____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|
|_|
`,
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Commands: []cli.Command{
app.AppCommand,
server.ServerCommand,
recipe.RecipeCommand,
catalogue.CatalogueCommand,
UpgradeCommand,
AutoCompleteCommand,
func newAbraApp(version, commit string) *cli.Command {
app := &cli.Command{
Name: "abra",
Usage: "The Co-op Cloud command-line utility belt 🎩🐇",
UsageText: "abra [command] [options] [arguments]",
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
internal.NoInputFlag,
},
BashComplete: autocomplete.SubcommandComplete,
Commands: []*cli.Command{
&app.AppCommand,
&server.ServerCommand,
&recipe.RecipeCommand,
&catalogue.CatalogueCommand,
&UpgradeCommand,
&AutoCompleteCommand,
},
EnableShellCompletion: true,
UseShortOptionHandling: true,
HideHelpCommand: true,
ShellComplete: autocomplete.SubcommandComplete,
}
app.EnableBashCompletion = true
app.Before = func(c *cli.Context) error {
app.Before = func(ctx context.Context, cmd *cli.Command) error {
paths := []string{
config.ABRA_DIR,
config.SERVERS_DIR,
@ -190,14 +174,19 @@ func newAbraApp(version, commit string) *cli.App {
}
}
log.Logger.SetStyles(log.Styles())
charmLog.SetDefault(log.Logger)
log.Debugf("abra version %s, commit %s", version, commit)
return nil
}
cli.HelpFlag = &cli.BoolFlag{
Name: "help",
Aliases: []string{"h, H"},
Usage: "Show help",
Persistent: true,
}
return app
}
@ -205,7 +194,7 @@ func newAbraApp(version, commit string) *cli.App {
func RunApp(version, commit string) {
app := newAbraApp(version, commit)
if err := app.Run(os.Args); err != nil {
if err := app.Run(context.Background(), os.Args); err != nil {
log.Fatal(err)
}
}

View File

@ -1,10 +1,11 @@
package internal
import (
"context"
"os"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
// Secrets stores the variable from SecretsFlag
@ -12,7 +13,8 @@ var Secrets bool
// SecretsFlag turns on/off automatically generating secrets
var SecretsFlag = &cli.BoolFlag{
Name: "secrets, S",
Name: "secrets",
Aliases: []string{"S"},
Usage: "Automatically generate secrets",
Destination: &Secrets,
}
@ -22,7 +24,8 @@ var Pass bool
// PassFlag turns on/off storing generated secrets in pass
var PassFlag = &cli.BoolFlag{
Name: "pass, p",
Name: "pass",
Aliases: []string{"p"},
Usage: "Store the generated secrets in a local pass store",
Destination: &Pass,
}
@ -32,21 +35,24 @@ var PassRemove bool
// PassRemoveFlag turns on/off removing generated secrets from pass
var PassRemoveFlag = &cli.BoolFlag{
Name: "pass, p",
Name: "pass",
Aliases: []string{"p"},
Usage: "Remove generated secrets from a local pass store",
Destination: &PassRemove,
}
var File bool
var FileFlag = &cli.BoolFlag{
Name: "file, f",
Name: "file",
Aliases: []string{"f"},
Usage: "Treat input as a file",
Destination: &File,
}
var Trim bool
var TrimFlag = &cli.BoolFlag{
Name: "trim, t",
Name: "trim",
Aliases: []string{"t"},
Usage: "Trim input",
Destination: &Trim,
}
@ -56,7 +62,8 @@ var Force bool
// ForceFlag turns on/off force functionality.
var ForceFlag = &cli.BoolFlag{
Name: "force, f",
Name: "force",
Aliases: []string{"f"},
Usage: "Perform action without further prompt. Use with care!",
Destination: &Force,
}
@ -66,7 +73,8 @@ var Chaos bool
// ChaosFlag turns on/off chaos functionality.
var ChaosFlag = &cli.BoolFlag{
Name: "chaos, C",
Name: "chaos",
Aliases: []string{"C"},
Usage: "Proceed with uncommitted recipes changes. Use with care!",
Destination: &Chaos,
}
@ -76,16 +84,19 @@ var Tty bool
// TtyFlag turns on/off tty mode.
var TtyFlag = &cli.BoolFlag{
Name: "tty, T",
Name: "tty",
Aliases: []string{"T"},
Usage: "Disables TTY mode to run this command from a script.",
Destination: &Tty,
}
var NoInput bool
var NoInputFlag = &cli.BoolFlag{
Name: "no-input, n",
Name: "no-input",
Aliases: []string{"n"},
Usage: "Toggle non-interactive mode",
Destination: &NoInput,
Persistent: true,
}
// Debug stores the variable from DebugFlag.
@ -93,8 +104,10 @@ var Debug bool
// DebugFlag turns on/off verbose logging down to the DEBUG level.
var DebugFlag = &cli.BoolFlag{
Name: "debug, d",
Name: "debug",
Aliases: []string{"d"},
Destination: &Debug,
Persistent: true,
Usage: "Show DEBUG messages",
}
@ -103,9 +116,11 @@ var Offline bool
// DebugFlag turns on/off offline mode.
var OfflineFlag = &cli.BoolFlag{
Name: "offline, o",
Name: "offline",
Aliases: []string{"o"},
Destination: &Offline,
Usage: "Prefer offline & filesystem access when possible",
Persistent: true,
}
// ReleaseNotes stores the variable from ReleaseNotesFlag.
@ -113,7 +128,8 @@ var ReleaseNotes bool
// ReleaseNotesFlag turns on/off printing only release notes when upgrading.
var ReleaseNotesFlag = &cli.BoolFlag{
Name: "releasenotes, r",
Name: "releasenotes",
Aliases: []string{"r"},
Destination: &ReleaseNotes,
Usage: "Only show release notes",
}
@ -123,7 +139,8 @@ var MachineReadable bool
// MachineReadableFlag turns on/off machine readable output where supported
var MachineReadableFlag = &cli.BoolFlag{
Name: "machine, m",
Name: "machine",
Aliases: []string{"m"},
Destination: &MachineReadable,
Usage: "Output in a machine-readable format (where supported)",
}
@ -133,49 +150,56 @@ var RC bool
// RCFlag chooses the latest release candidate for install
var RCFlag = &cli.BoolFlag{
Name: "rc, r",
Name: "rc",
Aliases: []string{"r"},
Destination: &RC,
Usage: "Install the latest release candidate",
}
var Major bool
var MajorFlag = &cli.BoolFlag{
Name: "major, x",
Name: "major",
Aliases: []string{"x"},
Usage: "Increase the major part of the version",
Destination: &Major,
}
var Minor bool
var MinorFlag = &cli.BoolFlag{
Name: "minor, y",
Name: "minor",
Aliases: []string{"y"},
Usage: "Increase the minor part of the version",
Destination: &Minor,
}
var Patch bool
var PatchFlag = &cli.BoolFlag{
Name: "patch, z",
Name: "patch",
Aliases: []string{"z"},
Usage: "Increase the patch part of the version",
Destination: &Patch,
}
var Dry bool
var DryFlag = &cli.BoolFlag{
Name: "dry-run, r",
Name: "dry-run",
Aliases: []string{"r"},
Usage: "Only reports changes that would be made",
Destination: &Dry,
}
var Publish bool
var PublishFlag = &cli.BoolFlag{
Name: "publish, p",
Name: "publish",
Aliases: []string{"p"},
Usage: "Publish changes to git.coopcloud.tech",
Destination: &Publish,
}
var Domain string
var DomainFlag = &cli.StringFlag{
Name: "domain, D",
Name: "domain",
Aliases: []string{"D"},
Value: "",
Usage: "Choose a domain name",
Destination: &Domain,
@ -183,7 +207,8 @@ var DomainFlag = &cli.StringFlag{
var NewAppServer string
var NewAppServerFlag = &cli.StringFlag{
Name: "server, s",
Name: "server",
Aliases: []string{"s"},
Value: "",
Usage: "Show apps of a specific server",
Destination: &NewAppServer,
@ -191,21 +216,24 @@ var NewAppServerFlag = &cli.StringFlag{
var NoDomainChecks bool
var NoDomainChecksFlag = &cli.BoolFlag{
Name: "no-domain-checks, D",
Name: "no-domain-checks",
Aliases: []string{"D"},
Usage: "Disable public DNS checks",
Destination: &NoDomainChecks,
}
var StdErrOnly bool
var StdErrOnlyFlag = &cli.BoolFlag{
Name: "stderr, s",
Name: "stderr",
Aliases: []string{"s"},
Usage: "Only tail stderr",
Destination: &StdErrOnly,
}
var SinceLogs string
var SinceLogsFlag = &cli.StringFlag{
Name: "since, S",
Name: "since",
Aliases: []string{"S"},
Value: "",
Usage: "tail logs since YYYY-MM-DDTHH:MM:SSZ",
Destination: &SinceLogs,
@ -213,49 +241,56 @@ var SinceLogsFlag = &cli.StringFlag{
var DontWaitConverge bool
var DontWaitConvergeFlag = &cli.BoolFlag{
Name: "no-converge-checks, c",
Name: "no-converge-checks",
Aliases: []string{"c"},
Usage: "Don't wait for converge logic checks",
Destination: &DontWaitConverge,
}
var Watch bool
var WatchFlag = &cli.BoolFlag{
Name: "watch, w",
Name: "watch",
Aliases: []string{"w"},
Usage: "Watch status by polling repeatedly",
Destination: &Watch,
}
var OnlyErrors bool
var OnlyErrorFlag = &cli.BoolFlag{
Name: "errors, e",
Name: "errors",
Aliases: []string{"e"},
Usage: "Only show errors",
Destination: &OnlyErrors,
}
var SkipUpdates bool
var SkipUpdatesFlag = &cli.BoolFlag{
Name: "skip-updates, s",
Name: "skip-updates",
Aliases: []string{"s"},
Usage: "Skip updating recipe repositories",
Destination: &SkipUpdates,
}
var AllTags bool
var AllTagsFlag = &cli.BoolFlag{
Name: "all-tags, a",
Name: "all-tags",
Aliases: []string{"a"},
Usage: "List all tags, not just upgrades",
Destination: &AllTags,
}
var LocalCmd bool
var LocalCmdFlag = &cli.BoolFlag{
Name: "local, l",
Name: "local",
Aliases: []string{"l"},
Usage: "Run command locally",
Destination: &LocalCmd,
}
var RemoteUser string
var RemoteUserFlag = &cli.StringFlag{
Name: "user, u",
Name: "user",
Aliases: []string{"u"},
Value: "",
Usage: "User to run command within a service context",
Destination: &RemoteUser,
@ -263,7 +298,8 @@ var RemoteUserFlag = &cli.StringFlag{
var GitName string
var GitNameFlag = &cli.StringFlag{
Name: "git-name, gn",
Name: "git-name",
Aliases: []string{"gn"},
Value: "",
Usage: "Git (user) name to do commits with",
Destination: &GitName,
@ -271,7 +307,8 @@ var GitNameFlag = &cli.StringFlag{
var GitEmail string
var GitEmailFlag = &cli.StringFlag{
Name: "git-email, ge",
Name: "git-email",
Aliases: []string{"ge"},
Value: "",
Usage: "Git email name to do commits with",
Destination: &GitEmail,
@ -279,13 +316,14 @@ var GitEmailFlag = &cli.StringFlag{
var AllServices bool
var AllServicesFlag = &cli.BoolFlag{
Name: "all-services, a",
Name: "all-services",
Aliases: []string{"a"},
Usage: "Restart all services",
Destination: &AllServices,
}
// SubCommandBefore wires up pre-action machinery (e.g. --debug handling).
func SubCommandBefore(c *cli.Context) error {
func SubCommandBefore(ctx context.Context, cmd *cli.Command) error {
if Debug {
log.SetLevel(log.DebugLevel)
log.SetOutput(os.Stderr)

View File

@ -6,41 +6,17 @@ import (
"strings"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/lipgloss"
dockerClient "github.com/docker/docker/client"
)
var borderStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.ThickBorder()).
Padding(0, 1, 0, 1).
MaxWidth(79).
BorderForeground(lipgloss.Color("63"))
var headerStyle = lipgloss.NewStyle().
Underline(true).
Bold(true)
var leftStyle = lipgloss.NewStyle().
Bold(true)
var rightStyle = lipgloss.NewStyle()
// horizontal is a JoinHorizontal helper function.
func horizontal(left, mid, right string) string {
return lipgloss.JoinHorizontal(lipgloss.Left, left, mid, right)
}
// NewVersionOverview shows an upgrade or downgrade overview
func NewVersionOverview(
app appPkg.App,
warnMessages []string,
kind,
currentVersion,
chaosVersion,
newVersion,
releaseNotes string) error {
func NewVersionOverview(app appPkg.App, currentVersion, chaosVersion, newVersion, releaseNotes string) error {
tableCol := []string{"server", "recipe", "config", "domain", "version", "chaos", "to deploy"}
table := formatter.CreateTable(tableCol)
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
@ -51,36 +27,22 @@ func NewVersionOverview(
server = "local"
}
body := strings.Builder{}
body.WriteString(
borderStyle.Render(
lipgloss.JoinVertical(
lipgloss.Center,
headerStyle.Render(fmt.Sprintf("%s OVERVIEW", strings.ToUpper(kind))),
lipgloss.JoinVertical(
lipgloss.Left,
horizontal(leftStyle.Render("SERVER"), " ", rightStyle.Render(server)),
horizontal(leftStyle.Render("DOMAIN"), " ", rightStyle.Render(app.Domain)),
horizontal(leftStyle.Render("RECIPE"), " ", rightStyle.Render(app.Recipe.Name)),
horizontal(leftStyle.Render("CONFIG"), " ", rightStyle.Render(deployConfig)),
horizontal(leftStyle.Render("VERSION"), " ", rightStyle.Render(currentVersion)),
horizontal(leftStyle.Render("CHAOS"), " ", rightStyle.Render(chaosVersion)),
horizontal(leftStyle.Render("DEPLOY"), " ", rightStyle.Padding(0).Render(newVersion)),
),
),
),
)
fmt.Println(body.String())
table.Append([]string{
server,
app.Recipe.Name,
deployConfig,
app.Domain,
currentVersion,
chaosVersion,
newVersion,
})
table.Render()
if releaseNotes != "" && newVersion != "" {
fmt.Println()
fmt.Print(releaseNotes)
} else {
warnMessages = append(warnMessages, fmt.Sprintf("no release notes available for %s", newVersion))
}
for _, msg := range warnMessages {
log.Warn(msg)
log.Warnf("no release notes available for %s", newVersion)
}
if NoInput {
@ -88,66 +50,16 @@ func NewVersionOverview(
}
response := false
prompt := &survey.Confirm{Message: "proceed?"}
prompt := &survey.Confirm{
Message: "continue with deployment?",
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
log.Fatal("deployment cancelled")
}
return nil
}
// DeployOverview shows a deployment overview
func DeployOverview(app appPkg.App, warnMessages []string, version, chaosVersion string) error {
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"
}
body := strings.Builder{}
body.WriteString(
borderStyle.Render(
lipgloss.JoinVertical(
lipgloss.Center,
headerStyle.Render("DEPLOY OVERVIEW"),
lipgloss.JoinVertical(
lipgloss.Left,
horizontal(leftStyle.Render("SERVER"), " ", rightStyle.Render(server)),
horizontal(leftStyle.Render("DOMAIN"), " ", rightStyle.Render(app.Domain)),
horizontal(leftStyle.Render("RECIPE"), " ", rightStyle.Render(app.Recipe.Name)),
horizontal(leftStyle.Render("CONFIG"), " ", rightStyle.Render(deployConfig)),
horizontal(leftStyle.Render("VERSION"), " ", rightStyle.Render(version)),
horizontal(leftStyle.Render("CHAOS"), " ", rightStyle.Padding(0).Render(chaosVersion)),
),
),
),
)
fmt.Println(body.String())
for _, msg := range warnMessages {
log.Warn(msg)
}
if NoInput {
return nil
}
response := false
prompt := &survey.Confirm{Message: "proceed?"}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
log.Fatal("deployment cancelled")
log.Fatal("exiting as requested")
}
return nil
@ -206,3 +118,48 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
}
return nil
}
// DeployOverview shows a deployment overview
func DeployOverview(app appPkg.App, version, chaosVersion, message string) error {
tableCol := []string{"server", "recipe", "config", "domain", "version", "chaos"}
table := formatter.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,
app.Recipe.Name,
deployConfig,
app.Domain,
version,
chaosVersion,
})
table.Render()
if NoInput {
return nil
}
response := false
prompt := &survey.Confirm{
Message: message,
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
log.Fatal("exiting as requested")
}
return nil
}

View File

@ -4,13 +4,13 @@ import (
"os"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
// ShowSubcommandHelpAndError exits the program on error, logs the error to the
// terminal, and shows the help command.
func ShowSubcommandHelpAndError(c *cli.Context, err interface{}) {
if err2 := cli.ShowSubcommandHelp(c); err2 != nil {
func ShowSubcommandHelpAndError(cmd *cli.Command, err interface{}) {
if err2 := cli.ShowSubcommandHelp(cmd); err2 != nil {
log.Error(err2)
}
log.Error(err)

View File

@ -9,12 +9,12 @@ import (
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
// ValidateRecipe ensures the recipe arg is valid.
func ValidateRecipe(c *cli.Context) recipe.Recipe {
recipeName := c.Args().First()
func ValidateRecipe(cmd *cli.Command) recipe.Recipe {
recipeName := cmd.Args().First()
if recipeName == "" && !NoInput {
var recipes []string
@ -54,7 +54,7 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
}
if recipeName == "" {
ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
ShowSubcommandHelpAndError(cmd, errors.New("no recipe name provided"))
}
chosenRecipe := recipe.Get(recipeName)
@ -64,7 +64,7 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
}
_, err = chosenRecipe.GetComposeConfig(nil)
if err != nil {
if c.Command.Name == "generate" {
if cmd.Name == "generate" {
if strings.Contains(err.Error(), "missing a compose") {
log.Fatal(err)
}
@ -83,11 +83,11 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
}
// ValidateApp ensures the app name arg is valid.
func ValidateApp(c *cli.Context) app.App {
appName := c.Args().First()
func ValidateApp(cmd *cli.Command) app.App {
appName := cmd.Args().First()
if appName == "" {
ShowSubcommandHelpAndError(c, errors.New("no app provided"))
ShowSubcommandHelpAndError(cmd, errors.New("no app provided"))
}
app, err := app.Get(appName)
@ -101,8 +101,8 @@ func ValidateApp(c *cli.Context) app.App {
}
// ValidateDomain ensures the domain name arg is valid.
func ValidateDomain(c *cli.Context) string {
domainName := c.Args().First()
func ValidateDomain(cmd *cli.Command) string {
domainName := cmd.Args().First()
if domainName == "" && !NoInput {
prompt := &survey.Input{
@ -115,7 +115,7 @@ func ValidateDomain(c *cli.Context) string {
}
if domainName == "" {
ShowSubcommandHelpAndError(c, errors.New("no domain provided"))
ShowSubcommandHelpAndError(cmd, errors.New("no domain provided"))
}
log.Debugf("validated %s as domain argument", domainName)
@ -124,10 +124,10 @@ func ValidateDomain(c *cli.Context) string {
}
// ValidateSubCmdFlags ensures flag order conforms to correct order
func ValidateSubCmdFlags(c *cli.Context) bool {
for argIdx, arg := range c.Args() {
func ValidateSubCmdFlags(cmd *cli.Command) bool {
for argIdx, arg := range cmd.Args().Slice() {
if !strings.HasPrefix(arg, "--") {
for _, flag := range c.Args()[argIdx:] {
for _, flag := range cmd.Args().Slice()[argIdx:] {
if strings.HasPrefix(flag, "--") {
return false
}
@ -138,8 +138,8 @@ func ValidateSubCmdFlags(c *cli.Context) bool {
}
// ValidateServer ensures the server name arg is valid.
func ValidateServer(c *cli.Context) string {
serverName := c.Args().First()
func ValidateServer(cmd *cli.Command) string {
serverName := cmd.Args().First()
serverNames, err := config.ReadServerNames()
if err != nil {
@ -164,11 +164,11 @@ func ValidateServer(c *cli.Context) string {
}
if serverName == "" {
ShowSubcommandHelpAndError(c, errors.New("no server provided"))
ShowSubcommandHelpAndError(cmd, errors.New("no server provided"))
}
if !matched {
ShowSubcommandHelpAndError(c, errors.New("server doesn't exist?"))
ShowSubcommandHelpAndError(cmd, errors.New("server doesn't exist?"))
}
log.Debugf("validated %s as server argument", serverName)

View File

@ -1,27 +1,27 @@
package recipe
import (
"context"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var recipeDiffCommand = cli.Command{
Name: "diff",
Usage: "Show unstaged changes in recipe config",
Description: "This command requires /usr/bin/git.",
Aliases: []string{"d"},
ArgsUsage: "<recipe>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
r := internal.ValidateRecipe(c)
Name: "diff",
Usage: "Show unstaged changes in recipe config",
Description: "This command requires /usr/bin/git.",
HideHelpCommand: true,
Aliases: []string{"d"},
UsageText: "abra recipe diff [options] <recipe>",
Before: internal.SubCommandBefore,
EnableShellCompletion: true,
ShellComplete: autocomplete.RecipeNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
r := internal.ValidateRecipe(cmd)
if err := gitPkg.DiffUnstaged(r.Dir); err != nil {
log.Fatal(err)

View File

@ -1,32 +1,30 @@
package recipe
import (
"context"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var recipeFetchCommand = cli.Command{
Name: "fetch",
Usage: "Fetch recipe(s)",
Aliases: []string{"f"},
ArgsUsage: "[<recipe>]",
Description: "Retrieves all recipes if no <recipe> argument is passed",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
Name: "fetch",
Usage: "Fetch recipe(s)",
Aliases: []string{"f"},
UsageText: "abra recipe fetch [options] [<recipe>]",
Description: "Retrieves all recipes if no <recipe> argument is passed",
Before: internal.SubCommandBefore,
EnableShellCompletion: true,
ShellComplete: autocomplete.RecipeNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
recipeName := cmd.Args().First()
r := recipe.Get(recipeName)
if recipeName != "" {
internal.ValidateRecipe(c)
internal.ValidateRecipe(cmd)
if err := r.Ensure(false, false); err != nil {
log.Fatal(err)
}

View File

@ -1,6 +1,7 @@
package recipe
import (
"context"
"fmt"
"coopcloud.tech/abra/cli/internal"
@ -8,49 +9,34 @@ import (
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var recipeLintCommand = cli.Command{
Name: "lint",
Usage: "Lint a recipe",
Aliases: []string{"l"},
ArgsUsage: "<recipe>",
Name: "lint",
Usage: "Lint a recipe",
Aliases: []string{"l"},
UsageText: "abra recipe lint [options] <recipe>",
HideHelpCommand: true,
Flags: []cli.Flag{
internal.DebugFlag,
internal.OnlyErrorFlag,
internal.OfflineFlag,
internal.NoInputFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
Before: internal.SubCommandBefore,
EnableShellCompletion: true,
ShellComplete: autocomplete.RecipeNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
recipe := internal.ValidateRecipe(cmd)
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
headers := []string{
"ref",
"rule",
"severity",
"satisfied",
"skipped",
"resolve",
}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table.Headers(headers...)
tableCol := []string{"ref", "rule", "severity", "satisfied", "skipped", "resolve"}
table := formatter.CreateTable(tableCol)
hasError := false
var rows [][]string
var warnMessages []string
bar := formatter.CreateProgressbar(-1, "running recipe lint rules...")
for level := range lint.LintRules {
for _, rule := range lint.LintRules[level] {
if internal.OnlyErrors && rule.Level != "error" {
@ -72,7 +58,7 @@ var recipeLintCommand = cli.Command{
if !skipped {
ok, err := rule.Function(recipe)
if err != nil {
warnMessages = append(warnMessages, err.Error())
log.Warn(err)
}
if !ok && rule.Level == "error" {
@ -92,30 +78,26 @@ var recipeLintCommand = cli.Command{
}
}
row := []string{
table.Append([]string{
rule.Ref,
rule.Description,
rule.Level,
satisfiedOutput,
skippedOutput,
rule.HowToResolve,
}
})
rows = append(rows, row)
table.Row(row...)
bar.Add(1)
}
}
if len(rows) > 0 {
fmt.Println(table)
if table.NumLines() > 0 {
fmt.Println()
table.Render()
}
for _, warnMsg := range warnMessages {
log.Warn(warnMsg)
}
if hasError {
log.Warnf("critical errors present in %s config", recipe.Name)
}
if hasError {
log.Warn("watch out, some critical errors are present in your recipe config")
}
return nil

View File

@ -1,6 +1,7 @@
package recipe
import (
"context"
"fmt"
"sort"
"strconv"
@ -10,29 +11,30 @@ import (
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var pattern string
var patternFlag = &cli.StringFlag{
Name: "pattern, p",
Name: "pattern",
Aliases: []string{"p"},
Value: "",
Usage: "Simple string to filter recipes",
Destination: &pattern,
}
var recipeListCommand = cli.Command{
Name: "list",
Usage: "List available recipes",
Aliases: []string{"ls"},
Name: "list",
Usage: "List recipes",
HideHelpCommand: true,
UsageText: "abra recipe list [options]",
Aliases: []string{"ls"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.MachineReadableFlag,
patternFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
Action: func(ctx context.Context, cmd *cli.Command) error {
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
log.Fatal(err.Error())
@ -41,27 +43,12 @@ var recipeListCommand = cli.Command{
recipes := catl.Flatten()
sort.Sort(recipe.ByRecipeName(recipes))
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
tableCol := []string{"name", "category", "status", "healthcheck", "backups", "email", "tests", "SSO"}
table := formatter.CreateTable(tableCol)
headers := []string{
"name",
"category",
"status",
"healthcheck",
"backups",
"email",
"tests",
"SSO",
}
table.Headers(headers...)
var rows [][]string
len := 0
for _, recipe := range recipes {
row := []string{
tableRow := []string{
recipe.Name,
recipe.Category,
strconv.Itoa(recipe.Features.Status),
@ -74,27 +61,23 @@ var recipeListCommand = cli.Command{
if pattern != "" {
if strings.Contains(recipe.Name, pattern) {
table.Row(row...)
rows = append(rows, row)
table.Append(tableRow)
len++
}
} else {
table.Row(row...)
rows = append(rows, row)
table.Append(tableRow)
len++
}
}
if len(rows) > 0 {
if table.NumLines() > 0 {
if internal.MachineReadable {
out, err := formatter.ToJSON(headers, rows)
if err != nil {
log.Fatal("unable to render to JSON: %s", err)
}
fmt.Println(out)
return nil
table.SetCaption(false, "")
table.JSONRender()
} else {
table.SetCaption(true, fmt.Sprintf("total recipes: %v", len))
table.Render()
}
fmt.Println(table)
log.Infof("total recipes: %v", len(rows))
}
return nil

View File

@ -2,6 +2,7 @@ package recipe
import (
"bytes"
"context"
"errors"
"fmt"
"os"
@ -13,7 +14,7 @@ import (
"coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
// recipeMetadata is the recipe metadata for the README.md
@ -34,27 +35,24 @@ var recipeNewCommand = cli.Command{
Name: "new",
Aliases: []string{"n"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.OfflineFlag,
internal.GitNameFlag,
internal.GitEmailFlag,
},
Before: internal.SubCommandBefore,
Usage: "Create a new recipe",
ArgsUsage: "<recipe>",
Description: `
Create a new recipe.
Before: internal.SubCommandBefore,
Usage: "Create a new recipe",
UsageText: "abra recipe new [options] <recipe>",
HideHelpCommand: true,
Description: `Create a new recipe.
Abra uses the built-in example repository which is available here:
https://git.coopcloud.tech/coop-cloud/example`,
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
Action: func(ctx context.Context, cmd *cli.Command) error {
recipeName := cmd.Args().First()
r := recipe.Get(recipeName)
if recipeName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
internal.ShowSubcommandHelpAndError(cmd, errors.New("no recipe name provided"))
}
if _, err := os.Stat(r.Dir); !os.IsNotExist(err) {

View File

@ -1,7 +1,7 @@
package recipe
import (
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
// RecipeCommand defines all recipe related sub-commands.
@ -9,9 +9,8 @@ var RecipeCommand = cli.Command{
Name: "recipe",
Aliases: []string{"r"},
Usage: "Manage recipes",
ArgsUsage: "<recipe>",
Description: `
A recipe is a blueprint for an app. It is a bunch of config files which
UsageText: "abra recipe [command] [options] [arguments]",
Description: `A recipe is a blueprint for an app. It is a bunch of config files which
describe how to deploy and maintain an app. Recipes are maintained by the Co-op
Cloud community and you can use Abra to read them, deploy them and create apps
for you.
@ -19,16 +18,17 @@ for you.
Anyone who uses a recipe can become a maintainer. Maintainers typically make
sure the recipe is in good working order and the config upgraded in a timely
manner.`,
Subcommands: []cli.Command{
recipeFetchCommand,
recipeLintCommand,
recipeListCommand,
recipeNewCommand,
recipeReleaseCommand,
recipeSyncCommand,
recipeUpgradeCommand,
recipeVersionCommand,
recipeResetCommand,
recipeDiffCommand,
HideHelpCommand: true,
Commands: []*cli.Command{
&recipeFetchCommand,
&recipeLintCommand,
&recipeListCommand,
&recipeNewCommand,
&recipeReleaseCommand,
&recipeSyncCommand,
&recipeUpgradeCommand,
&recipeVersionCommand,
&recipeResetCommand,
&recipeDiffCommand,
},
}

View File

@ -1,6 +1,7 @@
package recipe
import (
"context"
"errors"
"fmt"
"os"
@ -18,17 +19,19 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/distribution/reference"
"github.com/go-git/go-git/v5"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var recipeReleaseCommand = cli.Command{
Name: "release",
Aliases: []string{"rl"},
Usage: "Release a new recipe version",
ArgsUsage: "<recipe> [<version>]",
Description: `
Create a new version of a recipe. These versions are then published on the
Co-op Cloud recipe catalogue. These versions take the following form:
Name: "release",
Aliases: []string{"rl"},
Usage: "Release a new recipe version",
HideHelpCommand: true,
UsageText: "abra recipe release [options] <recipe> [<version>]",
Description: `Create a new version of a recipe.
These versions are then published on the Co-op Cloud recipe catalogue. These
versions take the following form:
a.b.c+x.y.z
@ -46,19 +49,17 @@ Publish your new release to git.coopcloud.tech with "-p/--publish". This
requires that you have permission to git push to these repositories and have
your SSH keys configured on your account.`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.DryFlag,
internal.MajorFlag,
internal.MinorFlag,
internal.PatchFlag,
internal.PublishFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
Before: internal.SubCommandBefore,
EnableShellCompletion: true,
ShellComplete: autocomplete.RecipeNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
recipe := internal.ValidateRecipe(cmd)
imagesTmp, err := getImageVersions(recipe)
if err != nil {
@ -75,7 +76,7 @@ your SSH keys configured on your account.`,
log.Fatalf("main app service version for %s is empty?", recipe.Name)
}
tagString := c.Args().Get(1)
tagString := cmd.Args().Get(1)
if tagString != "" {
if _, err := tagcmp.Parse(tagString); err != nil {
log.Fatalf("cannot parse %s, invalid tag specified?", tagString)

View File

@ -1,32 +1,32 @@
package recipe
import (
"context"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/go-git/go-git/v5"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var recipeResetCommand = cli.Command{
Name: "reset",
Usage: "Remove all unstaged changes from recipe config",
Description: "WARNING: this will delete your changes. Be Careful.",
Aliases: []string{"rs"},
ArgsUsage: "<recipe>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
Name: "reset",
Usage: "Remove all unstaged changes from recipe config",
Description: "WARNING: this will delete your changes. Be Careful.",
HideHelpCommand: true,
Aliases: []string{"rs"},
UsageText: "abra recipe reset [options] <recipe>",
Before: internal.SubCommandBefore,
EnableShellCompletion: true,
ShellComplete: autocomplete.RecipeNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
recipeName := cmd.Args().First()
r := recipe.Get(recipeName)
if recipeName != "" {
internal.ValidateRecipe(c)
internal.ValidateRecipe(cmd)
}
repo, err := git.PlainOpen(r.Dir)

View File

@ -1,6 +1,7 @@
package recipe
import (
"context"
"fmt"
"strconv"
@ -12,35 +13,35 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var recipeSyncCommand = cli.Command{
Name: "sync",
Aliases: []string{"s"},
Usage: "Sync recipe version label",
ArgsUsage: "<recipe> [<version>]",
Name: "sync",
Aliases: []string{"s"},
Usage: "Sync recipe version label",
HideHelpCommand: true,
UsageText: "abra recipe lint [options] <recipe> [<version>]",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.DryFlag,
internal.MajorFlag,
internal.MinorFlag,
internal.PatchFlag,
},
Before: internal.SubCommandBefore,
Description: `
Generate labels for the main recipe service (i.e. by convention, the service
named "app") which corresponds to the following format:
Description: `Generate labels for the main recipe service.
By convention, the service named "app" using the following format:
coop-cloud.${STACK_NAME}.version=<version>
Where <version> can be specifed on the command-line or Abra can attempt to
auto-generate it for you. The <recipe> configuration will be updated on the
local file system.`,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
EnableShellCompletion: true,
ShellComplete: autocomplete.RecipeNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
recipe := internal.ValidateRecipe(cmd)
mainApp, err := internal.GetMainAppImage(recipe)
if err != nil {
@ -59,7 +60,7 @@ local file system.`,
log.Fatal(err)
}
nextTag := c.Args().Get(1)
nextTag := cmd.Args().Get(1)
if len(tags) == 0 && nextTag == "" {
log.Warnf("no git tags found for %s", recipe.Name)
if internal.NoInput {

View File

@ -2,6 +2,7 @@ package recipe
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
@ -19,7 +20,7 @@ import (
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/distribution/reference"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
type imgPin struct {
@ -27,8 +28,8 @@ type imgPin struct {
version tagcmp.Tag
}
// anUpgrade represents a single service upgrade (as within a recipe), and the list of tags that it can be upgraded to,
// for serialization purposes.
// anUpgrade represents a single service upgrade (as within a recipe), and the
// list of tags that it can be upgraded to, for serialization purposes.
type anUpgrade struct {
Service string `json:"service"`
Image string `json:"image"`
@ -37,13 +38,13 @@ type anUpgrade struct {
}
var recipeUpgradeCommand = cli.Command{
Name: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade recipe image tags",
Description: `
Parse all image tags within the given <recipe> configuration and prompt with
more recent tags to upgrade to. It will update the relevant compose file tags
on the local file system.
Name: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade recipe image tags",
HideHelpCommand: true,
Description: `Upgrade a given <recipe> configuration.
It will update the relevant compose file tags on the local file system.
Some image tags cannot be parsed because they do not follow some sort of
semver-like convention. In this case, all possible tags will be listed and it
@ -53,25 +54,20 @@ The command is interactive and will show a select input which allows you to
make a seclection. Use the "?" key to see more help on navigating this
interface.
You may invoke this command in "wizard" mode and be prompted for input.
EXAMPLE:
abra recipe upgrade`,
ArgsUsage: "<recipe>",
You may invoke this command in "wizard" mode and be prompted for input.`,
UsageText: "abra recipe upgrade [options] [<recipe>]",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.PatchFlag,
internal.MinorFlag,
internal.MajorFlag,
internal.MachineReadableFlag,
internal.AllTagsFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
Before: internal.SubCommandBefore,
EnableShellCompletion: true,
ShellComplete: autocomplete.RecipeNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
recipe := internal.ValidateRecipe(cmd)
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)

View File

@ -1,6 +1,7 @@
package recipe
import (
"context"
"fmt"
"sort"
@ -9,7 +10,8 @@ import (
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli"
"github.com/olekukonko/tablewriter"
"github.com/urfave/cli/v3"
)
func sortServiceByName(versions [][]string) func(i, j int) bool {
@ -23,22 +25,19 @@ func sortServiceByName(versions [][]string) func(i, j int) bool {
}
var recipeVersionCommand = cli.Command{
Name: "versions",
Aliases: []string{"v"},
Usage: "List recipe versions",
ArgsUsage: "<recipe>",
Name: "versions",
Aliases: []string{"v"},
Usage: "List recipe versions",
UsageText: "abra recipe version [options] <recipe>",
HideHelpCommand: true,
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
internal.NoInputFlag,
internal.MachineReadableFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
var warnMessages []string
recipe := internal.ValidateRecipe(c)
Before: internal.SubCommandBefore,
EnableShellCompletion: true,
ShellComplete: autocomplete.RecipeNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
recipe := internal.ValidateRecipe(cmd)
catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
if err != nil {
@ -47,65 +46,47 @@ var recipeVersionCommand = cli.Command{
recipeMeta, ok := catl[recipe.Name]
if !ok {
warnMessages = append(warnMessages, "retrieved versions from local recipe repository")
log.Warn("no published versions in catalogue, trying local recipe repository")
recipeVersions, err := recipe.GetRecipeVersions()
if err != nil {
warnMessages = append(warnMessages, err.Error())
log.Warn(err)
}
recipeMeta = recipePkg.RecipeMeta{Versions: recipeVersions}
}
if len(recipeMeta.Versions) == 0 {
log.Fatalf("%s has no published versions?", recipe.Name)
log.Fatalf("%s has no catalogue published versions?", recipe.Name)
}
tableCols := []string{"version", "service", "image", "tag"}
aggregated_table := formatter.CreateTable(tableCols)
for i := len(recipeMeta.Versions) - 1; i >= 0; i-- {
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table.Headers("SERVICE", "NAME", "TAG")
table := formatter.CreateTable(tableCols)
for version, meta := range recipeMeta.Versions[i] {
var allRows [][]string
var rows [][]string
var versions [][]string
for service, serviceMeta := range meta {
rows = append(rows, []string{service, serviceMeta.Image, serviceMeta.Tag})
allRows = append(allRows, []string{version, service, serviceMeta.Image, serviceMeta.Tag})
versions = append(versions, []string{version, service, serviceMeta.Image, serviceMeta.Tag})
}
sort.Slice(rows, sortServiceByName(rows))
sort.Slice(versions, sortServiceByName(versions))
table.Rows(rows...)
for _, version := range versions {
table.Append(version)
aggregated_table.Append(version)
}
if !internal.MachineReadable {
fmt.Println(table)
log.Infof("VERSION: %s", version)
table.SetAutoMergeCellsByColumnIndex([]int{0})
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.Render()
fmt.Println()
continue
}
if internal.MachineReadable {
sort.Slice(allRows, sortServiceByName(allRows))
headers := []string{"VERSION", "SERVICE", "NAME", "TAG"}
out, err := formatter.ToJSON(headers, allRows)
if err != nil {
log.Fatal("unable to render to JSON: %s", err)
}
fmt.Println(out)
continue
}
}
}
if !internal.MachineReadable {
for _, warnMsg := range warnMessages {
log.Warn(warnMsg)
}
if internal.MachineReadable {
aggregated_table.JSONRender()
}
return nil

View File

@ -1,6 +1,7 @@
package server
import (
"context"
"errors"
"os"
"path/filepath"
@ -13,12 +14,13 @@ import (
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/server"
sshPkg "coopcloud.tech/abra/pkg/ssh"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var local bool
var localFlag = &cli.BoolFlag{
Name: "local, l",
Name: "local",
Aliases: []string{"l"},
Usage: "Use local server",
Destination: &local,
}
@ -92,15 +94,16 @@ func createServerDir(name string) (bool, error) {
}
var serverAddCommand = cli.Command{
Name: "add",
Aliases: []string{"a"},
Usage: "Add a new server to your configuration",
Description: `
Add a new server to your configuration so that it can be managed by Abra.
Name: "add",
Aliases: []string{"a"},
Usage: "Add a new server",
UsageText: "abra server add [options] <domain>",
HideHelpCommand: true,
Description: `Add a new server to your configuration so that it can be managed by Abra.
Abra relies on the standard SSH command-line and ~/.ssh/config for client
connection details. You must configure an entry per-host in your ~/.ssh/config
for each server. For example:
for each server:
Host example.com example
Hostname example.com
@ -108,32 +111,31 @@ for each server. For example:
Port 12345
IdentityFile ~/.ssh/example@somewhere
You can then add a server like so:
abra server add example.com
If "--local" is passed, then Abra assumes that the current local server is
intended as the target server. This is useful when you want to have your entire
Co-op Cloud config located on the server itself, and not on your local
developer machine. The domain is then set to "default".`,
developer machine. The domain is then set to "default".
You can also pass "--no-domain-checks/-D" flag to use any arbitrary name
instead of a real domain. The host will be resolved with the "Hostname" entry
of your ~/.ssh/config. Checks for a valid online domain will be skipped.`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.NoDomainChecksFlag,
localFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<name>",
Action: func(c *cli.Context) error {
if len(c.Args()) > 0 && local || !internal.ValidateSubCmdFlags(c) {
Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Len() > 0 && local || !internal.ValidateSubCmdFlags(cmd) {
err := errors.New("cannot use <name> and --local together")
internal.ShowSubcommandHelpAndError(c, err)
internal.ShowSubcommandHelpAndError(cmd, err)
}
var name string
if local {
name = "default"
} else {
name = internal.ValidateDomain(c)
name = internal.ValidateDomain(cmd)
}
// NOTE(d1): reasonable 5 second timeout for connections which can't
@ -163,8 +165,10 @@ developer machine. The domain is then set to "default".`,
return nil
}
if _, err := dns.EnsureIPv4(name); err != nil {
log.Warn(err)
if !internal.NoDomainChecks {
if _, err := dns.EnsureIPv4(name); err != nil {
log.Fatal(err)
}
}
_, err := createServerDir(name)

View File

@ -1,59 +1,54 @@
package server
import (
"fmt"
"context"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/context"
contextPkg "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/docker/cli/cli/connhelper/ssh"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var serverListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List managed servers",
Name: "list",
Aliases: []string{"ls"},
Usage: "List managed servers",
UsageText: "abra server list [options]",
HideHelpCommand: true,
Flags: []cli.Flag{
internal.DebugFlag,
internal.MachineReadableFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
dockerContextStore := context.NewDefaultDockerContextStore()
Action: func(ctx context.Context, cmd *cli.Command) error {
dockerContextStore := contextPkg.NewDefaultDockerContextStore()
contexts, err := dockerContextStore.Store.List()
if err != nil {
log.Fatal(err)
}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
headers := []string{"NAME", "HOST"}
table.Headers(headers...)
tableColumns := []string{"name", "host"}
table := formatter.CreateTable(tableColumns)
serverNames, err := config.ReadServerNames()
if err != nil {
log.Fatal(err)
}
var rows [][]string
for _, serverName := range serverNames {
var row []string
for _, ctx := range contexts {
endpoint, err := context.GetContextEndpoint(ctx)
for _, dockerCtx := range contexts {
endpoint, err := contextPkg.GetContextEndpoint(dockerCtx)
if err != nil && strings.Contains(err.Error(), "does not exist") {
// No local context found, we can continue safely
continue
}
if ctx.Name == serverName {
if dockerCtx.Name == serverName {
sp, err := ssh.ParseURL(endpoint)
if err != nil {
log.Fatal(err)
@ -64,7 +59,6 @@ var serverListCommand = cli.Command{
}
row = []string{serverName, sp.Host}
rows = append(rows, row)
}
}
@ -74,22 +68,17 @@ var serverListCommand = cli.Command{
} else {
row = []string{serverName, "unknown"}
}
rows = append(rows, row)
}
table.Row(row...)
table.Append(row)
}
if internal.MachineReadable {
out, err := formatter.ToJSON(headers, rows)
if err != nil {
log.Fatal("unable to render to JSON: %s", err)
}
fmt.Println(out)
table.JSONRender()
return nil
}
fmt.Println(table)
table.Render()
return nil
},

View File

@ -9,13 +9,14 @@ import (
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/docker/docker/api/types/filters"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var allFilter bool
var allFilterFlag = &cli.BoolFlag{
Name: "all, a",
Name: "all",
Aliases: []string{"a"},
Usage: "Remove all unused images not just dangling ones",
Destination: &allFilter,
}
@ -23,17 +24,19 @@ var allFilterFlag = &cli.BoolFlag{
var volumesFilter bool
var volumesFilterFlag = &cli.BoolFlag{
Name: "volumes, v",
Name: "volumes",
Aliases: []string{"v"},
Usage: "Prune volumes. This will remove app data, Be Careful!",
Destination: &volumesFilter,
}
var serverPruneCommand = cli.Command{
Name: "prune",
Aliases: []string{"p"},
Usage: "Prune resources on a server",
Description: `
Prunes unused containers, networks, and dangling images.
Name: "prune",
Aliases: []string{"p"},
Usage: "Prune resources on a server",
UsageText: "abra server prune [options] <server>",
HideHelpCommand: true,
Description: `Prunes unused containers, networks, and dangling images.
Use "-v/--volumes" to remove volumes that are not associated with a deployed
app. This can result in unwanted data loss if not used carefully.`,
@ -41,14 +44,12 @@ app. This can result in unwanted data loss if not used carefully.`,
Flags: []cli.Flag{
allFilterFlag,
volumesFilterFlag,
internal.DebugFlag,
internal.OfflineFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.ServerNameComplete,
Action: func(c *cli.Context) error {
serverName := internal.ValidateServer(c)
Before: internal.SubCommandBefore,
EnableShellCompletion: true,
ShellComplete: autocomplete.ServerNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
serverName := internal.ValidateServer(cmd)
cl, err := client.New(serverName)
if err != nil {
@ -57,7 +58,6 @@ app. This can result in unwanted data loss if not used carefully.`,
var args filters.Args
ctx := context.Background()
cr, err := cl.ContainersPrune(ctx, args)
if err != nil {
log.Fatal(err)

View File

@ -1,6 +1,7 @@
package server
import (
"context"
"os"
"path/filepath"
@ -9,29 +10,25 @@ import (
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
var serverRemoveCommand = cli.Command{
Name: "remove",
Aliases: []string{"rm"},
ArgsUsage: "<server>",
Usage: "Remove a managed server",
Description: `
Remove a managed server.
Name: "remove",
Aliases: []string{"rm"},
UsageText: "abra server remove [options] <domain>",
Usage: "Remove a managed server",
HideHelpCommand: true,
Description: `Remove a managed server.
Abra will remove the internal bookkeeping (~/.abra/servers/...) and underlying
client connection context. This server will then be lost in time, like tears in
rain.`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.ServerNameComplete,
Action: func(c *cli.Context) error {
serverName := internal.ValidateServer(c)
Abra will remove the internal bookkeeping ($ABRA_DIR/servers/...) and
underlying client connection context. This server will then be lost in time,
like tears in rain.`,
Before: internal.SubCommandBefore,
EnableShellCompletion: true,
ShellComplete: autocomplete.ServerNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
serverName := internal.ValidateServer(cmd)
if err := client.DeleteContext(serverName); err != nil {
log.Fatal(err)

View File

@ -1,18 +1,20 @@
package server
import (
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
// ServerCommand defines the `abra server` command and its subcommands
var ServerCommand = cli.Command{
Name: "server",
Aliases: []string{"s"},
Usage: "Manage servers",
Subcommands: []cli.Command{
serverAddCommand,
serverListCommand,
serverRemoveCommand,
serverPruneCommand,
Name: "server",
Aliases: []string{"s"},
Usage: "Manage servers",
UsageText: "abra server [command] [options] [arguments]",
HideHelpCommand: true,
Commands: []*cli.Command{
&serverAddCommand,
&serverListCommand,
&serverRemoveCommand,
&serverPruneCommand,
},
}

View File

@ -23,44 +23,44 @@ import (
dockerclient "github.com/docker/docker/client"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
const SERVER = "localhost"
var majorUpdate bool
var majorFlag = &cli.BoolFlag{
Name: "major, m",
Name: "major",
Aliases: []string{"m"},
Usage: "Also check for major updates",
Destination: &majorUpdate,
}
var updateAll bool
var allFlag = &cli.BoolFlag{
Name: "all, a",
Name: "all",
Aliases: []string{"a"},
Usage: "Update all deployed apps",
Destination: &updateAll,
}
// Notify checks for available upgrades
var Notify = cli.Command{
Name: "notify",
Aliases: []string{"n"},
Usage: "Check for available upgrades",
Name: "notify",
Aliases: []string{"n"},
Usage: "Check for available upgrades",
UsageText: "kadabra notify [options]",
HideHelpCommand: true,
Flags: []cli.Flag{
internal.DebugFlag,
majorFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `
Read the deployed app versions and look for new versions in the recipe
catalogue.
Description: `Notify on new versions for deployed apps.
If a new patch/minor version is available, a notification is printed.
Use "--major" to include new major versions.`,
Action: func(c *cli.Context) error {
Action: func(ctx context.Context, cmd *cli.Command) error {
cl, err := client.New("default")
if err != nil {
log.Fatal(err)
@ -92,20 +92,18 @@ Use "--major" to include new major versions.`,
// UpgradeApp upgrades apps.
var UpgradeApp = cli.Command{
Name: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade apps",
ArgsUsage: "<stack-name> <recipe>",
Name: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade apps",
UsageText: "kadabra notify [options] <stack> <recipe>",
HideHelpCommand: true,
Flags: []cli.Flag{
internal.DebugFlag,
internal.ChaosFlag,
majorFlag,
allFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `
Upgrade an app by specifying stack name and recipe.
Description: `Upgrade an app by specifying stack name and recipe.
Use "--all" to upgrade every deployed app.
@ -116,15 +114,15 @@ available, the app is upgraded.
To include major versions use the "--major" flag. You probably don't want that
as it will break things. Only apps that are not deployed with "--chaos" are
upgraded, to update chaos deployments use the "--chaos" flag. Use it with care.`,
Action: func(c *cli.Context) error {
Action: func(ctx context.Context, cmd *cli.Command) error {
cl, err := client.New("default")
if err != nil {
log.Fatal(err)
}
if !updateAll {
stackName := c.Args().Get(0)
recipeName := c.Args().Get(1)
stackName := cmd.Args().Get(0)
recipeName := cmd.Args().Get(1)
err = tryUpgrade(cl, stackName, recipeName)
if err != nil {
log.Fatal(err)
@ -468,30 +466,26 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion stri
return err
}
func newAbraApp(version, commit string) *cli.App {
app := &cli.App{
Name: "kadabra",
Usage: `The Co-op Cloud auto-updater
____ ____ _ _
/ ___|___ ___ _ __ / ___| | ___ _ _ __| |
| | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |
| |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |
\____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|
|_|
`,
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Commands: []cli.Command{
Notify,
UpgradeApp,
func newAbraApp(version, commit string) *cli.Command {
app := &cli.Command{
Name: "kadabra",
Usage: "The Co-op Cloud auto-updater 🤖🚀",
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
UsageText: "kadabra [command] [options] [arguments]",
HideHelpCommand: true,
Flags: []cli.Flag{
internal.OfflineFlag,
internal.DebugFlag,
},
Commands: []*cli.Command{
&Notify,
&UpgradeApp,
},
}
app.Before = func(c *cli.Context) error {
log.Logger.SetStyles(log.Styles())
app.Before = func(ctx context.Context, cmd *cli.Command) error {
charmLog.SetDefault(log.Logger)
log.Debugf("kadabra version %s, commit %s", version, commit)
return nil
}
@ -502,7 +496,7 @@ func newAbraApp(version, commit string) *cli.App {
func RunApp(version, commit string) {
app := newAbraApp(version, commit)
if err := app.Run(os.Args); err != nil {
if err := app.Run(context.Background(), os.Args); err != nil {
log.Fatal(err)
}
}

13
go.mod
View File

@ -2,11 +2,12 @@ module coopcloud.tech/abra
go 1.21
replace github.com/urfave/cli/v3 => github.com/fiatjaf/cli/v3 v3.0.0-20240704165307-ad0e1925dd42
require (
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/charmbracelet/lipgloss v0.11.1
github.com/charmbracelet/log v0.4.0
github.com/distribution/reference v0.6.0
github.com/docker/cli v27.0.3+incompatible
@ -16,9 +17,10 @@ require (
github.com/google/go-cmp v0.6.0
github.com/moby/sys/signal v0.7.0
github.com/moby/term v0.5.0
github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.14.4
golang.org/x/term v0.22.0
github.com/urfave/cli/v3 v3.0.0-alpha9
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.1
)
@ -33,10 +35,10 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/x/ansi v0.1.3 // indirect
github.com/charmbracelet/lipgloss v0.11.0 // indirect
github.com/charmbracelet/x/ansi v0.1.2 // indirect
github.com/cloudflare/circl v1.3.9 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/cyphar/filepath-securejoin v0.2.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
@ -84,7 +86,6 @@ require (
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.2.2 // indirect
github.com/spf13/pflag v1.0.5 // indirect
@ -105,6 +106,7 @@ require (
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/term v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
@ -133,7 +135,6 @@ require (
github.com/spf13/cobra v1.8.1 // indirect
github.com/stretchr/testify v1.9.0
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/urfave/cli v1.22.15
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
golang.org/x/sys v0.22.0
)

24
go.sum
View File

@ -49,7 +49,6 @@ github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@ -135,12 +134,12 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/lipgloss v0.11.1 h1:a8KgVPHa7kOoP95vm2tQQrjD2AKhbWmfr4uJ2RW6kNk=
github.com/charmbracelet/lipgloss v0.11.1/go.mod h1:beLlcmkF7MWA+5UrKKIRo/VJ21xGXr7YJ9miWfdMRIU=
github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g=
github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/charmbracelet/x/ansi v0.1.3 h1:RBh/eleNWML5R524mjUF0yVRePTwqN9tPtV+DPgO5Lw=
github.com/charmbracelet/x/ansi v0.1.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY=
github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
@ -274,7 +273,6 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@ -357,6 +355,8 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fiatjaf/cli/v3 v3.0.0-20240704165307-ad0e1925dd42 h1:yId1d3b2PHJ9vnYCxojs9NslXYVQ1iv7svK/CuDRoU0=
github.com/fiatjaf/cli/v3 v3.0.0-20240704165307-ad0e1925dd42/go.mod h1:Z1ItyMma7t6I7zHG9OpbExhHQOSkFf/96n+mAZ9MtVI=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@ -621,6 +621,7 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
@ -686,6 +687,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -800,7 +803,6 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
@ -858,9 +860,6 @@ github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@ -868,9 +867,6 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
@ -890,8 +886,6 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM=
github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0=
github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI=
github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=

View File

@ -569,34 +569,21 @@ func ReadAbraShCmdNames(abraSh string) ([]string, error) {
return cmdNames, nil
}
func (a App) WriteRecipeVersion(version string, dryRun bool) error {
func (a App) WriteRecipeVersion(version string) error {
file, err := os.Open(a.Path)
if err != nil {
return err
}
defer file.Close()
skipped := false
scanner := bufio.NewScanner(file)
lines := []string{}
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "RECIPE=") && !strings.HasPrefix(line, "TYPE=") {
if !strings.Contains(line, "RECIPE=") && !strings.Contains(line, "TYPE") {
lines = append(lines, line)
continue
}
if strings.HasPrefix(line, "#") {
lines = append(lines, line)
continue
}
if strings.Contains(line, version) {
skipped = true
lines = append(lines, line)
continue
}
splitted := strings.Split(line, ":")
line = fmt.Sprintf("%s:%s", splitted[0], version)
lines = append(lines, line)
@ -606,19 +593,5 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error {
log.Fatal(err)
}
if !dryRun {
if err := os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm); err != nil {
log.Fatal(err)
}
} else {
log.Debugf("skipping writing version %s because dry run", version)
}
if !skipped {
log.Infof("version %s saved to %s.env", version, a.Domain)
} else {
log.Debugf("skipping version %s write as already exists in %s.env", version, a.Domain)
}
return nil
return os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm)
}

View File

@ -1,22 +1,23 @@
package autocomplete
import (
"context"
"fmt"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
)
// AppNameComplete copletes app names.
func AppNameComplete(c *cli.Context) {
func AppNameComplete(ctx context.Context, cmd *cli.Command) {
appNames, err := app.GetAppNames()
if err != nil {
log.Warn(err)
}
if c.NArg() > 0 {
if cmd.NArg() > 0 {
return
}
@ -36,13 +37,13 @@ func ServiceNameComplete(appName string) {
}
// RecipeNameComplete completes recipe names.
func RecipeNameComplete(c *cli.Context) {
func RecipeNameComplete(ctx context.Context, cmd *cli.Command) {
catl, err := recipe.ReadRecipeCatalogue(false)
if err != nil {
log.Warn(err)
}
if c.NArg() > 0 {
if cmd.NArg() > 0 {
return
}
@ -66,13 +67,13 @@ func RecipeVersionComplete(recipeName string) {
}
// ServerNameComplete completes server names.
func ServerNameComplete(c *cli.Context) {
func ServerNameComplete(ctx context.Context, cmd *cli.Command) {
files, err := app.LoadAppFiles("")
if err != nil {
log.Fatal(err)
}
if c.NArg() > 0 {
if cmd.NArg() > 0 {
return
}
@ -82,8 +83,8 @@ func ServerNameComplete(c *cli.Context) {
}
// SubcommandComplete completes sub-commands.
func SubcommandComplete(c *cli.Context) {
if c.NArg() > 0 {
func SubcommandComplete(ctx context.Context, cmd *cli.Command) {
if cmd.NArg() > 0 {
return
}

View File

@ -107,5 +107,4 @@ var (
REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json"
SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git"
CHAOS_DEFAULT = "false"
)

View File

@ -11,7 +11,6 @@ import (
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/recipe"
testPkg "coopcloud.tech/abra/pkg/test"
"github.com/stretchr/testify/assert"
)
func TestGetAllFoldersInDirectory(t *testing.T) {
@ -44,7 +43,13 @@ func TestReadEnv(t *testing.T) {
t.Fatal(err)
}
if !reflect.DeepEqual(env, testPkg.ExpectedAppEnv) {
t.Fatal("did not get expected application settings")
t.Fatalf(
"did not get expected application settings. Expected: DOMAIN=%s RECIPE=%s; Got: DOMAIN=%s RECIPE=%s",
testPkg.ExpectedAppEnv["DOMAIN"],
testPkg.ExpectedAppEnv["RECIPE"],
env["DOMAIN"],
env["RECIPE"],
)
}
}
@ -223,21 +228,3 @@ func TestEnvVarModifiersIncluded(t *testing.T) {
}
}
}
func TestNoOverwriteNonVersionEnvVars(t *testing.T) {
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
if err != nil {
t.Fatal(err)
}
if err := app.WriteRecipeVersion("1.3.12", true); err != nil {
t.Fatal(err)
}
app, err = appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
if err != nil {
t.Fatal(err)
}
assert.NotEqual(t, app.Env["SMTP_AUTHTYPE"], "login:1.3.12")
}

View File

@ -1,26 +1,18 @@
package formatter
import (
"bytes"
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/docker/go-units"
"golang.org/x/term"
// "github.com/olekukonko/tablewriter"
"coopcloud.tech/abra/pkg/jsontable"
"coopcloud.tech/abra/pkg/log"
"github.com/schollz/progressbar/v3"
)
var BoldStyle = lipgloss.NewStyle().
Bold(true).
Underline(true)
func ShortenID(str string) string {
return str[:12]
}
@ -42,67 +34,11 @@ func HumanDuration(timestamp int64) string {
}
// CreateTable prepares a table layout for output.
func CreateTable() (*table.Table, error) {
table := table.New().
Border(lipgloss.ThickBorder()).
BorderStyle(
lipgloss.NewStyle().
Foreground(lipgloss.Color("63")),
)
if isAbraCI, ok := os.LookupEnv("ABRA_CI"); ok && isAbraCI == "1" {
// NOTE(d1): no width limits for CI testing since we test against outputs
log.Debug("detected ABRA_CI=1")
return table, nil
}
width, _, err := term.GetSize(0)
if err != nil {
return nil, err
}
if width-10 < 79 {
// NOTE(d1): maintain standard minimum width
table.Width(79)
} else {
// NOTE(d1): tests show that this produces stable border drawing
table.Width(width - 10)
}
return table, nil
}
// ToJSON converts a lipgloss.Table to JSON representation. It's not a robust
// implementation and mainly caters for our current use case which is basically
// a bunch of strings. See https://github.com/charmbracelet/lipgloss/issues/335
// for the real thing (hopefully).
func ToJSON(headers []string, rows [][]string) (string, error) {
var buff bytes.Buffer
buff.Write([]byte("["))
for idx, row := range rows {
payload := make(map[string]string)
for idx, header := range headers {
payload[strings.ToLower(header)] = row[idx]
}
serialized, err := json.Marshal(payload)
if err != nil {
return "", err
}
buff.Write(serialized)
if idx < (len(rows) - 1) {
buff.Write([]byte(","))
}
}
buff.Write([]byte("]"))
return buff.String(), nil
func CreateTable(columns []string) *jsontable.JSONTable {
table := jsontable.NewJSONTable(os.Stdout)
table.SetAutoWrapText(false)
table.SetHeader(columns)
return table
}
// CreateProgressbar generates a progress bar

211
pkg/jsontable/jsontable.go Normal file
View File

@ -0,0 +1,211 @@
package jsontable
import (
"fmt"
"io"
"strings"
"github.com/olekukonko/tablewriter"
)
// A quick-and-dirty proxy/emulator of tablewriter to enable more easy machine readable output
// - Does not strictly support types, just quoted or unquoted values
// - Does not support nested values.
// If a datalabel is set with SetDataLabel(true, "..."), that will be used as the key for teh data of the table,
// otherwise if the caption is set with SetCaption(true, "..."), the data label will be set to the default of
// "rows", otherwise the table will output as a JSON list.
//
// Proxys all actions through to the tablewriter except addrow and addbatch, which it does at render time
//
type JSONTable struct {
out io.Writer
colsize int
rows [][]string
keys []string
quoted []bool // hack to do output typing, quoted vs. unquoted
hasDataLabel bool
dataLabel string
hasCaption bool
caption string // the actual caption
hasCaptionLabel bool
captionLabel string // the key in the dictionary for the caption
tbl *tablewriter.Table
}
func writeChar(w io.Writer, c byte) {
w.Write([]byte{c})
}
func NewJSONTable(writer io.Writer) *JSONTable {
t := &JSONTable{
out: writer,
colsize: 0,
rows: [][]string{},
keys: []string{},
quoted: []bool{},
hasDataLabel: false,
dataLabel: "rows",
hasCaption: false,
caption: "",
hasCaptionLabel: false,
captionLabel: "caption",
tbl: tablewriter.NewWriter(writer),
}
return t
}
func (t *JSONTable) NumLines() int {
// JSON only but reflects a shared state.
return len(t.rows)
}
func (t *JSONTable) SetHeader(keys []string) {
// Set the keys value which will assign each column to the keys.
// Note that we'll ignore values that are beyond the length of the keys list
t.colsize = len(keys)
t.keys = []string{}
for _, k := range keys {
t.keys = append(t.keys, k)
t.quoted = append(t.quoted, true)
}
t.tbl.SetHeader(keys)
}
func (t *JSONTable) SetColumnQuoting(quoting []bool) {
// Specify which columns are quoted or unquoted in output
// JSON only
for i := 0; i < t.colsize; i++ {
t.quoted[i] = quoting[i]
}
}
func (t *JSONTable) Append(row []string) {
// We'll just append whatever to the rows list. If they fix the keys after appending rows, it'll work as
// expected.
// We should detect if the row is narrower than the key list tho.
// JSON only (but we use the rows later when rendering a regular table)
t.rows = append(t.rows, row)
}
func (t *JSONTable) Render() {
// Load the table with rows and render.
// Proxy only
for _, row := range t.rows {
t.tbl.Append(row)
}
t.tbl.Render()
}
func (t *JSONTable) _JSONRenderInner() {
// JSON only
// Render the list of dictionaries to the writer.
//// inner render loop
writeChar(t.out, '[')
for rowidx, row := range t.rows {
if rowidx != 0 {
writeChar(t.out, ',')
}
writeChar(t.out, '{')
for keyidx, key := range t.keys {
key := strings.ToLower(key)
key = strings.ReplaceAll(key, " ", "-")
value := "nil"
if keyidx < len(row) {
value = row[keyidx]
}
if keyidx != 0 {
writeChar(t.out, ',')
}
if t.quoted[keyidx] {
fmt.Fprintf(t.out, "\"%s\":\"%s\"", key, value)
} else {
fmt.Fprintf(t.out, "\"%s\":%s", key, value)
}
}
writeChar(t.out, '}')
}
writeChar(t.out, ']')
}
func (t *JSONTable) JSONRender() {
// write JSON table to output
// JSON only
if t.hasDataLabel || t.hasCaption {
// dict mode
writeChar(t.out, '{')
if t.hasCaption {
fmt.Fprintf(t.out, "\"%s\":\"%s\",", t.captionLabel, t.caption)
}
fmt.Fprintf(t.out, "\"%s\":", t.dataLabel)
}
// write list
t._JSONRenderInner()
if t.hasDataLabel || t.hasCaption {
// dict mode
writeChar(t.out, '}')
}
}
func (t *JSONTable) SetCaption(caption bool, captionText ...string) {
t.hasCaption = caption
if len(captionText) == 1 {
t.caption = captionText[0]
}
t.tbl.SetCaption(caption, captionText...)
}
func (t *JSONTable) SetCaptionLabel(captionLabel bool, captionLabelText ...string) {
// JSON only
t.hasCaptionLabel = captionLabel
if len(captionLabelText) == 1 {
t.captionLabel = captionLabelText[0]
}
}
func (t *JSONTable) SetDataLabel(dataLabel bool, dataLabelText ...string) {
// JSON only
t.hasDataLabel = dataLabel
if len(dataLabelText) == 1 {
t.dataLabel = dataLabelText[0]
}
}
func (t *JSONTable) AppendBulk(rows [][]string) {
// JSON only but reflects shared state
for _, row := range rows {
t.Append(row)
}
}
// Stuff we should implement but we just proxy for now.
func (t *JSONTable) SetAutoMergeCellsByColumnIndex(cols []int) {
// FIXME
t.tbl.SetAutoMergeCellsByColumnIndex(cols)
}
// Stuff we should implement but we just proxy for now.
func (t *JSONTable) SetAlignment(align int) {
// FIXME
t.tbl.SetAlignment(align)
}
func (t *JSONTable) SetAutoMergeCells(auto bool) {
// FIXME
t.tbl.SetAutoMergeCells(auto)
}
// Stub functions
func (t *JSONTable) SetAutoWrapText(auto bool) {
t.tbl.SetAutoWrapText(auto)
return
}

View File

@ -0,0 +1,83 @@
package jsontable
import (
"testing"
"bytes"
"encoding/json"
"github.com/olekukonko/tablewriter"
)
var TestLine = []string{"1", "2"}
var TestGroup = [][]string{{"1", "2", "3"}, {"a", "teohunteohu", "c", "d"}, {"☺", "☹"}}
var TestKeys = []string{"key0", "key1", "key2"}
// test creation
func TestNewTable(t *testing.T) {
var b bytes.Buffer
tbl := NewJSONTable(&b)
if tbl.NumLines() != 0 {
t.Fatalf("Something went weird when making table (should have 0 lines)")
}
}
// test adding things
func TestTableAdd(t *testing.T) {
var b bytes.Buffer
tbl := NewJSONTable(&b)
tbl.Append(TestLine)
if tbl.NumLines() != 1 {
t.Fatalf("Appending a line does not result in a length of 1.")
}
tbl.AppendBulk(TestGroup)
numlines := tbl.NumLines()
if numlines != (len(TestGroup) + 1) {
t.Fatalf("Appending two lines does not result in a length of 4 (length is %d).", numlines)
}
}
// test JSON output is parsable
func TestJsonParsable(t *testing.T) {
var b bytes.Buffer
tbl := NewJSONTable(&b)
tbl.AppendBulk(TestGroup)
tbl.SetHeader(TestKeys)
tbl.JSONRender()
var son []map[string]interface{}
err := json.Unmarshal(b.Bytes(), &son)
if err != nil {
t.Fatalf("Did not produce parsable JSON: %s", err.Error())
}
}
// test identical commands to a tablewriter and jsontable produce the same rendered output
func TestTableWriter(t *testing.T) {
var bjson bytes.Buffer
var btable bytes.Buffer
tbl := NewJSONTable(&bjson)
tbl.AppendBulk(TestGroup)
tbl.SetHeader(TestKeys)
tbl.Render()
wtbl := tablewriter.NewWriter(&btable)
wtbl.AppendBulk(TestGroup)
wtbl.SetHeader(TestKeys)
wtbl.Render()
if bytes.Compare(bjson.Bytes(), btable.Bytes()) != 0 {
t.Fatalf("JSON table and TableWriter produce non-identical outputs.\n%s\n%s", bjson.Bytes(), btable.Bytes())
}
}
/// FIXME test different output formats when captions etc. are added

View File

@ -3,9 +3,7 @@ package log
import (
"os"
"strings"
"github.com/charmbracelet/lipgloss"
charmLog "github.com/charmbracelet/log"
)
@ -34,42 +32,3 @@ var SetLevel = Logger.SetLevel
var DebugLevel = charmLog.DebugLevel
var SetOutput = charmLog.SetOutput
var SetReportCaller = charmLog.SetReportCaller
func Styles() *charmLog.Styles {
styles := charmLog.DefaultStyles()
styles.Levels = map[charmLog.Level]lipgloss.Style{
charmLog.DebugLevel: lipgloss.NewStyle().
SetString(strings.ToUpper(DebugLevel.String())).
Bold(true).
Padding(0, 1, 0, 1).
Background(lipgloss.Color("63")).
Foreground(lipgloss.Color("15")),
charmLog.InfoLevel: lipgloss.NewStyle().
SetString(strings.ToUpper(charmLog.InfoLevel.String())).
Bold(true).
Padding(0, 1, 0, 1).
Background(lipgloss.Color("86")).
Foreground(lipgloss.Color("16")),
charmLog.WarnLevel: lipgloss.NewStyle().
SetString(strings.ToUpper(charmLog.WarnLevel.String())).
Bold(true).
Padding(0, 1, 0, 1).
Background(lipgloss.Color("192")).
Foreground(lipgloss.Color("16")),
charmLog.ErrorLevel: lipgloss.NewStyle().
SetString(strings.ToUpper(charmLog.ErrorLevel.String())).
Bold(true).
Padding(0, 1, 0, 1).
Background(lipgloss.Color("204")).
Foreground(lipgloss.Color("15")),
charmLog.FatalLevel: lipgloss.NewStyle().
SetString(strings.ToUpper(charmLog.FatalLevel.String())).
Bold(true).
Padding(0, 1, 0, 1).
Background(lipgloss.Color("134")).
Foreground(lipgloss.Color("15")),
}
return styles
}

View File

@ -26,26 +26,20 @@ func (r Recipe) Ensure(chaos bool, offline bool) error {
if err := r.EnsureIsClean(); err != nil {
return err
}
if !offline {
if err := r.EnsureUpToDate(); err != nil {
log.Fatal(err)
}
}
if r.Version != "" {
log.Debugf("ensuring version %s", r.Version)
if _, err := r.EnsureVersion(r.Version); err != nil {
return err
}
return nil
} else {
if err := r.EnsureLatest(); err != nil {
return err
}
}
if err := r.EnsureLatest(); err != nil {
return err
}
return nil
}

View File

@ -127,9 +127,6 @@ func Get(name string) Recipe {
version := ""
if strings.Contains(name, ":") {
split := strings.Split(name, ":")
if len(split) > 2 {
log.Fatalf("version seems invalid: %s", name)
}
name = split[0]
version = split[1]
}

View File

@ -181,7 +181,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
if err := client.StoreSecret(cl, secret.RemoteName, passwords[0], server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") {
log.Warnf("%s already exists", secret.RemoteName)
log.Warnf("%s already exists, moving on...", secret.RemoteName)
ch <- nil
} else {
ch <- err
@ -201,7 +201,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
if err := client.StoreSecret(cl, secret.RemoteName, passphrases[0], server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") {
log.Warnf("%s already exists", secret.RemoteName)
log.Warnf("%s already exists, moving on...", secret.RemoteName)
ch <- nil
} else {
ch <- err

View File

@ -27,9 +27,8 @@ var (
)
var ExpectedAppEnv = envfile.AppEnv{
"DOMAIN": "ecloud.evil.corp",
"RECIPE": "ecloud",
"SMTP_AUTHTYPE": "login",
"DOMAIN": "ecloud.evil.corp",
"RECIPE": "ecloud",
}
var ExpectedApp = appPkg.App{

View File

@ -175,7 +175,6 @@ func pruneServices(ctx context.Context, cl *dockerClient.Client, namespace conve
pruneServices = append(pruneServices, service)
}
}
removeServices(ctx, cl, pruneServices)
}
@ -256,12 +255,10 @@ func deployCompose(ctx context.Context, cl *dockerClient.Client, opts Deploy, co
log.Infof("waiting for %s to deploy... please hold 🤚", appName)
if err := waitOnServices(ctx, cl, serviceIDs, appName); err != nil {
return err
if err := waitOnServices(ctx, cl, serviceIDs, appName); err == nil {
log.Infof("successfully deployed %s", appName)
}
log.Infof("successfully deployed %s", appName)
return nil
}
@ -398,7 +395,7 @@ func deployServices(
)
if service, exists := existingServiceMap[name]; exists {
log.Infof("updating %s", name)
log.Infof("updating service %s (id: %s)", name, service.ID)
updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}
@ -433,7 +430,7 @@ func deployServices(
response, err := cl.ServiceUpdate(ctx, service.ID, service.Version, serviceSpec, updateOpts)
if err != nil {
return nil, errors.Wrapf(err, "failed to update %s", name)
return nil, errors.Wrapf(err, "failed to update service %s", name)
}
for _, warning := range response.Warnings {
@ -442,7 +439,7 @@ func deployServices(
serviceIDs = append(serviceIDs, service.ID)
} else {
log.Infof("creating %s", name)
log.Infof("creating service %s", name)
createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth}
@ -453,7 +450,7 @@ func deployServices(
serviceCreateResponse, err := cl.ServiceCreate(ctx, serviceSpec, createOpts)
if err != nil {
return nil, errors.Wrapf(err, "failed to create %s", name)
return nil, errors.Wrapf(err, "failed to create service %s", name)
}
serviceIDs = append(serviceIDs, serviceCreateResponse.ID)
@ -513,14 +510,13 @@ func WaitOnService(ctx context.Context, cl *dockerClient.Client, serviceID, appN
case err := <-errChan:
return err
case <-sigintChannel:
return fmt.Errorf(`
return fmt.Errorf(fmt.Sprintf(`
Not waiting for %s to deploy. The deployment is ongoing...
If you want to stop the deployment, try:
abra app undeploy %s`, appName, appName)
abra app undeploy %s`, appName, appName))
case <-time.After(timeout):
return fmt.Errorf(`
return fmt.Errorf(fmt.Sprintf(`
%s has not converged (%s second timeout reached).
This does not necessarily mean your deployment has failed, it may just be that
@ -534,7 +530,7 @@ You can track latest deployment status with:
And inspect the logs with:
abra app logs %s
`, appName, timeout, appName, appName)
`, appName, timeout, appName, appName))
}
}
@ -552,7 +548,7 @@ func GetStacks(cl *dockerClient.Client) ([]*formatter.Stack, error) {
labels := service.Spec.Labels
name, ok := labels[convert.LabelNamespace]
if !ok {
return nil, errors.Errorf("cannot get label %s for %s",
return nil, errors.Errorf("cannot get label %s for service %s",
convert.LabelNamespace, service.ID)
}
ztack, ok := m[name]

View File

@ -8,9 +8,9 @@ _cli_bash_autocomplete() {
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == "-"* ]]; then
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-shell-completion )
else
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-shell-completion )
fi
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0

View File

@ -1,5 +1,5 @@
function complete_abra_args
set -l cmd (commandline -poc) --generate-bash-completion
set -l cmd (commandline -poc) --generate-shell-completion
$cmd
end
complete -c abra -f -n "not __fish_seen_subcommand_from -h --help -v --version complete_abra_args" -a "(complete_abra_args)"

View File

@ -2,7 +2,7 @@ $fn = $($MyInvocation.MyCommand.Name)
$name = $fn -replace "(.*)\.ps1$", '$1'
Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock {
param($commandName, $wordToComplete, $cursorPosition)
$other = "$wordToComplete --generate-bash-completion"
$other = "$wordToComplete --generate-shell-completion"
Invoke-Expression $other | ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
}

View File

@ -6,9 +6,9 @@ _cli_zsh_autocomplete() {
local cur
cur=${words[-1]}
if [[ "$cur" == "-"* ]]; then
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}")
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-shell-completion)}")
else
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}")
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-shell-completion)}")
fi
if [[ "${opts[1]}" != "" ]]; then

View File

@ -87,7 +87,7 @@ function install_abra_release {
x=$(echo $PATH | grep $HOME/.local/bin)
if [ $? -ne 0 ]; then
echo "$(tput setaf 3)WARNING: $HOME/.local/bin/ is not in \$PATH! If you want to run abra by just typing "abra" you should add it to your \$PATH! To do that run this once and restart your terminal:$(tput sgr0)"
echo "$(tput setaf 3)WARNING: $HOME/.local/bin/ is not in \$PATH! If you want to run abra by just typing "abra" you should add it to your \$PATH! To do that run:$(tput sgr0)"
p=$HOME/.local/bin
com="echo PATH=\$PATH:$p"
if [[ $SHELL =~ "bash" ]]; then

View File

@ -7,9 +7,10 @@
# destroys resources on the swarm server you run it against. This is for
# setup/teardown for the integration test suite.
#
# export DRONE_SOURCE_BRANCH=<your-branch-name>
# ./run-ci-int
set -eu
set +e
echo "========================================================================"
echo "WIPING DOCKER RESOURCES FOR A CLEAN SLATE"
@ -44,7 +45,17 @@ echo "========================================================================"
rm -rf abra
git clone ssh://git@git.coopcloud.tech:2222/coop-cloud/abra.git
cd abra
git checkout main
echo "========================================================================"
echo "========================================================================"
echo "FETCHING ABRA BRANCH FOR TESTING"
echo "========================================================================"
if [ -z "$DRONE_SOURCE_BRANCH" ]; then
DRONE_SOURCE_BRANCH="main"
fi
git fetch --all
git checkout $DRONE_SOURCE_BRANCH
echo "========================================================================"
echo "========================================================================"
@ -72,7 +83,6 @@ echo "========================================================================"
export ABRA_DIR="$HOME/.abra_test"
export TERM=xterm
export TEST_SERVER=default
export ABRA_CI=1
rm -rf "$ABRA_DIR"
bats -Tp tests/integration --filter-tags \!dns --print-output-on-failure

View File

@ -20,7 +20,6 @@ setup(){
teardown(){
_reset_recipe
_reset_tags
}
@test "validate app argument" {
@ -83,17 +82,19 @@ teardown(){
}
@test "ensure recipe not up to date if --offline" {
_ensure_env_version "0.1.0+1.20.0"
latestRelease=$(_latest_release)
wantHash=$(_get_n_hash 1)
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -d "$latestRelease"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~1
assert_success
# NOTE(d1): don't assert success because it might flake
assert_equal $(_get_current_hash) "$wantHash"
# NOTE(d1): we can't quite tell if this will fail or not in the future, so,
# since it isn't an important part of what we're testing here, we don't check
# it
run $ABRA app check "$TEST_APP_DOMAIN" --offline
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l
refute_output --partial "$latestRelease"
assert_equal $(_get_current_hash) "$wantHash"
}
@test "error if missing .env.sample" {
@ -117,20 +118,3 @@ teardown(){
assert_success
assert_output --partial '❌'
}
# bats test_tags=slow
@test "respects env version" {
tagHash=$(_get_tag_hash "0.1.0+1.20.0")
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
assert_success
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app check "$TEST_APP_DOMAIN"
assert_success
assert_equal $(_get_current_hash) "$tagHash"
}

View File

@ -19,9 +19,8 @@ setup(){
}
teardown(){
_reset_recipe
_reset_tags
_undeploy_app
_reset_recipe
}
# bats test_tags=slow
@ -106,18 +105,20 @@ test_cmd_export"
}
@test "ensure recipe not up to date if --offline" {
_ensure_env_version "0.1.0+1.20.0"
latestRelease=$(_latest_release)
wantHash=$(_get_n_hash 3)
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -d "$latestRelease"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
assert_equal $(_get_current_hash) "$wantHash"
run $ABRA app cmd --local --offline "$TEST_APP_DOMAIN" test_cmd
assert_success
assert_output --partial 'baz'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l
refute_output --partial "$latestRelease"
assert_equal $(_get_current_hash) $wantHash
_reset_recipe "$TEST_RECIPE"
}
@test "error if missing arguments without passing --local" {
@ -186,24 +187,6 @@ test_cmd_export"
assert_output --partial 'baz'
}
# bats test_tags=slow
@test "respects env version" {
tagHash=$(_get_tag_hash "0.1.0+1.20.0")
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input
assert_success
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app cmd "$TEST_APP_DOMAIN" app test_cmd
assert_success
assert_output --partial 'baz'
assert_equal $(_get_current_hash) "$tagHash"
}
# bats test_tags=slow
@test "error if missing service" {
_deploy_app

View File

@ -16,13 +16,12 @@ teardown_file(){
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_ensure_catalogue
}
teardown(){
_undeploy_app
_reset_recipe
_reset_app
_undeploy_app
_reset_tags
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
@ -87,18 +86,19 @@ teardown(){
# bats test_tags=slow
@test "ensure recipe not up to date if --offline" {
_ensure_env_version "0.1.0+1.20.0"
latestRelease=$(_latest_release)
wantHash=$(_get_n_hash 3)
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -d "$latestRelease"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
assert_equal $(_get_current_hash) "$wantHash"
# NOTE(d1): need to use --chaos to force same commit
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --offline
--no-input --no-converge-checks --chaos --offline
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l
refute_output --partial "$latestRelease"
assert_equal $(_get_current_hash) "$wantHash"
}
# bats test_tags=slow
@ -106,7 +106,6 @@ teardown(){
latestCommit="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)"
_remove_tags
_wipe_env_version
# NOTE(d1): need to pass --offline to stop tags being pulled again
run $ABRA app deploy "$TEST_APP_DOMAIN" \
@ -126,8 +125,6 @@ teardown(){
assert_equal $(_get_current_hash) "$wantHash"
_wipe_env_version
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
@ -174,12 +171,14 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --force
assert_success
assert_output --partial 'already deployed'
assert_output --partial 'already deployed but continuing'
assert_output --partial '--force'
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
assert_output --partial 'already deployed'
assert_output --partial 'already deployed but continuing'
assert_output --partial '--chaos'
}
# bats test_tags=slow
@ -352,6 +351,39 @@ teardown(){
_undeploy_app
run $ABRA app secret rm "$TEST_APP_DOMAIN" --all
# TODO(d1): use of `--chaos` is a hack while the following is not fixed
# https://git.coopcloud.tech/coop-cloud/organising/issues/620
run $ABRA app secret rm "$TEST_APP_DOMAIN" --all --chaos
assert_success
}
# bats test_tags=slow
@test "deploy chaos commit" {
tagHash=$(_get_tag_hash "0.1.0+1.20.0")
run $ABRA app deploy "$TEST_APP_DOMAIN" "$tagHash" --no-input --no-converge-checks
assert_success
assert_output --partial 'chaos mode'
}
# bats test_tags=slow
@test "deploy remote recipe" {
run sed -i 's/TYPE=abra-test-recipe/RECIPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial "git.coopcloud.tech/coop-cloud/abra-test-recipe"
}
# bats test_tags=slow
@test "deploy remote recipe with version" {
run sed -i 's/TYPE=abra-test-recipe/RECIPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe:0.2.0+1.21.0/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial '0.2.0+1.21.0'
}

View File

@ -1,99 +0,0 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_ensure_catalogue
}
teardown(){
_reset_recipe
_undeploy_app
_reset_app
}
# bats test_tags=slow
@test "deploy version written to env" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
assert_success
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
# bats test_tags=slow
@test "redeploy overwrites env version" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
assert_success
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks --force
assert_success
run grep -q "TYPE=$TEST_RECIPE:0.2.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
# bats test_tags=slow
@test "chaos commit written to env" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "1e83340e" --no-input --no-converge-checks
assert_success
run grep -q "TYPE=$TEST_RECIPE:1e83340e" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
# bats test_tags=slow
@test "redeploy reads from env version" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
assert_success
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
_undeploy_app
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial '0.1.0+1.20.0'
}
# bats test_tags=slow
@test "specific version overrides env version" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
assert_success
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks --force --debug
assert_success
assert_output --partial "overriding env file version"
run grep -q "TYPE=$TEST_RECIPE:0.2.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}

View File

@ -1,73 +0,0 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_ensure_catalogue
}
teardown(){
_reset_recipe
_undeploy_app
_reset_app
}
# bats test_tags=slow
@test "deploy remote recipe" {
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial "git.coopcloud.tech/coop-cloud/abra-test-recipe"
}
# bats test_tags=slow
@test "deploy remote recipe with version" {
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe:0.2.0+1.21.0/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial '0.2.0+1.21.0'
}
# bats test_tags=slow
@test "deploy remote recipe with chaos commit" {
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe:1e83340e/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial '1e83340e'
}
# bats test_tags=slow
@test "remote recipe version written to env" {
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
run grep -q "TYPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe:$(_latest_release)" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}

View File

@ -1,44 +0,0 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_ensure_catalogue
}
teardown(){
_reset_app
}
@test "badly formatted env version bails out" {
run sed -i 's/TYPE=abra-test-recipe/TYPE=abra-test-recipe:0.2.0+1.21.0/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_failure
assert_output --partial 'seems invalid'
}
@test "invalid env version bails out" {
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=abra-test-recipe:DOESNTEXIST/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_failure
assert_output --partial 'not found'
}

View File

@ -62,8 +62,8 @@ teardown(){
run $ABRA app ls --server foo.com
assert_success
refute_output --partial "SERVER: $TEST_SERVER"
assert_output --partial "SERVER: foo.com"
refute_output --partial "server: $TEST_SERVER |"
assert_output --partial "server: foo.com |"
run rm -rf "$ABRA_DIR/servers/foo.com"
assert_success
@ -97,8 +97,8 @@ teardown(){
@test "server stats are correct" {
run $ABRA app ls
assert_success
assert_output --partial "SERVER: $TEST_SERVER"
assert_output --partial "TOTAL APPS: 1"
assert_output --partial "server: $TEST_SERVER"
assert_output --partial "total apps: 1"
run mkdir -p "$ABRA_DIR/servers/foo.com"
assert_success
@ -113,8 +113,8 @@ teardown(){
assert_success
assert_output --partial "$TEST_SERVER"
assert_output --partial "foo.com"
assert_output --partial "TOTAL SERVERS: 2"
assert_output --partial "TOTAL APPS: 2"
assert_output --partial "total servers: 2"
assert_output --partial "total apps: 2"
run rm -rf "$ABRA_DIR/servers/foo.com"
assert_success

View File

@ -39,41 +39,17 @@ teardown(){
_get_head_hash
_get_current_hash
assert_equal "$headHash" "$currentHash"
run grep -q "TYPE=$TEST_RECIPE:$(_latest_release)" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "create new app with version" {
run $ABRA app new "$TEST_RECIPE" 0.3.0+1.21.0 \
run $ABRA app new "$TEST_RECIPE" 0.1.1+1.20.2 \
--no-input \
--server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN"
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_equal $(_get_tag_hash 0.3.0+1.21.0) $(_get_current_hash)
run grep -q "TYPE=$TEST_RECIPE:0.3.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "create new app with chaos commit" {
run $ABRA app new "$TEST_RECIPE" 1e83340e \
--no-input \
--server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN"
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
currentHash=$(_get_current_hash)
assert_equal 1e83340e ${currentHash:0:8}
run grep -q "TYPE=$TEST_RECIPE:1e83340e" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
assert_equal $(_get_tag_hash 0.1.1+1.20.2) $(_get_current_hash)
}
@test "does not overwrite existing env files" {

View File

@ -31,84 +31,6 @@ teardown(){
assert_output --partial 'cannot find app'
}
# bats test_tags=slow
@test "retrieve recipe if missing" {
_deploy_app
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE"
assert_success
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
run $ABRA app ps "$TEST_APP_DOMAIN"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
}
# bats test_tags=slow
@test "bail if unstaged changes and no --chaos" {
_deploy_app
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app ps "$TEST_APP_DOMAIN"
assert_failure
assert_output --partial 'locally unstaged changes'
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
# bats test_tags=slow
@test "do not bail if unstaged changes and --chaos" {
_deploy_app
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app ps --chaos "$TEST_APP_DOMAIN"
assert_success
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
# bats test_tags=slow
@test "ensure recipe up to date if no --offline" {
_deploy_app
wantHash=$(_get_n_hash 3)
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
assert_equal $(_get_current_hash) "$wantHash"
run $ABRA app ps "$TEST_APP_DOMAIN"
assert_success
assert_equal $(_get_head_hash) $(_get_current_hash)
}
@test "ensure recipe not up to date if --offline" {
_deploy_app
_ensure_env_version "0.1.0+1.20.0"
latestRelease=$(_latest_release)
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -d "$latestRelease"
assert_success
run $ABRA app ps --offline "$TEST_APP_DOMAIN"
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l
refute_output --partial "$latestRelease"
}
@test "error if not deployed" {
run $ABRA app ps "$TEST_APP_DOMAIN"
assert_failure
@ -119,11 +41,13 @@ teardown(){
@test "show ps report" {
_deploy_app
latestRelease=$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | tail -n 1)
run $ABRA app ps "$TEST_APP_DOMAIN"
assert_success
assert_output --partial 'app'
assert_output --partial 'healthy'
assert_output --partial $(_latest_release)
assert_output --partial "$latestRelease"
assert_output --partial 'false' # not a chaos deploy
}

View File

@ -58,18 +58,18 @@ teardown(){
}
@test "ensure recipe not up to date if --offline" {
_ensure_env_version "0.1.0+1.20.0"
latestRelease=$(_latest_release)
wantHash=$(_get_n_hash 3)
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -d "$latestRelease"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
assert_equal $(_get_current_hash) "$wantHash"
run $ABRA app rollback "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --offline
assert_failure
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l
refute_output --partial "$latestRelease"
assert_equal $(_get_current_hash) $wantHash
}
@test "error if not already deployed" {

View File

@ -1,44 +0,0 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
teardown(){
_undeploy_app
_reset_recipe
}
@test "rollback writes version to env file" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input --no-converge-checks
assert_success
assert_output --partial "0.2.0+1.21.0"
run grep -q "TYPE=abra-test-recipe:0.2.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
--no-input --no-converge-checks --debug
assert_success
assert_output --partial "0.1.0+1.20.0"
assert_output --partial "overriding env file version"
run grep -q "TYPE=abra-test-recipe:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}

View File

@ -217,8 +217,7 @@ teardown(){
run bash -c "echo bar >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app secret insert \
--chaos \
--file "$TEST_APP_DOMAIN" test_pass_one v1 "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
--file "$TEST_APP_DOMAIN" test_pass_one v1 "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_output --partial 'successfully stored on server'
@ -318,10 +317,9 @@ teardown(){
run $ABRA app secret generate "$TEST_APP_DOMAIN" --all
assert_success
run bash -c '$ABRA app secret ls "$TEST_APP_DOMAIN" --machine \
| jq -r ".[] | select(.name==\"test_pass_two\") | .version"'
run $ABRA app secret ls "$TEST_APP_DOMAIN" --machine
assert_success
assert_output --partial 'v1'
assert_output --partial '"created-on-server":"true"'
}
@test "ls: bail if unstaged changes and no --chaos" {

View File

@ -1,93 +0,0 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
# NOTE(d1): create new app without secrets
run $ABRA app new "$TEST_RECIPE" \
--no-input \
--server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN"
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
teardown(){
_reset_recipe
_reset_app
run $ABRA app secret rm "$TEST_APP_DOMAIN" --all --no-input
}
@test "generate: respect env version" {
tagHash=$(_get_tag_hash "0.2.0+1.21.0")
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=abra-test-recipe:0.2.0+1.21.0/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app secret generate "$TEST_APP_DOMAIN" --all
assert_success
assert_equal $(_get_current_hash) "$tagHash"
}
@test "insert: respect env version" {
tagHash=$(_get_tag_hash "0.2.0+1.21.0")
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=abra-test-recipe:0.2.0+1.21.0/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app secret insert "$TEST_APP_DOMAIN" test_pass_one v1 foo
assert_success
assert_output --partial 'successfully stored on server'
assert_equal $(_get_current_hash) "$tagHash"
}
@test "rm: respect env version" {
tagHash=$(_get_tag_hash "0.2.0+1.21.0")
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=abra-test-recipe:0.2.0+1.21.0/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app secret generate "$TEST_APP_DOMAIN" --all
assert_success
run $ABRA app secret rm "$TEST_APP_DOMAIN" --all
assert_success
assert_equal $(_get_current_hash) "$tagHash"
}
@test "ls: respect env version" {
tagHash=$(_get_tag_hash "0.2.0+1.21.0")
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=abra-test-recipe:0.2.0+1.21.0/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app secret generate "$TEST_APP_DOMAIN" --all
assert_success
run $ABRA app secret ls "$TEST_APP_DOMAIN"
assert_success
assert_output --partial 'true'
assert_equal $(_get_current_hash) "$tagHash"
}

View File

@ -18,7 +18,6 @@ setup(){
}
teardown(){
_reset_recipe
_undeploy_app
}
@ -32,61 +31,12 @@ teardown(){
assert_output --partial 'cannot find app'
}
# bats test_tags=slow
@test "ensure recipe up to date if no --offline" {
_deploy_app
wantHash=$(_get_n_hash 3)
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
assert_equal $(_get_current_hash) "$wantHash"
run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
assert_success
assert_equal $(_get_head_hash) $(_get_current_hash)
}
# bats test_tags=slow
@test "ensure recipe not up to date if --offline" {
_deploy_app
_ensure_env_version "0.1.0+1.20.0"
latestRelease=$(_latest_release)
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -d "$latestRelease"
assert_success
run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input --offline
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l
refute_output --partial "$latestRelease"
}
@test "error if not deployed" {
run $ABRA app undeploy "$TEST_APP_DOMAIN"
assert_failure
assert_output --partial 'is not deployed'
}
# bats test_tags=slow
@test "do not bail if unstaged changes (only query runtime)" {
_deploy_app
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
assert_success
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
# bats test_tags=slow
@test "undeploy app" {
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input

View File

@ -18,9 +18,8 @@ setup(){
}
teardown(){
_reset_recipe
_reset_app
_undeploy_app
_reset_recipe
}
@test "validate app argument" {
@ -124,9 +123,11 @@ teardown(){
assert_success
assert_output --partial '0.1.0+1.20.0'
latestRelease=$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | tail -n 1)
run $ABRA app upgrade "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial "$(_latest_release)"
assert_output --partial "$latestRelease"
}
# bats test_tags=slow
@ -135,9 +136,11 @@ teardown(){
assert_success
assert_output --partial '0.1.1+1.20.2'
latestRelease=$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | tail -n 1)
run $ABRA app upgrade "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial "$(_latest_release)"
assert_output --partial "$latestRelease"
assert_output --partial 'release notes baz' # 0.2.0+1.21.0
refute_output --partial 'release notes bar' # 0.1.1+1.20.2
}
@ -161,9 +164,11 @@ teardown(){
assert_success
assert_output --partial '0.1.0+1.20.0'
latestRelease=$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | tail -n 1)
run $ABRA app upgrade "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial "$(_latest_release)"
assert_output --partial "$latestRelease"
assert_output --partial 'release notes bar' # 0.1.1+1.20.2
assert_output --partial 'release notes baz' # 0.2.0+1.21.0
}

View File

@ -1,43 +0,0 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
teardown(){
_undeploy_app
_reset_recipe
}
@test "upgrade writes version to env file" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
assert_success
assert_output --partial '0.1.0+1.20.0'
run grep -q "TYPE=abra-test-recipe:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks --debug
assert_success
assert_output --partial "0.2.0+1.21.0"
assert_output --partial "overriding env file version"
run grep -q "TYPE=abra-test-recipe:0.2.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}

View File

@ -1,9 +1,5 @@
#!/usr/bin/env bash
_latest_release(){
echo $(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | tail -n 1)
}
_fetch_recipe() {
if [[ ! -d "$ABRA_DIR/recipes/$TEST_RECIPE" ]]; then
run mkdir -p "$ABRA_DIR/recipes"
@ -23,29 +19,10 @@ _reset_recipe(){
}
_ensure_latest_version(){
latestRelease=$(_latest_release)
latestRelease=$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | tail -n 1)
if [ ! $latestRelease = "$1" ]; then
echo "expected latest recipe version of '$1', saw: $latestRelease"
return 1
fi
}
_ensure_catalogue(){
if [[ ! -d "$ABRA_DIR/catalogue" ]]; then
run git clone https://git.coopcloud.tech/coop-cloud/recipes-catalogue-json.git $ABRA_DIR/catalogue
assert_success
fi
}
_ensure_env_version(){
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=abra-test-recipe:$1/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
_wipe_env_version(){
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=abra-test-recipe/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}

View File

@ -1,17 +1,10 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_ensure_catalogue
}
setup() {
load "$PWD/tests/integration/helpers/common"
_common_setup
}
@test "recipe versions" {
run $ABRA recipe versions gitea
assert_success
@ -19,9 +12,11 @@ setup() {
}
@test "local tags used if no catalogue entry" {
latestRelease=$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | tail -n 1)
run $ABRA recipe versions "$TEST_RECIPE"
assert_success
assert_output --partial "$(_latest_release)"
assert_output --partial "$latestRelease"
}
@test "versions listed in correct order" {

View File

@ -13,7 +13,6 @@ teardown_file(){
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
}
teardown(){
@ -62,13 +61,12 @@ teardown(){
}
@test "machine readable output" {
if [ ! "$TEST_SERVER" = "default" ]; then
skip "can only diff output against 'default' server (local)"
fi
output=$("$ABRA" server ls --machine)
run diff \
<(jq -S "." <(echo "$output")) \
<(jq -S "." <(echo '[{"host":"local","name":"default"}]'))
run "$ABRA" server ls --machine
assert_success
expectedOutput='[{"name":"'
expectedOutput+="$TEST_SERVER"
expectedOutput+='"'
assert_output --partial "$expectedOutput"
}

View File

@ -1,3 +1,2 @@
RECIPE=ecloud
DOMAIN=ecloud.evil.corp
SMTP_AUTHTYPE=login

View File

@ -1,18 +0,0 @@
---
kind: pipeline
name: coopcloud.tech/tagcmp
steps:
- name: gofmt
image: golang:1.21
commands:
- test -z "$(gofmt -l .)"
- name: go build
image: golang:1.21
commands:
- go build -v .
- name: go test
image: golang:1.21
commands:
- go test . -cover

View File

@ -1 +0,0 @@
fmtcoverage.html

View File

@ -1,15 +0,0 @@
Tagcmp: comparison operations for image tags.
Copyright (C) 2023 Co-op Cloud <helo@coopcloud.tech>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

View File

@ -1,161 +0,0 @@
# tagcmp
[![Build Status](https://build.coopcloud.tech/api/badges/coop-cloud/tagcmp/status.svg?ref=refs/heads/main)](https://build.coopcloud.tech/coop-cloud/tagcmp)
[![Go Report Card](https://goreportcard.com/badge/git.coopcloud.tech/coop-cloud/tagcmp)](https://goreportcard.com/report/git.coopcloud.tech/coop-cloud/tagcmp)
[![Go Reference](https://pkg.go.dev/badge/coopcloud.tech/tagcmp.svg)](https://pkg.go.dev/coopcloud.tech/tagcmp)
Comparison operations for image tags. Because registries aren't doing this for
us 🙄
This library is helpful if you're aiming to use only "stable" and "semver-like"
tags and want to be able to do things like compare them, find which tags are
more recent, sort them and other types of comparisons. This is a best-effort
implementation which follows the wisdom of [Renovate].
> Docker doesn't really have versioning, instead it supports "tags" and these
> are usually used by Docker image authors as a form of versioning ... It's
> pretty "wild west" for tagging and not always compliant with SemVer.
The Renovate implementation allows image tags to be automatically upgraded, is
the only show in town, apparently. This library follows that implementation
quite closely.
[renovate]: https://docs.renovatebot.com/docker/
## Example
```golang
package main
import (
"fmt"
"sort"
"coopcloud.tech/tagcmp"
)
func main() {
rawTags := []string{
"1.7.1",
"1.9.4-linux-arm64",
"1.14.2-rootless",
"linux-arm64-rootless",
"1.14.1-rootless",
"1.12.4-linux-amd64",
"1.14.0-rootless",
}
tag, err := tagcmp.Parse("1.14.0-rootless")
if err != nil {
panic(err)
}
var compatible []tagcmp.Tag
for _, rawTag := range rawTags {
parsed, _ := tagcmp.Parse(rawTag) // skips unsupported tags
if tag.IsCompatible(parsed) {
compatible = append(compatible, parsed)
}
}
sort.Sort(tagcmp.ByTagAsc(compatible))
fmt.Println(compatible)
}
```
Output:
```golang
[1.14.0-rootless 1.14.1-rootless 1.14.2-rootless]
```
## Types of versions supported
```golang
// semver
"5",
"2.6",
"4.3.5",
// semver with 'v'
"v1",
"v2.3",
"v1.0.2",
// semver with suffix
"6-alpine",
"6.2-alpine",
"6.2.1-alpine",
// semver with sufix and 'v'
"v6-alpine",
"v6.2-alpine",
"v6.2.1-alpine",
"v6.2.1-alpine",
// semver with multiple suffix values
"6.2.1-alpine-foo",
// semver with multiple suffix values and 'v'
"v6.2.1-alpine-foo",
```
## Types of versions not supported
> Please note, we could support some of these versions if people really need
> them to be supported. Some tags are using a unique format which we could
> support by implementing a very specific parser for (e.g. `ParseMinioTag`,
> `ParseZncTag`). For now, this library tries to provide a `Parse` function
> which handles more general cases. Please open an issue, change sets are
> welcome.
```golang
// empty
"",
// patametrized
"${MAILU_VERSION:-master}",
"${PHP_VERSION}-fpm-alpine3.13",
// commit hash like
"0a1b2c3d4e5f6a7b8c9d0a1b2c3d4e5f6a7b8c9d",
// numeric
"20191109",
"e02267d",
// not semver
"3.0.6.0",
"r1295",
"version-r1070",
// prerelease
"3.7.0b1",
"3.8.0b1-alpine",
// multiple versions
"5.36-backdrop-php7.4",
"v1.0.5_3.4.0",
"v1.0.5_3.4.0_openid-sso",
// tz based
"RELEASE.2021-04-22T15-44-28Z",
// only text
"alpine",
"latest",
"master",
// multiple - delimters
"apache-debian-1.8-prod",
"version-znc-1.8.2",
```
## License
[GPLv3+](./LICENSE)
## Who's using it?
- [`abra`](https://git.coopcloud.tech/coop-cloud/abra)

View File

@ -1,3 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

View File

@ -1,452 +0,0 @@
// Package tagcmp provides image tag comparison operations.
package tagcmp
import (
"fmt"
"regexp"
"strconv"
"strings"
)
type Tag struct {
Major string `json:",omitempty"` // major semver part
Minor string `json:",omitempty"` // minor semver part
MissingMinor bool // whether or not the minor semver part was left out
Patch string `json:",omitempty"` // patch semver part
MissingPatch bool // whether or not he patch semver part was left out
Suffix string // tag suffix (e.g. "-alpine") [would be release candidate in semver]
UsesV bool // whether or not the tag uses the "v" prefix
Metadata string // metadata: what's after + and after the first "-"
}
type TagDelta struct {
Major int // major semver difference
Minor int // minor semver difference
Patch int // patch semver difference
}
// ByTagAsc sorts tags in ascending order where the last element is the latest tag.
type ByTagAsc []Tag
func (t ByTagAsc) Len() int { return len(t) }
func (t ByTagAsc) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t ByTagAsc) Less(i, j int) bool {
return t[i].IsLessThan(t[j])
}
// ByTagDesc sorts tags in descending order where the first element is the latest tag.
type ByTagDesc []Tag
func (t ByTagDesc) Len() int { return len(t) }
func (t ByTagDesc) Swap(i, j int) { t[j], t[i] = t[i], t[j] }
func (t ByTagDesc) Less(i, j int) bool {
return t[j].IsLessThan(t[i])
}
// IsGreaterThan tests if a tag is greater than another. There are some
// tag-isms to take into account here, shorter is bigger (i.e. 2.1 > 2.1.1 ==
// true, 2 > 2.1 == true).
func (t Tag) IsGreaterThan(tag Tag) bool {
// shorter is bigger, i.e. 2.1 > 2.1.1
if t.MissingPatch && !tag.MissingPatch || t.MissingMinor && !tag.MissingMinor {
return true
}
if tag.MissingPatch && !t.MissingPatch || tag.MissingMinor && !t.MissingMinor {
return false
}
// ignore errors since Parse already handled
mj1, _ := strconv.Atoi(t.Major)
mj2, _ := strconv.Atoi(tag.Major)
if mj1 > mj2 {
return true
}
if mj2 > mj1 {
return false
}
mn1, _ := strconv.Atoi(t.Minor)
mn2, _ := strconv.Atoi(tag.Minor)
if mn1 > mn2 {
return true
}
if mn2 > mn1 {
return false
}
p1, _ := strconv.Atoi(t.Patch)
p2, _ := strconv.Atoi(tag.Patch)
if p1 > p2 {
return true
}
if p2 > p1 {
return false
}
return false
}
// IsLessThan tests if a tag is less than another. There are some tag-isms to
// take into account here, shorter is bigger (i.e. 2.1 < 2.1.1 == false, 2 <
// 2.1 == false).
func (t Tag) IsLessThan(tag Tag) bool {
return !t.IsGreaterThan(tag)
}
// Equals tests Tag equality
func (t Tag) Equals(tag Tag) bool {
if t.MissingPatch && !tag.MissingPatch || t.MissingMinor && !tag.MissingMinor {
return false
}
if tag.MissingPatch && !t.MissingPatch || tag.MissingMinor && !t.MissingMinor {
return false
}
if t.Metadata != tag.Metadata {
return false
}
// ignore errors since Parse already handled
mj1, _ := strconv.Atoi(t.Major)
mj2, _ := strconv.Atoi(tag.Major)
if mj1 != mj2 {
return false
}
mn1, _ := strconv.Atoi(t.Minor)
mn2, _ := strconv.Atoi(tag.Minor)
if mn1 != mn2 {
return false
}
p1, _ := strconv.Atoi(t.Patch)
p2, _ := strconv.Atoi(tag.Patch)
return p1 == p2
}
// String formats a Tag correctly in string representation
func (t Tag) String() string {
var repr string
if t.UsesV {
repr += "v"
}
repr += t.Major
if !t.MissingMinor {
repr += fmt.Sprintf(".%s", t.Minor)
}
if !t.MissingPatch {
repr += fmt.Sprintf(".%s", t.Patch)
}
if t.Suffix != "" {
repr += fmt.Sprintf("-%s", t.Suffix)
}
if t.Metadata != "" {
repr += fmt.Sprintf("+%s", t.Metadata)
}
return repr
}
func (t TagDelta) String() string {
var repr string
repr = fmt.Sprintf("%d.%d.%d", t.Major, t.Minor, t.Patch)
return repr
}
// IsCompatible determines if two tags can be compared together
func (t Tag) IsCompatible(tag Tag) bool {
if t.UsesV && !tag.UsesV || tag.UsesV && !t.UsesV {
return false
}
if t.Suffix != "" && tag.Suffix == "" || t.Suffix == "" && tag.Suffix != "" {
return false
}
if t.Suffix != "" && tag.Suffix != "" {
if t.Suffix != tag.Suffix {
return false
}
}
if t.MissingMinor && !tag.MissingMinor || tag.MissingMinor && !t.MissingMinor {
return false
}
if t.MissingPatch && !tag.MissingPatch || tag.MissingPatch && !t.MissingPatch {
return false
}
return true
}
// IsUpgradeCompatible chekcs if upTag is compatible with a pinned version tag.
// I.e. pinning to 22-fpm should return true if upTag is 22.2.0-fpm but not 22.2.0-alpine or 23.0.0-fpm
func (pin Tag) IsUpgradeCompatible(upTag Tag) bool {
if pin.Suffix != upTag.Suffix {
return false
}
if pin.Major != upTag.Major {
return false
}
if pin.MissingMinor {
return true
}
if pin.Minor != upTag.Minor {
return false
}
if pin.MissingPatch {
return true
}
if pin.Patch != upTag.Patch {
return false
}
return true
}
// UpgradeDelta returns a TagDelta object which is the difference between an old and new tag
// It can contain negative numbers if comparing with an older tag.
func (curTag Tag) UpgradeDelta(newTag Tag) (TagDelta, error) {
if !curTag.IsCompatible(newTag) {
return TagDelta{}, fmt.Errorf("%s and %s are not compatible with each other", curTag.String(), newTag.String())
}
diff := TagDelta{
Major: 0,
Minor: 0,
Patch: 0,
}
// assuming tags are correctly formatted
curMajor, _ := strconv.Atoi(curTag.Major)
newMajor, _ := strconv.Atoi(newTag.Major)
diff.Major = newMajor - curMajor
if !curTag.MissingMinor {
curMinor, _ := strconv.Atoi(curTag.Minor)
newMinor, _ := strconv.Atoi(newTag.Minor)
diff.Minor = newMinor - curMinor
}
if !curTag.MissingPatch {
curPatch, _ := strconv.Atoi(curTag.Patch)
newPatch, _ := strconv.Atoi(newTag.Patch)
diff.Patch = newPatch - curPatch
}
return diff, nil
}
// UpgradeType takes exit from UpgradeElemene and returns a numeric representation of upgrade or downgrade
// 1/-1: patch 2/-2: minor 4/-4: major 0: no change
func (d TagDelta) UpgradeType() int {
if d.Major > 0 {
return 4
}
if d.Major < 0 {
return -4
}
if d.Minor > 0 {
return 2
}
if d.Minor < 0 {
return -2
}
if d.Patch > 0 {
return 1
}
if d.Patch < 0 {
return -1
}
return 0
}
// CommitHashPattern matches commit-like hash tags
var CommitHashPattern = "^[a-f0-9]{7,40}$"
// DotPattern matches tags which contain multiple versions
var DotPattern = "([0-9]+)\\.([0-9]+)"
// EmptyPattern matches when tags are missing
var EmptyPattern = "^$"
// ParametrizedPattern matches when tags are parametrized
var ParametrizedPattern = "\\${.+}"
// StringPattern matches when tags are only made up of alphabetic characters
var StringPattern = "^[a-zA-Z]+$"
// patternMatches determines if a tag matches unsupported patterns
func patternMatches(tag string) error {
unsupported := []string{
CommitHashPattern,
EmptyPattern,
ParametrizedPattern,
StringPattern,
}
for _, pattern := range unsupported {
if match, _ := regexp.Match(pattern, []byte(tag)); match {
return fmt.Errorf("'%s' is not supported (%s)", tag, pattern)
}
}
return nil
}
// patternCounts determines if tags match unsupported patterns by counting occurences of matches
func patternCounts(tag string) error {
v := regexp.MustCompile(DotPattern)
tag = strings.Split(tag, "+")[0]
if m := v.FindAllStringIndex(tag, -1); len(m) > 1 {
return fmt.Errorf("'%s' is not supported (%s)", tag, DotPattern)
}
return nil
}
// parseVersionPart converts a semver version part to an integer
func parseVersionPart(part string) (int, error) {
p, err := strconv.Atoi(part)
if err != nil {
return 0, err
}
return p, nil
}
// ParseDelta converts a tag difference in the format of X, X.Y or X.Y.Z where
// X, Y, Z are positive or negative integers or 0
func ParseDelta(delta string) (TagDelta, error) {
tagDelta := TagDelta{
Major: 0,
Minor: 0,
Patch: 0,
}
splits := strings.Split(delta, ".")
if len(splits) > 3 {
return TagDelta{}, fmt.Errorf("'%s' has too much dots", delta)
}
major, err := strconv.Atoi(splits[0])
if err != nil {
return TagDelta{}, fmt.Errorf("Major part of '%s' is not an integer", delta)
}
tagDelta.Major = major
if len(splits) > 1 {
minor, err := strconv.Atoi(splits[1])
if err != nil {
return TagDelta{}, fmt.Errorf("Minor part of '%s' is not an integer", delta)
}
tagDelta.Minor = minor
}
if len(splits) > 2 {
patch, err := strconv.Atoi(splits[2])
if err != nil {
return TagDelta{}, fmt.Errorf("Minor part of '%s' is not an integer", delta)
}
tagDelta.Patch = patch
}
return tagDelta, nil
}
// Parse converts an image tag into a structured data format. It aims to to
// support the general case of tags which are "semver-like" and/or stable and
// parseable by heuristics. Image tags follow no formal specification and
// therefore this is a best-effort implementation. Examples of tags this
// function can parse are: "5", "5.2", "v4", "v5.3.6", "4-alpine",
// "v3.2.1-debian".
func Parse(tag string) (Tag, error) {
if err := patternMatches(tag); err != nil {
return Tag{}, err
}
if err := patternCounts(tag); err != nil {
return Tag{}, err
}
usesV := false
if string(tag[0]) == "v" {
tag = strings.TrimPrefix(tag, "v")
usesV = true
}
var metadata string
splits := strings.Split(tag, "+")
if len(splits) > 1 {
tag = splits[0]
metadata = splits[1]
}
var suffix string
splits = strings.SplitN(tag, "-", 2)
if len(splits) > 1 {
tag = splits[0]
suffix = splits[1]
}
var major, minor, patch string
var missingMinor, missingPatch bool
parts := strings.Split(tag, ".")
switch {
case len(parts) == 1:
if _, err := parseVersionPart(parts[0]); err != nil {
return Tag{}, fmt.Errorf("couldn't parse major part of '%s': '%s'", tag, parts[0])
}
major = parts[0]
missingMinor = true
missingPatch = true
case len(parts) == 2:
if _, err := parseVersionPart(parts[0]); err != nil {
return Tag{}, fmt.Errorf("couldn't parse major part of '%s': '%s'", tag, parts[0])
}
major = parts[0]
if _, err := parseVersionPart(parts[1]); err != nil {
return Tag{}, fmt.Errorf("couldn't parse minor part of '%s': '%s'", tag, parts[1])
}
minor = parts[1]
missingPatch = true
case len(parts) == 3:
if _, err := parseVersionPart(parts[0]); err != nil {
return Tag{}, fmt.Errorf("couldn't parse major part of '%s': '%s'", tag, parts[0])
}
major = parts[0]
if _, err := parseVersionPart(parts[1]); err != nil {
return Tag{}, fmt.Errorf("couldn't parse minor part of '%s': '%s'", tag, parts[1])
}
minor = parts[1]
if _, err := parseVersionPart(parts[2]); err != nil {
return Tag{}, fmt.Errorf("couldn't parse patch part of '%s': '%s'", tag, parts[2])
}
patch = parts[2]
default:
return Tag{}, fmt.Errorf("couldn't parse semver of '%s", tag)
}
parsedTag := Tag{
Major: major,
Minor: minor,
MissingMinor: missingMinor,
Patch: patch,
MissingPatch: missingPatch,
UsesV: usesV,
Suffix: suffix,
Metadata: metadata,
}
return parsedTag, nil
}
// IsParsable determines if a tag is supported by this library
func IsParsable(tag string) bool {
if _, err := Parse(tag); err != nil {
return false
}
return true
}

View File

@ -1,12 +0,0 @@
version = 1
test_patterns = [
"*_test.go"
]
[[analyzers]]
name = "go"
enabled = true
[analyzers.meta]
import_path = "dario.cat/mergo"

View File

@ -1,33 +0,0 @@
#### joe made this: http://goel.io/joe
#### go ####
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
#### vim ####
# Swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-v][a-z]
[._]sw[a-p]
# Session
Session.vim
# Temporary
.netrwhist
*~
# Auto-generated tag files
tags

View File

@ -1,12 +0,0 @@
language: go
arch:
- amd64
- ppc64le
install:
- go get -t
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
script:
- go test -race -v ./...
after_script:
- $HOME/gopath/bin/goveralls -service=travis-ci -repotoken $COVERALLS_TOKEN

View File

@ -1,46 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at i@dario.im. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

Some files were not shown because too many files have changed in this diff Show More