Compare commits

..

9 Commits

3646 changed files with 1610 additions and 983409 deletions

View File

@ -71,6 +71,7 @@ steps:
port: 22 port: 22
command_timeout: 60m command_timeout: 60m
script_stop: true script_stop: true
envs: [ DRONE_SOURCE_BRANCH ]
request_pty: true request_pty: true
script: 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_DIR="$HOME/.abra_test"
# export ABRA_TEST_DOMAIN=test.example.com # export ABRA_TEST_DOMAIN=test.example.com
# export ABRA_CI=1 # export ABRA_SKIP_TEARDOWN=1 # for faster feedback when developing tests
# release automation
# export GITEA_TOKEN=

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,24 +1,21 @@
package app package app
import ( import (
"fmt" "context"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/charmbracelet/lipgloss" "github.com/urfave/cli/v3"
"github.com/urfave/cli"
) )
var appCheckCommand = cli.Command{ var appCheckCommand = cli.Command{
Name: "check", Name: "check",
Aliases: []string{"chk"}, Aliases: []string{"chk"},
Usage: "Ensure an app is well configured", Usage: "Ensure an app is well configured",
Description: ` Description: `Compare env vars in both the app ".env" and recipe ".env.sample" file.
This command compares 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 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 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 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 recipes regardless of commenting or not (e.g. through the use of
${FOO:<default>} syntax). "check" does not confirm or deny this for you.`, ${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{ Flags: []cli.Flag{
internal.DebugFlag,
internal.ChaosFlag, internal.ChaosFlag,
internal.OfflineFlag, internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete, EnableShellCompletion: true,
Action: func(c *cli.Context) error { ShellComplete: autocomplete.AppNameComplete,
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 { if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err) log.Fatal(err)
} }
table, err := formatter.CreateTable() tableCol := []string{"recipe env sample", "app env"}
if err != nil { table := formatter.CreateTable(tableCol)
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)
}
})
envVars, err := appPkg.CheckEnv(app) envVars, err := appPkg.CheckEnv(app)
if err != nil { if err != nil {
@ -66,15 +51,13 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
for _, envVar := range envVars { for _, envVar := range envVars {
if envVar.Present { if envVar.Present {
val := []string{envVar.Name, "✅"} table.Append([]string{envVar.Name, "✅"})
table.Row(val...)
} else { } else {
val := []string{envVar.Name, "❌"} table.Append([]string{envVar.Name, "❌"})
table.Row(val...)
} }
} }
fmt.Println(table) table.Render()
return nil return nil
}, },

View File

@ -1,6 +1,7 @@
package app package app
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@ -14,7 +15,7 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli" "github.com/urfave/cli/v3"
) )
var appCmdCommand = cli.Command{ 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. 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 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 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". **WARNING**: [options] must be passed directly after the "cmd" sub-command.`,
UsageText: "abra app cmd [options] <domain> [<service>] <cmd> [-- <cmd-args>]",
EXAMPLE: HideHelpCommand: true,
abra app cmd --local example.com app create_user -- me@example.com`,
ArgsUsage: "<domain> [<service>] <command> [-- <args>]",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
internal.LocalCmdFlag, internal.LocalCmdFlag,
internal.RemoteUserFlag, internal.RemoteUserFlag,
internal.TtyFlag, internal.TtyFlag,
internal.OfflineFlag,
internal.ChaosFlag, internal.ChaosFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Subcommands: []cli.Command{appCmdListCommand}, Commands: []*cli.Command{
BashComplete: func(ctx *cli.Context) { &appCmdListCommand,
args := ctx.Args() },
switch len(args) { EnableShellCompletion: true,
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
args := cmd.Args()
switch args.Len() {
case 0: case 0:
autocomplete.AppNameComplete(ctx) autocomplete.AppNameComplete(ctx, cmd)
case 1: case 1:
autocomplete.ServiceNameComplete(args.Get(0)) autocomplete.ServiceNameComplete(args.Get(0))
case 2: case 2:
cmdNameComplete(args.Get(0)) cmdNameComplete(args.Get(0))
} }
}, },
Action: func(c *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(cmd)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if internal.LocalCmd && internal.RemoteUser != "" { 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 _, err := os.Stat(app.Recipe.AbraShPath); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
@ -76,11 +75,11 @@ EXAMPLE:
} }
if internal.LocalCmd { if internal.LocalCmd {
if !(len(c.Args()) >= 2) { if !(cmd.Args().Len() >= 2) {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments")) 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 { if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -112,13 +111,13 @@ EXAMPLE:
log.Fatal(err) log.Fatal(err)
} }
} else { } else {
if !(len(c.Args()) >= 3) { if !(cmd.Args().Len() >= 3) {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments")) 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 { if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -195,19 +194,19 @@ func cmdNameComplete(appName string) {
} }
var appCmdListCommand = cli.Command{ var appCmdListCommand = cli.Command{
Name: "list", Name: "list",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Usage: "List all available commands", Usage: "List all available commands",
ArgsUsage: "<domain>", UsageText: "abra app cmd ls [options] <domain>",
HideHelpCommand: true,
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
internal.ChaosFlag, internal.ChaosFlag,
}, },
BashComplete: autocomplete.AppNameComplete, EnableShellCompletion: true,
Before: internal.SubCommandBefore, ShellComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Before: internal.SubCommandBefore,
app := internal.ValidateApp(c) Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.EnsureExists(); err != nil { if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err) log.Fatal(err)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,41 +6,17 @@ import (
"strings" "strings"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/lipgloss"
dockerClient "github.com/docker/docker/client" 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 // NewVersionOverview shows an upgrade or downgrade overview
func NewVersionOverview( func NewVersionOverview(app appPkg.App, currentVersion, chaosVersion, newVersion, releaseNotes string) error {
app appPkg.App, tableCol := []string{"server", "recipe", "config", "domain", "version", "chaos", "to deploy"}
warnMessages []string, table := formatter.CreateTable(tableCol)
kind,
currentVersion,
chaosVersion,
newVersion,
releaseNotes string) error {
deployConfig := "compose.yml" deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
@ -51,36 +27,22 @@ func NewVersionOverview(
server = "local" server = "local"
} }
body := strings.Builder{} table.Append([]string{
body.WriteString( server,
borderStyle.Render( app.Recipe.Name,
lipgloss.JoinVertical( deployConfig,
lipgloss.Center, app.Domain,
headerStyle.Render(fmt.Sprintf("%s OVERVIEW", strings.ToUpper(kind))), currentVersion,
lipgloss.JoinVertical( chaosVersion,
lipgloss.Left, newVersion,
horizontal(leftStyle.Render("SERVER"), " ", rightStyle.Render(server)), })
horizontal(leftStyle.Render("DOMAIN"), " ", rightStyle.Render(app.Domain)), table.Render()
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())
if releaseNotes != "" && newVersion != "" { if releaseNotes != "" && newVersion != "" {
fmt.Println() fmt.Println()
fmt.Print(releaseNotes) fmt.Print(releaseNotes)
} else { } else {
warnMessages = append(warnMessages, fmt.Sprintf("no release notes available for %s", newVersion)) log.Warnf("no release notes available for %s", newVersion)
}
for _, msg := range warnMessages {
log.Warn(msg)
} }
if NoInput { if NoInput {
@ -88,66 +50,16 @@ func NewVersionOverview(
} }
response := false response := false
prompt := &survey.Confirm{Message: "proceed?"} prompt := &survey.Confirm{
Message: "continue with deployment?",
}
if err := survey.AskOne(prompt, &response); err != nil { if err := survey.AskOne(prompt, &response); err != nil {
return err return err
} }
if !response { if !response {
log.Fatal("deployment cancelled") log.Fatal("exiting as requested")
}
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")
} }
return nil return nil
@ -206,3 +118,48 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
} }
return nil 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" "os"
"coopcloud.tech/abra/pkg/log" "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 // ShowSubcommandHelpAndError exits the program on error, logs the error to the
// terminal, and shows the help command. // terminal, and shows the help command.
func ShowSubcommandHelpAndError(c *cli.Context, err interface{}) { func ShowSubcommandHelpAndError(cmd *cli.Command, err interface{}) {
if err2 := cli.ShowSubcommandHelp(c); err2 != nil { if err2 := cli.ShowSubcommandHelp(cmd); err2 != nil {
log.Error(err2) log.Error(err2)
} }
log.Error(err) log.Error(err)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package recipe package recipe
import ( import (
"context"
"fmt" "fmt"
"sort" "sort"
@ -9,7 +10,8 @@ import (
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe" 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 { 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{ var recipeVersionCommand = cli.Command{
Name: "versions", Name: "versions",
Aliases: []string{"v"}, Aliases: []string{"v"},
Usage: "List recipe versions", Usage: "List recipe versions",
ArgsUsage: "<recipe>", UsageText: "abra recipe version [options] <recipe>",
HideHelpCommand: true,
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
internal.NoInputFlag,
internal.MachineReadableFlag, internal.MachineReadableFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete, EnableShellCompletion: true,
Action: func(c *cli.Context) error { ShellComplete: autocomplete.RecipeNameComplete,
var warnMessages []string Action: func(ctx context.Context, cmd *cli.Command) error {
recipe := internal.ValidateRecipe(cmd)
recipe := internal.ValidateRecipe(c)
catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline) catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
if err != nil { if err != nil {
@ -47,65 +46,47 @@ var recipeVersionCommand = cli.Command{
recipeMeta, ok := catl[recipe.Name] recipeMeta, ok := catl[recipe.Name]
if !ok { 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() recipeVersions, err := recipe.GetRecipeVersions()
if err != nil { if err != nil {
warnMessages = append(warnMessages, err.Error()) log.Warn(err)
} }
recipeMeta = recipePkg.RecipeMeta{Versions: recipeVersions} recipeMeta = recipePkg.RecipeMeta{Versions: recipeVersions}
} }
if len(recipeMeta.Versions) == 0 { 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-- { for i := len(recipeMeta.Versions) - 1; i >= 0; i-- {
table, err := formatter.CreateTable() table := formatter.CreateTable(tableCols)
if err != nil {
log.Fatal(err)
}
table.Headers("SERVICE", "NAME", "TAG")
for version, meta := range recipeMeta.Versions[i] { for version, meta := range recipeMeta.Versions[i] {
var allRows [][]string var versions [][]string
var rows [][]string
for service, serviceMeta := range meta { for service, serviceMeta := range meta {
rows = append(rows, []string{service, serviceMeta.Image, serviceMeta.Tag}) versions = append(versions, []string{version, service, serviceMeta.Image, serviceMeta.Tag})
allRows = append(allRows, []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 { if !internal.MachineReadable {
fmt.Println(table) table.SetAutoMergeCellsByColumnIndex([]int{0})
log.Infof("VERSION: %s", version) table.SetAlignment(tablewriter.ALIGN_LEFT)
table.Render()
fmt.Println() 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 {
if !internal.MachineReadable { aggregated_table.JSONRender()
for _, warnMsg := range warnMessages {
log.Warn(warnMsg)
}
} }
return nil return nil

View File

@ -1,6 +1,7 @@
package server package server
import ( import (
"context"
"errors" "errors"
"os" "os"
"path/filepath" "path/filepath"
@ -13,12 +14,13 @@ import (
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/server" "coopcloud.tech/abra/pkg/server"
sshPkg "coopcloud.tech/abra/pkg/ssh" sshPkg "coopcloud.tech/abra/pkg/ssh"
"github.com/urfave/cli" "github.com/urfave/cli/v3"
) )
var local bool var local bool
var localFlag = &cli.BoolFlag{ var localFlag = &cli.BoolFlag{
Name: "local, l", Name: "local",
Aliases: []string{"l"},
Usage: "Use local server", Usage: "Use local server",
Destination: &local, Destination: &local,
} }
@ -92,15 +94,16 @@ func createServerDir(name string) (bool, error) {
} }
var serverAddCommand = cli.Command{ var serverAddCommand = cli.Command{
Name: "add", Name: "add",
Aliases: []string{"a"}, Aliases: []string{"a"},
Usage: "Add a new server to your configuration", Usage: "Add a new server",
Description: ` UsageText: "abra server add [options] <domain>",
Add a new server to your configuration so that it can be managed by Abra. 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 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 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 Host example.com example
Hostname example.com Hostname example.com
@ -108,32 +111,31 @@ for each server. For example:
Port 12345 Port 12345
IdentityFile ~/.ssh/example@somewhere 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 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 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 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{ Flags: []cli.Flag{
internal.DebugFlag, internal.NoDomainChecksFlag,
internal.NoInputFlag,
localFlag, localFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
ArgsUsage: "<name>", ArgsUsage: "<name>",
Action: func(c *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if len(c.Args()) > 0 && local || !internal.ValidateSubCmdFlags(c) { if cmd.Args().Len() > 0 && local || !internal.ValidateSubCmdFlags(cmd) {
err := errors.New("cannot use <name> and --local together") err := errors.New("cannot use <name> and --local together")
internal.ShowSubcommandHelpAndError(c, err) internal.ShowSubcommandHelpAndError(cmd, err)
} }
var name string var name string
if local { if local {
name = "default" name = "default"
} else { } else {
name = internal.ValidateDomain(c) name = internal.ValidateDomain(cmd)
} }
// NOTE(d1): reasonable 5 second timeout for connections which can't // 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 return nil
} }
if _, err := dns.EnsureIPv4(name); err != nil { if !internal.NoDomainChecks {
log.Warn(err) if _, err := dns.EnsureIPv4(name); err != nil {
log.Fatal(err)
}
} }
_, err := createServerDir(name) _, err := createServerDir(name)

View File

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

View File

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

View File

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

View File

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

View File

@ -23,44 +23,44 @@ import (
dockerclient "github.com/docker/docker/client" dockerclient "github.com/docker/docker/client"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli" "github.com/urfave/cli/v3"
) )
const SERVER = "localhost" const SERVER = "localhost"
var majorUpdate bool var majorUpdate bool
var majorFlag = &cli.BoolFlag{ var majorFlag = &cli.BoolFlag{
Name: "major, m", Name: "major",
Aliases: []string{"m"},
Usage: "Also check for major updates", Usage: "Also check for major updates",
Destination: &majorUpdate, Destination: &majorUpdate,
} }
var updateAll bool var updateAll bool
var allFlag = &cli.BoolFlag{ var allFlag = &cli.BoolFlag{
Name: "all, a", Name: "all",
Aliases: []string{"a"},
Usage: "Update all deployed apps", Usage: "Update all deployed apps",
Destination: &updateAll, Destination: &updateAll,
} }
// Notify checks for available upgrades // Notify checks for available upgrades
var Notify = cli.Command{ var Notify = cli.Command{
Name: "notify", Name: "notify",
Aliases: []string{"n"}, Aliases: []string{"n"},
Usage: "Check for available upgrades", Usage: "Check for available upgrades",
UsageText: "kadabra notify [options]",
HideHelpCommand: true,
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
majorFlag, majorFlag,
internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Description: ` Description: `Notify on new versions for deployed apps.
Read the deployed app versions and look for new versions in the recipe
catalogue.
If a new patch/minor version is available, a notification is printed. If a new patch/minor version is available, a notification is printed.
Use "--major" to include new major versions.`, 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") cl, err := client.New("default")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -92,20 +92,18 @@ Use "--major" to include new major versions.`,
// UpgradeApp upgrades apps. // UpgradeApp upgrades apps.
var UpgradeApp = cli.Command{ var UpgradeApp = cli.Command{
Name: "upgrade", Name: "upgrade",
Aliases: []string{"u"}, Aliases: []string{"u"},
Usage: "Upgrade apps", Usage: "Upgrade apps",
ArgsUsage: "<stack-name> <recipe>", UsageText: "kadabra notify [options] <stack> <recipe>",
HideHelpCommand: true,
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag,
internal.ChaosFlag, internal.ChaosFlag,
majorFlag, majorFlag,
allFlag, allFlag,
internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Description: ` Description: `Upgrade an app by specifying stack name and recipe.
Upgrade an app by specifying stack name and recipe.
Use "--all" to upgrade every deployed app. 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 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 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.`, 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") cl, err := client.New("default")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !updateAll { if !updateAll {
stackName := c.Args().Get(0) stackName := cmd.Args().Get(0)
recipeName := c.Args().Get(1) recipeName := cmd.Args().Get(1)
err = tryUpgrade(cl, stackName, recipeName) err = tryUpgrade(cl, stackName, recipeName)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -468,30 +466,26 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion stri
return err return err
} }
func newAbraApp(version, commit string) *cli.App { func newAbraApp(version, commit string) *cli.Command {
app := &cli.App{ app := &cli.Command{
Name: "kadabra", Name: "kadabra",
Usage: `The Co-op Cloud auto-updater 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,
`, },
Version: fmt.Sprintf("%s-%s", version, commit[:7]), Commands: []*cli.Command{
Commands: []cli.Command{ &Notify,
Notify, &UpgradeApp,
UpgradeApp,
}, },
} }
app.Before = func(c *cli.Context) error { app.Before = func(ctx context.Context, cmd *cli.Command) error {
log.Logger.SetStyles(log.Styles())
charmLog.SetDefault(log.Logger) charmLog.SetDefault(log.Logger)
log.Debugf("kadabra version %s, commit %s", version, commit) log.Debugf("kadabra version %s, commit %s", version, commit)
return nil return nil
} }
@ -502,7 +496,7 @@ func newAbraApp(version, commit string) *cli.App {
func RunApp(version, commit string) { func RunApp(version, commit string) {
app := newAbraApp(version, commit) 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) log.Fatal(err)
} }
} }

13
go.mod
View File

@ -2,11 +2,12 @@ module coopcloud.tech/abra
go 1.21 go 1.21
replace github.com/urfave/cli/v3 => github.com/fiatjaf/cli/v3 v3.0.0-20240704165307-ad0e1925dd42
require ( require (
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355
github.com/AlecAivazis/survey/v2 v2.3.7 github.com/AlecAivazis/survey/v2 v2.3.7
github.com/charmbracelet/lipgloss v0.11.1
github.com/charmbracelet/log v0.4.0 github.com/charmbracelet/log v0.4.0
github.com/distribution/reference v0.6.0 github.com/distribution/reference v0.6.0
github.com/docker/cli v27.0.3+incompatible github.com/docker/cli v27.0.3+incompatible
@ -16,9 +17,10 @@ require (
github.com/google/go-cmp v0.6.0 github.com/google/go-cmp v0.6.0
github.com/moby/sys/signal v0.7.0 github.com/moby/sys/signal v0.7.0
github.com/moby/term v0.5.0 github.com/moby/term v0.5.0
github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.14.4 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 gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.1 gotest.tools/v3 v3.5.1
) )
@ -33,10 +35,10 @@ require (
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.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/cloudflare/circl v1.3.9 // indirect
github.com/containerd/log v0.1.0 // 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/cyphar/filepath-securejoin v0.2.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.3+incompatible // 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/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.4.7 // 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/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.2.2 // indirect github.com/skeema/knownhosts v1.2.2 // indirect
github.com/spf13/pflag v1.0.5 // 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/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/net v0.27.0 // indirect golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.7.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/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // 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/spf13/cobra v1.8.1 // indirect
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/theupdateframework/notary v0.7.0 // indirect 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 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
golang.org/x/sys v0.22.0 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/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 v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 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 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 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= 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.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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g=
github.com/charmbracelet/lipgloss v0.11.1/go.mod h1:beLlcmkF7MWA+5UrKKIRo/VJ21xGXr7YJ9miWfdMRIU= 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 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= 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.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY=
github.com/charmbracelet/x/ansi v0.1.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= 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/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.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= 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/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-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.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/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.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/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 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 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/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/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= 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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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.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 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 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= 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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 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.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-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 v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/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 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 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.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/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/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 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.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.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 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 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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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.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.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.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 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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= 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.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.2/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.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/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 v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= 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 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) file, err := os.Open(a.Path)
if err != nil { if err != nil {
return err return err
} }
defer file.Close() defer file.Close()
skipped := false
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
lines := []string{} lines := []string{}
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() 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) lines = append(lines, line)
continue 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, ":") splitted := strings.Split(line, ":")
line = fmt.Sprintf("%s:%s", splitted[0], version) line = fmt.Sprintf("%s:%s", splitted[0], version)
lines = append(lines, line) lines = append(lines, line)
@ -606,19 +593,5 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error {
log.Fatal(err) log.Fatal(err)
} }
if !dryRun { return os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm)
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
} }

View File

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

View File

@ -107,5 +107,4 @@ var (
REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud" REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json" CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json"
SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git" 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/envfile"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
testPkg "coopcloud.tech/abra/pkg/test" testPkg "coopcloud.tech/abra/pkg/test"
"github.com/stretchr/testify/assert"
) )
func TestGetAllFoldersInDirectory(t *testing.T) { func TestGetAllFoldersInDirectory(t *testing.T) {
@ -44,7 +43,13 @@ func TestReadEnv(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if !reflect.DeepEqual(env, testPkg.ExpectedAppEnv) { 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 package formatter
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"os" "os"
"strings" "strings"
"time" "time"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/docker/go-units" "github.com/docker/go-units"
"golang.org/x/term" // "github.com/olekukonko/tablewriter"
"coopcloud.tech/abra/pkg/jsontable"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/schollz/progressbar/v3" "github.com/schollz/progressbar/v3"
) )
var BoldStyle = lipgloss.NewStyle().
Bold(true).
Underline(true)
func ShortenID(str string) string { func ShortenID(str string) string {
return str[:12] return str[:12]
} }
@ -42,67 +34,11 @@ func HumanDuration(timestamp int64) string {
} }
// CreateTable prepares a table layout for output. // CreateTable prepares a table layout for output.
func CreateTable() (*table.Table, error) { func CreateTable(columns []string) *jsontable.JSONTable {
table := table.New(). table := jsontable.NewJSONTable(os.Stdout)
Border(lipgloss.ThickBorder()). table.SetAutoWrapText(false)
BorderStyle( table.SetHeader(columns)
lipgloss.NewStyle(). return table
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
} }
// CreateProgressbar generates a progress bar // 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 ( import (
"os" "os"
"strings"
"github.com/charmbracelet/lipgloss"
charmLog "github.com/charmbracelet/log" charmLog "github.com/charmbracelet/log"
) )
@ -34,42 +32,3 @@ var SetLevel = Logger.SetLevel
var DebugLevel = charmLog.DebugLevel var DebugLevel = charmLog.DebugLevel
var SetOutput = charmLog.SetOutput var SetOutput = charmLog.SetOutput
var SetReportCaller = charmLog.SetReportCaller 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 { if err := r.EnsureIsClean(); err != nil {
return err return err
} }
if !offline { if !offline {
if err := r.EnsureUpToDate(); err != nil { if err := r.EnsureUpToDate(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
if r.Version != "" { if r.Version != "" {
log.Debugf("ensuring version %s", r.Version)
if _, err := r.EnsureVersion(r.Version); err != nil { if _, err := r.EnsureVersion(r.Version); err != nil {
return err return err
} }
} else {
return nil if err := r.EnsureLatest(); err != nil {
return err
}
} }
if err := r.EnsureLatest(); err != nil {
return err
}
return nil return nil
} }

View File

@ -127,9 +127,6 @@ func Get(name string) Recipe {
version := "" version := ""
if strings.Contains(name, ":") { if strings.Contains(name, ":") {
split := strings.Split(name, ":") split := strings.Split(name, ":")
if len(split) > 2 {
log.Fatalf("version seems invalid: %s", name)
}
name = split[0] name = split[0]
version = split[1] 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 err := client.StoreSecret(cl, secret.RemoteName, passwords[0], server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") { if strings.Contains(err.Error(), "AlreadyExists") {
log.Warnf("%s already exists", secret.RemoteName) log.Warnf("%s already exists, moving on...", secret.RemoteName)
ch <- nil ch <- nil
} else { } else {
ch <- err 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 err := client.StoreSecret(cl, secret.RemoteName, passphrases[0], server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") { if strings.Contains(err.Error(), "AlreadyExists") {
log.Warnf("%s already exists", secret.RemoteName) log.Warnf("%s already exists, moving on...", secret.RemoteName)
ch <- nil ch <- nil
} else { } else {
ch <- err ch <- err

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
function complete_abra_args function complete_abra_args
set -l cmd (commandline -poc) --generate-bash-completion set -l cmd (commandline -poc) --generate-shell-completion
$cmd $cmd
end end
complete -c abra -f -n "not __fish_seen_subcommand_from -h --help -v --version complete_abra_args" -a "(complete_abra_args)" 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' $name = $fn -replace "(.*)\.ps1$", '$1'
Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock { Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock {
param($commandName, $wordToComplete, $cursorPosition) param($commandName, $wordToComplete, $cursorPosition)
$other = "$wordToComplete --generate-bash-completion" $other = "$wordToComplete --generate-shell-completion"
Invoke-Expression $other | ForEach-Object { Invoke-Expression $other | ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
} }

View File

@ -6,9 +6,9 @@ _cli_zsh_autocomplete() {
local cur local cur
cur=${words[-1]} cur=${words[-1]}
if [[ "$cur" == "-"* ]]; then 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 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 fi
if [[ "${opts[1]}" != "" ]]; then if [[ "${opts[1]}" != "" ]]; then

View File

@ -87,7 +87,7 @@ function install_abra_release {
x=$(echo $PATH | grep $HOME/.local/bin) x=$(echo $PATH | grep $HOME/.local/bin)
if [ $? -ne 0 ]; then 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 p=$HOME/.local/bin
com="echo PATH=\$PATH:$p" com="echo PATH=\$PATH:$p"
if [[ $SHELL =~ "bash" ]]; then if [[ $SHELL =~ "bash" ]]; then

View File

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

View File

@ -20,7 +20,6 @@ setup(){
teardown(){ teardown(){
_reset_recipe _reset_recipe
_reset_tags
} }
@test "validate app argument" { @test "validate app argument" {
@ -83,17 +82,19 @@ teardown(){
} }
@test "ensure recipe not up to date if --offline" { @test "ensure recipe not up to date if --offline" {
_ensure_env_version "0.1.0+1.20.0" wantHash=$(_get_n_hash 1)
latestRelease=$(_latest_release)
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 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 $ABRA app check "$TEST_APP_DOMAIN" --offline
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l assert_equal $(_get_current_hash) "$wantHash"
refute_output --partial "$latestRelease"
} }
@test "error if missing .env.sample" { @test "error if missing .env.sample" {
@ -117,20 +118,3 @@ teardown(){
assert_success assert_success
assert_output --partial '❌' 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(){ teardown(){
_reset_recipe
_reset_tags
_undeploy_app _undeploy_app
_reset_recipe
} }
# bats test_tags=slow # bats test_tags=slow
@ -106,18 +105,20 @@ test_cmd_export"
} }
@test "ensure recipe not up to date if --offline" { @test "ensure recipe not up to date if --offline" {
_ensure_env_version "0.1.0+1.20.0" wantHash=$(_get_n_hash 3)
latestRelease=$(_latest_release)
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_success
assert_equal $(_get_current_hash) "$wantHash"
run $ABRA app cmd --local --offline "$TEST_APP_DOMAIN" test_cmd run $ABRA app cmd --local --offline "$TEST_APP_DOMAIN" test_cmd
assert_success assert_success
assert_output --partial 'baz' assert_output --partial 'baz'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l assert_equal $(_get_current_hash) $wantHash
refute_output --partial "$latestRelease"
_reset_recipe "$TEST_RECIPE"
} }
@test "error if missing arguments without passing --local" { @test "error if missing arguments without passing --local" {
@ -186,24 +187,6 @@ test_cmd_export"
assert_output --partial 'baz' 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 # bats test_tags=slow
@test "error if missing service" { @test "error if missing service" {
_deploy_app _deploy_app

View File

@ -16,13 +16,12 @@ teardown_file(){
setup(){ setup(){
load "$PWD/tests/integration/helpers/common" load "$PWD/tests/integration/helpers/common"
_common_setup _common_setup
_ensure_catalogue
} }
teardown(){ teardown(){
_undeploy_app
_reset_recipe _reset_recipe
_reset_app _reset_app
_undeploy_app
_reset_tags _reset_tags
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo" run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
@ -87,18 +86,19 @@ teardown(){
# bats test_tags=slow # bats test_tags=slow
@test "ensure recipe not up to date if --offline" { @test "ensure recipe not up to date if --offline" {
_ensure_env_version "0.1.0+1.20.0" wantHash=$(_get_n_hash 3)
latestRelease=$(_latest_release)
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_success
assert_equal $(_get_current_hash) "$wantHash"
# NOTE(d1): need to use --chaos to force same commit
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --offline --no-input --no-converge-checks --chaos --offline
assert_success assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l assert_equal $(_get_current_hash) "$wantHash"
refute_output --partial "$latestRelease"
} }
# bats test_tags=slow # bats test_tags=slow
@ -106,7 +106,6 @@ teardown(){
latestCommit="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)" latestCommit="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)"
_remove_tags _remove_tags
_wipe_env_version
# NOTE(d1): need to pass --offline to stop tags being pulled again # NOTE(d1): need to pass --offline to stop tags being pulled again
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
@ -126,8 +125,6 @@ teardown(){
assert_equal $(_get_current_hash) "$wantHash" assert_equal $(_get_current_hash) "$wantHash"
_wipe_env_version
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos --no-input --no-converge-checks --chaos
assert_success assert_success
@ -174,12 +171,14 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --force --no-input --no-converge-checks --force
assert_success 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" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos --no-input --no-converge-checks --chaos
assert_success assert_success
assert_output --partial 'already deployed' assert_output --partial 'already deployed but continuing'
assert_output --partial '--chaos'
} }
# bats test_tags=slow # bats test_tags=slow
@ -352,6 +351,39 @@ teardown(){
_undeploy_app _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 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 run $ABRA app ls --server foo.com
assert_success assert_success
refute_output --partial "SERVER: $TEST_SERVER" refute_output --partial "server: $TEST_SERVER |"
assert_output --partial "SERVER: foo.com" assert_output --partial "server: foo.com |"
run rm -rf "$ABRA_DIR/servers/foo.com" run rm -rf "$ABRA_DIR/servers/foo.com"
assert_success assert_success
@ -97,8 +97,8 @@ teardown(){
@test "server stats are correct" { @test "server stats are correct" {
run $ABRA app ls run $ABRA app ls
assert_success assert_success
assert_output --partial "SERVER: $TEST_SERVER" assert_output --partial "server: $TEST_SERVER"
assert_output --partial "TOTAL APPS: 1" assert_output --partial "total apps: 1"
run mkdir -p "$ABRA_DIR/servers/foo.com" run mkdir -p "$ABRA_DIR/servers/foo.com"
assert_success assert_success
@ -113,8 +113,8 @@ teardown(){
assert_success assert_success
assert_output --partial "$TEST_SERVER" assert_output --partial "$TEST_SERVER"
assert_output --partial "foo.com" assert_output --partial "foo.com"
assert_output --partial "TOTAL SERVERS: 2" assert_output --partial "total servers: 2"
assert_output --partial "TOTAL APPS: 2" assert_output --partial "total apps: 2"
run rm -rf "$ABRA_DIR/servers/foo.com" run rm -rf "$ABRA_DIR/servers/foo.com"
assert_success assert_success

View File

@ -39,41 +39,17 @@ teardown(){
_get_head_hash _get_head_hash
_get_current_hash _get_current_hash
assert_equal "$headHash" "$currentHash" 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" { @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 \ --no-input \
--server "$TEST_SERVER" \ --server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN" --domain "$TEST_APP_DOMAIN"
assert_success assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" 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) assert_equal $(_get_tag_hash 0.1.1+1.20.2) $(_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
} }
@test "does not overwrite existing env files" { @test "does not overwrite existing env files" {

View File

@ -31,84 +31,6 @@ teardown(){
assert_output --partial 'cannot find app' 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" { @test "error if not deployed" {
run $ABRA app ps "$TEST_APP_DOMAIN" run $ABRA app ps "$TEST_APP_DOMAIN"
assert_failure assert_failure
@ -119,11 +41,13 @@ teardown(){
@test "show ps report" { @test "show ps report" {
_deploy_app _deploy_app
latestRelease=$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | tail -n 1)
run $ABRA app ps "$TEST_APP_DOMAIN" run $ABRA app ps "$TEST_APP_DOMAIN"
assert_success assert_success
assert_output --partial 'app' assert_output --partial 'app'
assert_output --partial 'healthy' assert_output --partial 'healthy'
assert_output --partial $(_latest_release) assert_output --partial "$latestRelease"
assert_output --partial 'false' # not a chaos deploy assert_output --partial 'false' # not a chaos deploy
} }

View File

@ -58,18 +58,18 @@ teardown(){
} }
@test "ensure recipe not up to date if --offline" { @test "ensure recipe not up to date if --offline" {
_ensure_env_version "0.1.0+1.20.0" wantHash=$(_get_n_hash 3)
latestRelease=$(_latest_release)
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_success
assert_equal $(_get_current_hash) "$wantHash"
run $ABRA app rollback "$TEST_APP_DOMAIN" \ run $ABRA app rollback "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --offline --no-input --no-converge-checks --offline
assert_failure assert_failure
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l assert_equal $(_get_current_hash) $wantHash
refute_output --partial "$latestRelease"
} }
@test "error if not already deployed" { @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,7 +217,6 @@ teardown(){
run bash -c "echo bar >> $ABRA_DIR/recipes/$TEST_RECIPE/foo" run bash -c "echo bar >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app secret insert \ 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_success
assert_output --partial 'successfully stored on server' assert_output --partial 'successfully stored on server'
@ -318,10 +317,9 @@ teardown(){
run $ABRA app secret generate "$TEST_APP_DOMAIN" --all run $ABRA app secret generate "$TEST_APP_DOMAIN" --all
assert_success assert_success
run bash -c '$ABRA app secret ls "$TEST_APP_DOMAIN" --machine \ run $ABRA app secret ls "$TEST_APP_DOMAIN" --machine
| jq -r ".[] | select(.name==\"test_pass_two\") | .version"'
assert_success assert_success
assert_output --partial 'v1' assert_output --partial '"created-on-server":"true"'
} }
@test "ls: bail if unstaged changes and no --chaos" { @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(){ teardown(){
_reset_recipe
_undeploy_app _undeploy_app
} }
@ -32,61 +31,12 @@ teardown(){
assert_output --partial 'cannot find app' 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" { @test "error if not deployed" {
run $ABRA app undeploy "$TEST_APP_DOMAIN" run $ABRA app undeploy "$TEST_APP_DOMAIN"
assert_failure assert_failure
assert_output --partial 'is not deployed' 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 # bats test_tags=slow
@test "undeploy app" { @test "undeploy app" {
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input

View File

@ -18,9 +18,8 @@ setup(){
} }
teardown(){ teardown(){
_reset_recipe
_reset_app
_undeploy_app _undeploy_app
_reset_recipe
} }
@test "validate app argument" { @test "validate app argument" {
@ -124,9 +123,11 @@ teardown(){
assert_success assert_success
assert_output --partial '0.1.0+1.20.0' 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 run $ABRA app upgrade "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success assert_success
assert_output --partial "$(_latest_release)" assert_output --partial "$latestRelease"
} }
# bats test_tags=slow # bats test_tags=slow
@ -135,9 +136,11 @@ teardown(){
assert_success assert_success
assert_output --partial '0.1.1+1.20.2' 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 run $ABRA app upgrade "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success assert_success
assert_output --partial "$(_latest_release)" assert_output --partial "$latestRelease"
assert_output --partial 'release notes baz' # 0.2.0+1.21.0 assert_output --partial 'release notes baz' # 0.2.0+1.21.0
refute_output --partial 'release notes bar' # 0.1.1+1.20.2 refute_output --partial 'release notes bar' # 0.1.1+1.20.2
} }
@ -161,9 +164,11 @@ teardown(){
assert_success assert_success
assert_output --partial '0.1.0+1.20.0' 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 run $ABRA app upgrade "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success 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 bar' # 0.1.1+1.20.2
assert_output --partial 'release notes baz' # 0.2.0+1.21.0 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 #!/usr/bin/env bash
_latest_release(){
echo $(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | tail -n 1)
}
_fetch_recipe() { _fetch_recipe() {
if [[ ! -d "$ABRA_DIR/recipes/$TEST_RECIPE" ]]; then if [[ ! -d "$ABRA_DIR/recipes/$TEST_RECIPE" ]]; then
run mkdir -p "$ABRA_DIR/recipes" run mkdir -p "$ABRA_DIR/recipes"
@ -23,29 +19,10 @@ _reset_recipe(){
} }
_ensure_latest_version(){ _ensure_latest_version(){
latestRelease=$(_latest_release) latestRelease=$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | tail -n 1)
if [ ! $latestRelease = "$1" ]; then if [ ! $latestRelease = "$1" ]; then
echo "expected latest recipe version of '$1', saw: $latestRelease" echo "expected latest recipe version of '$1', saw: $latestRelease"
return 1 return 1
fi 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 #!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_ensure_catalogue
}
setup() { setup() {
load "$PWD/tests/integration/helpers/common" load "$PWD/tests/integration/helpers/common"
_common_setup _common_setup
} }
@test "recipe versions" { @test "recipe versions" {
run $ABRA recipe versions gitea run $ABRA recipe versions gitea
assert_success assert_success
@ -19,9 +12,11 @@ setup() {
} }
@test "local tags used if no catalogue entry" { @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" run $ABRA recipe versions "$TEST_RECIPE"
assert_success assert_success
assert_output --partial "$(_latest_release)" assert_output --partial "$latestRelease"
} }
@test "versions listed in correct order" { @test "versions listed in correct order" {

View File

@ -13,7 +13,6 @@ teardown_file(){
setup(){ setup(){
load "$PWD/tests/integration/helpers/common" load "$PWD/tests/integration/helpers/common"
_common_setup _common_setup
_add_server
} }
teardown(){ teardown(){
@ -62,13 +61,12 @@ teardown(){
} }
@test "machine readable output" { @test "machine readable output" {
if [ ! "$TEST_SERVER" = "default" ]; then run "$ABRA" server ls --machine
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"}]'))
assert_success assert_success
expectedOutput='[{"name":"'
expectedOutput+="$TEST_SERVER"
expectedOutput+='"'
assert_output --partial "$expectedOutput"
} }

View File

@ -1,3 +1,2 @@
RECIPE=ecloud RECIPE=ecloud
DOMAIN=ecloud.evil.corp 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