0
0
forked from toolshed/abra

Compare commits

...

43 Commits

Author SHA1 Message Date
5a4dac7e76 Merge remote-tracking branch 'upstream/main' into deploy-relax-force 2024-12-30 16:44:29 +01:00
628a9a4b3f feat: Relaxes --force enforcement on deploy an instead error in some cases 2024-12-30 16:39:20 +01:00
74108b0dd9 fix: create release dir in recipe if not exists #660 2024-12-29 18:12:53 +00:00
3727c7fa78 test: ensure catalogue 2024-12-29 00:44:47 +01:00
9a4414fd13 test: fix failing upgrade test 2024-12-29 00:14:16 +01:00
9f189680f3 fix: less newline 2024-12-28 23:47:50 +01:00
356e527f1f refactor!: upgrade/rollback vertical render / ui fixes
See toolshed/organising#658
2024-12-28 23:35:47 +01:00
7ec61c6d03 fix: sort versions upgrade/rollback/list
See toolshed/organising#649
2024-12-28 23:10:22 +01:00
fab93a559a fix: more robust <app> autocomplete + error handling
See toolshed/organising#652
2024-12-28 22:22:13 +01:00
8ac31330be fix: error out if missing "deploy.labels"
See toolshed/organising#643
2024-12-28 21:55:02 +01:00
03000c25e0 refactor: parametrize default value 2024-12-28 21:54:14 +01:00
3f32dbb1a3 fix: better "server add" failure
See toolshed/organising#570
2024-12-28 21:17:51 +01:00
27f68b1dc5 refactor!: recipe fetch [recipe | --all]
See toolshed/organising#639
2024-12-28 20:55:25 +01:00
a0da5299fe feat: write undeploy version
See toolshed/organising#633
2024-12-28 19:42:01 +00:00
866c5c4536 test: even moar integration suite patches 2024-12-28 17:16:53 +01:00
dc4c6784cb test: integration test patches 2024-12-28 16:39:58 +01:00
97959ef5da refactor!: vertical render & UI/UX fixes
See coop-cloud/abra#454
2024-12-28 15:00:31 +00:00
b6573720ec fix: Adds chaos flag to app/cp command 2024-12-28 13:56:43 +01:00
4e8995cc0e fix: moar integration test patches
See toolshed/organising#650
2024-12-27 21:55:01 +01:00
efb3fd8759 test: moar fixes
See toolshed/organising#650
2024-12-27 21:16:15 +01:00
008582c3d9 test: fixes for test suite post-cobra migrate
See toolshed/organising#650
2024-12-27 20:44:07 +01:00
8fa20e2c7f feat: new backup/restore 2024-12-27 19:27:56 +01:00
aa1dc795ef fix: disable default complete func 2024-12-27 13:55:45 +01:00
18df498295 chore: deps and vendor 2024-12-27 13:47:45 +01:00
671e1ca276 refactor!: cobra migrate 2024-12-27 13:32:29 +01:00
0df2b15c33 fix: reinstate no-input as a global flag 2024-12-23 11:27:11 +01:00
3f29084664 chore: refactor / docstrings 2024-12-21 19:22:26 +01:00
0bb25a00ec test: migrated server 2024-12-21 19:21:50 +01:00
28c7676413 replace code-descriptive comments with method level comments 2024-12-15 09:53:28 -08:00
730fef09a3 add test for SwitchToMain 2024-12-14 18:41:34 -08:00
8d076a308a bubble up errors on branch switch 2024-12-14 18:26:22 -08:00
9510c04aeb new recipe default branch main instead of master 2024-12-12 19:08:18 -08:00
d9e60afd71 chore: upgrade go version 2024-12-02 01:47:45 +01:00
31fa9b1a7a chore: make deps, go mod vendor 2024-12-02 01:45:06 +01:00
f664599836 [fix] chaos mode always fails deploy 2024-11-30 20:10:04 -08:00
bba1640913 Merge branch 'ammaratef45-removeDomainCheck' 2024-11-27 11:48:12 -08:00
7b54c2b5b9 remove whitespace 2024-11-27 11:38:49 -08:00
8ee1947fe9 remove -D on server add 2024-11-25 17:23:00 -08:00
b313b0a145 fix: use old auto-completion for 0.9.x compat
See toolshed/organising#644

Partial revert of 1f8662cd95
2024-10-27 08:54:43 +01:00
1f9b863be0 fix: appease formatter, ignore vendor 2024-10-21 16:46:39 +02:00
3b3ce85ef9 fix: rebase coop-cloud/organising#533 2024-10-21 16:39:36 +02:00
1f8662cd95 refactor: urfave v3 2024-10-21 16:39:27 +02:00
375e17a4a0 refactor: urfave v2 2024-10-21 11:00:35 +02:00
1044 changed files with 59369 additions and 30621 deletions

View File

@ -3,12 +3,12 @@ kind: pipeline
name: coopcloud.tech/abra name: coopcloud.tech/abra
steps: steps:
- name: make check - name: make check
image: golang:1.21 image: golang:1.22
commands: commands:
- make check - make check
- name: make test - name: make test
image: golang:1.21 image: golang:1.22
environment: environment:
CATL_URL: https://git.coopcloud.tech/coop-cloud/recipes-catalogue-json.git CATL_URL: https://git.coopcloud.tech/coop-cloud/recipes-catalogue-json.git
commands: commands:

2
.gitignore vendored
View File

@ -2,7 +2,7 @@
.e2e.env .e2e.env
.envrc .envrc
.vscode/ .vscode/
/abra
/kadabra /kadabra
abra
dist/ dist/
tests/integration/.bats tests/integration/.bats

View File

@ -1,5 +1,5 @@
# Build image # Build image
FROM golang:1.21-alpine AS build FROM golang:1.22-alpine AS build
ENV GOPRIVATE coopcloud.tech ENV GOPRIVATE coopcloud.tech

View File

@ -2,7 +2,7 @@ ABRA := ./cmd/abra
KADABRA := ./cmd/kadabra KADABRA := ./cmd/kadabra
COMMIT := $(shell git rev-list -1 HEAD) COMMIT := $(shell git rev-list -1 HEAD)
GOPATH := $(shell go env GOPATH) GOPATH := $(shell go env GOPATH)
GOVERSION := 1.21 GOVERSION := 1.22
LDFLAGS := "-X 'main.Commit=$(COMMIT)'" LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
DIST_LDFLAGS := $(LDFLAGS)" -s -w" DIST_LDFLAGS := $(LDFLAGS)" -s -w"
GCFLAGS := "all=-l -B" GCFLAGS := "all=-l -B"
@ -45,10 +45,10 @@ clean:
@rm '$(GOPATH)/bin/kadabra' @rm '$(GOPATH)/bin/kadabra'
format: format:
@gofmt -s -w . @gofmt -s -w $$(find . -type f -name '*.go' | grep -v "/vendor/")
check: check:
@test -z $$(gofmt -l .) || \ @test -z $$(gofmt -l $$(find . -type f -name '*.go' | grep -v "/vendor/")) || \
(echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1) (echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1)
test: test:

View File

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

View File

@ -7,44 +7,76 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var snapshot string var AppBackupListCommand = &cobra.Command{
var snapshotFlag = &cli.StringFlag{ Use: "list <app> [flags]",
Name: "snapshot, s",
Usage: "Lists specific snapshot",
Destination: &snapshot,
}
var includePath string
var includePathFlag = &cli.StringFlag{
Name: "path, p",
Usage: "Include path",
Destination: &includePath,
}
var resticRepo string
var resticRepoFlag = &cli.StringFlag{
Name: "repo, r",
Usage: "Restic repository",
Destination: &resticRepo,
}
var appBackupListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Flags: []cli.Flag{ Short: "List the contents of a snapshot",
internal.DebugFlag, Args: cobra.ExactArgs(1),
internal.OfflineFlag, ValidArgsFunction: func(
snapshotFlag, cmd *cobra.Command,
includePathFlag, args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
Usage: "List all backups", app := internal.ValidateApp(args)
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { cl, err := client.New(app.Server)
app := internal.ValidateApp(c) 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),
"MACHINE_LOGS=true",
}
if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
}
if showAllPaths {
log.Debugf("including SHOW_ALL=%v in backupbot exec invocation", showAllPaths)
execEnv = append(execEnv, fmt.Sprintf("SHOW_ALL=%v", showAllPaths))
}
if timestamps {
log.Debugf("including TIMESTAMPS=%v in backupbot exec invocation", timestamps)
execEnv = append(execEnv, fmt.Sprintf("TIMESTAMPS=%v", timestamps))
}
if _, err = internal.RunBackupCmdRemote(cl, "ls", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
}
},
}
var AppBackupDownloadCommand = &cobra.Command{
Use: "download <app> [flags]",
Aliases: []string{"d"},
Short: "Download a snapshot",
Long: `Downloads a backup.tar.gz to the current working directory.
"--volumes/-v" includes data contained in volumes alongide paths specified in
"backupbot.backup.path" labels.`,
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 { if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err) log.Fatal(err)
@ -60,80 +92,32 @@ var appBackupListCommand = cli.Command{
log.Fatal(err) log.Fatal(err)
} }
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)} execEnv := []string{
fmt.Sprintf("SERVICE=%s", app.Domain),
"MACHINE_LOGS=true",
}
if snapshot != "" { if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot) log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot)) execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
} }
if includePath != "" { if includePath != "" {
log.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath) log.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath)
execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath)) execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath))
} }
if err := internal.RunBackupCmdRemote(cl, "ls", targetContainer.ID, execEnv); err != nil { if includeSecrets {
log.Fatal(err) log.Debugf("including SECRETS=%v in backupbot exec invocation", includeSecrets)
execEnv = append(execEnv, fmt.Sprintf("SECRETS=%v", includeSecrets))
} }
return nil if includeVolumes {
}, log.Debugf("including VOLUMES=%v in backupbot exec invocation", includeVolumes)
} execEnv = append(execEnv, fmt.Sprintf("VOLUMES=%v", includeVolumes))
var appBackupDownloadCommand = cli.Command{
Name: "download",
Aliases: []string{"d"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
snapshotFlag,
includePathFlag,
},
Before: internal.SubCommandBefore,
Usage: "Download a backup",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)
} }
if !internal.Chaos { if _, err := internal.RunBackupCmdRemote(cl, "download", targetContainer.ID, execEnv); err != nil {
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) log.Fatal(err)
} }
@ -142,47 +126,27 @@ var appBackupDownloadCommand = cli.Command{
if err = CopyFromContainer(cl, targetContainer.ID, remoteBackupDir, currentWorkingDir); err != nil { if err = CopyFromContainer(cl, targetContainer.ID, remoteBackupDir, currentWorkingDir); err != nil {
log.Fatal(err) log.Fatal(err)
} }
fmt.Println("backup successfully downloaded to current working directory")
return nil
}, },
} }
var appBackupCreateCommand = cli.Command{ var AppBackupCreateCommand = &cobra.Command{
Name: "create", Use: "create <app> [flags]",
Aliases: []string{"c"}, Aliases: []string{"c"},
Flags: []cli.Flag{ Short: "Create a new snapshot",
internal.DebugFlag, Args: cobra.ExactArgs(1),
internal.OfflineFlag, ValidArgsFunction: func(
resticRepoFlag, cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
Usage: "Create a new backup", app := internal.ValidateApp(args)
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.EnsureExists(); err != nil { if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err) 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) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -193,53 +157,35 @@ var appBackupCreateCommand = cli.Command{
log.Fatal(err) log.Fatal(err)
} }
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)} execEnv := []string{
if resticRepo != "" { fmt.Sprintf("SERVICE=%s", app.Domain),
log.Debugf("including RESTIC_REPO=%s in backupbot exec invocation", resticRepo) "MACHINE_LOGS=true",
execEnv = append(execEnv, fmt.Sprintf("RESTIC_REPO=%s", resticRepo))
} }
if err := internal.RunBackupCmdRemote(cl, "create", targetContainer.ID, execEnv); err != nil { if retries != "" {
log.Debugf("including RETRIES=%s in backupbot exec invocation", retries)
execEnv = append(execEnv, fmt.Sprintf("RETRIES=%s", retries))
}
if _, err := internal.RunBackupCmdRemote(cl, "create", targetContainer.ID, execEnv); err != nil {
log.Fatal(err) log.Fatal(err)
} }
return nil
}, },
} }
var appBackupSnapshotsCommand = cli.Command{ var AppBackupSnapshotsCommand = &cobra.Command{
Name: "snapshots", Use: "snapshots <app> [flags]",
Aliases: []string{"s"}, Aliases: []string{"s"},
Flags: []cli.Flag{ Short: "List all snapshots",
internal.DebugFlag, Args: cobra.ExactArgs(1),
internal.OfflineFlag, ValidArgsFunction: func(
snapshotFlag, cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
Usage: "List backup snapshots", app := internal.ValidateApp(args)
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
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) cl, err := client.New(app.Server)
if err != nil { if err != nil {
@ -251,29 +197,111 @@ var appBackupSnapshotsCommand = cli.Command{
log.Fatal(err) log.Fatal(err)
} }
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)} execEnv := []string{
if snapshot != "" { fmt.Sprintf("SERVICE=%s", app.Domain),
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot) "MACHINE_LOGS=true",
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
} }
if err := internal.RunBackupCmdRemote(cl, "snapshots", targetContainer.ID, execEnv); err != nil { if _, err = internal.RunBackupCmdRemote(cl, "snapshots", targetContainer.ID, execEnv); err != nil {
log.Fatal(err) log.Fatal(err)
} }
return nil
}, },
} }
var appBackupCommand = cli.Command{ var AppBackupCommand = &cobra.Command{
Name: "backup", Use: "backup [cmd] [args] [flags]",
Aliases: []string{"b"}, Aliases: []string{"b"},
Usage: "Manage app backups", Short: "Manage app backups",
ArgsUsage: "<domain>", }
Subcommands: []cli.Command{
appBackupListCommand, var (
appBackupSnapshotsCommand, snapshot string
appBackupDownloadCommand, retries string
appBackupCreateCommand, includePath string
}, showAllPaths bool
timestamps bool
includeSecrets bool
includeVolumes bool
)
func init() {
AppBackupListCommand.Flags().StringVarP(
&snapshot,
"snapshot",
"s",
"",
"list specific snapshot",
)
AppBackupListCommand.Flags().BoolVarP(
&showAllPaths,
"all",
"a",
false,
"show all paths",
)
AppBackupListCommand.Flags().BoolVarP(
&timestamps,
"timestamps",
"t",
false,
"include timestamps",
)
AppBackupDownloadCommand.Flags().StringVarP(
&snapshot,
"snapshot",
"s",
"",
"list specific snapshot",
)
AppBackupDownloadCommand.Flags().StringVarP(
&includePath,
"path",
"p",
"",
"volumes path",
)
AppBackupDownloadCommand.Flags().BoolVarP(
&includeSecrets,
"secrets",
"S",
false,
"include secrets",
)
AppBackupDownloadCommand.Flags().BoolVarP(
&includeVolumes,
"volumes",
"v",
false,
"include volumes",
)
AppBackupDownloadCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
AppBackupCreateCommand.Flags().StringVarP(
&retries,
"retries",
"r",
"1",
"number of retry attempts",
)
AppBackupCreateCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
} }

View File

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

View File

@ -5,68 +5,102 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"slices"
"sort" "sort"
"strings" "strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var appCmdCommand = cli.Command{ var AppCmdCommand = &cobra.Command{
Name: "command", Use: "command <app> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]",
Aliases: []string{"cmd"}, Aliases: []string{"cmd"},
Usage: "Run app commands", Short: "Run app commands",
Description: `Run an app specific command. Long: `Run an app specific command.
These commands are bash functions, defined in the abra.sh of the recipe itself. These commands are bash functions, defined in the abra.sh of the recipe itself.
They can be run within the context of a service (e.g. app) or locally on your They can be run within the context of a service (e.g. app) or locally on your
work station by passing "--local". Arguments can be passed into these functions work station by passing "--local/-l".
using the "-- <args>" syntax.
**WARNING**: options must be passed directly after the sub-command "cmd". 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
EXAMPLE: # pass <cmd> args/flags with "--"
abra app cmd 1312.net app my_cmd_args --user bar -- foo -vvv
abra app cmd --local example.com app create_user -- me@example.com`, # drop the [service] arg if using "--local/-l"
ArgsUsage: "<domain> [<service>] <command> [-- <args>]", abra app cmd 1312.net my_cmd --local`,
Flags: []cli.Flag{ Args: func(cmd *cobra.Command, args []string) error {
internal.DebugFlag, if local {
internal.LocalCmdFlag, if !(len(args) >= 2) {
internal.RemoteUserFlag, return errors.New("requires at least 2 arguments with --local/-l")
internal.TtyFlag, }
internal.OfflineFlag,
internal.ChaosFlag, 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
return nil
}
if !(len(args) >= 3) {
return errors.New("requires at least 3 arguments")
}
return nil
}, },
Before: internal.SubCommandBefore, ValidArgsFunction: func(
Subcommands: []cli.Command{appCmdListCommand}, cmd *cobra.Command,
BashComplete: func(ctx *cli.Context) { args []string,
args := ctx.Args() toComplete string) ([]string, cobra.ShellCompDirective) {
switch len(args) { switch l := len(args); l {
case 0: case 0:
autocomplete.AppNameComplete(ctx) return autocomplete.AppNameComplete()
case 1: case 1:
autocomplete.ServiceNameComplete(args.Get(0)) if !local {
return autocomplete.ServiceNameComplete(args[0])
}
return autocomplete.CommandNameComplete(args[0])
case 2: case 2:
cmdNameComplete(args.Get(0)) if !local {
return autocomplete.CommandNameComplete(args[0])
}
return nil, cobra.ShellCompDirectiveDefault
default:
return nil, cobra.ShellCompDirectiveError
} }
}, },
Action: func(c *cli.Context) error { Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(c) app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if internal.LocalCmd && internal.RemoteUser != "" { if local && remoteUser != "" {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use --local & --user together")) log.Fatal("cannot use --local & --user together")
} }
hasCmdArgs, parsedCmdArgs := parseCmdArgs(c.Args(), internal.LocalCmd) hasCmdArgs, parsedCmdArgs := parseCmdArgs(args, local)
if _, err := os.Stat(app.Recipe.AbraShPath); err != nil { if _, err := os.Stat(app.Recipe.AbraShPath); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
@ -75,12 +109,8 @@ EXAMPLE:
log.Fatal(err) log.Fatal(err)
} }
if internal.LocalCmd { if local {
if !(len(c.Args()) >= 2) { cmdName := args[1]
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments"))
}
cmdName := c.Args().Get(1)
if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil { if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -111,53 +141,78 @@ EXAMPLE:
if err := internal.RunCmd(cmd); err != nil { if err := internal.RunCmd(cmd); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} else {
if !(len(c.Args()) >= 3) {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments"))
}
targetServiceName := c.Args().Get(1) return
}
cmdName := c.Args().Get(2) cmdName := args[2]
if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil { if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
log.Fatal(err) log.Fatal(err)
} }
serviceNames, err := appPkg.GetAppServiceNames(app.Name) serviceNames, err := appPkg.GetAppServiceNames(app.Name)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
matchingServiceName := false matchingServiceName := false
for _, serviceName := range serviceNames { targetServiceName := args[1]
if serviceName == targetServiceName { for _, serviceName := range serviceNames {
matchingServiceName = true 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)
} }
} }
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)
}
}, },
} }
@ -180,74 +235,42 @@ func parseCmdArgs(args []string, isLocal bool) (bool, string) {
return hasCmdArgs, parsedCmdArgs return hasCmdArgs, parsedCmdArgs
} }
func cmdNameComplete(appName string) { var (
app, err := app.Get(appName) local bool
if err != nil { remoteUser string
return requestTTY bool
} )
cmdNames, _ := getShCmdNames(app)
if err != nil { func init() {
return AppCmdCommand.Flags().BoolVarP(
} &local,
for _, n := range cmdNames { "local",
fmt.Println(n) "l",
} false,
} "run command locally",
)
var appCmdListCommand = cli.Command{
Name: "list", AppCmdCommand.Flags().StringVarP(
Aliases: []string{"ls"}, &remoteUser,
Usage: "List all available commands", "user",
ArgsUsage: "<domain>", "u",
Flags: []cli.Flag{ "",
internal.DebugFlag, "request remote user",
internal.OfflineFlag, )
internal.ChaosFlag,
}, AppCmdCommand.Flags().BoolVarP(
BashComplete: autocomplete.AppNameComplete, &requestTTY,
Before: internal.SubCommandBefore, "tty",
Action: func(c *cli.Context) error { "t",
app := internal.ValidateApp(c) false,
"request remote TTY",
if err := app.Recipe.EnsureExists(); err != nil { )
log.Fatal(err)
} AppCmdCommand.Flags().BoolVarP(
&internal.Chaos,
if !internal.Chaos { "chaos",
if err := app.Recipe.EnsureIsClean(); err != nil { "C",
log.Fatal(err) false,
} "ignore uncommitted recipes changes",
)
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
} }

View File

@ -1,40 +1,35 @@
package app package app
import ( import (
"errors"
"os" "os"
"os/exec" "os/exec"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var appConfigCommand = cli.Command{ var AppConfigCommand = &cobra.Command{
Name: "config", Use: "config <app> [flags]",
Aliases: []string{"cfg"}, Aliases: []string{"cfg"},
Usage: "Edit app config", Short: "Edit app config",
ArgsUsage: "<domain>", Example: " abra config 1312.net",
Flags: []cli.Flag{ Args: cobra.ExactArgs(1),
internal.DebugFlag, ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
appName := c.Args().First()
if appName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no app provided"))
}
files, err := appPkg.LoadAppFiles("") files, err := appPkg.LoadAppFiles("")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
appName := args[0]
appFile, exists := files[appName] appFile, exists := files[appName]
if !exists { if !exists {
log.Fatalf("cannot find app with name %s", appName) log.Fatalf("cannot find app with name %s", appName)
@ -51,14 +46,12 @@ var appConfigCommand = cli.Command{
} }
} }
cmd := exec.Command(ed, appFile.Path) c := exec.Command(ed, appFile.Path)
cmd.Stdin = os.Stdin c.Stdin = os.Stdin
cmd.Stdout = os.Stdout c.Stdout = os.Stdout
cmd.Stderr = os.Stderr c.Stderr = os.Stderr
if err := cmd.Run(); err != nil { if err := c.Run(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
return nil
}, },
} }

View File

@ -22,47 +22,39 @@ import (
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var appCpCommand = cli.Command{ var AppCpCommand = &cobra.Command{
Name: "cp", Use: "cp <app> <src> <dst> [flags]",
Aliases: []string{"c"}, Aliases: []string{"c"},
ArgsUsage: "<domain> <src> <dst>", Short: "Copy files to/from a deployed app service",
Flags: []cli.Flag{ Example: ` # copy myfile.txt to the root of the app service
internal.DebugFlag, abra app cp 1312.net myfile.txt app:/
internal.NoInputFlag,
# 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
}
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
Usage: "Copy files to/from a deployed app service", app := internal.ValidateApp(args)
Description: `
Copy files to and from any app service file system.
If you want to copy a myfile.txt to the root of the app service:
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 .
`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err) log.Fatal(err)
} }
src := c.Args().Get(1) src := args[1]
dst := c.Args().Get(2) dst := args[2]
if src == "" {
log.Fatal("missing <src> argument")
}
if dst == "" {
log.Fatal("missing <dest> argument")
}
srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst) srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -87,8 +79,6 @@ And if you want to copy that file back to your current working directory locally
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
return nil
}, },
} }
@ -381,3 +371,13 @@ func moveFile(sourcePath, destPath string) error {
} }
return nil return nil
} }
func init() {
AppCpCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
}

View File

@ -2,6 +2,7 @@ package app
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
@ -9,6 +10,7 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
"coopcloud.tech/tagcmp"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
@ -17,48 +19,62 @@ import (
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var appDeployCommand = cli.Command{ var AppDeployCommand = &cobra.Command{
Name: "deploy", Use: "deploy <app> [version] [flags]",
Aliases: []string{"d"}, Aliases: []string{"d"},
Usage: "Deploy an app", Short: "Deploy an app",
ArgsUsage: "<domain> [<version>]", Long: `Deploy an app.
Flags: []cli.Flag{
internal.DebugFlag, This command supports chaos operations. Use "--chaos/-c" to deploy your recipe
internal.NoInputFlag, checkout as-is. Recipe commit hashes are also supported values for "[version]".
internal.ForceFlag, Please note, "upgrade"/"rollback" do not support chaos operations.`,
internal.ChaosFlag, Example: ` # standard deployment
internal.NoDomainChecksFlag, abra app deploy 1312.net
internal.DontWaitConvergeFlag,
internal.OfflineFlag, # 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 {
errMsg := fmt.Sprintf("autocomplete failed: %s", err)
return []string{errMsg}, cobra.ShellCompDirectiveError
}
return autocomplete.RecipeVersionComplete(app.Recipe.Name)
default:
return nil, cobra.ShellCompDirectiveDefault
}
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
Description: `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.
EXAMPLE:
abra app deploy foo.example.com
abra app deploy foo.example.com 1.2.3+3.2.1
abra app deploy foo.example.com 1e83340e`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
var warnMessages []string var warnMessages []string
app := internal.ValidateApp(c) app := internal.ValidateApp(args)
stackName := app.StackName() stackName := app.StackName()
specificVersion := c.Args().Get(1) ok, err := validateChaosXORVersion(args)
if specificVersion != "" && internal.Chaos { if !ok {
log.Fatal("cannot use <version> and --chaos together") log.Fatalf(err.Error())
} }
specificVersion := getSpecifiedVersion(args)
if specificVersion != "" { if specificVersion != "" {
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion) log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion)
app.Recipe.Version = specificVersion app.Recipe.Version = specificVersion
@ -96,19 +112,19 @@ EXAMPLE:
// is because we need to deal with GetComposeFiles under the hood and these // is because we need to deal with GetComposeFiles under the hood and these
// files change from version to version which therefore affects which // files change from version to version which therefore affects which
// secrets might be generated // secrets might be generated
version := deployMeta.Version toDeployVersion := deployMeta.Version
if specificVersion != "" { if specificVersion != "" {
version = specificVersion toDeployVersion = specificVersion
log.Debugf("choosing %s as version to deploy", version) log.Debugf("choosing %s as version to deploy", toDeployVersion)
var err error var err error
isChaosCommit, err = app.Recipe.EnsureVersion(version) isChaosCommit, err = app.Recipe.EnsureVersion(toDeployVersion)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if isChaosCommit { if isChaosCommit {
log.Debugf("assuming '%s' is a chaos commit", version) log.Debugf("assuming '%s' is a chaos commit", toDeployVersion)
internal.Chaos = true internal.Chaos = true
} }
} }
@ -124,14 +140,6 @@ EXAMPLE:
} }
} }
if deployMeta.IsDeployed {
if internal.Force || internal.Chaos {
warnMessages = append(warnMessages, fmt.Sprintf("%s is already deployed", app.Name))
} else {
log.Fatalf("%s is already deployed", app.Name)
}
}
if !internal.Chaos && specificVersion == "" { if !internal.Chaos && specificVersion == "" {
versions, err := app.Recipe.Tags() versions, err := app.Recipe.Tags()
if err != nil { if err != nil {
@ -139,9 +147,9 @@ EXAMPLE:
} }
if len(versions) > 0 && !internal.Chaos { if len(versions) > 0 && !internal.Chaos {
version = versions[len(versions)-1] toDeployVersion = versions[len(versions)-1]
log.Debugf("choosing %s as version to deploy", version) log.Debugf("choosing %s as version to deploy", toDeployVersion)
if _, err := app.Recipe.EnsureVersion(version); err != nil { if _, err := app.Recipe.EnsureVersion(toDeployVersion); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} else { } else {
@ -149,31 +157,55 @@ EXAMPLE:
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
version = formatter.SmallSHA(head.String()) toDeployVersion = formatter.SmallSHA(head.String())
warnMessages = append(warnMessages, fmt.Sprintf("no versions detected, using latest commit"))
} }
} }
chaosVersion := config.CHAOS_DEFAULT toDeployChaosVersion := config.CHAOS_DEFAULT
if internal.Chaos { if internal.Chaos {
warnMessages = append(warnMessages, "chaos mode engaged")
if isChaosCommit { if isChaosCommit {
chaosVersion = specificVersion toDeployChaosVersion = specificVersion
versionLabelLocal, err := app.Recipe.GetVersionLabelLocal() versionLabelLocal, err := app.Recipe.GetVersionLabelLocal()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
version = versionLabelLocal toDeployVersion = versionLabelLocal
} else { } else {
var err error var err error
chaosVersion, err = app.Recipe.ChaosVersion() toDeployChaosVersion, err = app.Recipe.ChaosVersion()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
} }
if deployMeta.IsDeployed {
if !internal.Force {
appStatus, err := app.Status()
if err != nil {
log.Fatal(err)
}
if appStatus.Chaos && !internal.Chaos {
log.Fatalf("%s is deployed from a chaos version. Are you sure the local changes are in the current version (%s)?", app.Name, toDeployVersion)
}
if toDeployVersion != "" && appStatus.Version != "" {
localVersion, err := tagcmp.Parse(toDeployVersion)
if err != nil {
log.Fatal(err)
}
remoteVersion, err := tagcmp.Parse(appStatus.Version)
if err != nil {
log.Fatal(err)
}
if localVersion.IsLessThan(remoteVersion) {
log.Fatalf("%s is deployed at %s. Are you sure you want to downgrade to %s?", app.Name, appStatus.Version, toDeployVersion)
}
}
}
}
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath) abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -202,7 +234,7 @@ EXAMPLE:
appPkg.ExposeAllEnv(stackName, compose, app.Env) appPkg.ExposeAllEnv(stackName, compose, app.Env)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name) appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos) appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
appPkg.SetChaosVersionLabel(compose, stackName, chaosVersion) appPkg.SetChaosVersionLabel(compose, stackName, toDeployChaosVersion)
appPkg.SetUpdateLabel(compose, stackName, app.Env) appPkg.SetUpdateLabel(compose, stackName, app.Env)
envVars, err := appPkg.CheckEnv(app) envVars, err := appPkg.CheckEnv(app)
@ -225,13 +257,24 @@ EXAMPLE:
log.Fatal(err) log.Fatal(err)
} }
} else { } else {
warnMessages = append(warnMessages, "skipping domain checks as no DOMAIN=... configured for app") log.Debug("skipping domain checks as no DOMAIN=... configured for app")
} }
} else { } else {
warnMessages = append(warnMessages, "skipping domain checks as requested") log.Debug("skipping domain checks as requested")
} }
if err := internal.DeployOverview(app, warnMessages, version, chaosVersion); err != nil { deployedVersion := config.NO_VERSION_DEFAULT
if deployMeta.IsDeployed {
deployedVersion = deployMeta.Version
}
if err := internal.DeployOverview(
app,
warnMessages,
deployedVersion,
deployMeta.ChaosVersion,
toDeployVersion,
toDeployChaosVersion); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -253,15 +296,63 @@ EXAMPLE:
} }
} }
app.Recipe.Version = version app.Recipe.Version = toDeployVersion
if chaosVersion != config.CHAOS_DEFAULT { if toDeployChaosVersion != config.CHAOS_DEFAULT {
app.Recipe.Version = chaosVersion app.Recipe.Version = toDeployChaosVersion
} }
log.Debugf("choosing %s as version to save to env file", app.Recipe.Version) log.Debugf("choosing %s as version to save to env file", app.Recipe.Version)
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil { if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err) log.Fatalf("writing new recipe version in env file: %s", err)
} }
return nil
}, },
} }
// 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
}
// 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",
)
}

