refactor!: cobra migrate

This commit is contained in:
2024-12-26 17:53:25 +01:00
parent 0df2b15c33
commit 671e1ca276
76 changed files with 12042 additions and 2545 deletions

View File

@ -1,34 +1,11 @@
package app
import (
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var AppCommand = cli.Command{
Name: "app",
Aliases: []string{"a"},
Usage: "Manage apps",
UsageText: "abra app [command] [arguments] [options]",
Commands: []*cli.Command{
&appBackupCommand,
&appCheckCommand,
&appCmdCommand,
&appConfigCommand,
&appCpCommand,
&appDeployCommand,
&appListCommand,
&appLogsCommand,
&appNewCommand,
&appPsCommand,
&appRemoveCommand,
&appRestartCommand,
&appRestoreCommand,
&appRollbackCommand,
&appRunCommand,
&appSecretCommand,
&appServicesCommand,
&appUndeployCommand,
&appUpgradeCommand,
&appVolumeCommand,
},
var AppCommand = &cobra.Command{
Use: "app [cmd] [args] [flags]",
Aliases: []string{"a"},
Short: "Manage apps",
}

View File

@ -1,283 +0,0 @@
package app
import (
"context"
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli/v3"
)
var snapshot string
var snapshotFlag = &cli.StringFlag{
Name: "snapshot",
Aliases: []string{"s"},
Usage: "Lists specific snapshot",
Destination: &snapshot,
}
var includePath string
var includePathFlag = &cli.StringFlag{
Name: "path",
Aliases: []string{"p"},
Usage: "Include path",
Destination: &includePath,
}
var resticRepo string
var resticRepoFlag = &cli.StringFlag{
Name: "repo",
Aliases: []string{"r"},
Usage: "Restic repository",
Destination: &resticRepo,
}
var appBackupListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Flags: []cli.Flag{
snapshotFlag,
includePathFlag,
},
Before: internal.SubCommandBefore,
Usage: "List all backups",
UsageText: "abra app backup list <domain> [options]",
ShellComplete: autocomplete.AppNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
}
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
}
if includePath != "" {
log.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath)
execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath))
}
if err := internal.RunBackupCmdRemote(cl, "ls", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
}
return nil
},
}
var appBackupDownloadCommand = cli.Command{
Name: "download",
Aliases: []string{"d"},
Flags: []cli.Flag{
snapshotFlag,
includePathFlag,
},
Before: internal.SubCommandBefore,
Usage: "Download a backup",
UsageText: "abra app backup download <domain> [options]",
ShellComplete: autocomplete.AppNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)
}
if !internal.Chaos {
if err := app.Recipe.EnsureIsClean(); err != nil {
log.Fatal(err)
}
if !internal.Offline {
if err := app.Recipe.EnsureUpToDate(); err != nil {
log.Fatal(err)
}
}
if err := app.Recipe.EnsureLatest(); err != nil {
log.Fatal(err)
}
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
}
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
}
if includePath != "" {
log.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath)
execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath))
}
if err := internal.RunBackupCmdRemote(cl, "download", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
}
remoteBackupDir := "/tmp/backup.tar.gz"
currentWorkingDir := "."
if err = CopyFromContainer(cl, targetContainer.ID, remoteBackupDir, currentWorkingDir); err != nil {
log.Fatal(err)
}
fmt.Println("backup successfully downloaded to current working directory")
return nil
},
}
var appBackupCreateCommand = cli.Command{
Name: "create",
Aliases: []string{"c"},
Flags: []cli.Flag{
resticRepoFlag,
},
Before: internal.SubCommandBefore,
Usage: "Create a new backup",
UsageText: "abra app backup create <domain> [options]",
ShellComplete: autocomplete.AppNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)
}
if !internal.Chaos {
if err := app.Recipe.EnsureIsClean(); err != nil {
log.Fatal(err)
}
if !internal.Offline {
if err := app.Recipe.EnsureUpToDate(); err != nil {
log.Fatal(err)
}
}
if err := app.Recipe.EnsureLatest(); err != nil {
log.Fatal(err)
}
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
}
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
if resticRepo != "" {
log.Debugf("including RESTIC_REPO=%s in backupbot exec invocation", resticRepo)
execEnv = append(execEnv, fmt.Sprintf("RESTIC_REPO=%s", resticRepo))
}
if err := internal.RunBackupCmdRemote(cl, "create", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
}
return nil
},
}
var appBackupSnapshotsCommand = cli.Command{
Name: "snapshots",
Aliases: []string{"s"},
Flags: []cli.Flag{
snapshotFlag,
},
Before: internal.SubCommandBefore,
Usage: "List backup snapshots",
UsageText: "abra app backup snapshots <domain> [options]",
ShellComplete: autocomplete.AppNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)
}
if !internal.Chaos {
if err := app.Recipe.EnsureIsClean(); err != nil {
log.Fatal(err)
}
if !internal.Offline {
if err := app.Recipe.EnsureUpToDate(); err != nil {
log.Fatal(err)
}
}
if err := app.Recipe.EnsureLatest(); err != nil {
log.Fatal(err)
}
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
}
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
}
if err := internal.RunBackupCmdRemote(cl, "snapshots", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
}
return nil
},
}
var appBackupCommand = cli.Command{
Name: "backup",
Aliases: []string{"b"},
Usage: "Manage app backups",
UsageText: "abra app backup [command] [arguments] [options]",
Commands: []*cli.Command{
&appBackupListCommand,
&appBackupSnapshotsCommand,
&appBackupDownloadCommand,
&appBackupCreateCommand,
},
}

View File

@ -1,7 +1,6 @@
package app
import (
"context"
"fmt"
"coopcloud.tech/abra/cli/internal"
@ -10,15 +9,14 @@ import (
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/charmbracelet/lipgloss"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var appCheckCommand = cli.Command{
Name: "check",
Aliases: []string{"chk"},
UsageText: "abra app check <domain> [options]",
Usage: "Ensure an app is well configured",
Description: `Compare env vars in both the app ".env" and recipe ".env.sample" file.
var AppCheckCommand = &cobra.Command{
Use: "check <app> [flags]",
Aliases: []string{"chk"},
Short: "Ensure an app is well configured",
Long: `Compare env vars in both the app ".env" and recipe ".env.sample" file.
The goal is to ensure that recipe ".env.sample" env vars are defined in your
app ".env" file. Only env var definitions in the ".env.sample" which are
@ -28,15 +26,15 @@ these env vars, then "check" will complain.
Recipe maintainers may or may not provide defaults for env vars within their
recipes regardless of commenting or not (e.g. through the use of
${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
Flags: []cli.Flag{
internal.ChaosFlag,
internal.OfflineFlag,
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Before: internal.SubCommandBefore,
ShellComplete: autocomplete.AppNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
@ -74,7 +72,15 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
}
fmt.Println(table)
return nil
},
}
func init() {
AppCheckCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
}

View File

@ -1,67 +1,105 @@
package app
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"slices"
"sort"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var appCmdCommand = cli.Command{
Name: "command",
Aliases: []string{"cmd"},
Usage: "Run app commands",
UsageText: "abra app cmd <domain> [<service>] <cmd> [<cmd-args>] [options]",
Description: `Run an app specific command.
var AppCmdCommand = &cobra.Command{
Use: "command <app> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]",
Aliases: []string{"cmd"},
Short: "Run app commands",
Long: `Run an app specific command.
These commands are bash functions, defined in the abra.sh of the recipe itself.
They can be run within the context of a service (e.g. app) or locally on your
work station by passing "--local".`,
Flags: []cli.Flag{
internal.LocalCmdFlag,
internal.RemoteUserFlag,
internal.TtyFlag,
internal.ChaosFlag,
internal.NoInputFlag,
work station by passing "--local/-l".
N.B. If using the "--" style to pass arguments, flags (e.g. "--local/-l") must
be passed *before* the "--". It is possible to pass arguments without the "--"
as long as no dashes are present (i.e. "foo" works without "--", "-foo"
does not).`,
Example: ` # pass <cmd> args/flags without "--"
abra app cmd 1312.net app my_cmd_arg foo --user bar
# pass <cmd> args/flags with "--"
abra app cmd 1312.net app my_cmd_args --user bar -- foo -vvv
# drop the [service] arg if using "--local/-l"
abra app cmd 1312.net my_cmd --local`,
Args: func(cmd *cobra.Command, args []string) error {
if local {
if !(len(args) >= 2) {
return errors.New("requires at least 2 arguments with --local/-l")
}
if slices.Contains(os.Args, "--") {
if cmd.ArgsLenAtDash() > 2 {
return errors.New("accepts at most 2 args with --local/-l")
}
}
// NOTE(d1): it is unclear how to correctly validate this case
//
// abra app cmd 1312.net app test_cmd_args foo --local
// FATAL <recipe> doesn't have a app function
//
// "app" should not be there, but there is no reliable way to detect arg
// count when the user can pass an arbitrary amount of recipe command
// arguments
}
if !(len(args) >= 3) {
return errors.New("requires at least 3 arguments")
}
return nil
},
Before: internal.SubCommandBefore,
Commands: []*cli.Command{
&appCmdListCommand,
},
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
args := cmd.Args()
switch args.Len() {
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
autocomplete.AppNameComplete(ctx, cmd)
return autocomplete.AppNameComplete()
case 1:
autocomplete.ServiceNameComplete(args.Get(0))
if !local {
return autocomplete.ServiceNameComplete(args[0])
}
return autocomplete.CommandNameComplete(args[0])
case 2:
cmdNameComplete(args.Get(0))
if !local {
return autocomplete.CommandNameComplete(args[0])
}
return nil, cobra.ShellCompDirectiveDefault
default:
return nil, cobra.ShellCompDirectiveError
}
},
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
if internal.LocalCmd && internal.RemoteUser != "" {
internal.ShowSubcommandHelpAndError(cmd, errors.New("cannot use --local & --user together"))
if local && remoteUser != "" {
log.Fatal("cannot use --local & --user together")
}
hasCmdArgs, parsedCmdArgs := parseCmdArgs(cmd.Args().Slice(), internal.LocalCmd)
hasCmdArgs, parsedCmdArgs := parseCmdArgs(args, local)
if _, err := os.Stat(app.Recipe.AbraShPath); err != nil {
if os.IsNotExist(err) {
@ -70,12 +108,8 @@ work station by passing "--local".`,
log.Fatal(err)
}
if internal.LocalCmd {
if !(cmd.Args().Len() >= 2) {
internal.ShowSubcommandHelpAndError(cmd, errors.New("missing arguments"))
}
cmdName := cmd.Args().Get(1)
if local {
cmdName := args[1]
if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
log.Fatal(err)
}
@ -106,53 +140,78 @@ work station by passing "--local".`,
if err := internal.RunCmd(cmd); err != nil {
log.Fatal(err)
}
} else {
if !(cmd.Args().Len() >= 3) {
internal.ShowSubcommandHelpAndError(cmd, errors.New("missing arguments"))
}
targetServiceName := cmd.Args().Get(1)
return
}
cmdName := cmd.Args().Get(2)
if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
log.Fatal(err)
}
cmdName := args[2]
if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
log.Fatal(err)
}
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
if err != nil {
log.Fatal(err)
}
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
if err != nil {
log.Fatal(err)
}
matchingServiceName := false
for _, serviceName := range serviceNames {
if serviceName == targetServiceName {
matchingServiceName = true
}
}
if !matchingServiceName {
log.Fatalf("no service %s for %s?", targetServiceName, app.Name)
}
log.Debugf("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName)
if hasCmdArgs {
log.Debugf("parsed following command arguments: %s", parsedCmdArgs)
} else {
log.Debug("did not detect any command arguments")
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
if err := internal.RunCmdRemote(cl, app, app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs); err != nil {
log.Fatal(err)
matchingServiceName := false
targetServiceName := args[1]
for _, serviceName := range serviceNames {
if serviceName == targetServiceName {
matchingServiceName = true
}
}
return nil
if !matchingServiceName {
log.Fatalf("no service %s for %s?", targetServiceName, app.Name)
}
log.Debugf("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName)
if hasCmdArgs {
log.Debugf("parsed following command arguments: %s", parsedCmdArgs)
} else {
log.Debug("did not detect any command arguments")
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
if err := internal.RunCmdRemote(
cl,
app,
requestTTY,
app.Recipe.AbraShPath,
targetServiceName, cmdName, parsedCmdArgs, remoteUser); err != nil {
log.Fatal(err)
}
},
}
var AppCmdListCommand = &cobra.Command{
Use: "list <app> [flags]",
Aliases: []string{"ls"},
Short: "List all available commands",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath)
if err != nil {
log.Fatal(err)
}
sort.Strings(cmdNames)
for _, cmdName := range cmdNames {
fmt.Println(cmdName)
}
},
}
@ -175,73 +234,42 @@ func parseCmdArgs(args []string, isLocal bool) (bool, string) {
return hasCmdArgs, parsedCmdArgs
}
func cmdNameComplete(appName string) {
app, err := app.Get(appName)
if err != nil {
return
}
cmdNames, _ := getShCmdNames(app)
if err != nil {
return
}
for _, n := range cmdNames {
fmt.Println(n)
}
}
var appCmdListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List all available commands",
UsageText: "abra app cmd ls <domain> [options]",
Flags: []cli.Flag{
internal.ChaosFlag,
},
ShellComplete: autocomplete.AppNameComplete,
Before: internal.SubCommandBefore,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)
}
if !internal.Chaos {
if err := app.Recipe.EnsureIsClean(); err != nil {
log.Fatal(err)
}
if !internal.Offline {
if err := app.Recipe.EnsureUpToDate(); err != nil {
log.Fatal(err)
}
}
if err := app.Recipe.EnsureLatest(); err != nil {
log.Fatal(err)
}
}
cmdNames, err := getShCmdNames(app)
if err != nil {
log.Fatal(err)
}
for _, cmdName := range cmdNames {
fmt.Println(cmdName)
}
return nil
},
}
func getShCmdNames(app appPkg.App) ([]string, error) {
cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath)
if err != nil {
return nil, err
}
sort.Strings(cmdNames)
return cmdNames, nil
var (
local bool
remoteUser string
requestTTY bool
)
func init() {
AppCmdCommand.Flags().BoolVarP(
&local,
"local",
"l",
false,
"run command locally",
)
AppCmdCommand.Flags().StringVarP(
&remoteUser,
"user",
"u",
"",
"request remote user",
)
AppCmdCommand.Flags().BoolVarP(
&requestTTY,
"tty",
"t",
false,
"request remote TTY",
)
AppCmdCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
}

View File