57
cli/app/deploy_test.go Normal file
View File

@ -0,0 +1,57 @@
package app
import (
"testing"
"coopcloud.tech/abra/cli/internal"
)
func TestGetSpecificVersion(t *testing.T) {
tests := []struct {
input []string
expectedOutput string
}{
// No specified version when command has one or less args
{[]string{}, ""},
{[]string{"arg0"}, ""},
// Second in arg (index-1) is the specified result when command has more than 1 args
{[]string{"arg0", "arg1"}, "arg1"},
{[]string{"arg0", "arg1", "arg2"}, "arg1"},
}
for _, test := range tests {
if test.expectedOutput != getSpecifiedVersion(test.input) {
t.Fatalf("result for %s should be %s", test.input, test.expectedOutput)
}
}
}
func TestValidateChaosXORVersion(t *testing.T) {
tests := []struct {
input []string
isChaos bool
expectedResult bool
}{
// Chaos = true, Specified Version absent
{[]string{}, true, true},
// Chaos = false, Specified Version absent
{[]string{}, false, true},
// Chaos = true, Specified Version present
{[]string{"arg0", "arg1"}, true, false},
// Chaos = false, Specified Version present
{[]string{"arg0", "arg1", "arg2"}, false, true},
}
for _, test := range tests {
internal.Chaos = test.isChaos
res, _ := validateChaosXORVersion(test.input)
if res != test.expectedResult {
t.Fatalf(
"When args are %s and Chaos mode is %t result needs to be %t",
test.input,
test.isChaos,
test.expectedResult,
)
}
}
}

View File

@ -9,39 +9,11 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/urfave/cli" "github.com/spf13/cobra"
)
var (
status bool
statusFlag = &cli.BoolFlag{
Name: "status, S",
Usage: "Show app deployment status",
Destination: &status,
}
)
var (
recipeFilter string
recipeFlag = &cli.StringFlag{
Name: "recipe, r",
Value: "",
Usage: "Show apps of a specific recipe",
Destination: &recipeFilter,
}
)
var (
listAppServer string
listAppServerFlag = &cli.StringFlag{
Name: "server, s",
Value: "",
Usage: "Show apps of a specific server",
Destination: &listAppServer,
}
) )
type appStatus struct { type appStatus struct {
@ -66,27 +38,23 @@ type serverStatus struct {
UpgradeCount int `json:"upgradeCount"` UpgradeCount int `json:"upgradeCount"`
} }
var appListCommand = cli.Command{ var AppListCommand = &cobra.Command{
Name: "list", Use: "list [flags]",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Usage: "List all managed apps", Short: "List all managed apps",
Description: ` Long: `Generate a report of all managed apps.
Read the local file system listing of apps and servers (e.g. ~/.abra/) to
generate a report of all your apps.
By passing the "--status/-S" flag, you can query all your servers for the Use "--status/-S" flag to query all servers for the live deployment status.`,
actual live deployment status. Depending on how many servers you manage, this Example: ` # list apps of all servers without live status
can take some time.`, abra app ls
Flags: []cli.Flag{
internal.DebugFlag, # list apps of a specific server with live status
internal.MachineReadableFlag, abra app ls -s 1312.net -S
statusFlag,
listAppServerFlag, # list apps of all servers which match a specific recipe
recipeFlag, abra app ls -r gitea`,
internal.OfflineFlag, Args: cobra.NoArgs,
}, Run: func(cmd *cobra.Command, args []string) {
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
appFiles, err := appPkg.LoadAppFiles(listAppServer) appFiles, err := appPkg.LoadAppFiles(listAppServer)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -205,7 +173,7 @@ can take some time.`,
stats.LatestCount++ stats.LatestCount++
} }
} else { } else {
newUpdates = internal.ReverseStringList(newUpdates) newUpdates = internal.SortVersionsDesc(newUpdates)
appStats.Upgrade = strings.Join(newUpdates, "\n") appStats.Upgrade = strings.Join(newUpdates, "\n")
stats.UpgradeCount++ stats.UpgradeCount++
} }
@ -228,7 +196,8 @@ can take some time.`,
} else { } else {
fmt.Println(string(jsonstring)) fmt.Println(string(jsonstring))
} }
return nil
return
} }
alreadySeen := make(map[string]bool) alreadySeen := make(map[string]bool)
@ -239,7 +208,7 @@ can take some time.`,
serverStat := allStats[app.Server] serverStat := allStats[app.Server]
headers := []string{"RECIPE", "DOMAIN"} headers := []string{"RECIPE", "DOMAIN", "SERVER"}
if status { if status {
headers = append(headers, []string{ headers = append(headers, []string{
"STATUS", "STATUS",
@ -259,7 +228,7 @@ can take some time.`,
var rows [][]string var rows [][]string
for _, appStat := range serverStat.Apps { for _, appStat := range serverStat.Apps {
row := []string{appStat.Recipe, appStat.Domain} row := []string{appStat.Recipe, appStat.Domain, appStat.Server}
if status { if status {
chaosStatus := appStat.Chaos chaosStatus := appStat.Chaos
if chaosStatus != "unknown" { if chaosStatus != "unknown" {
@ -287,20 +256,8 @@ can take some time.`,
table.Rows(rows...) table.Rows(rows...)
if len(rows) > 0 { if len(rows) > 0 {
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
if status {
fmt.Println(fmt.Sprintf(
"SERVER: %s | TOTAL APPS: %v | VERSIONED: %v | UNVERSIONED: %v | LATEST : %v | UPGRADE: %v",
app.Server,
serverStat.AppCount,
serverStat.VersionCount,
serverStat.UnversionedCount,
serverStat.LatestCount,
serverStat.UpgradeCount,
))
} else {
log.Infof("SERVER: %s TOTAL APPS: %v", app.Server, serverStat.AppCount)
} }
if len(allStats) > 1 && len(rows) > 0 { if len(allStats) > 1 && len(rows) > 0 {
@ -310,13 +267,59 @@ can take some time.`,
alreadySeen[app.Server] = true alreadySeen[app.Server] = true
} }
if len(allStats) > 1 {
totalServers := formatter.BoldStyle.Render("TOTAL SERVERS")
totalApps := formatter.BoldStyle.Render("TOTAL APPS")
log.Infof("%s: %v | %s: %v ", totalServers, totalServersCount, totalApps, totalAppsCount)
}
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

@ -2,6 +2,7 @@ package app
import ( import (
"context" "context"
"fmt"
"io" "io"
"os" "os"
"slices" "slices"
@ -19,23 +20,34 @@ import (
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var appLogsCommand = cli.Command{ var AppLogsCommand = &cobra.Command{
Name: "logs", Use: "logs <app> [service] [flags]",
Aliases: []string{"l"}, Aliases: []string{"l"},
ArgsUsage: "<domain> [<service>]", Short: "Tail app logs",
Usage: "Tail app logs", Args: cobra.RangeArgs(1, 2),
Flags: []cli.Flag{ ValidArgsFunction: func(
internal.StdErrOnlyFlag, cmd *cobra.Command,
internal.SinceLogsFlag, args []string,
internal.DebugFlag, 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 {
errMsg := fmt.Sprintf("autocomplete failed: %s", err)
return []string{errMsg}, cobra.ShellCompDirectiveError
}
return autocomplete.ServiceNameComplete(app.Name)
default:
return nil, cobra.ShellCompDirectiveDefault
}
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
BashComplete: autocomplete.AppNameComplete, app := internal.ValidateApp(args)
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName() stackName := app.StackName()
if err := app.Recipe.EnsureExists(); err != nil { if err := app.Recipe.EnsureExists(); err != nil {
@ -56,17 +68,14 @@ var appLogsCommand = cli.Command{
log.Fatalf("%s is not deployed?", app.Name) log.Fatalf("%s is not deployed?", app.Name)
} }
serviceName := c.Args().Get(1) var serviceNames []string
serviceNames := []string{} if len(args) == 2 {
if serviceName != "" { serviceNames = []string{args[1]}
serviceNames = []string{serviceName}
}
err = tailLogs(cl, app, serviceNames)
if err != nil {
log.Fatal(err)
} }
return nil if err = tailLogs(cl, app, serviceNames); err != nil {
log.Fatal(err)
}
}, },
} }
@ -112,8 +121,8 @@ func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) er
go func(serviceID string) { go func(serviceID string) {
logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{ logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{
ShowStderr: true, ShowStderr: true,
ShowStdout: !internal.StdErrOnly, ShowStdout: !stdErr,
Since: internal.SinceLogs, Since: sinceLogs,
Until: "", Until: "",
Timestamps: true, Timestamps: true,
Follow: true, Follow: true,
@ -137,3 +146,26 @@ func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) er
return nil 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

@ -16,20 +16,21 @@ import (
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/lipgloss/table" "github.com/charmbracelet/lipgloss/table"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var appNewDescription = ` var appNewDescription = `Creates a new app from a default recipe.
Creates a new app from a default recipe. This new app configuration is stored
in your $ABRA_DIR directory under the appropriate server. This new app configuration is stored in your $ABRA_DIR directory under the
appropriate server.
This command does not deploy your app for you. You will need to run "abra app This command does not deploy your app for you. You will need to run "abra app
deploy <domain>" to do so. deploy <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". 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 Passing the "--secrets/-S" flag will automatically generate secrets for your
app and store them encrypted at rest on the chosen target server. These app and store them encrypted at rest on the chosen target server. These
@ -40,71 +41,76 @@ 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 pass store (see passwordstore.org for more). The pass command must be available
on your $PATH.` on your $PATH.`
var appNewCommand = cli.Command{ var AppNewCommand = &cobra.Command{
Name: "new", Use: "new [recipe] [version] [flags]",
Aliases: []string{"n"}, Aliases: []string{"n"},
Usage: "Create a new app", Short: "Create a new app",
Description: appNewDescription, Long: appNewDescription,
Flags: []cli.Flag{ Args: cobra.RangeArgs(0, 2),
internal.DebugFlag, ValidArgsFunction: func(
internal.NoInputFlag, cmd *cobra.Command,
internal.NewAppServerFlag, args []string,
internal.DomainFlag, toComplete string) ([]string, cobra.ShellCompDirective) {
internal.PassFlag, switch l := len(args); l {
internal.SecretsFlag,
internal.OfflineFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "[<recipe>] [<version>]",
BashComplete: func(ctx *cli.Context) {
args := ctx.Args()
switch len(args) {
case 0: case 0:
autocomplete.RecipeNameComplete(ctx) return autocomplete.RecipeNameComplete()
case 1: case 1:
autocomplete.RecipeVersionComplete(ctx.Args().Get(0)) recipe := internal.ValidateRecipe(args, cmd.Name())
return autocomplete.RecipeVersionComplete(recipe.Name)
default:
return nil, cobra.ShellCompDirectiveDefault
} }
}, },
Action: func(c *cli.Context) error { Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(c) recipe := internal.ValidateRecipe(args, cmd.Name())
if len(args) == 2 && internal.Chaos {
log.Fatal("cannot use [version] and --chaos together")
}
var recipeVersion string
if len(args) == 2 {
recipeVersion = args[1]
}
chaosVersion := config.CHAOS_DEFAULT
if internal.Chaos {
recipeVersion = chaosVersion
var version string
if !internal.Chaos {
if err := recipe.EnsureIsClean(); err != nil {
log.Fatal(err)
}
if !internal.Offline { if !internal.Offline {
if err := recipe.EnsureUpToDate(); err != nil { if err := recipe.EnsureUpToDate(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
}
if c.Args().Get(1) == "" { if !internal.Chaos {
recipeVersions, err := recipe.GetRecipeVersions() if err := recipe.EnsureIsClean(); err != nil {
if err != nil { log.Fatal(err)
log.Fatal(err) }
} }
if len(recipeVersions) > 0 { var recipeVersions recipePkg.RecipeVersions
latest := recipeVersions[len(recipeVersions)-1] if recipeVersion == "" {
for tag := range latest { var err error
version = tag recipeVersions, err = recipe.GetRecipeVersions()
} if err != nil {
log.Fatal(err)
}
}
if _, err := recipe.EnsureVersion(version); err != nil { if len(recipeVersions) > 0 {
log.Fatal(err) latest := recipeVersions[len(recipeVersions)-1]
} for tag := range latest {
} else { recipeVersion = tag
if err := recipe.EnsureLatest(); err != nil { }
log.Fatal(err)
} if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
} log.Fatal(err)
} else { }
version = c.Args().Get(1) } else {
if _, err := recipe.EnsureVersion(version); err != nil { if err := recipe.EnsureLatest(); err != nil {
log.Fatal(err) log.Fatal(err)
}
} }
} }
@ -112,25 +118,25 @@ var appNewCommand = cli.Command{
log.Fatal(err) log.Fatal(err)
} }
if err := ensureDomainFlag(recipe, internal.NewAppServer); err != nil { if err := ensureDomainFlag(recipe, newAppServer); err != nil {
log.Fatal(err) log.Fatal(err)
} }
sanitisedAppName := appPkg.SanitiseAppName(internal.Domain) sanitisedAppName := appPkg.SanitiseAppName(appDomain)
log.Debugf("%s sanitised as %s for new app", internal.Domain, sanitisedAppName) log.Debugf("%s sanitised as %s for new app", appDomain, sanitisedAppName)
if err := appPkg.TemplateAppEnvSample( if err := appPkg.TemplateAppEnvSample(
recipe, recipe,
internal.Domain, appDomain,
internal.NewAppServer, newAppServer,
internal.Domain, appDomain,
); err != nil { ); err != nil {
log.Fatal(err) log.Fatal(err)
} }
var secrets AppSecrets var appSecrets AppSecrets
var secretsTable *table.Table var secretsTable *table.Table
if internal.Secrets { if generateSecrets {
sampleEnv, err := recipe.SampleEnv() sampleEnv, err := recipe.SampleEnv()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -141,21 +147,25 @@ var appNewCommand = cli.Command{
log.Fatal(err) 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 { if err != nil {
return err log.Fatal(err)
} }
if err := promptForSecrets(recipe.Name, secretsConfig); err != nil { if err := promptForSecrets(recipe.Name, secretsConfig); err != nil {
log.Fatal(err) log.Fatal(err)
} }
cl, err := client.New(internal.NewAppServer) cl, err := client.New(newAppServer)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
secrets, err = createSecrets(cl, secretsConfig, sanitisedAppName) appSecrets, err = createSecrets(cl, secretsConfig, sanitisedAppName)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -168,62 +178,43 @@ var appNewCommand = cli.Command{
headers := []string{"NAME", "VALUE"} headers := []string{"NAME", "VALUE"}
secretsTable.Headers(headers...) secretsTable.Headers(headers...)
for name, val := range secrets { for name, val := range appSecrets {
secretsTable.Row(name, val) secretsTable.Row(name, val)
} }
} }
if internal.NewAppServer == "default" { if newAppServer == "default" {
internal.NewAppServer = "local" newAppServer = "local"
} }
table, err := formatter.CreateTable() log.Infof("%s created successfully (version: %s, chaos: %s)", appDomain, recipeVersion, chaosVersion)
if err != nil {
log.Fatal(err)
}
headers := []string{"SERVER", "DOMAIN", "RECIPE", "VERSION"} if len(appSecrets) > 0 {
table.Headers(headers...) rows := [][]string{}
for k, v := range appSecrets {
rows = append(rows, []string{k, v})
}
table.Row(internal.NewAppServer, internal.Domain, recipe.Name, version) overview := formatter.CreateOverview("SECRETS OVERVIEW", rows)
log.Infof("new app '%s' created 🌞", recipe.Name) fmt.Println(overview)
fmt.Println("")
fmt.Println(table)
fmt.Println("")
fmt.Println("Configure this app:")
fmt.Println(fmt.Sprintf("\n abra app config %s", internal.Domain))
fmt.Println("")
fmt.Println("Deploy this app:")
fmt.Println(fmt.Sprintf("\n abra app deploy %s", internal.Domain))
if len(secrets) > 0 {
fmt.Println("")
fmt.Println("Generated secrets:")
fmt.Println("")
fmt.Println(secretsTable)
log.Warnf( log.Warnf(
"generated secrets %s shown again, please take note of them %s", "secrets are %s shown again, please save them %s",
formatter.BoldStyle.Render("NOT"), formatter.BoldStyle.Render("NOT"),
formatter.BoldStyle.Render("NOW"), formatter.BoldStyle.Render("NOW"),
) )
} }
app, err := app.Get(internal.Domain) app, err := app.Get(appDomain)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("choosing %s as version to save to env file", version) log.Debugf("choosing %s as version to save to env file", recipeVersion)
if err := app.WriteRecipeVersion(version, false); err != nil { if err := app.WriteRecipeVersion(recipeVersion, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err) log.Fatalf("writing new recipe version in env file: %s", err)
} }
return nil
}, },
} }
@ -238,19 +229,19 @@ func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secr
sanitisedAppName = sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH] 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 { if err != nil {
return nil, err return nil, err
} }
if internal.Pass { if saveInPass {
for secretName := range secrets { for secretName := range secrets {
secretValue := secrets[secretName] secretValue := secrets[secretName]
if err := secret.PassInsertSecret( if err := secret.PassInsertSecret(
secretValue, secretValue,
secretName, secretName,
internal.Domain, appDomain,
internal.NewAppServer, newAppServer,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -262,17 +253,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/ // ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
func ensureDomainFlag(recipe recipePkg.Recipe, server string) error { func ensureDomainFlag(recipe recipePkg.Recipe, server string) error {
if internal.Domain == "" && !internal.NoInput { if appDomain == "" && !internal.NoInput {
prompt := &survey.Input{ prompt := &survey.Input{
Message: "Specify app domain", Message: "Specify app domain",
Default: fmt.Sprintf("%s.%s", recipe.Name, server), 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 return err
} }
} }
if internal.Domain == "" { if appDomain == "" {
return fmt.Errorf("no domain provided") return fmt.Errorf("no domain provided")
} }
@ -286,11 +277,11 @@ func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret)
return nil return nil
} }
if !internal.Secrets && !internal.NoInput { if !generateSecrets && !internal.NoInput {
prompt := &survey.Confirm{ prompt := &survey.Confirm{
Message: "Generate app secrets?", Message: "Generate app secrets?",
} }
if err := survey.AskOne(prompt, &internal.Secrets); err != nil { if err := survey.AskOne(prompt, &generateSecrets); err != nil {
return err return err
} }
} }
@ -305,19 +296,76 @@ func ensureServerFlag() error {
return err return err
} }
if internal.NewAppServer == "" && !internal.NoInput { if newAppServer == "" && !internal.NoInput {
prompt := &survey.Select{ prompt := &survey.Select{
Message: "Select app server:", Message: "Select app server:",
Options: servers, Options: servers,
} }
if err := survey.AskOne(prompt, &internal.NewAppServer); err != nil { if err := survey.AskOne(prompt, &newAppServer); err != nil {
return err return err
} }
} }
if internal.NewAppServer == "" { if newAppServer == "" {
return fmt.Errorf("no server provided") return fmt.Errorf("no server provided")
} }
return nil 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" containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var appPsCommand = cli.Command{ var AppPsCommand = &cobra.Command{
Name: "ps", Use: "ps <app> [flags]",
Aliases: []string{"p"}, Aliases: []string{"p"},
Usage: "Check app status", Short: "Check app status",
ArgsUsage: "<domain>", Args: cobra.ExactArgs(1),
Description: "Show status of a deployed app.", ValidArgsFunction: func(
Flags: []cli.Flag{ cmd *cobra.Command,
internal.MachineReadableFlag, args []string,
internal.DebugFlag, toComplete string) ([]string, cobra.ShellCompDirective) {
internal.ChaosFlag, return autocomplete.AppNameComplete()
internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
BashComplete: autocomplete.AppNameComplete, app := internal.ValidateApp(args)
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -67,8 +65,6 @@ var appPsCommand = cli.Command{
} }
showPSOutput(app, cl, deployMeta.Version, chaosVersion) showPSOutput(app, cl, deployMeta.Version, chaosVersion)
return nil
}, },
} }
@ -132,24 +128,35 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
allContainerStats[containerStats["service"]] = containerStats allContainerStats[containerStats["service"]] = containerStats
// NOTE(d1): don't clobber these variables for --machine output
dVersion := deployedVersion
cVersion := chaosVersion
if containerStats["service"] != "app" {
// NOTE(d1): don't repeat info which only relevant for the "app" service
dVersion = ""
cVersion = ""
}
row := []string{ row := []string{
containerStats["service"], containerStats["service"],
containerStats["image"], containerStats["image"],
containerStats["created"], dVersion,
cVersion,
containerStats["status"], containerStats["status"],
containerStats["state"],
containerStats["ports"],
} }
rows = append(rows, row) rows = append(rows, row)
} }
if internal.MachineReadable { if internal.MachineReadable {
jsonstring, err := json.Marshal(allContainerStats) rendered, err := json.Marshal(allContainerStats)
if err != nil { if err != nil {
log.Fatal("unable to convert to JSON: %s", err) log.Fatal("unable to convert to JSON: %s", err)
} }
fmt.Println(string(jsonstring))
fmt.Println(string(rendered))
return return
} }
@ -161,17 +168,34 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
headers := []string{ headers := []string{
"SERVICE", "SERVICE",
"IMAGE", "IMAGE",
"CREATED", "VERSION",
"CHAOS",
"STATUS", "STATUS",
"STATE",
"PORTS",
} }
table. table.
Headers(headers...). Headers(headers...).
Rows(rows...) Rows(rows...)
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
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,16 +12,14 @@ import (
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var appRemoveCommand = cli.Command{ var AppRemoveCommand = &cobra.Command{
Name: "remove", Use: "remove <app> [flags]",
Aliases: []string{"rm"}, Aliases: []string{"rm"},
ArgsUsage: "<domain>", Short: "Remove all app data, locally and remotely",
Usage: "Remove all app data, locally and remotely", Long: `Remove everything related to an app which is already undeployed.
Description: `
This command removes everything related to an app which is already undeployed.
By default, it will prompt for confirmation before proceeding. All secrets, By default, it will prompt for confirmation before proceeding. All secrets,
volumes and the local app env file will be deleted. volumes and the local app env file will be deleted.
@ -37,19 +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" To delete everything without prompt, use the "--force/-f" or the "--no-input/n"
flag.`, flag.`,
Flags: []cli.Flag{ Example: " abra app remove 1312.net",
internal.ForceFlag, Args: cobra.ExactArgs(1),
internal.DebugFlag, ValidArgsFunction: func(
internal.NoInputFlag, cmd *cobra.Command,
internal.OfflineFlag, args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
}, },
BashComplete: autocomplete.AppNameComplete, Run: func(cmd *cobra.Command, args []string) {
Before: internal.SubCommandBefore, app := internal.ValidateApp(args)
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if !internal.Force && !internal.NoInput { if !internal.Force && !internal.NoInput {
log.Warnf("ALERTA ALERTA: this will completely remove %s data and config locally and remotely", app.Name) log.Warnf("ALERTA ALERTA: deleting %s data and config (local/remote)", app.Name)
response := false response := false
prompt := &survey.Confirm{Message: "are you sure?"} prompt := &survey.Confirm{Message: "are you sure?"}
@ -132,7 +130,15 @@ flag.`,
} }
log.Info(fmt.Sprintf("file: %s removed", app.Path)) 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 ( import (
"context" "context"
"errors"
"fmt" "fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
@ -12,49 +11,62 @@ import (
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
upstream "coopcloud.tech/abra/pkg/upstream/service" upstream "coopcloud.tech/abra/pkg/upstream/service"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var appRestartCommand = cli.Command{ var AppRestartCommand = &cobra.Command{
Name: "restart", Use: "restart <app> [[service] | --all-services] [flags]",
Aliases: []string{"re"}, Aliases: []string{"re"},
Usage: "Restart an app", Short: "Restart an app",
ArgsUsage: "<domain> [<service>]", Long: `This command restarts services within a deployed app.
Flags: []cli.Flag{
internal.DebugFlag, Run "abra app ps <app>" to see a list of service names.
internal.OfflineFlag,
internal.AllServicesFlag, Pass "--all-services/-a" to restart all services.`,
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
}
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
Description: ` app := internal.ValidateApp(args)
This command restarts services within a deployed app.
Run "abra app ps <domain>" to see a list of service names.
Pass "--all-services/-a" to restart all services.
EXAMPLE:
abra app restart example.com app`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(false, false); err != nil { if err := app.Recipe.Ensure(false, false); err != nil {
log.Fatal(err) log.Fatal(err)
} }
serviceName := c.Args().Get(1) var serviceName string
if serviceName == "" && !internal.AllServices { if len(args) == 2 {
err := errors.New("missing <service>") serviceName = args[1]
internal.ShowSubcommandHelpAndError(c, err)
} }
if serviceName != "" && internal.AllServices { if serviceName == "" && !allServices {
log.Fatal("cannot use <service> and --all-services together") log.Fatal("missing [service]")
}
if serviceName != "" && allServices {
log.Fatal("cannot use [service] and --all-services/-a together")
} }
var serviceNames []string var serviceNames []string
if internal.AllServices { if allServices {
var err error var err error
serviceNames, err = appPkg.GetAppServiceNames(app.Name) serviceNames, err = appPkg.GetAppServiceNames(app.Name)
if err != nil { if err != nil {
@ -105,7 +117,17 @@ EXAMPLE:
log.Debugf("%s has been scaled to 1", stackServiceName) log.Debugf("%s has been scaled to 1", stackServiceName)
log.Infof("%s service successfully restarted", serviceName) 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

@ -2,35 +2,32 @@ package app
import ( import (
"fmt" "fmt"
"strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var targetPath string var AppRestoreCommand = &cobra.Command{
var targetPathFlag = &cli.StringFlag{ Use: "restore <app> [flags]",
Name: "target, t", Aliases: []string{"rs"},
Usage: "Target path", Short: "Restore a snapshot",
Destination: &targetPath, Long: `Snapshots are restored while apps are deployed.
}
var appRestoreCommand = cli.Command{ Some restore scenarios may require service / app restarts.`,
Name: "restore", Args: cobra.ExactArgs(1),
Aliases: []string{"rs"}, ValidArgsFunction: func(
Usage: "Restore an app backup", cmd *cobra.Command,
ArgsUsage: "<domain> <service>", args []string,
Flags: []cli.Flag{ toComplete string) ([]string, cobra.ShellCompDirective) {
internal.DebugFlag, return autocomplete.AppNameComplete()
internal.OfflineFlag,
targetPathFlag,
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
BashComplete: autocomplete.AppNameComplete, app := internal.ValidateApp(args)
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -45,20 +42,94 @@ var appRestoreCommand = cli.Command{
log.Fatal(err) log.Fatal(err)
} }
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)} execEnv := []string{
fmt.Sprintf("SERVICE=%s", app.Domain),
"MACHINE_LOGS=true",
}
if snapshot != "" { if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot) log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot)) execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
} }
if targetPath != "" { if targetPath != "" {
log.Debugf("including TARGET=%s in backupbot exec invocation", targetPath) log.Debugf("including TARGET=%s in backupbot exec invocation", targetPath)
execEnv = append(execEnv, fmt.Sprintf("TARGET=%s", targetPath)) execEnv = append(execEnv, fmt.Sprintf("TARGET=%s", targetPath))
} }
if err := internal.RunBackupCmdRemote(cl, "restore", targetContainer.ID, execEnv); err != nil { if internal.NoInput {
log.Fatal(err) log.Debugf("including NONINTERACTIVE=%v in backupbot exec invocation", internal.NoInput)
execEnv = append(execEnv, fmt.Sprintf("NONINTERACTIVE=%v", internal.NoInput))
} }
return nil if len(volumes) > 0 {
allVolumes := strings.Join(volumes, ",")
log.Debugf("including VOLUMES=%s in backupbot exec invocation", allVolumes)
execEnv = append(execEnv, fmt.Sprintf("VOLUMES=%s", allVolumes))
}
if len(services) > 0 {
allServices := strings.Join(services, ",")
log.Debugf("including CONTAINER=%s in backupbot exec invocation", allServices)
execEnv = append(execEnv, fmt.Sprintf("CONTAINER=%s", allServices))
}
if hooks {
log.Debugf("including NO_COMMANDS=%v in backupbot exec invocation", false)
execEnv = append(execEnv, fmt.Sprintf("NO_COMMANDS=%v", false))
}
if _, err := internal.RunBackupCmdRemote(cl, "restore", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
}
}, },
} }
var (
targetPath string
hooks bool
services []string
volumes []string
)
func init() {
AppRestoreCommand.Flags().StringVarP(
&targetPath,
"target",
"t",
"/",
"target path",
)
AppRestoreCommand.Flags().StringArrayVarP(
&services,
"services",
"s",
[]string{},
"restore specific services",
)
AppRestoreCommand.Flags().StringArrayVarP(
&volumes,
"volumes",
"v",
[]string{},
"restore specific volumes",
)
AppRestoreCommand.Flags().BoolVarP(
&hooks,
"hooks",
"H",
false,
"enable pre/post-hook command execution",
)
AppRestoreCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
}

View File

@ -16,44 +16,55 @@ import (
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var appRollbackCommand = cli.Command{ var AppRollbackCommand = &cobra.Command{
Name: "rollback", Use: "rollback <app> [version] [flags]",
Aliases: []string{"rl"}, Aliases: []string{"rl"},
Usage: "Roll an app back to a previous version", Short: "Roll an app back to a previous version",
ArgsUsage: "<domain> [<version>]", Long: `This command rolls an app back to a previous version.
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `
This command rolls an app back to a previous version.
Unlike "deploy", chaos operations are not supported here. Only recipe versions Unlike "deploy", chaos operations are not supported here. Only recipe versions
are supported values for "[<version>]". are supported values for "[<version>]".
A rollback can be destructive, please ensure you have a copy of your app data A rollback can be destructive, please ensure you have a copy of your app data
beforehand. beforehand.`,
Example: ` # standard rollback
abra app rollback 1312.net
EXAMPLE: # rollback to specific version
abra app rollback 1312.net 2.0.0+1.2.3`,
abra app rollback foo.example.com Args: cobra.RangeArgs(1, 2),
abra app rollback foo.example.com 1.2.3+3.2.1`, ValidArgsFunction: func(
BashComplete: autocomplete.AppNameComplete, cmd *cobra.Command,
Action: func(c *cli.Context) error { 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 {
errMsg := fmt.Sprintf("autocomplete failed: %s", err)
return []string{errMsg}, cobra.ShellCompDirectiveError
}
return autocomplete.RecipeVersionComplete(app.Recipe.Name)
default:
return nil, cobra.ShellCompDirectiveError
}
},
Run: func(cmd *cobra.Command, args []string) {
var warnMessages []string var warnMessages []string
app := internal.ValidateApp(c) app := internal.ValidateApp(args)
stackName := app.StackName() stackName := app.StackName()
specificVersion := c.Args().Get(1) var specificVersion string
if len(args) == 2 {
specificVersion = args[1]
}
if specificVersion != "" { if specificVersion != "" {
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion) log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion)
app.Recipe.Version = specificVersion app.Recipe.Version = specificVersion
@ -139,7 +150,7 @@ EXAMPLE:
if len(availableDowngrades) == 0 && !internal.Force { if len(availableDowngrades) == 0 && !internal.Force {
log.Info("no available downgrades") log.Info("no available downgrades")
return nil return
} }
} }
@ -156,11 +167,11 @@ EXAMPLE:
prompt := &survey.Select{ prompt := &survey.Select{
Message: msg, Message: msg,
Options: internal.ReverseStringList(availableDowngrades), Options: internal.SortVersionsDesc(availableDowngrades),
} }
if err := survey.AskOne(prompt, &chosenDowngrade); err != nil { if err := survey.AskOne(prompt, &chosenDowngrade); err != nil {
return err return
} }
} }
} }
@ -228,7 +239,30 @@ EXAMPLE:
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil { if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err) log.Fatalf("writing new recipe version in env file: %s", err)
} }
return nil
}, },
} }
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 ( import (
"context" "context"
"errors"
"fmt" "fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
@ -14,52 +13,48 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var user string var AppRunCommand = &cobra.Command{
var userFlag = &cli.StringFlag{ Use: "run <app> <service> <cmd> [[args] [flags] | [flags] -- [args]]",
Name: "user, u",
Value: "",
Destination: &user,
}
var noTTY bool
var noTTYFlag = &cli.BoolFlag{
Name: "no-tty, t",
Destination: &noTTY,
}
var appRunCommand = cli.Command{
Name: "run",
Aliases: []string{"r"}, Aliases: []string{"r"},
Flags: []cli.Flag{ Short: "Run a command inside a service container",
internal.DebugFlag, Example: ` # run <cmd> with args/flags
noTTYFlag, abra app run 1312.net app -- ls -lha
userFlag,
# 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, Run: func(cmd *cobra.Command, args []string) {
ArgsUsage: "<domain> <service> <args>...", app := internal.ValidateApp(args)
Usage: "Run a command in a service container",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if len(c.Args()) < 2 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <service> provided?"))
}
if len(c.Args()) < 3 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <args> provided?"))
}
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
serviceName := c.Args().Get(1) serviceName := args[1]
stackAndServiceName := fmt.Sprintf("^%s_%s", app.StackName(), serviceName) stackAndServiceName := fmt.Sprintf("^%s_%s", app.StackName(), serviceName)
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", stackAndServiceName) filters.Add("name", stackAndServiceName)
@ -68,24 +63,23 @@ var appRunCommand = cli.Command{
log.Fatal(err) log.Fatal(err)
} }
cmd := c.Args()[2:] userCmd := args[2:]
execCreateOpts := types.ExecConfig{ execCreateOpts := types.ExecConfig{
AttachStderr: true, AttachStderr: true,
AttachStdin: true, AttachStdin: true,
AttachStdout: true, AttachStdout: true,
Cmd: cmd, Cmd: userCmd,
Detach: false, Detach: false,
Tty: true, Tty: true,
} }
if user != "" { if runAsUser != "" {
execCreateOpts.User = user execCreateOpts.User = runAsUser
} }
if noTTY { if noTTY {
execCreateOpts.Tty = false execCreateOpts.Tty = false
} }
// FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli() dcli, err := command.NewDockerCli()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -94,7 +88,27 @@ var appRunCommand = cli.Command{
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
log.Fatal(err) 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 ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"strconv" "strconv"
@ -17,56 +16,45 @@ import (
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var ( var AppSecretGenerateCommand = &cobra.Command{
allSecrets bool Use: "generate <app> [[secret] [version] | --all] [flags]",
allSecretsFlag = &cli.BoolFlag{ Aliases: []string{"g"},
Name: "all, a", Short: "Generate secrets",
Destination: &allSecrets, Args: cobra.RangeArgs(1, 3),
Usage: "Generate all secrets", ValidArgsFunction: func(
} cmd *cobra.Command,
) args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
var ( switch l := len(args); l {
rmAllSecrets bool case 0:
rmAllSecretsFlag = &cli.BoolFlag{ return autocomplete.AppNameComplete()
Name: "all, a", case 1:
Destination: &rmAllSecrets, app, err := appPkg.Get(args[0])
Usage: "Remove all secrets", if err != nil {
} errMsg := fmt.Sprintf("autocomplete failed: %s", err)
) return []string{errMsg}, cobra.ShellCompDirectiveError
}
var appSecretGenerateCommand = cli.Command{ return autocomplete.SecretComplete(app.Recipe.Name)
Name: "generate", default:
Aliases: []string{"g"}, return nil, cobra.ShellCompDirectiveDefault
Usage: "Generate secrets", }
ArgsUsage: "<domain> <secret> <version>",
Flags: []cli.Flag{
internal.DebugFlag,
allSecretsFlag,
internal.PassFlag,
internal.MachineReadableFlag,
internal.OfflineFlag,
internal.ChaosFlag,
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
BashComplete: autocomplete.AppNameComplete, app := internal.ValidateApp(args)
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if len(c.Args()) == 1 && !allSecrets { if len(args) == 1 && !generateAllSecrets {
err := errors.New("missing arguments <secret>/<version> or '--all'") log.Fatal("missing arguments [secret]/[version] or '--all'")
internal.ShowSubcommandHelpAndError(c, err)
} }
if c.Args().Get(1) != "" && allSecrets { if len(args) > 1 && generateAllSecrets {
err := errors.New("cannot use '<secret> <version>' and '--all' together") log.Fatal("cannot use '[secret] [version]' and '--all' together")
internal.ShowSubcommandHelpAndError(c, err)
} }
composeFiles, err := app.Recipe.GetComposeFiles(app.Env) composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
@ -79,9 +67,9 @@ var appSecretGenerateCommand = cli.Command{
log.Fatal(err) log.Fatal(err)
} }
if !allSecrets { if !generateAllSecrets {
secretName := c.Args().Get(1) secretName := args[1]
secretVersion := c.Args().Get(2) secretVersion := args[2]
s, ok := secrets[secretName] s, ok := secrets[secretName]
if !ok { if !ok {
log.Fatalf("%s doesn't exist in the env config?", secretName) log.Fatalf("%s doesn't exist in the env config?", secretName)
@ -102,7 +90,7 @@ var appSecretGenerateCommand = cli.Command{
log.Fatal(err) log.Fatal(err)
} }
if internal.Pass { if storeInPass {
for name, data := range secretVals { for name, data := range secretVals {
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil { if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
log.Fatal(err) log.Fatal(err)
@ -136,67 +124,66 @@ var appSecretGenerateCommand = cli.Command{
log.Fatal("unable to render to JSON: %s", err) log.Fatal("unable to render to JSON: %s", err)
} }
fmt.Println(out) fmt.Println(out)
return nil return
} }
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
log.Warnf( log.Warnf(
"generated secrets %s shown again, please take note of them %s", "generated secrets %s shown again, please take note of them %s",
formatter.BoldStyle.Render("NOT"), formatter.BoldStyle.Render("NOT"),
formatter.BoldStyle.Render("NOW"), formatter.BoldStyle.Render("NOW"),
) )
return nil
}, },
} }
var appSecretInsertCommand = cli.Command{ var AppSecretInsertCommand = &cobra.Command{
Name: "insert", Use: "insert <app> <secret> <version> <data> [flags]",
Aliases: []string{"i"}, Aliases: []string{"i"},
Usage: "Insert secret", Short: "Insert secret",
Flags: []cli.Flag{ Long: `This command inserts a secret into an app environment.
internal.DebugFlag,
internal.PassFlag,
internal.FileFlag,
internal.TrimFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> <secret-name> <version> <data>",
BashComplete: autocomplete.AppNameComplete,
Description: `
This command inserts a secret into an app environment.
This can be useful when you want to manually generate secrets for an app This can be useful when you want to manually generate secrets for an app
environment. Typically, you can let Abra generate them for you on app creation environment. Typically, you can let Abra generate them for you on app creation
(see "abra app new --secrets" for more). (see "abra app new --secrets/-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 {
errMsg := fmt.Sprintf("autocomplete failed: %s", err)
return []string{errMsg}, cobra.ShellCompDirectiveError
}
return autocomplete.SecretComplete(app.Recipe.Name)
default:
return nil, cobra.ShellCompDirectiveDefault
}
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
Example:
abra app secret insert myapp db_pass v1 mySecretPassword
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if len(c.Args()) != 4 {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments?"))
}
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
name := c.Args().Get(1) name := args[1]
version := c.Args().Get(2) version := args[2]
data := c.Args().Get(3) data := args[3]
if internal.File { if insertFromFile {
raw, err := os.ReadFile(data) raw, err := os.ReadFile(data)
if err != nil { if err != nil {
log.Fatalf("reading secret from file: %s", err) log.Fatalf("reading secret from file: %s", err)
@ -204,7 +191,7 @@ Example:
data = string(raw) data = string(raw)
} }
if internal.Trim { if trimInput {
data = strings.TrimSpace(data) data = strings.TrimSpace(data)
} }
@ -215,13 +202,11 @@ Example:
log.Infof("%s successfully stored on server", secretName) 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 { if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
return nil
}, },
} }
@ -233,7 +218,7 @@ func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string
log.Infof("deleted %s successfully from server", secretName) log.Infof("deleted %s successfully from server", secretName)
if internal.PassRemove { if removeFromPass {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil { if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
return err return err
} }
@ -244,30 +229,35 @@ func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string
return nil return nil
} }
var appSecretRmCommand = cli.Command{ var AppSecretRmCommand = &cobra.Command{
Name: "remove", Use: "remove <app> [[secret] | --all] [flags]",
Aliases: []string{"rm"}, Aliases: []string{"rm"},
Usage: "Remove a secret", Short: "Remove a secret",
Flags: []cli.Flag{ Args: cobra.RangeArgs(1, 2),
internal.DebugFlag, ValidArgsFunction: func(
internal.NoInputFlag, cmd *cobra.Command,
rmAllSecretsFlag, args []string,
internal.PassRemoveFlag, toComplete string) ([]string, cobra.ShellCompDirective) {
internal.OfflineFlag, switch l := len(args); l {
internal.ChaosFlag, case 0:
return autocomplete.AppNameComplete()
case 1:
if !rmAllSecrets {
app, err := appPkg.Get(args[0])
if err != nil {
errMsg := fmt.Sprintf("autocomplete failed: %s", err)
return []string{errMsg}, cobra.ShellCompDirectiveError
}
return autocomplete.SecretComplete(app.Recipe.Name)
}
return nil, cobra.ShellCompDirectiveDefault
default:
return nil, cobra.ShellCompDirectiveError
}
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
ArgsUsage: "<domain> [<secret-name>]", app := internal.ValidateApp(args)
BashComplete: autocomplete.AppNameComplete,
Description: `
This command removes app secrets.
Example:
abra app secret remove myapp db_pass
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -282,12 +272,12 @@ Example:
log.Fatal(err) log.Fatal(err)
} }
if c.Args().Get(1) != "" && rmAllSecrets { if len(args) == 2 && rmAllSecrets {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<secret-name>' and '--all' together")) log.Fatal("cannot use [secret] and --all/-a together")
} }
if c.Args().Get(1) == "" && !rmAllSecrets { if len(args) != 2 && !rmAllSecrets {
internal.ShowSubcommandHelpAndError(c, errors.New("no secret(s) specified?")) log.Fatal("no secret(s) specified?")
} }
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
@ -310,8 +300,12 @@ Example:
remoteSecretNames[cont.Spec.Annotations.Name] = true remoteSecretNames[cont.Spec.Annotations.Name] = true
} }
var secretToRm string
if len(args) == 2 {
secretToRm = args[1]
}
match := false match := false
secretToRm := c.Args().Get(1)
for secretName, val := range secrets { for secretName, val := range secrets {
secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, val.Version) secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, val.Version)
if _, ok := remoteSecretNames[secretRemoteName]; ok { if _, ok := remoteSecretNames[secretRemoteName]; ok {
@ -321,7 +315,7 @@ Example:
log.Fatal(err) log.Fatal(err)
} }
return nil return
} }
} else { } else {
match = true match = true
@ -340,25 +334,23 @@ Example:
if !match { if !match {
log.Fatal("no secrets to remove?") log.Fatal("no secrets to remove?")
} }
return nil
}, },
} }
var appSecretLsCommand = cli.Command{ var AppSecretLsCommand = &cobra.Command{
Name: "list", Use: "list <app>",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Flags: []cli.Flag{ Short: "List all secrets",
internal.DebugFlag, Args: cobra.MinimumNArgs(1),
internal.OfflineFlag, ValidArgsFunction: func(
internal.ChaosFlag, cmd *cobra.Command,
internal.MachineReadableFlag, args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
Usage: "List all secrets", app := internal.ValidateApp(args)
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -401,28 +393,137 @@ var appSecretLsCommand = cli.Command{
log.Fatal("unable to render to JSON: %s", err) log.Fatal("unable to render to JSON: %s", err)
} }
fmt.Println(out) fmt.Println(out)
return nil return
} }
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
return nil log.Fatal(err)
}
return
} }
log.Warnf("no secrets stored for %s", app.Name) log.Warnf("no secrets stored for %s", app.Name)
return nil
}, },
} }
var appSecretCommand = cli.Command{ var AppSecretCommand = &cobra.Command{
Name: "secret", Use: "secret [cmd] [args] [flags]",
Aliases: []string{"s"}, Aliases: []string{"s"},
Usage: "Manage app secrets", Short: "Manage app secrets",
ArgsUsage: "<domain>", }
Subcommands: []cli.Command{
appSecretGenerateCommand, var (
appSecretInsertCommand, storeInPass bool
appSecretRmCommand, insertFromFile bool
appSecretLsCommand, 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",
)
AppSecretGenerateCommand.Flags().BoolVarP(
&generateAllSecrets,
"all",
"a",
false,
"generate all secrets",
)
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,21 +13,23 @@ import (
"coopcloud.tech/abra/pkg/service" "coopcloud.tech/abra/pkg/service"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
containerTypes "github.com/docker/docker/api/types/container" containerTypes "github.com/docker/docker/api/types/container"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var appServicesCommand = cli.Command{ var AppServicesCommand = &cobra.Command{
Name: "services", Use: "services <app> [flags]",
Aliases: []string{"sr"}, Aliases: []string{"sr"},
Usage: "Display all services of an app", Short: "Display all services of an app",
ArgsUsage: "<domain>", Args: cobra.ExactArgs(1),
Flags: []cli.Flag{ ValidArgsFunction: func(
internal.DebugFlag, cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
BashComplete: autocomplete.AppNameComplete, app := internal.ValidateApp(args)
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -61,7 +63,7 @@ var appServicesCommand = cli.Command{
log.Fatal(err) log.Fatal(err)
} }
headers := []string{"SERVICE (SHORT)", "SERVICE (LONG)", "IMAGE"} headers := []string{"SERVICE (SHORT)", "SERVICE (LONG)"}
table.Headers(headers...) table.Headers(headers...)
var rows [][]string var rows [][]string
@ -78,7 +80,6 @@ var appServicesCommand = cli.Command{
row := []string{ row := []string{
serviceShortName, serviceShortName,
serviceLongName, serviceLongName,
formatter.RemoveSha(container.Image),
} }
rows = append(rows, row) rows = append(rows, row)
@ -87,9 +88,9 @@ var appServicesCommand = cli.Command{
table.Rows(rows...) table.Rows(rows...)
if len(rows) > 0 { if len(rows) > 0 {
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
} }
return nil
}, },
} }

View File

@ -14,15 +14,77 @@ import (
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var prune bool var AppUndeployCommand = &cobra.Command{
Use: "undeploy <app> [flags]",
Aliases: []string{"un"},
Short: "Undeploy an app",
Long: `This does not destroy any application data.
var pruneFlag = &cli.BoolFlag{ However, you should remain vigilant, as your swarm installation will consider
Name: "prune, p", any previously attached volumes as eligible for pruning once undeployed.
Destination: &prune,
Usage: "Prunes unused containers, networks, and dangling images for an app", 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)
if err != nil {
log.Fatal(err)
}
log.Debugf("checking whether %s is already deployed", stackName)
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
log.Fatal(err)
}
if !deployMeta.IsDeployed {
log.Fatalf("%s is not deployed?", app.Name)
}
chaosVersion := config.CHAOS_DEFAULT
if deployMeta.IsChaos {
chaosVersion = deployMeta.ChaosVersion
}
if err := internal.UndeployOverview(
app,
deployMeta.Version,
chaosVersion); err != nil {
log.Fatal(err)
}
rmOpts := stack.Remove{
Namespaces: []string{app.StackName()},
Detach: false,
}
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
log.Fatal(err)
}
if prune {
if err := pruneApp(cl, app); err != nil {
log.Fatal(err)
}
}
log.Debugf("choosing %s as version to save to env file", deployMeta.Version)
if err := app.WriteRecipeVersion(deployMeta.Version, false); err != nil {
log.Fatalf("writing undeployed recipe version in env file: %s", err)
}
},
} }
// pruneApp runs the equivalent of a "docker system prune" but only filtering // pruneApp runs the equivalent of a "docker system prune" but only filtering
@ -61,69 +123,16 @@ func pruneApp(cl *dockerClient.Client, app appPkg.App) error {
return nil return nil
} }
var appUndeployCommand = cli.Command{ var (
Name: "undeploy", prune bool
Aliases: []string{"un"}, )
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.OfflineFlag,
pruneFlag,
},
Before: internal.SubCommandBefore,
Usage: "Undeploy an app",
BashComplete: autocomplete.AppNameComplete,
Description: `
This does not destroy any of the application data.
However, you should remain vigilant, as your swarm installation will consider func init() {
any previously attached volumes as eligible for pruning once undeployed. AppUndeployCommand.Flags().BoolVarP(
&prune,
Passing "-p/--prune" does not remove those volumes.`, "prune",
Action: func(c *cli.Context) error { "p",
app := internal.ValidateApp(c) false,
stackName := app.StackName() "prune unused containers, networks, and dangling images",
)
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
log.Debugf("checking whether %s is already deployed", stackName)
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
log.Fatal(err)
}
if !deployMeta.IsDeployed {
log.Fatalf("%s is not deployed?", app.Name)
}
chaosVersion := config.CHAOS_DEFAULT
if deployMeta.IsChaos {
chaosVersion = deployMeta.ChaosVersion
}
if err := internal.DeployOverview(app, []string{}, deployMeta.Version, chaosVersion); err != nil {
log.Fatal(err)
}
rmOpts := stack.Remove{
Namespaces: []string{app.StackName()},
Detach: false,
}
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
log.Fatal(err)
}
if prune {
if err := pruneApp(cl, app); err != nil {
log.Fatal(err)
}
}
return nil
},
} }

View File

@ -15,45 +15,50 @@ import (
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var appUpgradeCommand = cli.Command{ var AppUpgradeCommand = &cobra.Command{
Name: "upgrade", Use: "upgrade <app> [version] [flags]",
Aliases: []string{"up"}, Aliases: []string{"up"},
Usage: "Upgrade an app", Short: "Upgrade an app",
ArgsUsage: "<domain> [<version>]", Long: `Upgrade an app.
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
internal.OfflineFlag,
internal.ReleaseNotesFlag,
},
Before: internal.SubCommandBefore,
Description: `
Upgrade an app.
Unlike "deploy", chaos operations are not supported here. Only recipe versions Unlike "deploy", chaos operations are not supported here. Only recipe versions
are supported values for "[<version>]". are supported values for "[version]".
An upgrade can be destructive, please ensure you have a copy of your app data An upgrade can be destructive, please ensure you have a copy of your app data
beforehand. beforehand.`,
Args: cobra.RangeArgs(1, 2),
EXAMPLE: ValidArgsFunction: func(
cmd *cobra.Command,
abra app upgrade foo.example.com args []string,
abra app upgrade foo.example.com 1.2.3+3.2.1`, toComplete string) ([]string, cobra.ShellCompDirective) {
BashComplete: autocomplete.AppNameComplete, switch l := len(args); l {
Action: func(c *cli.Context) error { case 0:
return autocomplete.AppNameComplete()
case 1:
app, err := appPkg.Get(args[0])
if err != nil {
errMsg := fmt.Sprintf("autocomplete failed: %s", err)
return []string{errMsg}, cobra.ShellCompDirectiveError
}
return autocomplete.RecipeVersionComplete(app.Recipe.Name)
default:
return nil, cobra.ShellCompDirectiveError
}
},
Run: func(cmd *cobra.Command, args []string) {
var warnMessages []string var warnMessages []string
app := internal.ValidateApp(c) app := internal.ValidateApp(args)
stackName := app.StackName() stackName := app.StackName()
specificVersion := c.Args().Get(1) var specificVersion string
if len(args) == 2 {
specificVersion = args[1]
}
if specificVersion != "" { if specificVersion != "" {
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion) log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion)
app.Recipe.Version = specificVersion app.Recipe.Version = specificVersion
@ -137,7 +142,7 @@ EXAMPLE:
if len(availableUpgrades) == 0 && !internal.Force { if len(availableUpgrades) == 0 && !internal.Force {
log.Info("no available upgrades") log.Info("no available upgrades")
return nil return
} }
} }
@ -154,11 +159,11 @@ EXAMPLE:
prompt := &survey.Select{ prompt := &survey.Select{
Message: msg, Message: msg,
Options: internal.ReverseStringList(availableUpgrades), Options: internal.SortVersionsDesc(availableUpgrades),
} }
if err := survey.AskOne(prompt, &chosenUpgrade); err != nil { if err := survey.AskOne(prompt, &chosenUpgrade); err != nil {
return err return
} }
} }
} }
@ -185,7 +190,7 @@ EXAMPLE:
if parsedVersion.IsGreaterThan(parsedDeployedVersion) && parsedVersion.IsLessThan(parsedChosenUpgrade) { if parsedVersion.IsGreaterThan(parsedDeployedVersion) && parsedVersion.IsLessThan(parsedChosenUpgrade) {
note, err := app.Recipe.GetReleaseNotes(version) note, err := app.Recipe.GetReleaseNotes(version)
if err != nil { if err != nil {
return err log.Fatal(err)
} }
if note != "" { if note != "" {
releaseNotes += fmt.Sprintf("%s\n", note) releaseNotes += fmt.Sprintf("%s\n", note)
@ -244,10 +249,9 @@ EXAMPLE:
} }
} }
if internal.ReleaseNotes { if showReleaseNotes {
fmt.Println()
fmt.Print(releaseNotes) fmt.Print(releaseNotes)
return nil return
} }
chaosVersion := config.CHAOS_DEFAULT chaosVersion := config.CHAOS_DEFAULT
@ -289,7 +293,42 @@ EXAMPLE:
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil { if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err) log.Fatalf("writing new recipe version in env file: %s", err)
} }
return nil
}, },
} }
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

@ -2,7 +2,6 @@ package app
import ( import (
"context" "context"
"fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
@ -11,22 +10,22 @@ import (
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var appVolumeListCommand = cli.Command{ var AppVolumeListCommand = &cobra.Command{
Name: "list", Use: "list <app> [flags]",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
ArgsUsage: "<domain>", Short: "List volumes associated with an app",
Flags: []cli.Flag{ Args: cobra.ExactArgs(1),
internal.DebugFlag, ValidArgsFunction: func(
internal.NoInputFlag, cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
Usage: "List volumes associated with an app", app := internal.ValidateApp(args)
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
@ -43,7 +42,7 @@ var appVolumeListCommand = cli.Command{
log.Fatal(err) log.Fatal(err)
} }
headers := []string{"name", "created", "mounted"} headers := []string{"NAME", "ON SERVER"}
table, err := formatter.CreateTable() table, err := formatter.CreateTable()
if err != nil { if err != nil {
@ -54,47 +53,46 @@ var appVolumeListCommand = cli.Command{
var rows [][]string var rows [][]string
for _, volume := range volumes { for _, volume := range volumes {
row := []string{volume.Name, volume.CreatedAt, volume.Mountpoint} row := []string{volume.Name, volume.Mountpoint}
rows = append(rows, row) rows = append(rows, row)
} }
table.Rows(rows...) table.Rows(rows...)
if len(rows) > 0 { if len(rows) > 0 {
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
return nil log.Fatal(err)
}
return
} }
log.Warnf("no volumes created for %s", app.Name) log.Warnf("no volumes created for %s", app.Name)
return nil
}, },
} }
var appVolumeRemoveCommand = cli.Command{ var AppVolumeRemoveCommand = &cobra.Command{
Name: "remove", Use: "remove <app> [flags]",
Usage: "Remove volume(s) associated with an app", Short: "Remove volume(s) associated with an app",
Description: ` Long: `Remove volumes associated with an app.
This command supports removing volumes associated with an app. The app in
question must be undeployed before you try to remove volumes. See "abra app The app in question must be undeployed before you try to remove volumes. See
undeploy <domain>" for more. "abra app undeploy <app>" for more.
The command is interactive and will show a multiple select input which allows The command is interactive and will show a multiple select input which allows
you to make a seclection. Use the "?" key to see more help on navigating this you to make a seclection. Use the "?" key to see more help on navigating this
interface. interface.
Passing "--force/-f" will select all volumes for removal. Be careful.`, Passing "--force/-f" will select all volumes for removal. Be careful.`,
ArgsUsage: "<domain>", Aliases: []string{"rm"},
Aliases: []string{"rm"}, Args: cobra.MinimumNArgs(1),
Flags: []cli.Flag{ ValidArgsFunction: func(
internal.DebugFlag, cmd *cobra.Command,
internal.NoInputFlag, args []string,
internal.ForceFlag, toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
BashComplete: autocomplete.AppNameComplete, app := internal.ValidateApp(args)
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
@ -149,18 +147,21 @@ Passing "--force/-f" will select all volumes for removal. Be careful.`,
} else { } else {
log.Info("no volumes removed") log.Info("no volumes removed")
} }
return nil
}, },
} }
var appVolumeCommand = cli.Command{ var AppVolumeCommand = &cobra.Command{
Name: "volume", Use: "volume [cmd] [args] [flags]",
Aliases: []string{"vl"}, Aliases: []string{"vl"},
Usage: "Manage app volumes", Short: "Manage app volumes",
ArgsUsage: "<domain>", }
Subcommands: []cli.Command{
appVolumeListCommand, func init() {
appVolumeRemoveCommand, AppVolumeRemoveCommand.Flags().BoolVarP(
}, &internal.Force,
"force",
"f",
false,
"perform action without further prompt",
)
} }

View File

@ -15,44 +15,42 @@ import (
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var catalogueGenerateCommand = cli.Command{ var CatalogueGenerateCommand = &cobra.Command{
Name: "generate", Use: "generate [recipe] [flags]",
Aliases: []string{"g"}, Aliases: []string{"g"},
Usage: "Generate the recipe catalogue", Short: "Generate the recipe catalogue",
Flags: []cli.Flag{ Long: `Generate a new copy of the recipe catalogue.
internal.DebugFlag,
internal.NoInputFlag,
internal.PublishFlag,
internal.DryFlag,
internal.SkipUpdatesFlag,
internal.ChaosFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `
Generate a new copy of the recipe catalogue.
It is possible to generate new metadata for a single recipe by passing It is possible to generate new metadata for a single recipe by passing
<recipe>. The existing local catalogue will be updated, not overwritten. [recipe]. The existing local catalogue will be updated, not overwritten.
It is quite easy to get rate limited by Docker Hub when running this command. 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 If you have a Hub account you can have Abra log you in to avoid this. Pass
"--user" and "--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 that you have permission to git push to these repositories and have your SSH
keys configured on your account.`, keys configured on your account.`,
ArgsUsage: "[<recipe>]", Args: cobra.RangeArgs(0, 1),
BashComplete: autocomplete.RecipeNameComplete, ValidArgsFunction: func(
Action: func(c *cli.Context) error { cmd *cobra.Command,
recipeName := c.Args().First() 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) r := recipe.Get(recipeName)
if recipeName != "" { if recipeName != "" {
internal.ValidateRecipe(c) internal.ValidateRecipe(args, cmd.Name())
} }
if !internal.Chaos { if !internal.Chaos {
@ -76,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) 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) log.Warn(logMsg)
if err := recipe.UpdateRepositories(repos, recipeName); err != nil { if err := recipe.UpdateRepositories(repos, recipeName); err != nil {
log.Fatal(err) log.Fatal(err)
@ -147,7 +145,7 @@ keys configured on your account.`,
log.Infof("generated new recipe catalogue in %s", config.RECIPES_JSON) log.Infof("generated new recipe catalogue in %s", config.RECIPES_JSON)
cataloguePath := path.Join(config.ABRA_DIR, "catalogue") cataloguePath := path.Join(config.ABRA_DIR, "catalogue")
if internal.Publish { if publishChanges {
isClean, err := gitPkg.IsClean(cataloguePath) isClean, err := gitPkg.IsClean(cataloguePath)
if err != nil { if err != nil {
@ -190,7 +188,7 @@ keys configured on your account.`,
log.Fatal(err) 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()) 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) log.Infof("new changes published: %s", url)
} }
@ -198,18 +196,51 @@ keys configured on your account.`,
if internal.Dry { if internal.Dry {
log.Info("dry run: no changes published") log.Info("dry run: no changes published")
} }
return nil
}, },
} }
// CatalogueCommand defines the `abra catalogue` command and sub-commands. // CatalogueCommand defines the `abra catalogue` command and sub-commands.
var CatalogueCommand = cli.Command{ var CatalogueCommand = &cobra.Command{
Name: "catalogue", Use: "catalogue [cmd] [args] [flags]",
Usage: "Manage the recipe catalogue", Short: "Manage the recipe catalogue",
Aliases: []string{"c"}, Aliases: []string{"c"},
ArgsUsage: "<recipe>", }
Subcommands: []cli.Command{
catalogueGenerateCommand, 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,211 +0,0 @@
// Package cli provides the interface for the command-line.
package cli
import (
"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"
)
// AutoCompleteCommand helps people set up auto-complete in their shells
var AutoCompleteCommand = cli.Command{
Name: "autocomplete",
Aliases: []string{"ac"},
Usage: "Configure shell autocompletion",
Description: `
Set up shell auto-completion.
Supported shells are: bash, fish, fizsh & zsh.
EXAMPLE:
abra autocomplete bash`,
ArgsUsage: "<shell>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Action: func(c *cli.Context) error {
shellType := c.Args().First()
if shellType == "" {
internal.ShowSubcommandHelpAndError(c, 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",
Description: `
Upgrade abra in-place with the latest stable or release candidate.
Use "-r/--rc" to install the latest release candidate. Please bear in mind that
it may contain absolutely catastrophic deal-breaker bugs. Thank you very much
for the testing efforts 💗
EXAMPLE:
abra upgrade
abra upgrade --rc`,
Flags: []cli.Flag{internal.RCFlag},
Action: func(c *cli.Context) error {
mainURL := "https://install.abra.coopcloud.tech"
cmd := exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash", mainURL))
if internal.RC {
releaseCandidateURL := "https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer"
cmd = exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash -s -- --rc", releaseCandidateURL))
}
log.Debugf("attempting to run %s", cmd)
if err := internal.RunCmd(cmd); err != nil {
log.Fatal(err)
}
return nil
},
}
func newAbraApp(version, commit string) *cli.App {
app := &cli.App{
Name: "abra",
Usage: `the Co-op Cloud command-line utility belt 🎩🐇
____ ____ _ _
/ ___|___ ___ _ __ / ___| | ___ _ _ __| |
| | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |
| |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |
\____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|
|_|
`,
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Commands: []cli.Command{
app.AppCommand,
server.ServerCommand,
recipe.RecipeCommand,
catalogue.CatalogueCommand,
UpgradeCommand,
AutoCompleteCommand,
},
BashComplete: autocomplete.SubcommandComplete,
}
app.EnableBashCompletion = true
app.Before = func(c *cli.Context) 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
}
return app
}
// RunApp runs CLI abra app.
func RunApp(version, commit string) {
app := newAbraApp(version, commit)
if err := app.Run(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

@ -2,6 +2,8 @@ package internal
import ( import (
"context" "context"
"fmt"
"io"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container" containerPkg "coopcloud.tech/abra/pkg/container"
@ -19,7 +21,7 @@ func RetrieveBackupBotContainer(cl *dockerClient.Client) (types.Container, error
ctx := context.Background() ctx := context.Background()
chosenService, err := service.GetServiceByLabel(ctx, cl, config.BackupbotLabel, NoInput) chosenService, err := service.GetServiceByLabel(ctx, cl, config.BackupbotLabel, NoInput)
if err != nil { if err != nil {
return types.Container{}, err return types.Container{}, fmt.Errorf("no backupbot discovered, is it deployed?")
} }
log.Debugf("retrieved %s as backup enabled service", chosenService.Spec.Name) log.Debugf("retrieved %s as backup enabled service", chosenService.Spec.Name)
@ -40,7 +42,11 @@ func RetrieveBackupBotContainer(cl *dockerClient.Client) (types.Container, error
} }
// RunBackupCmdRemote runs a backup related command on a remote backupbot container. // RunBackupCmdRemote runs a backup related command on a remote backupbot container.
func RunBackupCmdRemote(cl *dockerClient.Client, backupCmd string, containerID string, execEnv []string) error { func RunBackupCmdRemote(
cl *dockerClient.Client,
backupCmd string,
containerID string,
execEnv []string) (io.Writer, error) {
execBackupListOpts := types.ExecConfig{ execBackupListOpts := types.ExecConfig{
AttachStderr: true, AttachStderr: true,
AttachStdin: true, AttachStdin: true,
@ -56,12 +62,13 @@ func RunBackupCmdRemote(cl *dockerClient.Client, backupCmd string, containerID s
// FIXME: avoid instantiating a new CLI // FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli() dcli, err := command.NewDockerCli()
if err != nil { if err != nil {
return err return nil, err
} }
if _, err := container.RunExec(dcli, cl, containerID, &execBackupListOpts); err != nil { out, err := container.RunExec(dcli, cl, containerID, &execBackupListOpts)
return err if err != nil {
return nil, err
} }
return nil return out, nil
} }

View File

@ -1,296 +1,19 @@
package internal package internal
import ( var (
"os" // NOTE(d1): global
Debug bool
NoInput bool
Offline bool
"coopcloud.tech/abra/pkg/log" // NOTE(d1): sub-command specific
"github.com/urfave/cli" 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, 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, 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, p",
Usage: "Remove generated secrets from a local pass store",
Destination: &PassRemove,
}
var File bool
var FileFlag = &cli.BoolFlag{
Name: "file, f",
Usage: "Treat input as a file",
Destination: &File,
}
var Trim bool
var TrimFlag = &cli.BoolFlag{
Name: "trim, 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, 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, C",
Usage: "Proceed with 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, T",
Usage: "Disables TTY mode to run this command from a script.",
Destination: &Tty,
}
var NoInput bool
var NoInputFlag = &cli.BoolFlag{
Name: "no-input, n",
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, 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, o",
Destination: &Offline,
Usage: "Prefer offline & filesystem access when possible",
}
// 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, 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, m",
Destination: &MachineReadable,
Usage: "Output in a machine-readable format (where supported)",
}
// RC signifies the latest release candidate
var RC bool
// RCFlag chooses the latest release candidate for install
var RCFlag = &cli.BoolFlag{
Name: "rc, r",
Destination: &RC,
Usage: "Install the latest release candidate",
}
var Major bool
var MajorFlag = &cli.BoolFlag{
Name: "major, x",
Usage: "Increase the major part of the version",
Destination: &Major,
}
var Minor bool
var MinorFlag = &cli.BoolFlag{
Name: "minor, y",
Usage: "Increase the minor part of the version",
Destination: &Minor,
}
var Patch bool
var PatchFlag = &cli.BoolFlag{
Name: "patch, z",
Usage: "Increase the patch part of the version",
Destination: &Patch,
}
var Dry bool
var DryFlag = &cli.BoolFlag{
Name: "dry-run, r",
Usage: "Only reports changes that would be made",
Destination: &Dry,
}
var Publish bool
var PublishFlag = &cli.BoolFlag{
Name: "publish, p",
Usage: "Publish changes to git.coopcloud.tech",
Destination: &Publish,
}
var Domain string
var DomainFlag = &cli.StringFlag{
Name: "domain, D",
Value: "",
Usage: "Choose a domain name",
Destination: &Domain,
}
var NewAppServer string
var NewAppServerFlag = &cli.StringFlag{
Name: "server, s",
Value: "",
Usage: "Show apps of a specific server",
Destination: &NewAppServer,
}
var NoDomainChecks bool
var NoDomainChecksFlag = &cli.BoolFlag{
Name: "no-domain-checks, D",
Usage: "Disable public DNS checks",
Destination: &NoDomainChecks,
}
var StdErrOnly bool
var StdErrOnlyFlag = &cli.BoolFlag{
Name: "stderr, s",
Usage: "Only tail stderr",
Destination: &StdErrOnly,
}
var SinceLogs string
var SinceLogsFlag = &cli.StringFlag{
Name: "since, S",
Value: "",
Usage: "tail logs since YYYY-MM-DDTHH:MM:SSZ",
Destination: &SinceLogs,
}
var DontWaitConverge bool
var DontWaitConvergeFlag = &cli.BoolFlag{
Name: "no-converge-checks, c",
Usage: "Don't wait for converge logic checks",
Destination: &DontWaitConverge,
}
var Watch bool
var WatchFlag = &cli.BoolFlag{
Name: "watch, w",
Usage: "Watch status by polling repeatedly",
Destination: &Watch,
}
var OnlyErrors bool
var OnlyErrorFlag = &cli.BoolFlag{
Name: "errors, e",
Usage: "Only show errors",
Destination: &OnlyErrors,
}
var SkipUpdates bool
var SkipUpdatesFlag = &cli.BoolFlag{
Name: "skip-updates, s",
Usage: "Skip updating recipe repositories",
Destination: &SkipUpdates,
}
var AllTags bool
var AllTagsFlag = &cli.BoolFlag{
Name: "all-tags, a",
Usage: "List all tags, not just upgrades",
Destination: &AllTags,
}
var LocalCmd bool
var LocalCmdFlag = &cli.BoolFlag{
Name: "local, l",
Usage: "Run command locally",
Destination: &LocalCmd,
}
var RemoteUser string
var RemoteUserFlag = &cli.StringFlag{
Name: "user, u",
Value: "",
Usage: "User to run command within a service context",
Destination: &RemoteUser,
}
var GitName string
var GitNameFlag = &cli.StringFlag{
Name: "git-name, gn",
Value: "",
Usage: "Git (user) name to do commits with",
Destination: &GitName,
}
var GitEmail string
var GitEmailFlag = &cli.StringFlag{
Name: "git-email, ge",
Value: "",
Usage: "Git email name to do commits with",
Destination: &GitEmail,
}
var AllServices bool
var AllServicesFlag = &cli.BoolFlag{
Name: "all-services, a",
Usage: "Restart all services",
Destination: &AllServices,
}
// SubCommandBefore wires up pre-action machinery (e.g. --debug handling).
func SubCommandBefore(c *cli.Context) 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 // 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 := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName)) 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, " ")) log.Debugf("running command: %s", strings.Join(cmd, " "))
if RemoteUser != "" { if remoteUser != "" {
log.Debugf("running command with user %s", RemoteUser) log.Debugf("running command with user %s", remoteUser)
execCreateOpts.User = RemoteUser execCreateOpts.User = remoteUser
} }
execCreateOpts.Cmd = cmd execCreateOpts.Cmd = cmd
execCreateOpts.Tty = true execCreateOpts.Tty = requestTTY
if Tty { if !requestTTY {
execCreateOpts.Tty = false log.Debugf("not requesting a remote TTY")
} }
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {

View File

@ -3,10 +3,14 @@ package internal
import ( import (
"fmt" "fmt"
"os" "os"
"sort"
"strings" "strings"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
@ -20,7 +24,8 @@ var borderStyle = lipgloss.NewStyle().
var headerStyle = lipgloss.NewStyle(). var headerStyle = lipgloss.NewStyle().
Underline(true). Underline(true).
Bold(true) Bold(true).
PaddingBottom(1)
var leftStyle = lipgloss.NewStyle(). var leftStyle = lipgloss.NewStyle().
Bold(true) Bold(true)
@ -37,13 +42,13 @@ func NewVersionOverview(
app appPkg.App, app appPkg.App,
warnMessages []string, warnMessages []string,
kind, kind,
currentVersion, deployedVersion,
chaosVersion, deployedChaosVersion,
newVersion, toDeployVersion,
releaseNotes string) error { releaseNotes string) error {
deployConfig := "compose.yml" deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") deployConfig = composeFiles
} }
server := app.Server server := app.Server
@ -51,32 +56,35 @@ func NewVersionOverview(
server = "local" server = "local"
} }
body := strings.Builder{} domain := app.Domain
body.WriteString( if domain == "" {
borderStyle.Render( domain = config.NO_DOMAIN_DEFAULT
lipgloss.JoinVertical( }
lipgloss.Center,
headerStyle.Render(fmt.Sprintf("%s OVERVIEW", strings.ToUpper(kind))),
lipgloss.JoinVertical(
lipgloss.Left,
horizontal(leftStyle.Render("SERVER"), " ", rightStyle.Render(server)),
horizontal(leftStyle.Render("DOMAIN"), " ", rightStyle.Render(app.Domain)),
horizontal(leftStyle.Render("RECIPE"), " ", rightStyle.Render(app.Recipe.Name)),
horizontal(leftStyle.Render("CONFIG"), " ", rightStyle.Render(deployConfig)),
horizontal(leftStyle.Render("VERSION"), " ", rightStyle.Render(currentVersion)),
horizontal(leftStyle.Render("CHAOS"), " ", rightStyle.Render(chaosVersion)),
horizontal(leftStyle.Render("DEPLOY"), " ", rightStyle.Padding(0).Render(newVersion)),
),
),
),
)
fmt.Println(body.String())
if releaseNotes != "" && newVersion != "" { rows := [][]string{
fmt.Println() []string{"APP", domain},
[]string{"RECIPE", app.Recipe.Name},
[]string{"SERVER", server},
[]string{"DEPLOYED", deployedVersion},
[]string{"CURRENT CHAOS ", deployedChaosVersion},
[]string{fmt.Sprintf("TO %s", strings.ToUpper(kind)), toDeployVersion},
[]string{"CONFIG", deployConfig},
}
overview := formatter.CreateOverview(
fmt.Sprintf("%s OVERVIEW", strings.ToUpper(kind)),
rows,
)
fmt.Println(overview)
if releaseNotes != "" && toDeployVersion != "" {
fmt.Print(releaseNotes) fmt.Print(releaseNotes)
} else { } else {
warnMessages = append(warnMessages, fmt.Sprintf("no release notes available for %s", newVersion)) warnMessages = append(
warnMessages,
fmt.Sprintf("no release notes available for %s", toDeployVersion),
)
} }
for _, msg := range warnMessages { for _, msg := range warnMessages {
@ -101,10 +109,16 @@ func NewVersionOverview(
} }
// DeployOverview shows a deployment overview // DeployOverview shows a deployment overview
func DeployOverview(app appPkg.App, warnMessages []string, version, chaosVersion string) error { func DeployOverview(
app appPkg.App,
warnMessages []string,
deployedVersion string,
deployedChaosVersion string,
toDeployVersion,
toDeployChaosVersion string) error {
deployConfig := "compose.yml" deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") deployConfig = composeFiles
} }
server := app.Server server := app.Server
@ -112,25 +126,25 @@ func DeployOverview(app appPkg.App, warnMessages []string, version, chaosVersion
server = "local" server = "local"
} }
body := strings.Builder{} domain := app.Domain
body.WriteString( if domain == "" {
borderStyle.Render( domain = config.NO_DOMAIN_DEFAULT
lipgloss.JoinVertical( }
lipgloss.Center,
headerStyle.Render("DEPLOY OVERVIEW"), rows := [][]string{
lipgloss.JoinVertical( []string{"APP", domain},
lipgloss.Left, []string{"RECIPE", app.Recipe.Name},
horizontal(leftStyle.Render("SERVER"), " ", rightStyle.Render(server)), []string{"SERVER", server},
horizontal(leftStyle.Render("DOMAIN"), " ", rightStyle.Render(app.Domain)), []string{"DEPLOYED", deployedVersion},
horizontal(leftStyle.Render("RECIPE"), " ", rightStyle.Render(app.Recipe.Name)), []string{"CURRENT CHAOS ", deployedChaosVersion},
horizontal(leftStyle.Render("CONFIG"), " ", rightStyle.Render(deployConfig)), []string{"TO DEPLOY", toDeployVersion},
horizontal(leftStyle.Render("VERSION"), " ", rightStyle.Render(version)), []string{"NEW CHAOS", toDeployChaosVersion},
horizontal(leftStyle.Render("CHAOS"), " ", rightStyle.Padding(0).Render(chaosVersion)), []string{"CONFIG", deployConfig},
), }
),
), overview := formatter.CreateOverview("DEPLOY OVERVIEW", rows)
)
fmt.Println(body.String()) fmt.Println(overview)
for _, msg := range warnMessages { for _, msg := range warnMessages {
log.Warn(msg) log.Warn(msg)
@ -153,6 +167,56 @@ func DeployOverview(app appPkg.App, warnMessages []string, version, chaosVersion
return nil return nil
} }
// UndeployOverview shows an undeployment overview
func UndeployOverview(
app appPkg.App,
version,
chaosVersion string) error {
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = composeFiles
}
server := app.Server
if app.Server == "default" {
server = "local"
}
domain := app.Domain
if domain == "" {
domain = config.NO_DOMAIN_DEFAULT
}
rows := [][]string{
[]string{"APP", domain},
[]string{"RECIPE", app.Recipe.Name},
[]string{"SERVER", server},
[]string{"DEPLOYED", version},
[]string{"CHAOS", chaosVersion},
[]string{"CONFIG", deployConfig},
}
overview := formatter.CreateOverview("UNDEPLOY OVERVIEW", rows)
fmt.Println(overview)
if NoInput {
return nil
}
response := false
prompt := &survey.Confirm{Message: "proceed?"}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
log.Fatal("undeploy cancelled")
}
return nil
}
// PostCmds parses a string of commands and executes them inside of the respective services // PostCmds parses a string of commands and executes them inside of the respective services
// the commands string must have the following format: // the commands string must have the following format:
// "<service> <command> <arguments>|<service> <command> <arguments>|... " // "<service> <command> <arguments>|<service> <command> <arguments>|... "
@ -199,10 +263,33 @@ 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) log.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName)
Tty = true requestTTY := true
if err := RunCmdRemote(cl, app, app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs); err != nil { if err := RunCmdRemote(
cl,
app,
requestTTY,
app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs, ""); err != nil {
return err return err
} }
} }
return nil return nil
} }
// SortVersionsDesc sorts versions in descending order.
func SortVersionsDesc(versions []string) []string {
var tags []tagcmp.Tag
for _, v := range versions {
parsed, _ := tagcmp.Parse(v) // skips unsupported tags
tags = append(tags, parsed)
}
sort.Sort(tagcmp.ByTagDesc(tags))
var desc []string
for _, t := range tags {
desc = append(desc, t.String())
}
return desc
}

View File

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

View File

@ -1,10 +0,0 @@
package internal
// ReverseStringList reverses a list of a strings. Roll on Go generics.
func ReverseStringList(strings []string) []string {
for i, j := 0, len(strings)-1; i < j; i, j = i+1, j-1 {
strings[i], strings[j] = strings[j], strings[i]
}
return strings
}

View File

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

View File

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

View File

@ -6,31 +6,40 @@ import (
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var recipeFetchCommand = cli.Command{ var RecipeFetchCommand = &cobra.Command{
Name: "fetch", Use: "fetch [recipe | --all] [flags]",
Usage: "Fetch recipe(s)", Aliases: []string{"f"},
Aliases: []string{"f"}, Short: "Clone recipe(s) locally",
ArgsUsage: "[<recipe>]", Args: cobra.RangeArgs(0, 1),
Description: "Retrieves all recipes if no <recipe> argument is passed", ValidArgsFunction: func(
Flags: []cli.Flag{ cmd *cobra.Command,
internal.DebugFlag, args []string,
internal.NoInputFlag, toComplete string) ([]string, cobra.ShellCompDirective) {
internal.OfflineFlag, return autocomplete.RecipeNameComplete()
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
BashComplete: autocomplete.RecipeNameComplete, var recipeName string
Action: func(c *cli.Context) error { if len(args) > 0 {
recipeName := c.Args().First() recipeName = args[0]
r := recipe.Get(recipeName) }
if recipeName == "" && !fetchAllRecipes {
log.Fatal("missing [recipe] or --all/-a")
}
if recipeName != "" && fetchAllRecipes {
log.Fatal("cannot use [recipe] and --all/-a together")
}
if recipeName != "" { if recipeName != "" {
internal.ValidateRecipe(c) r := internal.ValidateRecipe(args, cmd.Name())
if err := r.Ensure(false, false); err != nil { if err := r.Ensure(false, false); err != nil {
log.Fatal(err) log.Fatal(err)
} }
return nil return
} }
catalogue, err := recipe.ReadRecipeCatalogue(internal.Offline) catalogue, err := recipe.ReadRecipeCatalogue(internal.Offline)
@ -46,7 +55,19 @@ var recipeFetchCommand = cli.Command{
} }
catlBar.Add(1) catlBar.Add(1)
} }
return nil
}, },
} }
var (
fetchAllRecipes bool
)
func init() {
RecipeFetchCommand.Flags().BoolVarP(
&fetchAllRecipes,
"all",
"a",
false,
"fetch all recipes",
)
}

View File

@ -1,32 +1,27 @@
package recipe package recipe
import ( import (
"fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var recipeLintCommand = cli.Command{ var RecipeLintCommand = &cobra.Command{
Name: "lint", Use: "lint <recipe> [flags]",
Usage: "Lint a recipe", Short: "Lint a recipe",
Aliases: []string{"l"}, Aliases: []string{"l"},
ArgsUsage: "<recipe>", Args: cobra.MinimumNArgs(1),
Flags: []cli.Flag{ ValidArgsFunction: func(
internal.DebugFlag, cmd *cobra.Command,
internal.OnlyErrorFlag, args []string,
internal.OfflineFlag, toComplete string) ([]string, cobra.ShellCompDirective) {
internal.NoInputFlag, return autocomplete.RecipeNameComplete()
internal.ChaosFlag,
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
BashComplete: autocomplete.RecipeNameComplete, recipe := internal.ValidateRecipe(args, cmd.Name())
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err) log.Fatal(err)
@ -53,7 +48,7 @@ var recipeLintCommand = cli.Command{
var warnMessages []string var warnMessages []string
for level := range lint.LintRules { for level := range lint.LintRules {
for _, rule := range lint.LintRules[level] { for _, rule := range lint.LintRules[level] {
if internal.OnlyErrors && rule.Level != "error" { if onlyError && rule.Level != "error" {
log.Debugf("skipping %s, does not have level \"error\"", rule.Ref) log.Debugf("skipping %s, does not have level \"error\"", rule.Ref)
continue continue
} }
@ -107,7 +102,9 @@ var recipeLintCommand = cli.Command{
} }
if len(rows) > 0 { if len(rows) > 0 {
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
for _, warnMsg := range warnMessages { for _, warnMsg := range warnMessages {
log.Warn(warnMsg) log.Warn(warnMsg)
@ -117,7 +114,27 @@ var recipeLintCommand = cli.Command{
log.Warnf("critical errors present in %s config", recipe.Name) 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

@ -10,32 +10,18 @@ import (
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var pattern string var RecipeListCommand = &cobra.Command{
var patternFlag = &cli.StringFlag{ Use: "list",
Name: "pattern, p", Short: "List recipes",
Value: "",
Usage: "Simple string to filter recipes",
Destination: &pattern,
}
var recipeListCommand = cli.Command{
Name: "list",
Usage: "List available recipes",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Flags: []cli.Flag{ Args: cobra.NoArgs,
internal.DebugFlag, Run: func(cmd *cobra.Command, args []string) {
internal.MachineReadableFlag,
patternFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
catl, err := recipe.ReadRecipeCatalogue(internal.Offline) catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil { if err != nil {
log.Fatal(err.Error()) log.Fatal(err)
} }
recipes := catl.Flatten() recipes := catl.Flatten()
@ -90,13 +76,34 @@ var recipeListCommand = cli.Command{
log.Fatal("unable to render to JSON: %s", err) log.Fatal("unable to render to JSON: %s", err)
} }
fmt.Println(out) fmt.Println(out)
return nil return
} }
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
log.Infof("total recipes: %v", len(rows)) log.Fatal(err)
}
} }
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,18 +2,17 @@ package recipe
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"os" "os"
"path" "path"
"text/template" "text/template"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
// recipeMetadata is the recipe metadata for the README.md // recipeMetadata is the recipe metadata for the README.md
@ -30,33 +29,22 @@ type recipeMetadata struct {
SSO string SSO string
} }
var recipeNewCommand = cli.Command{ var RecipeNewCommand = &cobra.Command{
Name: "new", Use: "new <recipe> [flags]",
Aliases: []string{"n"}, Aliases: []string{"n"},
Flags: []cli.Flag{ Short: "Create a new recipe",
internal.DebugFlag, Long: `A community managed recipe template is used.`,
internal.NoInputFlag, Args: cobra.ExactArgs(1),
internal.OfflineFlag, ValidArgsFunction: func(
internal.GitNameFlag, cmd *cobra.Command,
internal.GitEmailFlag, args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
Usage: "Create a new recipe", recipeName := args[0]
ArgsUsage: "<recipe>",
Description: `
Create a new recipe.
Abra uses the built-in example repository which is available here:
https://git.coopcloud.tech/coop-cloud/example`,
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
r := recipe.Get(recipeName) r := recipe.Get(recipeName)
if recipeName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
}
if _, err := os.Stat(r.Dir); !os.IsNotExist(err) { if _, err := os.Stat(r.Dir); !os.IsNotExist(err) {
log.Fatalf("%s recipe directory already exists?", r.Dir) log.Fatalf("%s recipe directory already exists?", r.Dir)
} }
@ -91,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.Fatal(err)
} }
log.Infof("new recipe '%s' created: %s", recipeName, path.Join(r.Dir)) log.Infof("new recipe '%s' created: %s", recipeName, path.Join(r.Dir))
log.Info("happy hacking 🎉") log.Info("happy hacking 🎉")
return nil
}, },
} }
@ -117,3 +103,26 @@ func newRecipeMeta(recipeName string) recipeMetadata {
SSO: "No", 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,34 +1,19 @@
package recipe package recipe
import ( import "github.com/spf13/cobra"
"github.com/urfave/cli"
)
// RecipeCommand defines all recipe related sub-commands. // RecipeCommand defines all recipe related sub-commands.
var RecipeCommand = cli.Command{ var RecipeCommand = &cobra.Command{
Name: "recipe", Use: "recipe [cmd] [args] [flags]",
Aliases: []string{"r"}, Aliases: []string{"r"},
Usage: "Manage recipes", Short: "Manage recipes",
ArgsUsage: "<recipe>", Long: `A recipe is a blueprint for an app.
Description: `
A recipe is a blueprint for an app. It is a bunch of config files which It is a bunch of config files which describe how to deploy and maintain an app.
describe how to deploy and maintain an app. Recipes are maintained by the Co-op Recipes are maintained by the Co-op Cloud community and you can use Abra to
Cloud community and you can use Abra to read them, deploy them and create apps read them, deploy them and create apps for you.
for you.
Anyone who uses a recipe can become a maintainer. Maintainers typically make Anyone who uses a recipe can become a maintainer. Maintainers typically make
sure the recipe is in good working order and the config upgraded in a timely sure the recipe is in good working order and the config upgraded in a timely
manner.`, manner.`,
Subcommands: []cli.Command{
recipeFetchCommand,
recipeLintCommand,
recipeListCommand,
recipeNewCommand,
recipeReleaseCommand,
recipeSyncCommand,
recipeUpgradeCommand,
recipeVersionCommand,
recipeResetCommand,
recipeDiffCommand,
},
} }

View File

@ -18,17 +18,17 @@ import (
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/distribution/reference" "github.com/distribution/reference"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var recipeReleaseCommand = cli.Command{ var RecipeReleaseCommand = &cobra.Command{
Name: "release", Use: "release <recipe> [version] [flags]",
Aliases: []string{"rl"}, Aliases: []string{"rl"},
Usage: "Release a new recipe version", Short: "Release a new recipe version",
ArgsUsage: "<recipe> [<version>]", Long: `Create a new version of a recipe.
Description: `
Create a new version of a recipe. These versions are then published on the These versions are then published on the Co-op Cloud recipe catalogue. These
Co-op Cloud recipe catalogue. These versions take the following form: versions take the following form:
a.b.c+x.y.z a.b.c+x.y.z
@ -42,23 +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 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. 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 requires that you have permission to git push to these repositories and have
your SSH keys configured on your account.`, your SSH keys configured on your account.`,
Flags: []cli.Flag{ Args: cobra.RangeArgs(1, 2),
internal.DebugFlag, ValidArgsFunction: func(
internal.NoInputFlag, cmd *cobra.Command,
internal.DryFlag, args []string,
internal.MajorFlag, toComplete string) ([]string, cobra.ShellCompDirective) {
internal.MinorFlag, switch l := len(args); l {
internal.PatchFlag, case 0:
internal.PublishFlag, return autocomplete.RecipeNameComplete()
internal.OfflineFlag, case 1:
return autocomplete.RecipeVersionComplete(args[0])
default:
return nil, cobra.ShellCompDirectiveDefault
}
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
BashComplete: autocomplete.RecipeNameComplete, recipe := internal.ValidateRecipe(args, cmd.Name())
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
imagesTmp, err := getImageVersions(recipe) imagesTmp, err := getImageVersions(recipe)
if err != nil { 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) log.Fatalf("main app service version for %s is empty?", recipe.Name)
} }
tagString := c.Args().Get(1) var tagString string
if len(args) == 2 {
tagString = args[1]
}
if tagString != "" { if tagString != "" {
if _, err := tagcmp.Parse(tagString); err != nil { if _, err := tagcmp.Parse(tagString); err != nil {
log.Fatalf("cannot parse %s, invalid tag specified?", tagString) log.Fatalf("cannot parse %s, invalid tag specified?", tagString)
@ -133,7 +139,7 @@ your SSH keys configured on your account.`,
} }
} }
return nil return
}, },
} }
@ -246,7 +252,14 @@ func getTagCreateOptions(tag string) (git.CreateTagOptions, error) {
// addReleaseNotes checks if the release/next release note exists and moves the // addReleaseNotes checks if the release/next release note exists and moves the
// file to release/<tag>. // file to release/<tag>.
func addReleaseNotes(recipe recipe.Recipe, tag string) error { func addReleaseNotes(recipe recipe.Recipe, tag string) error {
tagReleaseNotePath := path.Join(recipe.Dir, "release", tag) releaseDir := path.Join(recipe.Dir, "release")
if _, err := os.Stat(releaseDir); errors.Is(err, os.ErrNotExist) {
if err := os.Mkdir(releaseDir, 0755); err != nil {
return err
}
}
tagReleaseNotePath := path.Join(releaseDir, tag)
if _, err := os.Stat(tagReleaseNotePath); err == nil { if _, err := os.Stat(tagReleaseNotePath); err == nil {
// Release note for current tag already exist exists. // Release note for current tag already exist exists.
return nil return nil
@ -254,13 +267,14 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
return err return err
} }
nextReleaseNotePath := path.Join(recipe.Dir, "release", "next") nextReleaseNotePath := path.Join(releaseDir, "next")
if _, err := os.Stat(nextReleaseNotePath); err == nil { if _, err := os.Stat(nextReleaseNotePath); err == nil {
// release/next note exists. Move it to release/<tag> // release/next note exists. Move it to release/<tag>
if internal.Dry { if internal.Dry {
log.Debugf("dry run: move release note from 'next' to %s", tag) log.Debugf("dry run: move release note from 'next' to %s", tag)
return nil return nil
} }
if !internal.NoInput { if !internal.NoInput {
prompt := &survey.Input{ prompt := &survey.Input{
Message: "Use release note in release/next?", Message: "Use release note in release/next?",
@ -273,14 +287,17 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
return nil return nil
} }
} }
err := os.Rename(nextReleaseNotePath, tagReleaseNotePath) err := os.Rename(nextReleaseNotePath, tagReleaseNotePath)
if err != nil { if err != nil {
return err return err
} }
err = gitPkg.Add(recipe.Dir, path.Join("release", "next"), internal.Dry) err = gitPkg.Add(recipe.Dir, path.Join("release", "next"), internal.Dry)
if err != nil { if err != nil {
return err return err
} }
err = gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry) err = gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry)
if err != nil { if err != nil {
return err return err
@ -297,6 +314,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
prompt := &survey.Input{ prompt := &survey.Input{
Message: "Release Note (leave empty for no release note)", Message: "Release Note (leave empty for no release note)",
} }
var releaseNote string var releaseNote string
if err := survey.AskOne(prompt, &releaseNote); err != nil { if err := survey.AskOne(prompt, &releaseNote); err != nil {
return err return err
@ -306,12 +324,11 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
return nil return nil
} }
err := os.WriteFile(tagReleaseNotePath, []byte(releaseNote), 0o644) if err := os.WriteFile(tagReleaseNotePath, []byte(releaseNote), 0o644); err != nil {
if err != nil {
return err 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 return err
} }
@ -376,17 +393,17 @@ func pushRelease(recipe recipe.Recipe, tagString string) error {
return nil return nil
} }
if !internal.Publish && !internal.NoInput { if !publish && !internal.NoInput {
prompt := &survey.Confirm{ prompt := &survey.Confirm{
Message: "publish new release?", Message: "publish new release?",
} }
if err := survey.AskOne(prompt, &internal.Publish); err != nil { if err := survey.AskOne(prompt, &publish); err != nil {
return err return err
} }
} }
if internal.Publish { if publish {
if err := recipe.Push(internal.Dry); err != nil { if err := recipe.Push(internal.Dry); err != nil {
return err return err
} }
@ -546,3 +563,50 @@ func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
return initTag, nil 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

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

View File

@ -12,35 +12,38 @@ import (
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var recipeSyncCommand = cli.Command{ var RecipeSyncCommand = &cobra.Command{
Name: "sync", Use: "sync <recipe> [version] [flags]",
Aliases: []string{"s"}, Aliases: []string{"s"},
Usage: "Sync recipe version label", Short: "Sync recipe version label",
ArgsUsage: "<recipe> [<version>]", Long: `Generate labels for the main recipe service.
Flags: []cli.Flag{
internal.DebugFlag, By convention, the service named "app" using the following format:
internal.NoInputFlag,
internal.DryFlag,
internal.MajorFlag,
internal.MinorFlag,
internal.PatchFlag,
},
Before: internal.SubCommandBefore,
Description: `
Generate labels for the main recipe service (i.e. by convention, the service
named "app") which corresponds to the following format:
coop-cloud.${STACK_NAME}.version=<version> coop-cloud.${STACK_NAME}.version=<version>
Where <version> can be specifed on the command-line or Abra can attempt to Where [version] can be specifed on the command-line or Abra can attempt to
auto-generate it for you. The <recipe> configuration will be updated on the auto-generate it for you. The <recipe> configuration will be updated on the
local file system.`, local file system.`,
BashComplete: autocomplete.RecipeNameComplete, Args: cobra.RangeArgs(1, 2),
Action: func(c *cli.Context) error { ValidArgsFunction: func(
recipe := internal.ValidateRecipe(c) 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) mainApp, err := internal.GetMainAppImage(recipe)
if err != nil { if err != nil {
@ -59,7 +62,11 @@ local file system.`,
log.Fatal(err) log.Fatal(err)
} }
nextTag := c.Args().Get(1) var nextTag string
if len(args) == 2 {
nextTag = args[1]
}
if len(tags) == 0 && nextTag == "" { if len(tags) == 0 && nextTag == "" {
log.Warnf("no git tags found for %s", recipe.Name) log.Warnf("no git tags found for %s", recipe.Name)
if internal.NoInput { if internal.NoInput {
@ -205,7 +212,39 @@ likely to change.
log.Fatal(err) 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

@ -19,7 +19,7 @@ import (
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/distribution/reference" "github.com/distribution/reference"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
type imgPin struct { type imgPin struct {
@ -27,8 +27,8 @@ type imgPin struct {
version tagcmp.Tag version tagcmp.Tag
} }
// anUpgrade represents a single service upgrade (as within a recipe), and the list of tags that it can be upgraded to, // anUpgrade represents a single service upgrade (as within a recipe), and the
// for serialization purposes. // list of tags that it can be upgraded to, for serialization purposes.
type anUpgrade struct { type anUpgrade struct {
Service string `json:"service"` Service string `json:"service"`
Image string `json:"image"` Image string `json:"image"`
@ -36,14 +36,13 @@ type anUpgrade struct {
UpgradeTags []string `json:"upgrades"` UpgradeTags []string `json:"upgrades"`
} }
var recipeUpgradeCommand = cli.Command{ var RecipeUpgradeCommand = &cobra.Command{
Name: "upgrade", Use: "upgrade <recipe> [flags]",
Aliases: []string{"u"}, Aliases: []string{"u"},
Usage: "Upgrade recipe image tags", Short: "Upgrade recipe image tags",
Description: ` Long: `Upgrade a given <recipe> configuration.
Parse all image tags within the given <recipe> configuration and prompt with
more recent tags to upgrade to. It will update the relevant compose file tags It will update the relevant compose file tags on the local file system.
on the local file system.
Some image tags cannot be parsed because they do not follow some sort of Some image tags cannot be parsed because they do not follow some sort of
semver-like convention. In this case, all possible tags will be listed and it semver-like convention. In this case, all possible tags will be listed and it
@ -53,25 +52,16 @@ The command is interactive and will show a select input which allows you to
make a seclection. Use the "?" key to see more help on navigating this make a seclection. Use the "?" key to see more help on navigating this
interface. interface.
You may invoke this command in "wizard" mode and be prompted for input. You may invoke this command in "wizard" mode and be prompted for input.`,
Args: cobra.RangeArgs(0, 1),
EXAMPLE: ValidArgsFunction: func(
cmd *cobra.Command,
abra recipe upgrade`, args []string,
ArgsUsage: "<recipe>", toComplete string) ([]string, cobra.ShellCompDirective) {
Flags: []cli.Flag{ return autocomplete.RecipeNameComplete()
internal.DebugFlag,
internal.NoInputFlag,
internal.PatchFlag,
internal.MinorFlag,
internal.MajorFlag,
internal.MachineReadableFlag,
internal.AllTagsFlag,
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
BashComplete: autocomplete.RecipeNameComplete, recipe := internal.ValidateRecipe(args, cmd.Name())
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err) log.Fatal(err)
@ -182,7 +172,7 @@ EXAMPLE:
sort.Sort(tagcmp.ByTagDesc(compatible)) 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)) 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 continue // skip on to the next tag and don't update any compose files
} }
@ -236,7 +226,7 @@ EXAMPLE:
for _, upTag := range compatible { for _, upTag := range compatible {
upElement, err := tag.UpgradeDelta(upTag) upElement, err := tag.UpgradeDelta(upTag)
if err != nil { if err != nil {
return err return
} }
delta := upElement.UpgradeType() delta := upElement.UpgradeType()
if delta <= bumpType { if delta <= bumpType {
@ -250,9 +240,9 @@ EXAMPLE:
} }
} else { } else {
msg := fmt.Sprintf("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, tag) 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() 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)) 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) msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
@ -320,7 +310,7 @@ EXAMPLE:
fmt.Println(string(jsonstring)) fmt.Println(string(jsonstring))
return nil return
} }
for _, upgrade := range upgradeList { for _, upgrade := range upgradeList {
@ -341,7 +331,51 @@ EXAMPLE:
log.Fatal(err) 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

@ -9,36 +9,24 @@ import (
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
func sortServiceByName(versions [][]string) func(i, j int) bool { var RecipeVersionCommand = &cobra.Command{
return func(i, j int) bool { Use: "versions <recipe> [flags]",
// NOTE(d1): corresponds to the `tableCol` definition below Aliases: []string{"v"},
if versions[i][1] == "app" { Short: "List recipe versions",
return true Args: cobra.ExactArgs(1),
} ValidArgsFunction: func(
return versions[i][1] < versions[j][1] cmd *cobra.Command,
} args []string,
} toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
var recipeVersionCommand = cli.Command{
Name: "versions",
Aliases: []string{"v"},
Usage: "List recipe versions",
ArgsUsage: "<recipe>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
internal.NoInputFlag,
internal.MachineReadableFlag,
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
var warnMessages []string var warnMessages []string
recipe := internal.ValidateRecipe(c) recipe := internal.ValidateRecipe(args, cmd.Name())
catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline) catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
if err != nil { if err != nil {
@ -67,15 +55,32 @@ var recipeVersionCommand = cli.Command{
log.Fatal(err) log.Fatal(err)
} }
table.Headers("SERVICE", "NAME", "TAG") table.Headers("SERVICE", "IMAGE", "TAG", "VERSION")
for version, meta := range recipeMeta.Versions[i] { for version, meta := range recipeMeta.Versions[i] {
var allRows [][]string var allRows [][]string
var rows [][]string var rows [][]string
for service, serviceMeta := range meta { for service, serviceMeta := range meta {
rows = append(rows, []string{service, serviceMeta.Image, serviceMeta.Tag}) recipeVersion := version
allRows = append(allRows, []string{version, service, serviceMeta.Image, serviceMeta.Tag}) if service != "app" {
recipeVersion = ""
}
rows = append(rows, []string{
service,
serviceMeta.Image,
serviceMeta.Tag,
recipeVersion,
})
allRows = append(allRows, []string{
version,
service,
serviceMeta.Image,
serviceMeta.Tag,
recipeVersion,
})
} }
sort.Slice(rows, sortServiceByName(rows)) sort.Slice(rows, sortServiceByName(rows))
@ -83,9 +88,9 @@ var recipeVersionCommand = cli.Command{
table.Rows(rows...) table.Rows(rows...)
if !internal.MachineReadable { if !internal.MachineReadable {
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
log.Infof("VERSION: %s", version) log.Fatal(err)
fmt.Println() }
continue continue
} }
@ -107,7 +112,21 @@ var recipeVersionCommand = cli.Command{
log.Warn(warnMsg) log.Warn(warnMsg)
} }
} }
return nil
}, },
} }
func sortServiceByName(versions [][]string) func(i, j int) bool {
return func(i, j int) bool {
return versions[i][0] < versions[j][0]
}
}
func init() {
RecipeVersionCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
false,
"print machine-readable output",
)
}

195
cli/run.go Normal file
View File

@ -0,0 +1,195 @@
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)
},
}
rootCmd.CompletionOptions.DisableDefaultCmd = true
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.AppBackupCommand.AddCommand(
app.AppBackupListCommand,
app.AppBackupDownloadCommand,
app.AppBackupCreateCommand,
app.AppBackupSnapshotsCommand,
)
app.AppCommand.AddCommand(
app.AppBackupCommand,
app.AppCheckCommand,
app.AppCmdCommand,
app.AppConfigCommand,
app.AppCpCommand,
app.AppDeployCommand,
app.AppListCommand,
app.AppLogsCommand,
app.AppNewCommand,
app.AppPsCommand,
app.AppRemoveCommand,
app.AppRestartCommand,
app.AppRestoreCommand,
app.AppRollbackCommand,
app.AppRunCommand,
app.AppSecretCommand,
app.AppServicesCommand,
app.AppUndeployCommand,
app.AppUpgradeCommand,
app.AppVolumeCommand,
)
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
}

View File

@ -1,11 +1,11 @@
package server package server
import ( import (
"errors"
"os" "os"
"path/filepath" "path/filepath"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
contextPkg "coopcloud.tech/abra/pkg/context" contextPkg "coopcloud.tech/abra/pkg/context"
@ -13,14 +13,112 @@ import (
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/server" "coopcloud.tech/abra/pkg/server"
sshPkg "coopcloud.tech/abra/pkg/ssh" sshPkg "coopcloud.tech/abra/pkg/ssh"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var local bool var ServerAddCommand = &cobra.Command{
var localFlag = &cli.BoolFlag{ Use: "add [[server] | --local] [flags]",
Name: "local, l", Aliases: []string{"a"},
Usage: "Use local server", Short: "Add a new server",
Destination: &local, 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
}
_, err := createServerDir(name)
if err != nil {
log.Fatal(err)
}
created, err := newContext(name)
if err != nil {
cleanUp(name)
log.Fatalf("unable to create local context: %s", err)
}
log.Debugf("attempting to create client for %s", name)
if _, err := client.New(name, timeout); err != nil {
cleanUp(name)
log.Debugf("ssh %s error: %s", name, sshPkg.Fatal(name, err))
log.Fatalf("can't ssh to %s, make sure \"ssh %s\" works", name, name)
}
if created {
log.Infof("%s successfully added", name)
if _, err := dns.EnsureIPv4(name); err != nil {
log.Warnf("unable to resolve IPv4 for %s", name)
}
return
}
log.Warnf("%s already exists", name)
},
} }
// cleanUp cleans up the partially created context/client details for a failed // cleanUp cleans up the partially created context/client details for a failed
@ -91,114 +189,16 @@ func createServerDir(name string) (bool, error) {
return true, nil return true, nil
} }
var serverAddCommand = cli.Command{ var (
Name: "add", local bool
Aliases: []string{"a"}, )
Usage: "Add a new server to your configuration",
Description: `
Add a new server to your configuration so that it can be managed by Abra.
Abra relies on the standard SSH command-line and ~/.ssh/config for client func init() {
connection details. You must configure an entry per-host in your ~/.ssh/config ServerAddCommand.Flags().BoolVarP(
for each server. For example: &local,
"local",
Host example.com example "l",
Hostname example.com false,
User exampleUser "use local server",
Port 12345 )
IdentityFile ~/.ssh/example@somewhere
You can then add a server like so:
abra server add example.com
If "--local" is passed, then Abra assumes that the current local server is
intended as the target server. This is useful when you want to have your entire
Co-op Cloud config located on the server itself, and not on your local
developer machine. The domain is then set to "default".
You can also pass "--no-domain-checks/-D" flag to use any arbitrary name
instead of a real domain. The host will be resolved with the "Hostname" entry
of your ~/.ssh/config. Checks for a valid online domain will be skipped:
abra server add -D example`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.NoDomainChecksFlag,
localFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<name>",
Action: func(c *cli.Context) error {
if len(c.Args()) > 0 && local || !internal.ValidateSubCmdFlags(c) {
err := errors.New("cannot use <name> and --local together")
internal.ShowSubcommandHelpAndError(c, err)
}
var name string
if local {
name = "default"
} else {
name = internal.ValidateDomain(c)
}
// 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 !internal.NoDomainChecks {
if _, err := dns.EnsureIPv4(name); err != nil {
log.Fatal(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
},
} }

View File

@ -6,25 +6,20 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/context" contextPkg "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/docker/cli/cli/connhelper/ssh" "github.com/docker/cli/cli/connhelper/ssh"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var serverListCommand = cli.Command{ var ServerListCommand = &cobra.Command{
Name: "list", Use: "list [flags]",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Usage: "List managed servers", Short: "List managed servers",
Flags: []cli.Flag{ Args: cobra.NoArgs,
internal.DebugFlag, Run: func(cmd *cobra.Command, args []string) {
internal.MachineReadableFlag, dockerContextStore := contextPkg.NewDefaultDockerContextStore()
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
dockerContextStore := context.NewDefaultDockerContextStore()
contexts, err := dockerContextStore.Store.List() contexts, err := dockerContextStore.Store.List()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -46,14 +41,14 @@ var serverListCommand = cli.Command{
var rows [][]string var rows [][]string
for _, serverName := range serverNames { for _, serverName := range serverNames {
var row []string var row []string
for _, ctx := range contexts { for _, dockerCtx := range contexts {
endpoint, err := context.GetContextEndpoint(ctx) endpoint, err := contextPkg.GetContextEndpoint(dockerCtx)
if err != nil && strings.Contains(err.Error(), "does not exist") { if err != nil && strings.Contains(err.Error(), "does not exist") {
// No local context found, we can continue safely // No local context found, we can continue safely
continue continue
} }
if ctx.Name == serverName { if dockerCtx.Name == serverName {
sp, err := ssh.ParseURL(endpoint) sp, err := ssh.ParseURL(endpoint)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -85,12 +80,24 @@ var serverListCommand = cli.Command{
if err != nil { if err != nil {
log.Fatal("unable to render to JSON: %s", err) log.Fatal("unable to render to JSON: %s", err)
} }
fmt.Println(out) fmt.Println(out)
return nil
return
} }
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
return nil }
}, },
} }
func init() {
ServerListCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
false,
"print machine-readable output",
)
}

View File

@ -1,64 +1,41 @@
package server package server
import ( import (
"context"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
var allFilter bool var ServerPruneCommand = &cobra.Command{
Use: "prune <server> [flags]",
var allFilterFlag = &cli.BoolFlag{
Name: "all, a",
Usage: "Remove all unused images not just dangling ones",
Destination: &allFilter,
}
var volumesFilter bool
var volumesFilterFlag = &cli.BoolFlag{
Name: "volumes, v",
Usage: "Prune volumes. This will remove app data, Be Careful!",
Destination: &volumesFilter,
}
var serverPruneCommand = cli.Command{
Name: "prune",
Aliases: []string{"p"}, Aliases: []string{"p"},
Usage: "Prune resources on a server", Short: "Prune resources on a server",
Description: ` Long: `Prunes unused containers, networks, and dangling images.
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.`, app. This can result in unwanted data loss if not used carefully.`,
ArgsUsage: "[<server>]", Args: cobra.ExactArgs(1),
Flags: []cli.Flag{ ValidArgsFunction: func(
allFilterFlag, cmd *cobra.Command,
volumesFilterFlag, args []string,
internal.DebugFlag, toComplete string) ([]string, cobra.ShellCompDirective) {
internal.OfflineFlag, return autocomplete.ServerNameComplete()
internal.NoInputFlag,
}, },
Before: internal.SubCommandBefore, Run: func(cmd *cobra.Command, args []string) {
BashComplete: autocomplete.ServerNameComplete, serverName := internal.ValidateServer(args)
Action: func(c *cli.Context) error {
serverName := internal.ValidateServer(c)
cl, err := client.New(serverName) cl, err := client.New(serverName)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
var args filters.Args var filterArgs filters.Args
ctx := context.Background() cr, err := cl.ContainersPrune(cmd.Context(), filterArgs)
cr, err := cl.ContainersPrune(ctx, args)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -66,7 +43,7 @@ app. This can result in unwanted data loss if not used carefully.`,
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed) cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
log.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed) 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -79,7 +56,7 @@ app. This can result in unwanted data loss if not used carefully.`,
pruneFilters.Add("dangling", "false") pruneFilters.Add("dangling", "false")
} }
ir, err := cl.ImagesPrune(ctx, pruneFilters) ir, err := cl.ImagesPrune(cmd.Context(), pruneFilters)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -88,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) log.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed)
if volumesFilter { if volumesFilter {
vr, err := cl.VolumesPrune(ctx, args) vr, err := cl.VolumesPrune(cmd.Context(), filterArgs)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -97,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) 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

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

View File

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

View File

@ -21,46 +21,25 @@ import (
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerclient "github.com/docker/docker/client" dockerclient "github.com/docker/docker/client"
"github.com/spf13/cobra"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli"
) )
const SERVER = "localhost" const SERVER = "localhost"
var majorUpdate bool // NotifyCommand checks for available upgrades.
var majorFlag = &cli.BoolFlag{ var NotifyCommand = &cobra.Command{
Name: "major, m", Use: "notify [flags]",
Usage: "Also check for major updates",
Destination: &majorUpdate,
}
var updateAll bool
var allFlag = &cli.BoolFlag{
Name: "all, a",
Usage: "Update all deployed apps",
Destination: &updateAll,
}
// Notify checks for available upgrades
var Notify = cli.Command{
Name: "notify",
Aliases: []string{"n"}, Aliases: []string{"n"},
Usage: "Check for available upgrades", Short: "Check for available upgrades",
Flags: []cli.Flag{ Long: `Notify on new versions for deployed apps.
internal.DebugFlag,
majorFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `
Read the deployed app versions and look for new versions in the recipe
catalogue.
If a new patch/minor version is available, a notification is printed. If a new patch/minor version is available, a notification is printed.
Use "--major" to include new major versions.`, Use "--major/-m" to include new major versions.`,
Action: func(c *cli.Context) error { Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
cl, err := client.New("default") cl, err := client.New("default")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -85,27 +64,15 @@ Use "--major" to include new major versions.`,
} }
} }
} }
return nil
}, },
} }
// UpgradeApp upgrades apps. // UpgradeCommand upgrades apps.
var UpgradeApp = cli.Command{ var UpgradeCommand = &cobra.Command{
Name: "upgrade", Use: "upgrade [[stack] [recipe] | --all] [flags]",
Aliases: []string{"u"}, Aliases: []string{"u"},
Usage: "Upgrade apps", Short: "Upgrade apps",
ArgsUsage: "<stack-name> <recipe>", Long: `Upgrade an app by specifying stack name and recipe.
Flags: []cli.Flag{
internal.DebugFlag,
internal.ChaosFlag,
majorFlag,
allFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `
Upgrade an app by specifying stack name and recipe.
Use "--all" to upgrade every deployed app. Use "--all" to upgrade every deployed app.
@ -113,24 +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 the current recipe catalogue version. If a new patch/minor version is
available, the app is upgraded. available, the app is upgraded.
To include major versions use the "--major" flag. You probably don't want that To include major versions use the "--major/-m" flag. You probably don't want
as it will break things. Only apps that are not deployed with "--chaos" are that as it will break things. Only apps that are not deployed with "--chaos/-C"
upgraded, to update chaos deployments use the "--chaos" flag. Use it with care.`, are upgraded, to update chaos deployments use the "--chaos/-C" flag. Use it
Action: func(c *cli.Context) error { 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") cl, err := client.New("default")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !updateAll && len(args) != 2 {
log.Fatal("missing arguments or --all/-a flag")
}
if !updateAll { if !updateAll {
stackName := c.Args().Get(0) stackName := args[0]
recipeName := c.Args().Get(1) recipeName := args[1]
err = tryUpgrade(cl, stackName, recipeName) err = tryUpgrade(cl, stackName, recipeName)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
return nil return
} }
stacks, err := stack.GetStacks(cl) stacks, err := stack.GetStacks(cl)
@ -150,8 +130,6 @@ upgraded, to update chaos deployments use the "--chaos" flag. Use it with care.`
log.Fatal(err) log.Fatal(err)
} }
} }
return nil
}, },
} }
@ -311,7 +289,7 @@ func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName
return nil, err return nil, err
} }
if 0 < versionDelta.UpgradeType() && (versionDelta.UpgradeType() < 4 || majorUpdate) { if 0 < versionDelta.UpgradeType() && (versionDelta.UpgradeType() < 4 || includeMajorUpdates) {
availableUpgrades = append(availableUpgrades, version) availableUpgrades = append(availableUpgrades, version)
} }
} }
@ -468,41 +446,87 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion stri
return err return err
} }
func newAbraApp(version, commit string) *cli.App { func newKadabraApp(version, commit string) *cobra.Command {
app := &cli.App{ rootCmd := &cobra.Command{
Name: "kadabra", Use: "kadabra [cmd] [flags]",
Usage: `The Co-op Cloud auto-updater
____ ____ _ _
/ ___|___ ___ _ __ / ___| | ___ _ _ __| |
| | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |
| |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |
\____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|
|_|
`,
Version: fmt.Sprintf("%s-%s", version, commit[:7]), Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Commands: []cli.Command{ Short: "The Co-op Cloud auto-updater 🤖 🚀",
Notify, PersistentPreRun: func(cmd *cobra.Command, args []string) {
UpgradeApp, 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(c *cli.Context) error { rootCmd.PersistentFlags().BoolVarP(
log.Logger.SetStyles(log.Styles()) &internal.Debug, "debug", "d", false,
charmLog.SetDefault(log.Logger) "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,
)
return app return rootCmd
} }
// RunApp runs CLI abra app. // RunApp runs CLI abra app.
func RunApp(version, commit string) { func RunApp(version, commit string) {
app := newAbraApp(version, commit) app := newKadabraApp(version, commit)
if err := app.Run(os.Args); err != nil { if err := app.Execute(); err != nil {
log.Fatal(err) 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)",
)
}

View File

@ -19,5 +19,5 @@ func main() {
Commit = " " Commit = " "
} }
cli.RunApp(Version, Commit) cli.Run(Version, Commit)
} }

109
go.mod
View File

@ -1,43 +1,49 @@
module coopcloud.tech/abra module coopcloud.tech/abra
go 1.21 go 1.22.7
toolchain go1.23.1
replace github.com/urfave/cli/v3 => github.com/urfave/cli/v3 v3.0.0-alpha9.1.0.20241019193437-5053ec708a44
require ( require (
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355
github.com/AlecAivazis/survey/v2 v2.3.7 github.com/AlecAivazis/survey/v2 v2.3.7
github.com/charmbracelet/lipgloss v0.11.1 github.com/charmbracelet/lipgloss v1.0.0
github.com/charmbracelet/log v0.4.0 github.com/charmbracelet/log v0.4.0
github.com/distribution/reference v0.6.0 github.com/distribution/reference v0.6.0
github.com/docker/cli v27.0.3+incompatible github.com/docker/cli v27.4.1+incompatible
github.com/docker/docker v27.0.3+incompatible github.com/docker/docker v27.4.1+incompatible
github.com/docker/go-units v0.5.0 github.com/docker/go-units v0.5.0
github.com/go-git/go-git/v5 v5.12.0 github.com/go-git/go-git/v5 v5.12.0
github.com/google/go-cmp v0.6.0 github.com/google/go-cmp v0.6.0
github.com/moby/sys/signal v0.7.0 github.com/moby/sys/signal v0.7.1
github.com/moby/term v0.5.0 github.com/moby/term v0.5.0
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.14.4 github.com/schollz/progressbar/v3 v3.17.1
golang.org/x/term v0.22.0 github.com/urfave/cli/v3 v3.0.0-alpha9
golang.org/x/term v0.27.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.1 gotest.tools/v3 v3.5.1
) )
require ( require (
dario.cat/mergo v1.0.0 // indirect dario.cat/mergo v1.0.1 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/ProtonMail/go-crypto v1.1.3 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/x/ansi v0.1.3 // indirect github.com/charmbracelet/x/ansi v0.6.0 // indirect
github.com/cloudflare/circl v1.3.9 // indirect github.com/cloudflare/circl v1.5.0 // indirect
github.com/containerd/log v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/cyphar/filepath-securejoin v0.2.5 // indirect github.com/cyphar/filepath-securejoin v0.3.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
@ -46,77 +52,83 @@ require (
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-git/go-billy/v5 v5.6.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/compress v1.17.11 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/user v0.1.0 // indirect github.com/moby/sys/mountinfo v0.6.2 // indirect
github.com/moby/sys/user v0.3.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.0.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect github.com/muesli/termenv v0.15.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/runc v1.1.13 // indirect github.com/opencontainers/runc v1.1.13 // indirect
github.com/opencontainers/runtime-spec v1.1.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/common v0.61.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.2.2 // indirect github.com/skeema/knownhosts v1.3.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 // indirect go.opentelemetry.io/otel v1.33.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.28.0 // indirect go.opentelemetry.io/otel/metric v1.33.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect go.opentelemetry.io/otel/sdk v1.33.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.opentelemetry.io/otel/sdk/metric v1.33.0 // indirect
golang.org/x/crypto v0.25.0 // indirect go.opentelemetry.io/otel/trace v1.33.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect go.opentelemetry.io/proto/otlp v1.4.0 // indirect
golang.org/x/net v0.27.0 // indirect golang.org/x/crypto v0.31.0 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect
golang.org/x/text v0.16.0 // indirect golang.org/x/net v0.33.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/sync v0.10.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect golang.org/x/text v0.21.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect golang.org/x/time v0.8.0 // indirect
google.golang.org/grpc v1.65.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8 // indirect
google.golang.org/protobuf v1.34.2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
google.golang.org/grpc v1.69.2 // indirect
google.golang.org/protobuf v1.36.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
) )
require ( require (
github.com/containerd/containerd v1.7.19 // indirect
github.com/containers/image v3.0.2+incompatible github.com/containers/image v3.0.2+incompatible
github.com/containers/storage v1.38.2 // indirect github.com/containers/storage v1.38.2 // indirect
github.com/decentral1se/passgen v1.0.1 github.com/decentral1se/passgen v1.0.1
@ -126,14 +138,13 @@ require (
github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 github.com/hashicorp/go-retryablehttp v0.7.7
github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect github.com/moby/sys/sequential v0.6.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_golang v1.20.5 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.10.0
github.com/theupdateframework/notary v0.7.0 // indirect github.com/theupdateframework/notary v0.7.0 // indirect
github.com/urfave/cli v1.22.15
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
golang.org/x/sys v0.22.0 golang.org/x/sys v0.28.0
) )

287
go.sum
View File

@ -24,8 +24,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb h1:Ws6WEwKXeaYEkfdkX6AqX1XLPuaCeyStEtxbmEJPllk= coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb h1:Ws6WEwKXeaYEkfdkX6AqX1XLPuaCeyStEtxbmEJPllk=
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ= coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 h1:tCv2B4qoN6RMheKDnCzIafOkWS5BB1h7hwhmo+9bVeE= git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 h1:tCv2B4qoN6RMheKDnCzIafOkWS5BB1h7hwhmo+9bVeE=
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355/go.mod h1:Q8V1zbtPAlzYSr/Dvky3wS6x58IQAl3rot2me1oSO2Q= git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355/go.mod h1:Q8V1zbtPAlzYSr/Dvky3wS6x58IQAl3rot2me1oSO2Q=
@ -49,7 +49,6 @@ github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@ -80,8 +79,8 @@ github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb0
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
@ -104,6 +103,8 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:l
github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@ -126,24 +127,30 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembj
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/lipgloss v0.11.1 h1:a8KgVPHa7kOoP95vm2tQQrjD2AKhbWmfr4uJ2RW6kNk= github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
github.com/charmbracelet/lipgloss v0.11.1/go.mod h1:beLlcmkF7MWA+5UrKKIRo/VJ21xGXr7YJ9miWfdMRIU= github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/charmbracelet/x/ansi v0.1.3 h1:RBh/eleNWML5R524mjUF0yVRePTwqN9tPtV+DPgO5Lw= github.com/charmbracelet/x/ansi v0.5.2 h1:dEa1x2qdOZXD/6439s+wF7xjV+kZLu/iN00GuXXrU9E=
github.com/charmbracelet/x/ansi v0.1.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/ansi v0.5.2/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
github.com/charmbracelet/x/ansi v0.6.0 h1:qOznutrb93gx9oMiGf7caF7bqqubh6YIM0SWKyA08pA=
github.com/charmbracelet/x/ansi v0.6.0/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -156,9 +163,8 @@ github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2u
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ=
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
@ -197,8 +203,6 @@ github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09Zvgq
github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s=
github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g=
github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c=
github.com/containerd/containerd v1.7.19 h1:/xQ4XRJ0tamDkdzrrBAUy/LE5nCcxFKdBm4EcPrSMEE=
github.com/containerd/containerd v1.7.19/go.mod h1:h4FtNYUUMB4Phr6v+xG89RYKj9XccvbNSCKjdufCrkc=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
@ -276,6 +280,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
@ -283,8 +289,10 @@ github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8=
github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM=
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
@ -303,16 +311,20 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v27.0.3+incompatible h1:usGs0/BoBW8MWxGeEtqPMkzOY56jZ6kYlSN5BLDioCQ= github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ=
github.com/docker/cli v27.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI=
github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE= github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4=
github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
@ -374,8 +386,8 @@ github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8=
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
@ -411,8 +423,8 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
github.com/go-sql-driver/mysql v1.3.0 h1:pgwjLi/dvffoP9aabwkT3AKpXQM93QARkjFhDDqC1UE= github.com/go-sql-driver/mysql v1.3.0 h1:pgwjLi/dvffoP9aabwkT3AKpXQM93QARkjFhDDqC1UE=
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
@ -436,8 +448,8 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@ -517,9 +529,12 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -570,7 +585,6 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
@ -582,8 +596,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -599,6 +613,8 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
@ -621,8 +637,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
@ -656,13 +672,15 @@ github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2J
github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI= github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0=
github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8=
github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ=
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
@ -702,8 +720,8 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
@ -760,8 +778,8 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@ -775,8 +793,10 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
@ -797,15 +817,16 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/schollz/progressbar/v3 v3.14.4 h1:W9ZrDSJk7eqmQhd3uxFNNcTr0QL+xuGNI9dEMrw0r74= github.com/schollz/progressbar/v3 v3.17.1 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U=
github.com/schollz/progressbar/v3 v3.14.4/go.mod h1:aT3UQ7yGm+2ZjeXPqsjTenwL3ddUiuZ0kfQ/2tHlyNI= github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4=
github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
@ -822,8 +843,8 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
@ -858,9 +879,6 @@ github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@ -868,11 +886,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
@ -890,8 +905,8 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM= github.com/urfave/cli/v3 v3.0.0-alpha9.1.0.20241019193437-5053ec708a44 h1:BeSTAZEDkDVNv9EOrycIGCkEg+6EhRRgSsbdc93Q3OM=
github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0= github.com/urfave/cli/v3 v3.0.0-alpha9.1.0.20241019193437-5053ec708a44/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI= github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI=
github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
@ -930,29 +945,51 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 h1:U2guen0GhqH8o/G2un8f/aG/y++OuW6MyCo6hT9prXk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0/go.mod h1:yeGZANgEcpdx/WK0IvvRFC+2oLiMS2u4L/0Rj2M2Qr0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s= go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw= go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 h1:7F29RDmnlqk6B5d+sUqemt8TBfDqxryYW5gX6L74RFA=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0/go.mod h1:ZiGDq7xwDMKmWDrN1XsXAj0iC7hns+2DhxBFSncNHSE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08= go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg= go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU=
go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@ -977,10 +1014,10 @@ golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWP
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -991,8 +1028,10 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -1015,7 +1054,6 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -1057,11 +1095,10 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1079,9 +1116,10 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1159,22 +1197,19 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1184,18 +1219,18 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -1241,7 +1276,6 @@ golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4X
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -1290,10 +1324,14 @@ google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfG
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ=
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8 h1:st3LcW/BPi75W4q1jJTEor/QWwbNlPlDG0JTn6XhZu0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:klhJGKFyG8Tn50enBn7gizg4nXGXJ+jqEREdCWaPcV4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@ -1313,8 +1351,10 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -1328,8 +1368,10 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII= gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
@ -1369,6 +1411,7 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=

View File

@ -89,6 +89,8 @@ type App struct {
Env envfile.AppEnv Env envfile.AppEnv
Server string Server string
Path string Path string
status *Status
} }
// Type aliases to make code hints easier to understand // Type aliases to make code hints easier to understand
@ -132,6 +134,19 @@ func StackName(appName string) string {
return stackName return stackName
} }
func (a App) Status() (Status, error) {
if a.status != nil {
return *a.status, nil
}
appStatuses, err := GetAppStatuses([]App{a}, true)
if err != nil {
return Status{}, err
}
appStatus := appStatuses[a.StackName()]
a.status = &appStatus
return appStatus, nil
}
// Filters retrieves app filters for querying the container runtime. By default // Filters retrieves app filters for querying the container runtime. By default
// it filters on all services in the app. It is also possible to pass an // it filters on all services in the app. It is also possible to pass an
// otional list of service names, which get filtered instead. // otional list of service names, which get filtered instead.
@ -397,9 +412,17 @@ func SanitiseAppName(name string) string {
return strings.ReplaceAll(name, ".", "_") return strings.ReplaceAll(name, ".", "_")
} }
type Status struct {
Status string
Version string
Chaos bool
ChaosVersion string
AutoUpdate bool
}
// GetAppStatuses queries servers to check the deployment status of given apps. // GetAppStatuses queries servers to check the deployment status of given apps.
func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]string, error) { func GetAppStatuses(apps []App, MachineReadable bool) (map[string]Status, error) {
statuses := make(map[string]map[string]string) statuses := make(map[string]Status)
servers := make(map[string]struct{}) servers := make(map[string]struct{})
for _, app := range apps { for _, app := range apps {
@ -435,36 +458,32 @@ func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]str
} }
for _, service := range status.Services { for _, service := range status.Services {
result := make(map[string]string) result := Status{}
name := service.Spec.Labels[convert.LabelNamespace] name := service.Spec.Labels[convert.LabelNamespace]
if _, ok := statuses[name]; !ok { if _, ok := statuses[name]; !ok {
result["status"] = "deployed" result.Status = "deployed"
} }
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", name) labelKey := fmt.Sprintf("coop-cloud.%s.chaos", name)
chaos, ok := service.Spec.Labels[labelKey] chaos, ok := service.Spec.Labels[labelKey]
if ok { if ok {
result["chaos"] = chaos result.Chaos = chaos == "true"
} }
labelKey = fmt.Sprintf("coop-cloud.%s.chaos-version", name) labelKey = fmt.Sprintf("coop-cloud.%s.chaos-version", name)
if chaosVersion, ok := service.Spec.Labels[labelKey]; ok { if chaosVersion, ok := service.Spec.Labels[labelKey]; ok {
result["chaosVersion"] = chaosVersion result.ChaosVersion = chaosVersion
} }
labelKey = fmt.Sprintf("coop-cloud.%s.autoupdate", name) labelKey = fmt.Sprintf("coop-cloud.%s.autoupdate", name)
if autoUpdate, ok := service.Spec.Labels[labelKey]; ok { if autoUpdate, ok := service.Spec.Labels[labelKey]; ok {
result["autoUpdate"] = autoUpdate result.AutoUpdate = autoUpdate == "true"
} else {
result["autoUpdate"] = "false"
} }
labelKey = fmt.Sprintf("coop-cloud.%s.version", name) labelKey = fmt.Sprintf("coop-cloud.%s.version", name)
if version, ok := service.Spec.Labels[labelKey]; ok { if version, ok := service.Spec.Labels[labelKey]; ok {
result["version"] = version result.Version = version
} else {
continue
} }
statuses[name] = result statuses[name] = result
@ -615,7 +634,7 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error {
} }
if !skipped { if !skipped {
log.Infof("version %s saved to %s.env", version, a.Domain) log.Debugf("version %s saved to %s.env", version, a.Domain)
} else { } else {
log.Debugf("skipping version %s write as already exists in %s.env", version, a.Domain) log.Debugf("skipping version %s write as already exists in %s.env", version, a.Domain)
} }

View File

@ -2,101 +2,123 @@ package autocomplete
import ( import (
"fmt" "fmt"
"sort"
"coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/log" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli" "github.com/spf13/cobra"
) )
// AppNameComplete copletes app names. // AppNameComplete copletes app names.
func AppNameComplete(c *cli.Context) { func AppNameComplete() ([]string, cobra.ShellCompDirective) {
appNames, err := app.GetAppNames() appFiles, err := app.LoadAppFiles("")
if err != nil { if err != nil {
log.Warn(err) err := fmt.Sprintf("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError
} }
if c.NArg() > 0 { var appNames []string
return for appName := range appFiles {
appNames = append(appNames, appName)
} }
for _, a := range appNames { return appNames, cobra.ShellCompDirectiveDefault
fmt.Println(a)
}
} }
func ServiceNameComplete(appName string) { func ServiceNameComplete(appName string) ([]string, cobra.ShellCompDirective) {
serviceNames, err := app.GetAppServiceNames(appName) serviceNames, err := app.GetAppServiceNames(appName)
if err != nil { if err != nil {
return err := fmt.Sprintf("autocomplete failed: %s", err)
} return []string{err}, cobra.ShellCompDirectiveError
for _, s := range serviceNames {
fmt.Println(s)
} }
return serviceNames, cobra.ShellCompDirectiveDefault
} }
// RecipeNameComplete completes recipe names. // RecipeNameComplete completes recipe names.
func RecipeNameComplete(c *cli.Context) { func RecipeNameComplete() ([]string, cobra.ShellCompDirective) {
catl, err := recipe.ReadRecipeCatalogue(false) catl, err := recipe.ReadRecipeCatalogue(false)
if err != nil { if err != nil {
log.Warn(err) err := fmt.Sprintf("autocomplete failed: %s", err)
} return []string{err}, cobra.ShellCompDirectiveError
if c.NArg() > 0 {
return
} }
var recipeNames []string
for name := range catl { for name := range catl {
fmt.Println(name) recipeNames = append(recipeNames, name)
} }
return recipeNames, cobra.ShellCompDirectiveDefault
} }
// RecipeVersionComplete completes versions for the recipe. // RecipeVersionComplete completes versions for the recipe.
func RecipeVersionComplete(recipeName string) { func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirective) {
catl, err := recipe.ReadRecipeCatalogue(false) catl, err := recipe.ReadRecipeCatalogue(false)
if err != nil { if err != nil {
log.Warn(err) err := fmt.Sprintf("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError
} }
var recipeVersions []string
for _, v := range catl[recipeName].Versions { for _, v := range catl[recipeName].Versions {
for v2 := range v { for v2 := range v {
fmt.Println(v2) recipeVersions = append(recipeVersions, v2)
} }
} }
return recipeVersions, cobra.ShellCompDirectiveDefault
} }
// ServerNameComplete completes server names. // ServerNameComplete completes server names.
func ServerNameComplete(c *cli.Context) { func ServerNameComplete() ([]string, cobra.ShellCompDirective) {
files, err := app.LoadAppFiles("") files, err := app.LoadAppFiles("")
if err != nil { if err != nil {
log.Fatal(err) err := fmt.Sprintf("autocomplete failed: %s", err)
} return []string{err}, cobra.ShellCompDirectiveError
if c.NArg() > 0 {
return
} }
var serverNames []string
for _, appFile := range files { for _, appFile := range files {
fmt.Println(appFile.Server) serverNames = append(serverNames, appFile.Server)
} }
return serverNames, cobra.ShellCompDirectiveDefault
} }
// SubcommandComplete completes sub-commands. // CommandNameComplete completes recipe commands.
func SubcommandComplete(c *cli.Context) { func CommandNameComplete(appName string) ([]string, cobra.ShellCompDirective) {
if c.NArg() > 0 { app, err := app.Get(appName)
return if err != nil {
err := fmt.Sprintf("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError
} }
subcmds := []string{ cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath)
"app", if err != nil {
"autocomplete", err := fmt.Sprintf("autocomplete failed: %s", err)
"catalogue", return []string{err}, cobra.ShellCompDirectiveError
"recipe",
"server",
"upgrade",
} }
for _, cmd := range subcmds { sort.Strings(cmdNames)
fmt.Println(cmd)
} return cmdNames, cobra.ShellCompDirectiveDefault
}
// SecretsComplete completes recipe secrets.
func SecretComplete(recipeName string) ([]string, cobra.ShellCompDirective) {
r := recipe.Get(recipeName)
config, err := r.GetComposeConfig(nil)
if err != nil {
err := fmt.Sprintf("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError
}
var secretNames []string
for name := range config.Secrets {
secretNames = append(secretNames, name)
}
return secretNames, cobra.ShellCompDirectiveDefault
} }

View File

@ -107,5 +107,13 @@ var (
REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud" REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json" CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json"
SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git" SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git"
CHAOS_DEFAULT = "false"
// NOTE(d1): please note, this was done purely out of laziness on our part
// AFAICR. it's easy to punt the value into the label because that is what is
// expects. it's not particularly useful in terms of UI/UX but hey, nobody
// complained yet!
CHAOS_DEFAULT = "false"
NO_DOMAIN_DEFAULT = "N/A"
NO_VERSION_DEFAULT = "N/A"
) )

View File

@ -9,12 +9,11 @@ import (
func EnsureIPv4(domainName string) (string, error) { func EnsureIPv4(domainName string) (string, error) {
ipv4, err := net.ResolveIPAddr("ip4", domainName) ipv4, err := net.ResolveIPAddr("ip4", domainName)
if err != nil { if err != nil {
return "", fmt.Errorf("unable to resolve ipv4 address for %s, %s", domainName, err) return "", fmt.Errorf("%s: unable to resolve IPv4 address: %s", domainName, err)
} }
// NOTE(d1): e.g. when there is only an ipv6 record available
if ipv4 == nil { if ipv4 == nil {
return "", fmt.Errorf("unable to resolve ipv4 address for %s", domainName) return "", fmt.Errorf("%s: no IPv4 available", domainName)
} }
return ipv4.String(), nil return ipv4.String(), nil

View File

@ -15,10 +15,10 @@ func TestEnsureDomainsResolveSameIPv4(t *testing.T) {
}{ }{
// NOTE(d1): DNS records get checked, so use something that is maintained // NOTE(d1): DNS records get checked, so use something that is maintained
// within the federation. if you're here because of a failing test, try // within the federation. if you're here because of a failing test, try
// `dig +short <domain>` to ensure stuff matches first! If flakyness // `dig +short <app>` to ensure stuff matches first! If flakyness
// becomes an issue we can look into mocking // becomes an issue we can look into mocking
{"docs.coopcloud.tech", "swarm-0.coopcloud.tech", true},
{"docs.coopcloud.tech", "coopcloud.tech", true}, {"docs.coopcloud.tech", "coopcloud.tech", true},
{"docs.coopcloud.tech", "swarm.autonomic.zone", true},
// NOTE(d1): special case handling for "--local" // NOTE(d1): special case handling for "--local"
{"", "default", true}, {"", "default", true},
@ -43,7 +43,7 @@ func TestEnsureDomainsResolveSameIPv4(t *testing.T) {
func TestEnsureIpv4(t *testing.T) { func TestEnsureIpv4(t *testing.T) {
// NOTE(d1): DNS records get checked, so use something that is maintained // NOTE(d1): DNS records get checked, so use something that is maintained
// within the federation. if you're here because of a failing test, try `dig // within the federation. if you're here because of a failing test, try `dig
// +short <domain>` to ensure stuff matches first! If flakyness becomes an // +short <app>` to ensure stuff matches first! If flakyness becomes an
// issue we can look into mocking // issue we can look into mocking
domainName := "collabora.ostrom.collective.tools" domainName := "collabora.ostrom.collective.tools"
serverName := "ostrom.collective.tools" serverName := "ostrom.collective.tools"

View File

@ -43,33 +43,122 @@ func HumanDuration(timestamp int64) string {
// CreateTable prepares a table layout for output. // CreateTable prepares a table layout for output.
func CreateTable() (*table.Table, error) { func CreateTable() (*table.Table, error) {
var (
renderer = lipgloss.NewRenderer(os.Stdout)
headerStyle = renderer.NewStyle().Bold(true).Align(lipgloss.Center)
cellStyle = renderer.NewStyle().Padding(0, 1)
borderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
)
table := table.New(). table := table.New().
Border(lipgloss.ThickBorder()). Border(lipgloss.ThickBorder()).
BorderStyle( BorderStyle(borderStyle).
lipgloss.NewStyle(). StyleFunc(func(row, col int) lipgloss.Style {
Foreground(lipgloss.Color("63")), var style lipgloss.Style
)
switch {
case row == table.HeaderRow:
return headerStyle
default:
style = cellStyle
}
return style
})
return table, nil
}
func PrintTable(t *table.Table) error {
if isAbraCI, ok := os.LookupEnv("ABRA_CI"); ok && isAbraCI == "1" { if isAbraCI, ok := os.LookupEnv("ABRA_CI"); ok && isAbraCI == "1" {
// NOTE(d1): no width limits for CI testing since we test against outputs // NOTE(d1): no width limits for CI testing since we test against outputs
log.Debug("detected ABRA_CI=1") log.Debug("detected ABRA_CI=1")
return table, nil fmt.Println(t)
return nil
} }
tWidth, _ := lipgloss.Size(t.String())
width, _, err := term.GetSize(0) width, _, err := term.GetSize(0)
if err != nil { if err != nil {
return nil, err return err
} }
if width-10 < 79 { if tWidth > width {
// NOTE(d1): maintain standard minimum width t.Width(width - 10)
table.Width(79)
} else {
// NOTE(d1): tests show that this produces stable border drawing
table.Width(width - 10)
} }
return table, nil fmt.Println(t)
return nil
}
// horizontal is a JoinHorizontal helper function.
func horizontal(left, mid, right string) string {
return lipgloss.JoinHorizontal(lipgloss.Right, left, mid, right)
}
func CreateOverview(header string, rows [][]string) string {
var borderStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.ThickBorder()).
Padding(0, 1, 0, 1).
MaxWidth(79).
BorderForeground(lipgloss.Color("63"))
var headerStyle = lipgloss.NewStyle().
Underline(true).
Bold(true).
PaddingBottom(1)
var leftStyle = lipgloss.NewStyle().
Bold(true)
var rightStyle = lipgloss.NewStyle()
var longest int
for _, row := range rows {
if len(row[0]) > longest {
longest = len(row[0])
}
}
var renderedRows []string
for _, row := range rows {
if len(row) > 2 {
panic("CreateOverview: only accepts rows of len == 2")
}
lenOffset := 4
if len(row[0]) < longest {
lenOffset += longest - len(row[0])
}
offset := ""
for range lenOffset {
offset = offset + " "
}
renderedRows = append(
renderedRows,
horizontal(leftStyle.Render(row[0]), offset, rightStyle.Render(row[1])),
)
}
body := strings.Builder{}
body.WriteString(
borderStyle.Render(
lipgloss.JoinVertical(
lipgloss.Center,
headerStyle.Render(header),
lipgloss.JoinVertical(
lipgloss.Left,
renderedRows...,
),
),
),
)
return body.String()
} }
// ToJSON converts a lipgloss.Table to JSON representation. It's not a robust // ToJSON converts a lipgloss.Table to JSON representation. It's not a robust

View File

@ -5,14 +5,19 @@ import (
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/object"
) )
// Init inits a new repo and commits all the stuff if you want // Init inits a new repo and commits all the stuff if you want
func Init(repoPath string, commit bool, gitName, gitEmail string) error { func Init(repoPath string, commit bool, gitName, gitEmail string) error {
if _, err := git.PlainInit(repoPath, false); err != nil { repo, err := git.PlainInit(repoPath, false)
if err != nil {
return fmt.Errorf("git init: %s", err) return fmt.Errorf("git init: %s", err)
} }
if err = SwitchToMain(repo); err != nil {
return fmt.Errorf("git branch rename: %s", err)
}
log.Debugf("initialised new git repo in %s", repoPath) log.Debugf("initialised new git repo in %s", repoPath)
if commit { if commit {
@ -42,3 +47,21 @@ func Init(repoPath string, commit bool, gitName, gitEmail string) error {
return nil return nil
} }
// SwitchToMain sets the default branch to "main".
func SwitchToMain(repo *git.Repository) error {
ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.ReferenceName("refs/heads/main"))
if err := repo.Storer.SetReference(ref); err != nil {
return fmt.Errorf("set reference: %s", err)
}
cfg, err := repo.Config()
if err != nil {
return fmt.Errorf("repo config: %s", err)
}
cfg.Init.DefaultBranch = "main"
if err := repo.SetConfig(cfg); err != nil {
return fmt.Errorf("repo set config: %s", err)
}
log.Debug("set 'main' as the default branch.")
return nil
}

35
pkg/git/init_test.go Normal file
View File

@ -0,0 +1,35 @@
package git
import (
"testing"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/storage/memory"
)
func TestSwitchToMain(t *testing.T) {
repo, err := git.Init(memory.NewStorage(), nil)
if err != nil {
t.Fatalf("failed to create in-memory repository: %v", err)
}
if err = SwitchToMain(repo); err != nil {
t.Fatalf("SwitchToMain failed: %v", err)
}
ref, err := repo.Reference(plumbing.HEAD, false)
if err != nil {
t.Fatalf("failed to get HEAD reference: %v", err)
}
if ref.Target().String() != "refs/heads/main" {
t.Errorf("expected HEAD to point to 'refs/heads/main', got %s", ref.Target().String())
}
cfg, err := repo.Config()
if err != nil {
t.Fatalf("failed to get repository config: %v", err)
}
if cfg.Init.DefaultBranch != "main" {
t.Errorf("expected default branch to be 'main', got %s", cfg.Init.DefaultBranch)
}
}

View File

@ -137,6 +137,13 @@ var LintRules = map[string][]LintRule{
HowToResolve: "name a servce 'app'", HowToResolve: "name a servce 'app'",
Function: LintAppService, Function: LintAppService,
}, },
{
Ref: "R015",
Level: "error",
Description: "deploy labels stanza present",
HowToResolve: "include \"deploy: labels: ...\" stanza",
Function: LintDeployLabelsPresent,
},
{ {
Ref: "R010", Ref: "R010",
Level: "error", Level: "error",
@ -269,6 +276,21 @@ func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) {
return false, nil return false, nil
} }
func LintDeployLabelsPresent(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return false, err
}
for _, service := range config.Services {
if service.Name == "app" && service.Deploy.Labels != nil {
return true, nil
}
}
return false, nil
}
func LintHealthchecks(recipe recipe.Recipe) (bool, error) { func LintHealthchecks(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil) config, err := recipe.GetComposeConfig(nil)
if err != nil { if err != nil {

View File

@ -6,6 +6,7 @@ import (
"path" "path"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter"
) )
func (r Recipe) SampleEnv() (map[string]string, error) { func (r Recipe) SampleEnv() (map[string]string, error) {
@ -29,7 +30,10 @@ func (r Recipe) GetReleaseNotes(version string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
withTitle := fmt.Sprintf("%s release notes:\n%s", version, string(releaseNotes))
title := formatter.BoldStyle.Render(fmt.Sprintf("%s release notes:", version))
withTitle := fmt.Sprintf("%s\n%s\n", title, releaseNotes)
return withTitle, nil return withTitle, nil
} }

View File

@ -247,7 +247,7 @@ func (r Recipe) ChaosVersion() (string, error) {
} }
if !isClean { if !isClean {
version = fmt.Sprintf("%s + unstaged changes", version) version = fmt.Sprintf("%s+U", version)
} }
return version, nil return version, nil

0
scripts/autocomplete/bash Executable file → Normal file
View File

View File

@ -1,188 +0,0 @@
#!/usr/bin/env bash
setup_file() {
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file() {
_rm_app
_rm_server
_reset_recipe
}
setup() {
load "$PWD/tests/integration/helpers/common"
_common_setup
}
teardown(){
_undeploy_app
}
@test "retrieve recipe if missing" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE"
assert_success
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
run $ABRA app backup "$TEST_APP_DOMAIN"
assert_failure
assert_output --partial 'no containers matching'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
}
@test "bail if unstaged changes and no --chaos" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_success
assert_output --partial 'foo'
run $ABRA app backup "$TEST_APP_DOMAIN" app
assert_failure
assert_output --partial 'locally unstaged changes'
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
_checkout_recipe
}
# bats test_tags=slow
@test "do not bail if unstaged changes and --chaos" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
run bash -c 'echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"'
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_success
assert_output --partial 'foo'
run $ABRA app deploy "$TEST_APP_DOMAIN" --chaos --no-input
assert_success
run $ABRA app backup "$TEST_APP_DOMAIN" app --chaos
assert_success
assert_output --partial 'running backup for the app service'
_undeploy_app
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@test "ensure recipe up to date if no --offline" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3'
run $ABRA app backup "$TEST_APP_DOMAIN" --debug
assert_failure
assert_output --partial 'no containers matching'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
refute_output --partial 'behind 3'
_reset_recipe
}
@test "ensure recipe not up to date if --offline" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
latestCommit="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)"
refute [ -z "$latestCommit" ];
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3'
run $ABRA app backup "$TEST_APP_DOMAIN" --debug --offline
assert_failure
assert_output --partial 'no containers matching'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial 'behind 3'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$latestCommit"
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
refute_output --partial 'behind 3'
_reset_recipe
}
@test "detect backup labels" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
run $ABRA app backup "$TEST_APP_DOMAIN" --debug
assert_failure
assert_output --partial 'no containers matching'
assert_output --partial 'detected backup paths'
assert_output --partial 'detected pre-hook command'
assert_output --partial 'detected post-hook command'
}
@test "error if backups not enabled" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
run sed -i '/backupbot.backup=true/d' "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"
assert_success
run $ABRA app backup "$TEST_APP_DOMAIN" app --chaos
assert_failure
assert_output --partial 'no backup config for app'
_checkout_recipe
}
@test "error if backup paths not configured" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
run sed -i '/backupbot.backup.path=.*/d' "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"
assert_success
run $ABRA app backup "$TEST_APP_DOMAIN" app --chaos
assert_failure
assert_output --partial 'backup paths are empty for app?'
_checkout_recipe
}
# bats test_tags=slow
@test "backup single service" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
_deploy_app
run $ABRA app backup "$TEST_APP_DOMAIN" app
assert_success
assert_output --partial 'running backup for the app service'
sanitisedDomainName="${TEST_APP_DOMAIN//./_}"
assert_output --partial "_$sanitisedDomainName_app"
assert_exists "$ABRA_DIR/backups"
assert bash -c "ls $ABRA_DIR/backups | grep -q $1_$sanitisedDomainName_app"
_undeploy_app
}

View File

@ -26,11 +26,9 @@ teardown(){
@test "validate app argument" { @test "validate app argument" {
run $ABRA app check run $ABRA app check
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app check DOESNTEXIST run $ABRA app check DOESNTEXIST
assert_failure assert_failure
assert_output --partial 'cannot find app'
} }
@test "retrieve recipe if missing" { @test "retrieve recipe if missing" {

View File

@ -24,32 +24,12 @@ teardown(){
_undeploy_app _undeploy_app
} }
# bats test_tags=slow
@test "autocomplete" {
run $ABRA app cmd --generate-bash-completion
assert_success
assert_output "$TEST_APP_DOMAIN"
run $ABRA app cmd "$TEST_APP_DOMAIN" --generate-bash-completion
assert_success
assert_output "app"
run $ABRA app cmd "$TEST_APP_DOMAIN" app --generate-bash-completion
assert_success
assert_output "test_cmd
test_cmd_arg
test_cmd_args
test_cmd_export"
}
@test "validate app argument" { @test "validate app argument" {
run $ABRA app cmd run $ABRA app cmd
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app cmd DOESNTEXIST run $ABRA app cmd DOESNTEXIST
assert_failure assert_failure
assert_output --partial 'cannot find app'
} }
@test "retrieve recipe if missing" { @test "retrieve recipe if missing" {
@ -123,13 +103,11 @@ test_cmd_export"
@test "error if missing arguments without passing --local" { @test "error if missing arguments without passing --local" {
run $ABRA app cmd "$TEST_APP_DOMAIN" run $ABRA app cmd "$TEST_APP_DOMAIN"
assert_failure assert_failure
assert_output --partial 'missing arguments'
} }
@test "error if missing arguments when passing --local" { @test "error if missing arguments when passing --local" {
run $ABRA app cmd --local "$TEST_APP_DOMAIN" run $ABRA app cmd --local "$TEST_APP_DOMAIN"
assert_failure assert_failure
assert_output --partial 'missing arguments'
} }
@test "cannot use --local and --user at same time" { @test "cannot use --local and --user at same time" {
@ -162,17 +140,13 @@ test_cmd_export"
} }
@test "run command with single arg" { @test "run command with single arg" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/581"
run $ABRA app cmd --local "$TEST_APP_DOMAIN" test_cmd_arg -- bing run $ABRA app cmd --local "$TEST_APP_DOMAIN" test_cmd_arg -- bing
assert_success assert_success
assert_output --partial 'bing' assert_output --partial 'bing'
} }
@test "run command with several args" { @test "run command with several args" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/581" run $ABRA app cmd --local "$TEST_APP_DOMAIN" test_cmd_args bong bang
run $ABRA app cmd --local "$TEST_APP_DOMAIN" test_cmd_args -- bong bang
assert_success assert_success
assert_output --partial 'bong bang' assert_output --partial 'bong bang'
} }

View File

@ -18,9 +18,7 @@ setup(){
@test "validate app argument" { @test "validate app argument" {
run $ABRA app config run $ABRA app config
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app config DOESNTEXIST run $ABRA app config DOESNTEXIST
assert_failure assert_failure
assert_output --partial 'cannot find app'
} }

View File

@ -29,21 +29,17 @@ teardown(){
@test "validate app argument" { @test "validate app argument" {
run $ABRA app cp run $ABRA app cp
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app cp DOESNTEXIST run $ABRA app cp DOESNTEXIST
assert_failure assert_failure
assert_output --partial 'cannot find app'
} }
@test "error if missing src/dest arguments" { @test "error if missing src/dest arguments" {
run $ABRA app cp "$TEST_APP_DOMAIN" run $ABRA app cp "$TEST_APP_DOMAIN"
assert_failure assert_failure
assert_output --partial 'missing <src> argument'
run $ABRA app cp "$TEST_APP_DOMAIN" myfile.txt run $ABRA app cp "$TEST_APP_DOMAIN" myfile.txt
assert_failure assert_failure
assert_output --partial 'missing <dest> argument'
} }
@test "either src/dest has correct syntax" { @test "either src/dest has correct syntax" {
@ -159,7 +155,7 @@ teardown(){
# bats test_tags=slow # bats test_tags=slow
@test "copy container file to local directory" { @test "copy container file to local directory" {
run $ABRA app run "$TEST_APP_DOMAIN" app bash -c "echo foo > /etc/myfile.txt" run $ABRA app run "$TEST_APP_DOMAIN" app -- bash -c "echo foo > /etc/myfile.txt"
assert_success assert_success
run $ABRA app cp "$TEST_APP_DOMAIN" app:/etc/myfile.txt "$BATS_TMPDIR" run $ABRA app cp "$TEST_APP_DOMAIN" app:/etc/myfile.txt "$BATS_TMPDIR"
@ -170,7 +166,7 @@ teardown(){
# bats test_tags=slow # bats test_tags=slow
@test "copy container file to local file" { @test "copy container file to local file" {
run $ABRA app run "$TEST_APP_DOMAIN" app bash -c "echo foo > /etc/myfile.txt" run $ABRA app run "$TEST_APP_DOMAIN" app -- bash -c "echo foo > /etc/myfile.txt"
assert_success assert_success
run $ABRA app cp "$TEST_APP_DOMAIN" app:/etc/myfile.txt "$BATS_TMPDIR/myfile.txt" run $ABRA app cp "$TEST_APP_DOMAIN" app:/etc/myfile.txt "$BATS_TMPDIR/myfile.txt"
@ -181,7 +177,7 @@ teardown(){
# bats test_tags=slow # bats test_tags=slow
@test "copy container file to local file and rename" { @test "copy container file to local file and rename" {
run $ABRA app run "$TEST_APP_DOMAIN" app bash -c "echo foo > /etc/myfile.txt" run $ABRA app run "$TEST_APP_DOMAIN" app -- bash -c "echo foo > /etc/myfile.txt"
assert_success assert_success
run $ABRA app cp "$TEST_APP_DOMAIN" app:/etc/myfile.txt "$BATS_TMPDIR/myfile2.txt" run $ABRA app cp "$TEST_APP_DOMAIN" app:/etc/myfile.txt "$BATS_TMPDIR/myfile2.txt"
@ -192,10 +188,10 @@ teardown(){
# bats test_tags=slow # bats test_tags=slow
@test "copy container directory to local directory" { @test "copy container directory to local directory" {
run $ABRA app run "$TEST_APP_DOMAIN" app bash -c "echo foo > /etc/myfile.txt" run $ABRA app run "$TEST_APP_DOMAIN" app -- bash -c "echo foo > /etc/myfile.txt"
assert_success assert_success
run $ABRA app run "$TEST_APP_DOMAIN" app bash -c "echo bar > /etc/myfile2.txt" run $ABRA app run "$TEST_APP_DOMAIN" app -- bash -c "echo bar > /etc/myfile2.txt"
assert_success assert_success
mkdir "$BATS_TMPDIR/mydir" mkdir "$BATS_TMPDIR/mydir"
@ -209,10 +205,10 @@ teardown(){
# bats test_tags=slow # bats test_tags=slow
@test "copy container files to local directory" { @test "copy container files to local directory" {
run $ABRA app run "$TEST_APP_DOMAIN" app bash -c "echo foo > /etc/myfile.txt" run $ABRA app run "$TEST_APP_DOMAIN" app -- bash -c "echo foo > /etc/myfile.txt"
assert_success assert_success
run $ABRA app run "$TEST_APP_DOMAIN" app bash -c "echo bar > /etc/myfile2.txt" run $ABRA app run "$TEST_APP_DOMAIN" app -- bash -c "echo bar > /etc/myfile2.txt"
assert_success assert_success
mkdir "$BATS_TMPDIR/mydir" mkdir "$BATS_TMPDIR/mydir"

View File

@ -32,11 +32,9 @@ teardown(){
@test "validate app argument" { @test "validate app argument" {
run $ABRA app deploy run $ABRA app deploy
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app deploy DOESNTEXIST run $ABRA app deploy DOESNTEXIST
assert_failure assert_failure
assert_output --partial 'cannot find app'
} }
@test "bail if unstaged changes and no --chaos" { @test "bail if unstaged changes and no --chaos" {
@ -67,7 +65,7 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
--chaos --no-input --no-converge-checks --chaos --no-input --no-converge-checks
assert_success assert_success
assert_output --partial 'chaos' assert_output --partial 'NEW CHAOS'
} }
# bats test_tags=slow # bats test_tags=slow
@ -113,8 +111,6 @@ teardown(){
--no-input --no-converge-checks --offline --no-input --no-converge-checks --offline
assert_success assert_success
assert_output --partial "$latestCommit" assert_output --partial "$latestCommit"
assert_output --partial 'using latest commit'
refute_output --partial 'chaos'
} }
# bats test_tags=slow # bats test_tags=slow
@ -132,7 +128,7 @@ teardown(){
--no-input --no-converge-checks --chaos --no-input --no-converge-checks --chaos
assert_success assert_success
assert_output --partial "${wantHash:0:8}" assert_output --partial "${wantHash:0:8}"
assert_output --partial 'chaos' assert_output --partial 'NEW CHAOS'
} }
# bats test_tags=slow # bats test_tags=slow
@ -174,12 +170,10 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --force --no-input --no-converge-checks --force
assert_success assert_success
assert_output --partial 'already deployed'
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos --no-input --no-converge-checks --chaos
assert_success assert_success
assert_output --partial 'already deployed'
} }
# bats test_tags=slow # bats test_tags=slow
@ -230,7 +224,6 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success assert_success
assert_output --partial 'no DOMAIN=... configured for app'
run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
assert_success assert_success
@ -264,7 +257,6 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --no-domain-checks --no-input --no-converge-checks --no-domain-checks
assert_success assert_success
assert_output --partial 'skipping domain checks as requested'
} }
@test "error if specific version does not exist" { @test "error if specific version does not exist" {

View File

@ -60,11 +60,6 @@ teardown(){
assert_output --partial "$TEST_SERVER" assert_output --partial "$TEST_SERVER"
assert_output --partial "foo.com" assert_output --partial "foo.com"
run $ABRA app ls --server foo.com
assert_success
refute_output --partial "SERVER: $TEST_SERVER"
assert_output --partial "SERVER: foo.com"
run rm -rf "$ABRA_DIR/servers/foo.com" run rm -rf "$ABRA_DIR/servers/foo.com"
assert_success assert_success
assert_not_exists "$ABRA_DIR/servers/foo.com" assert_not_exists "$ABRA_DIR/servers/foo.com"
@ -94,33 +89,6 @@ teardown(){
assert_output --partial "foo-recipe" assert_output --partial "foo-recipe"
} }
@test "server stats are correct" {
run $ABRA app ls
assert_success
assert_output --partial "SERVER: $TEST_SERVER"
assert_output --partial "TOTAL APPS: 1"
run mkdir -p "$ABRA_DIR/servers/foo.com"
assert_success
assert_exists "$ABRA_DIR/servers/foo.com"
run cp \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" \
"$ABRA_DIR/servers/foo.com/app.foo.com.env"
assert_exists "$ABRA_DIR/servers/foo.com/app.foo.com.env"
run $ABRA app ls
assert_success
assert_output --partial "$TEST_SERVER"
assert_output --partial "foo.com"
assert_output --partial "TOTAL SERVERS: 2"
assert_output --partial "TOTAL APPS: 2"
run rm -rf "$ABRA_DIR/servers/foo.com"
assert_success
assert_not_exists "$ABRA_DIR/servers/foo.com"
}
@test "output is machine readable" { @test "output is machine readable" {
run $ABRA app ls --machine run $ABRA app ls --machine

View File

@ -20,11 +20,9 @@ setup(){
@test "validate app argument" { @test "validate app argument" {
run $ABRA app logs run $ABRA app logs
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app logs DOESNTEXIST run $ABRA app logs DOESNTEXIST
assert_failure assert_failure
assert_output --partial 'cannot find app'
} }
@test "error if not deployed" { @test "error if not deployed" {

View File

@ -22,12 +22,6 @@ teardown(){
_reset_recipe _reset_recipe
} }
@test "autocomplete" {
run $ABRA app new --generate-bash-completion
assert_success
assert_output --partial "traefik"
}
@test "create new app" { @test "create new app" {
run $ABRA app new "$TEST_RECIPE" \ run $ABRA app new "$TEST_RECIPE" \
--no-input \ --no-input \
@ -179,8 +173,6 @@ teardown(){
--domain "$TEST_APP_DOMAIN" \ --domain "$TEST_APP_DOMAIN" \
--secrets --secrets
assert_success assert_success
assert_output --partial 'generated secrets'
assert_output --partial 'test_pass_one'
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
run $ABRA app secret ls "$TEST_APP_DOMAIN" run $ABRA app secret ls "$TEST_APP_DOMAIN"

View File

@ -24,11 +24,9 @@ teardown(){
@test "validate app argument" { @test "validate app argument" {
run $ABRA app ps run $ABRA app ps
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app ps DOESNTEXIST run $ABRA app ps DOESNTEXIST
assert_failure assert_failure
assert_output --partial 'cannot find app'
} }
# bats test_tags=slow # bats test_tags=slow

View File

@ -28,11 +28,9 @@ teardown(){
@test "validate app argument" { @test "validate app argument" {
run $ABRA app deploy run $ABRA app deploy
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app deploy DOESNTEXIST run $ABRA app deploy DOESNTEXIST
assert_failure assert_failure
assert_output --partial 'cannot find app'
} }
@test "do not show ALERTA warning if --force / --no-input" { @test "do not show ALERTA warning if --force / --no-input" {

View File

@ -24,17 +24,14 @@ teardown(){
@test "validate app argument" { @test "validate app argument" {
run $ABRA app restart run $ABRA app restart
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app restart DOESNTEXIST run $ABRA app restart DOESNTEXIST
assert_failure assert_failure
assert_output --partial 'cannot find app'
} }
@test "error if service missing" { @test "error if service missing" {
run $ABRA app restart "$TEST_APP_DOMAIN" run $ABRA app restart "$TEST_APP_DOMAIN"
assert_failure assert_failure
assert_output --partial 'missing <service>'
} }
@test "error if not deployed" { @test "error if not deployed" {

View File

@ -1,272 +0,0 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
teardown(){
_undeploy_app
}
@test "validate app argument" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
run $ABRA app restore
assert_failure
assert_output --partial 'no app provided'
run $ABRA app restore DOESNTEXIST
assert_failure
assert_output --partial 'cannot find app'
}
@test "retrieve recipe if missing" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE"
assert_success
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
run $ABRA app restore "$TEST_APP_DOMAIN" app DOESNTEXIST
assert_failure
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
}
@test "bail if unstaged changes and no --chaos" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_success
assert_output --partial 'foo'
run $ABRA app restore "$TEST_APP_DOMAIN" app DOESNTEXIST
assert_failure
assert_output --partial 'locally unstaged changes'
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
_checkout_recipe
}
# bats test_tags=slow
@test "do not bail if unstaged changes and --chaos" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
run bash -c 'echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"'
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_success
assert_output --partial 'foo'
run touch "$BATS_TMPDIR/foo.txt"
assert_success
assert_exists "$BATS_TMPDIR/foo.txt"
run tar -cvf "$BATS_TMPDIR/foo.tar.gz" "$BATS_TMPDIR/foo.txt"
assert_success
assert_exists "$BATS_TMPDIR/foo.tar.gz"
run $ABRA app deploy "$TEST_APP_DOMAIN" --chaos --no-input
assert_success
run $ABRA app restore "$TEST_APP_DOMAIN" app "$BATS_TMPDIR/foo.tar.gz" \
--debug --chaos
assert_success
assert_output --partial 'restore config detected'
assert_output --partial 'detected pre-hook command'
assert_output --partial 'detected post-hook command'
run $ABRA app run "$TEST_APP_DOMAIN" app ls "$BATS_TMPDIR"
assert_success
assert_output --partial 'foo.txt'
_undeploy_app
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
_checkout_recipe
}
@test "ensure recipe up to date if no --offline" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --regexp 'behind .* 3 commits'
run $ABRA app restore "$TEST_APP_DOMAIN" app
assert_failure
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial "up to date"
}
@test "ensure recipe not up to date if --offline" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
latestCommit="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)"
refute [ -z "$latestCommit" ];
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --regexp 'behind .* 3 commits'
run $ABRA app restore "$TEST_APP_DOMAIN" app --offline
assert_failure
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --regexp 'behind .* 3 commits'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$latestCommit"
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_output --partial "HEAD detached at $latestCommit"
}
@test "error if missing service" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
run $ABRA app restore "$TEST_APP_DOMAIN"
assert_failure
assert_output --partial 'missing <service>'
run $ABRA app restore "$TEST_APP_DOMAIN" app
assert_failure
assert_output --partial 'missing <file>'
}
@test "error if file doesn't exist" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
run $ABRA app restore "$TEST_APP_DOMAIN" app DOESNTEXIST
assert_failure
assert_output --partial "doesn't exist"
}
# bats test_tags=slow
@test "detect labels if restore enabled" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
run touch "$BATS_TMPDIR/foo.txt"
assert_success
assert_exists "$BATS_TMPDIR/foo.txt"
run tar -cvf "$BATS_TMPDIR/foo.tar.gz" "$BATS_TMPDIR/foo.txt"
assert_success
assert_exists "$BATS_TMPDIR/foo.tar.gz"
_deploy_app
run $ABRA app restore "$TEST_APP_DOMAIN" app "$BATS_TMPDIR/foo.tar.gz" --debug
assert_success
assert_output --partial 'restore config detected'
assert_output --partial 'detected pre-hook command'
assert_output --partial 'detected post-hook command'
_undeploy_app
}
# bats test_tags=slow
@test "no error if restore not enabled" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
run sed -i '/backupbot.restore=.*/d' "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"
assert_success
run touch "$BATS_TMPDIR/foo.txt"
assert_success
assert_exists "$BATS_TMPDIR/foo.txt"
run tar -cvf "$BATS_TMPDIR/foo.tar.gz" "$BATS_TMPDIR/foo.txt"
assert_success
assert_exists "$BATS_TMPDIR/foo.tar.gz"
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --chaos
assert_success
run $ABRA app restore "$TEST_APP_DOMAIN" app "$BATS_TMPDIR/foo.tar.gz" \
--debug --chaos
assert_success
refute_output --partial 'restore config detected'
refute_output --partial 'detected pre-hook command'
refute_output --partial 'detected post-hook command'
_undeploy_app
_checkout_recipe
}
# bats test_tags=slow
@test "error if service doesn't exist" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
run touch "$BATS_TMPDIR/foo.txt"
assert_success
assert_exists "$BATS_TMPDIR/foo.txt"
run tar -cvf "$BATS_TMPDIR/foo.tar.gz" "$BATS_TMPDIR/foo.txt"
assert_success
assert_exists "$BATS_TMPDIR/foo.tar.gz"
_deploy_app
run $ABRA app restore "$TEST_APP_DOMAIN" DOESNTEXIST "$BATS_TMPDIR/foo.tar.gz" --debug
assert_failure
assert_output --partial 'no containers matching'
_undeploy_app
}
# bats test_tags=slow
@test "restore backup" {
skip "https://git.coopcloud.tech/coop-cloud/organising/issues/583"
run touch "$BATS_TMPDIR/foo.txt"
assert_success
assert_exists "$BATS_TMPDIR/foo.txt"
run tar -cvf "$BATS_TMPDIR/foo.tar.gz" "$BATS_TMPDIR/foo.txt"
assert_success
assert_exists "$BATS_TMPDIR/foo.tar.gz"
_deploy_app
run $ABRA app restore "$TEST_APP_DOMAIN" app "$BATS_TMPDIR/foo.tar.gz" --debug
assert_success
assert_output --partial 'restore config detected'
assert_output --partial 'detected pre-hook command'
assert_output --partial 'detected post-hook command'
run $ABRA app run "$TEST_APP_DOMAIN" app ls "$BATS_TMPDIR"
assert_success
assert_output --partial 'foo.txt'
_undeploy_app
}

View File

@ -26,11 +26,9 @@ teardown(){
@test "validate app argument" { @test "validate app argument" {
run $ABRA app rollback run $ABRA app rollback
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app rollback DOESNTEXIST run $ABRA app rollback DOESNTEXIST
assert_failure assert_failure
assert_output --partial 'cannot find app'
} }
@test "retrieve recipe if missing" { @test "retrieve recipe if missing" {

View File

@ -24,21 +24,17 @@ teardown(){
@test "validate app argument" { @test "validate app argument" {
run $ABRA app run run $ABRA app run
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app run DOESNTEXIST run $ABRA app run DOESNTEXIST
assert_failure assert_failure
assert_output --partial 'cannot find app'
} }
@test "error if missing service" { @test "error if missing service" {
run $ABRA app run "$TEST_APP_DOMAIN" run $ABRA app run "$TEST_APP_DOMAIN"
assert_failure assert_failure
assert_output --partial 'no <service> provided'
run $ABRA app run "$TEST_APP_DOMAIN" app run $ABRA app run "$TEST_APP_DOMAIN" app
assert_failure assert_failure
assert_output --partial 'no <args> provided'
} }
# bats test_tags=slow # bats test_tags=slow
@ -54,7 +50,7 @@ teardown(){
@test "run command" { @test "run command" {
_deploy_app _deploy_app
run $ABRA app run "$TEST_APP_DOMAIN" app ls / run $ABRA app run "$TEST_APP_DOMAIN" app -- ls /
assert_success assert_success
assert_output --partial 'root' assert_output --partial 'root'
} }

View File

@ -35,15 +35,12 @@ teardown(){
@test "generate: validate arguments" { @test "generate: validate arguments" {
run $ABRA app secret generate run $ABRA app secret generate
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app secret generate DOESNTEXIST run $ABRA app secret generate DOESNTEXIST
assert_failure assert_failure
assert_output --partial 'cannot find app'
run $ABRA app secret generate "$TEST_APP_DOMAIN" run $ABRA app secret generate "$TEST_APP_DOMAIN"
assert_failure assert_failure
assert_output --partial 'missing arguments'
run $ABRA app secret generate "$TEST_APP_DOMAIN" testSecret testVersion --all run $ABRA app secret generate "$TEST_APP_DOMAIN" testSecret testVersion --all
assert_failure assert_failure
@ -89,7 +86,6 @@ teardown(){
run $ABRA app secret generate "$TEST_APP_DOMAIN" --all run $ABRA app secret generate "$TEST_APP_DOMAIN" --all
assert_failure assert_failure
assert_output --partial 'missing version' assert_output --partial 'missing version'
} }
@test "generate: use version from app env" { @test "generate: use version from app env" {
@ -180,19 +176,15 @@ teardown(){
@test "insert: validate arguments" { @test "insert: validate arguments" {
run $ABRA app secret insert run $ABRA app secret insert
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app secret insert "$TEST_APP_DOMAIN" run $ABRA app secret insert "$TEST_APP_DOMAIN"
assert_failure assert_failure
assert_output --partial 'missing arguments'
run $ABRA app secret insert "$TEST_APP_DOMAIN" bar run $ABRA app secret insert "$TEST_APP_DOMAIN" bar
assert_failure assert_failure
assert_output --partial 'missing arguments'
run $ABRA app secret insert "$TEST_APP_DOMAIN" bar baz run $ABRA app secret insert "$TEST_APP_DOMAIN" bar baz
assert_failure assert_failure
assert_output --partial 'missing arguments'
} }
@test "insert: create secret" { @test "insert: create secret" {
@ -230,20 +222,16 @@ teardown(){
@test "rm: validate arguments" { @test "rm: validate arguments" {
run $ABRA app secret rm run $ABRA app secret rm
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app secret rm DOESNTEXIST run $ABRA app secret rm DOESNTEXIST
assert_failure assert_failure
assert_output --partial 'cannot find app'
run $ABRA app secret rm "$TEST_APP_DOMAIN" run $ABRA app secret rm "$TEST_APP_DOMAIN"
assert_failure assert_failure
assert_output --partial 'no secret(s) specified'
run $ABRA app secret rm "$TEST_APP_DOMAIN" test_pass_one --all run $ABRA app secret rm "$TEST_APP_DOMAIN" test_pass_one --all
assert_failure assert_failure
assert_output --partial 'cannot use' assert_output --regexp 'cannot use .* together'
assert_output --partial "'--all' together"
} }
@test "rm: single secret no match" { @test "rm: single secret no match" {
@ -290,11 +278,9 @@ teardown(){
@test "ls: validate arguments" { @test "ls: validate arguments" {
run $ABRA app secret ls run $ABRA app secret ls
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app secret ls DOESNTEXIST run $ABRA app secret ls DOESNTEXIST
assert_failure assert_failure
assert_output --partial 'cannot find app'
} }
@test "ls: show secrets" { @test "ls: show secrets" {

View File

@ -25,11 +25,9 @@ teardown(){
@test "validate app argument" { @test "validate app argument" {
run $ABRA app services run $ABRA app services
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app services DOESNTEXIST run $ABRA app services DOESNTEXIST
assert_failure assert_failure
assert_output --partial 'cannot find app'
} }
@test "error if not deployed" { @test "error if not deployed" {
@ -47,5 +45,4 @@ teardown(){
sanitisedDomainName="${TEST_APP_DOMAIN//./_}" sanitisedDomainName="${TEST_APP_DOMAIN//./_}"
assert_output --partial "$sanitisedDomainName_app" assert_output --partial "$sanitisedDomainName_app"
assert_output --partial "nginx"
} }

View File

@ -25,11 +25,9 @@ teardown(){
@test "validate app argument" { @test "validate app argument" {
run $ABRA app undeploy run $ABRA app undeploy
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app undeploy DOESNTEXIST run $ABRA app undeploy DOESNTEXIST
assert_failure assert_failure
assert_output --partial 'cannot find app'
} }
# bats test_tags=slow # bats test_tags=slow
@ -109,10 +107,10 @@ teardown(){
# bats test_tags=slow # bats test_tags=slow
@test "undeploy chaos deployment" { @test "undeploy chaos deployment" {
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --chaos
--no-input --no-converge-checks --chaos
_undeploy_app run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
assert_success
# NOTE(d1): ensure chaos undeploy # NOTE(d1): ensure chaos undeploy
assert_output --partial ${_get_current_hash:0:8} assert_output --partial ${_get_current_hash:0:8}

View File

@ -0,0 +1,60 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_ensure_catalogue
}
teardown(){
_reset_recipe
_undeploy_app
_reset_app
}
# bats test_tags=slow
@test "undeploy version written to env" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
--no-input --no-converge-checks
assert_success
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run sed -i "s/TYPE=$TEST_RECIPE:.*/TYPE=$TEST_RECIPE/g" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
assert_success
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
# bats test_tags=slow
@test "chaos commit written to env" {
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --chaos
run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
assert_success
run grep -q "TYPE=$TEST_RECIPE:${_get_current_hash:0:8}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}

View File

@ -15,22 +15,22 @@ teardown_file(){
setup(){ setup(){
load "$PWD/tests/integration/helpers/common" load "$PWD/tests/integration/helpers/common"
_common_setup _common_setup
_ensure_catalogue
} }
teardown(){ teardown(){
_reset_recipe _reset_recipe
_reset_app _reset_app
_undeploy_app _undeploy_app
_reset_app # NOTE(d1): _undeploy_app writes version
} }
@test "validate app argument" { @test "validate app argument" {
run $ABRA app upgrade run $ABRA app upgrade
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app upgrade DOESNTEXIST run $ABRA app upgrade DOESNTEXIST
assert_failure assert_failure
assert_output --partial 'cannot find app'
} }
# bats test_tags=slow # bats test_tags=slow

View File

@ -24,11 +24,9 @@ teardown(){
@test "ls validate app argument" { @test "ls validate app argument" {
run $ABRA app volume ls run $ABRA app volume ls
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app volume ls DOESNTEXIST run $ABRA app volume ls DOESNTEXIST
assert_failure assert_failure
assert_output --partial 'cannot find app'
} }
@test "list no volumes" { @test "list no volumes" {
@ -49,11 +47,9 @@ teardown(){
@test "rm validate app argument" { @test "rm validate app argument" {
run $ABRA app volume rm run $ABRA app volume rm
assert_failure assert_failure
assert_output --partial 'no app provided'
run $ABRA app volume rm DOESNTEXIST run $ABRA app volume rm DOESNTEXIST
assert_failure assert_failure
assert_output --partial 'cannot find app'
} }
# bats test_tags=slow # bats test_tags=slow

View File

@ -1,30 +0,0 @@
#!/usr/bin/env bash
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
@test "bash autocompletion" {
run $ABRA autocomplete bash
assert_success
assert_exists "$ABRA_DIR/autocompletion/bash"
}
@test "zsh autocompletion" {
run $ABRA autocomplete zsh
assert_success
assert_exists "$ABRA_DIR/autocompletion/zsh"
}
@test "fish autocompletion" {
run $ABRA autocomplete fish
assert_success
assert_exists "$ABRA_DIR/autocompletion/fish"
}
@test "fizsh autocompletion" {
run $ABRA autocomplete fizsh
assert_success
assert_exists "$ABRA_DIR/autocompletion/zsh"
}

View File

@ -4,7 +4,7 @@ _mkfile() {
} }
_mkfile_remote() { _mkfile_remote() {
run $ABRA app run "$TEST_APP_DOMAIN" app "bash -c \"echo $2 > $1\"" run $ABRA app run "$TEST_APP_DOMAIN" app -- "bash -c \"echo $2 > $1\""
assert_success assert_success
} }
@ -19,6 +19,6 @@ _rm() {
} }
_rm_remote() { _rm_remote() {
run "$ABRA" app run "$TEST_APP_DOMAIN" app rm -rf "$1" run "$ABRA" app run "$TEST_APP_DOMAIN" app -- rm -rf "$1"
assert_success assert_success
} }

View File

@ -11,7 +11,7 @@ setup() {
assert_success assert_success
assert_not_exists "$ABRA_DIR/recipes/matrix-synapse" assert_not_exists "$ABRA_DIR/recipes/matrix-synapse"
run $ABRA recipe fetch run $ABRA recipe fetch --all
assert_success assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse" assert_exists "$ABRA_DIR/recipes/matrix-synapse"
} }
@ -25,3 +25,13 @@ setup() {
assert_success assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse" assert_exists "$ABRA_DIR/recipes/matrix-synapse"
} }
@test "error if missing args/flags" {
run $ABRA recipe fetch
assert_failure
}
@test "error if single recipe and --all" {
run $ABRA recipe fetch matrix-synapse --all
assert_failure
}

View File

@ -5,13 +5,6 @@ setup() {
_common_setup _common_setup
} }
@test "recipe list" {
run $ABRA recipe list
assert_success
NUM_RECIPES=$(jq length "$ABRA_DIR/catalogue/recipes.json")
assert_output --partial "total recipes: $NUM_RECIPES"
}
@test "recipe list with pattern" { @test "recipe list with pattern" {
run $ABRA recipe list --pattern cloud run $ABRA recipe list --pattern cloud
assert_success assert_success

View File

@ -48,11 +48,9 @@ teardown(){
--server "$TEST_SERVER" \ --server "$TEST_SERVER" \
--domain "foobar.$TEST_SERVER" --domain "foobar.$TEST_SERVER"
assert_success assert_success
assert_output --partial "new app 'foobar' created"
run $ABRA app deploy "foobar.$TEST_SERVER" --no-input run $ABRA app deploy "foobar.$TEST_SERVER" --no-input
assert_success assert_success
assert_output --partial 'using latest commit'
} }
@test "create new recipe with git credentials" { @test "create new recipe with git credentials" {

View File

@ -25,11 +25,9 @@ teardown() {
@test "validate recipe argument" { @test "validate recipe argument" {
run $ABRA recipe release --no-input run $ABRA recipe release --no-input
assert_failure assert_failure
assert_output --partial 'no recipe name provided'
run $ABRA recipe release DOESNTEXIST --no-input run $ABRA recipe release DOESNTEXIST --no-input
assert_failure assert_failure
assert_output --partial 'unable to clone DOESNTEXIST'
} }
@test "release patch bump" { @test "release patch bump" {

View File

@ -24,11 +24,9 @@ teardown(){
@test "validate recipe argument" { @test "validate recipe argument" {
run $ABRA recipe sync --no-input run $ABRA recipe sync --no-input
assert_failure assert_failure
assert_output --partial 'no recipe name provided'
run $ABRA recipe sync DOESNTEXIST --no-input run $ABRA recipe sync DOESNTEXIST --no-input
assert_failure assert_failure
assert_output --partial 'unable to clone DOESNTEXIST'
} }
@test "allow unstaged changes" { @test "allow unstaged changes" {

View File

@ -24,11 +24,9 @@ teardown(){
@test "validate recipe argument" { @test "validate recipe argument" {
run $ABRA recipe upgrade --no-input run $ABRA recipe upgrade --no-input
assert_failure assert_failure
assert_output --partial 'no recipe name provided'
run $ABRA recipe upgrade DOESNTEXIST --no-input run $ABRA recipe upgrade DOESNTEXIST --no-input
assert_failure assert_failure
assert_output --partial 'unable to clone DOESNTEXIST'
} }
@test "retrieve recipe if missing" { @test "retrieve recipe if missing" {

View File

@ -32,13 +32,3 @@ setup() {
assert_success assert_success
assert_output "$latestVersion" assert_output "$latestVersion"
} }
@test "app is first service listed" {
run bash -c '$ABRA recipe versions gitea --machine | jq -r ".[0].service" | uniq'
assert_success
assert_output 'app'
run bash -c '$ABRA recipe versions gitea --machine | jq -r ".[1].service" | uniq'
assert_success
assert_output 'db'
}

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