@ -1,39 +1,35 @@
package app
import (
"context"
"errors"
"os"
"os/exec"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var appConfigCommand = cli.Command{
Name: "config",
Aliases: []string{"cfg"},
Usage: "Edit app config",
UsageText: "abra app config <domain> [options]",
Before: internal.SubCommandBefore,
ShellComplete: autocomplete.AppNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
appName := cmd.Args().First()
if appName == "" {
internal.ShowSubcommandHelpAndError(cmd, errors.New("no app provided"))
}
var AppConfigCommand = &cobra.Command{
Use: "config <app> [flags]",
Aliases: []string{"cfg"},
Short: "Edit app config",
Example: " abra config 1312.net",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
files, err := appPkg.LoadAppFiles("")
if err != nil {
log.Fatal(err)
}
appName := args[0]
appFile, exists := files[appName]
if !exists {
log.Fatalf("cannot find app with name %s", appName)
@ -57,7 +53,5 @@ var appConfigCommand = cli.Command{
if err := c.Run(); err != nil {
log.Fatal(err)
}
return nil
},
}

View File

@ -22,41 +22,39 @@ import (
dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/archive"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var appCpCommand = cli.Command{
Name: "cp",
Aliases: []string{"c"},
Before: internal.SubCommandBefore,
Usage: "Copy files to/from a deployed app service",
UsageText: "abra app cp <domain> <src> <dst> [options]",
Description: `Copy files to and from any app service file system.
var AppCpCommand = &cobra.Command{
Use: "cp <app> <src> <dst> [flags]",
Aliases: []string{"c"},
Short: "Copy files to/from a deployed app service",
Example: ` # copy myfile.txt to the root of the app service
abra app cp 1312.net myfile.txt app:/
If you want to copy a myfile.txt to the root of the app service:
# copy that file back to your current working directory locally
abra app cp 1312.net app:/myfile.txt`,
Args: cobra.ExactArgs(3),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
default:
return nil, cobra.ShellCompDirectiveDefault
}
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
abra app cp <domain> myfile.txt app:/
And if you want to copy that file back to your current working directory locally:
abra app cp <domain> app:/myfile.txt`,
ShellComplete: autocomplete.AppNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
src := cmd.Args().Get(1)
dst := cmd.Args().Get(2)
if src == "" {
log.Fatal("missing <src> argument")
}
if dst == "" {
log.Fatal("missing <dest> argument")
}
src := args[1]
dst := args[2]
srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst)
if err != nil {
log.Fatal(err)
@ -81,8 +79,6 @@ And if you want to copy that file back to your current working directory locally
if err != nil {
log.Fatal(err)
}
return nil
},
}

View File

@ -18,41 +18,60 @@ import (
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var appDeployCommand = cli.Command{
Name: "deploy",
Aliases: []string{"d"},
Usage: "Deploy an app",
UsageText: "abra app deploy <domain> [<version>] [options]",
Flags: []cli.Flag{
internal.ForceFlag,
internal.ChaosFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
},
Before: internal.SubCommandBefore,
Description: `Deploy an app.
var AppDeployCommand = &cobra.Command{
Use: "deploy <app> [version] [flags]",
Aliases: []string{"d"},
Short: "Deploy an app",
Long: `Deploy an app.
This command supports chaos operations. Use "--chaos" to deploy your recipe
checkout as-is. Recipe commit hashes are also supported values for
"[<version>]". Please note, "upgrade"/"rollback" do not support chaos
operations.`,
ShellComplete: autocomplete.AppNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
This command supports chaos operations. Use "--chaos/-c" to deploy your recipe
checkout as-is. Recipe commit hashes are also supported values for "[version]".
Please note, "upgrade"/"rollback" do not support chaos operations.`,
Example: ` # standard deployment
abra app deploy 1312.net
# chaos deployment
abra app deploy 1312.net --chaos
# deploy specific version
abra app deploy 1312.net 2.0.0+1.2.3
# deploy a specific git hash
abra app deploy 1312.net 886db76d`,
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
app, err := appPkg.Get(args[0])
if err != nil {
log.Debugf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveDefault
}
return autocomplete.RecipeVersionComplete(app.Recipe.Name)
default:
return nil, cobra.ShellCompDirectiveDefault
}
},
Run: func(cmd *cobra.Command, args []string) {
var warnMessages []string
app := internal.ValidateApp(cmd)
app := internal.ValidateApp(args)
stackName := app.StackName()
ok, err := validateChaosXORVersion(cmd.Args())
ok, err := validateChaosXORVersion(args)
if !ok {
log.Fatalf(err.Error())
}
specificVersion := getSpecifiedVersion(cmd.Args())
specificVersion := getSpecifiedVersion(args)
if specificVersion != "" {
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion)
@ -256,21 +275,54 @@ operations.`,
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
return nil
},
}
// This is not really xor since both can be absent
//
// but, I say we let it slide this time!
func validateChaosXORVersion(args cli.Args) (bool, error) {
// validateChaosXORVersion xor checks version/chaos mode
func validateChaosXORVersion(args []string) (bool, error) {
if getSpecifiedVersion(args) != "" && internal.Chaos {
return false, errors.New("cannot use <version> and --chaos together")
}
return true, nil
}
func getSpecifiedVersion(args cli.Args) string {
return args.Get(1)
// getSpecifiedVersion retrieves the specific version if available
func getSpecifiedVersion(args []string) string {
if len(args) >= 2 {
return args[1]
}
return ""
}
func init() {
AppDeployCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
AppDeployCommand.Flags().BoolVarP(
&internal.Force,
"force",
"f",
false,
"perform action without further prompt",
)
AppDeployCommand.Flags().BoolVarP(
&internal.NoDomainChecks,
"no-domain-checks",
"D",
false,
"disable public DNS checks",
)
AppDeployCommand.Flags().BoolVarP(
&internal.DontWaitConverge, "no-converge-checks",
"c",
false,
"do not wait for converge logic checks",
)
}

View File

@ -8,19 +8,19 @@ import (
func TestGetSpecificVersion(t *testing.T) {
tests := []struct {
input mockArgs
input []string
expectedOutput string
}{
// No specified version when command has one or less args
{mockArgs{}, ""},
{mockArgs{[]string{"arg0"}}, ""},
{[]string{}, ""},
{[]string{"arg0"}, ""},
// Second in arg (index-1) is the specified result when command has more than 1 args
{mockArgs{[]string{"arg0", "arg1"}}, "arg1"},
{mockArgs{[]string{"arg0", "arg1", "arg2"}}, "arg1"},
{[]string{"arg0", "arg1"}, "arg1"},
{[]string{"arg0", "arg1", "arg2"}, "arg1"},
}
for _, test := range tests {
if test.expectedOutput != getSpecifiedVersion(&test.input) {
if test.expectedOutput != getSpecifiedVersion(test.input) {
t.Fatalf("result for %s should be %s", test.input, test.expectedOutput)
}
}
@ -28,23 +28,23 @@ func TestGetSpecificVersion(t *testing.T) {
func TestValidateChaosXORVersion(t *testing.T) {
tests := []struct {
input mockArgs
input []string
isChaos bool
expectedResult bool
}{
// Chaos = true, Specified Version absent
{mockArgs{}, true, true},
{[]string{}, true, true},
// Chaos = false, Specified Version absent
{mockArgs{}, false, true},
{[]string{}, false, true},
// Chaos = true, Specified Version present
{mockArgs{[]string{"arg0", "arg1"}}, true, false},
{[]string{"arg0", "arg1"}, true, false},
// Chaos = false, Specified Version present
{mockArgs{[]string{"arg0", "arg1", "arg2"}}, false, true},
{[]string{"arg0", "arg1", "arg2"}, false, true},
}
for _, test := range tests {
internal.Chaos = test.isChaos
res, _ := validateChaosXORVersion(&test.input)
res, _ := validateChaosXORVersion(test.input)
if res != test.expectedResult {
t.Fatalf(
"When args are %s and Chaos mode is %t result needs to be %t",
@ -55,43 +55,3 @@ func TestValidateChaosXORVersion(t *testing.T) {
}
}
}
type mockArgs struct {
v []string
}
func (a *mockArgs) Get(n int) string {
if len(a.v) > n {
return a.v[n]
}
return ""
}
func (a *mockArgs) First() string {
return a.Get(0)
}
func (a *mockArgs) Tail() []string {
if a.Len() >= 2 {
tail := a.v[1:]
ret := make([]string, len(tail))
copy(ret, tail)
return ret
}
return []string{}
}
func (a *mockArgs) Len() int {
return len(a.v)
}
func (a *mockArgs) Present() bool {
return a.Len() != 0
}
func (a *mockArgs) Slice() []string {
ret := make([]string, len(a.v))
copy(ret, a.v)
return ret
}

View File

@ -1,7 +1,6 @@
package app
import (
"context"
"encoding/json"
"fmt"
"sort"
@ -10,42 +9,11 @@ import (
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/tagcmp"
"github.com/urfave/cli/v3"
)
var (
status bool
statusFlag = &cli.BoolFlag{
Name: "status",
Aliases: []string{"S"},
Usage: "Show app deployment status",
Destination: &status,
}
)
var (
recipeFilter string
recipeFlag = &cli.StringFlag{
Name: "recipe",
Aliases: []string{"r"},
Value: "",
Usage: "Show apps of a specific recipe",
Destination: &recipeFilter,
}
)
var (
listAppServer string
listAppServerFlag = &cli.StringFlag{
Name: "server",
Aliases: []string{"s"},
Value: "",
Usage: "Show apps of a specific server",
Destination: &listAppServer,
}
"github.com/spf13/cobra"
)
type appStatus struct {
@ -70,25 +38,23 @@ type serverStatus struct {
UpgradeCount int `json:"upgradeCount"`
}
var appListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List all managed apps",
UsageText: "abra app list [options]",
Description: `Generate a report of all managed apps.
var AppListCommand = &cobra.Command{
Use: "list [flags]",
Aliases: []string{"ls"},
Short: "List all managed apps",
Long: `Generate a report of all managed apps.
By passing the "--status/-S" flag, you can query all your servers for the
actual live deployment status. Depending on how many servers you manage, this
can take some time.`,
Flags: []cli.Flag{
internal.MachineReadableFlag,
statusFlag,
listAppServerFlag,
recipeFlag,
},
Before: internal.SubCommandBefore,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
Use "--status/-S" flag to query all servers for the live deployment status.`,
Example: ` # list apps of all servers without live status
abra app ls
# list apps of a specific server with live status
abra app ls -s 1312.net -S
# list apps of all servers which match a specific recipe
abra app ls -r gitea`,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
appFiles, err := appPkg.LoadAppFiles(listAppServer)
if err != nil {
log.Fatal(err)
@ -230,7 +196,8 @@ can take some time.`,
} else {
fmt.Println(string(jsonstring))
}
return nil
return
}
alreadySeen := make(map[string]bool)
@ -318,7 +285,59 @@ can take some time.`,
totalApps := formatter.BoldStyle.Render("TOTAL APPS")
log.Infof("%s: %v | %s: %v ", totalServers, totalServersCount, totalApps, totalAppsCount)
}
return nil
},
}
var (
status bool
recipeFilter string
listAppServer string
)
func init() {
AppListCommand.Flags().BoolVarP(
&status,
"status",
"S",
false,
"show app deployment status",
)
AppListCommand.Flags().StringVarP(
&recipeFilter,
"recipe",
"r",
"",
"show apps of a specific recipe",
)
AppListCommand.RegisterFlagCompletionFunc(
"recipe",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
)
AppListCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
false,
"print machine-readable output",
)
AppListCommand.Flags().StringVarP(
&listAppServer,
"server",
"s",
"",
"show apps of a specific server",
)
AppListCommand.RegisterFlagCompletionFunc(
"server",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.ServerNameComplete()
},
)
}

View File

@ -19,23 +19,34 @@ import (
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
dockerClient "github.com/docker/docker/client"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var appLogsCommand = cli.Command{
Name: "logs",
Aliases: []string{"l"},
Usage: "Tail app logs",
UsageText: "abra app logs <domain> [<service>] [options]",
Flags: []cli.Flag{
internal.StdErrOnlyFlag,
internal.SinceLogsFlag,
var AppLogsCommand = &cobra.Command{
Use: "logs <app> [service] [flags]",
Aliases: []string{"l"},
Short: "Tail app logs",
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
app, err := appPkg.Get(args[0])
if err != nil {
log.Debugf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveDefault
}
return autocomplete.ServiceNameComplete(app.Name)
default:
return nil, cobra.ShellCompDirectiveDefault
}
},
Before: internal.SubCommandBefore,
ShellComplete: autocomplete.AppNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
stackName := app.StackName()
if err := app.Recipe.EnsureExists(); err != nil {
@ -56,17 +67,14 @@ var appLogsCommand = cli.Command{
log.Fatalf("%s is not deployed?", app.Name)
}
serviceName := cmd.Args().Get(1)
serviceNames := []string{}
if serviceName != "" {
serviceNames = []string{serviceName}
}
err = tailLogs(cl, app, serviceNames)
if err != nil {
log.Fatal(err)
var serviceNames []string
if len(args) == 2 {
serviceNames = []string{args[1]}
}
return nil
if err = tailLogs(cl, app, serviceNames); err != nil {
log.Fatal(err)
}
},
}
@ -112,8 +120,8 @@ func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) er
go func(serviceID string) {
logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{
ShowStderr: true,
ShowStdout: !internal.StdErrOnly,
Since: internal.SinceLogs,
ShowStdout: !stdErr,
Since: sinceLogs,
Until: "",
Timestamps: true,
Follow: true,
@ -137,3 +145,26 @@ func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) er
return nil
}
var (
stdErr bool
sinceLogs string
)
func init() {
AppLogsCommand.Flags().BoolVarP(
&stdErr,
"stderr",
"s",
false,
"only tail stderr",
)
AppLogsCommand.Flags().StringVarP(
&sinceLogs,
"since",
"S",
"",
"tail logs since YYYY-MM-DDTHH:MM:SSZ",
)
}

View File

@ -1,7 +1,6 @@
package app
import (
"context"
"fmt"
"coopcloud.tech/abra/cli/internal"
@ -17,7 +16,7 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/lipgloss/table"
dockerClient "github.com/docker/docker/client"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var appNewDescription = `Creates a new app from a default recipe.
@ -26,12 +25,12 @@ This new app configuration is stored in your $ABRA_DIR directory under the
appropriate server.
This command does not deploy your app for you. You will need to run "abra app
deploy <domain>" to do so.
deploy <app>" 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".
Recipe commit hashes are supported values for "[<version>]".
Recipe commit hashes are supported values for "[version]".
Passing the "--secrets/-S" flag will automatically generate secrets for your
app and store them encrypted at rest on the chosen target server. These
@ -42,32 +41,28 @@ You can use the "--pass/-P" to store these generated passwords locally in a
pass store (see passwordstore.org for more). The pass command must be available
on your $PATH.`
var appNewCommand = cli.Command{
Name: "new",
Aliases: []string{"n"},
Usage: "Create a new app",
UsageText: "abra app new [<recipe>] [<version>] [options]",
Description: appNewDescription,
Flags: []cli.Flag{
internal.NewAppServerFlag,
internal.DomainFlag,
internal.PassFlag,
internal.SecretsFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
HideHelp: true,
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
args := cmd.Args()
switch args.Len() {
var AppNewCommand = &cobra.Command{
Use: "new [recipe] [version] [flags]",
Aliases: []string{"n"},
Short: "Create a new app",
Long: appNewDescription,
Args: cobra.RangeArgs(0, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
autocomplete.RecipeNameComplete(ctx, cmd)
return autocomplete.RecipeNameComplete()
case 1:
autocomplete.RecipeVersionComplete(cmd.Args().Get(0))
recipe := internal.ValidateRecipe(args, cmd.Name())
return autocomplete.RecipeVersionComplete(recipe.Name)
default:
return nil, cobra.ShellCompDirectiveDefault
}
},
Action: func(ctx context.Context, cmd *cli.Command) error {
recipe := internal.ValidateRecipe(cmd)
Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name())
var version string
if !internal.Chaos {
@ -80,7 +75,12 @@ var appNewCommand = cli.Command{
}
}
if cmd.Args().Get(1) == "" {
var recipeVersion string
if len(args) == 2 {
recipeVersion = args[1]
}
if recipeVersion == "" {
recipeVersions, err := recipe.GetRecipeVersions()
if err != nil {
log.Fatal(err)
@ -101,8 +101,7 @@ var appNewCommand = cli.Command{
}
}
} else {
version = cmd.Args().Get(1)
if _, err := recipe.EnsureVersion(version); err != nil {
if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
log.Fatal(err)
}
}
@ -112,25 +111,25 @@ var appNewCommand = cli.Command{
log.Fatal(err)
}
if err := ensureDomainFlag(recipe, internal.NewAppServer); err != nil {
if err := ensureDomainFlag(recipe, newAppServer); err != nil {
log.Fatal(err)
}
sanitisedAppName := appPkg.SanitiseAppName(internal.Domain)
log.Debugf("%s sanitised as %s for new app", internal.Domain, sanitisedAppName)
sanitisedAppName := appPkg.SanitiseAppName(appDomain)
log.Debugf("%s sanitised as %s for new app", appDomain, sanitisedAppName)
if err := appPkg.TemplateAppEnvSample(
recipe,
internal.Domain,
internal.NewAppServer,
internal.Domain,
appDomain,
newAppServer,
appDomain,
); err != nil {
log.Fatal(err)
}
var secrets AppSecrets
var appSecrets AppSecrets
var secretsTable *table.Table
if internal.Secrets {
if generateSecrets {
sampleEnv, err := recipe.SampleEnv()
if err != nil {
log.Fatal(err)
@ -141,21 +140,25 @@ var appNewCommand = cli.Command{
log.Fatal(err)
}
secretsConfig, err := secret.ReadSecretsConfig(recipe.SampleEnvPath, composeFiles, appPkg.StackName(internal.Domain))
secretsConfig, err := secret.ReadSecretsConfig(
recipe.SampleEnvPath,
composeFiles,
appPkg.StackName(appDomain),
)
if err != nil {
return err
log.Fatal(err)
}
if err := promptForSecrets(recipe.Name, secretsConfig); err != nil {
log.Fatal(err)
}
cl, err := client.New(internal.NewAppServer)
cl, err := client.New(newAppServer)
if err != nil {
log.Fatal(err)
}
secrets, err = createSecrets(cl, secretsConfig, sanitisedAppName)
appSecrets, err = createSecrets(cl, secretsConfig, sanitisedAppName)
if err != nil {
log.Fatal(err)
}
@ -168,13 +171,13 @@ var appNewCommand = cli.Command{
headers := []string{"NAME", "VALUE"}
secretsTable.Headers(headers...)
for name, val := range secrets {
for name, val := range appSecrets {
secretsTable.Row(name, val)
}
}
if internal.NewAppServer == "default" {
internal.NewAppServer = "local"
if newAppServer == "default" {
newAppServer = "local"
}
table, err := formatter.CreateTable()
@ -185,7 +188,7 @@ var appNewCommand = cli.Command{
headers := []string{"SERVER", "DOMAIN", "RECIPE", "VERSION"}
table.Headers(headers...)
table.Row(internal.NewAppServer, internal.Domain, recipe.Name, version)
table.Row(newAppServer, appDomain, recipe.Name, version)
log.Infof("new app '%s' created 🌞", recipe.Name)
@ -194,13 +197,13 @@ var appNewCommand = cli.Command{
fmt.Println("")
fmt.Println("Configure this app:")
fmt.Println(fmt.Sprintf("\n abra app config %s", internal.Domain))
fmt.Println(fmt.Sprintf("\n abra app config %s", appDomain))
fmt.Println("")
fmt.Println("Deploy this app:")
fmt.Println(fmt.Sprintf("\n abra app deploy %s", internal.Domain))
fmt.Println(fmt.Sprintf("\n abra app deploy %s", appDomain))
if len(secrets) > 0 {
if len(appSecrets) > 0 {
fmt.Println("")
fmt.Println("Generated secrets:")
fmt.Println("")
@ -213,7 +216,7 @@ var appNewCommand = cli.Command{
)
}
app, err := app.Get(internal.Domain)
app, err := app.Get(appDomain)
if err != nil {
log.Fatal(err)
}
@ -222,8 +225,6 @@ var appNewCommand = cli.Command{
if err := app.WriteRecipeVersion(version, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
return nil
},
}
@ -238,19 +239,19 @@ func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secr
sanitisedAppName = sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH]
}
secrets, err := secret.GenerateSecrets(cl, secretsConfig, internal.NewAppServer)
secrets, err := secret.GenerateSecrets(cl, secretsConfig, newAppServer)
if err != nil {
return nil, err
}
if internal.Pass {
if saveInPass {
for secretName := range secrets {
secretValue := secrets[secretName]
if err := secret.PassInsertSecret(
secretValue,
secretName,
internal.Domain,
internal.NewAppServer,
appDomain,
newAppServer,
); err != nil {
return nil, err
}
@ -262,17 +263,17 @@ func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secr
// ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
func ensureDomainFlag(recipe recipePkg.Recipe, server string) error {
if internal.Domain == "" && !internal.NoInput {
if appDomain == "" && !internal.NoInput {
prompt := &survey.Input{
Message: "Specify app domain",
Default: fmt.Sprintf("%s.%s", recipe.Name, server),
}
if err := survey.AskOne(prompt, &internal.Domain); err != nil {
if err := survey.AskOne(prompt, &appDomain); err != nil {
return err
}
}
if internal.Domain == "" {
if appDomain == "" {
return fmt.Errorf("no domain provided")
}
@ -286,11 +287,11 @@ func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret)
return nil
}
if !internal.Secrets && !internal.NoInput {
if !generateSecrets && !internal.NoInput {
prompt := &survey.Confirm{
Message: "Generate app secrets?",
}
if err := survey.AskOne(prompt, &internal.Secrets); err != nil {
if err := survey.AskOne(prompt, &generateSecrets); err != nil {
return err
}
}
@ -305,19 +306,76 @@ func ensureServerFlag() error {
return err
}
if internal.NewAppServer == "" && !internal.NoInput {
if newAppServer == "" && !internal.NoInput {
prompt := &survey.Select{
Message: "Select app server:",
Options: servers,
}
if err := survey.AskOne(prompt, &internal.NewAppServer); err != nil {
if err := survey.AskOne(prompt, &newAppServer); err != nil {
return err
}
}
if internal.NewAppServer == "" {
if newAppServer == "" {
return fmt.Errorf("no server provided")
}
return nil
}
var (
newAppServer string
appDomain string
saveInPass bool
generateSecrets bool
)
func init() {
AppNewCommand.Flags().StringVarP(
&newAppServer,
"server",
"s",
"",
"specify server for new app",
)
AppNewCommand.RegisterFlagCompletionFunc(
"server",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.ServerNameComplete()
},
)
AppNewCommand.Flags().StringVarP(
&appDomain,
"domain",
"D",
"",
"domain name for app",
)
AppNewCommand.Flags().BoolVarP(
&saveInPass,
"pass",
"p",
false,
"store secrets in a local pass store",
)
AppNewCommand.Flags().BoolVarP(
&generateSecrets,
"secrets",
"S",
false,
"automatically generate secrets",
)
AppNewCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
}

View File

@ -18,25 +18,23 @@ import (
containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var appPsCommand = cli.Command{
Name: "ps",
Aliases: []string{"p"},
Usage: "Check app status",
UsageText: "abra app ps <domain> [options]",
Description: "Show status of a deployed app.",
Flags: []cli.Flag{
internal.MachineReadableFlag,
internal.ChaosFlag,
internal.OfflineFlag,
var AppPsCommand = &cobra.Command{
Use: "ps <app> [flags]",
Aliases: []string{"p"},
Short: "Check app status",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Before: internal.SubCommandBefore,
ShellComplete: autocomplete.AppNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -67,8 +65,6 @@ var appPsCommand = cli.Command{
}
showPSOutput(app, cl, deployMeta.Version, chaosVersion)
return nil
},
}
@ -175,3 +171,21 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
log.Infof("VERSION: %s CHAOS: %s", deployedVersion, chaosVersion)
}
func init() {
AppPsCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
false,
"print machine-readable output",
)
AppPsCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
}

View File

@ -12,15 +12,14 @@ import (
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var appRemoveCommand = cli.Command{
Name: "remove",
Aliases: []string{"rm"},
UsageText: "abra app remove <domain> [options]",
Usage: "Remove all app data, locally and remotely",
Description: `Remove everything related to an app which is already undeployed.
var AppRemoveCommand = &cobra.Command{
Use: "remove <app> [flags]",
Aliases: []string{"rm"},
Short: "Remove all app data, locally and remotely",
Long: `Remove everything related to an app which is already undeployed.
By default, it will prompt for confirmation before proceeding. All secrets,
volumes and the local app env file will be deleted.
@ -36,17 +35,19 @@ secrets first, Abra will *not* be able to help you remove them afterwards.
To delete everything without prompt, use the "--force/-f" or the "--no-input/n"
flag.`,
Flags: []cli.Flag{
internal.ForceFlag,
Example: " abra app remove 1312.net",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
ShellComplete: autocomplete.AppNameComplete,
Before: internal.SubCommandBefore,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if !internal.Force && !internal.NoInput {
log.Warnf("ALERTA ALERTA: this will completely remove %s data and config locally and remotely", app.Name)
log.Warnf("ALERTA ALERTA: deleting %s data and config (local/remote)", app.Name)
response := false
prompt := &survey.Confirm{Message: "are you sure?"}
@ -129,7 +130,15 @@ flag.`,
}
log.Info(fmt.Sprintf("file: %s removed", app.Path))
return nil
},
}
func init() {
AppRemoveCommand.Flags().BoolVarP(
&internal.Force,
"force",
"f",
false,
"perform action without further prompt",
)
}

View File

@ -2,7 +2,6 @@ package app
import (
"context"
"errors"
"fmt"
"coopcloud.tech/abra/cli/internal"
@ -12,43 +11,62 @@ import (
"coopcloud.tech/abra/pkg/log"
upstream "coopcloud.tech/abra/pkg/upstream/service"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var appRestartCommand = cli.Command{
Name: "restart",
Aliases: []string{"re"},
Usage: "Restart an app",
UsageText: "abra app restart <domain> [<service>] [options]",
Flags: []cli.Flag{
internal.AllServicesFlag,
},
Before: internal.SubCommandBefore,
Description: `This command restarts services within a deployed app.
var AppRestartCommand = &cobra.Command{
Use: "restart <app> [[service] | --all-services] [flags]",
Aliases: []string{"re"},
Short: "Restart an app",
Long: `This command restarts services within a deployed app.
Run "abra app ps <domain>" to see a list of service names.
Run "abra app ps <app>" to see a list of service names.
Pass "--all-services/-a" to restart all services.`,
ShellComplete: autocomplete.AppNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
Example: ` # restart a single app service
abra app restart 1312.net app
# restart all app services
abra app restart 1312.net -a`,
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
if !allServices {
return autocomplete.ServiceNameComplete(args[0])
}
return nil, cobra.ShellCompDirectiveDefault
default:
return nil, cobra.ShellCompDirectiveError
}
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(false, false); err != nil {
log.Fatal(err)
}
serviceName := cmd.Args().Get(1)
if serviceName == "" && !internal.AllServices {
err := errors.New("missing <service>")
internal.ShowSubcommandHelpAndError(cmd, err)
var serviceName string
if len(args) == 2 {
serviceName = args[1]
}
if serviceName != "" && internal.AllServices {
log.Fatal("cannot use <service> and --all-services together")
if serviceName == "" && !allServices {
log.Fatal("missing [service]")
}
if serviceName != "" && allServices {
log.Fatal("cannot use [service] and --all-services/-a together")
}
var serviceNames []string
if internal.AllServices {
if allServices {
var err error
serviceNames, err = appPkg.GetAppServiceNames(app.Name)
if err != nil {
@ -99,7 +117,17 @@ Pass "--all-services/-a" to restart all services.`,
log.Debugf("%s has been scaled to 1", stackServiceName)
log.Infof("%s service successfully restarted", serviceName)
}
return nil
},
}
var allServices bool
func init() {
AppRestartCommand.Flags().BoolVarP(
&allServices,
"all-services",
"a",
false,
"restart all services",
)
}

View File

@ -1,65 +0,0 @@
package app
import (
"context"
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli/v3"
)
var targetPath string
var targetPathFlag = &cli.StringFlag{
Name: "target",
Aliases: []string{"t"},
Usage: "Target path",
Destination: &targetPath,
}
var appRestoreCommand = cli.Command{
Name: "restore",
Aliases: []string{"rs"},
Usage: "Restore an app backup",
UsageText: "abra app restore <domain> <service> [options]",
Flags: []cli.Flag{
targetPathFlag,
},
Before: internal.SubCommandBefore,
ShellComplete: autocomplete.AppNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
}
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
}
if targetPath != "" {
log.Debugf("including TARGET=%s in backupbot exec invocation", targetPath)
execEnv = append(execEnv, fmt.Sprintf("TARGET=%s", targetPath))
}
if err := internal.RunBackupCmdRemote(cl, "restore", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
}
return nil
},
}

View File

@ -16,36 +16,55 @@ import (
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var appRollbackCommand = cli.Command{
Name: "rollback",
Aliases: []string{"rl"},
Usage: "Roll an app back to a previous version",
UsageText: "abra app rollback <domain> [<version>] [options]",
Flags: []cli.Flag{
internal.ForceFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
},
Before: internal.SubCommandBefore,
Description: `This command rolls an app back to a previous version.
var AppRollbackCommand = &cobra.Command{
Use: "rollback <app> [version] [flags]",
Aliases: []string{"rl"},
Short: "Roll an app back to a previous version",
Long: `This command rolls an app back to a previous version.
Unlike "deploy", chaos operations are not supported here. Only recipe versions
are supported values for "[<version>]".
A rollback can be destructive, please ensure you have a copy of your app data
beforehand.`,
ShellComplete: autocomplete.AppNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
Example: ` # standard rollback
abra app rollback 1312.net
# rollback to specific version
abra app rollback 1312.net 2.0.0+1.2.3`,
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
app, err := appPkg.Get(args[0])
if err != nil {
log.Debugf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveDefault
}
return autocomplete.RecipeVersionComplete(app.Recipe.Name)
default:
return nil, cobra.ShellCompDirectiveError
}
},
Run: func(cmd *cobra.Command, args []string) {
var warnMessages []string
app := internal.ValidateApp(cmd)
app := internal.ValidateApp(args)
stackName := app.StackName()
specificVersion := cmd.Args().Get(1)
var specificVersion string
if len(args) == 2 {
specificVersion = args[1]
}
if specificVersion != "" {
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion)
app.Recipe.Version = specificVersion
@ -131,7 +150,7 @@ beforehand.`,
if len(availableDowngrades) == 0 && !internal.Force {
log.Info("no available downgrades")
return nil
return
}
}
@ -152,7 +171,7 @@ beforehand.`,
}
if err := survey.AskOne(prompt, &chosenDowngrade); err != nil {
return err
return
}
}
}
@ -220,7 +239,30 @@ beforehand.`,
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
return nil
},
}
func init() {
AppRollbackCommand.Flags().BoolVarP(
&internal.Force,
"force",
"f",
false,
"perform action without further prompt",
)
AppRollbackCommand.Flags().BoolVarP(
&internal.NoDomainChecks,
"no-domain-checks",
"D",
false,
"disable public DNS checks",
)
AppRollbackCommand.Flags().BoolVarP(
&internal.DontWaitConverge, "no-converge-checks",
"c",
false,
"do not wait for converge logic checks",
)
}

View File

@ -2,7 +2,6 @@ package app
import (
"context"
"errors"
"fmt"
"coopcloud.tech/abra/cli/internal"
@ -14,54 +13,48 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var user string
var userFlag = &cli.StringFlag{
Name: "user",
Aliases: []string{"u"},
Value: "",
Destination: &user,
}
var noTTY bool
var noTTYFlag = &cli.BoolFlag{
Name: "no-tty",
Aliases: []string{"t"},
Destination: &noTTY,
}
var appRunCommand = cli.Command{
Name: "run",
var AppRunCommand = &cobra.Command{
Use: "run <app> <service> <cmd> [[args] [flags] | [flags] -- [args]]",
Aliases: []string{"r"},
Flags: []cli.Flag{
noTTYFlag,
userFlag,
Short: "Run a command inside a service container",
Example: ` # run <cmd> with args/flags
abra app run 1312.net app -- ls -lha
# run <cmd> without args/flags
abra app run 1312.net app bash --user nobody
# run <cmd> with both kinds of args/flags
abra app run 1312.net app --user nobody -- ls -lha`,
Args: cobra.MinimumNArgs(3),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
return autocomplete.ServiceNameComplete(args[0])
case 2:
return autocomplete.CommandNameComplete(args[0])
default:
return nil, cobra.ShellCompDirectiveError
}
},
Before: internal.SubCommandBefore,
Usage: "Run a command in an app service",
UsageText: "abra app run <domain> <service> <args> [options]",
ShellComplete: autocomplete.AppNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if cmd.Args().Len() < 2 {
internal.ShowSubcommandHelpAndError(cmd, errors.New("no <service> provided?"))
}
if cmd.Args().Len() < 3 {
internal.ShowSubcommandHelpAndError(cmd, errors.New("no <args> provided?"))
}
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
serviceName := cmd.Args().Get(1)
serviceName := args[1]
stackAndServiceName := fmt.Sprintf("^%s_%s", app.StackName(), serviceName)
filters := filters.NewArgs()
filters.Add("name", stackAndServiceName)
@ -70,24 +63,23 @@ var appRunCommand = cli.Command{
log.Fatal(err)
}
c := cmd.Args().Slice()[2:]
userCmd := args[2:]
execCreateOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: c,
Cmd: userCmd,
Detach: false,
Tty: true,
}
if user != "" {
execCreateOpts.User = user
if runAsUser != "" {
execCreateOpts.User = runAsUser
}
if noTTY {
execCreateOpts.Tty = false
}
// FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli()
if err != nil {
log.Fatal(err)
@ -96,7 +88,27 @@ var appRunCommand = cli.Command{
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
log.Fatal(err)
}
return nil
},
}
var (
noTTY bool
runAsUser string
)
func init() {
AppRunCommand.Flags().BoolVarP(&noTTY,
"no-tty",
"t",
false,
"do not request a TTY",
)
AppRunCommand.Flags().StringVarP(
&runAsUser,
"user",
"u",
"",
"run command as user",
)
}

View File

@ -2,7 +2,6 @@ package app
import (
"context"
"errors"
"fmt"
"os"
"strconv"
@ -17,57 +16,45 @@ import (
"coopcloud.tech/abra/pkg/secret"
"github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var (
allSecrets bool
allSecretsFlag = &cli.BoolFlag{
Name: "all",
Aliases: []string{"a"},
Destination: &allSecrets,
Usage: "Generate all secrets",
}
)
var (
rmAllSecrets bool
rmAllSecretsFlag = &cli.BoolFlag{
Name: "all",
Aliases: []string{"a"},
Destination: &rmAllSecrets,
Usage: "Remove all secrets",
}
)
var appSecretGenerateCommand = cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate secrets",
UsageText: "abra app secret generate <domain> <secret> <version> [options]",
Flags: []cli.Flag{
allSecretsFlag,
internal.PassFlag,
internal.MachineReadableFlag,
internal.ChaosFlag,
var AppSecretGenerateCommand = &cobra.Command{
Use: "generate <app> [[secret] [version] | --all] [flags]",
Aliases: []string{"g"},
Short: "Generate secrets",
Args: cobra.RangeArgs(1, 3),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
app, err := appPkg.Get(args[0])
if err != nil {
log.Debugf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveDefault
}
return autocomplete.SecretComplete(app.Recipe.Name)
default:
return nil, cobra.ShellCompDirectiveDefault
}
},
Before: internal.SubCommandBefore,
ShellComplete: autocomplete.AppNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
if cmd.Args().Len() == 1 && !allSecrets {
err := errors.New("missing arguments <secret>/<version> or '--all'")
internal.ShowSubcommandHelpAndError(cmd, err)
if len(args) == 1 && !generateAllSecrets {
log.Fatal("missing arguments [secret]/[version] or '--all'")
}
if cmd.Args().Get(1) != "" && allSecrets {
err := errors.New("cannot use '<secret> <version>' and '--all' together")
internal.ShowSubcommandHelpAndError(cmd, err)
if len(args) > 1 && generateAllSecrets {
log.Fatal("cannot use '[secret] [version]' and '--all' together")
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
@ -80,9 +67,9 @@ var appSecretGenerateCommand = cli.Command{
log.Fatal(err)
}
if !allSecrets {
secretName := cmd.Args().Get(1)
secretVersion := cmd.Args().Get(2)
if !generateAllSecrets {
secretName := args[1]
secretVersion := args[2]
s, ok := secrets[secretName]
if !ok {
log.Fatalf("%s doesn't exist in the env config?", secretName)
@ -103,7 +90,7 @@ var appSecretGenerateCommand = cli.Command{
log.Fatal(err)
}
if internal.Pass {
if storeInPass {
for name, data := range secretVals {
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
log.Fatal(err)
@ -137,7 +124,7 @@ var appSecretGenerateCommand = cli.Command{
log.Fatal("unable to render to JSON: %s", err)
}
fmt.Println(out)
return nil
return
}
fmt.Println(table)
@ -147,50 +134,54 @@ var appSecretGenerateCommand = cli.Command{
formatter.BoldStyle.Render("NOT"),
formatter.BoldStyle.Render("NOW"),
)
return nil
},
}
var appSecretInsertCommand = cli.Command{
Name: "insert",
Aliases: []string{"i"},
Usage: "Insert secret",
UsageText: "abra app secret insert <domain> <secret> <version> <data> [options]",
Flags: []cli.Flag{
internal.PassFlag,
internal.FileFlag,
internal.TrimFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
ShellComplete: autocomplete.AppNameComplete,
HideHelpCommand: true,
Description: `This command inserts a secret into an app environment.
var AppSecretInsertCommand = &cobra.Command{
Use: "insert <app> <secret> <version> <data> [flags]",
Aliases: []string{"i"},
Short: "Insert secret",
Long: `This command inserts a secret into an app environment.
This can be useful when you want to manually generate secrets for an app
environment. Typically, you can let Abra generate them for you on app creation
(see "abra app new --secrets" for more).`,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
(see "abra app new --secrets/-S" for more).`,
Args: cobra.MinimumNArgs(4),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
app, err := appPkg.Get(args[0])
if err != nil {
log.Debugf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveDefault
}
return autocomplete.SecretComplete(app.Recipe.Name)
default:
return nil, cobra.ShellCompDirectiveDefault
}
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
if cmd.Args().Len() != 4 {
internal.ShowSubcommandHelpAndError(cmd, errors.New("missing arguments?"))
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
name := cmd.Args().Get(1)
version := cmd.Args().Get(2)
data := cmd.Args().Get(3)
name := args[1]
version := args[2]
data := args[3]
if internal.File {
if insertFromFile {
raw, err := os.ReadFile(data)
if err != nil {
log.Fatalf("reading secret from file: %s", err)
@ -198,7 +189,7 @@ environment. Typically, you can let Abra generate them for you on app creation
data = string(raw)
}
if internal.Trim {
if trimInput {
data = strings.TrimSpace(data)
}
@ -209,13 +200,11 @@ environment. Typically, you can let Abra generate them for you on app creation
log.Infof("%s successfully stored on server", secretName)
if internal.Pass {
if storeInPass {
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
log.Fatal(err)
}
}
return nil
},
}
@ -227,7 +216,7 @@ func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string
log.Infof("deleted %s successfully from server", secretName)
if internal.PassRemove {
if removeFromPass {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
return err
}
@ -238,29 +227,35 @@ func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string
return nil
}
var appSecretRmCommand = cli.Command{
Name: "remove",
Aliases: []string{"rm"},
Usage: "Remove a secret",
UsageText: "abra app remove <domainabra app remove <domain> [options]",
Flags: []cli.Flag{
internal.NoInputFlag,
rmAllSecretsFlag,
internal.PassRemoveFlag,
internal.OfflineFlag,
internal.ChaosFlag,
var AppSecretRmCommand = &cobra.Command{
Use: "remove <app> [[secret] | --all] [flags]",
Aliases: []string{"rm"},
Short: "Remove a secret",
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
if !rmAllSecrets {
app, err := appPkg.Get(args[0])
if err != nil {
log.Debugf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveDefault
}
return autocomplete.SecretComplete(app.Recipe.Name)
}
return nil, cobra.ShellCompDirectiveDefault
default:
return nil, cobra.ShellCompDirectiveError
}
},
Before: internal.SubCommandBefore,
ShellComplete: autocomplete.AppNameComplete,
Description: `
This command removes app secrets.
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
Example:
abra app secret remove myapp db_pass`,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -275,12 +270,12 @@ Example:
log.Fatal(err)
}
if cmd.Args().Get(1) != "" && rmAllSecrets {
internal.ShowSubcommandHelpAndError(cmd, errors.New("cannot use '<secret-name>' and '--all' together"))
if len(args) == 2 && rmAllSecrets {
log.Fatal("cannot use [secret] and --all/-a together")
}
if cmd.Args().Get(1) == "" && !rmAllSecrets {
internal.ShowSubcommandHelpAndError(cmd, errors.New("no secret(s) specified?"))
if len(args) != 2 && !rmAllSecrets {
log.Fatal("no secret(s) specified?")
}
cl, err := client.New(app.Server)
@ -303,8 +298,12 @@ Example:
remoteSecretNames[cont.Spec.Annotations.Name] = true
}
var secretToRm string
if len(args) == 2 {
secretToRm = args[1]
}
match := false
secretToRm := cmd.Args().Get(1)
for secretName, val := range secrets {
secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, val.Version)
if _, ok := remoteSecretNames[secretRemoteName]; ok {
@ -314,7 +313,7 @@ Example:
log.Fatal(err)
}
return nil
return
}
} else {
match = true
@ -333,26 +332,23 @@ Example:
if !match {
log.Fatal("no secrets to remove?")
}
return nil
},
}
var appSecretLsCommand = cli.Command{
Name: "list",
var AppSecretLsCommand = &cobra.Command{
Use: "list <app>",
Aliases: []string{"ls"},
Flags: []cli.Flag{
internal.OfflineFlag,
internal.ChaosFlag,
internal.MachineReadableFlag,
Short: "List all secrets",
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Before: internal.SubCommandBefore,
Usage: "List all secrets",
UsageText: "abra app secret list [options]",
HideHelp: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -395,28 +391,126 @@ var appSecretLsCommand = cli.Command{
log.Fatal("unable to render to JSON: %s", err)
}
fmt.Println(out)
return nil
return
}
fmt.Println(table)
return nil
return
}
log.Warnf("no secrets stored for %s", app.Name)
return nil
},
}
var appSecretCommand = cli.Command{
Name: "secret",
Aliases: []string{"s"},
Usage: "Manage app secrets",
UsageText: "abra app secret [command] [arguments] [options]",
Commands: []*cli.Command{
&appSecretGenerateCommand,
&appSecretInsertCommand,
&appSecretRmCommand,
&appSecretLsCommand,
},
var AppSecretCommand = &cobra.Command{
Use: "secret [cmd] [args] [flags]",
Aliases: []string{"s"},
Short: "Manage app secrets",
}
var (
storeInPass bool
insertFromFile bool
trimInput bool
rmAllSecrets bool
generateAllSecrets bool
removeFromPass bool
)
func init() {
AppSecretGenerateCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
false,
"print machine-readable output",
)
AppSecretGenerateCommand.Flags().BoolVarP(
&storeInPass,
"pass",
"p",
false,
"store generated secrets in a local pass store",
)
AppSecretGenerateCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
AppSecretInsertCommand.Flags().BoolVarP(
&storeInPass,
"pass",
"p",
false,
"store generated secrets in a local pass store",
)
AppSecretInsertCommand.Flags().BoolVarP(
&insertFromFile,
"file",
"f",
false,
"treat input as a file",
)
AppSecretInsertCommand.Flags().BoolVarP(
&trimInput,
"trim",
"t",
false,
"trim input",
)
AppSecretInsertCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
AppSecretRmCommand.Flags().BoolVarP(
&rmAllSecrets,
"all",
"a",
false,
"remove all secrets",
)
AppSecretRmCommand.Flags().BoolVarP(
&removeFromPass,
"pass",
"p",
false,
"remove generated secrets from a local pass store",
)
AppSecretRmCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
AppSecretLsCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
AppSecretLsCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
false,
"print machine-readable output",
)
}

View File

@ -13,19 +13,23 @@ import (
"coopcloud.tech/abra/pkg/service"
stack "coopcloud.tech/abra/pkg/upstream/stack"
containerTypes "github.com/docker/docker/api/types/container"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var appServicesCommand = cli.Command{
Name: "services",
Aliases: []string{"sr"},
Usage: "Display all services of an app",
UsageText: "abra app services <domain> [options]",
Before: internal.SubCommandBefore,
ShellComplete: autocomplete.AppNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
var AppServicesCommand = &cobra.Command{
Use: "services <app> [flags]",
Aliases: []string{"sr"},
Short: "Display all services of an app",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -87,7 +91,5 @@ var appServicesCommand = cli.Command{
if len(rows) > 0 {
fmt.Println(table)
}
return nil
},
}

View File

@ -14,75 +14,28 @@ import (
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var prune bool
var pruneFlag = &cli.BoolFlag{
Name: "prune",
Aliases: []string{"p"},
Destination: &prune,
Usage: "Prunes unused containers, networks, and dangling images for an app",
}
// pruneApp runs the equivalent of a "docker system prune" but only filtering
// against resources connected with the app deployment. It is not a system wide
// prune. Volumes are not pruned to avoid unwated data loss.
func pruneApp(cl *dockerClient.Client, app appPkg.App) error {
stackName := app.StackName()
ctx := context.Background()
pruneFilters := filters.NewArgs()
stackSearch := fmt.Sprintf("%s*", stackName)
pruneFilters.Add("label", stackSearch)
cr, err := cl.ContainersPrune(ctx, pruneFilters)
if err != nil {
return err
}
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
log.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed)
nr, err := cl.NetworksPrune(ctx, pruneFilters)
if err != nil {
return err
}
log.Infof("networks pruned: %d", len(nr.NetworksDeleted))
ir, err := cl.ImagesPrune(ctx, pruneFilters)
if err != nil {
return err
}
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
log.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed)
return nil
}
var appUndeployCommand = cli.Command{
Name: "undeploy",
Aliases: []string{"un"},
UsageText: "abra app undeploy <domain> [options]",
Flags: []cli.Flag{
internal.NoInputFlag,
internal.OfflineFlag,
pruneFlag,
},
Before: internal.SubCommandBefore,
Usage: "Undeploy an app",
ShellComplete: autocomplete.AppNameComplete,
HideHelp: true,
Description: `This does not destroy any of the application data.
var AppUndeployCommand = &cobra.Command{
Use: "undeploy <app> [flags]",
Aliases: []string{"un"},
Short: "Undeploy an app",
Long: `This does not destroy any of the application data.
However, you should remain vigilant, as your swarm installation will consider
any previously attached volumes as eligible for pruning once undeployed.
Passing "-p/--prune" does not remove those volumes.`,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
Passing "--prune/-p" does not remove those volumes.`,
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
stackName := app.StackName()
cl, err := client.New(app.Server)
@ -123,7 +76,55 @@ Passing "-p/--prune" does not remove those volumes.`,
log.Fatal(err)
}
}
return nil
},
}
// pruneApp runs the equivalent of a "docker system prune" but only filtering
// against resources connected with the app deployment. It is not a system wide
// prune. Volumes are not pruned to avoid unwated data loss.
func pruneApp(cl *dockerClient.Client, app appPkg.App) error {
stackName := app.StackName()
ctx := context.Background()
pruneFilters := filters.NewArgs()
stackSearch := fmt.Sprintf("%s*", stackName)
pruneFilters.Add("label", stackSearch)
cr, err := cl.ContainersPrune(ctx, pruneFilters)
if err != nil {
return err
}
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
log.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed)
nr, err := cl.NetworksPrune(ctx, pruneFilters)
if err != nil {
return err
}
log.Infof("networks pruned: %d", len(nr.NetworksDeleted))
ir, err := cl.ImagesPrune(ctx, pruneFilters)
if err != nil {
return err
}
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
log.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed)
return nil
}
var (
prune bool
)
func init() {
AppUndeployCommand.Flags().BoolVarP(
&prune,
"prune",
"p",
false,
"prune unused containers, networks, and dangling images",
)
}

View File

@ -15,37 +15,50 @@ import (
stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var appUpgradeCommand = cli.Command{
Name: "upgrade",
Aliases: []string{"up"},
Usage: "Upgrade an app",
UsageText: "abra app upgrade <domain> [<version>] [options]",
Flags: []cli.Flag{
internal.ForceFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
internal.ReleaseNotesFlag,
},
Before: internal.SubCommandBefore,
Description: `Upgrade an app.
var AppUpgradeCommand = &cobra.Command{
Use: "upgrade <app> [version] [flags]",
Aliases: []string{"up"},
Short: "Upgrade an app",
Long: `Upgrade an app.
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
beforehand.`,
HideHelp: true,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
app, err := appPkg.Get(args[0])
if err != nil {
log.Debugf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveDefault
}
return autocomplete.RecipeVersionComplete(app.Recipe.Name)
default:
return nil, cobra.ShellCompDirectiveError
}
},
Run: func(cmd *cobra.Command, args []string) {
var warnMessages []string
app := internal.ValidateApp(cmd)
app := internal.ValidateApp(args)
stackName := app.StackName()
specificVersion := cmd.Args().Get(1)
var specificVersion string
if len(args) == 2 {
specificVersion = args[1]
}
if specificVersion != "" {
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion)
app.Recipe.Version = specificVersion
@ -129,7 +142,7 @@ beforehand.`,
if len(availableUpgrades) == 0 && !internal.Force {
log.Info("no available upgrades")
return nil
return
}
}
@ -150,7 +163,7 @@ beforehand.`,
}
if err := survey.AskOne(prompt, &chosenUpgrade); err != nil {
return err
return
}
}
}
@ -177,7 +190,7 @@ beforehand.`,
if parsedVersion.IsGreaterThan(parsedDeployedVersion) && parsedVersion.IsLessThan(parsedChosenUpgrade) {
note, err := app.Recipe.GetReleaseNotes(version)
if err != nil {
return err
log.Fatal(err)
}
if note != "" {
releaseNotes += fmt.Sprintf("%s\n", note)
@ -236,10 +249,10 @@ beforehand.`,
}
}
if internal.ReleaseNotes {
if showReleaseNotes {
fmt.Println()
fmt.Print(releaseNotes)
return nil
return
}
chaosVersion := config.CHAOS_DEFAULT
@ -281,7 +294,42 @@ beforehand.`,
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
return nil
},
}
var (
showReleaseNotes bool
)
func init() {
AppUpgradeCommand.Flags().BoolVarP(
&internal.Force,
"force",
"f",
false,
"perform action without further prompt",
)
AppUpgradeCommand.Flags().BoolVarP(
&internal.NoDomainChecks,
"no-domain-checks",
"D",
false,
"disable public DNS checks",
)
AppUpgradeCommand.Flags().BoolVarP(
&internal.DontWaitConverge, "no-converge-checks",
"c",
false,
"do not wait for converge logic checks",
)
AppUpgradeCommand.Flags().BoolVarP(
&showReleaseNotes,
"releasenotes",
"r",
false,
"only show release notes",
)
}

View File

@ -11,19 +11,22 @@ import (
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var appVolumeListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
UsageText: "abra app volume list <domain> [options]",
Before: internal.SubCommandBefore,
Usage: "List volumes associated with an app",
ShellComplete: autocomplete.AppNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
var AppVolumeListCommand = &cobra.Command{
Use: "list <app> [flags]",
Aliases: []string{"ls"},
Short: "List volumes associated with an app",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
cl, err := client.New(app.Server)
if err != nil {
@ -59,38 +62,36 @@ var appVolumeListCommand = cli.Command{
if len(rows) > 0 {
fmt.Println(table)
return nil
return
}
log.Warnf("no volumes created for %s", app.Name)
return nil
},
}
var appVolumeRemoveCommand = cli.Command{
Name: "remove",
Usage: "Remove volume(s) associated with an app",
Description: `Remove volumes associated with an app.
var AppVolumeRemoveCommand = &cobra.Command{
Use: "remove <app> [flags]",
Short: "Remove volume(s) associated with an app",
Long: `Remove volumes associated with an app.
The app in question must be undeployed before you try to remove volumes. See
"abra app undeploy <domain>" for more.
"abra app undeploy <app>" for more.
The command is interactive and will show a multiple select input which allows
you to make a seclection. Use the "?" key to see more help on navigating this
interface.
Passing "--force/-f" will select all volumes for removal. Be careful.`,
UsageText: "abra app volume remove [options] <domain>",
Aliases: []string{"rm"},
Flags: []cli.Flag{
internal.NoInputFlag,
internal.ForceFlag,
Aliases: []string{"rm"},
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Before: internal.SubCommandBefore,
ShellComplete: autocomplete.AppNameComplete,
Action: func(ctx context.Context, cmd *cli.Command) error {
app := internal.ValidateApp(cmd)
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
cl, err := client.New(app.Server)
if err != nil {
@ -145,18 +146,21 @@ Passing "--force/-f" will select all volumes for removal. Be careful.`,
} else {
log.Info("no volumes removed")
}
return nil
},
}
var appVolumeCommand = cli.Command{
Name: "volume",
Aliases: []string{"vl"},
Usage: "Manage app volumes",
UsageText: "abra app volume [command] [options] [arguments]",
Commands: []*cli.Command{
&appVolumeListCommand,
&appVolumeRemoveCommand,
},
var AppVolumeCommand = &cobra.Command{
Use: "volume [cmd] [args] [flags]",
Aliases: []string{"vl"},
Short: "Manage app volumes",
}
func init() {
AppVolumeRemoveCommand.Flags().BoolVarP(
&internal.Force,
"force",
"f",
false,
"perform action without further prompt",
)
}

View File

@ -1,7 +1,6 @@
package catalogue
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
@ -16,41 +15,42 @@ import (
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/go-git/go-git/v5"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var catalogueGenerateCommand = cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate the recipe catalogue",
UsageText: "abra catalogue generate [<recipe>] [options]",
Flags: []cli.Flag{
internal.PublishFlag,
internal.DryFlag,
internal.SkipUpdatesFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
Description: `Generate a new copy of the recipe catalogue.
var CatalogueGenerateCommand = &cobra.Command{
Use: "generate [recipe] [flags]",
Aliases: []string{"g"},
Short: "Generate the recipe catalogue",
Long: `Generate a new copy of the recipe catalogue.
It is possible to generate new metadata for a single recipe by passing
<recipe>. The existing local catalogue will be updated, not overwritten.
[recipe]. The existing local catalogue will be updated, not overwritten.
It is quite easy to get rate limited by Docker Hub when running this command.
If you have a Hub account you can have Abra log you in to avoid this. Pass
"--user" and "--pass".
Push your new release to git.coopcloud.tech with "-p/--publish". This requires
Push your new release to git.coopcloud.tech with "--publish/-p". This requires
that you have permission to git push to these repositories and have your SSH
keys configured on your account.`,
ShellComplete: autocomplete.RecipeNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
recipeName := cmd.Args().First()
Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
var recipeName string
if len(args) > 0 {
recipeName = args[0]
}
r := recipe.Get(recipeName)
if recipeName != "" {
internal.ValidateRecipe(cmd)
internal.ValidateRecipe(args, cmd.Name())
}
if !internal.Chaos {
@ -74,7 +74,7 @@ keys configured on your account.`,
logMsg = fmt.Sprintf("ensuring %v recipes are cloned & up-to-date, this could take some time...", barLength)
}
if !internal.SkipUpdates {
if !skipUpdates {
log.Warn(logMsg)
if err := recipe.UpdateRepositories(repos, recipeName); err != nil {
log.Fatal(err)
@ -145,7 +145,7 @@ keys configured on your account.`,
log.Infof("generated new recipe catalogue in %s", config.RECIPES_JSON)
cataloguePath := path.Join(config.ABRA_DIR, "catalogue")
if internal.Publish {
if publishChanges {
isClean, err := gitPkg.IsClean(cataloguePath)
if err != nil {
@ -188,7 +188,7 @@ keys configured on your account.`,
log.Fatal(err)
}
if !internal.Dry && internal.Publish {
if !internal.Dry && publishChanges {
url := fmt.Sprintf("%s/%s/commit/%s", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME, head.Hash())
log.Infof("new changes published: %s", url)
}
@ -196,18 +196,51 @@ keys configured on your account.`,
if internal.Dry {
log.Info("dry run: no changes published")
}
return nil
},
}
// CatalogueCommand defines the `abra catalogue` command and sub-commands.
var CatalogueCommand = cli.Command{
Name: "catalogue",
Usage: "Manage the recipe catalogue",
Aliases: []string{"c"},
UsageText: "abra catalogue [command] [options] [arguments]",
Commands: []*cli.Command{
&catalogueGenerateCommand,
},
var CatalogueCommand = &cobra.Command{
Use: "catalogue [cmd] [args] [flags]",
Short: "Manage the recipe catalogue",
Aliases: []string{"c"},
}
var (
publishChanges bool
skipUpdates bool
)
func init() {
CatalogueGenerateCommand.Flags().BoolVarP(
&publishChanges,
"publish",
"p",
false,
"publish changes to git.coopcloud.tech",
)
CatalogueGenerateCommand.Flags().BoolVarP(
&internal.Dry,
"dry-run",
"r",
false,
"report changes that would be made",
)
CatalogueGenerateCommand.Flags().BoolVarP(
&skipUpdates,
"skip-updates",
"s",
false,
"skip updating recipe repositories",
)
CatalogueGenerateCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
}

View File

@ -1,207 +0,0 @@
// Package cli provides the interface for the command-line.
package cli
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path"
"coopcloud.tech/abra/cli/app"
"coopcloud.tech/abra/cli/catalogue"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/cli/recipe"
"coopcloud.tech/abra/cli/server"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/web"
charmLog "github.com/charmbracelet/log"
"github.com/urfave/cli/v3"
)
// AutoCompleteCommand helps people set up auto-complete in their shells
var AutoCompleteCommand = cli.Command{
Name: "autocomplete",
Aliases: []string{"ac"},
Usage: "Configure shell autocompletion",
UsageText: "abra autocomplete <shell> [options]",
Description: `Set up shell auto-completion.
Supported shells are: bash, fish, fizsh & zsh.`,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
shellType := cmd.Args().First()
if shellType == "" {
internal.ShowSubcommandHelpAndError(cmd, errors.New("no shell provided"))
}
supportedShells := map[string]bool{
"bash": true,
"zsh": true,
"fizsh": true,
"fish": true,
}
if _, ok := supportedShells[shellType]; !ok {
log.Fatalf("%s is not a supported shell right now, sorry", shellType)
}
if shellType == "fizsh" {
shellType = "zsh" // handled the same on the autocompletion side
}
autocompletionDir := path.Join(config.ABRA_DIR, "autocompletion")
if err := os.Mkdir(autocompletionDir, 0764); err != nil {
if !os.IsExist(err) {
log.Fatal(err)
}
log.Debugf("%s already created", autocompletionDir)
}
autocompletionFile := path.Join(config.ABRA_DIR, "autocompletion", shellType)
if _, err := os.Stat(autocompletionFile); err != nil && os.IsNotExist(err) {
url := fmt.Sprintf("https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/autocomplete/%s", shellType)
log.Infof("fetching %s", url)
if err := web.GetFile(autocompletionFile, url); err != nil {
log.Fatal(err)
}
}
switch shellType {
case "bash":
fmt.Println(fmt.Sprintf(`
# run the following commands once to install auto-completion
sudo mkdir -p /etc/bash_completion.d/
sudo cp %s /etc/bash_completion.d/abra
echo "source /etc/bash_completion.d/abra" >> ~/.bashrc
source /etc/bash_completion.d/abra
# To test, run the following: "abra app <hit tab key>" - you should see command completion!
`, autocompletionFile))
case "zsh":
fmt.Println(fmt.Sprintf(`
# run the following commands to once install auto-completion
sudo mkdir -p /etc/zsh/completion.d/
sudo cp %s /etc/zsh/completion.d/abra
echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc
source /etc/zsh/completion.d/abra
# to test, run the following: "abra app <hit tab key>" - you should see command completion!
`, autocompletionFile))
case "fish":
fmt.Println(fmt.Sprintf(`
# run the following commands once to install auto-completion
sudo mkdir -p /etc/fish/completions
sudo cp %s /etc/fish/completions/abra
echo "source /etc/fish/completions/abra" >> ~/.config/fish/config.fish
source /etc/fish/completions/abra
# to test, run the following: "abra app <hit tab key>" - you should see command completion!
`, autocompletionFile))
}
return nil
},
}
// UpgradeCommand upgrades abra in-place.
var UpgradeCommand = cli.Command{
Name: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade abra",
UsageText: "abra upgrade [options]",
Description: `Upgrade abra in-place with the latest stable or release candidate.
Use "--rc/-r" to install the latest release candidate. Please bear in mind that
it may contain absolutely catastrophic deal-breaker bugs. Thank you very much
for the testing efforts 💗`,
Flags: []cli.Flag{internal.RCFlag},
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
mainURL := "https://install.abra.coopcloud.tech"
c := exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash", mainURL))
if internal.RC {
releaseCandidateURL := "https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer"
c = exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash -s -- --rc", releaseCandidateURL))
}
log.Debugf("attempting to run %s", c)
if err := internal.RunCmd(c); err != nil {
log.Fatal(err)
}
return nil
},
}
func newAbraApp(version, commit string) *cli.Command {
app := &cli.Command{
Name: "abra",
Usage: "The Co-op Cloud command-line utility belt 🎩🐇",
UsageText: "abra [command] [arguments] [options]",
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Flags: []cli.Flag{
// NOTE(d1): "GLOBAL OPTIONS" flags
internal.NoInputFlag,
internal.DebugFlag,
},
Commands: []*cli.Command{
&app.AppCommand,
&server.ServerCommand,
&recipe.RecipeCommand,
&catalogue.CatalogueCommand,
&UpgradeCommand,
&AutoCompleteCommand,
},
EnableShellCompletion: true,
UseShortOptionHandling: true,
HideHelpCommand: true,
ShellComplete: autocomplete.SubcommandComplete,
}
app.Before = func(ctx context.Context, cmd *cli.Command) error {
paths := []string{
config.ABRA_DIR,
config.SERVERS_DIR,
config.RECIPES_DIR,
config.VENDOR_DIR,
config.BACKUP_DIR,
}
for _, path := range paths {
if err := os.Mkdir(path, 0764); err != nil {
if !os.IsExist(err) {
log.Fatal(err)
}
continue
}
}
log.Logger.SetStyles(log.Styles())
charmLog.SetDefault(log.Logger)
log.Debugf("abra version %s, commit %s", version, commit)
return nil
}
cli.HelpFlag = &cli.BoolFlag{
Name: "help",
Aliases: []string{"h, H"},
Usage: "Show help",
}
return app
}
// RunApp runs CLI abra app.
func RunApp(version, commit string) {
app := newAbraApp(version, commit)
if err := app.Run(context.Background(), os.Args); err != nil {
log.Fatal(err)
}
}

65
cli/complete.go Normal file
View File

@ -0,0 +1,65 @@
package cli
import (
"os"
"github.com/spf13/cobra"
)
var AutocompleteCommand = &cobra.Command{
Use: "autocomplete [bash|zsh|fish|powershell]",
Short: "Generate autocompletion script",
Long: `To load completions:
Bash:
$ source <(abra autocomplete bash)
# To load autocompletion for each session, execute once:
# Linux:
$ abra autocomplete bash > /etc/bash_completion.d/abra
# macOS:
$ abra autocomplete bash > $(brew --prefix)/etc/bash_completion.d/abra
Zsh:
# If shell autocompletion is not already enabled in your environment,
# you will need to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load autocompletions for each session, execute once:
$ abra autocomplete zsh > "${fpath[1]}/_abra"
# You will need to start a new shell for this setup to take effect.
fish:
$ abra autocomplete fish | source
# To load autocompletions for each session, execute once:
$ abra autocomplete fish > ~/.config/fish/completions/abra.fish
PowerShell:
PS> abra autocomplete powershell | Out-String | Invoke-Expression
# To load autocompletions for every new session, run:
PS> abra autocomplete powershell > abra.ps1
# and source this file from your PowerShell profile.`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
},
}

View File

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

View File

@ -21,7 +21,11 @@ import (
)
// RunCmdRemote executes an abra.sh command in the target service
func RunCmdRemote(cl *dockerClient.Client, app appPkg.App, abraSh, serviceName, cmdName, cmdArgs string) error {
func RunCmdRemote(
cl *dockerClient.Client,
app appPkg.App,
requestTTY bool,
abraSh, serviceName, cmdName, cmdArgs, remoteUser string) error {
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
@ -74,15 +78,15 @@ func RunCmdRemote(cl *dockerClient.Client, app appPkg.App, abraSh, serviceName,
log.Debugf("running command: %s", strings.Join(cmd, " "))
if RemoteUser != "" {
log.Debugf("running command with user %s", RemoteUser)
execCreateOpts.User = RemoteUser
if remoteUser != "" {
log.Debugf("running command with user %s", remoteUser)
execCreateOpts.User = remoteUser
}
execCreateOpts.Cmd = cmd
execCreateOpts.Tty = true
if Tty {
execCreateOpts.Tty = false
execCreateOpts.Tty = requestTTY
if !requestTTY {
log.Debugf("not requesting a remote TTY")
}
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {

View File

@ -199,8 +199,12 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
log.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName)
Tty = true
if err := RunCmdRemote(cl, app, app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs); err != nil {
requestTTY := true
if err := RunCmdRemote(
cl,
app,
requestTTY,
app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs, ""); err != nil {
return err
}
}

View File

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

View File

@ -1,31 +1,29 @@
package recipe
import (
"context"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var recipeDiffCommand = cli.Command{
Name: "diff",
Usage: "Show unstaged changes in recipe config",
Description: "This command requires /usr/bin/git.",
Aliases: []string{"d"},
UsageText: "abra recipe diff <recipe> [options]",
Before: internal.SubCommandBefore,
ShellComplete: autocomplete.RecipeNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
r := internal.ValidateRecipe(cmd)
var RecipeDiffCommand = &cobra.Command{
Use: "diff <recipe> [flags]",
Aliases: []string{"d"},
Short: "Show unstaged changes in recipe config",
Long: "This command requires /usr/bin/git.",
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
r := internal.ValidateRecipe(args, cmd.Name())
if err := gitPkg.DiffUnstaged(r.Dir); err != nil {
log.Fatal(err)
}
return nil
},
}

View File

@ -1,34 +1,38 @@
package recipe
import (
"context"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var recipeFetchCommand = cli.Command{
Name: "fetch",
Usage: "Fetch recipe(s)",
Aliases: []string{"f"},
UsageText: "abra recipe fetch [<recipe>] [options]",
Description: "Retrieves all recipes if no <recipe> argument is passed",
Before: internal.SubCommandBefore,
ShellComplete: autocomplete.RecipeNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
recipeName := cmd.Args().First()
r := recipe.Get(recipeName)
var RecipeFetchCommand = &cobra.Command{
Use: "fetch [recipe] [flags]",
Aliases: []string{"f"},
Short: "Fetch recipe(s)",
Long: "Retrieves all recipes if no [recipe] argument is passed.",
Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
var recipeName string
if len(args) > 0 {
recipeName = args[0]
}
if recipeName != "" {
internal.ValidateRecipe(cmd)
r := internal.ValidateRecipe(args, cmd.Name())
if err := r.Ensure(false, false); err != nil {
log.Fatal(err)
}
return nil
return
}
catalogue, err := recipe.ReadRecipeCatalogue(internal.Offline)
@ -44,7 +48,5 @@ var recipeFetchCommand = cli.Command{
}
catlBar.Add(1)
}
return nil
},
}

View File

@ -1,7 +1,6 @@
package recipe
import (
"context"
"fmt"
"coopcloud.tech/abra/cli/internal"
@ -9,23 +8,22 @@ import (
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var recipeLintCommand = cli.Command{
Name: "lint",
Usage: "Lint a recipe",
Aliases: []string{"l"},
UsageText: "abra recipe lint <recipe> [options]",
Flags: []cli.Flag{
internal.OnlyErrorFlag,
internal.ChaosFlag,
var RecipeLintCommand = &cobra.Command{
Use: "lint <recipe> [flags]",
Short: "Lint a recipe",
Aliases: []string{"l"},
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
Before: internal.SubCommandBefore,
ShellComplete: autocomplete.RecipeNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
recipe := internal.ValidateRecipe(cmd)
Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name())
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
@ -52,7 +50,7 @@ var recipeLintCommand = cli.Command{
var warnMessages []string
for level := range lint.LintRules {
for _, rule := range lint.LintRules[level] {
if internal.OnlyErrors && rule.Level != "error" {
if onlyError && rule.Level != "error" {
log.Debugf("skipping %s, does not have level \"error\"", rule.Ref)
continue
}
@ -116,7 +114,27 @@ var recipeLintCommand = cli.Command{
log.Warnf("critical errors present in %s config", recipe.Name)
}
}
return nil
},
}
var (
onlyError bool
)
func init() {
RecipeLintCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
RecipeLintCommand.Flags().BoolVarP(
&onlyError,
"error",
"e",
false,
"only show errors",
)
}

View File

@ -1,7 +1,6 @@
package recipe
import (
"context"
"fmt"
"sort"
"strconv"
@ -11,33 +10,18 @@ import (
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var pattern string
var patternFlag = &cli.StringFlag{
Name: "pattern",
Aliases: []string{"p"},
Value: "",
Usage: "Simple string to filter recipes",
Destination: &pattern,
}
var recipeListCommand = cli.Command{
Name: "list",
Usage: "List recipes",
UsageText: "abra recipe list [options]",
Aliases: []string{"ls"},
Flags: []cli.Flag{
internal.MachineReadableFlag,
patternFlag,
},
Before: internal.SubCommandBefore,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
var RecipeListCommand = &cobra.Command{
Use: "list",
Short: "List recipes",
Aliases: []string{"ls"},
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
log.Fatal(err.Error())
log.Fatal(err)
}
recipes := catl.Flatten()
@ -92,13 +76,33 @@ var recipeListCommand = cli.Command{
log.Fatal("unable to render to JSON: %s", err)
}
fmt.Println(out)
return nil
return
}
fmt.Println(table)
log.Infof("total recipes: %v", len(rows))
}
return nil
},
}
var (
pattern string
)
func init() {
RecipeListCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
false,
"print machine-readable output",
)
RecipeListCommand.Flags().StringVarP(
&pattern,
"pattern",
"p",
"",
"filter by recipe",
)
}

View File

@ -2,19 +2,17 @@ package recipe
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path"
"text/template"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
// recipeMetadata is the recipe metadata for the README.md
@ -31,30 +29,22 @@ type recipeMetadata struct {
SSO string
}
var recipeNewCommand = cli.Command{
Name: "new",
var RecipeNewCommand = &cobra.Command{
Use: "new <recipe> [flags]",
Aliases: []string{"n"},
Flags: []cli.Flag{
internal.GitNameFlag,
internal.GitEmailFlag,
Short: "Create a new recipe",
Long: `A community managed recipe template is used.`,
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
Before: internal.SubCommandBefore,
Usage: "Create a new recipe",
UsageText: "abra recipe new <recipe> [options]",
Description: `Create a new recipe.
Run: func(cmd *cobra.Command, args []string) {
recipeName := args[0]
Abra uses the built-in example repository which is available here:
https://git.coopcloud.tech/coop-cloud/example`,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
recipeName := cmd.Args().First()
r := recipe.Get(recipeName)
if recipeName == "" {
internal.ShowSubcommandHelpAndError(cmd, errors.New("no recipe name provided"))
}
if _, err := os.Stat(r.Dir); !os.IsNotExist(err) {
log.Fatalf("%s recipe directory already exists?", r.Dir)
}
@ -89,14 +79,12 @@ Abra uses the built-in example repository which is available here:
}
if err := git.Init(r.Dir, true, internal.GitName, internal.GitEmail); err != nil {
if err := git.Init(r.Dir, true, gitName, gitEmail); err != nil {
log.Fatal(err)
}
log.Infof("new recipe '%s' created: %s", recipeName, path.Join(r.Dir))
log.Info("happy hacking 🎉")
return nil
},
}
@ -115,3 +103,26 @@ func newRecipeMeta(recipeName string) recipeMetadata {
SSO: "No",
}
}
var (
gitName string
gitEmail string
)
func init() {
RecipeNewCommand.Flags().StringVarP(
&gitName,
"git-name",
"N",
"",
"Git (user) name to do commits with",
)
RecipeNewCommand.Flags().StringVarP(
&gitEmail,
"git-email",
"e",
"",
"Git email name to do commits with",
)
}

View File

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

View File

@ -1,7 +1,6 @@
package recipe
import (
"context"
"errors"
"fmt"
"os"
@ -19,15 +18,14 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/distribution/reference"
"github.com/go-git/go-git/v5"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var recipeReleaseCommand = cli.Command{
Name: "release",
Aliases: []string{"rl"},
Usage: "Release a new recipe version",
UsageText: "abra recipe release <recipe> [<version>] [options]",
Description: `Create a new version of a recipe.
var RecipeReleaseCommand = &cobra.Command{
Use: "release <recipe> [version] [flags]",
Aliases: []string{"rl"},
Short: "Release a new recipe version",
Long: `Create a new version of a recipe.
These versions are then published on the Co-op Cloud recipe catalogue. These
versions take the following form:
@ -44,21 +42,25 @@ recipe updates are properly communicated. I.e. developers of an app might
publish a minor version but that might lead to changes in the recipe which are
major and therefore require intervention while doing the upgrade work.
Publish your new release to git.coopcloud.tech with "-p/--publish". This
Publish your new release to git.coopcloud.tech with "--publish/-p". This
requires that you have permission to git push to these repositories and have
your SSH keys configured on your account.`,
Flags: []cli.Flag{
internal.DryFlag,
internal.MajorFlag,
internal.MinorFlag,
internal.PatchFlag,
internal.PublishFlag,
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.RecipeNameComplete()
case 1:
return autocomplete.RecipeVersionComplete(args[0])
default:
return nil, cobra.ShellCompDirectiveDefault
}
},
Before: internal.SubCommandBefore,
ShellComplete: autocomplete.RecipeNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
recipe := internal.ValidateRecipe(cmd)
Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name())
imagesTmp, err := getImageVersions(recipe)
if err != nil {
@ -75,7 +77,11 @@ your SSH keys configured on your account.`,
log.Fatalf("main app service version for %s is empty?", recipe.Name)
}
tagString := cmd.Args().Get(1)
var tagString string
if len(args) == 2 {
tagString = args[1]
}
if tagString != "" {
if _, err := tagcmp.Parse(tagString); err != nil {
log.Fatalf("cannot parse %s, invalid tag specified?", tagString)
@ -133,7 +139,7 @@ your SSH keys configured on your account.`,
}
}
return nil
return
},
}
@ -261,6 +267,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
log.Debugf("dry run: move release note from 'next' to %s", tag)
return nil
}
if !internal.NoInput {
prompt := &survey.Input{
Message: "Use release note in release/next?",
@ -273,14 +280,17 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
return nil
}
}
err := os.Rename(nextReleaseNotePath, tagReleaseNotePath)
if err != nil {
return err
}
err = gitPkg.Add(recipe.Dir, path.Join("release", "next"), internal.Dry)
if err != nil {
return err
}
err = gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry)
if err != nil {
return err
@ -297,6 +307,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
prompt := &survey.Input{
Message: "Release Note (leave empty for no release note)",
}
var releaseNote string
if err := survey.AskOne(prompt, &releaseNote); err != nil {
return err
@ -306,12 +317,11 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
return nil
}
err := os.WriteFile(tagReleaseNotePath, []byte(releaseNote), 0o644)
if err != nil {
if err := os.WriteFile(tagReleaseNotePath, []byte(releaseNote), 0o644); err != nil {
return err
}
err = gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry)
if err != nil {
if err := gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry); err != nil {
return err
}
@ -376,17 +386,17 @@ func pushRelease(recipe recipe.Recipe, tagString string) error {
return nil
}
if !internal.Publish && !internal.NoInput {
if !publish && !internal.NoInput {
prompt := &survey.Confirm{
Message: "publish new release?",
}
if err := survey.AskOne(prompt, &internal.Publish); err != nil {
if err := survey.AskOne(prompt, &publish); err != nil {
return err
}
}
if internal.Publish {
if publish {
if err := recipe.Push(internal.Dry); err != nil {
return err
}
@ -546,3 +556,50 @@ func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
return initTag, nil
}
var (
publish bool
)
func init() {
RecipeReleaseCommand.Flags().BoolVarP(
&internal.Dry,
"dry-run",
"r",
false,
"report changes that would be made",
)
RecipeReleaseCommand.Flags().BoolVarP(
&internal.Major,
"major",
"x",
false,
"increase the major part of the version",
)
RecipeReleaseCommand.Flags().BoolVarP(
&internal.Minor,
"minor",
"y",
false,
"increase the minor part of the version",
)
RecipeReleaseCommand.Flags().BoolVarP(
&internal.Patch,
"patch",
"z",
false,
"increase the patch part of the version",
)
RecipeReleaseCommand.Flags().BoolVarP(
&publish,
"publish",
"p",
false,
"publish changes to git.coopcloud.tech",
)
}

View File

@ -1,32 +1,27 @@
package recipe
import (
"context"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/go-git/go-git/v5"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var recipeResetCommand = cli.Command{
Name: "reset",
Usage: "Remove all unstaged changes from recipe config",
Description: "WARNING: this will delete your changes. Be Careful.",
Aliases: []string{"rs"},
UsageText: "abra recipe reset <recipe> [options]",
Before: internal.SubCommandBefore,
ShellComplete: autocomplete.RecipeNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
recipeName := cmd.Args().First()
r := recipe.Get(recipeName)
if recipeName != "" {
internal.ValidateRecipe(cmd)
}
var RecipeResetCommand = &cobra.Command{
Use: "reset <recipe> [flags]",
Aliases: []string{"rs"},
Short: "Remove all unstaged changes from recipe config",
Long: "WARNING: this will delete your changes. Be Careful.",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
r := internal.ValidateRecipe(args, cmd.Name())
repo, err := git.PlainOpen(r.Dir)
if err != nil {
@ -47,7 +42,5 @@ var recipeResetCommand = cli.Command{
if err := worktree.Reset(opts); err != nil {
log.Fatal(err)
}
return nil
},
}

View File

@ -1,7 +1,6 @@
package recipe
import (
"context"
"fmt"
"strconv"
@ -13,34 +12,38 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var recipeSyncCommand = cli.Command{
Name: "sync",
Aliases: []string{"s"},
Usage: "Sync recipe version label",
UsageText: "abra recipe lint <recipe> [<version>] [options]",
Flags: []cli.Flag{
internal.DryFlag,
internal.MajorFlag,
internal.MinorFlag,
internal.PatchFlag,
},
Before: internal.SubCommandBefore,
Description: `Generate labels for the main recipe service.
var RecipeSyncCommand = &cobra.Command{
Use: "sync <recipe> [version] [flags]",
Aliases: []string{"s"},
Short: "Sync recipe version label",
Long: `Generate labels for the main recipe service.
By convention, the service named "app" using the following format:
coop-cloud.${STACK_NAME}.version=<version>
Where <version> can be specifed on the command-line or Abra can attempt to
Where [version] can be specifed on the command-line or Abra can attempt to
auto-generate it for you. The <recipe> configuration will be updated on the
local file system.`,
ShellComplete: autocomplete.RecipeNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
recipe := internal.ValidateRecipe(cmd)
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.RecipeNameComplete()
case 1:
return autocomplete.RecipeVersionComplete(args[0])
default:
return nil, cobra.ShellCompDirectiveError
}
},
Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name())
mainApp, err := internal.GetMainAppImage(recipe)
if err != nil {
@ -59,7 +62,11 @@ local file system.`,
log.Fatal(err)
}
nextTag := cmd.Args().Get(1)
var nextTag string
if len(args) == 2 {
nextTag = args[1]
}
if len(tags) == 0 && nextTag == "" {
log.Warnf("no git tags found for %s", recipe.Name)
if internal.NoInput {
@ -205,7 +212,39 @@ likely to change.
log.Fatal(err)
}
}
return nil
},
}
func init() {
RecipeSyncCommand.Flags().BoolVarP(
&internal.Dry,
"dry-run",
"r",
false,
"report changes that would be made",
)
RecipeSyncCommand.Flags().BoolVarP(
&internal.Major,
"major",
"x",
false,
"increase the major part of the version",
)
RecipeSyncCommand.Flags().BoolVarP(
&internal.Minor,
"minor",
"y",
false,
"increase the minor part of the version",
)
RecipeSyncCommand.Flags().BoolVarP(
&internal.Patch,
"patch",
"z",
false,
"increase the patch part of the version",
)
}

View File

@ -2,7 +2,6 @@ package recipe
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
@ -20,7 +19,7 @@ import (
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/distribution/reference"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
type imgPin struct {
@ -37,12 +36,11 @@ type anUpgrade struct {
UpgradeTags []string `json:"upgrades"`
}
var recipeUpgradeCommand = cli.Command{
Name: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade recipe image tags",
UsageText: "abra recipe upgrade [<recipe>] [options]",
Description: `Upgrade a given <recipe> configuration.
var RecipeUpgradeCommand = &cobra.Command{
Use: "upgrade <recipe> [flags]",
Aliases: []string{"u"},
Short: "Upgrade recipe image tags",
Long: `Upgrade a given <recipe> configuration.
It will update the relevant compose file tags on the local file system.
@ -55,18 +53,15 @@ make a seclection. Use the "?" key to see more help on navigating this
interface.
You may invoke this command in "wizard" mode and be prompted for input.`,
Flags: []cli.Flag{
internal.PatchFlag,
internal.MinorFlag,
internal.MajorFlag,
internal.MachineReadableFlag,
internal.AllTagsFlag,
Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
Before: internal.SubCommandBefore,
ShellComplete: autocomplete.RecipeNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
recipe := internal.ValidateRecipe(cmd)
Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name())
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
@ -177,7 +172,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
sort.Sort(tagcmp.ByTagDesc(compatible))
if len(compatible) == 0 && !internal.AllTags {
if len(compatible) == 0 && !allTags {
log.Info(fmt.Sprintf("no new versions available for %s, assuming %s is the latest (use -a/--all-tags to see all anyway)", image, tag))
continue // skip on to the next tag and don't update any compose files
}
@ -231,7 +226,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
for _, upTag := range compatible {
upElement, err := tag.UpgradeDelta(upTag)
if err != nil {
return err
return
}
delta := upElement.UpgradeType()
if delta <= bumpType {
@ -245,9 +240,9 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
}
} else {
msg := fmt.Sprintf("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, tag)
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || internal.AllTags {
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || allTags {
tag := img.(reference.NamedTagged).Tag()
if !internal.AllTags {
if !allTags {
log.Warn(fmt.Sprintf("unable to determine versioning semantics of %s, listing all tags", tag))
}
msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
@ -315,7 +310,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
fmt.Println(string(jsonstring))
return nil
return
}
for _, upgrade := range upgradeList {
@ -336,7 +331,51 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
log.Fatal(err)
}
}
return nil
},
}
var (
allTags bool
)
func init() {
RecipeUpgradeCommand.Flags().BoolVarP(
&internal.Major,
"major",
"x",
false,
"increase the major part of the version",
)
RecipeUpgradeCommand.Flags().BoolVarP(
&internal.Minor,
"minor",
"y",
false,
"increase the minor part of the version",
)
RecipeUpgradeCommand.Flags().BoolVarP(
&internal.Patch,
"patch",
"z",
false,
"increase the patch part of the version",
)
RecipeUpgradeCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
false,
"print machine-readable output",
)
RecipeUpgradeCommand.Flags().BoolVarP(
&allTags,
"all-tags",
"a",
false,
"list all tags, not just upgrades",
)
}

View File

@ -1,7 +1,6 @@
package recipe
import (
"context"
"fmt"
"sort"
@ -10,34 +9,24 @@ import (
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
func sortServiceByName(versions [][]string) func(i, j int) bool {
return func(i, j int) bool {
// NOTE(d1): corresponds to the `tableCol` definition below
if versions[i][1] == "app" {
return true
}
return versions[i][1] < versions[j][1]
}
}
var recipeVersionCommand = cli.Command{
Name: "versions",
Aliases: []string{"v"},
Usage: "List recipe versions",
UsageText: "abra recipe version <recipe> [options]",
Flags: []cli.Flag{
internal.MachineReadableFlag,
var RecipeVersionCommand = &cobra.Command{
Use: "versions <recipe> [flags]",
Aliases: []string{"v"},
Short: "List recipe versions",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
Before: internal.SubCommandBefore,
ShellComplete: autocomplete.RecipeNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
Run: func(cmd *cobra.Command, args []string) {
var warnMessages []string
recipe := internal.ValidateRecipe(cmd)
recipe := internal.ValidateRecipe(args, cmd.Name())
catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
if err != nil {
@ -106,7 +95,25 @@ var recipeVersionCommand = cli.Command{
log.Warn(warnMsg)
}
}
return nil
},
}
func sortServiceByName(versions [][]string) func(i, j int) bool {
return func(i, j int) bool {
// NOTE(d1): corresponds to the `tableCol` definition below
if versions[i][1] == "app" {
return true
}
return versions[i][1] < versions[j][1]
}
}
func init() {
RecipeVersionCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
false,
"print machine-readable output",
)
}

184
cli/run.go Normal file
View File

@ -0,0 +1,184 @@
package cli
import (
"fmt"
"os"
"coopcloud.tech/abra/cli/app"
"coopcloud.tech/abra/cli/catalogue"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/cli/recipe"
"coopcloud.tech/abra/cli/server"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log"
charmLog "github.com/charmbracelet/log"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
)
func Run(version, commit string) {
rootCmd := &cobra.Command{
Use: "abra [cmd] [args] [flags]",
Short: "The Co-op Cloud command-line utility belt 🎩🐇",
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
ValidArgs: []string{
"app",
"autocomplete",
"catalogue",
"man",
"recipe",
"server",
"upgrade",
},
PersistentPreRun: func(cmd *cobra.Command, args []string) {
paths := []string{
config.ABRA_DIR,
config.SERVERS_DIR,
config.RECIPES_DIR,
config.VENDOR_DIR, // TODO(d1): remove > 0.9.x
config.BACKUP_DIR, // TODO(d1): remove > 0.9.x
}
for _, path := range paths {
if err := os.Mkdir(path, 0764); err != nil {
if !os.IsExist(err) {
log.Fatal(err)
}
continue
}
}
log.Logger.SetStyles(log.Styles())
charmLog.SetDefault(log.Logger)
if internal.Debug {
log.SetLevel(log.DebugLevel)
log.SetOutput(os.Stderr)
log.SetReportCaller(true)
}
log.Debugf("abra version %s, commit %s", version, commit)
},
}
manCommand := &cobra.Command{
Use: "man [flags]",
Aliases: []string{"m"},
Short: "Generate manpage",
Example: ` # generate the man pages into /usr/local/share/man/man1
sudo abra man
sudo mandb
# read the man pages
man abra
man abra-app-deploy`,
Run: func(cmd *cobra.Command, args []string) {
header := &doc.GenManHeader{
Title: "ABRA",
Section: "1",
}
manDir := "/usr/local/share/man/man1"
if _, err := os.Stat(manDir); os.IsNotExist(err) {
log.Fatalf("unable to proceed, '%s' does not exist?")
}
err := doc.GenManTree(rootCmd, header, manDir)
if err != nil {
log.Fatal(err)
}
log.Info("don't forget to run 'sudo mandb'")
},
}
rootCmd.PersistentFlags().BoolVarP(
&internal.Debug, "debug", "d", false,
"show debug messages",
)
rootCmd.PersistentFlags().BoolVarP(
&internal.NoInput, "no-input", "n", false,
"toggle non-interactive mode",
)
rootCmd.PersistentFlags().BoolVarP(
&internal.Offline, "offline", "o", false,
"prefer offline & filesystem access",
)
catalogue.CatalogueCommand.AddCommand(
catalogue.CatalogueGenerateCommand,
)
server.ServerCommand.AddCommand(
server.ServerAddCommand,
server.ServerListCommand,
server.ServerPruneCommand,
server.ServerRemoveCommand,
)
recipe.RecipeCommand.AddCommand(
recipe.RecipeDiffCommand,
recipe.RecipeFetchCommand,
recipe.RecipeLintCommand,
recipe.RecipeListCommand,
recipe.RecipeNewCommand,
recipe.RecipeReleaseCommand,
recipe.RecipeResetCommand,
recipe.RecipeSyncCommand,
recipe.RecipeUpgradeCommand,
recipe.RecipeVersionCommand,
)
rootCmd.AddCommand(
UpgradeCommand,
AutocompleteCommand,
manCommand,
app.AppCommand,
catalogue.CatalogueCommand,
server.ServerCommand,
recipe.RecipeCommand,
)
app.AppCmdCommand.AddCommand(
app.AppCmdListCommand,
)
app.AppSecretCommand.AddCommand(
app.AppSecretGenerateCommand,
app.AppSecretInsertCommand,
app.AppSecretRmCommand,
app.AppSecretLsCommand,
)
app.AppVolumeCommand.AddCommand(
app.AppVolumeListCommand,
app.AppVolumeRemoveCommand,
)
app.AppCommand.AddCommand(
app.AppRunCommand,
app.AppCmdCommand,
app.AppCheckCommand,
app.AppConfigCommand,
app.AppCpCommand,
app.AppDeployCommand,
app.AppListCommand,
app.AppLogsCommand,
app.AppNewCommand,
app.AppPsCommand,
app.AppRemoveCommand,
app.AppRestartCommand,
app.AppRollbackCommand,
app.AppSecretCommand,
app.AppServicesCommand,
app.AppUndeployCommand,
app.AppUpgradeCommand,
app.AppVolumeCommand,
)
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
}

View File

@ -1,12 +1,11 @@
package server
import (
"context"
"errors"
"os"
"path/filepath"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
contextPkg "coopcloud.tech/abra/pkg/context"
@ -14,15 +13,109 @@ import (
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/server"
sshPkg "coopcloud.tech/abra/pkg/ssh"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var local bool
var localFlag = &cli.BoolFlag{
Name: "local",
Aliases: []string{"l"},
Usage: "Use local server",
Destination: &local,
var ServerAddCommand = &cobra.Command{
Use: "add [[server] | --local] [flags]",
Aliases: []string{"a"},
Short: "Add a new server",
Long: `Add a new server to your configuration so that it can be managed by Abra.
Abra relies on the standard SSH command-line and ~/.ssh/config for client
connection details. You must configure an entry per-host in your ~/.ssh/config
for each server:
Host 1312.net 1312
Hostname 1312.net
User antifa
Port 12345
IdentityFile ~/.ssh/antifa@somewhere
If "--local" is passed, then Abra assumes that the current local server is
intended as the target server. This is useful when you want to have your entire
Co-op Cloud config located on the server itself, and not on your local
developer machine. The domain is then set to "default".`,
Example: " abra server add 1312.net",
Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
if !local {
return autocomplete.ServerNameComplete()
}
return nil, cobra.ShellCompDirectiveDefault
},
Run: func(cmd *cobra.Command, args []string) {
if len(args) > 0 && local {
log.Fatal("cannot use [server] and --local together")
}
if len(args) == 0 && !local {
log.Fatal("missing argument or --local/-l flag")
}
name := "default"
if !local {
name = internal.ValidateDomain(args)
}
// NOTE(d1): reasonable 5 second timeout for connections which can't
// succeed. The connection is attempted twice, so this results in 10
// seconds.
timeout := client.WithTimeout(5)
if local {
created, err := createServerDir(name)
if err != nil {
log.Fatal(err)
}
log.Debugf("attempting to create client for %s", name)
if _, err := client.New(name, timeout); err != nil {
cleanUp(name)
log.Fatal(err)
}
if created {
log.Info("local server successfully added")
} else {
log.Warn("local server already exists")
}
return
}
if _, err := dns.EnsureIPv4(name); err != nil {
log.Warn(err)
}
_, err := createServerDir(name)
if err != nil {
log.Fatal(err)
}
created, err := newContext(name)
if err != nil {
cleanUp(name)
log.Fatal(err)
}
log.Debugf("attempting to create client for %s", name)
if _, err := client.New(name, timeout); err != nil {
cleanUp(name)
log.Fatal(sshPkg.Fatal(name, err))
}
if created {
log.Infof("%s successfully added", name)
} else {
log.Warnf("%s already exists", name)
}
},
}
// cleanUp cleans up the partially created context/client details for a failed
@ -93,101 +186,16 @@ func createServerDir(name string) (bool, error) {
return true, nil
}
var serverAddCommand = cli.Command{
Name: "add",
Aliases: []string{"a"},
Usage: "Add a new server",
UsageText: "abra server add <domain> [options]",
Description: `Add a new server to your configuration so that it can be managed by Abra.
var (
local bool
)
Abra relies on the standard SSH command-line and ~/.ssh/config for client
connection details. You must configure an entry per-host in your ~/.ssh/config
for each server:
Host example.com example
Hostname example.com
User exampleUser
Port 12345
IdentityFile ~/.ssh/example@somewhere
If "--local" is passed, then Abra assumes that the current local server is
intended as the target server. This is useful when you want to have your entire
Co-op Cloud config located on the server itself, and not on your local
developer machine. The domain is then set to "default".`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
localFlag,
},
Before: internal.SubCommandBefore,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Len() > 0 && local || !internal.ValidateSubCmdFlags(cmd) {
err := errors.New("cannot use <name> and --local together")
internal.ShowSubcommandHelpAndError(cmd, err)
}
var name string
if local {
name = "default"
} else {
name = internal.ValidateDomain(cmd)
}
// NOTE(d1): reasonable 5 second timeout for connections which can't
// succeed. The connection is attempted twice, so this results in 10
// seconds.
timeout := client.WithTimeout(5)
if local {
created, err := createServerDir(name)
if err != nil {
log.Fatal(err)
}
log.Debugf("attempting to create client for %s", name)
if _, err := client.New(name, timeout); err != nil {
cleanUp(name)
log.Fatal(err)
}
if created {
log.Info("local server successfully added")
} else {
log.Warn("local server already exists")
}
return nil
}
if _, err := dns.EnsureIPv4(name); err != nil {
log.Warn(err)
}
_, err := createServerDir(name)
if err != nil {
log.Fatal(err)
}
created, err := newContext(name)
if err != nil {
cleanUp(name)
log.Fatal(err)
}
log.Debugf("attempting to create client for %s", name)
if _, err := client.New(name, timeout); err != nil {
cleanUp(name)
log.Fatal(sshPkg.Fatal(name, err))
}
if created {
log.Infof("%s successfully added", name)
} else {
log.Warnf("%s already exists", name)
}
return nil
},
func init() {
ServerAddCommand.Flags().BoolVarP(
&local,
"local",
"l",
false,
"use local server",
)
}

View File

@ -1,7 +1,6 @@
package server
import (
"context"
"fmt"
"strings"
@ -11,20 +10,15 @@ import (
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/docker/cli/cli/connhelper/ssh"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var serverListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List managed servers",
UsageText: "abra server list [options]",
Flags: []cli.Flag{
internal.MachineReadableFlag,
},
Before: internal.SubCommandBefore,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
var ServerListCommand = &cobra.Command{
Use: "list [flags]",
Aliases: []string{"ls"},
Short: "List managed servers",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
dockerContextStore := contextPkg.NewDefaultDockerContextStore()
contexts, err := dockerContextStore.Store.List()
if err != nil {
@ -86,12 +80,22 @@ var serverListCommand = cli.Command{
if err != nil {
log.Fatal("unable to render to JSON: %s", err)
}
fmt.Println(out)
return nil
return
}
fmt.Println(table)
return nil
},
}
func init() {
ServerListCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
false,
"print machine-readable output",
)
}

View File

@ -1,62 +1,41 @@
package server
import (
"context"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/docker/docker/api/types/filters"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var allFilter bool
var ServerPruneCommand = &cobra.Command{
Use: "prune <server> [flags]",
Aliases: []string{"p"},
Short: "Prune resources on a server",
Long: `Prunes unused containers, networks, and dangling images.
var allFilterFlag = &cli.BoolFlag{
Name: "all",
Aliases: []string{"a"},
Usage: "Remove all unused images not just dangling ones",
Destination: &allFilter,
}
var volumesFilter bool
var volumesFilterFlag = &cli.BoolFlag{
Name: "volumes",
Aliases: []string{"v"},
Usage: "Prune volumes. This will remove app data, Be Careful!",
Destination: &volumesFilter,
}
var serverPruneCommand = cli.Command{
Name: "prune",
Aliases: []string{"p"},
Usage: "Prune resources on a server",
UsageText: "abra server prune <server> [options]",
Description: `Prunes unused containers, networks, and dangling images.
Use "-v/--volumes" to remove volumes that are not associated with a deployed
Use "--volumes/-v" to remove volumes that are not associated with a deployed
app. This can result in unwanted data loss if not used carefully.`,
Flags: []cli.Flag{
allFilterFlag,
volumesFilterFlag,
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.ServerNameComplete()
},
Before: internal.SubCommandBefore,
ShellComplete: autocomplete.ServerNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
serverName := internal.ValidateServer(cmd)
Run: func(cmd *cobra.Command, args []string) {
serverName := internal.ValidateServer(args)
cl, err := client.New(serverName)
if err != nil {
log.Fatal(err)
}
var args filters.Args
var filterArgs filters.Args
cr, err := cl.ContainersPrune(ctx, args)
cr, err := cl.ContainersPrune(cmd.Context(), filterArgs)
if err != nil {
log.Fatal(err)
}
@ -64,7 +43,7 @@ app. This can result in unwanted data loss if not used carefully.`,
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
log.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed)
nr, err := cl.NetworksPrune(ctx, args)
nr, err := cl.NetworksPrune(cmd.Context(), filterArgs)
if err != nil {
log.Fatal(err)
}
@ -77,7 +56,7 @@ app. This can result in unwanted data loss if not used carefully.`,
pruneFilters.Add("dangling", "false")
}
ir, err := cl.ImagesPrune(ctx, pruneFilters)
ir, err := cl.ImagesPrune(cmd.Context(), pruneFilters)
if err != nil {
log.Fatal(err)
}
@ -86,7 +65,7 @@ app. This can result in unwanted data loss if not used carefully.`,
log.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed)
if volumesFilter {
vr, err := cl.VolumesPrune(ctx, args)
vr, err := cl.VolumesPrune(cmd.Context(), filterArgs)
if err != nil {
log.Fatal(err)
}
@ -95,6 +74,29 @@ app. This can result in unwanted data loss if not used carefully.`,
log.Infof("volumes pruned: %d; space reclaimed: %s", len(vr.VolumesDeleted), volSpaceReclaimed)
}
return nil
return
},
}
var (
allFilter bool
volumesFilter bool
)
func init() {
ServerPruneCommand.Flags().BoolVarP(
&allFilter,
"all",
"a",
false,
"remove all unused images",
)
ServerPruneCommand.Flags().BoolVarP(
&volumesFilter,
"volumes",
"v",
false,
"remove volumes",
)
}

View File

@ -1,7 +1,6 @@
package server
import (
"context"
"os"
"path/filepath"
@ -10,24 +9,27 @@ import (
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli/v3"
"github.com/spf13/cobra"
)
var serverRemoveCommand = cli.Command{
Name: "remove",
Aliases: []string{"rm"},
UsageText: "abra server remove <domain> [options]",
Usage: "Remove a managed server",
Description: `Remove a managed server.
var ServerRemoveCommand = &cobra.Command{
Use: "remove <server> [flags]",
Aliases: []string{"rm"},
Short: "Remove a managed server",
Long: `Remove a managed server.
Abra will remove the internal bookkeeping ($ABRA_DIR/servers/...) and
underlying client connection context. This server will then be lost in time,
like tears in rain.`,
Before: internal.SubCommandBefore,
ShellComplete: autocomplete.ServerNameComplete,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
serverName := internal.ValidateServer(cmd)
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.ServerNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
serverName := internal.ValidateServer(args)
if err := client.DeleteContext(serverName); err != nil {
log.Fatal(err)
@ -39,6 +41,6 @@ like tears in rain.`,
log.Infof("%s is now lost in time, like tears in rain", serverName)
return nil
return
},
}

View File

@ -1,19 +1,10 @@
package server
import (
"github.com/urfave/cli/v3"
)
import "github.com/spf13/cobra"
// ServerCommand defines the `abra server` command and its subcommands
var ServerCommand = cli.Command{
Name: "server",
Aliases: []string{"s"},
Usage: "Manage servers",
UsageText: "abra server [command] [arguments] [options]",
Commands: []*cli.Command{
&serverAddCommand,
&serverListCommand,
&serverRemoveCommand,
&serverPruneCommand,
},
var ServerCommand = &cobra.Command{
Use: "server [cmd] [args] [flags]",
Aliases: []string{"s"},
Short: "Manage servers",
}

View File

@ -21,46 +21,25 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerclient "github.com/docker/docker/client"
"github.com/spf13/cobra"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli/v3"
)
const SERVER = "localhost"
var majorUpdate bool
var majorFlag = &cli.BoolFlag{
Name: "major",
Aliases: []string{"m"},
Usage: "Also check for major updates",
Destination: &majorUpdate,
}
var updateAll bool
var allFlag = &cli.BoolFlag{
Name: "all",
Aliases: []string{"a"},
Usage: "Update all deployed apps",
Destination: &updateAll,
}
// Notify checks for available upgrades
var Notify = cli.Command{
Name: "notify",
Aliases: []string{"n"},
Usage: "Check for available upgrades",
UsageText: "kadabra notify [options]",
Flags: []cli.Flag{
majorFlag,
},
Before: internal.SubCommandBefore,
Description: `Notify on new versions for deployed apps.
// NotifyCommand checks for available upgrades.
var NotifyCommand = &cobra.Command{
Use: "notify [flags]",
Aliases: []string{"n"},
Short: "Check for available upgrades",
Long: `Notify on new versions for deployed apps.
If a new patch/minor version is available, a notification is printed.
Use "--major" to include new major versions.`,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
Use "--major/-m" to include new major versions.`,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
cl, err := client.New("default")
if err != nil {
log.Fatal(err)
@ -85,24 +64,15 @@ Use "--major" to include new major versions.`,
}
}
}
return nil
},
}
// UpgradeApp upgrades apps.
var UpgradeApp = cli.Command{
Name: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade apps",
UsageText: "kadabra notify <stack> <recipe> [options]",
Flags: []cli.Flag{
internal.ChaosFlag,
majorFlag,
allFlag,
},
Before: internal.SubCommandBefore,
Description: `Upgrade an app by specifying stack name and recipe.
// UpgradeCommand upgrades apps.
var UpgradeCommand = &cobra.Command{
Use: "upgrade [[stack] [recipe] | --all] [flags]",
Aliases: []string{"u"},
Short: "Upgrade apps",
Long: `Upgrade an app by specifying stack name and recipe.
Use "--all" to upgrade every deployed app.
@ -110,25 +80,37 @@ For each app with auto updates enabled, the deployed version is compared with
the current recipe catalogue version. If a new patch/minor version is
available, the app is upgraded.
To include major versions use the "--major" flag. You probably don't want that
as it will break things. Only apps that are not deployed with "--chaos" are
upgraded, to update chaos deployments use the "--chaos" flag. Use it with care.`,
HideHelp: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
To include major versions use the "--major/-m" flag. You probably don't want
that as it will break things. Only apps that are not deployed with "--chaos/-C"
are upgraded, to update chaos deployments use the "--chaos/-C" flag. Use it
with care.`,
Args: cobra.RangeArgs(0, 2),
// TODO(d1): complete stack/recipe
// ValidArgsFunction: func(
// cmd *cobra.Command,
// args []string,
// toComplete string) ([]string, cobra.ShellCompDirective) {
// },
Run: func(cmd *cobra.Command, args []string) {
cl, err := client.New("default")
if err != nil {
log.Fatal(err)
}
if !updateAll && len(args) != 2 {
log.Fatal("missing arguments or --all/-a flag")
}
if !updateAll {
stackName := cmd.Args().Get(0)
recipeName := cmd.Args().Get(1)
stackName := args[0]
recipeName := args[1]
err = tryUpgrade(cl, stackName, recipeName)
if err != nil {
log.Fatal(err)
}
return nil
return
}
stacks, err := stack.GetStacks(cl)
@ -148,8 +130,6 @@ upgraded, to update chaos deployments use the "--chaos" flag. Use it with care.`
log.Fatal(err)
}
}
return nil
},
}
@ -309,7 +289,7 @@ func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName
return nil, err
}
if 0 < versionDelta.UpgradeType() && (versionDelta.UpgradeType() < 4 || majorUpdate) {
if 0 < versionDelta.UpgradeType() && (versionDelta.UpgradeType() < 4 || includeMajorUpdates) {
availableUpgrades = append(availableUpgrades, version)
}
}
@ -466,48 +446,87 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion stri
return err
}
func newKadabraApp(version, commit string) *cli.Command {
app := &cli.Command{
Name: "kadabra",
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Usage: "The Co-op Cloud auto-updater 🤖 🚀",
UsageText: "kadabra [command] [options]",
UseShortOptionHandling: true,
HideHelpCommand: true,
Flags: []cli.Flag{
// NOTE(d1): "GLOBAL OPTIONS" flags
internal.DebugFlag,
internal.NoInputFlag,
},
Commands: []*cli.Command{
&Notify,
&UpgradeApp,
func newKadabraApp(version, commit string) *cobra.Command {
rootCmd := &cobra.Command{
Use: "kadabra [cmd] [flags]",
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Short: "The Co-op Cloud auto-updater 🤖 🚀",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.Logger.SetStyles(log.Styles())
charmLog.SetDefault(log.Logger)
if internal.Debug {
log.SetLevel(log.DebugLevel)
log.SetOutput(os.Stderr)
log.SetReportCaller(true)
}
log.Debugf("kadabra version %s, commit %s", version, commit)
},
}
app.Before = func(ctx context.Context, cmd *cli.Command) error {
log.Logger.SetStyles(log.Styles())
charmLog.SetDefault(log.Logger)
rootCmd.PersistentFlags().BoolVarP(
&internal.Debug, "debug", "d", false,
"show debug messages",
)
log.Debugf("kadabra version %s, commit %s", version, commit)
rootCmd.PersistentFlags().BoolVarP(
&internal.NoInput, "no-input", "n", false,
"toggle non-interactive mode",
)
return nil
}
rootCmd.AddCommand(
NotifyCommand,
UpgradeCommand,
)
cli.HelpFlag = &cli.BoolFlag{
Name: "help",
Aliases: []string{"h, H"},
Usage: "Show help",
}
return app
return rootCmd
}
// RunApp runs CLI abra app.
func RunApp(version, commit string) {
app := newKadabraApp(version, commit)
if err := app.Run(context.Background(), os.Args); err != nil {
if err := app.Execute(); err != nil {
log.Fatal(err)
}
}
var (
includeMajorUpdates bool
updateAll bool
)
func init() {
NotifyCommand.Flags().BoolVarP(
&includeMajorUpdates,
"major",
"m",
false,
"check for major updates",
)
UpgradeCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
UpgradeCommand.Flags().BoolVarP(
&includeMajorUpdates,
"major",
"m",
false,
"check for major updates",
)
UpgradeCommand.Flags().BoolVarP(
&updateAll,
"all",
"a",
false,
"update all deployed apps",
)
}

56
cli/upgrade.go Normal file
View File

@ -0,0 +1,56 @@
// Package cli provides the interface for the command-line.
package cli
import (
"fmt"
"os/exec"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra"
)
// UpgradeCommand upgrades abra in-place.
var UpgradeCommand = &cobra.Command{
Use: "upgrade [flags]",
Aliases: []string{"u"},
Short: "Upgrade abra",
Long: `Upgrade abra in-place with the latest stable or release candidate.
By default, the latest stable release is downloaded.
Use "--rc/-r" to install the latest release candidate. Please bear in mind that
it may contain absolutely catastrophic deal-breaker bugs. Thank you very much
for the testing efforts 💗`,
Example: " abra upgrade --rc",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
mainURL := "https://install.abra.coopcloud.tech"
c := exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash", mainURL))
if releaseCandidate {
releaseCandidateURL := "https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer"
c = exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash -s -- --rc", releaseCandidateURL))
}
log.Debugf("attempting to run %s", c)
if err := internal.RunCmd(c); err != nil {
log.Fatal(err)
}
},
}
var (
releaseCandidate bool
)
func init() {
UpgradeCommand.Flags().BoolVarP(
&releaseCandidate,
"rc",
"r",
false,
"install release candidate (may contain bugs)",
)
}