Compare commits

...

46 Commits

Author SHA1 Message Date
ff217837b2 WIP: feat: translation support
Some checks failed
continuous-integration/drone/push Build is failing
See #483
2025-08-23 16:20:48 +02:00
5cf6048ecb test: also wipe env version
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-19 09:49:02 +02:00
3e2797c433 test: fixups for latest nightly failures
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-19 09:41:37 +02:00
df89e8143a chore: clean cruft / formatting / add whitespace
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-19 07:30:55 +00:00
b4ddd3e77c feat: handle generate=false env var mod
See toolshed/organising#461
2025-08-19 07:30:55 +00:00
81c28e3006 test: appease the tester
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-18 20:50:50 +02:00
34d2e3b092 chore: go mod vendor 2025-08-18 20:50:49 +02:00
1894c2f5fc chore: appease formatter 2025-08-18 20:50:48 +02:00
e0bd03bec3 chore: bump deps 2025-08-18 20:50:47 +02:00
77ff146991 fix: better parsing errors
See toolshed/organising#608
See toolshed/organising#531
2025-08-18 20:50:46 +02:00
6fad1a1dcc test: check app list doesn't explode if missing .env
All checks were successful
continuous-integration/drone/push Build is passing
See #560
2025-08-18 09:47:10 +02:00
a90e239547 refactor!: ensure insert/remove not arbitrary
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-08-18 09:25:31 +02:00
9ee094fcd7 docs: secret removal examples 2025-08-18 09:25:20 +02:00
1aa7016789 fix: skip name validation for remote recipes
All checks were successful
continuous-integration/drone/push Build is passing
See #601
2025-08-18 08:56:52 +02:00
60b3af1fa4 test: retrieve abra-test-recipe by hand now
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-18 06:32:41 +00:00
5f4b5e0fad fix: return error, not log.Fatal
See #582
2025-08-18 06:32:41 +00:00
feadfca0d6 refactor: move ensureCtx closer to usage 2025-08-18 06:32:41 +00:00
73d4ee1c98 refactor: always validate recipe
This can slow things significantly down by requiring the catalogue and
if you don't have that, cause a slow `git clone`. However, the current
behvaiour is very confusing because it never actually checks if what the
user passes is actually a recipe. `abra recipe fetch DOESNTEXIST` gives
a better error to the user now. I'm hoping we can speed up the catalogue
handling at some point.
2025-08-18 06:32:41 +00:00
f46c18c8d7 fix: warn on unknown server
All checks were successful
continuous-integration/drone/push Build is passing
See #581
2025-08-18 08:29:14 +02:00
f5a843bd90 feat: remove old app configs
All checks were successful
continuous-integration/drone/push Build is passing
See #577
2025-08-17 13:27:35 +00:00
fac372dc73 test: use latest release for check
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-17 15:18:05 +02:00
8a3be01c3e fix: $ABRA_DIR/servers subdirs also 0700
All checks were successful
continuous-integration/drone/push Build is passing
See 38f308910a
See #580
2025-08-17 14:13:47 +02:00
4193d63d23 test: advertise locally to avoid multiple ip error
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-08-17 14:04:00 +02:00
38f308910a fix: $ABRA_DIR/servers=0700, $ABRA_DIR/servers/foo=0600
See #580
2025-08-17 14:03:15 +02:00
4aaa7400b8 fix: also ensure server is created with 0600
All checks were successful
continuous-integration/drone/push Build is passing
See 6849e3554d
2025-08-17 13:32:37 +02:00
091611b984 feat: add volume arg to volume rm
All checks were successful
continuous-integration/drone/push Build is passing
See #574
2025-08-17 11:16:30 +00:00
2cfc40dc28 fix: ensure recipe with undeploy
All checks were successful
continuous-integration/drone/push Build is passing
See #573
2025-08-17 09:36:48 +00:00
6849e3554d fix: ensure $ABRA_DIR/servers is 0600
All checks were successful
continuous-integration/drone/push Build is passing
Also remove deprecated folders while I'm here: `vendor` / `backup`

See #580
2025-08-17 09:17:00 +00:00
452de7fdc2 docs: show HOWTO generate in abra man help
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #568
2025-08-13 13:08:48 +02:00
952d768ab0 docs: show app secret rm example
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Closes #558
2025-08-12 21:34:57 +02:00
2c91d2040e fix: app ls -S didn't show updates sometimes (#561)
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-12 13:10:16 +00:00
3wc
eff4435971 ABRA_TEST_DOMAIN → TEST_SERVER
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-12 12:18:49 +00:00
3wc
032fe99086 Appease formatter
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-12 12:18:29 +00:00
3wc
7add56df00 Add integration tests for new "--chaos to blast past lint errors"
Re toolshed/organising#497
2025-08-12 12:18:29 +00:00
3wc
0ab05cece2 Reformat linting errors in LintForErrors
See toolshed/organising#497 (comment)
2025-08-12 12:18:29 +00:00
3wc
c63f6db61e WARN on recipe linting errors during --chaos..
..and show multiple linting errors instead of bailing on the first one.

Re #497
2025-08-12 12:18:29 +00:00
56a68dfa91 chore: bump deps
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-12 05:17:15 +00:00
157d131b37 feat: Retrieves auth token from image
All checks were successful
continuous-integration/drone/push Build is passing
This allows using a private registry for an image.
To use it, you have to run docker login on your local machine before
running abra deploy.
2025-08-12 05:01:42 +00:00
3fae036db2 change to debug log
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-11 12:47:43 +00:00
ce9d0934b6 fix: Does not error when recipes folder does not exist in app new 2025-08-11 12:47:43 +00:00
a32e30374f Translated using Weblate (Spanish)
Some checks reported errors
continuous-integration/drone/push Build is passing
continuous-integration/drone Build was killed
Currently translated at 100.0% (1 of 1 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2025-08-04 16:51:35 +02:00
3wc
cf46569f04 Add stub es catalogue 2025-08-04 16:51:34 +02:00
3wc
022606c13c Add default POT catalogue, don't alias gotext.Get 2025-08-04 16:51:33 +02:00
8cfda5229f feat: weblate 2025-08-04 16:51:26 +02:00
855a4c37c4 chore: bump installer script 2025-08-04 15:26:24 +02:00
3wc
7c3b740e14 Update the server used to deploy the installer script
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-07 15:01:31 +01:00
1123 changed files with 56981 additions and 41632 deletions

View File

@ -1,6 +1,6 @@
# integration test suite # integration test suite
# export ABRA_DIR="$HOME/.abra_test" # export ABRA_DIR="$HOME/.abra_test"
# export ABRA_TEST_DOMAIN=test.example.com # export TEST_SERVER=test.example.com
# export ABRA_CI=1 # export ABRA_CI=1
# release automation # release automation

View File

@ -3,6 +3,7 @@
[![Build Status](https://build.coopcloud.tech/api/badges/toolshed/abra/status.svg?ref=refs/heads/main)](https://build.coopcloud.tech/toolshed/abra) [![Build Status](https://build.coopcloud.tech/api/badges/toolshed/abra/status.svg?ref=refs/heads/main)](https://build.coopcloud.tech/toolshed/abra)
[![Go Report Card](https://goreportcard.com/badge/git.coopcloud.tech/toolshed/abra)](https://goreportcard.com/report/git.coopcloud.tech/toolshed/abra) [![Go Report Card](https://goreportcard.com/badge/git.coopcloud.tech/toolshed/abra)](https://goreportcard.com/report/git.coopcloud.tech/toolshed/abra)
[![Go Reference](https://pkg.go.dev/badge/coopcloud.tech/abra.svg)](https://pkg.go.dev/coopcloud.tech/abra) [![Go Reference](https://pkg.go.dev/badge/coopcloud.tech/abra.svg)](https://pkg.go.dev/coopcloud.tech/abra)
[![Translation status](https://translate.coopcloud.tech/widget/co-op-cloud/svg-badge.svg)](https://translate.coopcloud.tech/engage/co-op-cloud/)
The Co-op Cloud utility belt 🎩🐇 The Co-op Cloud utility belt 🎩🐇

View File

@ -1,11 +1,12 @@
package app package app
import ( import (
"coopcloud.tech/abra/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var AppCommand = &cobra.Command{ var AppCommand = &cobra.Command{
Use: "app [cmd] [args] [flags]", Use: i18n.G("app [cmd] [args] [flags]"),
Aliases: []string{"a"}, Aliases: []string{i18n.G("a")},
Short: "Manage apps", Short: i18n.G("Manage apps"),
} }

View File

@ -6,14 +6,15 @@ 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/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var AppBackupListCommand = &cobra.Command{ var AppBackupListCommand = &cobra.Command{
Use: "list <domain> [flags]", Use: i18n.G("list <domain> [flags]"),
Aliases: []string{"ls"}, Aliases: []string{i18n.G("ls")},
Short: "List the contents of a snapshot", Short: i18n.G("List the contents of a snapshot"),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -40,17 +41,17 @@ var AppBackupListCommand = &cobra.Command{
} }
if snapshot != "" { if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot) log.Debug(i18n.G("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 showAllPaths { if showAllPaths {
log.Debugf("including SHOW_ALL=%v in backupbot exec invocation", showAllPaths) log.Debug(i18n.G("including SHOW_ALL=%v in backupbot exec invocation", showAllPaths))
execEnv = append(execEnv, fmt.Sprintf("SHOW_ALL=%v", showAllPaths)) execEnv = append(execEnv, fmt.Sprintf("SHOW_ALL=%v", showAllPaths))
} }
if timestamps { if timestamps {
log.Debugf("including TIMESTAMPS=%v in backupbot exec invocation", timestamps) log.Debug(i18n.G("including TIMESTAMPS=%v in backupbot exec invocation", timestamps))
execEnv = append(execEnv, fmt.Sprintf("TIMESTAMPS=%v", timestamps)) execEnv = append(execEnv, fmt.Sprintf("TIMESTAMPS=%v", timestamps))
} }
@ -61,13 +62,13 @@ var AppBackupListCommand = &cobra.Command{
} }
var AppBackupDownloadCommand = &cobra.Command{ var AppBackupDownloadCommand = &cobra.Command{
Use: "download <domain> [flags]", Use: i18n.G("download <domain> [flags]"),
Aliases: []string{"d"}, Aliases: []string{i18n.G("d")},
Short: "Download a snapshot", Short: i18n.G("Download a snapshot"),
Long: `Downloads a backup.tar.gz to the current working directory. Long: i18n.G(`Downloads a backup.tar.gz to the current working directory.
"--volumes/-v" includes data contained in volumes alongide paths specified in "--volumes/-v" includes data contained in volumes alongide paths specified in
"backupbot.backup.path" labels.`, "backupbot.backup.path" labels.`),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -98,22 +99,22 @@ var AppBackupDownloadCommand = &cobra.Command{
} }
if snapshot != "" { if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot) log.Debug(i18n.G("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.Debug(i18n.G("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 includeSecrets { if includeSecrets {
log.Debugf("including SECRETS=%v in backupbot exec invocation", includeSecrets) log.Debug(i18n.G("including SECRETS=%v in backupbot exec invocation", includeSecrets))
execEnv = append(execEnv, fmt.Sprintf("SECRETS=%v", includeSecrets)) execEnv = append(execEnv, fmt.Sprintf("SECRETS=%v", includeSecrets))
} }
if includeVolumes { if includeVolumes {
log.Debugf("including VOLUMES=%v in backupbot exec invocation", includeVolumes) log.Debug(i18n.G("including VOLUMES=%v in backupbot exec invocation", includeVolumes))
execEnv = append(execEnv, fmt.Sprintf("VOLUMES=%v", includeVolumes)) execEnv = append(execEnv, fmt.Sprintf("VOLUMES=%v", includeVolumes))
} }
@ -130,9 +131,9 @@ var AppBackupDownloadCommand = &cobra.Command{
} }
var AppBackupCreateCommand = &cobra.Command{ var AppBackupCreateCommand = &cobra.Command{
Use: "create <domain> [flags]", Use: i18n.G("create <domain> [flags]"),
Aliases: []string{"c"}, Aliases: []string{i18n.G("c")},
Short: "Create a new snapshot", Short: i18n.G("Create a new snapshot"),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -163,7 +164,7 @@ var AppBackupCreateCommand = &cobra.Command{
} }
if retries != "" { if retries != "" {
log.Debugf("including RETRIES=%s in backupbot exec invocation", retries) log.Debug(i18n.G("including RETRIES=%s in backupbot exec invocation", retries))
execEnv = append(execEnv, fmt.Sprintf("RETRIES=%s", retries)) execEnv = append(execEnv, fmt.Sprintf("RETRIES=%s", retries))
} }
@ -174,9 +175,9 @@ var AppBackupCreateCommand = &cobra.Command{
} }
var AppBackupSnapshotsCommand = &cobra.Command{ var AppBackupSnapshotsCommand = &cobra.Command{
Use: "snapshots <domain> [flags]", Use: i18n.G("snapshots <domain> [flags]"),
Aliases: []string{"s"}, Aliases: []string{i18n.G("s")},
Short: "List all snapshots", Short: i18n.G("List all snapshots"),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -209,9 +210,9 @@ var AppBackupSnapshotsCommand = &cobra.Command{
} }
var AppBackupCommand = &cobra.Command{ var AppBackupCommand = &cobra.Command{
Use: "backup [cmd] [args] [flags]", Use: i18n.G("backup [cmd] [args] [flags]"),
Aliases: []string{"b"}, Aliases: []string{i18n.G("b")},
Short: "Manage app backups", Short: i18n.G("Manage app backups"),
} }
var ( var (
@ -227,81 +228,81 @@ var (
func init() { func init() {
AppBackupListCommand.Flags().StringVarP( AppBackupListCommand.Flags().StringVarP(
&snapshot, &snapshot,
"snapshot", i18n.G("snapshot"),
"s", i18n.G("s"),
"", "",
"list specific snapshot", i18n.G("list specific snapshot"),
) )
AppBackupListCommand.Flags().BoolVarP( AppBackupListCommand.Flags().BoolVarP(
&showAllPaths, &showAllPaths,
"all", i18n.G("all"),
"a", i18n.G("a"),
false, false,
"show all paths", i18n.G("show all paths"),
) )
AppBackupListCommand.Flags().BoolVarP( AppBackupListCommand.Flags().BoolVarP(
&timestamps, &timestamps,
"timestamps", i18n.G("timestamps"),
"t", i18n.G("t"),
false, false,
"include timestamps", i18n.G("include timestamps"),
) )
AppBackupDownloadCommand.Flags().StringVarP( AppBackupDownloadCommand.Flags().StringVarP(
&snapshot, &snapshot,
"snapshot", i18n.G("snapshot"),
"s", i18n.G("s"),
"", "",
"list specific snapshot", i18n.G("list specific snapshot"),
) )
AppBackupDownloadCommand.Flags().StringVarP( AppBackupDownloadCommand.Flags().StringVarP(
&includePath, &includePath,
"path", i18n.G("path"),
"p", i18n.G("p"),
"", "",
"volumes path", i18n.G("volumes path"),
) )
AppBackupDownloadCommand.Flags().BoolVarP( AppBackupDownloadCommand.Flags().BoolVarP(
&includeSecrets, &includeSecrets,
"secrets", i18n.G("secrets"),
"S", i18n.G("S"),
false, false,
"include secrets", i18n.G("include secrets"),
) )
AppBackupDownloadCommand.Flags().BoolVarP( AppBackupDownloadCommand.Flags().BoolVarP(
&includeVolumes, &includeVolumes,
"volumes", i18n.G("volumes"),
"v", i18n.G("v"),
false, false,
"include volumes", i18n.G("include volumes"),
) )
AppBackupDownloadCommand.Flags().BoolVarP( AppBackupDownloadCommand.Flags().BoolVarP(
&internal.Chaos, &internal.Chaos,
"chaos", i18n.G("chaos"),
"C", i18n.G("C"),
false, false,
"ignore uncommitted recipes changes", i18n.G("ignore uncommitted recipes changes"),
) )
AppBackupCreateCommand.Flags().StringVarP( AppBackupCreateCommand.Flags().StringVarP(
&retries, &retries,
"retries", i18n.G("retries"),
"r", i18n.G("r"),
"1", "1",
"number of retry attempts", i18n.G("number of retry attempts"),
) )
AppBackupCreateCommand.Flags().BoolVarP( AppBackupCreateCommand.Flags().BoolVarP(
&internal.Chaos, &internal.Chaos,
"chaos", i18n.G("chaos"),
"C", i18n.G("C"),
false, false,
"ignore uncommitted recipes changes", i18n.G("ignore uncommitted recipes changes"),
) )
} }

View File

@ -7,16 +7,17 @@ import (
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var AppCheckCommand = &cobra.Command{ var AppCheckCommand = &cobra.Command{
Use: "check <domain> [flags]", Use: i18n.G("check <domain> [flags]"),
Aliases: []string{"chk"}, Aliases: []string{i18n.G("chk")},
Short: "Ensure an app is well configured", Short: i18n.G("Ensure an app is well configured"),
Long: `Compare env vars in both the app ".env" and recipe ".env.sample" file. Long: i18n.G(`Compare env vars in both the app ".env" and recipe ".env.sample" file.
The goal is to ensure that recipe ".env.sample" env vars are defined in your 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
@ -25,7 +26,7 @@ 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.`),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -83,9 +84,9 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
func init() { func init() {
AppCheckCommand.Flags().BoolVarP( AppCheckCommand.Flags().BoolVarP(
&internal.Chaos, &internal.Chaos,
"chaos", i18n.G("chaos"),
"C", i18n.G("C"),
false, false,
"ignore uncommitted recipes changes", i18n.G("ignore uncommitted recipes changes"),
) )
} }

View File

@ -13,15 +13,16 @@ import (
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var AppCmdCommand = &cobra.Command{ var AppCmdCommand = &cobra.Command{
Use: "command <domain> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]", Use: i18n.G("command <domain> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]"),
Aliases: []string{"cmd"}, Aliases: []string{i18n.G("cmd")},
Short: "Run app commands", Short: i18n.G("Run app commands"),
Long: `Run an app specific command. Long: i18n.G(`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
@ -30,24 +31,24 @@ work station by passing "--local/-l".
N.B. If using the "--" style to pass arguments, flags (e.g. "--local/-l") must 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 "--" 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" as long as no dashes are present (i.e. "foo" works without "--", "-foo"
does not).`, does not).`),
Example: ` # pass <cmd> args/flags without "--" Example: i18n.G(` # pass <cmd> args/flags without "--"
abra app cmd 1312.net app my_cmd_arg foo --user bar abra app cmd 1312.net app my_cmd_arg foo --user bar
# pass <cmd> args/flags with "--" # pass <cmd> args/flags with "--"
abra app cmd 1312.net app my_cmd_args --user bar -- foo -vvv abra app cmd 1312.net app my_cmd_args --user bar -- foo -vvv
# drop the [service] arg if using "--local/-l" # drop the [service] arg if using "--local/-l"
abra app cmd 1312.net my_cmd --local`, abra app cmd 1312.net my_cmd --local`),
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
if local { if local {
if !(len(args) >= 2) { if !(len(args) >= 2) {
return errors.New("requires at least 2 arguments with --local/-l") return errors.New(i18n.G("requires at least 2 arguments with --local/-l"))
} }
if slices.Contains(os.Args, "--") { if slices.Contains(os.Args, "--") {
if cmd.ArgsLenAtDash() > 2 { if cmd.ArgsLenAtDash() > 2 {
return errors.New("accepts at most 2 args with --local/-l") return errors.New(i18n.G("accepts at most 2 args with --local/-l"))
} }
} }
@ -63,7 +64,7 @@ does not).`,
} }
if !(len(args) >= 3) { if !(len(args) >= 3) {
return errors.New("requires at least 3 arguments") return errors.New(i18n.G("requires at least 3 arguments"))
} }
return nil return nil
@ -97,14 +98,14 @@ does not).`,
} }
if local && remoteUser != "" { if local && remoteUser != "" {
log.Fatal("cannot use --local & --user together") log.Fatal(i18n.G("cannot use --local & --user together"))
} }
hasCmdArgs, parsedCmdArgs := parseCmdArgs(args, local) 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) {
log.Fatalf("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name) log.Fatal(i18n.G("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name))
} }
log.Fatal(err) log.Fatal(err)
} }
@ -115,7 +116,7 @@ does not).`,
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("--local detected, running %s on local work station", cmdName) log.Debug(i18n.G("--local detected, running %s on local work station", cmdName))
var exportEnv string var exportEnv string
for k, v := range app.Env { for k, v := range app.Env {
@ -124,16 +125,16 @@ does not).`,
var sourceAndExec string var sourceAndExec string
if hasCmdArgs { if hasCmdArgs {
log.Debugf("parsed following command arguments: %s", parsedCmdArgs) log.Debug(i18n.G("parsed following command arguments: %s", parsedCmdArgs))
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s %s", app.Name, app.StackName(), exportEnv, app.Recipe.AbraShPath, cmdName, parsedCmdArgs) sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s %s", app.Name, app.StackName(), exportEnv, app.Recipe.AbraShPath, cmdName, parsedCmdArgs)
} else { } else {
log.Debug("did not detect any command arguments") log.Debug(i18n.G("did not detect any command arguments"))
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s", app.Name, app.StackName(), exportEnv, app.Recipe.AbraShPath, cmdName) sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s", app.Name, app.StackName(), exportEnv, app.Recipe.AbraShPath, cmdName)
} }
shell := "/bin/bash" shell := "/bin/bash"
if _, err := os.Stat(shell); errors.Is(err, os.ErrNotExist) { if _, err := os.Stat(shell); errors.Is(err, os.ErrNotExist) {
log.Debugf("%s does not exist locally, use /bin/sh as fallback", shell) log.Debug(i18n.G("%s does not exist locally, use /bin/sh as fallback", shell))
shell = "/bin/sh" shell = "/bin/sh"
} }
cmd := exec.Command(shell, "-c", sourceAndExec) cmd := exec.Command(shell, "-c", sourceAndExec)
@ -164,15 +165,15 @@ does not).`,
} }
if !matchingServiceName { if !matchingServiceName {
log.Fatalf("no service %s for %s?", targetServiceName, app.Name) log.Fatal(i18n.G("no service %s for %s?", targetServiceName, app.Name))
} }
log.Debugf("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName) log.Debug(i18n.G("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName))
if hasCmdArgs { if hasCmdArgs {
log.Debugf("parsed following command arguments: %s", parsedCmdArgs) log.Debug(i18n.G("parsed following command arguments: %s", parsedCmdArgs))
} else { } else {
log.Debug("did not detect any command arguments") log.Debug(i18n.G("did not detect any command arguments"))
} }
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
@ -192,9 +193,9 @@ does not).`,
} }
var AppCmdListCommand = &cobra.Command{ var AppCmdListCommand = &cobra.Command{
Use: "list <domain> [flags]", Use: i18n.G("list <domain> [flags]"),
Aliases: []string{"ls"}, Aliases: []string{i18n.G("ls")},
Short: "List all available commands", Short: i18n.G("List all available commands"),
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args) app := internal.ValidateApp(args)
@ -244,33 +245,33 @@ var (
func init() { func init() {
AppCmdCommand.Flags().BoolVarP( AppCmdCommand.Flags().BoolVarP(
&local, &local,
"local", i18n.G("local"),
"l", i18n.G("l"),
false, false,
"run command locally", i18n.G("run command locally"),
) )
AppCmdCommand.Flags().StringVarP( AppCmdCommand.Flags().StringVarP(
&remoteUser, &remoteUser,
"user", i18n.G("user"),
"u", i18n.G("u"),
"", "",
"request remote user", i18n.G("request remote user"),
) )
AppCmdCommand.Flags().BoolVarP( AppCmdCommand.Flags().BoolVarP(
&disableTTY, &disableTTY,
"tty", i18n.G("tty"),
"T", i18n.G("T"),
false, false,
"disable remote TTY", i18n.G("disable remote TTY"),
) )
AppCmdCommand.Flags().BoolVarP( AppCmdCommand.Flags().BoolVarP(
&internal.Chaos, &internal.Chaos,
"chaos", i18n.G("chaos"),
"C", i18n.G("C"),
false, false,
"ignore uncommitted recipes changes", i18n.G("ignore uncommitted recipes changes"),
) )
} }

View File

@ -6,16 +6,17 @@ import (
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var AppConfigCommand = &cobra.Command{ var AppConfigCommand = &cobra.Command{
Use: "config <domain> [flags]", Use: i18n.G("config <domain> [flags]"),
Aliases: []string{"cfg"}, Aliases: []string{i18n.G("cfg")},
Short: "Edit app config", Short: i18n.G("Edit app config"),
Example: " abra config 1312.net", Example: i18n.G(" abra config 1312.net"),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -32,13 +33,13 @@ var AppConfigCommand = &cobra.Command{
appName := args[0] 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.Fatal(i18n.G("cannot find app with name %s", appName))
} }
ed, ok := os.LookupEnv("EDITOR") ed, ok := os.LookupEnv("EDITOR")
if !ok { if !ok {
edPrompt := &survey.Select{ edPrompt := &survey.Select{
Message: "which editor do you wish to use?", Message: i18n.G("which editor do you wish to use?"),
Options: []string{"vi", "vim", "nvim", "nano", "pico", "emacs"}, Options: []string{"vi", "vim", "nvim", "nano", "pico", "emacs"},
} }
if err := survey.AskOne(edPrompt, &ed); err != nil { if err := survey.AskOne(edPrompt, &ed); err != nil {

View File

@ -3,7 +3,6 @@ package app
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"os" "os"
"path" "path"
@ -15,6 +14,7 @@ import (
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
containerPkg "coopcloud.tech/abra/pkg/container" containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/container" "coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
@ -26,14 +26,14 @@ import (
) )
var AppCpCommand = &cobra.Command{ var AppCpCommand = &cobra.Command{
Use: "cp <domain> <src> <dst> [flags]", Use: i18n.G("cp <domain> <src> <dst> [flags]"),
Aliases: []string{"c"}, Aliases: []string{i18n.G("c")},
Short: "Copy files to/from a deployed app service", Short: i18n.G("Copy files to/from a deployed app service"),
Example: ` # copy myfile.txt to the root of the app service Example: i18n.G(` # copy myfile.txt to the root of the app service
abra app cp 1312.net myfile.txt app:/ abra app cp 1312.net myfile.txt app:/
# copy that file back to your current working directory locally # copy that file back to your current working directory locally
abra app cp 1312.net app:/myfile.txt ./`, abra app cp 1312.net app:/myfile.txt ./`),
Args: cobra.ExactArgs(3), Args: cobra.ExactArgs(3),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -69,7 +69,7 @@ var AppCpCommand = &cobra.Command{
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server) log.Debug(i18n.G("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server))
if toContainer { if toContainer {
err = CopyToContainer(cl, container.ID, srcPath, dstPath) err = CopyToContainer(cl, container.ID, srcPath, dstPath)
@ -82,7 +82,7 @@ var AppCpCommand = &cobra.Command{
}, },
} }
var errServiceMissing = errors.New("one of <src>/<dest> arguments must take $SERVICE:$PATH form") var errServiceMissing = errors.New(i18n.G("one of <src>/<dest> arguments must take $SERVICE:$PATH form"))
// parseSrcAndDst parses src and dest string. One of src or dst must be of the form $SERVICE:$PATH // parseSrcAndDst parses src and dest string. One of src or dst must be of the form $SERVICE:$PATH
func parseSrcAndDst(src, dst string) (srcPath string, dstPath string, service string, toContainer bool, err error) { func parseSrcAndDst(src, dst string) (srcPath string, dstPath string, service string, toContainer bool, err error) {
@ -105,7 +105,7 @@ func parseSrcAndDst(src, dst string) (srcPath string, dstPath string, service st
func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath string) error { func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath string) error {
srcStat, err := os.Stat(srcPath) srcStat, err := os.Stat(srcPath)
if err != nil { if err != nil {
return fmt.Errorf("local %s ", err) return errors.New(i18n.G("local %s ", err))
} }
dstStat, err := cl.ContainerStatPath(context.Background(), containerID, dstPath) dstStat, err := cl.ContainerStatPath(context.Background(), containerID, dstPath)
@ -114,7 +114,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
if errdefs.IsNotFound(err) { if errdefs.IsNotFound(err) {
dstExists = false dstExists = false
} else { } else {
return fmt.Errorf("remote path: %s", err) return errors.New(i18n.G("remote path: %s", err))
} }
} }
@ -142,7 +142,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
Detach: false, Detach: false,
Tty: true, Tty: true,
}); err != nil { }); err != nil {
return fmt.Errorf("create remote directory: %s", err) return errors.New(i18n.G("create remote directory: %s", err))
} }
case CopyModeFileToFile: case CopyModeFileToFile:
// Remove the file component from the path, since docker can only copy // Remove the file component from the path, since docker can only copy
@ -161,7 +161,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
return err return err
} }
log.Debugf("copy %s from local to %s on container", srcPath, dstPath) log.Debug(i18n.G("copy %s from local to %s on container", srcPath, dstPath))
copyOpts := containertypes.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false} copyOpts := containertypes.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(context.Background(), containerID, dstPath, content, copyOpts); err != nil { if err := cl.CopyToContainer(context.Background(), containerID, dstPath, content, copyOpts); err != nil {
return err return err
@ -181,7 +181,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
Detach: false, Detach: false,
Tty: true, Tty: true,
}); err != nil { }); err != nil {
return fmt.Errorf("create remote directory: %s", err) return errors.New(i18n.G("create remote directory: %s", err))
} }
} }
@ -194,9 +194,9 @@ func CopyFromContainer(cl *dockerClient.Client, containerID, srcPath, dstPath st
srcStat, err := cl.ContainerStatPath(context.Background(), containerID, srcPath) srcStat, err := cl.ContainerStatPath(context.Background(), containerID, srcPath)
if err != nil { if err != nil {
if errdefs.IsNotFound(err) { if errdefs.IsNotFound(err) {
return fmt.Errorf("remote: %s does not exist", srcPath) return errors.New(i18n.G("remote: %s does not exist", srcPath))
} else { } else {
return fmt.Errorf("remote path: %s", err) return errors.New(i18n.G("remote path: %s", err))
} }
} }
@ -207,7 +207,7 @@ func CopyFromContainer(cl *dockerClient.Client, containerID, srcPath, dstPath st
if os.IsNotExist(err) { if os.IsNotExist(err) {
dstExists = false dstExists = false
} else { } else {
return fmt.Errorf("remote path: %s", err) return errors.New(i18n.G("remote path: %s", err))
} }
} else { } else {
dstMode = dstStat.Mode() dstMode = dstStat.Mode()
@ -242,7 +242,7 @@ func CopyFromContainer(cl *dockerClient.Client, containerID, srcPath, dstPath st
content, _, err := cl.CopyFromContainer(context.Background(), containerID, srcPath) content, _, err := cl.CopyFromContainer(context.Background(), containerID, srcPath)
if err != nil { if err != nil {
return fmt.Errorf("copy: %s", err) return errors.New(i18n.G("copy: %s", err))
} }
defer content.Close() defer content.Close()
if err := archive.Untar(content, dstPath, &archive.TarOptions{ if err := archive.Untar(content, dstPath, &archive.TarOptions{
@ -250,7 +250,7 @@ func CopyFromContainer(cl *dockerClient.Client, containerID, srcPath, dstPath st
Compression: archive.Gzip, Compression: archive.Gzip,
NoLchown: true, NoLchown: true,
}); err != nil { }); err != nil {
return fmt.Errorf("untar: %s", err) return errors.New(i18n.G("untar: %s", err))
} }
if moveDstFile != "" { if moveDstFile != "" {
@ -269,8 +269,8 @@ func CopyFromContainer(cl *dockerClient.Client, containerID, srcPath, dstPath st
} }
var ( var (
ErrCopyDirToFile = fmt.Errorf("can't copy dir to file") ErrCopyDirToFile = errors.New(i18n.G("can't copy dir to file"))
ErrDstDirNotExist = fmt.Errorf("destination directory does not exist") ErrDstDirNotExist = errors.New(i18n.G("destination directory does not exist"))
) )
type CopyMode int type CopyMode int
@ -375,9 +375,9 @@ func moveFile(sourcePath, destPath string) error {
func init() { func init() {
AppCpCommand.Flags().BoolVarP( AppCpCommand.Flags().BoolVarP(
&internal.Chaos, &internal.Chaos,
"chaos", i18n.G("chaos"),
"C", i18n.G("C"),
false, false,
"ignore uncommitted recipes changes", i18n.G("ignore uncommitted recipes changes"),
) )
} }

View File

@ -2,7 +2,7 @@ package app
import ( import (
"context" "context"
"fmt" "errors"
"strings" "strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
@ -16,6 +16,7 @@ import (
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/dns" "coopcloud.tech/abra/pkg/dns"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"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"
@ -24,15 +25,15 @@ import (
) )
var AppDeployCommand = &cobra.Command{ var AppDeployCommand = &cobra.Command{
Use: "deploy <domain> [version] [flags]", Use: i18n.G("deploy <domain> [version] [flags]"),
Aliases: []string{"d"}, Aliases: []string{i18n.G("d")},
Short: "Deploy an app", Short: i18n.G("Deploy an app"),
Long: `Deploy an app. Long: i18n.G(`Deploy an app.
This command supports chaos operations. Use "--chaos/-C" to deploy your recipe This command supports chaos operations. Use "--chaos/-C" to deploy your recipe
checkout as-is. Recipe commit hashes are also supported as values for checkout as-is. Recipe commit hashes are also supported as values for
"[version]". Please note, "upgrade"/"rollback" do not support chaos operations.`, "[version]". Please note, "upgrade"/"rollback" do not support chaos operations.`),
Example: ` # standard deployment Example: i18n.G(` # standard deployment
abra app deploy 1312.net abra app deploy 1312.net
# chaos deployment # chaos deployment
@ -42,7 +43,7 @@ checkout as-is. Recipe commit hashes are also supported as values for
abra app deploy 1312.net 2.0.0+1.2.3 abra app deploy 1312.net 2.0.0+1.2.3
# deploy a specific git hash # deploy a specific git hash
abra app deploy 1312.net 886db76d`, abra app deploy 1312.net 886db76d`),
Args: cobra.RangeArgs(1, 2), Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -55,7 +56,7 @@ checkout as-is. Recipe commit hashes are also supported as values for
case 1: case 1:
app, err := appPkg.Get(args[0]) app, err := appPkg.Get(args[0])
if err != nil { if err != nil {
errMsg := fmt.Sprintf("autocomplete failed: %s", err) errMsg := i18n.G("autocomplete failed: %s", err)
return []string{errMsg}, cobra.ShellCompDirectiveError return []string{errMsg}, cobra.ShellCompDirectiveError
} }
return autocomplete.RecipeVersionComplete(app.Recipe.Name) return autocomplete.RecipeVersionComplete(app.Recipe.Name)
@ -84,7 +85,7 @@ checkout as-is. Recipe commit hashes are also supported as values for
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("checking whether %s is already deployed", app.StackName()) log.Debug(i18n.G("checking whether %s is already deployed", app.StackName()))
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName()) deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil { if err != nil {
@ -92,23 +93,27 @@ checkout as-is. Recipe commit hashes are also supported as values for
} }
if deployMeta.IsDeployed && !(internal.Force || internal.Chaos) { if deployMeta.IsDeployed && !(internal.Force || internal.Chaos) {
log.Fatalf("%s is already deployed", app.Name) log.Fatal(i18n.G("%s is already deployed", app.Name))
} }
toDeployVersion, err = getDeployVersion(args, deployMeta, app) toDeployVersion, err = getDeployVersion(args, deployMeta, app)
if err != nil { if err != nil {
log.Fatal(fmt.Errorf("get deploy version: %s", err)) log.Fatal(i18n.G("get deploy version: %s", err))
} }
if !internal.Chaos { if !internal.Chaos {
_, err = app.Recipe.EnsureVersion(toDeployVersion) _, err = app.Recipe.EnsureVersion(toDeployVersion)
if err != nil { if err != nil {
log.Fatalf("ensure recipe: %s", err) log.Fatal(i18n.G("ensure recipe: %s", err))
} }
} }
if err := lint.LintForErrors(app.Recipe); err != nil { if err := lint.LintForErrors(app.Recipe); err != nil {
log.Fatal(err) if internal.Chaos {
log.Warn(err)
} else {
log.Fatal(err)
}
} }
if err := validateSecrets(cl, app); err != nil { if err := validateSecrets(cl, app); err != nil {
@ -158,7 +163,7 @@ checkout as-is. Recipe commit hashes are also supported as values for
for _, envVar := range envVars { for _, envVar := range envVars {
if !envVar.Present { if !envVar.Present {
deployWarnMessages = append(deployWarnMessages, deployWarnMessages = append(deployWarnMessages,
fmt.Sprintf("%s missing from %s.env", envVar.Name, app.Domain), i18n.G("%s missing from %s.env", envVar.Name, app.Domain),
) )
} }
} }
@ -169,10 +174,10 @@ checkout as-is. Recipe commit hashes are also supported as values for
log.Fatal(err) log.Fatal(err)
} }
} else { } else {
log.Debug("skipping domain checks, no DOMAIN=... configured") log.Debug(i18n.G("skipping domain checks, no DOMAIN=... configured"))
} }
} else { } else {
log.Debug("skipping domain checks") log.Debug(i18n.G("skipping domain checks"))
} }
deployedVersion := config.NO_VERSION_DEFAULT deployedVersion := config.NO_VERSION_DEFAULT
@ -195,7 +200,7 @@ checkout as-is. Recipe commit hashes are also supported as values for
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout) log.Debug(i18n.G("set waiting timeout to %d second(s)", stack.WaitTimeout))
serviceNames, err := appPkg.GetAppServiceNames(app.Name) serviceNames, err := appPkg.GetAppServiceNames(app.Name)
if err != nil { if err != nil {
@ -221,14 +226,14 @@ checkout as-is. Recipe commit hashes are also supported as values for
postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"] postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"]
if ok && !internal.DontWaitConverge { if ok && !internal.DontWaitConverge {
log.Debugf("run the following post-deploy commands: %s", postDeployCmds) log.Debug(i18n.G("run the following post-deploy commands: %s", postDeployCmds))
if err := internal.PostCmds(cl, app, postDeployCmds); err != nil { if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
log.Fatalf("attempting to run post deploy commands, saw: %s", err) log.Fatal(i18n.G("attempting to run post deploy commands, saw: %s", err))
} }
} }
if err := app.WriteRecipeVersion(toDeployVersion, false); err != nil { if err := app.WriteRecipeVersion(toDeployVersion, false); err != nil {
log.Fatalf("writing recipe version failed: %s", err) log.Fatal(i18n.G("writing recipe version failed: %s", err))
} }
}, },
} }
@ -254,7 +259,7 @@ func getLatestVersionOrCommit(app app.App) (string, error) {
// validateArgsAndFlags ensures compatible args/flags. // validateArgsAndFlags ensures compatible args/flags.
func validateArgsAndFlags(args []string) error { func validateArgsAndFlags(args []string) error {
if len(args) == 2 && args[1] != "" && internal.Chaos { if len(args) == 2 && args[1] != "" && internal.Chaos {
return fmt.Errorf("cannot use [version] and --chaos together") return errors.New(i18n.G("cannot use [version] and --chaos together"))
} }
return nil return nil
@ -268,7 +273,7 @@ func validateSecrets(cl *dockerClient.Client, app app.App) error {
for _, secStat := range secStats { for _, secStat := range secStats {
if !secStat.CreatedOnRemote { if !secStat.CreatedOnRemote {
return fmt.Errorf("secret not generated: %s", secStat.LocalName) return errors.New(i18n.G("secret not generated: %s", secStat.LocalName))
} }
} }
@ -282,33 +287,33 @@ func getDeployVersion(cliArgs []string, deployMeta stack.DeployMeta, app app.App
if err != nil { if err != nil {
return "", err return "", err
} }
log.Debugf("version: taking chaos version: %s", v) log.Debug(i18n.G("version: taking chaos version: %s", v))
return v, nil return v, nil
} }
// Check if the deploy version is set with a cli argument // Check if the deploy version is set with a cli argument
if len(cliArgs) == 2 && cliArgs[1] != "" { if len(cliArgs) == 2 && cliArgs[1] != "" {
log.Debugf("version: taking version from cli arg: %s", cliArgs[1]) log.Debug(i18n.G("version: taking version from cli arg: %s", cliArgs[1]))
return cliArgs[1], nil return cliArgs[1], nil
} }
// Check if the recipe has a version in the .env file // Check if the recipe has a version in the .env file
if app.Recipe.EnvVersion != "" && !internal.IgnoreEnvVersion { if app.Recipe.EnvVersion != "" && !internal.IgnoreEnvVersion {
if strings.HasSuffix(app.Recipe.EnvVersionRaw, "+U") { if strings.HasSuffix(app.Recipe.EnvVersionRaw, "+U") {
return "", fmt.Errorf("version: can not redeploy chaos version %s", app.Recipe.EnvVersionRaw) return "", errors.New(i18n.G("version: can not redeploy chaos version %s", app.Recipe.EnvVersionRaw))
} }
log.Debugf("version: taking version from .env file: %s", app.Recipe.EnvVersion) log.Debug(i18n.G("version: taking version from .env file: %s", app.Recipe.EnvVersion))
return app.Recipe.EnvVersion, nil return app.Recipe.EnvVersion, nil
} }
// Take deployed version // Take deployed version
if deployMeta.IsDeployed { if deployMeta.IsDeployed {
log.Debugf("version: taking deployed version: %s", deployMeta.Version) log.Debug(i18n.G("version: taking deployed version: %s", deployMeta.Version))
return deployMeta.Version, nil return deployMeta.Version, nil
} }
v, err := getLatestVersionOrCommit(app) v, err := getLatestVersionOrCommit(app)
log.Debugf("version: taking new recipe version: %s", v) log.Debug(i18n.G("version: taking new recipe version: %s", v))
if err != nil { if err != nil {
return "", err return "", err
} }
@ -318,33 +323,33 @@ func getDeployVersion(cliArgs []string, deployMeta stack.DeployMeta, app app.App
func init() { func init() {
AppDeployCommand.Flags().BoolVarP( AppDeployCommand.Flags().BoolVarP(
&internal.Chaos, &internal.Chaos,
"chaos", i18n.G("chaos"),
"C", i18n.G("C"),
false, false,
"ignore uncommitted recipes changes", i18n.G("ignore uncommitted recipes changes"),
) )
AppDeployCommand.Flags().BoolVarP( AppDeployCommand.Flags().BoolVarP(
&internal.Force, &internal.Force,
"force", i18n.G("force"),
"f", i18n.G("f"),
false, false,
"perform action without further prompt", i18n.G("perform action without further prompt"),
) )
AppDeployCommand.Flags().BoolVarP( AppDeployCommand.Flags().BoolVarP(
&internal.NoDomainChecks, &internal.NoDomainChecks,
"no-domain-checks", i18n.G("no-domain-checks"),
"D", i18n.G("D"),
false, false,
"disable public DNS checks", i18n.G("disable public DNS checks"),
) )
AppDeployCommand.Flags().BoolVarP( AppDeployCommand.Flags().BoolVarP(
&internal.DontWaitConverge, &internal.DontWaitConverge,
"no-converge-checks", i18n.G("no-converge-checks"),
"c", i18n.G("c"),
false, false,
"disable converge logic checks", i18n.G("disable converge logic checks"),
) )
} }

View File

@ -7,14 +7,15 @@ 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/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var AppEnvCommand = &cobra.Command{ var AppEnvCommand = &cobra.Command{
Use: "env <domain> [flags]", Use: i18n.G("env <domain> [flags]"),
Aliases: []string{"e"}, Aliases: []string{i18n.G("e")},
Short: "Show app .env values", Short: i18n.G("Show app .env values"),
Example: " abra app env 1312.net", Example: i18n.G(" abra app env 1312.net"),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -37,7 +38,7 @@ var AppEnvCommand = &cobra.Command{
rows = append(rows, []string{k, app.Env[k]}) rows = append(rows, []string{k, app.Env[k]})
} }
overview := formatter.CreateOverview("ENV OVERVIEW", rows) overview := formatter.CreateOverview(i18n.G("ENV OVERVIEW"), rows)
fmt.Println(overview) fmt.Println(overview)
}, },
} }

View File

@ -9,6 +9,7 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/convert" "coopcloud.tech/abra/pkg/upstream/convert"
composetypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types"
@ -19,11 +20,11 @@ import (
) )
var AppLabelsCommand = &cobra.Command{ var AppLabelsCommand = &cobra.Command{
Use: "labels <domain> [flags]", Use: i18n.G("labels <domain> [flags]"),
Aliases: []string{"lb"}, Aliases: []string{i18n.G("lb")},
Short: "Show deployment labels", Short: i18n.G("Show deployment labels"),
Long: "Both local recipe and live deployment labels are shown.", Long: i18n.G("Both local recipe and live deployment labels are shown."),
Example: " abra app labels 1312.net", Example: i18n.G(" abra app labels 1312.net"),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -49,7 +50,7 @@ var AppLabelsCommand = &cobra.Command{
} }
rows := [][]string{ rows := [][]string{
{"DEPLOYED LABELS", "---"}, {i18n.G("DEPLOYED LABELS"), "---"},
} }
remoteLabelKeys := make([]string, 0, len(remoteLabels)) remoteLabelKeys := make([]string, 0, len(remoteLabels))
@ -67,10 +68,10 @@ var AppLabelsCommand = &cobra.Command{
} }
if len(remoteLabelKeys) == 0 { if len(remoteLabelKeys) == 0 {
rows = append(rows, []string{"unknown"}) rows = append(rows, []string{i18n.G("unknown")})
} }
rows = append(rows, []string{"RECIPE LABELS", "---"}) rows = append(rows, []string{i18n.G("RECIPE LABELS"), "---"})
config, err := app.Recipe.GetComposeConfig(app.Env) config, err := app.Recipe.GetComposeConfig(app.Env)
if err != nil { if err != nil {
@ -98,7 +99,7 @@ var AppLabelsCommand = &cobra.Command{
}) })
} }
overview := formatter.CreateOverview("LABELS OVERVIEW", rows) overview := formatter.CreateOverview(i18n.G("LABELS OVERVIEW"), rows)
fmt.Println(overview) fmt.Println(overview)
}, },
} }
@ -131,9 +132,9 @@ func getLabels(cl *dockerClient.Client, stackName string) (map[string]string, er
func init() { func init() {
AppLabelsCommand.Flags().BoolVarP( AppLabelsCommand.Flags().BoolVarP(
&internal.Chaos, &internal.Chaos,
"chaos", i18n.G("chaos"),
"C", i18n.G("C"),
false, false,
"ignore uncommitted recipes changes", i18n.G("ignore uncommitted recipes changes"),
) )
} }

View File

@ -11,6 +11,7 @@ import (
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -39,20 +40,20 @@ type serverStatus struct {
} }
var AppListCommand = &cobra.Command{ var AppListCommand = &cobra.Command{
Use: "list [flags]", Use: i18n.G("list [flags]"),
Aliases: []string{"ls"}, Aliases: []string{i18n.G("ls")},
Short: "List all managed apps", Short: i18n.G("List all managed apps"),
Long: `Generate a report of all managed apps. Long: i18n.G(`Generate a report of all managed apps.
Use "--status/-S" flag to query all servers for the live deployment status.`, Use "--status/-S" flag to query all servers for the live deployment status.`),
Example: ` # list apps of all servers without live status Example: i18n.G(` # list apps of all servers without live status
abra app ls abra app ls
# list apps of a specific server with live status # list apps of a specific server with live status
abra app ls -s 1312.net -S abra app ls -s 1312.net -S
# list apps of all servers which match a specific recipe # list apps of all servers which match a specific recipe
abra app ls -r gitea`, abra app ls -r gitea`),
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
appFiles, err := appPkg.LoadAppFiles(listAppServer) appFiles, err := appPkg.LoadAppFiles(listAppServer)
@ -107,11 +108,11 @@ Use "--status/-S" flag to query all servers for the live deployment status.`,
totalAppsCount++ totalAppsCount++
if status { if status {
status := "unknown" status := i18n.G("unknown")
version := "unknown" version := i18n.G("unknown")
chaos := "unknown" chaos := i18n.G("unknown")
chaosVersion := "unknown" chaosVersion := i18n.G("unknown")
autoUpdate := "unknown" autoUpdate := i18n.G("unknown")
if statusMeta, ok := statuses[app.StackName()]; ok { if statusMeta, ok := statuses[app.StackName()]; ok {
if currentVersion, exists := statusMeta["version"]; exists { if currentVersion, exists := statusMeta["version"]; exists {
if currentVersion != "" { if currentVersion != "" {
@ -142,14 +143,14 @@ Use "--status/-S" flag to query all servers for the live deployment status.`,
appStats.AutoUpdate = autoUpdate appStats.AutoUpdate = autoUpdate
var newUpdates []string var newUpdates []string
if version != "unknown" && chaosVersion == "unknown" { if version != "unknown" && chaos == "false" {
if err := app.Recipe.EnsureExists(); err != nil { if err := app.Recipe.EnsureExists(); err != nil {
log.Fatalf("unable to clone %s: %s", app.Name, err) log.Fatal(i18n.G("unable to clone %s: %s", app.Name, err))
} }
updates, err := app.Recipe.Tags() updates, err := app.Recipe.Tags()
if err != nil { if err != nil {
log.Fatalf("unable to retrieve tags for %s: %s", app.Name, err) log.Fatal(i18n.G("unable to retrieve tags for %s: %s", app.Name, err))
} }
parsedVersion, err := tagcmp.Parse(version) parsedVersion, err := tagcmp.Parse(version)
@ -171,9 +172,9 @@ Use "--status/-S" flag to query all servers for the live deployment status.`,
if len(newUpdates) == 0 { if len(newUpdates) == 0 {
if version == "unknown" { if version == "unknown" {
appStats.Upgrade = "unknown" appStats.Upgrade = i18n.G("unknown")
} else { } else {
appStats.Upgrade = "latest" appStats.Upgrade = i18n.G("latest")
stats.LatestCount++ stats.LatestCount++
} }
} else { } else {
@ -212,14 +213,15 @@ Use "--status/-S" flag to query all servers for the live deployment status.`,
serverStat := allStats[app.Server] serverStat := allStats[app.Server]
headers := []string{"RECIPE", "DOMAIN", "SERVER"} headers := []string{i18n.G("RECIPE"), i18n.G("DOMAIN"), i18n.G("SERVER")}
if status { if status {
headers = append(headers, []string{ headers = append(headers, []string{
"STATUS", i18n.G("STATUS"),
"CHAOS", i18n.G("CHAOS"),
"VERSION", i18n.G("VERSION"),
"UPGRADE", i18n.G("UPGRADE"),
"AUTOUPDATE"}..., i18n.G("AUTOUPDATE"),
}...,
) )
} }
@ -283,22 +285,22 @@ var (
func init() { func init() {
AppListCommand.Flags().BoolVarP( AppListCommand.Flags().BoolVarP(
&status, &status,
"status", i18n.G("status"),
"S", i18n.G("S"),
false, false,
"show app deployment status", i18n.G("show app deployment status"),
) )
AppListCommand.Flags().StringVarP( AppListCommand.Flags().StringVarP(
&recipeFilter, &recipeFilter,
"recipe", i18n.G("recipe"),
"r", i18n.G("r"),
"", "",
"show apps of a specific recipe", i18n.G("show apps of a specific recipe"),
) )
AppListCommand.RegisterFlagCompletionFunc( AppListCommand.RegisterFlagCompletionFunc(
"recipe", i18n.G("recipe"),
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete() return autocomplete.RecipeNameComplete()
}, },
@ -306,22 +308,22 @@ func init() {
AppListCommand.Flags().BoolVarP( AppListCommand.Flags().BoolVarP(
&internal.MachineReadable, &internal.MachineReadable,
"machine", i18n.G("machine"),
"m", i18n.G("m"),
false, false,
"print machine-readable output", i18n.G("print machine-readable output"),
) )
AppListCommand.Flags().StringVarP( AppListCommand.Flags().StringVarP(
&listAppServer, &listAppServer,
"server", i18n.G("server"),
"s", i18n.G("s"),
"", "",
"show apps of a specific server", i18n.G("show apps of a specific server"),
) )
AppListCommand.RegisterFlagCompletionFunc( AppListCommand.RegisterFlagCompletionFunc(
"server", i18n.G("server"),
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.ServerNameComplete() return autocomplete.ServerNameComplete()
}, },

View File

@ -2,12 +2,12 @@ package app
import ( import (
"context" "context"
"fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/logs" "coopcloud.tech/abra/pkg/logs"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
@ -15,9 +15,9 @@ import (
) )
var AppLogsCommand = &cobra.Command{ var AppLogsCommand = &cobra.Command{
Use: "logs <domain> [service] [flags]", Use: i18n.G("logs <domain> [service] [flags]"),
Aliases: []string{"l"}, Aliases: []string{i18n.G("l")},
Short: "Tail app logs", Short: i18n.G("Tail app logs"),
Args: cobra.RangeArgs(1, 2), Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -29,8 +29,7 @@ var AppLogsCommand = &cobra.Command{
case 1: case 1:
app, err := appPkg.Get(args[0]) app, err := appPkg.Get(args[0])
if err != nil { if err != nil {
errMsg := fmt.Sprintf("autocomplete failed: %s", err) return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
return []string{errMsg}, cobra.ShellCompDirectiveError
} }
return autocomplete.ServiceNameComplete(app.Name) return autocomplete.ServiceNameComplete(app.Name)
default: default:
@ -56,7 +55,7 @@ var AppLogsCommand = &cobra.Command{
} }
if !deployMeta.IsDeployed { if !deployMeta.IsDeployed {
log.Fatalf("%s is not deployed?", app.Name) log.Fatal(i18n.G("%s is not deployed?", app.Name))
} }
var serviceNames []string var serviceNames []string
@ -91,17 +90,17 @@ var (
func init() { func init() {
AppLogsCommand.Flags().BoolVarP( AppLogsCommand.Flags().BoolVarP(
&stdErr, &stdErr,
"stderr", i18n.G("stderr"),
"s", i18n.G("s"),
false, false,
"only tail stderr", i18n.G("only tail stderr"),
) )
AppLogsCommand.Flags().StringVarP( AppLogsCommand.Flags().StringVarP(
&sinceLogs, &sinceLogs,
"since", i18n.G("since"),
"S", i18n.G("S"),
"", "",
"tail logs since YYYY-MM-DDTHH:MM:SSZ", i18n.G("tail logs since YYYY-MM-DDTHH:MM:SSZ"),
) )
} }

View File

@ -1,6 +1,7 @@
package app package app
import ( import (
"errors"
"fmt" "fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
@ -10,6 +11,7 @@ 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/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
@ -19,7 +21,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var appNewDescription = `Creates a new app from a default recipe. var appNewDescription = i18n.G(`Creates a new app from a default recipe.
This new app configuration is stored in your $ABRA_DIR directory under the This new app configuration is stored in your $ABRA_DIR directory under the
appropriate server. appropriate server.
@ -39,12 +41,12 @@ store them somewhere safe.
You can use the "--pass/-P" to store these generated passwords locally in a 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 = &cobra.Command{ var AppNewCommand = &cobra.Command{
Use: "new [recipe] [version] [flags]", Use: i18n.G("new [recipe] [version] [flags]"),
Aliases: []string{"n"}, Aliases: []string{i18n.G("n")},
Short: "Create a new app", Short: i18n.G("Create a new app"),
Long: appNewDescription, Long: appNewDescription,
Args: cobra.RangeArgs(0, 2), Args: cobra.RangeArgs(0, 2),
ValidArgsFunction: func( ValidArgsFunction: func(
@ -65,7 +67,7 @@ var AppNewCommand = &cobra.Command{
recipe := internal.ValidateRecipe(args, cmd.Name()) recipe := internal.ValidateRecipe(args, cmd.Name())
if len(args) == 2 && internal.Chaos { if len(args) == 2 && internal.Chaos {
log.Fatal("cannot use [version] and --chaos together") log.Fatal(i18n.G("cannot use [version] and --chaos together"))
} }
var recipeVersion string var recipeVersion string
@ -113,7 +115,7 @@ var AppNewCommand = &cobra.Command{
if recipeVersion == "" { if recipeVersion == "" {
head, err := recipe.Head() head, err := recipe.Head()
if err != nil { if err != nil {
log.Fatalf("failed to retrieve latest commit for %s: %s", recipe.Name, err) log.Fatal(i18n.G("failed to retrieve latest commit for %s: %s", recipe.Name, err))
} }
recipeVersion = formatter.SmallSHA(head.String()) recipeVersion = formatter.SmallSHA(head.String())
@ -130,7 +132,7 @@ var AppNewCommand = &cobra.Command{
} }
sanitisedAppName := appPkg.SanitiseAppName(appDomain) sanitisedAppName := appPkg.SanitiseAppName(appDomain)
log.Debugf("%s sanitised as %s for new app", appDomain, sanitisedAppName) log.Debug(i18n.G("%s sanitised as %s for new app", appDomain, sanitisedAppName))
if err := appPkg.TemplateAppEnvSample( if err := appPkg.TemplateAppEnvSample(
recipe, recipe,
@ -182,7 +184,7 @@ var AppNewCommand = &cobra.Command{
log.Fatal(err) log.Fatal(err)
} }
headers := []string{"NAME", "VALUE"} headers := []string{i18n.G("NAME"), i18n.G("VALUE")}
secretsTable.Headers(headers...) secretsTable.Headers(headers...)
for name, val := range appSecrets { for name, val := range appSecrets {
@ -194,7 +196,7 @@ var AppNewCommand = &cobra.Command{
newAppServer = "local" newAppServer = "local"
} }
log.Infof("%s created (version: %s)", appDomain, recipeVersion) log.Info(i18n.G("%s created (version: %s)", appDomain, recipeVersion))
if len(appSecrets) > 0 { if len(appSecrets) > 0 {
rows := [][]string{} rows := [][]string{}
@ -202,15 +204,15 @@ var AppNewCommand = &cobra.Command{
rows = append(rows, []string{k, v}) rows = append(rows, []string{k, v})
} }
overview := formatter.CreateOverview("SECRETS OVERVIEW", rows) overview := formatter.CreateOverview(i18n.G("SECRETS OVERVIEW"), rows)
fmt.Println(overview) fmt.Println(overview)
log.Warnf( log.Warn(i18n.G(
"secrets are %s shown again, please save them %s", "secrets are %s shown again, please save them %s",
formatter.BoldUnderlineStyle.Render("NOT"), formatter.BoldUnderlineStyle.Render("NOT"),
formatter.BoldUnderlineStyle.Render("NOW"), formatter.BoldUnderlineStyle.Render("NOW"),
) ))
} }
app, err := app.Get(appDomain) app, err := app.Get(appDomain)
@ -219,7 +221,7 @@ var AppNewCommand = &cobra.Command{
} }
if err := app.WriteRecipeVersion(recipeVersion, false); err != nil { if err := app.WriteRecipeVersion(recipeVersion, false); err != nil {
log.Fatalf("writing recipe version failed: %s", err) log.Fatal(i18n.G("writing recipe version failed: %s", err))
} }
}, },
} }
@ -231,7 +233,7 @@ type AppSecrets map[string]string
func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secret, sanitisedAppName string) (AppSecrets, error) { func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secret, sanitisedAppName string) (AppSecrets, error) {
// NOTE(d1): trim to match app.StackName() implementation // NOTE(d1): trim to match app.StackName() implementation
if len(sanitisedAppName) > config.MAX_SANITISED_APP_NAME_LENGTH { if len(sanitisedAppName) > config.MAX_SANITISED_APP_NAME_LENGTH {
log.Debugf("trimming %s to %s to avoid runtime limits", sanitisedAppName, sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH]) log.Debug(i18n.G("trimming %s to %s to avoid runtime limits", sanitisedAppName, sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH]))
sanitisedAppName = sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH] sanitisedAppName = sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH]
} }
@ -261,7 +263,7 @@ func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secr
func ensureDomainFlag(recipe recipePkg.Recipe, server string) error { func ensureDomainFlag(recipe recipePkg.Recipe, server string) error {
if appDomain == "" && !internal.NoInput { if appDomain == "" && !internal.NoInput {
prompt := &survey.Input{ prompt := &survey.Input{
Message: "Specify app domain", Message: i18n.G("Specify app domain"),
Default: fmt.Sprintf("%s.%s", recipe.Name, server), Default: fmt.Sprintf("%s.%s", recipe.Name, server),
} }
if err := survey.AskOne(prompt, &appDomain); err != nil { if err := survey.AskOne(prompt, &appDomain); err != nil {
@ -270,7 +272,7 @@ func ensureDomainFlag(recipe recipePkg.Recipe, server string) error {
} }
if appDomain == "" { if appDomain == "" {
return fmt.Errorf("no domain provided") return fmt.Errorf(i18n.G("no domain provided"))
} }
return nil return nil
@ -279,13 +281,13 @@ func ensureDomainFlag(recipe recipePkg.Recipe, server string) error {
// promptForSecrets asks if we should generate secrets for a new app. // promptForSecrets asks if we should generate secrets for a new app.
func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret) error { func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret) error {
if len(secretsConfig) == 0 { if len(secretsConfig) == 0 {
log.Debugf("%s has no secrets to generate, skipping...", recipeName) log.Debug(i18n.G("%s has no secrets to generate, skipping...", recipeName))
return nil return nil
} }
if !generateSecrets && !internal.NoInput { if !generateSecrets && !internal.NoInput {
prompt := &survey.Confirm{ prompt := &survey.Confirm{
Message: "Generate app secrets?", Message: i18n.G("Generate app secrets?"),
} }
if err := survey.AskOne(prompt, &generateSecrets); err != nil { if err := survey.AskOne(prompt, &generateSecrets); err != nil {
return err return err
@ -304,13 +306,13 @@ func ensureServerFlag() error {
if len(servers) == 1 { if len(servers) == 1 {
newAppServer = servers[0] newAppServer = servers[0]
log.Infof("single server detected, choosing %s automatically", newAppServer) log.Info(i18n.G("single server detected, choosing %s automatically", newAppServer))
return nil return nil
} }
if newAppServer == "" && !internal.NoInput { if newAppServer == "" && !internal.NoInput {
prompt := &survey.Select{ prompt := &survey.Select{
Message: "Select app server:", Message: i18n.G("Select app server:"),
Options: servers, Options: servers,
} }
if err := survey.AskOne(prompt, &newAppServer); err != nil { if err := survey.AskOne(prompt, &newAppServer); err != nil {
@ -319,7 +321,7 @@ func ensureServerFlag() error {
} }
if newAppServer == "" { if newAppServer == "" {
return fmt.Errorf("no server provided") return errors.New(i18n.G("no server provided"))
} }
return nil return nil
@ -335,14 +337,14 @@ var (
func init() { func init() {
AppNewCommand.Flags().StringVarP( AppNewCommand.Flags().StringVarP(
&newAppServer, &newAppServer,
"server", i18n.G("server"),
"s", i18n.G("s"),
"", "",
"specify server for new app", i18n.G("specify server for new app"),
) )
AppNewCommand.RegisterFlagCompletionFunc( AppNewCommand.RegisterFlagCompletionFunc(
"server", i18n.G("server"),
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.ServerNameComplete() return autocomplete.ServerNameComplete()
}, },
@ -350,34 +352,34 @@ func init() {
AppNewCommand.Flags().StringVarP( AppNewCommand.Flags().StringVarP(
&appDomain, &appDomain,
"domain", i18n.G("domain"),
"D", i18n.G("D"),
"", "",
"domain name for app", i18n.G("domain name for app"),
) )
AppNewCommand.Flags().BoolVarP( AppNewCommand.Flags().BoolVarP(
&saveInPass, &saveInPass,
"pass", i18n.G("pass"),
"p", i18n.G("p"),
false, false,
"store secrets in a local pass store", i18n.G("store secrets in a local pass store"),
) )
AppNewCommand.Flags().BoolVarP( AppNewCommand.Flags().BoolVarP(
&generateSecrets, &generateSecrets,
"secrets", i18n.G("secrets"),
"S", i18n.G("S"),
false, false,
"automatically generate secrets", i18n.G("automatically generate secrets"),
) )
AppNewCommand.Flags().BoolVarP( AppNewCommand.Flags().BoolVarP(
&internal.Chaos, &internal.Chaos,
"chaos", i18n.G("chaos"),
"C", i18n.G("C"),
false, false,
"ignore uncommitted recipes changes", i18n.G("ignore uncommitted recipes changes"),
) )
} }

View File

@ -13,6 +13,7 @@ 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/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
abraService "coopcloud.tech/abra/pkg/service" abraService "coopcloud.tech/abra/pkg/service"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
@ -24,9 +25,9 @@ import (
) )
var AppPsCommand = &cobra.Command{ var AppPsCommand = &cobra.Command{
Use: "ps <domain> [flags]", Use: i18n.G("ps <domain> [flags]"),
Aliases: []string{"p"}, Aliases: []string{i18n.G("p")},
Short: "Check app deployment status", Short: i18n.G("Check app deployment status"),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -52,7 +53,7 @@ var AppPsCommand = &cobra.Command{
} }
if !deployMeta.IsDeployed { if !deployMeta.IsDeployed {
log.Fatalf("%s is not deployed?", app.Name) log.Fatal(i18n.G("%s is not deployed?", app.Name))
} }
chaosVersion := config.CHAOS_DEFAULT chaosVersion := config.CHAOS_DEFAULT
@ -115,11 +116,11 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
"version": deployedVersion, "version": deployedVersion,
"chaos": chaosVersion, "chaos": chaosVersion,
"service": service.Name, "service": service.Name,
"image": "unknown", "image": i18n.G("unknown"),
"created": "unknown", "created": i18n.G("unknown"),
"status": "unknown", "status": i18n.G("unknown"),
"state": "unknown", "state": i18n.G("unknown"),
"ports": "unknown", "ports": i18n.G("unknown"),
} }
} else { } else {
container := containers[0] container := containers[0]
@ -161,7 +162,7 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
if internal.MachineReadable { if internal.MachineReadable {
rendered, 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(i18n.G("unable to convert to JSON: %s", err))
} }
fmt.Println(string(rendered)) fmt.Println(string(rendered))
@ -175,11 +176,11 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
} }
headers := []string{ headers := []string{
"SERVICE", i18n.G("SERVICE"),
"STATUS", i18n.G("STATUS"),
"IMAGE", i18n.G("IMAGE"),
"VERSION", i18n.G("VERSION"),
"CHAOS", i18n.G("CHAOS"),
} }
table. table.
@ -194,17 +195,17 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
func init() { func init() {
AppPsCommand.Flags().BoolVarP( AppPsCommand.Flags().BoolVarP(
&internal.MachineReadable, &internal.MachineReadable,
"machine", i18n.G("machine"),
"m", i18n.G("m"),
false, false,
"print machine-readable output", i18n.G("print machine-readable output"),
) )
AppPsCommand.Flags().BoolVarP( AppPsCommand.Flags().BoolVarP(
&internal.Chaos, &internal.Chaos,
"chaos", i18n.G("chaos"),
"C", i18n.G("C"),
false, false,
"ignore uncommitted recipes changes", i18n.G("ignore uncommitted recipes changes"),
) )
} }

View File

@ -2,24 +2,24 @@ package app
import ( import (
"context" "context"
"fmt"
"os" "os"
"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/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
stack "coopcloud.tech/abra/pkg/upstream/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/spf13/cobra" "github.com/spf13/cobra"
) )
var AppRemoveCommand = &cobra.Command{ var AppRemoveCommand = &cobra.Command{
Use: "remove <domain> [flags]", Use: i18n.G("remove <domain> [flags]"),
Aliases: []string{"rm"}, Aliases: []string{i18n.G("rm")},
Short: "Remove all app data, locally and remotely", Short: i18n.G("Remove all app data, locally and remotely"),
Long: `Remove everything related to an app which is already undeployed. Long: i18n.G(`Remove everything related to an app which is already undeployed.
By default, it will prompt for confirmation before proceeding. All secrets, By default, it will prompt for confirmation before proceeding. All secrets,
volumes and the local app env file will be deleted. volumes and the local app env file will be deleted.
@ -34,8 +34,8 @@ Please note, if you delete the local app env file without removing volumes and
secrets first, Abra will *not* be able to help you remove them afterwards. 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.`),
Example: " abra app remove 1312.net", Example: i18n.G(" abra app remove 1312.net"),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -47,16 +47,16 @@ flag.`,
app := internal.ValidateApp(args) app := internal.ValidateApp(args)
if !internal.Force && !internal.NoInput { if !internal.Force && !internal.NoInput {
log.Warnf("ALERTA ALERTA: deleting %s data and config (local/remote)", app.Name) log.Warn(i18n.G("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: i18n.G("are you sure?")}
if err := survey.AskOne(prompt, &response); err != nil { if err := survey.AskOne(prompt, &response); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !response { if !response {
log.Fatal("aborting as requested") log.Fatal(i18n.G("aborting as requested"))
} }
} }
@ -70,7 +70,7 @@ flag.`,
log.Fatal(err) log.Fatal(err)
} }
if deployMeta.IsDeployed { if deployMeta.IsDeployed {
log.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name) log.Fatal(i18n.G("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name))
} }
fs, err := app.Filters(false, false) fs, err := app.Filters(false, false)
@ -78,6 +78,22 @@ flag.`,
log.Fatal(err) log.Fatal(err)
} }
configs, err := client.GetConfigs(cl, context.Background(), app.Server, fs)
if err != nil {
log.Fatal(err)
}
configNames := client.GetConfigNames(configs)
if len(configNames) > 0 {
if err := client.RemoveConfigs(cl, context.Background(), configNames, internal.Force); err != nil {
log.Fatal(i18n.G("removing configs failed: %s", err))
}
log.Info(i18n.G("%d config(s) removed successfully", len(configNames)))
} else {
log.Info(i18n.G("no configs to remove"))
}
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: fs}) secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: fs})
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -97,10 +113,10 @@ flag.`,
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
log.Info(fmt.Sprintf("secret: %s removed", name)) log.Info(i18n.G("secret: %s removed", name))
} }
} else { } else {
log.Info("no secrets to remove") log.Info(i18n.G("no secrets to remove"))
} }
fs, err = app.Filters(false, true) fs, err = app.Filters(false, true)
@ -117,28 +133,28 @@ flag.`,
if len(volumeNames) > 0 { if len(volumeNames) > 0 {
err := client.RemoveVolumes(cl, context.Background(), volumeNames, internal.Force, 5) err := client.RemoveVolumes(cl, context.Background(), volumeNames, internal.Force, 5)
if err != nil { if err != nil {
log.Fatalf("removing volumes failed: %s", err) log.Fatal(i18n.G("removing volumes failed: %s", err))
} }
log.Infof("%d volumes removed successfully", len(volumeNames)) log.Info(i18n.G("%d volume(s) removed successfully", len(volumeNames)))
} else { } else {
log.Info("no volumes to remove") log.Info(i18n.G("no volumes to remove"))
} }
if err = os.Remove(app.Path); err != nil { if err = os.Remove(app.Path); err != nil {
log.Fatal(err) log.Fatal(err)
} }
log.Info(fmt.Sprintf("file: %s removed", app.Path)) log.Info(i18n.G("file: %s removed", app.Path))
}, },
} }
func init() { func init() {
AppRemoveCommand.Flags().BoolVarP( AppRemoveCommand.Flags().BoolVarP(
&internal.Force, &internal.Force,
"force", i18n.G("force"),
"f", i18n.G("f"),
false, false,
"perform action without further prompt", i18n.G("perform action without further prompt"),
) )
} }

View File

@ -8,6 +8,7 @@ import (
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/ui" "coopcloud.tech/abra/pkg/ui"
upstream "coopcloud.tech/abra/pkg/upstream/service" upstream "coopcloud.tech/abra/pkg/upstream/service"
@ -17,19 +18,19 @@ import (
) )
var AppRestartCommand = &cobra.Command{ var AppRestartCommand = &cobra.Command{
Use: "restart <domain> [[service] | --all-services] [flags]", Use: i18n.G("restart <domain> [[service] | --all-services] [flags]"),
Aliases: []string{"re"}, Aliases: []string{i18n.G("re")},
Short: "Restart an app", Short: i18n.G("Restart an app"),
Long: `This command restarts services within a deployed app. Long: i18n.G(`This command restarts services within a deployed app.
Run "abra app ps <domain>" to see a list of service names. Run "abra app ps <domain>" to see a list of service names.
Pass "--all-services/-a" to restart all services.`, Pass "--all-services/-a" to restart all services.`),
Example: ` # restart a single app service Example: i18n.G(` # restart a single app service
abra app restart 1312.net app abra app restart 1312.net app
# restart all app services # restart all app services
abra app restart 1312.net -a`, abra app restart 1312.net -a`),
Args: cobra.RangeArgs(1, 2), Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -60,11 +61,11 @@ Pass "--all-services/-a" to restart all services.`,
} }
if serviceName == "" && !allServices { if serviceName == "" && !allServices {
log.Fatal("missing [service]") log.Fatal(i18n.G("missing [service]"))
} }
if serviceName != "" && allServices { if serviceName != "" && allServices {
log.Fatal("cannot use [service] and --all-services/-a together") log.Fatal(i18n.G("cannot use [service] and --all-services/-a together"))
} }
var serviceNames []string var serviceNames []string
@ -89,7 +90,7 @@ Pass "--all-services/-a" to restart all services.`,
} }
if !deployMeta.IsDeployed { if !deployMeta.IsDeployed {
log.Fatalf("%s is not deployed?", app.Name) log.Fatal(i18n.G("%s is not deployed?", app.Name))
} }
for _, serviceName := range serviceNames { for _, serviceName := range serviceNames {
@ -104,7 +105,7 @@ Pass "--all-services/-a" to restart all services.`,
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("attempting to scale %s to 0", stackServiceName) log.Debug(i18n.G("attempting to scale %s to 0", stackServiceName))
if err := upstream.RunServiceScale(context.Background(), cl, stackServiceName, 0); err != nil { if err := upstream.RunServiceScale(context.Background(), cl, stackServiceName, 0); err != nil {
log.Fatal(err) log.Fatal(err)
@ -128,8 +129,8 @@ Pass "--all-services/-a" to restart all services.`,
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("%s has been scaled to 0", stackServiceName) log.Debug(i18n.G("%s has been scaled to 0", stackServiceName))
log.Debugf("attempting to scale %s to 1", stackServiceName) log.Debug(i18n.G("attempting to scale %s to 1", stackServiceName))
if err := upstream.RunServiceScale(context.Background(), cl, stackServiceName, 1); err != nil { if err := upstream.RunServiceScale(context.Background(), cl, stackServiceName, 1); err != nil {
log.Fatal(err) log.Fatal(err)
@ -139,8 +140,8 @@ Pass "--all-services/-a" to restart all services.`,
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("%s has been scaled to 1", stackServiceName) log.Debug(i18n.G("%s has been scaled to 1", stackServiceName))
log.Infof("%s service successfully restarted", serviceName) log.Info(i18n.G("%s service successfully restarted", serviceName))
} }
}, },
} }
@ -150,16 +151,16 @@ var allServices bool
func init() { func init() {
AppRestartCommand.Flags().BoolVarP( AppRestartCommand.Flags().BoolVarP(
&internal.Chaos, &internal.Chaos,
"chaos", i18n.G("chaos"),
"C", i18n.G("C"),
false, false,
"ignore uncommitted recipes changes", i18n.G("ignore uncommitted recipes changes"),
) )
AppRestartCommand.Flags().BoolVarP( AppRestartCommand.Flags().BoolVarP(
&allServices, &allServices,
"all-services", i18n.G("all-services"),
"a", i18n.G("a"),
false, false,
"restart all services", i18n.G("restart all services"),
) )
} }

View File

@ -7,17 +7,18 @@ 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/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var AppRestoreCommand = &cobra.Command{ var AppRestoreCommand = &cobra.Command{
Use: "restore <domain> [flags]", Use: i18n.G("restore <domain> [flags]"),
Aliases: []string{"rs"}, Aliases: []string{i18n.G("rs")},
Short: "Restore a snapshot", Short: i18n.G("Restore a snapshot"),
Long: `Snapshots are restored while apps are deployed. Long: i18n.G(`Snapshots are restored while apps are deployed.
Some restore scenarios may require service / app restarts.`, Some restore scenarios may require service / app restarts.`),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -48,34 +49,34 @@ Some restore scenarios may require service / app restarts.`,
} }
if snapshot != "" { if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot) log.Debug(i18n.G("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.Debug(i18n.G("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 internal.NoInput { if internal.NoInput {
log.Debugf("including NONINTERACTIVE=%v in backupbot exec invocation", internal.NoInput) log.Debug(i18n.G("including NONINTERACTIVE=%v in backupbot exec invocation", internal.NoInput))
execEnv = append(execEnv, fmt.Sprintf("NONINTERACTIVE=%v", internal.NoInput)) execEnv = append(execEnv, fmt.Sprintf("NONINTERACTIVE=%v", internal.NoInput))
} }
if len(volumes) > 0 { if len(volumes) > 0 {
allVolumes := strings.Join(volumes, ",") allVolumes := strings.Join(volumes, ",")
log.Debugf("including VOLUMES=%s in backupbot exec invocation", allVolumes) log.Debug(i18n.G("including VOLUMES=%s in backupbot exec invocation", allVolumes))
execEnv = append(execEnv, fmt.Sprintf("VOLUMES=%s", allVolumes)) execEnv = append(execEnv, fmt.Sprintf("VOLUMES=%s", allVolumes))
} }
if len(services) > 0 { if len(services) > 0 {
allServices := strings.Join(services, ",") allServices := strings.Join(services, ",")
log.Debugf("including CONTAINER=%s in backupbot exec invocation", allServices) log.Debug(i18n.G("including CONTAINER=%s in backupbot exec invocation", allServices))
execEnv = append(execEnv, fmt.Sprintf("CONTAINER=%s", allServices)) execEnv = append(execEnv, fmt.Sprintf("CONTAINER=%s", allServices))
} }
if hooks { if hooks {
log.Debugf("including NO_COMMANDS=%v in backupbot exec invocation", false) log.Debug(i18n.G("including NO_COMMANDS=%v in backupbot exec invocation", false))
execEnv = append(execEnv, fmt.Sprintf("NO_COMMANDS=%v", false)) execEnv = append(execEnv, fmt.Sprintf("NO_COMMANDS=%v", false))
} }
@ -95,41 +96,41 @@ var (
func init() { func init() {
AppRestoreCommand.Flags().StringVarP( AppRestoreCommand.Flags().StringVarP(
&targetPath, &targetPath,
"target", i18n.G("target"),
"t", i18n.G("t"),
"/", "/",
"target path", i18n.G("target path"),
) )
AppRestoreCommand.Flags().StringArrayVarP( AppRestoreCommand.Flags().StringArrayVarP(
&services, &services,
"services", i18n.G("services"),
"s", i18n.G("s"),
[]string{}, []string{},
"restore specific services", i18n.G("restore specific services"),
) )
AppRestoreCommand.Flags().StringArrayVarP( AppRestoreCommand.Flags().StringArrayVarP(
&volumes, &volumes,
"volumes", i18n.G("volumes"),
"v", i18n.G("v"),
[]string{}, []string{},
"restore specific volumes", i18n.G("restore specific volumes"),
) )
AppRestoreCommand.Flags().BoolVarP( AppRestoreCommand.Flags().BoolVarP(
&hooks, &hooks,
"hooks", i18n.G("hooks"),
"H", i18n.G("H"),
false, false,
"enable pre/post-hook command execution", i18n.G("enable pre/post-hook command execution"),
) )
AppRestoreCommand.Flags().BoolVarP( AppRestoreCommand.Flags().BoolVarP(
&internal.Chaos, &internal.Chaos,
"chaos", i18n.G("chaos"),
"C", i18n.G("C"),
false, false,
"ignore uncommitted recipes changes", i18n.G("ignore uncommitted recipes changes"),
) )
} }

View File

@ -1,7 +1,7 @@
package app package app
import ( import (
"fmt" "errors"
"coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
@ -9,6 +9,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/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
@ -21,10 +22,10 @@ import (
) )
var AppRollbackCommand = &cobra.Command{ var AppRollbackCommand = &cobra.Command{
Use: "rollback <domain> [version] [flags]", Use: i18n.G("rollback <domain> [version] [flags]"),
Aliases: []string{"rl"}, Aliases: []string{i18n.G("rl")},
Short: "Roll an app back to a previous version", Short: i18n.G("Roll an app back to a previous version"),
Long: `This command rolls an app back to a previous version. Long: i18n.G(`This command rolls an app back to a previous version.
Unlike "abra app deploy", chaos operations are not supported here. Only recipe Unlike "abra app deploy", chaos operations are not supported here. Only recipe
versions are supported values for "[version]". versions are supported values for "[version]".
@ -37,12 +38,12 @@ are available. The live deployment version is the "source of truth" in this
case. The stored .env version is not consulted. case. The stored .env version is not consulted.
A downgrade can be destructive, please ensure you have a copy of your app data A downgrade can be destructive, please ensure you have a copy of your app data
beforehand. See "abra app backup" for more.`, beforehand. See "abra app backup" for more.`),
Example: ` # standard rollback Example: i18n.G(` # standard rollback
abra app rollback 1312.net abra app rollback 1312.net
# rollback to specific version # rollback to specific version
abra app rollback 1312.net 2.0.0+1.2.3`, abra app rollback 1312.net 2.0.0+1.2.3`),
Args: cobra.RangeArgs(1, 2), Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -54,8 +55,7 @@ beforehand. See "abra app backup" for more.`,
case 1: case 1:
app, err := appPkg.Get(args[0]) app, err := appPkg.Get(args[0])
if err != nil { if err != nil {
errMsg := fmt.Sprintf("autocomplete failed: %s", err) return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
return []string{errMsg}, cobra.ShellCompDirectiveError
} }
return autocomplete.RecipeVersionComplete(app.Recipe.Name) return autocomplete.RecipeVersionComplete(app.Recipe.Name)
default: default:
@ -117,7 +117,7 @@ beforehand. See "abra app backup" for more.`,
} }
if !downgradeAvailable { if !downgradeAvailable {
log.Info("no available downgrades") log.Info(i18n.G("no available downgrades"))
return return
} }
} }
@ -139,10 +139,10 @@ beforehand. See "abra app backup" for more.`,
} }
if chosenDowngrade == "" { if chosenDowngrade == "" {
log.Fatal("unknown deployed version, unable to downgrade") log.Fatal(i18n.G("unknown deployed version, unable to downgrade"))
} }
log.Debugf("choosing %s as version to rollback", chosenDowngrade) log.Debug(i18n.G("choosing %s as version to rollback", chosenDowngrade))
if _, err := app.Recipe.EnsureVersion(chosenDowngrade); err != nil { if _, err := app.Recipe.EnsureVersion(chosenDowngrade); err != nil {
log.Fatal(err) log.Fatal(err)
@ -199,7 +199,7 @@ beforehand. See "abra app backup" for more.`,
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout) log.Debug(i18n.G("set waiting timeout to %d second(s)", stack.WaitTimeout))
serviceNames, err := appPkg.GetAppServiceNames(app.Name) serviceNames, err := appPkg.GetAppServiceNames(app.Name)
if err != nil { if err != nil {
@ -224,7 +224,7 @@ beforehand. See "abra app backup" for more.`,
} }
if err := app.WriteRecipeVersion(chosenDowngrade, false); err != nil { if err := app.WriteRecipeVersion(chosenDowngrade, false); err != nil {
log.Fatalf("writing recipe version failed: %s", err) log.Fatal(i18n.G("writing recipe version failed: %s", err))
} }
}, },
} }
@ -235,12 +235,12 @@ func chooseDowngrade(
deployMeta stack.DeployMeta, deployMeta stack.DeployMeta,
chosenDowngrade *string, chosenDowngrade *string,
) error { ) error {
msg := fmt.Sprintf("please select a downgrade (version: %s):", deployMeta.Version) msg := i18n.G("please select a downgrade (version: %s):", deployMeta.Version)
if deployMeta.IsChaos { if deployMeta.IsChaos {
chaosVersion := formatter.BoldDirtyDefault(deployMeta.ChaosVersion) chaosVersion := formatter.BoldDirtyDefault(deployMeta.ChaosVersion)
msg = fmt.Sprintf( msg = i18n.G(
"please select a downgrade (version: %s, chaos: %s):", "please select a downgrade (version: %s, chaos: %s):",
deployMeta.Version, deployMeta.Version,
chaosVersion, chaosVersion,
@ -267,21 +267,21 @@ func validateDowngradeVersionArg(
) error { ) error {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version) parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil { if err != nil {
return fmt.Errorf("current deployment '%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name) return errors.New(i18n.G("current deployment '%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name))
} }
parsedSpecificVersion, err := tagcmp.Parse(specificVersion) parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil { if err != nil {
return fmt.Errorf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name) return errors.New(i18n.G("'%s' is not a known version for %s", specificVersion, app.Recipe.Name))
} }
if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) && if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) &&
!parsedSpecificVersion.Equals(parsedDeployedVersion) { !parsedSpecificVersion.Equals(parsedDeployedVersion) {
return fmt.Errorf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion) return errors.New(i18n.G("%s is not a downgrade for %s?", deployMeta.Version, specificVersion))
} }
if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force { if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
return fmt.Errorf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion) return errors.New(i18n.G("%s is not a downgrade for %s?", deployMeta.Version, specificVersion))
} }
return nil return nil
@ -320,24 +320,25 @@ func ensureDowngradesAvailable(
func init() { func init() {
AppRollbackCommand.Flags().BoolVarP( AppRollbackCommand.Flags().BoolVarP(
&internal.Force, &internal.Force,
"force", i18n.G("force"),
"f", i18n.G("f"),
false, false,
"perform action without further prompt", i18n.G("perform action without further prompt"),
) )
AppRollbackCommand.Flags().BoolVarP( AppRollbackCommand.Flags().BoolVarP(
&internal.NoDomainChecks, &internal.NoDomainChecks,
"no-domain-checks", i18n.G("no-domain-checks"),
"D", i18n.G("D"),
false, false,
"disable public DNS checks", i18n.G("disable public DNS checks"),
) )
AppRollbackCommand.Flags().BoolVarP( AppRollbackCommand.Flags().BoolVarP(
&internal.DontWaitConverge, "no-converge-checks", &internal.DontWaitConverge,
"c", i18n.G("no-converge-checks"),
i18n.G("c"),
false, false,
"disable converge logic checks", i18n.G("disable converge logic checks"),
) )
} }

View File

@ -8,6 +8,7 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
containerPkg "coopcloud.tech/abra/pkg/container" containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/container" "coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
@ -17,17 +18,17 @@ import (
) )
var AppRunCommand = &cobra.Command{ var AppRunCommand = &cobra.Command{
Use: "run <domain> <service> <cmd> [[args] [flags] | [flags] -- [args]]", Use: i18n.G("run <domain> <service> <cmd> [[args] [flags] | [flags] -- [args]]"),
Aliases: []string{"r"}, Aliases: []string{i18n.G("r")},
Short: "Run a command inside a service container", Short: i18n.G("Run a command inside a service container"),
Example: ` # run <cmd> with args/flags Example: i18n.G(` # run <cmd> with args/flags
abra app run 1312.net app -- ls -lha abra app run 1312.net app -- ls -lha
# run <cmd> without args/flags # run <cmd> without args/flags
abra app run 1312.net app bash --user nobody abra app run 1312.net app bash --user nobody
# run <cmd> with both kinds of args/flags # run <cmd> with both kinds of args/flags
abra app run 1312.net app --user nobody -- ls -lha`, abra app run 1312.net app --user nobody -- ls -lha`),
Args: cobra.MinimumNArgs(3), Args: cobra.MinimumNArgs(3),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -98,17 +99,17 @@ var (
func init() { func init() {
AppRunCommand.Flags().BoolVarP(&noTTY, AppRunCommand.Flags().BoolVarP(&noTTY,
"no-tty", i18n.G("no-tty"),
"t", i18n.G("t"),
false, false,
"do not request a TTY", i18n.G("do not request a TTY"),
) )
AppRunCommand.Flags().StringVarP( AppRunCommand.Flags().StringVarP(
&runAsUser, &runAsUser,
"user", i18n.G("user"),
"u", i18n.G("u"),
"", "",
"run command as user", i18n.G("run command as user"),
) )
} }

View File

@ -12,6 +12,7 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
@ -20,9 +21,9 @@ import (
) )
var AppSecretGenerateCommand = &cobra.Command{ var AppSecretGenerateCommand = &cobra.Command{
Use: "generate <domain> [[secret] [version] | --all] [flags]", Use: i18n.G("generate <domain> [[secret] [version] | --all] [flags]"),
Aliases: []string{"g"}, Aliases: []string{i18n.G("g")},
Short: "Generate secrets", Short: i18n.G("Generate secrets"),
Args: cobra.RangeArgs(1, 3), Args: cobra.RangeArgs(1, 3),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -34,8 +35,7 @@ var AppSecretGenerateCommand = &cobra.Command{
case 1: case 1:
app, err := appPkg.Get(args[0]) app, err := appPkg.Get(args[0])
if err != nil { if err != nil {
errMsg := fmt.Sprintf("autocomplete failed: %s", err) return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
return []string{errMsg}, cobra.ShellCompDirectiveError
} }
return autocomplete.SecretComplete(app.Recipe.Name) return autocomplete.SecretComplete(app.Recipe.Name)
default: default:
@ -50,11 +50,11 @@ var AppSecretGenerateCommand = &cobra.Command{
} }
if len(args) <= 2 && !generateAllSecrets { if len(args) <= 2 && !generateAllSecrets {
log.Fatal("missing arguments [secret]/[version] or '--all'") log.Fatal(i18n.G("missing arguments [secret]/[version] or '--all'"))
} }
if len(args) > 2 && generateAllSecrets { if len(args) > 2 && generateAllSecrets {
log.Fatal("cannot use '[secret] [version]' and '--all' together") log.Fatal(i18n.G("cannot use '[secret] [version]' and '--all' together"))
} }
composeFiles, err := app.Recipe.GetComposeFiles(app.Env) composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
@ -72,7 +72,7 @@ var AppSecretGenerateCommand = &cobra.Command{
secretVersion := args[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.Fatal(i18n.G("%s doesn't exist in the env config?", secretName))
} }
s.Version = secretVersion s.Version = secretVersion
secrets = map[string]secret.Secret{ secrets = map[string]secret.Secret{
@ -99,11 +99,11 @@ var AppSecretGenerateCommand = &cobra.Command{
} }
if len(secretVals) == 0 { if len(secretVals) == 0 {
log.Warn("no secrets generated") log.Warn(i18n.G("no secrets generated"))
os.Exit(1) os.Exit(1)
} }
headers := []string{"NAME", "VALUE"} headers := []string{i18n.G("NAME"), i18n.G("VALUE")}
table, err := formatter.CreateTable() table, err := formatter.CreateTable()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -121,7 +121,7 @@ var AppSecretGenerateCommand = &cobra.Command{
if internal.MachineReadable { if internal.MachineReadable {
out, err := formatter.ToJSON(headers, rows) out, err := formatter.ToJSON(headers, rows)
if err != nil { if err != nil {
log.Fatal("unable to render to JSON: %s", err) log.Fatal(i18n.G("unable to render to JSON: %s", err))
} }
fmt.Println(out) fmt.Println(out)
return return
@ -131,23 +131,31 @@ var AppSecretGenerateCommand = &cobra.Command{
log.Fatal(err) log.Fatal(err)
} }
log.Warnf( log.Warn(i18n.G(
"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(i18n.G("NOT")),
formatter.BoldStyle.Render("NOW"), formatter.BoldStyle.Render(i18n.G("NOW")),
) ))
}, },
} }
var AppSecretInsertCommand = &cobra.Command{ var AppSecretInsertCommand = &cobra.Command{
Use: "insert <domain> <secret> <version> <data> [flags]", Use: i18n.G("insert <domain> <secret> <version> <data> [flags]"),
Aliases: []string{"i"}, Aliases: []string{i18n.G("i")},
Short: "Insert secret", Short: i18n.G("Insert secret"),
Long: `This command inserts a secret into an app environment. Long: i18n.G(`This command inserts a secret into an app environment.
Arbitrary secret insertion is not supported. Secrets that are inserted must
match those configured in the recipe beforehand.
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/-S" for more).`, (see "abra app new --secrets/-S" for more).`),
Example: i18n.G(` # insert regular secret
abra app secret insert 1312.net my_secret v1 mySuperSecret
# insert secret as file
abra app secret insert 1312.net my_secret v1 secret.txt -f`),
Args: cobra.MinimumNArgs(4), Args: cobra.MinimumNArgs(4),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -159,8 +167,7 @@ environment. Typically, you can let Abra generate them for you on app creation
case 1: case 1:
app, err := appPkg.Get(args[0]) app, err := appPkg.Get(args[0])
if err != nil { if err != nil {
errMsg := fmt.Sprintf("autocomplete failed: %s", err) return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
return []string{errMsg}, cobra.ShellCompDirectiveError
} }
return autocomplete.SecretComplete(app.Recipe.Name) return autocomplete.SecretComplete(app.Recipe.Name)
default: default:
@ -183,10 +190,30 @@ environment. Typically, you can let Abra generate them for you on app creation
version := args[2] version := args[2]
data := args[3] data := args[3]
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil {
log.Fatal(err)
}
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
if err != nil {
log.Fatal(err)
}
var isRecipeSecret bool
for secretName := range secrets {
if secretName == name {
isRecipeSecret = true
}
}
if !isRecipeSecret {
log.Fatal(i18n.G("no secret %s available for recipe %s?", name, app.Recipe.Name))
}
if insertFromFile { 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.Fatal(i18n.G("reading secret from file: %s", err))
} }
data = string(raw) data = string(raw)
} }
@ -200,7 +227,7 @@ environment. Typically, you can let Abra generate them for you on app creation
log.Fatal(err) log.Fatal(err)
} }
log.Infof("%s successfully stored on server", secretName) log.Info(i18n.G("%s successfully stored on server", secretName))
if storeInPass { 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 {
@ -216,23 +243,28 @@ func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string
return err return err
} }
log.Infof("deleted %s successfully from server", secretName) log.Info(i18n.G("deleted %s successfully from server", secretName))
if removeFromPass { 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
} }
log.Infof("deleted %s successfully from local pass store", secretName) log.Info(i18n.G("deleted %s successfully from local pass store", secretName))
} }
return nil return nil
} }
var AppSecretRmCommand = &cobra.Command{ var AppSecretRmCommand = &cobra.Command{
Use: "remove <domain> [[secret] | --all] [flags]", Use: i18n.G("remove <domain> [[secret] | --all] [flags]"),
Aliases: []string{"rm"}, Aliases: []string{i18n.G("rm")},
Short: "Remove a secret", Short: i18n.G("Remove a secret"),
Long: i18n.G(`This command removes a secret from an app environment.
Arbitrary secret removal is not supported. Secrets that are removed must
match those configured in the recipe beforehand.`),
Example: i18n.G(" abra app secret rm 1312.net oauth_key"),
Args: cobra.RangeArgs(1, 2), Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -245,8 +277,7 @@ var AppSecretRmCommand = &cobra.Command{
if !rmAllSecrets { if !rmAllSecrets {
app, err := appPkg.Get(args[0]) app, err := appPkg.Get(args[0])
if err != nil { if err != nil {
errMsg := fmt.Sprintf("autocomplete failed: %s", err) return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
return []string{errMsg}, cobra.ShellCompDirectiveError
} }
return autocomplete.SecretComplete(app.Recipe.Name) return autocomplete.SecretComplete(app.Recipe.Name)
} }
@ -273,11 +304,11 @@ var AppSecretRmCommand = &cobra.Command{
} }
if len(args) == 2 && rmAllSecrets { if len(args) == 2 && rmAllSecrets {
log.Fatal("cannot use [secret] and --all/-a together") log.Fatal(i18n.G("cannot use [secret] and --all/-a together"))
} }
if len(args) != 2 && !rmAllSecrets { if len(args) != 2 && !rmAllSecrets {
log.Fatal("no secret(s) specified?") log.Fatal(i18n.G("no secret(s) specified?"))
} }
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
@ -328,19 +359,19 @@ var AppSecretRmCommand = &cobra.Command{
} }
if !match && secretToRm != "" { if !match && secretToRm != "" {
log.Fatalf("%s doesn't exist on server?", secretToRm) log.Fatal(i18n.G("%s doesn't exist on server?", secretToRm))
} }
if !match { if !match {
log.Fatal("no secrets to remove?") log.Fatal(i18n.G("no secrets to remove?"))
} }
}, },
} }
var AppSecretLsCommand = &cobra.Command{ var AppSecretLsCommand = &cobra.Command{
Use: "list <domain>", Use: i18n.G("list <domain>"),
Aliases: []string{"ls"}, Aliases: []string{i18n.G("ls")},
Short: "List all secrets", Short: i18n.G("List all secrets"),
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -360,7 +391,7 @@ var AppSecretLsCommand = &cobra.Command{
log.Fatal(err) log.Fatal(err)
} }
headers := []string{"NAME", "VERSION", "GENERATED NAME", "CREATED ON SERVER"} headers := []string{i18n.G("NAME"), i18n.G("VERSION"), i18n.G("GENERATED NAME"), i18n.G("CREATED ON SERVER")}
table, err := formatter.CreateTable() table, err := formatter.CreateTable()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -390,7 +421,7 @@ var AppSecretLsCommand = &cobra.Command{
if internal.MachineReadable { if internal.MachineReadable {
out, err := formatter.ToJSON(headers, rows) out, err := formatter.ToJSON(headers, rows)
if err != nil { if err != nil {
log.Fatal("unable to render to JSON: %s", err) log.Fatal(i18n.G("unable to render to JSON: %s", err))
} }
fmt.Println(out) fmt.Println(out)
return return
@ -403,14 +434,14 @@ var AppSecretLsCommand = &cobra.Command{
return return
} }
log.Warnf("no secrets stored for %s", app.Name) log.Warn(i18n.G("no secrets stored for %s", app.Name))
}, },
} }
var AppSecretCommand = &cobra.Command{ var AppSecretCommand = &cobra.Command{
Use: "secret [cmd] [args] [flags]", Use: i18n.G("secret [cmd] [args] [flags]"),
Aliases: []string{"s"}, Aliases: []string{i18n.G("s")},
Short: "Manage app secrets", Short: i18n.G("Manage app secrets"),
} }
var ( var (
@ -425,105 +456,105 @@ var (
func init() { func init() {
AppSecretGenerateCommand.Flags().BoolVarP( AppSecretGenerateCommand.Flags().BoolVarP(
&internal.MachineReadable, &internal.MachineReadable,
"machine", i18n.G("machine"),
"m", i18n.G("m"),
false, false,
"print machine-readable output", i18n.G("print machine-readable output"),
) )
AppSecretGenerateCommand.Flags().BoolVarP( AppSecretGenerateCommand.Flags().BoolVarP(
&storeInPass, &storeInPass,
"pass", i18n.G("pass"),
"p", i18n.G("p"),
false, false,
"store generated secrets in a local pass store", i18n.G("store generated secrets in a local pass store"),
) )
AppSecretGenerateCommand.Flags().BoolVarP( AppSecretGenerateCommand.Flags().BoolVarP(
&internal.Chaos, &internal.Chaos,
"chaos", i18n.G("chaos"),
"C", i18n.G("C"),
false, false,
"ignore uncommitted recipes changes", i18n.G("ignore uncommitted recipes changes"),
) )
AppSecretGenerateCommand.Flags().BoolVarP( AppSecretGenerateCommand.Flags().BoolVarP(
&generateAllSecrets, &generateAllSecrets,
"all", i18n.G("all"),
"a", i18n.G("a"),
false, false,
"generate all secrets", i18n.G("generate all secrets"),
) )
AppSecretInsertCommand.Flags().BoolVarP( AppSecretInsertCommand.Flags().BoolVarP(
&storeInPass, &storeInPass,
"pass", i18n.G("pass"),
"p", i18n.G("p"),
false, false,
"store generated secrets in a local pass store", i18n.G("store generated secrets in a local pass store"),
) )
AppSecretInsertCommand.Flags().BoolVarP( AppSecretInsertCommand.Flags().BoolVarP(
&insertFromFile, &insertFromFile,
"file", i18n.G("file"),
"f", i18n.G("f"),
false, false,
"treat input as a file", i18n.G("treat input as a file"),
) )
AppSecretInsertCommand.Flags().BoolVarP( AppSecretInsertCommand.Flags().BoolVarP(
&trimInput, &trimInput,
"trim", i18n.G("trim"),
"t", i18n.G("t"),
false, false,
"trim input", i18n.G("trim input"),
) )
AppSecretInsertCommand.Flags().BoolVarP( AppSecretInsertCommand.Flags().BoolVarP(
&internal.Chaos, &internal.Chaos,
"chaos", i18n.G("chaos"),
"C", i18n.G("C"),
false, false,
"ignore uncommitted recipes changes", i18n.G("ignore uncommitted recipes changes"),
) )
AppSecretRmCommand.Flags().BoolVarP( AppSecretRmCommand.Flags().BoolVarP(
&rmAllSecrets, &rmAllSecrets,
"all", i18n.G("all"),
"a", i18n.G("a"),
false, false,
"remove all secrets", i18n.G("remove all secrets"),
) )
AppSecretRmCommand.Flags().BoolVarP( AppSecretRmCommand.Flags().BoolVarP(
&removeFromPass, &removeFromPass,
"pass", i18n.G("pass"),
"p", i18n.G("p"),
false, false,
"remove generated secrets from a local pass store", i18n.G("remove generated secrets from a local pass store"),
) )
AppSecretRmCommand.Flags().BoolVarP( AppSecretRmCommand.Flags().BoolVarP(
&internal.Chaos, &internal.Chaos,
"chaos", i18n.G("chaos"),
"C", i18n.G("C"),
false, false,
"ignore uncommitted recipes changes", i18n.G("ignore uncommitted recipes changes"),
) )
AppSecretLsCommand.Flags().BoolVarP( AppSecretLsCommand.Flags().BoolVarP(
&internal.Chaos, &internal.Chaos,
"chaos", i18n.G("chaos"),
"C", i18n.G("C"),
false, false,
"ignore uncommitted recipes changes", i18n.G("ignore uncommitted recipes changes"),
) )
AppSecretLsCommand.Flags().BoolVarP( AppSecretLsCommand.Flags().BoolVarP(
&internal.MachineReadable, &internal.MachineReadable,
"machine", i18n.G("machine"),
"m", i18n.G("m"),
false, false,
"print machine-readable output", i18n.G("print machine-readable output"),
) )
} }

View File

@ -9,6 +9,7 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/service" "coopcloud.tech/abra/pkg/service"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
@ -17,9 +18,9 @@ import (
) )
var AppServicesCommand = &cobra.Command{ var AppServicesCommand = &cobra.Command{
Use: "services <domain> [flags]", Use: i18n.G("services <domain> [flags]"),
Aliases: []string{"sr"}, Aliases: []string{i18n.G("sr")},
Short: "Display all services of an app", Short: i18n.G("Display all services of an app"),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -45,7 +46,7 @@ var AppServicesCommand = &cobra.Command{
} }
if !deployMeta.IsDeployed { if !deployMeta.IsDeployed {
log.Fatalf("%s is not deployed?", app.Name) log.Fatal(i18n.G("%s is not deployed?", app.Name))
} }
filters, err := app.Filters(true, true) filters, err := app.Filters(true, true)
@ -63,7 +64,7 @@ var AppServicesCommand = &cobra.Command{
log.Fatal(err) log.Fatal(err)
} }
headers := []string{"SERVICE (SHORT)", "SERVICE (LONG)"} headers := []string{i18n.G("SERVICE (SHORT)"), i18n.G("SERVICE (LONG)")}
table.Headers(headers...) table.Headers(headers...)
var rows [][]string var rows [][]string

View File

@ -10,6 +10,7 @@ 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/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
@ -18,15 +19,15 @@ import (
) )
var AppUndeployCommand = &cobra.Command{ var AppUndeployCommand = &cobra.Command{
Use: "undeploy <domain> [flags]", Use: i18n.G("undeploy <domain> [flags]"),
Aliases: []string{"un"}, Aliases: []string{i18n.G("un")},
Short: "Undeploy an app", Short: i18n.G("Undeploy an app"),
Long: `This does not destroy any application data. Long: i18n.G(`This does not destroy any application data.
However, you should remain vigilant, as your swarm installation will consider However, you should remain vigilant, as your swarm installation will consider
any previously attached volumes as eligible for pruning once undeployed. any previously attached volumes as eligible for pruning once undeployed.
Passing "--prune/-p" does not remove those volumes.`, Passing "--prune/-p" does not remove those volumes.`),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -38,12 +39,16 @@ Passing "--prune/-p" does not remove those volumes.`,
app := internal.ValidateApp(args) app := internal.ValidateApp(args)
stackName := app.StackName() stackName := app.StackName()
if err := app.Recipe.EnsureExists(); 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)
} }
log.Debugf("checking whether %s is already deployed", stackName) log.Debug(i18n.G("checking whether %s is already deployed", stackName))
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName) deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil { if err != nil {
@ -51,7 +56,7 @@ Passing "--prune/-p" does not remove those volumes.`,
} }
if !deployMeta.IsDeployed { if !deployMeta.IsDeployed {
log.Fatalf("%s is not deployed?", app.Name) log.Fatal(i18n.G("%s is not deployed?", app.Name))
} }
if err := internal.DeployOverview( if err := internal.DeployOverview(
@ -80,7 +85,7 @@ Passing "--prune/-p" does not remove those volumes.`,
log.Fatal(err) log.Fatal(err)
} }
log.Info("initialising undeploy") log.Info(i18n.G("initialising undeploy"))
rmOpts := stack.Remove{ rmOpts := stack.Remove{
Namespaces: []string{stackName}, Namespaces: []string{stackName},
@ -96,10 +101,10 @@ Passing "--prune/-p" does not remove those volumes.`,
} }
} }
log.Info("undeploy succeeded 🟢") log.Info(i18n.G("undeploy succeeded 🟢"))
if err := app.WriteRecipeVersion(deployMeta.Version, false); err != nil { if err := app.WriteRecipeVersion(deployMeta.Version, false); err != nil {
log.Fatalf("writing recipe version failed: %s", err) log.Fatal(i18n.G("writing recipe version failed: %s", err))
} }
}, },
} }
@ -120,14 +125,14 @@ func pruneApp(cl *dockerClient.Client, app appPkg.App) error {
} }
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed) cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
log.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed) log.Info(i18n.G("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed))
nr, err := cl.NetworksPrune(ctx, pruneFilters) nr, err := cl.NetworksPrune(ctx, pruneFilters)
if err != nil { if err != nil {
return err return err
} }
log.Infof("networks pruned: %d", len(nr.NetworksDeleted)) log.Info(i18n.G("networks pruned: %d", len(nr.NetworksDeleted)))
ir, err := cl.ImagesPrune(ctx, pruneFilters) ir, err := cl.ImagesPrune(ctx, pruneFilters)
if err != nil { if err != nil {
@ -135,7 +140,7 @@ func pruneApp(cl *dockerClient.Client, app appPkg.App) error {
} }
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed) imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
log.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed) log.Info(i18n.G("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed))
return nil return nil
} }
@ -147,9 +152,9 @@ var (
func init() { func init() {
AppUndeployCommand.Flags().BoolVarP( AppUndeployCommand.Flags().BoolVarP(
&prune, &prune,
"prune", i18n.G("prune"),
"p", i18n.G("p"),
false, false,
"prune unused containers, networks, and dangling images", i18n.G("prune unused containers, networks, and dangling images"),
) )
} }

View File

@ -2,6 +2,7 @@ package app
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
@ -13,6 +14,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/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
@ -24,10 +26,10 @@ import (
) )
var AppUpgradeCommand = &cobra.Command{ var AppUpgradeCommand = &cobra.Command{
Use: "upgrade <domain> [version] [flags]", Use: i18n.G("upgrade <domain> [version] [flags]"),
Aliases: []string{"up"}, Aliases: []string{i18n.G("up")},
Short: "Upgrade an app", Short: i18n.G("Upgrade an app"),
Long: `Upgrade an app. Long: i18n.G(`Upgrade an app.
Unlike "abra app deploy", chaos operations are not supported here. Only recipe Unlike "abra app deploy", chaos operations are not supported here. Only recipe
versions are supported values for "[version]". versions are supported values for "[version]".
@ -40,7 +42,7 @@ are available. The live deployment version is the "source of truth" in this
case. The stored .env version is not consulted. case. The stored .env version is not consulted.
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. See "abra app backup" for more.`, beforehand. See "abra app backup" for more.`),
Args: cobra.RangeArgs(1, 2), Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -53,8 +55,7 @@ beforehand. See "abra app backup" for more.`,
case 1: case 1:
app, err := appPkg.Get(args[0]) app, err := appPkg.Get(args[0])
if err != nil { if err != nil {
errMsg := fmt.Sprintf("autocomplete failed: %s", err) return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
return []string{errMsg}, cobra.ShellCompDirectiveError
} }
return autocomplete.RecipeVersionComplete(app.Recipe.Name) return autocomplete.RecipeVersionComplete(app.Recipe.Name)
default: default:
@ -117,13 +118,13 @@ beforehand. See "abra app backup" for more.`,
} }
if deployMeta.Version != config.UNKNOWN_DEFAULT && chosenUpgrade == "" { if deployMeta.Version != config.UNKNOWN_DEFAULT && chosenUpgrade == "" {
upgradeAvailable, err := ensureUpgradesAvailable(versions, &availableUpgrades, deployMeta) upgradeAvailable, err := ensureUpgradesAvailable(app, versions, &availableUpgrades, deployMeta)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !upgradeAvailable { if !upgradeAvailable {
log.Info("no available upgrades") log.Info(i18n.G("no available upgrades"))
return return
} }
} }
@ -145,10 +146,10 @@ beforehand. See "abra app backup" for more.`,
} }
if chosenUpgrade == "" { if chosenUpgrade == "" {
log.Fatal("unknown deployed version, unable to upgrade") log.Fatal(i18n.G("unknown deployed version, unable to upgrade"))
} }
log.Debugf("choosing %s as version to upgrade", chosenUpgrade) log.Debug(i18n.G("choosing %s as version to upgrade", chosenUpgrade))
// Get the release notes before checking out the new version in the // Get the release notes before checking out the new version in the
// recipe. This enables us to get release notes, that were added after // recipe. This enables us to get release notes, that were added after
@ -204,7 +205,7 @@ beforehand. See "abra app backup" for more.`,
for _, envVar := range envVars { for _, envVar := range envVars {
if !envVar.Present { if !envVar.Present {
upgradeWarnMessages = append(upgradeWarnMessages, upgradeWarnMessages = append(upgradeWarnMessages,
fmt.Sprintf("%s missing from %s.env", envVar.Name, app.Domain), i18n.G("%s missing from %s.env", envVar.Name, app.Domain),
) )
} }
} }
@ -236,7 +237,7 @@ beforehand. See "abra app backup" for more.`,
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout) log.Debug(i18n.G("set waiting timeout to %d second(s)", stack.WaitTimeout))
serviceNames, err := appPkg.GetAppServiceNames(app.Name) serviceNames, err := appPkg.GetAppServiceNames(app.Name)
if err != nil { if err != nil {
@ -262,15 +263,15 @@ beforehand. See "abra app backup" for more.`,
postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"] postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"]
if ok && !internal.DontWaitConverge { if ok && !internal.DontWaitConverge {
log.Debugf("run the following post-deploy commands: %s", postDeployCmds) log.Debug(i18n.G("run the following post-deploy commands: %s", postDeployCmds))
if err := internal.PostCmds(cl, app, postDeployCmds); err != nil { if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
log.Fatalf("attempting to run post deploy commands, saw: %s", err) log.Fatal(i18n.G("attempting to run post deploy commands, saw: %s", err))
} }
} }
if err := app.WriteRecipeVersion(chosenUpgrade, false); err != nil { if err := app.WriteRecipeVersion(chosenUpgrade, false); err != nil {
log.Fatalf("writing recipe version failed: %s", err) log.Fatal(i18n.G("writing recipe version failed: %s", err))
} }
}, },
} }
@ -281,12 +282,12 @@ func chooseUpgrade(
deployMeta stack.DeployMeta, deployMeta stack.DeployMeta,
chosenUpgrade *string, chosenUpgrade *string,
) error { ) error {
msg := fmt.Sprintf("please select an upgrade (version: %s):", deployMeta.Version) msg := i18n.G("please select an upgrade (version: %s):", deployMeta.Version)
if deployMeta.IsChaos { if deployMeta.IsChaos {
chaosVersion := formatter.BoldDirtyDefault(deployMeta.ChaosVersion) chaosVersion := formatter.BoldDirtyDefault(deployMeta.ChaosVersion)
msg = fmt.Sprintf( msg = i18n.G(
"please select an upgrade (version: %s, chaos: %s):", "please select an upgrade (version: %s, chaos: %s):",
deployMeta.Version, deployMeta.Version,
chaosVersion, chaosVersion,
@ -314,18 +315,18 @@ func getReleaseNotes(
) error { ) error {
parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade) parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade)
if err != nil { if err != nil {
return err return errors.New(i18n.G("parsing chosen upgrade version failed: %s", err))
} }
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version) parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil { if err != nil {
return err return errors.New(i18n.G("parsing deployment version failed: %s", err))
} }
for _, version := range internal.SortVersionsDesc(versions) { for _, version := range internal.SortVersionsDesc(versions) {
parsedVersion, err := tagcmp.Parse(version) parsedVersion, err := tagcmp.Parse(version)
if err != nil { if err != nil {
return err return errors.New(i18n.G("parsing recipe version failed: %s", err))
} }
if parsedVersion.IsGreaterThan(parsedDeployedVersion) && if parsedVersion.IsGreaterThan(parsedDeployedVersion) &&
@ -351,19 +352,20 @@ func getReleaseNotes(
// ensureUpgradesAvailable ensures that there are available upgrades. // ensureUpgradesAvailable ensures that there are available upgrades.
func ensureUpgradesAvailable( func ensureUpgradesAvailable(
app app.App,
versions []string, versions []string,
availableUpgrades *[]string, availableUpgrades *[]string,
deployMeta stack.DeployMeta, deployMeta stack.DeployMeta,
) (bool, error) { ) (bool, error) {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version) parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil { if err != nil {
return false, err return false, errors.New(i18n.G("parsing deployed version failed: %s", err))
} }
for _, version := range versions { for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version) parsedVersion, err := tagcmp.Parse(version)
if err != nil { if err != nil {
return false, err return false, errors.New(i18n.G("parsing recipe version failed: %s", err))
} }
if parsedVersion.IsGreaterThan(parsedDeployedVersion) && if parsedVersion.IsGreaterThan(parsedDeployedVersion) &&
@ -387,21 +389,21 @@ func validateUpgradeVersionArg(
) error { ) error {
parsedSpecificVersion, err := tagcmp.Parse(specificVersion) parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil { if err != nil {
return fmt.Errorf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name) return errors.New(i18n.G("'%s' is not a known version for %s", specificVersion, app.Recipe.Name))
} }
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version) parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil { if err != nil {
return fmt.Errorf("'%s' is not a known version", deployMeta.Version) return errors.New(i18n.G("'%s' is not a known version", deployMeta.Version))
} }
if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) && if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) &&
!parsedSpecificVersion.Equals(parsedDeployedVersion) { !parsedSpecificVersion.Equals(parsedDeployedVersion) {
return fmt.Errorf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion) return errors.New(i18n.G("%s is not an upgrade for %s?", deployMeta.Version, specificVersion))
} }
if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force { if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
return fmt.Errorf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion) return errors.New(i18n.G("%s is not an upgrade for %s?", deployMeta.Version, specificVersion))
} }
return nil return nil
@ -410,7 +412,7 @@ func validateUpgradeVersionArg(
// ensureDeployed ensures the app is deployed and if so, returns deployment // ensureDeployed ensures the app is deployed and if so, returns deployment
// meta info. // meta info.
func ensureDeployed(cl *dockerClient.Client, app app.App) (stack.DeployMeta, error) { func ensureDeployed(cl *dockerClient.Client, app app.App) (stack.DeployMeta, error) {
log.Debugf("checking whether %s is already deployed", app.StackName()) log.Debug(i18n.G("checking whether %s is already deployed", app.StackName()))
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName()) deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil { if err != nil {
@ -418,7 +420,7 @@ func ensureDeployed(cl *dockerClient.Client, app app.App) (stack.DeployMeta, err
} }
if !deployMeta.IsDeployed { if !deployMeta.IsDeployed {
return stack.DeployMeta{}, fmt.Errorf("%s is not deployed?", app.Name) return stack.DeployMeta{}, errors.New(i18n.G("%s is not deployed?", app.Name))
} }
return deployMeta, nil return deployMeta, nil
@ -429,32 +431,33 @@ var showReleaseNotes bool
func init() { func init() {
AppUpgradeCommand.Flags().BoolVarP( AppUpgradeCommand.Flags().BoolVarP(
&internal.Force, &internal.Force,
"force", i18n.G("force"),
"f", i18n.G("f"),
false, false,
"perform action without further prompt", i18n.G("perform action without further prompt"),
) )
AppUpgradeCommand.Flags().BoolVarP( AppUpgradeCommand.Flags().BoolVarP(
&internal.NoDomainChecks, &internal.NoDomainChecks,
"no-domain-checks", i18n.G("no-domain-checks"),
"D", i18n.G("D"),
false, false,
"disable public DNS checks", i18n.G("disable public DNS checks"),
) )
AppUpgradeCommand.Flags().BoolVarP( AppUpgradeCommand.Flags().BoolVarP(
&internal.DontWaitConverge, "no-converge-checks", &internal.DontWaitConverge,
"c", i18n.G("no-converge-checks"),
i18n.G("c"),
false, false,
"disable converge logic checks", i18n.G("disable converge logic checks"),
) )
AppUpgradeCommand.Flags().BoolVarP( AppUpgradeCommand.Flags().BoolVarP(
&showReleaseNotes, &showReleaseNotes,
"releasenotes", i18n.G("releasenotes"),
"r", i18n.G("r"),
false, false,
"only show release notes", i18n.G("only show release notes"),
) )
} }

View File

@ -2,11 +2,13 @@ package app
import ( import (
"context" "context"
"fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"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"
@ -14,9 +16,9 @@ import (
) )
var AppVolumeListCommand = &cobra.Command{ var AppVolumeListCommand = &cobra.Command{
Use: "list <domain> [flags]", Use: i18n.G("list <domain> [flags]"),
Aliases: []string{"ls"}, Aliases: []string{i18n.G("ls")},
Short: "List volumes associated with an app", Short: i18n.G("List volumes associated with an app"),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -42,7 +44,7 @@ var AppVolumeListCommand = &cobra.Command{
log.Fatal(err) log.Fatal(err)
} }
headers := []string{"NAME", "ON SERVER"} headers := []string{i18n.G("NAME"), i18n.G("ON SERVER")}
table, err := formatter.CreateTable() table, err := formatter.CreateTable()
if err != nil { if err != nil {
@ -66,14 +68,14 @@ var AppVolumeListCommand = &cobra.Command{
return return
} }
log.Warnf("no volumes created for %s", app.Name) log.Warn(i18n.G("no volumes created for %s", app.Name))
}, },
} }
var AppVolumeRemoveCommand = &cobra.Command{ var AppVolumeRemoveCommand = &cobra.Command{
Use: "remove <domain> [flags]", Use: i18n.G("remove <domain> [volume] [flags]"),
Short: "Remove volume(s) associated with an app", Short: i18n.G("Remove volume(s) associated with an app"),
Long: `Remove volumes associated with an app. Long: i18n.G(`Remove volumes associated with an app.
The app in question must be undeployed before you try to remove volumes. See The app in question must be undeployed before you try to remove volumes. See
"abra app undeploy <domain>" for more. "abra app undeploy <domain>" for more.
@ -82,8 +84,13 @@ 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.`),
Aliases: []string{"rm"}, Example: i18n.G(` # delete volumes interactively
abra app volume rm 1312.net
# delete specific volume
abra app volume rm 1312.net my_volume`),
Aliases: []string{i18n.G("rm")},
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -94,6 +101,11 @@ Passing "--force/-f" will select all volumes for removal. Be careful.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args) app := internal.ValidateApp(args)
var volumeToDelete string
if len(args) == 2 {
volumeToDelete = args[1]
}
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -105,7 +117,7 @@ Passing "--force/-f" will select all volumes for removal. Be careful.`,
} }
if deployMeta.IsDeployed { if deployMeta.IsDeployed {
log.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name) log.Fatal(i18n.G("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name))
} }
filters, err := app.Filters(false, true) filters, err := app.Filters(false, true)
@ -119,11 +131,35 @@ Passing "--force/-f" will select all volumes for removal. Be careful.`,
} }
volumeNames := client.GetVolumeNames(volumeList) volumeNames := client.GetVolumeNames(volumeList)
if volumeToDelete != "" {
var exactMatch bool
fullVolumeToDeleteName := fmt.Sprintf("%s_%s", app.StackName(), volumeToDelete)
for _, volName := range volumeNames {
if volName == fullVolumeToDeleteName {
exactMatch = true
}
}
if !exactMatch {
log.Fatal(i18n.G("unable to remove volume: no volume with name '%s'?", volumeToDelete))
}
err := client.RemoveVolumes(cl, context.Background(), []string{fullVolumeToDeleteName}, internal.Force, 5)
if err != nil {
log.Fatal(i18n.G("removing volume %s failed: %s", volumeToDelete, err))
}
log.Info(i18n.G("volume %s removed successfully", volumeToDelete))
return
}
var volumesToRemove []string var volumesToRemove []string
if !internal.Force && !internal.NoInput { if !internal.Force && !internal.NoInput {
volumesPrompt := &survey.MultiSelect{ volumesPrompt := &survey.MultiSelect{
Message: "which volumes do you want to remove?", Message: i18n.G("which volumes do you want to remove?"),
Help: "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled", Help: i18n.G("'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled"),
VimMode: true, VimMode: true,
Options: volumeNames, Options: volumeNames,
Default: volumeNames, Default: volumeNames,
@ -140,28 +176,28 @@ Passing "--force/-f" will select all volumes for removal. Be careful.`,
if len(volumesToRemove) > 0 { if len(volumesToRemove) > 0 {
err := client.RemoveVolumes(cl, context.Background(), volumesToRemove, internal.Force, 5) err := client.RemoveVolumes(cl, context.Background(), volumesToRemove, internal.Force, 5)
if err != nil { if err != nil {
log.Fatalf("removing volumes failed: %s", err) log.Fatal(i18n.G("removing volumes failed: %s", err))
} }
log.Infof("%d volumes removed successfully", len(volumesToRemove)) log.Info(i18n.G("%d volumes removed successfully", len(volumesToRemove)))
} else { } else {
log.Info("no volumes removed") log.Info(i18n.G("no volumes removed"))
} }
}, },
} }
var AppVolumeCommand = &cobra.Command{ var AppVolumeCommand = &cobra.Command{
Use: "volume [cmd] [args] [flags]", Use: i18n.G("volume [cmd] [args] [flags]"),
Aliases: []string{"vl"}, Aliases: []string{i18n.G("vl")},
Short: "Manage app volumes", Short: i18n.G("Manage app volumes"),
} }
func init() { func init() {
AppVolumeRemoveCommand.Flags().BoolVarP( AppVolumeRemoveCommand.Flags().BoolVarP(
&internal.Force, &internal.Force,
"force", i18n.G("force"),
"f", i18n.G("f"),
false, false,
"perform action without further prompt", i18n.G("perform action without further prompt"),
) )
} }

View File

@ -13,6 +13,7 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/i18n"
"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"
@ -20,10 +21,10 @@ import (
) )
var CatalogueGenerateCommand = &cobra.Command{ var CatalogueGenerateCommand = &cobra.Command{
Use: "generate [recipe] [flags]", Use: i18n.G("generate [recipe] [flags]"),
Aliases: []string{"g"}, Aliases: []string{i18n.G("g")},
Short: "Generate the recipe catalogue", Short: i18n.G("Generate the recipe catalogue"),
Long: `Generate a new copy of the recipe catalogue. Long: i18n.G(`Generate a new copy of the recipe catalogue.
N.B. this command **will** wipe local unstaged changes from your local recipes N.B. this command **will** wipe local unstaged changes from your local recipes
if present. "--chaos/-C" on this command refers to the catalogue repository if present. "--chaos/-C" on this command refers to the catalogue repository
@ -39,7 +40,7 @@ use those details.
Push your new release to git.coopcloud.tech with "--publish/-p". 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.`),
Args: cobra.RangeArgs(0, 1), Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -85,7 +86,7 @@ keys configured on your account.`,
var warnings []string var warnings []string
catl := make(recipe.RecipeCatalogue) catl := make(recipe.RecipeCatalogue)
catlBar := formatter.CreateProgressbar(barLength, "collecting catalogue metadata") catlBar := formatter.CreateProgressbar(barLength, i18n.G("collecting catalogue metadata"))
for _, recipeMeta := range repos { for _, recipeMeta := range repos {
if recipeName != "" && recipeName != recipeMeta.Name { if recipeName != "" && recipeName != recipeMeta.Name {
if !internal.Debug { if !internal.Debug {
@ -171,7 +172,7 @@ keys configured on your account.`,
} }
} }
log.Infof("generated recipe catalogue: %s", config.RECIPES_JSON) log.Info(i18n.G("generated recipe catalogue: %s", config.RECIPES_JSON))
cataloguePath := path.Join(config.ABRA_DIR, "catalogue") cataloguePath := path.Join(config.ABRA_DIR, "catalogue")
if publishChanges { if publishChanges {
@ -183,11 +184,11 @@ keys configured on your account.`,
if isClean { if isClean {
if !internal.Dry { if !internal.Dry {
log.Fatalf("no changes discovered in %s, nothing to publish?", cataloguePath) log.Fatal(i18n.G("no changes discovered in %s, nothing to publish?", cataloguePath))
} }
} }
msg := "chore: publish new catalogue release changes" msg := i18n.G("chore: publish new catalogue release changes")
if err := gitPkg.Commit(cataloguePath, msg, internal.Dry); err != nil { if err := gitPkg.Commit(cataloguePath, msg, internal.Dry); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -219,19 +220,19 @@ keys configured on your account.`,
if !internal.Dry && publishChanges { 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.Info(i18n.G("new changes published: %s", url))
} }
if internal.Dry { if internal.Dry {
log.Info("dry run: no changes published") log.Info(i18n.G("dry run: no changes published"))
} }
}, },
} }
// CatalogueCommand defines the `abra catalogue` command and sub-commands. // CatalogueCommand defines the `abra catalogue` command and sub-commands.
var CatalogueCommand = &cobra.Command{ var CatalogueCommand = &cobra.Command{
Use: "catalogue [cmd] [args] [flags]", Use: i18n.G("catalogue [cmd] [args] [flags]"),
Short: "Manage the recipe catalogue", Short: i18n.G("Manage the recipe catalogue"),
Aliases: []string{"c"}, Aliases: []string{"c"},
} }
@ -243,33 +244,33 @@ var (
func init() { func init() {
CatalogueGenerateCommand.Flags().BoolVarP( CatalogueGenerateCommand.Flags().BoolVarP(
&publishChanges, &publishChanges,
"publish", i18n.G("publish"),
"p", i18n.G("p"),
false, false,
"publish changes to git.coopcloud.tech", i18n.G("publish changes to git.coopcloud.tech"),
) )
CatalogueGenerateCommand.Flags().BoolVarP( CatalogueGenerateCommand.Flags().BoolVarP(
&internal.Dry, &internal.Dry,
"dry-run", i18n.G("dry-run"),
"r", i18n.G("r"),
false, false,
"report changes that would be made", i18n.G("report changes that would be made"),
) )
CatalogueGenerateCommand.Flags().BoolVarP( CatalogueGenerateCommand.Flags().BoolVarP(
&skipUpdates, &skipUpdates,
"skip-updates", i18n.G("skip-updates"),
"s", i18n.G("s"),
false, false,
"skip updating recipe repositories", i18n.G("skip updating recipe repositories"),
) )
CatalogueGenerateCommand.Flags().BoolVarP( CatalogueGenerateCommand.Flags().BoolVarP(
&internal.Chaos, &internal.Chaos,
"chaos", i18n.G("chaos"),
"C", i18n.G("C"),
false, false,
"ignore uncommitted recipes changes", i18n.G("ignore uncommitted recipes changes"),
) )
} }

View File

@ -3,13 +3,14 @@ package cli
import ( import (
"os" "os"
"coopcloud.tech/abra/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var AutocompleteCommand = &cobra.Command{ var AutocompleteCommand = &cobra.Command{
Use: "autocomplete [bash|zsh|fish|powershell]", Use: i18n.G("autocomplete [bash|zsh|fish|powershell]"),
Short: "Generate autocompletion script", Short: i18n.G("Generate autocompletion script"),
Long: `To load completions: Long: i18n.G(`To load completions:
Bash: Bash:
# Load autocompletion for the current Bash session # Load autocompletion for the current Bash session
@ -43,7 +44,7 @@ PowerShell:
# To load autocompletions for every new session, run: # To load autocompletions for every new session, run:
PS> abra autocomplete powershell > abra.ps1 PS> abra autocomplete powershell > abra.ps1
# and source this file from your PowerShell profile.`, # and source this file from your PowerShell profile.`),
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),

View File

@ -2,11 +2,12 @@ package internal
import ( import (
"context" "context"
"fmt" "errors"
"io" "io"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container" containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/service" "coopcloud.tech/abra/pkg/service"
"coopcloud.tech/abra/pkg/upstream/container" "coopcloud.tech/abra/pkg/upstream/container"
@ -22,10 +23,10 @@ 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{}, fmt.Errorf("no backupbot discovered, is it deployed?") return types.Container{}, errors.New(i18n.G("no backupbot discovered, is it deployed?"))
} }
log.Debugf("retrieved %s as backup enabled service", chosenService.Spec.Name) log.Debug(i18n.G("retrieved %s as backup enabled service", chosenService.Spec.Name))
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", chosenService.Spec.Name) filters.Add("name", chosenService.Spec.Name)
@ -58,7 +59,7 @@ func RunBackupCmdRemote(
Tty: true, Tty: true,
} }
log.Debugf("running backup %s on %s with exec config %v", backupCmd, containerID, execBackupListOpts) log.Debug(i18n.G("running backup %s on %s with exec config %v", backupCmd, containerID, execBackupListOpts))
// FIXME: avoid instantiating a new CLI // FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli() dcli, err := command.NewDockerCli()

View File

@ -3,6 +3,7 @@ package internal
import ( import (
"bufio" "bufio"
"context" "context"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os/exec" "os/exec"
@ -11,6 +12,7 @@ import (
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
containerPkg "coopcloud.tech/abra/pkg/container" containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/container" "coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
@ -34,7 +36,7 @@ func RunCmdRemote(
return err return err
} }
log.Debugf("retrieved %s as target container on %s", formatter.ShortenID(targetContainer.ID), app.Server) log.Debug(i18n.G("retrieved %s as target container on %s", formatter.ShortenID(targetContainer.ID), app.Server))
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip} toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
content, err := archive.TarWithOptions(abraSh, toTarOpts) content, err := archive.TarWithOptions(abraSh, toTarOpts)
@ -65,7 +67,7 @@ func RunCmdRemote(
} }
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
log.Infof("%s does not exist for %s, use /bin/sh as fallback", shell, app.Name) log.Info(i18n.G("%s does not exist for %s, use /bin/sh as fallback", shell, app.Name))
shell = "/bin/sh" shell = "/bin/sh"
} }
@ -76,10 +78,10 @@ func RunCmdRemote(
cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s", serviceName, app.Name, app.StackName(), cmdName)} cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s", serviceName, app.Name, app.StackName(), cmdName)}
} }
log.Debugf("running command: %s", strings.Join(cmd, " ")) log.Debug(i18n.G("running command: %s", strings.Join(cmd, " ")))
if remoteUser != "" { if remoteUser != "" {
log.Debugf("running command with user %s", remoteUser) log.Debug(i18n.G("running command with user %s", remoteUser))
execCreateOpts.User = remoteUser execCreateOpts.User = remoteUser
} }
@ -88,7 +90,7 @@ func RunCmdRemote(
execCreateOpts.Tty = true execCreateOpts.Tty = true
if disableTTY { if disableTTY {
execCreateOpts.Tty = false execCreateOpts.Tty = false
log.Debugf("not requesting a remote TTY") log.Debug(i18n.G("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 {
@ -105,7 +107,7 @@ func EnsureCommand(abraSh, recipeName, execCmd string) error {
} }
if !strings.Contains(string(bytes), execCmd) { if !strings.Contains(string(bytes), execCmd) {
return fmt.Errorf("%s doesn't have a %s function", recipeName, execCmd) return errors.New(i18n.G("%s doesn't have a %s function", recipeName, execCmd))
} }
return nil return nil

View File

@ -1,6 +1,7 @@
package internal package internal
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"sort" "sort"
@ -9,6 +10,7 @@ import (
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
@ -70,18 +72,18 @@ func DeployOverview(
} }
rows := [][]string{ rows := [][]string{
{"DOMAIN", domain}, {i18n.G("DOMAIN"), domain},
{"RECIPE", app.Recipe.Name}, {i18n.G("RECIPE"), app.Recipe.Name},
{"SERVER", server}, {i18n.G("SERVER"), server},
{"CONFIG", deployConfig}, {i18n.G("CONFIG"), deployConfig},
{"", ""}, {"", ""},
{"CURRENT DEPLOYMENT", formatter.BoldDirtyDefault(deployedVersion)}, {i18n.G("CURRENT DEPLOYMENT"), formatter.BoldDirtyDefault(deployedVersion)},
{"ENV VERSION", formatter.BoldDirtyDefault(envVersion)}, {i18n.G("ENV VERSION"), formatter.BoldDirtyDefault(envVersion)},
{"NEW DEPLOYMENT", formatter.BoldDirtyDefault(toDeployVersion)}, {i18n.G("NEW DEPLOYMENT"), formatter.BoldDirtyDefault(toDeployVersion)},
} }
deployType := getDeployType(deployedVersion, toDeployVersion) deployType := getDeployType(deployedVersion, toDeployVersion)
overview := formatter.CreateOverview(fmt.Sprintf("%s OVERVIEW", deployType), rows) overview := formatter.CreateOverview(i18n.G("%s OVERVIEW", deployType), rows)
fmt.Println(overview) fmt.Println(overview)
@ -104,7 +106,7 @@ func DeployOverview(
} }
if !response { if !response {
log.Fatal("deployment cancelled") log.Fatal(i18n.G("deployment cancelled"))
} }
return nil return nil
@ -112,32 +114,32 @@ func DeployOverview(
func getDeployType(currentVersion, newVersion string) string { func getDeployType(currentVersion, newVersion string) string {
if newVersion == config.NO_DOMAIN_DEFAULT { if newVersion == config.NO_DOMAIN_DEFAULT {
return "UNDEPLOY" return i18n.G("UNDEPLOY")
} }
if strings.Contains(newVersion, "+U") { if strings.Contains(newVersion, "+U") {
return "CHAOS DEPLOY" return i18n.G("CHAOS DEPLOY")
} }
if strings.Contains(currentVersion, "+U") { if strings.Contains(currentVersion, "+U") {
return "UNCHAOS DEPLOY" return i18n.G("UNCHAOS DEPLOY")
} }
if currentVersion == newVersion { if currentVersion == newVersion {
return "REDEPLOY" return ("REDEPLOY")
} }
if currentVersion == config.NO_VERSION_DEFAULT { if currentVersion == config.NO_VERSION_DEFAULT {
return "NEW DEPLOY" return i18n.G("NEW DEPLOY")
} }
currentParsed, err := tagcmp.Parse(currentVersion) currentParsed, err := tagcmp.Parse(currentVersion)
if err != nil { if err != nil {
return "DEPLOY" return i18n.G("DEPLOY")
} }
newParsed, err := tagcmp.Parse(newVersion) newParsed, err := tagcmp.Parse(newVersion)
if err != nil { if err != nil {
return "DEPLOY" return i18n.G("DEPLOY")
} }
if currentParsed.IsLessThan(newParsed) { if currentParsed.IsLessThan(newParsed) {
return "UPGRADE" return i18n.G("UPGRADE")
} }
return "DOWNGRADE" return i18n.G("DOWNGRADE")
} }
// 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
@ -146,7 +148,7 @@ func getDeployType(currentVersion, newVersion string) string {
func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error { func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
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) {
return fmt.Errorf(fmt.Sprintf("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name)) return errors.New(i18n.G("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name))
} }
return err return err
} }
@ -154,7 +156,7 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
for _, command := range strings.Split(commands, "|") { for _, command := range strings.Split(commands, "|") {
commandParts := strings.Split(command, " ") commandParts := strings.Split(command, " ")
if len(commandParts) < 2 { if len(commandParts) < 2 {
return fmt.Errorf(fmt.Sprintf("not enough arguments: %s", command)) return errors.New(i18n.G("not enough arguments: %s", command))
} }
targetServiceName := commandParts[0] targetServiceName := commandParts[0]
cmdName := commandParts[1] cmdName := commandParts[1]
@ -162,7 +164,7 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
if len(commandParts) > 2 { if len(commandParts) > 2 {
parsedCmdArgs = fmt.Sprintf("%s ", strings.Join(commandParts[2:], " ")) parsedCmdArgs = fmt.Sprintf("%s ", strings.Join(commandParts[2:], " "))
} }
log.Infof("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName) log.Info(i18n.G("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName))
if err := EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil { if err := EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
return err return err
@ -181,10 +183,10 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
} }
if !matchingServiceName { if !matchingServiceName {
return fmt.Errorf(fmt.Sprintf("no service %s for %s?", targetServiceName, app.Name)) return fmt.Errorf("no service %s for %s?", targetServiceName, app.Name)
} }
log.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName) log.Debug(i18n.G("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName))
requestTTY := true requestTTY := true
if err := RunCmdRemote( if err := RunCmdRemote(

View File

@ -1,9 +1,11 @@
package internal package internal
import ( import (
"errors"
"fmt" "fmt"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"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"
@ -13,7 +15,7 @@ import (
// PromptBumpType prompts for version bump type // PromptBumpType prompts for version bump type
func PromptBumpType(tagString, latestRelease string) error { func PromptBumpType(tagString, latestRelease string) error {
if (!Major && !Minor && !Patch) && tagString == "" { if (!Major && !Minor && !Patch) && tagString == "" {
fmt.Printf(` fmt.Print(i18n.G(`
You need to make a decision about what kind of an update this new recipe You need to make a decision about what kind of an update this new recipe
version is. If someone else performs this upgrade, do they have to do some version is. If someone else performs this upgrade, do they have to do some
migration work or take care of some breaking changes? This can be signaled in migration work or take care of some breaking changes? This can be signaled in
@ -36,12 +38,12 @@ Here is a semver cheat sheet (more on https://semver.org):
should also Just Work and is mostly to do with minor bug fixes should also Just Work and is mostly to do with minor bug fixes
and/or security patches. "nothing to worry about". and/or security patches. "nothing to worry about".
`, latestRelease) `, latestRelease))
var chosenBumpType string var chosenBumpType string
prompt := &survey.Select{ prompt := &survey.Select{
Message: fmt.Sprintf("select recipe version increment type"), Message: fmt.Sprintf("select recipe version increment type"),
Options: []string{"major", "minor", "patch"}, Options: []string{i18n.G("major"), i18n.G("minor"), i18n.G("patch")},
} }
if err := survey.AskOne(prompt, &chosenBumpType); err != nil { if err := survey.AskOne(prompt, &chosenBumpType); err != nil {
@ -59,13 +61,13 @@ func GetBumpType() string {
var bumpType string var bumpType string
if Major { if Major {
bumpType = "major" bumpType = i18n.G("major")
} else if Minor { } else if Minor {
bumpType = "minor" bumpType = i18n.G("minor")
} else if Patch { } else if Patch {
bumpType = "patch" bumpType = i18n.G("patch")
} else { } else {
log.Fatal("no version bump type specififed?") log.Fatal(i18n.G("no version bump type specififed?"))
} }
return bumpType return bumpType
@ -73,14 +75,14 @@ func GetBumpType() string {
// SetBumpType figures out which bump type is specified // SetBumpType figures out which bump type is specified
func SetBumpType(bumpType string) { func SetBumpType(bumpType string) {
if bumpType == "major" { if bumpType == i18n.G("major") {
Major = true Major = true
} else if bumpType == "minor" { } else if bumpType == i18n.G("minor") {
Minor = true Minor = true
} else if bumpType == "patch" { } else if bumpType == i18n.G("patch") {
Patch = true Patch = true
} else { } else {
log.Fatal("no version bump type specififed?") log.Fatal(i18n.G("no version bump type specififed?"))
} }
} }
@ -107,7 +109,7 @@ func GetMainAppImage(recipe recipe.Recipe) (string, error) {
} }
if path == "" { if path == "" {
return path, fmt.Errorf("%s has no main 'app' service?", recipe.Name) return path, errors.New(i18n.G("%s has no main 'app' service?", recipe.Name))
} }
return path, nil return path, nil

View File

@ -5,6 +5,7 @@ import (
"coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/i18n"
"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"
@ -17,36 +18,36 @@ func ValidateRecipe(args []string, cmdName string) recipe.Recipe {
recipeName = args[0] recipeName = args[0]
} }
if recipeName == "" && !NoInput { var recipes []string
var recipes []string
catl, err := recipe.ReadRecipeCatalogue(Offline) catl, err := recipe.ReadRecipeCatalogue(Offline)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
knownRecipes := make(map[string]bool) knownRecipes := make(map[string]bool)
for name := range catl { for name := range catl {
knownRecipes[name] = true knownRecipes[name] = true
} }
localRecipes, err := recipe.GetRecipesLocal()
if err != nil {
log.Fatal(err)
}
localRecipes, err := recipe.GetRecipesLocal()
if err != nil {
log.Debug(i18n.G("can't read local recipes: %s", err))
} else {
for _, recipeLocal := range localRecipes { for _, recipeLocal := range localRecipes {
if _, ok := knownRecipes[recipeLocal]; !ok { if _, ok := knownRecipes[recipeLocal]; !ok {
knownRecipes[recipeLocal] = true knownRecipes[recipeLocal] = true
} }
} }
}
for recipeName := range knownRecipes { for recipeName := range knownRecipes {
recipes = append(recipes, recipeName) recipes = append(recipes, recipeName)
} }
if recipeName == "" && !NoInput {
prompt := &survey.Select{ prompt := &survey.Select{
Message: "Select recipe", Message: i18n.G("Select recipe"),
Options: recipes, Options: recipes,
} }
if err := survey.AskOne(prompt, &recipeName); err != nil { if err := survey.AskOne(prompt, &recipeName); err != nil {
@ -55,30 +56,36 @@ func ValidateRecipe(args []string, cmdName string) recipe.Recipe {
} }
if recipeName == "" { if recipeName == "" {
log.Fatal("no recipe name provided") log.Fatal(i18n.G("no recipe name provided"))
}
if _, ok := knownRecipes[recipeName]; !ok {
if !strings.Contains(recipeName, "/") {
log.Fatal(i18n.G("no recipe '%s' exists?", recipeName))
}
} }
chosenRecipe := recipe.Get(recipeName) chosenRecipe := recipe.Get(recipeName)
err := chosenRecipe.EnsureExists() if err := chosenRecipe.EnsureExists(); err != nil {
if err != nil {
log.Fatal(err) log.Fatal(err)
} }
_, err = chosenRecipe.GetComposeConfig(nil) _, err = chosenRecipe.GetComposeConfig(nil)
if err != nil { if err != nil {
if cmdName == "generate" { if cmdName == i18n.G("generate") {
if strings.Contains(err.Error(), "missing a compose") { if strings.Contains(err.Error(), "missing a compose") {
log.Fatal(err) log.Fatal(err)
} }
log.Warn(err) log.Warn(err)
} else { } else {
if strings.Contains(err.Error(), "template_driver is not allowed") { if strings.Contains(err.Error(), "template_driver is not allowed") {
log.Warnf("ensure %s recipe compose.* files include \"version: '3.8'\"", recipeName) log.Warn(i18n.G("ensure %s recipe compose.* files include \"version: '3.8'\"", recipeName))
} }
log.Fatalf("unable to validate recipe: %s", err) log.Fatal(i18n.G("unable to validate recipe: %s", err))
} }
} }
log.Debugf("validated %s as recipe argument", recipeName) log.Debug(i18n.G("validated %s as recipe argument", recipeName))
return chosenRecipe return chosenRecipe
} }
@ -86,7 +93,7 @@ func ValidateRecipe(args []string, cmdName string) recipe.Recipe {
// ValidateApp ensures the app name arg is valid. // ValidateApp ensures the app name arg is valid.
func ValidateApp(args []string) app.App { func ValidateApp(args []string) app.App {
if len(args) == 0 { if len(args) == 0 {
log.Fatal("no app provided") log.Fatal(i18n.G("no app provided"))
} }
appName := args[0] appName := args[0]
@ -96,7 +103,7 @@ func ValidateApp(args []string) app.App {
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("validated %s as app argument", appName) log.Debug(i18n.G("validated %s as app argument", appName))
return app return app
} }
@ -110,8 +117,8 @@ func ValidateDomain(args []string) string {
if domainName == "" && !NoInput { if domainName == "" && !NoInput {
prompt := &survey.Input{ prompt := &survey.Input{
Message: "Specify a domain name", Message: i18n.G("Specify a domain name"),
Default: "example.com", Default: "1312.net",
} }
if err := survey.AskOne(prompt, &domainName); err != nil { if err := survey.AskOne(prompt, &domainName); err != nil {
log.Fatal(err) log.Fatal(err)
@ -119,10 +126,10 @@ func ValidateDomain(args []string) string {
} }
if domainName == "" { if domainName == "" {
log.Fatal("no domain provided") log.Fatal(i18n.G("no domain provided"))
} }
log.Debugf("validated %s as domain argument", domainName) log.Debug(i18n.G("validated %s as domain argument", domainName))
return domainName return domainName
} }
@ -141,7 +148,7 @@ func ValidateServer(args []string) string {
if serverName == "" && !NoInput { if serverName == "" && !NoInput {
prompt := &survey.Select{ prompt := &survey.Select{
Message: "Specify a server name", Message: i18n.G("Specify a server name"),
Options: serverNames, Options: serverNames,
} }
if err := survey.AskOne(prompt, &serverName); err != nil { if err := survey.AskOne(prompt, &serverName); err != nil {
@ -157,14 +164,14 @@ func ValidateServer(args []string) string {
} }
if serverName == "" { if serverName == "" {
log.Fatal("no server provided") log.Fatal(i18n.G("no server provided"))
} }
if !matched { if !matched {
log.Fatal("server doesn't exist?") log.Fatal(i18n.G("server doesn't exist?"))
} }
log.Debugf("validated %s as server argument", serverName) log.Debug(i18n.G("validated %s as server argument", serverName))
return serverName return serverName
} }

View File

@ -4,15 +4,16 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var RecipeDiffCommand = &cobra.Command{ var RecipeDiffCommand = &cobra.Command{
Use: "diff <recipe> [flags]", Use: i18n.G("diff <recipe> [flags]"),
Aliases: []string{"d"}, Aliases: []string{i18n.G("d")},
Short: "Show unstaged changes in recipe config", Short: i18n.G("Show unstaged changes in recipe config"),
Long: "This command requires /usr/bin/git.", Long: i18n.G("This command requires /usr/bin/git."),
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,

View File

@ -6,6 +6,7 @@ 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/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"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"
@ -14,19 +15,19 @@ import (
) )
var RecipeFetchCommand = &cobra.Command{ var RecipeFetchCommand = &cobra.Command{
Use: "fetch [recipe | --all] [flags]", Use: i18n.G("fetch [recipe | --all] [flags]"),
Aliases: []string{"f"}, Aliases: []string{i18n.G("f")},
Short: "Clone recipe(s) locally", Short: i18n.G("Clone recipe(s) locally"),
Long: `Using "--force/-f" Git syncs an existing recipe. It does not erase unstaged changes.`, Long: i18n.G(`Using "--force/-f" Git syncs an existing recipe. It does not erase unstaged changes.`),
Args: cobra.RangeArgs(0, 1), Args: cobra.RangeArgs(0, 1),
Example: ` # fetch from recipe catalogue Example: i18n.G(` # fetch from recipe catalogue
abra recipe fetch gitea abra recipe fetch gitea
# fetch from remote recipe # fetch from remote recipe
abra recipe fetch git.foo.org/recipes/myrecipe abra recipe fetch git.foo.org/recipes/myrecipe
# fetch with ssh remote for hacking # fetch with ssh remote for hacking
abra recipe fetch gitea --ssh`, abra recipe fetch gitea --ssh`),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
args []string, args []string,
@ -40,19 +41,18 @@ var RecipeFetchCommand = &cobra.Command{
} }
if recipeName == "" && !fetchAllRecipes { if recipeName == "" && !fetchAllRecipes {
log.Fatal("missing [recipe] or --all/-a") log.Fatal(i18n.G("missing [recipe] or --all/-a"))
} }
if recipeName != "" && fetchAllRecipes { if recipeName != "" && fetchAllRecipes {
log.Fatal("cannot use [recipe] and --all/-a together") log.Fatal(i18n.G("cannot use [recipe] and --all/-a together"))
} }
ensureCtx := internal.GetEnsureContext()
if recipeName != "" { if recipeName != "" {
r := recipe.Get(recipeName) r := recipe.Get(recipeName)
if _, err := os.Stat(r.Dir); !os.IsNotExist(err) { if _, err := os.Stat(r.Dir); !os.IsNotExist(err) {
if !force { if !force {
log.Warnf("%s is already fetched", r.Name) log.Warn(i18n.G("%s is already fetched", r.Name))
return return
} }
} }
@ -61,24 +61,24 @@ var RecipeFetchCommand = &cobra.Command{
if sshRemote { if sshRemote {
if r.SSHURL == "" { if r.SSHURL == "" {
log.Warnf("unable to discover SSH remote for %s", r.Name) log.Warn(i18n.G("unable to discover SSH remote for %s", r.Name))
return return
} }
repo, err := git.PlainOpen(r.Dir) repo, err := git.PlainOpen(r.Dir)
if err != nil { if err != nil {
log.Fatalf("unable to open %s: %s", r.Dir, err) log.Fatal(i18n.G("unable to open %s: %s", r.Dir, err))
} }
if err = repo.DeleteRemote("origin"); err != nil { if err = repo.DeleteRemote("origin"); err != nil {
log.Fatalf("unable to remove default remote in %s: %s", r.Dir, err) log.Fatal(i18n.G("unable to remove default remote in %s: %s", r.Dir, err))
} }
if _, err := repo.CreateRemote(&gitCfg.RemoteConfig{ if _, err := repo.CreateRemote(&gitCfg.RemoteConfig{
Name: "origin", Name: "origin",
URLs: []string{r.SSHURL}, URLs: []string{r.SSHURL},
}); err != nil { }); err != nil {
log.Fatalf("unable to set SSH remote in %s: %s", r.Dir, err) log.Fatal(i18n.G("unable to set SSH remote in %s: %s", r.Dir, err))
} }
} }
@ -90,7 +90,8 @@ var RecipeFetchCommand = &cobra.Command{
log.Fatal(err) log.Fatal(err)
} }
catlBar := formatter.CreateProgressbar(len(catalogue), "fetching latest recipes...") catlBar := formatter.CreateProgressbar(len(catalogue), i18n.G("fetching latest recipes..."))
ensureCtx := internal.GetEnsureContext()
for recipeName := range catalogue { for recipeName := range catalogue {
r := recipe.Get(recipeName) r := recipe.Get(recipeName)
if err := r.Ensure(ensureCtx); err != nil { if err := r.Ensure(ensureCtx); err != nil {
@ -110,25 +111,25 @@ var (
func init() { func init() {
RecipeFetchCommand.Flags().BoolVarP( RecipeFetchCommand.Flags().BoolVarP(
&fetchAllRecipes, &fetchAllRecipes,
"all", i18n.G("all"),
"a", i18n.G("a"),
false, false,
"fetch all recipes", i18n.G("fetch all recipes"),
) )
RecipeFetchCommand.Flags().BoolVarP( RecipeFetchCommand.Flags().BoolVarP(
&sshRemote, &sshRemote,
"ssh", i18n.G("ssh"),
"s", i18n.G("s"),
false, false,
"automatically set ssh remote", i18n.G("automatically set ssh remote"),
) )
RecipeFetchCommand.Flags().BoolVarP( RecipeFetchCommand.Flags().BoolVarP(
&force, &force,
"force", i18n.G("force"),
"f", i18n.G("f"),
false, false,
"force re-fetch", i18n.G("force re-fetch"),
) )
} }

View File

@ -4,15 +4,16 @@ 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/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var RecipeLintCommand = &cobra.Command{ var RecipeLintCommand = &cobra.Command{
Use: "lint <recipe> [flags]", Use: i18n.G("lint <recipe> [flags]"),
Short: "Lint a recipe", Short: i18n.G("Lint a recipe"),
Aliases: []string{"l"}, Aliases: []string{i18n.G("l")},
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -28,12 +29,12 @@ var RecipeLintCommand = &cobra.Command{
} }
headers := []string{ headers := []string{
"ref", i18n.G("ref"),
"rule", i18n.G("rule"),
"severity", i18n.G("severity"),
"satisfied", i18n.G("satisfied"),
"skipped", i18n.G("skipped"),
"resolve", i18n.G("resolve"),
} }
table, err := formatter.CreateTable() table, err := formatter.CreateTable()
@ -49,7 +50,7 @@ var RecipeLintCommand = &cobra.Command{
for level := range lint.LintRules { for level := range lint.LintRules {
for _, rule := range lint.LintRules[level] { for _, rule := range lint.LintRules[level] {
if onlyError && rule.Level != "error" { if onlyError && rule.Level != "error" {
log.Debugf("skipping %s, does not have level \"error\"", rule.Ref) log.Debug(i18n.G("skipping %s, does not have level \"error\"", rule.Ref))
continue continue
} }
@ -70,7 +71,7 @@ var RecipeLintCommand = &cobra.Command{
warnMessages = append(warnMessages, err.Error()) warnMessages = append(warnMessages, err.Error())
} }
if !ok && rule.Level == "error" { if !ok && rule.Level == i18n.G("error") {
hasError = true hasError = true
} }
@ -111,7 +112,7 @@ var RecipeLintCommand = &cobra.Command{
} }
if hasError { if hasError {
log.Warnf("critical errors present in %s config", recipe.Name) log.Warn(i18n.G("critical errors present in %s config", recipe.Name))
} }
} }
}, },
@ -124,17 +125,17 @@ var (
func init() { func init() {
RecipeLintCommand.Flags().BoolVarP( RecipeLintCommand.Flags().BoolVarP(
&internal.Chaos, &internal.Chaos,
"chaos", i18n.G("chaos"),
"C", i18n.G("C"),
false, false,
"ignore uncommitted recipes changes", i18n.G("ignore uncommitted recipes changes"),
) )
RecipeLintCommand.Flags().BoolVarP( RecipeLintCommand.Flags().BoolVarP(
&onlyError, &onlyError,
"error", i18n.G("error"),
"e", i18n.G("e"),
false, false,
"only show errors", i18n.G("only show errors"),
) )
} }

View File

@ -8,15 +8,16 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var RecipeListCommand = &cobra.Command{ var RecipeListCommand = &cobra.Command{
Use: "list", Use: i18n.G("list"),
Short: "List recipes", Short: i18n.G("List recipes"),
Aliases: []string{"ls"}, Aliases: []string{i18n.G("ls")},
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
catl, err := recipe.ReadRecipeCatalogue(internal.Offline) catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
@ -33,14 +34,14 @@ var RecipeListCommand = &cobra.Command{
} }
headers := []string{ headers := []string{
"name", i18n.G("name"),
"category", i18n.G("category"),
"status", i18n.G("status"),
"healthcheck", i18n.G("healthcheck"),
"backups", i18n.G("backups"),
"email", i18n.G("email"),
"tests", i18n.G("tests"),
"SSO", i18n.G("SSO"),
} }
table.Headers(headers...) table.Headers(headers...)
@ -73,7 +74,7 @@ var RecipeListCommand = &cobra.Command{
if internal.MachineReadable { if internal.MachineReadable {
out, err := formatter.ToJSON(headers, rows) out, err := formatter.ToJSON(headers, rows)
if err != nil { if err != nil {
log.Fatal("unable to render to JSON: %s", err) log.Fatal(i18n.G("unable to render to JSON: %s", err))
} }
fmt.Println(out) fmt.Println(out)
return return
@ -93,17 +94,17 @@ var (
func init() { func init() {
RecipeListCommand.Flags().BoolVarP( RecipeListCommand.Flags().BoolVarP(
&internal.MachineReadable, &internal.MachineReadable,
"machine", i18n.G("machine"),
"m", i18n.G("m"),
false, false,
"print machine-readable output", i18n.G("print machine-readable output"),
) )
RecipeListCommand.Flags().StringVarP( RecipeListCommand.Flags().StringVarP(
&pattern, &pattern,
"pattern", i18n.G("pattern"),
"p", i18n.G("p"),
"", "",
"filter by recipe", i18n.G("filter by recipe"),
) )
} }

View File

@ -10,6 +10,7 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "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/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -30,10 +31,10 @@ type recipeMetadata struct {
} }
var RecipeNewCommand = &cobra.Command{ var RecipeNewCommand = &cobra.Command{
Use: "new <recipe> [flags]", Use: i18n.G("new <recipe> [flags]"),
Aliases: []string{"n"}, Aliases: []string{i18n.G("n")},
Short: "Create a new recipe", Short: i18n.G("Create a new recipe"),
Long: `A community managed recipe template is used.`, Long: i18n.G(`A community managed recipe template is used.`),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -46,10 +47,10 @@ var RecipeNewCommand = &cobra.Command{
r := recipe.Get(recipeName) r := recipe.Get(recipeName)
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.Fatal(i18n.G("%s recipe directory already exists?", r.Dir))
} }
url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL) url := i18n.G("%s/example.git", config.REPOS_BASE_URL)
if err := git.Clone(r.Dir, url); err != nil { if err := git.Clone(r.Dir, url); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -58,7 +59,7 @@ var RecipeNewCommand = &cobra.Command{
if err := os.RemoveAll(gitRepo); err != nil { if err := os.RemoveAll(gitRepo); err != nil {
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("removed .git repo in %s", gitRepo) log.Debug(i18n.G("removed .git repo in %s", gitRepo))
meta := newRecipeMeta(recipeName) meta := newRecipeMeta(recipeName)
@ -82,8 +83,8 @@ var RecipeNewCommand = &cobra.Command{
log.Fatal(err) log.Fatal(err)
} }
log.Infof("new recipe '%s' created: %s", recipeName, path.Join(r.Dir)) log.Info(i18n.G("new recipe '%s' created: %s", recipeName, path.Join(r.Dir)))
log.Info("happy hacking 🎉") log.Info(i18n.G("happy hacking 🎉"))
}, },
} }
@ -111,17 +112,17 @@ var (
func init() { func init() {
RecipeNewCommand.Flags().StringVarP( RecipeNewCommand.Flags().StringVarP(
&gitName, &gitName,
"git-name", i18n.G("git-name"),
"N", i18n.G("N"),
"", "",
"Git (user) name to do commits with", i18n.G("Git (user) name to do commits with"),
) )
RecipeNewCommand.Flags().StringVarP( RecipeNewCommand.Flags().StringVarP(
&gitEmail, &gitEmail,
"git-email", i18n.G("git-email"),
"e", i18n.G("e"),
"", "",
"Git email name to do commits with", i18n.G("Git email name to do commits with"),
) )
} }

View File

@ -1,13 +1,16 @@
package recipe package recipe
import "github.com/spf13/cobra" import (
"coopcloud.tech/abra/pkg/i18n"
"github.com/spf13/cobra"
)
// RecipeCommand defines all recipe related sub-commands. // RecipeCommand defines all recipe related sub-commands.
var RecipeCommand = &cobra.Command{ var RecipeCommand = &cobra.Command{
Use: "recipe [cmd] [args] [flags]", Use: i18n.G("recipe [cmd] [args] [flags]"),
Aliases: []string{"r"}, Aliases: []string{i18n.G("r")},
Short: "Manage recipes", Short: i18n.G("Manage recipes"),
Long: `A recipe is a blueprint for an app. Long: i18n.G(`A recipe is a blueprint for an app.
It is a bunch of config files which describe how to deploy and maintain an app. It is a bunch of config files which describe how to deploy and maintain an app.
Recipes are maintained by the Co-op Cloud community and you can use Abra to Recipes are maintained by the Co-op Cloud community and you can use Abra to
@ -15,5 +18,5 @@ read them, deploy them and create apps 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.`),
} }

View File

@ -12,6 +12,7 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
@ -22,10 +23,10 @@ import (
) )
var RecipeReleaseCommand = &cobra.Command{ var RecipeReleaseCommand = &cobra.Command{
Use: "release <recipe> [version] [flags]", Use: i18n.G("release <recipe> [version] [flags]"),
Aliases: []string{"rl"}, Aliases: []string{i18n.G("rl")},
Short: "Release a new recipe version", Short: i18n.G("Release a new recipe version"),
Long: `Create a new version of a recipe. Long: i18n.G(`Create a new version of a recipe.
These versions are then published on the Co-op Cloud recipe catalogue. These These versions are then published on the Co-op Cloud recipe catalogue. These
versions take the following form: versions take the following form:
@ -44,7 +45,7 @@ major and therefore require intervention while doing the upgrade work.
Publish your new release to git.coopcloud.tech with "--publish/-p". 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.`),
Args: cobra.RangeArgs(1, 2), Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -74,7 +75,7 @@ your SSH keys configured on your account.`,
mainAppVersion := imagesTmp[mainApp] mainAppVersion := imagesTmp[mainApp]
if mainAppVersion == "" { if mainAppVersion == "" {
log.Fatalf("main app service version for %s is empty?", recipe.Name) log.Fatal(i18n.G("main app service version for %s is empty?", recipe.Name))
} }
var tagString string var tagString string
@ -84,12 +85,12 @@ your SSH keys configured on your account.`,
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.Fatal(i18n.G("cannot parse %s, invalid tag specified?", tagString))
} }
} }
if (internal.Major || internal.Minor || internal.Patch) && tagString != "" { if (internal.Major || internal.Minor || internal.Patch) && tagString != "" {
log.Fatal("cannot specify tag and bump type at the same time") log.Fatal(i18n.G("cannot specify tag and bump type at the same time"))
} }
if tagString != "" { if tagString != "" {
@ -117,19 +118,19 @@ your SSH keys configured on your account.`,
} }
if !isClean { if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name) log.Info(i18n.G("%s currently has these unstaged changes 👇", recipe.Name))
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil { if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
if len(tags) > 0 { if len(tags) > 0 {
log.Warnf("previous git tags detected, assuming this is a new semver release") log.Warn(i18n.G("previous git tags detected, assuming this is a new semver release"))
if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil { if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} else { } else {
log.Warnf("no tag specified and no previous tag available for %s, assuming this is the initial release", recipe.Name) log.Warn(i18n.G("no tag specified and no previous tag available for %s, assuming this is the initial release", recipe.Name))
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil { if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
if cleanUpErr := cleanUpTag(recipe, tagString); err != nil { if cleanUpErr := cleanUpTag(recipe, tagString); err != nil {
@ -181,7 +182,7 @@ func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
} }
if missingTag { if missingTag {
return services, fmt.Errorf("app service is missing image tag?") return services, errors.New(i18n.G("app service is missing image tag?"))
} }
return services, nil return services, nil
@ -245,7 +246,7 @@ func btoi(b bool) int {
// getTagCreateOptions constructs git tag create options // getTagCreateOptions constructs git tag create options
func getTagCreateOptions(tag string) (git.CreateTagOptions, error) { func getTagCreateOptions(tag string) (git.CreateTagOptions, error) {
msg := fmt.Sprintf("chore: publish %s release", tag) msg := i18n.G("chore: publish %s release", tag)
return git.CreateTagOptions{Message: msg}, nil return git.CreateTagOptions{Message: msg}, nil
} }
@ -273,13 +274,13 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
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.Debug(i18n.G("dry run: move release note from 'next' to %s", tag))
return nil return nil
} }
if !internal.NoInput { if !internal.NoInput {
prompt := &survey.Confirm{ prompt := &survey.Confirm{
Message: "Use release note in release/next?", Message: i18n.G("Use release note in release/next?"),
} }
if err := survey.AskOne(prompt, &addNextAsReleaseNotes); err != nil { if err := survey.AskOne(prompt, &addNextAsReleaseNotes); err != nil {
@ -313,7 +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: i18n.G("Release Note (leave empty for no release note)"),
} }
var releaseNote string var releaseNote string
@ -338,7 +339,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
func commitRelease(recipe recipe.Recipe, tag string) error { func commitRelease(recipe recipe.Recipe, tag string) error {
if internal.Dry { if internal.Dry {
log.Debugf("dry run: no changes committed") log.Debug(i18n.G("dry run: no changes committed"))
return nil return nil
} }
@ -349,7 +350,7 @@ func commitRelease(recipe recipe.Recipe, tag string) error {
if isClean { if isClean {
if !internal.Dry { if !internal.Dry {
return fmt.Errorf("no changes discovered in %s, nothing to publish?", recipe.Dir) return errors.New(i18n.G("no changes discovered in %s, nothing to publish?", recipe.Dir))
} }
} }
@ -363,7 +364,7 @@ func commitRelease(recipe recipe.Recipe, tag string) error {
func tagRelease(tagString string, repo *git.Repository) error { func tagRelease(tagString string, repo *git.Repository) error {
if internal.Dry { if internal.Dry {
log.Debugf("dry run: no git tag created (%s)", tagString) log.Debug(i18n.G("dry run: no git tag created (%s)", tagString))
return nil return nil
} }
@ -383,20 +384,20 @@ func tagRelease(tagString string, repo *git.Repository) error {
} }
hash := formatter.SmallSHA(head.Hash().String()) hash := formatter.SmallSHA(head.Hash().String())
log.Debugf(fmt.Sprintf("created tag %s at %s", tagString, hash)) log.Debug(i18n.G("created tag %s at %s", tagString, hash))
return nil return nil
} }
func pushRelease(recipe recipe.Recipe, tagString string) error { func pushRelease(recipe recipe.Recipe, tagString string) error {
if internal.Dry { if internal.Dry {
log.Info("dry run: no changes published") log.Info(i18n.G("dry run: no changes published"))
return nil return nil
} }
if !publish && !internal.NoInput { if !publish && !internal.NoInput {
prompt := &survey.Confirm{ prompt := &survey.Confirm{
Message: "publish new release?", Message: i18n.G("publish new release?"),
} }
if err := survey.AskOne(prompt, &publish); err != nil { if err := survey.AskOne(prompt, &publish); err != nil {
@ -409,9 +410,9 @@ func pushRelease(recipe recipe.Recipe, tagString string) error {
return err return err
} }
url := fmt.Sprintf("%s/src/tag/%s", recipe.GitURL, tagString) url := fmt.Sprintf("%s/src/tag/%s", recipe.GitURL, tagString)
log.Infof("new release published: %s", url) log.Info(i18n.G("new release published: %s", url))
} else { } else {
log.Info("no -p/--publish passed, not publishing") log.Info(i18n.G("no -p/--publish passed, not publishing"))
} }
return nil return nil
@ -426,7 +427,7 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch) bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 { if bumpType != 0 {
if (bumpType & (bumpType - 1)) != 0 { if (bumpType & (bumpType - 1)) != 0 {
return fmt.Errorf("you can only use one of: --major, --minor, --patch") return errors.New(i18n.G("you can only use one of: --major, --minor, --patch"))
} }
} }
@ -483,12 +484,12 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
} }
if lastGitTag.String() == tagString { if lastGitTag.String() == tagString {
log.Fatalf("latest git tag (%s) and synced label (%s) are the same?", lastGitTag, tagString) log.Fatal(i18n.G("latest git tag (%s) and synced label (%s) are the same?", lastGitTag, tagString))
} }
if !internal.NoInput { if !internal.NoInput {
prompt := &survey.Confirm{ prompt := &survey.Confirm{
Message: fmt.Sprintf("current: %s, new: %s, correct?", lastGitTag, tagString), Message: i18n.G("current: %s, new: %s, correct?", lastGitTag, tagString),
} }
var ok bool var ok bool
@ -497,7 +498,7 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
} }
if !ok { if !ok {
log.Fatal("exiting as requested") log.Fatal(i18n.G("exiting as requested"))
} }
} }
@ -506,15 +507,15 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
} }
if err := commitRelease(recipe, tagString); err != nil { if err := commitRelease(recipe, tagString); err != nil {
log.Fatalf("failed to commit changes: %s", err.Error()) log.Fatal(i18n.G("failed to commit changes: %s", err.Error()))
} }
if err := tagRelease(tagString, repo); err != nil { if err := tagRelease(tagString, repo); err != nil {
log.Fatalf("failed to tag release: %s", err.Error()) log.Fatal(i18n.G("failed to tag release: %s", err.Error()))
} }
if err := pushRelease(recipe, tagString); err != nil { if err := pushRelease(recipe, tagString); err != nil {
log.Fatalf("failed to publish new release: %s", err.Error()) log.Fatal(i18n.G("failed to publish new release: %s", err.Error()))
} }
return nil return nil
@ -533,7 +534,7 @@ func cleanUpTag(recipe recipe.Recipe, tag string) error {
} }
} }
log.Debugf("removed freshly created tag %s", tag) log.Debug(i18n.G("removed freshly created tag %s", tag))
return nil return nil
} }
@ -545,20 +546,20 @@ func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
} }
if initTag == "" { if initTag == "" {
log.Fatalf("unable to read version for %s from synced label. Did you try running \"abra recipe sync %s\" already?", recipe.Name, recipe.Name) log.Fatal(i18n.G("unable to read version for %s from synced label. Did you try running \"abra recipe sync %s\" already?", recipe.Name, recipe.Name))
} }
log.Warnf("discovered %s as currently synced recipe label", initTag) log.Warn(i18n.G("discovered %s as currently synced recipe label", initTag))
if prompt && !internal.NoInput { if prompt && !internal.NoInput {
var response bool var response bool
prompt := &survey.Confirm{Message: fmt.Sprintf("use %s as the new version?", initTag)} prompt := &survey.Confirm{Message: i18n.G("use %s as the new version?", initTag)}
if err := survey.AskOne(prompt, &response); err != nil { if err := survey.AskOne(prompt, &response); err != nil {
return "", err return "", err
} }
if !response { if !response {
return "", fmt.Errorf("please fix your synced label for %s and re-run this command", recipe.Name) return "", errors.New(i18n.G("please fix your synced label for %s and re-run this command", recipe.Name))
} }
} }
@ -572,42 +573,41 @@ var (
func init() { func init() {
RecipeReleaseCommand.Flags().BoolVarP( RecipeReleaseCommand.Flags().BoolVarP(
&internal.Dry, &internal.Dry,
"dry-run", i18n.G("dry-run"),
"r", i18n.G("r"),
false, false,
"report changes that would be made", i18n.G("report changes that would be made"),
) )
RecipeReleaseCommand.Flags().BoolVarP( RecipeReleaseCommand.Flags().BoolVarP(
&internal.Major, &internal.Major,
"major", i18n.G("major"),
"x", i18n.G("x"),
false, false,
"increase the major part of the version", i18n.G("increase the major part of the version"),
) )
RecipeReleaseCommand.Flags().BoolVarP( RecipeReleaseCommand.Flags().BoolVarP(
&internal.Minor, &internal.Minor,
"minor", i18n.G("minor"),
"y", i18n.G("y"),
false, false,
"increase the minor part of the version", i18n.G("increase the minor part of the version"),
) )
RecipeReleaseCommand.Flags().BoolVarP( RecipeReleaseCommand.Flags().BoolVarP(
&internal.Patch, &internal.Patch,
"patch", i18n.G("patch"),
"z", i18n.G("z"),
false, false,
"increase the patch part of the version", i18n.G("increase the patch part of the version"),
) )
RecipeReleaseCommand.Flags().BoolVarP( RecipeReleaseCommand.Flags().BoolVarP(
&publish, &publish,
"publish", i18n.G("publish"),
"p", i18n.G("p"),
false, false,
"publish changes to git.coopcloud.tech", i18n.G("publish changes to git.coopcloud.tech"),
) )
} }

View File

@ -3,16 +3,17 @@ package recipe
import ( 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/i18n"
"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/spf13/cobra" "github.com/spf13/cobra"
) )
var RecipeResetCommand = &cobra.Command{ var RecipeResetCommand = &cobra.Command{
Use: "reset <recipe> [flags]", Use: i18n.G("reset <recipe> [flags]"),
Aliases: []string{"rs"}, Aliases: []string{i18n.G("rs")},
Short: "Remove all unstaged changes from recipe config", Short: i18n.G("Remove all unstaged changes from recipe config"),
Long: "WARNING: this will delete your changes. Be Careful.", Long: i18n.G("WARNING: this will delete your changes. Be Careful."),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,

View File

@ -7,6 +7,7 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
@ -16,10 +17,10 @@ import (
) )
var RecipeSyncCommand = &cobra.Command{ var RecipeSyncCommand = &cobra.Command{
Use: "sync <recipe> [version] [flags]", Use: i18n.G("sync <recipe> [version] [flags]"),
Aliases: []string{"s"}, Aliases: []string{i18n.G("s")},
Short: "Sync recipe version label", Short: i18n.G("Sync recipe version label"),
Long: `Generate labels for the main recipe service. Long: i18n.G(`Generate labels for the main recipe service.
By convention, the service named "app" using the following format: By convention, the service named "app" using the following format:
@ -27,7 +28,7 @@ By convention, the service named "app" using the following format:
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.`),
Args: cobra.RangeArgs(1, 2), Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -68,11 +69,11 @@ local file system.`,
} }
if len(tags) == 0 && nextTag == "" { if len(tags) == 0 && nextTag == "" {
log.Warnf("no git tags found for %s", recipe.Name) log.Warn(i18n.G("no git tags found for %s", recipe.Name))
if internal.NoInput { if internal.NoInput {
log.Fatalf("unable to continue, input required for initial version") log.Fatal(i18n.G("unable to continue, input required for initial version"))
} }
fmt.Println(fmt.Sprintf(` fmt.Println(i18n.G(`
The following options are two types of initial semantic version that you can The following options are two types of initial semantic version that you can
pick for %s that will be published in the recipe catalogue. This follows the pick for %s that will be published in the recipe catalogue. This follows the
semver convention (more on https://semver.org), here is a short cheatsheet semver convention (more on https://semver.org), here is a short cheatsheet
@ -92,7 +93,7 @@ likely to change.
`, recipe.Name, recipe.Name)) `, recipe.Name, recipe.Name))
var chosenVersion string var chosenVersion string
edPrompt := &survey.Select{ edPrompt := &survey.Select{
Message: "which version do you want to begin with?", Message: i18n.G("which version do you want to begin with?"),
Options: []string{"0.1.0", "1.0.0"}, Options: []string{"0.1.0", "1.0.0"},
} }
@ -125,7 +126,7 @@ likely to change.
if err := iter.ForEach(func(ref *plumbing.Reference) error { if err := iter.ForEach(func(ref *plumbing.Reference) error {
obj, err := repo.TagObject(ref.Hash()) obj, err := repo.TagObject(ref.Hash())
if err != nil { if err != nil {
log.Fatal("Tag at commit ", ref.Hash(), " is unannotated or otherwise broken. Please fix it.") log.Fatal(i18n.G("tag at commit %s is unannotated or otherwise broken", ref.Hash()))
return err return err
} }
@ -150,7 +151,7 @@ likely to change.
if bumpType != 0 { if bumpType != 0 {
// a bitwise check if the number is a power of 2 // a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 { if (bumpType & (bumpType - 1)) != 0 {
log.Fatal("you can only use one version flag: --major, --minor or --patch") log.Fatal(i18n.G("you can only use one version flag: --major, --minor or --patch"))
} }
} }
@ -184,22 +185,22 @@ likely to change.
} }
newTag.Metadata = mainAppVersion newTag.Metadata = mainAppVersion
log.Debugf("choosing %s as new version for %s", newTag.String(), recipe.Name) log.Debug(i18n.G("choosing %s as new version for %s", newTag.String(), recipe.Name))
nextTag = newTag.String() nextTag = newTag.String()
} }
if _, err := tagcmp.Parse(nextTag); err != nil { if _, err := tagcmp.Parse(nextTag); err != nil {
log.Fatalf("invalid version %s specified", nextTag) log.Fatal(i18n.G("invalid version %s specified", nextTag))
} }
mainService := "app" mainService := "app"
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag) label := i18n.G("coop-cloud.${STACK_NAME}.version=%s", nextTag)
if !internal.Dry { if !internal.Dry {
if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil { if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} else { } else {
log.Infof("dry run: not syncing label %s for recipe %s", nextTag, recipe.Name) log.Info(i18n.G("dry run: not syncing label %s for recipe %s", nextTag, recipe.Name))
} }
isClean, err := gitPkg.IsClean(recipe.Dir) isClean, err := gitPkg.IsClean(recipe.Dir)
@ -207,7 +208,7 @@ likely to change.
log.Fatal(err) log.Fatal(err)
} }
if !isClean { if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name) log.Info(i18n.G("%s currently has these unstaged changes 👇", recipe.Name))
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil { if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -218,33 +219,33 @@ likely to change.
func init() { func init() {
RecipeSyncCommand.Flags().BoolVarP( RecipeSyncCommand.Flags().BoolVarP(
&internal.Dry, &internal.Dry,
"dry-run", i18n.G("dry-run"),
"r", i18n.G("r"),
false, false,
"report changes that would be made", i18n.G("report changes that would be made"),
) )
RecipeSyncCommand.Flags().BoolVarP( RecipeSyncCommand.Flags().BoolVarP(
&internal.Major, &internal.Major,
"major", i18n.G("major"),
"x", i18n.G("x"),
false, false,
"increase the major part of the version", i18n.G("increase the major part of the version"),
) )
RecipeSyncCommand.Flags().BoolVarP( RecipeSyncCommand.Flags().BoolVarP(
&internal.Minor, &internal.Minor,
"minor", i18n.G("minor"),
"y", i18n.G("y"),
false, false,
"increase the minor part of the version", i18n.G("increase the minor part of the version"),
) )
RecipeSyncCommand.Flags().BoolVarP( RecipeSyncCommand.Flags().BoolVarP(
&internal.Patch, &internal.Patch,
"patch", i18n.G("patch"),
"z", i18n.G("z"),
false, false,
"increase the patch part of the version", i18n.G("increase the patch part of the version"),
) )
} }

View File

@ -14,6 +14,7 @@ import (
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
@ -37,10 +38,10 @@ type anUpgrade struct {
} }
var RecipeUpgradeCommand = &cobra.Command{ var RecipeUpgradeCommand = &cobra.Command{
Use: "upgrade <recipe> [flags]", Use: i18n.G("upgrade <recipe> [flags]"),
Aliases: []string{"u"}, Aliases: []string{i18n.G("u")},
Short: "Upgrade recipe image tags", Short: i18n.G("Upgrade recipe image tags"),
Long: `Upgrade a given <recipe> configuration. Long: i18n.G(`Upgrade a given <recipe> configuration.
It will update the relevant compose file tags on the local file system. It will update the relevant compose file tags on the local file system.
@ -52,7 +53,7 @@ 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), Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -71,7 +72,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
if bumpType != 0 { if bumpType != 0 {
// a bitwise check if the number is a power of 2 // a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 { if (bumpType & (bumpType - 1)) != 0 {
log.Fatal("you can only use one of: --major, --minor, --patch.") log.Fatal(i18n.G("you can only use one of: --major, --minor, --patch."))
} }
} }
@ -87,7 +88,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
versionsPath := path.Join(recipe.Dir, "versions") versionsPath := path.Join(recipe.Dir, "versions")
servicePins := make(map[string]imgPin) servicePins := make(map[string]imgPin)
if _, err := os.Stat(versionsPath); err == nil { if _, err := os.Stat(versionsPath); err == nil {
log.Debugf("found versions file for %s", recipe.Name) log.Debug(i18n.G("found versions file for %s", recipe.Name))
file, err := os.Open(versionsPath) file, err := os.Open(versionsPath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -97,7 +98,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
line := scanner.Text() line := scanner.Text()
splitLine := strings.Split(line, " ") splitLine := strings.Split(line, " ")
if splitLine[0] != "pin" || len(splitLine) != 3 { if splitLine[0] != "pin" || len(splitLine) != 3 {
log.Fatalf("malformed version pin specification: %s", line) log.Fatal(i18n.G("malformed version pin specification: %s", line))
} }
pinSlice := strings.Split(splitLine[2], ":") pinSlice := strings.Split(splitLine[2], ":")
pinTag, err := tagcmp.Parse(pinSlice[1]) pinTag, err := tagcmp.Parse(pinSlice[1])
@ -115,7 +116,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
} }
versionsPresent = true versionsPresent = true
} else { } else {
log.Debugf("did not find versions file for %s", recipe.Name) log.Debug(i18n.G("did not find versions file for %s", recipe.Name))
} }
config, err := recipe.GetComposeConfig(nil) config, err := recipe.GetComposeConfig(nil)
@ -135,26 +136,26 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
} }
image := reference.Path(img) image := reference.Path(img)
log.Debugf("retrieved %s from remote registry for %s", regVersions, image) log.Debug(i18n.G("retrieved %s from remote registry for %s", regVersions, image))
image = formatter.StripTagMeta(image) image = formatter.StripTagMeta(image)
switch img.(type) { switch img.(type) {
case reference.NamedTagged: case reference.NamedTagged:
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) { if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
log.Debugf("%s not considered semver-like", img.(reference.NamedTagged).Tag()) log.Debug(i18n.G("%s not considered semver-like", img.(reference.NamedTagged).Tag()))
} }
default: default:
log.Warnf("unable to read tag for image %s, is it missing? skipping upgrade for %s", image, service.Name) log.Warn(i18n.G("unable to read tag for image %s, is it missing? skipping upgrade for %s", image, service.Name))
continue continue
} }
tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag()) tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag())
if err != nil { if err != nil {
log.Warnf("unable to parse %s, error was: %s, skipping upgrade for %s", image, err.Error(), service.Name) log.Warn(i18n.G("unable to parse %s, error was: %s, skipping upgrade for %s", image, err.Error(), service.Name))
continue continue
} }
log.Debugf("parsed %s for %s", tag, service.Name) log.Debug(i18n.G("parsed %s for %s", tag, service.Name))
var compatible []tagcmp.Tag var compatible []tagcmp.Tag
for _, regVersion := range regVersions { for _, regVersion := range regVersions {
@ -168,12 +169,12 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
} }
} }
log.Debugf("detected potential upgradable tags %s for %s", compatible, service.Name) log.Debug(i18n.G("detected potential upgradable tags %s for %s", compatible, service.Name))
sort.Sort(tagcmp.ByTagDesc(compatible)) sort.Sort(tagcmp.ByTagDesc(compatible))
if len(compatible) == 0 && !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(i18n.G("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
} }
@ -195,7 +196,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
} }
} }
log.Debugf("detected compatible upgradable tags %s for %s", compatibleStrings, service.Name) log.Debug(i18n.G("detected compatible upgradable tags %s for %s", compatibleStrings, service.Name))
var upgradeTag string var upgradeTag string
_, ok := servicePins[service.Name] _, ok := servicePins[service.Name]
@ -212,13 +213,13 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
} }
} }
if contains { if contains {
log.Infof("upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString) log.Info(i18n.G("upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString))
} else { } else {
log.Infof("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString) log.Info(i18n.G("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString))
continue continue
} }
} else { } else {
log.Fatalf("service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String()) log.Fatal(i18n.G("service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String()))
continue continue
} }
} else { } else {
@ -235,17 +236,17 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
} }
} }
if upgradeTag == "" { if upgradeTag == "" {
log.Warnf("not upgrading from %s to %s for %s, because the upgrade type is more serious than what user wants", tag.String(), compatible[0].String(), image) log.Warn(i18n.G("not upgrading from %s to %s for %s, because the upgrade type is more serious than what user wants", tag.String(), compatible[0].String(), image))
continue continue
} }
} else { } else {
msg := fmt.Sprintf("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, tag) msg := i18n.G("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, tag)
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || allTags { if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || allTags {
tag := img.(reference.NamedTagged).Tag() tag := img.(reference.NamedTagged).Tag()
if !allTags { if !allTags {
log.Warn(fmt.Sprintf("unable to determine versioning semantics of %s, listing all tags", tag)) log.Warn(i18n.G("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 = i18n.G("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
compatibleStrings = []string{"skip"} compatibleStrings = []string{"skip"}
for _, regVersion := range regVersions { for _, regVersion := range regVersions {
compatibleStrings = append(compatibleStrings, regVersion) compatibleStrings = append(compatibleStrings, regVersion)
@ -276,7 +277,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
} else { } else {
prompt := &survey.Select{ prompt := &survey.Select{
Message: msg, Message: msg,
Help: "enter / return to confirm, choose 'skip' to not upgrade this tag, vim mode is enabled", Help: i18n.G("enter / return to confirm, choose 'skip' to not upgrade this tag, vim mode is enabled"),
VimMode: true, VimMode: true,
Options: compatibleStrings, Options: compatibleStrings,
} }
@ -292,11 +293,11 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
log.Fatal(err) log.Fatal(err)
} }
if ok { if ok {
log.Infof("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image) log.Info(i18n.G("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image))
} }
} else { } else {
if !internal.NoInput { if !internal.NoInput {
log.Warnf("not upgrading %s, skipping as requested", image) log.Warn(i18n.G("not upgrading %s, skipping as requested", image))
} }
} }
} }
@ -314,7 +315,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
} }
for _, upgrade := range upgradeList { for _, upgrade := range upgradeList {
log.Infof("can upgrade service: %s, image: %s, tag: %s ::", upgrade.Service, upgrade.Image, upgrade.Tag) log.Info(i18n.G("can upgrade service: %s, image: %s, tag: %s ::", upgrade.Service, upgrade.Image, upgrade.Tag))
for _, utag := range upgrade.UpgradeTags { for _, utag := range upgrade.UpgradeTags {
log.Infof(" %s", utag) log.Infof(" %s", utag)
} }
@ -326,7 +327,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
log.Fatal(err) log.Fatal(err)
} }
if !isClean { if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name) log.Info(i18n.G("%s currently has these unstaged changes 👇", recipe.Name))
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil { if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -341,41 +342,41 @@ var (
func init() { func init() {
RecipeUpgradeCommand.Flags().BoolVarP( RecipeUpgradeCommand.Flags().BoolVarP(
&internal.Major, &internal.Major,
"major", i18n.G("major"),
"x", i18n.G("x"),
false, false,
"increase the major part of the version", i18n.G("increase the major part of the version"),
) )
RecipeUpgradeCommand.Flags().BoolVarP( RecipeUpgradeCommand.Flags().BoolVarP(
&internal.Minor, &internal.Minor,
"minor", i18n.G("minor"),
"y", i18n.G("y"),
false, false,
"increase the minor part of the version", i18n.G("increase the minor part of the version"),
) )
RecipeUpgradeCommand.Flags().BoolVarP( RecipeUpgradeCommand.Flags().BoolVarP(
&internal.Patch, &internal.Patch,
"patch", i18n.G("patch"),
"z", i18n.G("z"),
false, false,
"increase the patch part of the version", i18n.G("increase the patch part of the version"),
) )
RecipeUpgradeCommand.Flags().BoolVarP( RecipeUpgradeCommand.Flags().BoolVarP(
&internal.MachineReadable, &internal.MachineReadable,
"machine", i18n.G("machine"),
"m", i18n.G("m"),
false, false,
"print machine-readable output", i18n.G("print machine-readable output"),
) )
RecipeUpgradeCommand.Flags().BoolVarP( RecipeUpgradeCommand.Flags().BoolVarP(
&allTags, &allTags,
"all-tags", i18n.G("all-tags"),
"a", i18n.G("a"),
false, false,
"list all tags, not just upgrades", i18n.G("list all tags, not just upgrades"),
) )
} }

View File

@ -7,15 +7,16 @@ 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/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var RecipeVersionCommand = &cobra.Command{ var RecipeVersionCommand = &cobra.Command{
Use: "versions <recipe> [flags]", Use: i18n.G("versions <recipe> [flags]"),
Aliases: []string{"v"}, Aliases: []string{i18n.G("v")},
Short: "List recipe versions", Short: i18n.G("List recipe versions"),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -35,7 +36,7 @@ var RecipeVersionCommand = &cobra.Command{
recipeMeta, ok := catl[recipe.Name] recipeMeta, ok := catl[recipe.Name]
if !ok { if !ok {
warnMessages = append(warnMessages, "retrieved versions from local recipe repository") warnMessages = append(warnMessages, i18n.G("retrieved versions from local recipe repository"))
recipeVersions, warnMsg, err := recipe.GetRecipeVersions() recipeVersions, warnMsg, err := recipe.GetRecipeVersions()
if err != nil { if err != nil {
@ -49,7 +50,7 @@ var RecipeVersionCommand = &cobra.Command{
} }
if len(recipeMeta.Versions) == 0 { if len(recipeMeta.Versions) == 0 {
log.Fatalf("%s has no published versions?", recipe.Name) log.Fatal(i18n.G("%s has no published versions?", recipe.Name))
} }
for i := len(recipeMeta.Versions) - 1; i >= 0; i-- { for i := len(recipeMeta.Versions) - 1; i >= 0; i-- {
@ -58,7 +59,7 @@ var RecipeVersionCommand = &cobra.Command{
log.Fatal(err) log.Fatal(err)
} }
table.Headers("SERVICE", "IMAGE", "TAG", "VERSION") table.Headers(i18n.G("SERVICE"), i18n.G("IMAGE"), i18n.G("TAG"), i18n.G("VERSION"))
for version, meta := range recipeMeta.Versions[i] { for version, meta := range recipeMeta.Versions[i] {
var allRows [][]string var allRows [][]string
@ -99,10 +100,10 @@ var RecipeVersionCommand = &cobra.Command{
if internal.MachineReadable { if internal.MachineReadable {
sort.Slice(allRows, sortServiceByName(allRows)) sort.Slice(allRows, sortServiceByName(allRows))
headers := []string{"VERSION", "SERVICE", "NAME", "TAG"} headers := []string{i18n.G("VERSION"), i18n.G("SERVICE"), i18n.G("NAME"), i18n.G("TAG")}
out, err := formatter.ToJSON(headers, allRows) out, err := formatter.ToJSON(headers, allRows)
if err != nil { if err != nil {
log.Fatal("unable to render to JSON: %s", err) log.Fatal(i18n.G("unable to render to JSON: %s", err))
} }
fmt.Println(out) fmt.Println(out)
continue continue
@ -127,9 +128,9 @@ func sortServiceByName(versions [][]string) func(i, j int) bool {
func init() { func init() {
RecipeVersionCommand.Flags().BoolVarP( RecipeVersionCommand.Flags().BoolVarP(
&internal.MachineReadable, &internal.MachineReadable,
"machine", i18n.G("machine"),
"m", i18n.G("m"),
false, false,
"print machine-readable output", i18n.G("print machine-readable output"),
) )
} }

View File

@ -1,6 +1,7 @@
package cli package cli
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
@ -10,6 +11,8 @@ import (
"coopcloud.tech/abra/cli/recipe" "coopcloud.tech/abra/cli/recipe"
"coopcloud.tech/abra/cli/server" "coopcloud.tech/abra/cli/server"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
charmLog "github.com/charmbracelet/log" charmLog "github.com/charmbracelet/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -18,34 +21,35 @@ import (
func Run(version, commit string) { func Run(version, commit string) {
rootCmd := &cobra.Command{ rootCmd := &cobra.Command{
Use: "abra [cmd] [args] [flags]", Use: i18n.G("abra [cmd] [args] [flags]"),
Short: "The Co-op Cloud command-line utility belt 🎩🐇", Short: i18n.G("The Co-op Cloud command-line utility belt 🎩🐇"),
Version: fmt.Sprintf("%s-%s", version, commit[:7]), Version: fmt.Sprintf("%s-%s", version, commit[:7]),
ValidArgs: []string{ ValidArgs: []string{
"app", i18n.G("app"),
"autocomplete", i18n.G("autocomplete"),
"catalogue", i18n.G("catalogue"),
"man", i18n.G("man"),
"recipe", i18n.G("recipe"),
"server", i18n.G("server"),
"upgrade", i18n.G("upgrade"),
}, },
PersistentPreRun: func(cmd *cobra.Command, args []string) {
paths := []string{ PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
config.ABRA_DIR, dirs := []map[string]os.FileMode{
config.SERVERS_DIR, {config.ABRA_DIR: 0764},
config.RECIPES_DIR, {config.SERVERS_DIR: 0700},
config.LOGS_DIR, {config.RECIPES_DIR: 0764},
config.VENDOR_DIR, // TODO(d1): remove > 0.9.x {config.LOGS_DIR: 0764},
config.BACKUP_DIR, // TODO(d1): remove > 0.9.x
} }
for _, path := range paths { for _, dir := range dirs {
if err := os.Mkdir(path, 0764); err != nil { for path, perm := range dir {
if !os.IsExist(err) { if err := os.Mkdir(path, perm); err != nil {
log.Fatal(err) if !os.IsExist(err) {
return errors.New(i18n.G("unable to create %s: %s", path, err))
}
continue
} }
continue
} }
} }
@ -62,23 +66,29 @@ func Run(version, commit string) {
log.SetReportCaller(true) log.SetReportCaller(true)
} }
log.Debugf("abra version %s, commit %s", version, commit) log.Debug(i18n.G(
"abra version: %s, commit: %s, lang: %s",
version, formatter.SmallSHA(commit), i18n.Locale,
))
return nil
}, },
} }
rootCmd.CompletionOptions.DisableDefaultCmd = true rootCmd.CompletionOptions.DisableDefaultCmd = true
manCommand := &cobra.Command{ manCommand := &cobra.Command{
Use: "man [flags]", Use: i18n.G("man [flags]"),
Aliases: []string{"m"}, Aliases: []string{"m"},
Short: "Generate manpage", Short: i18n.G("Generate manpage"),
Example: ` # generate the man pages into /usr/local/share/man/man1 Example: i18n.G(` # generate the man pages into /usr/local/share/man/man1
sudo abra man abra_path=$(which abra) # pass abra absolute path to sudo below
sudo $abra_path man
sudo mandb sudo mandb
# read the man pages # read the man pages
man abra man abra
man abra-app-deploy`, man abra-app-deploy`),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
header := &doc.GenManHeader{ header := &doc.GenManHeader{
Title: "ABRA", Title: "ABRA",
@ -87,7 +97,7 @@ func Run(version, commit string) {
manDir := "/usr/local/share/man/man1" manDir := "/usr/local/share/man/man1"
if _, err := os.Stat(manDir); os.IsNotExist(err) { if _, err := os.Stat(manDir); os.IsNotExist(err) {
log.Fatalf("unable to proceed, '%s' does not exist?") log.Fatal(i18n.G("unable to proceed, %s does not exist?", manDir))
} }
err := doc.GenManTree(rootCmd, header, manDir) err := doc.GenManTree(rootCmd, header, manDir)
@ -95,7 +105,7 @@ func Run(version, commit string) {
log.Fatal(err) log.Fatal(err)
} }
log.Info("don't forget to run 'sudo mandb'") log.Info(i18n.G("don't forget to run 'sudo mandb'"))
}, },
} }
@ -104,7 +114,7 @@ func Run(version, commit string) {
"debug", "debug",
"d", "d",
false, false,
"show debug messages", i18n.G("show debug messages"),
) )
rootCmd.PersistentFlags().BoolVarP( rootCmd.PersistentFlags().BoolVarP(
@ -112,7 +122,7 @@ func Run(version, commit string) {
"no-input", "no-input",
"n", "n",
false, false,
"toggle non-interactive mode", i18n.G("toggle non-interactive mode"),
) )
rootCmd.PersistentFlags().BoolVarP( rootCmd.PersistentFlags().BoolVarP(
@ -120,7 +130,7 @@ func Run(version, commit string) {
"offline", "offline",
"o", "o",
false, false,
"prefer offline & filesystem access", i18n.G("prefer offline & filesystem access"),
) )
rootCmd.PersistentFlags().BoolVarP( rootCmd.PersistentFlags().BoolVarP(
@ -128,7 +138,7 @@ func Run(version, commit string) {
"ignore-env-version", "ignore-env-version",
"i", "i",
false, false,
"ignore .env version checkout", i18n.G("ignore .env version checkout"),
) )
catalogue.CatalogueCommand.AddCommand( catalogue.CatalogueCommand.AddCommand(

View File

@ -10,6 +10,7 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
contextPkg "coopcloud.tech/abra/pkg/context" contextPkg "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/dns" "coopcloud.tech/abra/pkg/dns"
"coopcloud.tech/abra/pkg/i18n"
"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"
@ -17,10 +18,10 @@ import (
) )
var ServerAddCommand = &cobra.Command{ var ServerAddCommand = &cobra.Command{
Use: "add [[server] | --local] [flags]", Use: i18n.G("add [[server] | --local] [flags]"),
Aliases: []string{"a"}, Aliases: []string{i18n.G("a")},
Short: "Add a new server", Short: i18n.G("Add a new server"),
Long: `Add a new server to your configuration so that it can be managed by Abra. Long: i18n.G(`Add a new server to your configuration so that it can be managed by Abra.
Abra relies on the standard SSH command-line and ~/.ssh/config for client Abra relies on the standard SSH command-line and ~/.ssh/config for client
connection details. You must configure an entry per-host in your ~/.ssh/config connection details. You must configure an entry per-host in your ~/.ssh/config
@ -35,8 +36,8 @@ for each server:
If "--local" is passed, then Abra assumes that the current local server is If "--local" is passed, then Abra assumes that the current local server is
intended as the target server. This is useful when you want to have your entire intended as the target server. This is useful when you want to have your entire
Co-op Cloud config located on the server itself, and not on your local Co-op Cloud config located on the server itself, and not on your local
developer machine. The domain is then set to "default".`, developer machine. The domain is then set to "default".`),
Example: " abra server add 1312.net", Example: i18n.G(" abra server add 1312.net"),
Args: cobra.RangeArgs(0, 1), Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -49,11 +50,11 @@ developer machine. The domain is then set to "default".`,
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if len(args) > 0 && local { if len(args) > 0 && local {
log.Fatal("cannot use [server] and --local together") log.Fatal(i18n.G("cannot use [server] and --local together"))
} }
if len(args) == 0 && !local { if len(args) == 0 && !local {
log.Fatal("missing argument or --local/-l flag") log.Fatal(i18n.G("missing argument or --local/-l flag"))
} }
name := "default" name := "default"
@ -72,7 +73,7 @@ developer machine. The domain is then set to "default".`,
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("attempting to create client for %s", name) log.Debug(i18n.G("attempting to create client for %s", name))
if _, err := client.New(name, timeout); err != nil { if _, err := client.New(name, timeout); err != nil {
cleanUp(name) cleanUp(name)
@ -80,9 +81,9 @@ developer machine. The domain is then set to "default".`,
} }
if created { if created {
log.Info("local server successfully added") log.Info(i18n.G("local server successfully added"))
} else { } else {
log.Warn("local server already exists") log.Warn(i18n.G("local server already exists"))
} }
return return
@ -96,27 +97,27 @@ developer machine. The domain is then set to "default".`,
created, err := newContext(name) created, err := newContext(name)
if err != nil { if err != nil {
cleanUp(name) cleanUp(name)
log.Fatalf("unable to create local context: %s", err) log.Fatal(i18n.G("unable to create local context: %s", err))
} }
log.Debugf("attempting to create client for %s", name) log.Debug(i18n.G("attempting to create client for %s", name))
if _, err := client.New(name, timeout); err != nil { if _, err := client.New(name, timeout); err != nil {
cleanUp(name) cleanUp(name)
log.Fatalf("ssh %s error: %s", name, sshPkg.Fatal(name, err)) log.Fatal(i18n.G("ssh %s error: %s", name, sshPkg.Fatal(name, err)))
} }
if created { if created {
log.Infof("%s successfully added", name) log.Info(i18n.G("%s successfully added", name))
if _, err := dns.EnsureIPv4(name); err != nil { if _, err := dns.EnsureIPv4(name); err != nil {
log.Warnf("unable to resolve IPv4 for %s", name) log.Warn(i18n.G("unable to resolve IPv4 for %s", name))
} }
return return
} }
log.Warnf("%s already exists", name) log.Warn(i18n.G("%s already exists", name))
}, },
} }
@ -124,7 +125,7 @@ developer machine. The domain is then set to "default".`,
// "server add" attempt. // "server add" attempt.
func cleanUp(name string) { func cleanUp(name string) {
if name != "default" { if name != "default" {
log.Debugf("serverAdd: cleanUp: cleaning up context for %s", name) log.Debug(i18n.G("serverAdd: cleanUp: cleaning up context for %s", name))
if err := client.DeleteContext(name); err != nil { if err := client.DeleteContext(name); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -133,16 +134,16 @@ func cleanUp(name string) {
serverDir := filepath.Join(config.SERVERS_DIR, name) serverDir := filepath.Join(config.SERVERS_DIR, name)
files, err := config.GetAllFilesInDirectory(serverDir) files, err := config.GetAllFilesInDirectory(serverDir)
if err != nil { if err != nil {
log.Fatalf("serverAdd: cleanUp: unable to list files in %s: %s", serverDir, err) log.Fatal(i18n.G("serverAdd: cleanUp: unable to list files in %s: %s", serverDir, err))
} }
if len(files) > 0 { if len(files) > 0 {
log.Debugf("serverAdd: cleanUp: %s is not empty, aborting cleanup", serverDir) log.Debug(i18n.G("serverAdd: cleanUp: %s is not empty, aborting cleanup", serverDir))
return return
} }
if err := os.RemoveAll(serverDir); err != nil { if err := os.RemoveAll(serverDir); err != nil {
log.Fatalf("serverAdd: cleanUp: failed to remove %s: %s", serverDir, err) log.Fatal(i18n.G("serverAdd: cleanUp: failed to remove %s: %s", serverDir, err))
} }
} }
@ -159,12 +160,12 @@ func newContext(name string) (bool, error) {
for _, context := range contexts { for _, context := range contexts {
if context.Name == name { if context.Name == name {
log.Debugf("context for %s already exists", name) log.Debug(i18n.G("context for %s already exists", name))
return false, nil return false, nil
} }
} }
log.Debugf("creating context with domain %s", name) log.Debugf(i18n.G("creating context with domain %s", name))
if err := client.CreateContext(name); err != nil { if err := client.CreateContext(name); err != nil {
return false, nil return false, nil
@ -180,7 +181,7 @@ func createServerDir(name string) (bool, error) {
return false, err return false, err
} }
log.Debugf("server dir for %s already created", name) log.Debug(i18n.G("server dir for %s already created", name))
return false, nil return false, nil
} }
@ -195,9 +196,9 @@ var (
func init() { func init() {
ServerAddCommand.Flags().BoolVarP( ServerAddCommand.Flags().BoolVarP(
&local, &local,
"local", i18n.G("local"),
"l", i18n.G("l"),
false, false,
"use local server", i18n.G("use local server"),
) )
} }

View File

@ -8,15 +8,16 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
contextPkg "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/i18n"
"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/spf13/cobra" "github.com/spf13/cobra"
) )
var ServerListCommand = &cobra.Command{ var ServerListCommand = &cobra.Command{
Use: "list [flags]", Use: i18n.G("list [flags]"),
Aliases: []string{"ls"}, Aliases: []string{i18n.G("ls")},
Short: "List managed servers", Short: i18n.G("List managed servers"),
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
dockerContextStore := contextPkg.NewDefaultDockerContextStore() dockerContextStore := contextPkg.NewDefaultDockerContextStore()
@ -30,7 +31,7 @@ var ServerListCommand = &cobra.Command{
log.Fatal(err) log.Fatal(err)
} }
headers := []string{"NAME", "HOST"} headers := []string{i18n.G("NAME"), i18n.G("HOST")}
table.Headers(headers...) table.Headers(headers...)
serverNames, err := config.ReadServerNames() serverNames, err := config.ReadServerNames()
@ -55,7 +56,7 @@ var ServerListCommand = &cobra.Command{
} }
if sp.Host == "" { if sp.Host == "" {
sp.Host = "unknown" sp.Host = i18n.G("unknown")
} }
row = []string{serverName, sp.Host} row = []string{serverName, sp.Host}
@ -65,9 +66,9 @@ var ServerListCommand = &cobra.Command{
if len(row) == 0 { if len(row) == 0 {
if serverName == "default" { if serverName == "default" {
row = []string{serverName, "local"} row = []string{serverName, i18n.G("local")}
} else { } else {
row = []string{serverName, "unknown"} row = []string{serverName, i18n.G("unknown")}
} }
rows = append(rows, row) rows = append(rows, row)
} }
@ -78,7 +79,7 @@ var ServerListCommand = &cobra.Command{
if internal.MachineReadable { if internal.MachineReadable {
out, err := formatter.ToJSON(headers, rows) out, err := formatter.ToJSON(headers, rows)
if err != nil { if err != nil {
log.Fatal("unable to render to JSON: %s", err) log.Fatal(i18n.G("unable to render to JSON: %s", err))
} }
fmt.Println(out) fmt.Println(out)
@ -95,9 +96,9 @@ var ServerListCommand = &cobra.Command{
func init() { func init() {
ServerListCommand.Flags().BoolVarP( ServerListCommand.Flags().BoolVarP(
&internal.MachineReadable, &internal.MachineReadable,
"machine", i18n.G("machine"),
"m", i18n.G("m"),
false, false,
"print machine-readable output", i18n.G("print machine-readable output"),
) )
} }

View File

@ -5,19 +5,20 @@ 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/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"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/spf13/cobra" "github.com/spf13/cobra"
) )
var ServerPruneCommand = &cobra.Command{ var ServerPruneCommand = &cobra.Command{
Use: "prune <server> [flags]", Use: i18n.G("prune <server> [flags]"),
Aliases: []string{"p"}, Aliases: []string{i18n.G("p")},
Short: "Prune resources on a server", Short: i18n.G("Prune resources on a server"),
Long: `Prunes unused containers, networks, and dangling images. Long: i18n.G(`Prunes unused containers, networks, and dangling images.
Use "--volumes/-v" 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.`),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -41,18 +42,18 @@ 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.Info(i18n.G("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed))
nr, err := cl.NetworksPrune(cmd.Context(), filterArgs) nr, err := cl.NetworksPrune(cmd.Context(), filterArgs)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
log.Infof("networks pruned: %d", len(nr.NetworksDeleted)) log.Info(i18n.G("networks pruned: %d", len(nr.NetworksDeleted)))
pruneFilters := filters.NewArgs() pruneFilters := filters.NewArgs()
if allFilter { if allFilter {
log.Debugf("removing all images, not only dangling ones") log.Debug(i18n.G("removing all images, not only dangling ones"))
pruneFilters.Add("dangling", "false") pruneFilters.Add("dangling", "false")
} }
@ -62,7 +63,7 @@ app. This can result in unwanted data loss if not used carefully.`,
} }
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed) imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
log.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed) log.Info(i18n.G("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed))
if volumesFilter { if volumesFilter {
vr, err := cl.VolumesPrune(cmd.Context(), filterArgs) vr, err := cl.VolumesPrune(cmd.Context(), filterArgs)
@ -71,7 +72,7 @@ app. This can result in unwanted data loss if not used carefully.`,
} }
volSpaceReclaimed := formatter.ByteCountSI(vr.SpaceReclaimed) volSpaceReclaimed := formatter.ByteCountSI(vr.SpaceReclaimed)
log.Infof("volumes pruned: %d; space reclaimed: %s", len(vr.VolumesDeleted), volSpaceReclaimed) log.Info(i18n.G("volumes pruned: %d; space reclaimed: %s", len(vr.VolumesDeleted), volSpaceReclaimed))
} }
return return
@ -86,17 +87,17 @@ var (
func init() { func init() {
ServerPruneCommand.Flags().BoolVarP( ServerPruneCommand.Flags().BoolVarP(
&allFilter, &allFilter,
"all", i18n.G("all"),
"a", i18n.G("a"),
false, false,
"remove all unused images", i18n.G("remove all unused images"),
) )
ServerPruneCommand.Flags().BoolVarP( ServerPruneCommand.Flags().BoolVarP(
&volumesFilter, &volumesFilter,
"volumes", i18n.G("volumes"),
"v", i18n.G("v"),
false, false,
"remove volumes", i18n.G("remove volumes"),
) )
} }

View File

@ -8,19 +8,20 @@ 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/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var ServerRemoveCommand = &cobra.Command{ var ServerRemoveCommand = &cobra.Command{
Use: "remove <server> [flags]", Use: i18n.G("remove <server> [flags]"),
Aliases: []string{"rm"}, Aliases: []string{i18n.G("rm")},
Short: "Remove a managed server", Short: i18n.G("Remove a managed server"),
Long: `Remove a managed server. Long: i18n.G(`Remove a managed server.
Abra will remove the internal bookkeeping ($ABRA_DIR/servers/...) and Abra will remove the internal bookkeeping ($ABRA_DIR/servers/...) and
underlying client connection context. This server will then be lost in time, underlying client connection context. This server will then be lost in time,
like tears in rain.`, like tears in rain.`),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -39,7 +40,7 @@ like tears in rain.`,
log.Fatal(err) log.Fatal(err)
} }
log.Infof("%s is now lost in time, like tears in rain", serverName) log.Info(i18n.G("%s is now lost in time, like tears in rain", serverName))
return return
}, },

View File

@ -1,10 +1,13 @@
package server package server
import "github.com/spf13/cobra" import (
"coopcloud.tech/abra/pkg/i18n"
"github.com/spf13/cobra"
)
// ServerCommand defines the `abra server` command and its subcommands // ServerCommand defines the `abra server` command and its subcommands
var ServerCommand = &cobra.Command{ var ServerCommand = &cobra.Command{
Use: "server [cmd] [args] [flags]", Use: i18n.G("server [cmd] [args] [flags]"),
Aliases: []string{"s"}, Aliases: []string{i18n.G("s")},
Short: "Manage servers", Short: i18n.G("Manage servers"),
} }

View File

@ -2,6 +2,7 @@ package updater
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"strconv" "strconv"
@ -11,6 +12,7 @@ import (
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/convert" "coopcloud.tech/abra/pkg/upstream/convert"
@ -30,14 +32,14 @@ const SERVER = "localhost"
// NotifyCommand checks for available upgrades. // NotifyCommand checks for available upgrades.
var NotifyCommand = &cobra.Command{ var NotifyCommand = &cobra.Command{
Use: "notify [flags]", Use: i18n.G("notify [flags]"),
Aliases: []string{"n"}, Aliases: []string{i18n.G("n")},
Short: "Check for available upgrades", Short: i18n.G("Check for available upgrades"),
Long: `Notify on new versions for deployed apps. Long: i18n.G(`Notify on new versions for deployed apps.
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/-m" to include new major versions.`, Use "--major/-m" to include new major versions.`),
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
cl, err := client.New("default") cl, err := client.New("default")
@ -69,10 +71,10 @@ Use "--major/-m" to include new major versions.`,
// UpgradeCommand upgrades apps. // UpgradeCommand upgrades apps.
var UpgradeCommand = &cobra.Command{ var UpgradeCommand = &cobra.Command{
Use: "upgrade [[stack] [recipe] | --all] [flags]", Use: i18n.G("upgrade [[stack] [recipe] | --all] [flags]"),
Aliases: []string{"u"}, Aliases: []string{i18n.G("u")},
Short: "Upgrade apps", Short: i18n.G("Upgrade apps"),
Long: `Upgrade an app by specifying stack name and recipe. Long: i18n.G(`Upgrade an app by specifying stack name and recipe.
Use "--all" to upgrade every deployed app. Use "--all" to upgrade every deployed app.
@ -83,7 +85,7 @@ available, the app is upgraded.
To include major versions use the "--major/-m" flag. You probably don't want To include major versions use the "--major/-m" flag. You probably don't want
that as it will break things. Only apps that are not deployed with "--chaos/-C" that as it will break things. Only apps that are not deployed with "--chaos/-C"
are upgraded, to update chaos deployments use the "--chaos/-C" flag. Use it are upgraded, to update chaos deployments use the "--chaos/-C" flag. Use it
with care.`, with care.`),
Args: cobra.RangeArgs(0, 2), Args: cobra.RangeArgs(0, 2),
// TODO(d1): complete stack/recipe // TODO(d1): complete stack/recipe
// ValidArgsFunction: func( // ValidArgsFunction: func(
@ -98,7 +100,7 @@ with care.`,
} }
if !updateAll && len(args) != 2 { if !updateAll && len(args) != 2 {
log.Fatal("missing arguments or --all/-a flag") log.Fatal(i18n.G("missing arguments or --all/-a flag"))
} }
if !updateAll { if !updateAll {
@ -150,7 +152,7 @@ func getLabel(cl *dockerclient.Client, stackName string, label string) (string,
} }
} }
log.Debugf("no %s label found for %s", label, stackName) log.Debug(i18n.G("no %s label found for %s", label, stackName))
return "", nil return "", nil
} }
@ -171,7 +173,7 @@ func getBoolLabel(cl *dockerclient.Client, stackName string, label string) (bool
return value, nil return value, nil
} }
log.Debugf("boolean label %s could not be found for %s, set default to false.", label, stackName) log.Debug(i18n.G("boolean label %s could not be found for %s, set default to false.", label, stackName))
return false, nil return false, nil
} }
@ -192,12 +194,12 @@ func getEnv(cl *dockerclient.Client, stackName string) (envfile.AppEnv, error) {
for _, envString := range envList { for _, envString := range envList {
splitString := strings.SplitN(envString, "=", 2) splitString := strings.SplitN(envString, "=", 2)
if len(splitString) != 2 { if len(splitString) != 2 {
log.Debugf("can't separate key from value: %s (this variable is probably unset)", envString) log.Debug(i18n.G("can't separate key from value: %s (this variable is probably unset)", envString))
continue continue
} }
k := splitString[0] k := splitString[0]
v := splitString[1] v := splitString[1]
log.Debugf("for %s read env %s with value: %s from docker service", stackName, k, v) log.Debugf(i18n.G("for %s read env %s with value: %s from docker service", stackName, k, v))
envMap[k] = v envMap[k] = v
} }
} }
@ -219,14 +221,14 @@ func getLatestUpgrade(cl *dockerclient.Client, stackName string, recipeName stri
} }
if len(availableUpgrades) == 0 { if len(availableUpgrades) == 0 {
log.Debugf("no available upgrades for %s", stackName) log.Debugf(i18n.G("no available upgrades for %s", stackName))
return "", nil return "", nil
} }
var chosenUpgrade string var chosenUpgrade string
if len(availableUpgrades) > 0 { if len(availableUpgrades) > 0 {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1] chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
log.Infof("%s (%s) can be upgraded from version %s to %s", stackName, recipeName, deployedVersion, chosenUpgrade) log.Info(i18n.G("%s (%s) can be upgraded from version %s to %s", stackName, recipeName, deployedVersion, chosenUpgrade))
} }
return chosenUpgrade, nil return chosenUpgrade, nil
@ -234,7 +236,7 @@ func getLatestUpgrade(cl *dockerclient.Client, stackName string, recipeName stri
// getDeployedVersion returns the currently deployed version of an app. // getDeployedVersion returns the currently deployed version of an app.
func getDeployedVersion(cl *dockerclient.Client, stackName string, recipeName string) (string, error) { func getDeployedVersion(cl *dockerclient.Client, stackName string, recipeName string) (string, error) {
log.Debugf("retrieve deployed version whether %s is already deployed", stackName) log.Debug(i18n.G("retrieve deployed version whether %s is already deployed", stackName))
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName) deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil { if err != nil {
@ -242,11 +244,11 @@ func getDeployedVersion(cl *dockerclient.Client, stackName string, recipeName st
} }
if !deployMeta.IsDeployed { if !deployMeta.IsDeployed {
return "", fmt.Errorf("%s is not deployed?", stackName) return "", errors.New(i18n.G("%s is not deployed?", stackName))
} }
if deployMeta.Version == "unknown" { if deployMeta.Version == "unknown" {
return "", fmt.Errorf("failed to determine deployed version of %s", stackName) return "", errors.New(i18n.G("failed to determine deployed version of %s", stackName))
} }
return deployMeta.Version, nil return deployMeta.Version, nil
@ -268,7 +270,7 @@ func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName
} }
if len(versions) == 0 { if len(versions) == 0 {
log.Warnf("no published releases for %s in the recipe catalogue?", recipeName) log.Warn(i18n.G("no published releases for %s in the recipe catalogue?", recipeName))
return nil, nil return nil, nil
} }
@ -294,7 +296,7 @@ func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName
} }
} }
log.Debugf("available updates for %s: %s", stackName, availableUpgrades) log.Debug(i18n.G("available updates for %s: %s", stackName, availableUpgrades))
return availableUpgrades, nil return availableUpgrades, nil
} }
@ -371,7 +373,7 @@ func createDeployConfig(r recipe.Recipe, stackName string, env envfile.AppEnv) (
// tryUpgrade performs the upgrade if all the requirements are fulfilled. // tryUpgrade performs the upgrade if all the requirements are fulfilled.
func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error { func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
if recipeName == "" { if recipeName == "" {
log.Debugf("don't update %s due to missing recipe name", stackName) log.Debug(i18n.G("don't update %s due to missing recipe name", stackName))
return nil return nil
} }
@ -381,7 +383,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
} }
if chaos && !internal.Chaos { if chaos && !internal.Chaos {
log.Debugf("don't update %s due to chaos deployment", stackName) log.Debug(i18n.G("don't update %s due to chaos deployment", stackName))
return nil return nil
} }
@ -391,7 +393,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
} }
if !updatesEnabled { if !updatesEnabled {
log.Debugf("don't update %s due to disabled auto updates or missing ENABLE_AUTO_UPDATE env", stackName) log.Debug(i18n.G("don't update %s due to disabled auto updates or missing ENABLE_AUTO_UPDATE env", stackName))
return nil return nil
} }
@ -401,7 +403,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
} }
if upgradeVersion == "" { if upgradeVersion == "" {
log.Debugf("don't update %s due to no new version", stackName) log.Debug(i18n.G("don't update %s due to no new version", stackName))
return nil return nil
} }
@ -439,7 +441,7 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion stri
return err return err
} }
log.Infof("upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion) log.Info(i18n.G("upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion))
serviceNames, err := appPkg.GetAppServiceNames(app.Name) serviceNames, err := appPkg.GetAppServiceNames(app.Name)
if err != nil { if err != nil {
@ -466,9 +468,9 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion stri
func newKadabraApp(version, commit string) *cobra.Command { func newKadabraApp(version, commit string) *cobra.Command {
rootCmd := &cobra.Command{ rootCmd := &cobra.Command{
Use: "kadabra [cmd] [flags]", Use: i18n.G("kadabra [cmd] [flags]"),
Version: fmt.Sprintf("%s-%s", version, commit[:7]), Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Short: "The Co-op Cloud auto-updater 🤖 🚀", Short: i18n.G("The Co-op Cloud auto-updater 🤖 🚀"),
PersistentPreRun: func(cmd *cobra.Command, args []string) { PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.Logger.SetStyles(charmLog.DefaultStyles()) log.Logger.SetStyles(charmLog.DefaultStyles())
charmLog.SetDefault(log.Logger) charmLog.SetDefault(log.Logger)
@ -479,18 +481,24 @@ func newKadabraApp(version, commit string) *cobra.Command {
log.SetReportCaller(true) log.SetReportCaller(true)
} }
log.Debugf("kadabra version %s, commit %s", version, commit) log.Debug(i18n.G("kadabra version %s, commit %s", version, commit))
}, },
} }
rootCmd.PersistentFlags().BoolVarP( rootCmd.PersistentFlags().BoolVarP(
&internal.Debug, "debug", "d", false, &internal.Debug,
"show debug messages", i18n.G("debug"),
i18n.G("d"),
false,
i18n.G("show debug messages"),
) )
rootCmd.PersistentFlags().BoolVarP( rootCmd.PersistentFlags().BoolVarP(
&internal.NoInput, "no-input", "n", false, &internal.NoInput,
"toggle non-interactive mode", i18n.G("no-input"),
i18n.G("n"),
false,
i18n.G("toggle non-interactive mode"),
) )
rootCmd.AddCommand( rootCmd.AddCommand(
@ -526,25 +534,25 @@ func init() {
UpgradeCommand.Flags().BoolVarP( UpgradeCommand.Flags().BoolVarP(
&internal.Chaos, &internal.Chaos,
"chaos", i18n.G("chaos"),
"C", i18n.G("C"),
false, false,
"ignore uncommitted recipes changes", i18n.G("ignore uncommitted recipes changes"),
) )
UpgradeCommand.Flags().BoolVarP( UpgradeCommand.Flags().BoolVarP(
&includeMajorUpdates, &includeMajorUpdates,
"major", i18n.G("major"),
"m", i18n.G("m"),
false, false,
"check for major updates", i18n.G("check for major updates"),
) )
UpgradeCommand.Flags().BoolVarP( UpgradeCommand.Flags().BoolVarP(
&updateAll, &updateAll,
"all", i18n.G("all"),
"a", i18n.G("a"),
false, false,
"update all deployed apps", i18n.G("update all deployed apps"),
) )
} }

View File

@ -7,22 +7,23 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// UpgradeCommand upgrades abra in-place. // UpgradeCommand upgrades abra in-place.
var UpgradeCommand = &cobra.Command{ var UpgradeCommand = &cobra.Command{
Use: "upgrade [flags]", Use: i18n.G("upgrade [flags]"),
Aliases: []string{"u"}, Aliases: []string{"u"},
Short: "Upgrade abra", Short: i18n.G("Upgrade abra"),
Long: `Upgrade abra in-place with the latest stable or release candidate. Long: i18n.G(`Upgrade abra in-place with the latest stable or release candidate.
By default, the latest stable release is downloaded. By default, the latest stable release is downloaded.
Use "--rc/-r" to install the latest release candidate. Please bear in mind that 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 it may contain absolutely catastrophic deal-breaker bugs. Thank you very much
for the testing efforts 💗`, for the testing efforts 💗`),
Example: " abra upgrade --rc", Example: i18n.G(" abra upgrade --rc"),
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
mainURL := "https://install.abra.coopcloud.tech" mainURL := "https://install.abra.coopcloud.tech"
@ -33,7 +34,7 @@ for the testing efforts 💗`,
c = exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash -s -- --rc", releaseCandidateURL)) c = exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash -s -- --rc", releaseCandidateURL))
} }
log.Debugf("attempting to run %s", c) log.Debugf(i18n.G("attempting to run %s", c))
if err := internal.RunCmd(c); err != nil { if err := internal.RunCmd(c); err != nil {
log.Fatal(err) log.Fatal(err)
@ -51,6 +52,6 @@ func init() {
"rc", "rc",
"r", "r",
false, false,
"install release candidate (may contain bugs)", i18n.G("install release candidate (may contain bugs)"),
) )
} }

113
go.mod
View File

@ -1,71 +1,73 @@
module coopcloud.tech/abra module coopcloud.tech/abra
go 1.23.0 go 1.24.0
toolchain go1.23.1 toolchain go1.24.1
require ( require (
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c
github.com/AlecAivazis/survey/v2 v2.3.7 github.com/AlecAivazis/survey/v2 v2.3.7
github.com/charmbracelet/bubbletea v1.3.4 github.com/charmbracelet/bubbletea v1.3.6
github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v0.4.1 github.com/charmbracelet/log v0.4.2
github.com/distribution/reference v0.6.0 github.com/distribution/reference v0.6.0
github.com/docker/cli v28.0.1+incompatible github.com/docker/cli v28.3.3+incompatible
github.com/docker/docker v28.0.1+incompatible github.com/docker/docker v28.3.3+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.14.0 github.com/go-git/go-git/v5 v5.16.2
github.com/google/go-cmp v0.7.0 github.com/google/go-cmp v0.7.0
github.com/leonelquinteros/gotext v1.7.2
github.com/moby/sys/signal v0.7.1 github.com/moby/sys/signal v0.7.1
github.com/moby/term v0.5.2 github.com/moby/term v0.5.2
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.18.0 github.com/schollz/progressbar/v3 v3.18.0
golang.org/x/term v0.30.0 golang.org/x/term v0.34.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.2 gotest.tools/v3 v3.5.2
) )
require ( require (
dario.cat/mergo v1.0.1 // indirect dario.cat/mergo v1.0.2 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/BurntSushi/toml v1.4.0 // indirect github.com/BurntSushi/toml v1.5.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.1.6 // indirect github.com/ProtonMail/go-crypto v1.3.0 // 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/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/colorprofile v0.3.2 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cloudflare/circl v1.6.0 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.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.6 // indirect github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // 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-connections v0.6.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-metrics v0.0.1 // indirect
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/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // 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.6.2 // indirect github.com/go-git/go-billy/v5 v5.6.2 // 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.3 // 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.2.1 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // 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.26.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.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
@ -80,10 +82,10 @@ require (
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/moby/docker-image-spec v1.3.1 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/mountinfo v0.6.2 // indirect github.com/moby/go-archive v0.1.0 // indirect
github.com/moby/sys/user v0.3.0 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.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/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
@ -93,43 +95,42 @@ require (
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/opencontainers/runtime-spec v1.1.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pjbgf/sha1cd v0.4.0 // indirect
github.com/pjbgf/sha1cd v0.3.2 // 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.2 // indirect
github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.17.0 // 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.3.1 // indirect github.com/skeema/knownhosts v1.3.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect github.com/spf13/pflag v1.0.7 // 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
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.35.0 // indirect go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect
golang.org/x/crypto v0.36.0 // indirect golang.org/x/crypto v0.41.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
golang.org/x/net v0.37.0 // indirect golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.12.0 // indirect golang.org/x/sync v0.16.0 // indirect
golang.org/x/text v0.23.0 // indirect golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.11.0 // indirect golang.org/x/time v0.12.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect
google.golang.org/grpc v1.71.0 // indirect google.golang.org/grpc v1.74.2 // indirect
google.golang.org/protobuf v1.36.5 // indirect google.golang.org/protobuf v1.36.7 // 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
) )
@ -142,15 +143,15 @@ require (
github.com/fvbommel/sortorder v1.1.0 // indirect github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.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.8
github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/sequential v0.6.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/prometheus/client_golang v1.21.1 // indirect github.com/prometheus/client_golang v1.23.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sergi/go-diff v1.4.0 // indirect
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.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/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
golang.org/x/sys v0.31.0 golang.org/x/sys v0.35.0
) )

238
go.sum
View File

@ -22,15 +22,15 @@ cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIA
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb h1:Ws6WEwKXeaYEkfdkX6AqX1XLPuaCeyStEtxbmEJPllk= coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca h1:gSD53tBAsbIGq4SnFfq+mEep6foekQ2a5ea7b38qkm0=
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ= coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
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/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c h1:oeKnUB79PKYD8D0/unYuu7MRcWryQQWOns8+JL+acrs= git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c h1:oeKnUB79PKYD8D0/unYuu7MRcWryQQWOns8+JL+acrs=
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c/go.mod h1:fQuhwrpg6qb9NlFXKYi/LysWu1wxjraS8sxyW12CUF0= git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c/go.mod h1:fQuhwrpg6qb9NlFXKYi/LysWu1wxjraS8sxyW12CUF0=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
@ -49,8 +49,8 @@ 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.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.5.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=
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
@ -79,14 +79,13 @@ 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.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
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=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@ -121,30 +120,29 @@ github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dR
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0 h1:s7+5BfS4WFJoVF9pnB8kBk03S7pZXRdKamnV0FOl5Sc=
github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ=
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/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/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/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
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/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/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpTyk= github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I= github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
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 h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
@ -166,10 +164,9 @@ github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJ
github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
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/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.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
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=
@ -215,6 +212,10 @@ github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cE
github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y=
github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ=
github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
@ -237,6 +238,8 @@ github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3
github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c=
github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY=
github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/containerd/stargz-snapshotter/estargz v0.4.1/go.mod h1:x7Q9dg9QYb4+ELgxmo4gBUeJB0tl5dqH1Sdz0nJU1QM= github.com/containerd/stargz-snapshotter/estargz v0.4.1/go.mod h1:x7Q9dg9QYb4+ELgxmo4gBUeJB0tl5dqH1Sdz0nJU1QM=
github.com/containerd/stargz-snapshotter/estargz v0.11.0/go.mod h1:/KsZXsJRllMbTKFfG0miFQWViQKdI9+9aSXs+HN0+ac= github.com/containerd/stargz-snapshotter/estargz v0.11.0/go.mod h1:/KsZXsJRllMbTKFfG0miFQWViQKdI9+9aSXs+HN0+ac=
github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
@ -283,8 +286,9 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/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=
@ -312,24 +316,24 @@ 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 v28.0.1+incompatible h1:g0h5NQNda3/CxIsaZfH4Tyf6vpxFth7PYl3hgCPOKzs= github.com/docker/cli v28.3.3+incompatible h1:fp9ZHAr1WWPGdIWBM1b3zLtgCF+83gRdVMTJsUeiyAo=
github.com/docker/cli v28.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v28.3.3+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 v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0= github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.3.3+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.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
@ -372,8 +376,6 @@ github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
@ -389,8 +391,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
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.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -404,8 +406,8 @@ github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KE
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
@ -419,11 +421,10 @@ github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
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.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.4.0/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=
@ -475,7 +476,6 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93 h1:jc2UWq7CbdszqeH6qu1ougXMIUBfSy8Pbh/anURYbGI=
github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@ -529,9 +529,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
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/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.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
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=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -542,11 +541,10 @@ github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH
github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
@ -563,9 +561,7 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE=
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc=
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
@ -611,12 +607,13 @@ 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 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+YP7G1Mc=
github.com/leonelquinteros/gotext v1.7.2/go.mod h1:9/haCkm5P7Jay1sxKDGJ5WIg4zkz8oZKw4ekNpALob8=
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=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@ -656,26 +653,28 @@ github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
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.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0= github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0=
github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8= 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.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/user v0.4.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 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= 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=
@ -755,15 +754,12 @@ github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqi
github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo=
github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8=
github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -779,23 +775,23 @@ 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.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
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=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
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.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
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=
@ -807,8 +803,8 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
@ -828,8 +824,8 @@ github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8G
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=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
@ -848,10 +844,8 @@ github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:s
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
@ -860,7 +854,6 @@ github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHN
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
@ -868,10 +861,10 @@ github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bd
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -946,29 +939,29 @@ 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/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 h1:zG8GlgXCJQd5BU98C0hZnBbElszTmUgCNCfYneaDL0A=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0/go.mod h1:hOfBCz8kv/wuq73Mx2H2QnWokh/kHZxkh6SNF2bdKtw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
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.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
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.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
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=
@ -993,8 +986,8 @@ 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.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
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=
@ -1005,8 +998,8 @@ 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-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
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=
@ -1070,8 +1063,8 @@ 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.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
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=
@ -1089,8 +1082,8 @@ 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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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=
@ -1168,15 +1161,14 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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-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-20220908164124-27713097b956/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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
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=
@ -1186,16 +1178,16 @@ 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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
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.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
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=
@ -1289,10 +1281,10 @@ 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-20250313205543-e70fdf4c4cb4 h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY= google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a h1:DMCgtIAIQGZqJXMVzJF4MV8BlWoJh2ZuFiRdAleyr58=
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg= google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a/go.mod h1:y2yVLIE/CSMCPXaHnSKXxu1spLPnglFLegmgdY23uuE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a h1:tPE/Kp+x9dMSwUm/uM0JKK0IfdiJkwAbSMSeZBXXJXc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
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=
@ -1312,8 +1304,8 @@ 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.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
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=
@ -1327,11 +1319,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.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
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/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU= gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -1347,7 +1338,6 @@ gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKW
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx73duwUwM=
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw= gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw=
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=

View File

@ -2,6 +2,7 @@ package app
import ( import (
"bufio" "bufio"
"errors"
"fmt" "fmt"
"os" "os"
"path" "path"
@ -13,6 +14,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/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/convert" "coopcloud.tech/abra/pkg/upstream/convert"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
@ -36,7 +38,7 @@ func Get(appName string) (App, error) {
return App{}, err return App{}, err
} }
log.Debugf("loaded app %s: %s", appName, app) log.Debug(i18n.G("loaded app %s: %s", appName, app))
return app, nil return app, nil
} }
@ -47,7 +49,7 @@ func Get(appName string) (App, error) {
func GetApp(apps AppFiles, name AppName) (App, error) { func GetApp(apps AppFiles, name AppName) (App, error) {
appFile, exists := apps[name] appFile, exists := apps[name]
if !exists { if !exists {
return App{}, fmt.Errorf("cannot find app with name %s", name) return App{}, errors.New(i18n.G("cannot find app with name %s", name))
} }
app, err := ReadAppEnvFile(appFile, name) app, err := ReadAppEnvFile(appFile, name)
@ -136,7 +138,7 @@ func StackName(appName string) string {
stackName := SanitiseAppName(appName) stackName := SanitiseAppName(appName)
if len(stackName) > config.MAX_SANITISED_APP_NAME_LENGTH { if len(stackName) > config.MAX_SANITISED_APP_NAME_LENGTH {
log.Debugf("trimming %s to %s to avoid runtime limits", stackName, stackName[:config.MAX_SANITISED_APP_NAME_LENGTH]) log.Debug(i18n.G("trimming %s to %s to avoid runtime limits", stackName, stackName[:config.MAX_SANITISED_APP_NAME_LENGTH]))
stackName = stackName[:config.MAX_SANITISED_APP_NAME_LENGTH] stackName = stackName[:config.MAX_SANITISED_APP_NAME_LENGTH]
} }
@ -243,12 +245,12 @@ func (a ByName) Less(i, j int) bool {
func ReadAppEnvFile(appFile AppFile, name AppName) (App, error) { func ReadAppEnvFile(appFile AppFile, name AppName) (App, error) {
env, err := envfile.ReadEnv(appFile.Path) env, err := envfile.ReadEnv(appFile.Path)
if err != nil { if err != nil {
return App{}, fmt.Errorf("env file for %s couldn't be read: %s", name, err.Error()) return App{}, errors.New(i18n.G("env file for %s couldn't be read: %s", name, err.Error()))
} }
app, err := NewApp(env, name, appFile) app, err := NewApp(env, name, appFile)
if err != nil { if err != nil {
return App{}, fmt.Errorf("env file for %s has issues: %s", name, err.Error()) return App{}, errors.New(i18n.G("env file for %s has issues: %s", name, err.Error()))
} }
return app, nil return app, nil
@ -262,7 +264,7 @@ func NewApp(env envfile.AppEnv, name string, appFile AppFile) (App, error) {
if !exists { if !exists {
recipeName, exists = env["TYPE"] recipeName, exists = env["TYPE"]
if !exists { if !exists {
return App{}, fmt.Errorf("%s is missing the TYPE env var?", name) return App{}, errors.New(i18n.G("%s is missing the TYPE env var?", name))
} }
} }
@ -290,13 +292,13 @@ func LoadAppFiles(servers ...string) (AppFiles, error) {
} }
} }
log.Debugf("collecting metadata from %v servers: %s", len(servers), strings.Join(servers, ", ")) log.Debug(i18n.G("collecting metadata from %v servers: %s", len(servers), strings.Join(servers, ", ")))
for _, server := range servers { for _, server := range servers {
serverDir := path.Join(config.SERVERS_DIR, server) serverDir := path.Join(config.SERVERS_DIR, server)
files, err := config.GetAllFilesInDirectory(serverDir) files, err := config.GetAllFilesInDirectory(serverDir)
if err != nil { if err != nil {
return appFiles, fmt.Errorf("server %s doesn't exist? Run \"abra server ls\" to check", server) return appFiles, errors.New(i18n.G("server %s doesn't exist? Run \"abra server ls\" to check", server))
} }
for _, file := range files { for _, file := range files {
@ -375,7 +377,7 @@ func TemplateAppEnvSample(r recipe.Recipe, appName, server, domain string) error
appEnvPath := path.Join(config.ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName)) appEnvPath := path.Join(config.ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName))
if _, err := os.Stat(appEnvPath); !os.IsNotExist(err) { if _, err := os.Stat(appEnvPath); !os.IsNotExist(err) {
return fmt.Errorf("%s already exists?", appEnvPath) return errors.New(i18n.G("%s already exists?", appEnvPath))
} }
err = os.WriteFile(appEnvPath, envSample, 0o664) err = os.WriteFile(appEnvPath, envSample, 0o664)
@ -395,7 +397,7 @@ func TemplateAppEnvSample(r recipe.Recipe, appName, server, domain string) error
return err return err
} }
log.Debugf("copied & templated %s to %s", r.SampleEnvPath, appEnvPath) log.Debug(i18n.G("copied & templated %s to %s", r.SampleEnvPath, appEnvPath))
return nil return nil
} }
@ -419,14 +421,16 @@ func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]str
var bar *progressbar.ProgressBar var bar *progressbar.ProgressBar
if !MachineReadable { if !MachineReadable {
bar = formatter.CreateProgressbar(len(servers), "querying remote servers...") bar = formatter.CreateProgressbar(len(servers), i18n.G("querying remote servers..."))
} }
ch := make(chan stack.StackStatus, len(servers)) ch := make(chan stack.StackStatus, len(servers))
for server := range servers { for server := range servers {
cl, err := client.New(server) cl, err := client.New(server)
if err != nil { if err != nil {
return statuses, err log.Warn(err)
ch <- stack.StackStatus{}
continue
} }
go func(s string) { go func(s string) {
@ -480,7 +484,7 @@ func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]str
} }
} }
log.Debugf("retrieved app statuses: %s", statuses) log.Debug(i18n.G("retrieved app statuses: %s", statuses))
return statuses, nil return statuses, nil
} }
@ -494,7 +498,7 @@ func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv envfile.AppEnv
return &composetypes.Config{}, err return &composetypes.Config{}, err
} }
log.Debugf("retrieved %s for %s", compose.Filename, recipe) log.Debug(i18n.G("retrieved %s for %s", compose.Filename, recipe))
return compose, nil return compose, nil
} }
@ -503,13 +507,13 @@ func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv envfile.AppEnv
func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv envfile.AppEnv) { func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv envfile.AppEnv) {
for _, service := range compose.Services { for _, service := range compose.Services {
if service.Name == "app" { if service.Name == "app" {
log.Debugf("adding env vars to %s service config", stackName) log.Debug(i18n.G("adding env vars to %s service config", stackName))
for k, v := range appEnv { for k, v := range appEnv {
_, exists := service.Environment[k] _, exists := service.Environment[k]
if !exists { if !exists {
value := v value := v
service.Environment[k] = &value service.Environment[k] = &value
log.Debugf("%s: %s: %s", stackName, k, value) log.Debug(i18n.G("%s: %s: %s", stackName, k, value))
} }
} }
} }
@ -570,9 +574,9 @@ func ReadAbraShCmdNames(abraSh string) ([]string, error) {
} }
if len(cmdNames) > 0 { if len(cmdNames) > 0 {
log.Debugf("read %s from %s", strings.Join(cmdNames, " "), abraSh) log.Debug(i18n.G("read %s from %s", strings.Join(cmdNames, " "), abraSh))
} else { } else {
log.Debugf("read 0 command names from %s", abraSh) log.Debug(i18n.G("read 0 command names from %s", abraSh))
} }
return cmdNames, nil return cmdNames, nil
@ -615,7 +619,7 @@ func (a App) WipeRecipeVersion() error {
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("version wiped from %s.env", a.Domain) log.Debug(i18n.G("version wiped from %s.env", a.Domain))
return nil return nil
} }
@ -672,13 +676,13 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error {
log.Fatal(err) log.Fatal(err)
} }
} else { } else {
log.Debugf("skipping writing version %s because dry run", version) log.Debug(i18n.G("skipping writing version %s because dry run", version))
} }
if !skipped { if !skipped {
log.Debugf("version %s saved to %s.env", version, a.Domain) log.Debug(i18n.G("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.Debug(i18n.G("skipping version %s write as already exists in %s.env", version, a.Domain))
} }
return nil return nil

View File

@ -5,6 +5,7 @@ import (
"strconv" "strconv"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
composetypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types"
) )
@ -14,7 +15,7 @@ import (
func SetRecipeLabel(compose *composetypes.Config, stackName string, recipe string) { func SetRecipeLabel(compose *composetypes.Config, stackName string, recipe string) {
for _, service := range compose.Services { for _, service := range compose.Services {
if service.Name == "app" { if service.Name == "app" {
log.Debugf("set recipe label 'coop-cloud.%s.recipe' to %s for %s", stackName, recipe, stackName) log.Debug(i18n.G("set recipe label 'coop-cloud.%s.recipe' to %s for %s", stackName, recipe, stackName))
labelKey := fmt.Sprintf("coop-cloud.%s.recipe", stackName) labelKey := fmt.Sprintf("coop-cloud.%s.recipe", stackName)
service.Deploy.Labels[labelKey] = recipe service.Deploy.Labels[labelKey] = recipe
} }
@ -26,7 +27,7 @@ func SetRecipeLabel(compose *composetypes.Config, stackName string, recipe strin
func SetChaosLabel(compose *composetypes.Config, stackName string, chaos bool) { func SetChaosLabel(compose *composetypes.Config, stackName string, chaos bool) {
for _, service := range compose.Services { for _, service := range compose.Services {
if service.Name == "app" { if service.Name == "app" {
log.Debugf("set label 'coop-cloud.%s.chaos' to %v for %s", stackName, chaos, stackName) log.Debug(i18n.G("set label 'coop-cloud.%s.chaos' to %v for %s", stackName, chaos, stackName))
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", stackName) labelKey := fmt.Sprintf("coop-cloud.%s.chaos", stackName)
service.Deploy.Labels[labelKey] = strconv.FormatBool(chaos) service.Deploy.Labels[labelKey] = strconv.FormatBool(chaos)
} }
@ -37,7 +38,7 @@ func SetChaosLabel(compose *composetypes.Config, stackName string, chaos bool) {
func SetChaosVersionLabel(compose *composetypes.Config, stackName string, chaosVersion string) { func SetChaosVersionLabel(compose *composetypes.Config, stackName string, chaosVersion string) {
for _, service := range compose.Services { for _, service := range compose.Services {
if service.Name == "app" { if service.Name == "app" {
log.Debugf("set label 'coop-cloud.%s.chaos-version' to %v for %s", stackName, chaosVersion, stackName) log.Debug(i18n.G("set label 'coop-cloud.%s.chaos-version' to %v for %s", stackName, chaosVersion, stackName))
labelKey := fmt.Sprintf("coop-cloud.%s.chaos-version", stackName) labelKey := fmt.Sprintf("coop-cloud.%s.chaos-version", stackName)
service.Deploy.Labels[labelKey] = chaosVersion service.Deploy.Labels[labelKey] = chaosVersion
} }
@ -47,7 +48,7 @@ func SetChaosVersionLabel(compose *composetypes.Config, stackName string, chaosV
func SetVersionLabel(compose *composetypes.Config, stackName string, version string) { func SetVersionLabel(compose *composetypes.Config, stackName string, version string) {
for _, service := range compose.Services { for _, service := range compose.Services {
if service.Name == "app" { if service.Name == "app" {
log.Debugf("set label 'coop-cloud.%s.version' to %v for %s", stackName, version, stackName) log.Debug(i18n.G("set label 'coop-cloud.%s.version' to %v for %s", stackName, version, stackName))
labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName) labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName)
service.Deploy.Labels[labelKey] = version service.Deploy.Labels[labelKey] = version
} }
@ -64,7 +65,7 @@ func SetUpdateLabel(compose *composetypes.Config, stackName string, appEnv envfi
if !exists { if !exists {
enable_auto_update = "false" enable_auto_update = "false"
} }
log.Debugf("set label 'coop-cloud.%s.autoupdate' to %s for %s", stackName, enable_auto_update, stackName) log.Debug(i18n.G("set label 'coop-cloud.%s.autoupdate' to %s for %s", stackName, enable_auto_update, stackName))
labelKey := fmt.Sprintf("coop-cloud.%s.autoupdate", stackName) labelKey := fmt.Sprintf("coop-cloud.%s.autoupdate", stackName)
service.Deploy.Labels[labelKey] = enable_auto_update service.Deploy.Labels[labelKey] = enable_auto_update
} }
@ -76,13 +77,13 @@ func GetLabel(compose *composetypes.Config, stackName string, label string) stri
for _, service := range compose.Services { for _, service := range compose.Services {
if service.Name == "app" { if service.Name == "app" {
labelKey := fmt.Sprintf("coop-cloud.%s.%s", stackName, label) labelKey := fmt.Sprintf("coop-cloud.%s.%s", stackName, label)
log.Debugf("get label '%s'", labelKey) log.Debug(i18n.G("get label '%s'", labelKey))
if labelValue, ok := service.Deploy.Labels[labelKey]; ok { if labelValue, ok := service.Deploy.Labels[labelKey]; ok {
return labelValue return labelValue
} }
} }
} }
log.Debugf("no %s label found for %s", label, stackName) log.Debug(i18n.G("no %s label found for %s", label, stackName))
return "" return ""
} }
@ -91,7 +92,7 @@ func GetTimeoutFromLabel(compose *composetypes.Config, stackName string) (int, e
timeout := 50 // Default Timeout timeout := 50 // Default Timeout
var err error = nil var err error = nil
if timeoutLabel := GetLabel(compose, stackName, "timeout"); timeoutLabel != "" { if timeoutLabel := GetLabel(compose, stackName, "timeout"); timeoutLabel != "" {
log.Debugf("timeout label: %s", timeoutLabel) log.Debug(i18n.G("timeout label: %s", timeoutLabel))
timeout, err = strconv.Atoi(timeoutLabel) timeout, err = strconv.Atoi(timeoutLabel)
} }
return timeout, err return timeout, err

View File

@ -1,11 +1,11 @@
package autocomplete package autocomplete
import ( import (
"fmt"
"sort" "sort"
"coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -14,7 +14,7 @@ import (
func AppNameComplete() ([]string, cobra.ShellCompDirective) { func AppNameComplete() ([]string, cobra.ShellCompDirective) {
appFiles, err := app.LoadAppFiles("") appFiles, err := app.LoadAppFiles("")
if err != nil { if err != nil {
err := fmt.Sprintf("autocomplete failed: %s", err) err := i18n.G("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError return []string{err}, cobra.ShellCompDirectiveError
} }
@ -29,7 +29,7 @@ func AppNameComplete() ([]string, cobra.ShellCompDirective) {
func ServiceNameComplete(appName string) ([]string, cobra.ShellCompDirective) { func ServiceNameComplete(appName string) ([]string, cobra.ShellCompDirective) {
serviceNames, err := app.GetAppServiceNames(appName) serviceNames, err := app.GetAppServiceNames(appName)
if err != nil { if err != nil {
err := fmt.Sprintf("autocomplete failed: %s", err) err := i18n.G("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError return []string{err}, cobra.ShellCompDirectiveError
} }
@ -40,7 +40,7 @@ func ServiceNameComplete(appName string) ([]string, cobra.ShellCompDirective) {
func RecipeNameComplete() ([]string, cobra.ShellCompDirective) { func RecipeNameComplete() ([]string, cobra.ShellCompDirective) {
catl, err := recipe.ReadRecipeCatalogue(false) catl, err := recipe.ReadRecipeCatalogue(false)
if err != nil { if err != nil {
err := fmt.Sprintf("autocomplete failed: %s", err) err := i18n.G("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError return []string{err}, cobra.ShellCompDirectiveError
} }
@ -56,7 +56,7 @@ func RecipeNameComplete() ([]string, cobra.ShellCompDirective) {
func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirective) { func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirective) {
catl, err := recipe.ReadRecipeCatalogue(true) catl, err := recipe.ReadRecipeCatalogue(true)
if err != nil { if err != nil {
err := fmt.Sprintf("autocomplete failed: %s", err) err := i18n.G("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError return []string{err}, cobra.ShellCompDirectiveError
} }
@ -74,7 +74,7 @@ func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirectiv
func ServerNameComplete() ([]string, cobra.ShellCompDirective) { func ServerNameComplete() ([]string, cobra.ShellCompDirective) {
files, err := app.LoadAppFiles("") files, err := app.LoadAppFiles("")
if err != nil { if err != nil {
err := fmt.Sprintf("autocomplete failed: %s", err) err := i18n.G("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError return []string{err}, cobra.ShellCompDirectiveError
} }
@ -90,13 +90,13 @@ func ServerNameComplete() ([]string, cobra.ShellCompDirective) {
func CommandNameComplete(appName string) ([]string, cobra.ShellCompDirective) { func CommandNameComplete(appName string) ([]string, cobra.ShellCompDirective) {
app, err := app.Get(appName) app, err := app.Get(appName)
if err != nil { if err != nil {
err := fmt.Sprintf("autocomplete failed: %s", err) err := i18n.G("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError return []string{err}, cobra.ShellCompDirectiveError
} }
cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath) cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath)
if err != nil { if err != nil {
err := fmt.Sprintf("autocomplete failed: %s", err) err := i18n.G("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError return []string{err}, cobra.ShellCompDirectiveError
} }
@ -111,7 +111,7 @@ func SecretComplete(recipeName string) ([]string, cobra.ShellCompDirective) {
config, err := r.GetComposeConfig(nil) config, err := r.GetComposeConfig(nil)
if err != nil { if err != nil {
err := fmt.Sprintf("autocomplete failed: %s", err) err := i18n.G("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError return []string{err}, cobra.ShellCompDirectiveError
} }

View File

@ -1,6 +1,7 @@
package catalogue package catalogue
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path" "path"
@ -8,6 +9,7 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
) )
@ -16,7 +18,7 @@ import (
func EnsureCatalogue() error { func EnsureCatalogue() error {
catalogueDir := path.Join(config.ABRA_DIR, "catalogue") catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) { if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) {
log.Debugf("catalogue is missing, retrieving now") log.Debug(i18n.G("catalogue is missing, retrieving now"))
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME) url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME)
if err := gitPkg.Clone(catalogueDir, url); err != nil { if err := gitPkg.Clone(catalogueDir, url); err != nil {
@ -35,8 +37,7 @@ func EnsureIsClean() error {
} }
if !isClean { if !isClean {
msg := "%s has locally unstaged changes? please commit/remove your changes before proceeding" return errors.New(i18n.G("%s has locally unstaged changes? please commit/remove your changes before proceeding", config.CATALOGUE_DIR))
return fmt.Errorf(msg, config.CATALOGUE_DIR)
} }
return nil return nil
@ -55,8 +56,7 @@ func EnsureUpToDate() error {
} }
if len(remotes) == 0 { if len(remotes) == 0 {
msg := "cannot ensure %s is up-to-date, no git remotes configured" log.Debug(i18n.G("cannot ensure %s is up-to-date, no git remotes configured", config.CATALOGUE_DIR))
log.Debugf(msg, config.CATALOGUE_DIR)
return nil return nil
} }
@ -81,7 +81,7 @@ func EnsureUpToDate() error {
} }
} }
log.Debugf("fetched latest git changes for %s", config.CATALOGUE_DIR) log.Debug(i18n.G("fetched latest git changes for %s", config.CATALOGUE_DIR))
return nil return nil
} }

View File

@ -4,12 +4,12 @@ package client
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"net/http" "net/http"
"os" "os"
"time" "time"
contextPkg "coopcloud.tech/abra/pkg/context" contextPkg "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
sshPkg "coopcloud.tech/abra/pkg/ssh" sshPkg "coopcloud.tech/abra/pkg/ssh"
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn" commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
@ -41,7 +41,7 @@ func New(serverName string, opts ...Opt) (*client.Client, error) {
if serverName != "default" { if serverName != "default" {
context, err := GetContext(serverName) context, err := GetContext(serverName)
if err != nil { if err != nil {
return nil, fmt.Errorf("unknown server, run \"abra server add %s\"?", serverName) return nil, errors.New(i18n.G("unknown server, run \"abra server add %s\"?", serverName))
} }
ctxEndpoint, err := contextPkg.GetContextEndpoint(context) ctxEndpoint, err := contextPkg.GetContextEndpoint(context)
@ -85,7 +85,7 @@ func New(serverName string, opts ...Opt) (*client.Client, error) {
return nil, err return nil, err
} }
log.Debugf("created client for %s", serverName) log.Debug(i18n.G("created client for %s", serverName))
info, err := cl.Info(context.Background()) info, err := cl.Info(context.Background())
if err != nil { if err != nil {
@ -94,10 +94,10 @@ func New(serverName string, opts ...Opt) (*client.Client, error) {
if info.Swarm.LocalNodeState == "inactive" { if info.Swarm.LocalNodeState == "inactive" {
if serverName != "default" { if serverName != "default" {
return cl, fmt.Errorf("swarm mode not enabled on %s?", serverName) return cl, errors.New(i18n.G("swarm mode not enabled on %s?", serverName))
} }
return cl, errors.New("swarm mode not enabled on local server?") return cl, errors.New(i18n.G("swarm mode not enabled on local server?"))
} }
return cl, nil return cl, nil

39
pkg/client/configs.go Normal file
View File

@ -0,0 +1,39 @@
package client
import (
"context"
"errors"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"coopcloud.tech/abra/pkg/i18n"
)
func GetConfigs(cl *client.Client, ctx context.Context, server string, fs filters.Args) ([]swarm.Config, error) {
configList, err := cl.ConfigList(ctx, swarm.ConfigListOptions{Filters: fs})
if err != nil {
return configList, err
}
return configList, nil
}
func GetConfigNames(configs []swarm.Config) []string {
var confNames []string
for _, conf := range configs {
confNames = append(confNames, conf.Spec.Name)
}
return confNames
}
func RemoveConfigs(cl *client.Client, ctx context.Context, configNames []string, force bool) error {
for _, confName := range configNames {
if err := cl.ConfigRemove(context.Background(), confName); err != nil {
return errors.New(i18n.G("conf %s: %s", confName, err))
}
}
return nil
}

View File

@ -10,6 +10,7 @@ import (
dConfig "github.com/docker/cli/cli/config" dConfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/context/docker" "github.com/docker/cli/cli/context/docker"
contextStore "github.com/docker/cli/cli/context/store" contextStore "github.com/docker/cli/cli/context/store"
"coopcloud.tech/abra/pkg/i18n"
) )
type Context = contextStore.Metadata type Context = contextStore.Metadata
@ -22,7 +23,7 @@ func CreateContext(contextName string) error {
return err return err
} }
log.Debugf("created the %s context", contextName) log.Debug(i18n.G("created the %s context", contextName))
return nil return nil
} }
@ -62,7 +63,7 @@ func createContext(name string, host string) error {
func DeleteContext(name string) error { func DeleteContext(name string) error {
if name == "default" { if name == "default" {
return errors.New("context 'default' cannot be removed") return errors.New(i18n.G("context 'default' cannot be removed"))
} }
if _, err := GetContext(name); err != nil { if _, err := GetContext(name); err != nil {

View File

@ -2,11 +2,13 @@ package client
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"github.com/containers/image/docker" "github.com/containers/image/docker"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/distribution/reference" "github.com/distribution/reference"
"coopcloud.tech/abra/pkg/i18n"
) )
// GetRegistryTags retrieves all tags of an image from a container registry. // GetRegistryTags retrieves all tags of an image from a container registry.
@ -15,7 +17,7 @@ func GetRegistryTags(img reference.Named) ([]string, error) {
ref, err := docker.ParseReference(fmt.Sprintf("//%s", img)) ref, err := docker.ParseReference(fmt.Sprintf("//%s", img))
if err != nil { if err != nil {
return tags, fmt.Errorf("failed to parse image %s, saw: %s", img, err.Error()) return tags, errors.New(i18n.G("failed to parse image %s, saw: %s", img, err.Error()))
} }
ctx := context.Background() ctx := context.Background()

View File

@ -2,9 +2,10 @@ package client
import ( import (
"context" "context"
"fmt" "errors"
"time" "time"
"coopcloud.tech/abra/pkg/i18n"
"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/docker/docker/api/types/volume" "github.com/docker/docker/api/types/volume"
@ -37,7 +38,7 @@ func RemoveVolumes(cl *client.Client, ctx context.Context, volumeNames []string,
return cl.VolumeRemove(context.Background(), volName, force) return cl.VolumeRemove(context.Background(), volName, force)
}) })
if err != nil { if err != nil {
return fmt.Errorf("volume %s: %s", volName, err) return errors.New(i18n.G("volume %s: %s", volName, err))
} }
} }
return nil return nil
@ -54,9 +55,9 @@ func retryFunc(retries int, fn func() error) error {
} }
if i+1 < retries { if i+1 < retries {
sleep := time.Duration(i+1) * time.Duration(i+1) sleep := time.Duration(i+1) * time.Duration(i+1)
log.Infof("%s: waiting %d seconds before next retry", err, sleep) log.Info(i18n.G("%s: waiting %d seconds before next retry", err, sleep))
time.Sleep(sleep * time.Second) time.Sleep(sleep * time.Second)
} }
} }
return fmt.Errorf("%d retries failed", retries) return errors.New(i18n.G("%d retries failed", retries))
} }

View File

@ -5,6 +5,7 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@ -16,13 +17,13 @@ func LoadAbraConfig() Abra {
wd, _ := os.Getwd() wd, _ := os.Getwd()
configFile := findAbraConfig(wd) configFile := findAbraConfig(wd)
if configFile == "" { if configFile == "" {
log.Debugf("no config file found") log.Debug(i18n.G("no config file found"))
return Abra{} return Abra{}
} }
data, err := os.ReadFile(configFile) data, err := os.ReadFile(configFile)
if err != nil { if err != nil {
// Do nothing, when an error occurs // Do nothing, when an error occurs
log.Debugf("error reading config file: %s", err) log.Debug(i18n.G("error reading config file: %s", err))
return Abra{} return Abra{}
} }
@ -30,10 +31,10 @@ func LoadAbraConfig() Abra {
err = yaml.Unmarshal(data, &config) err = yaml.Unmarshal(data, &config)
if err != nil { if err != nil {
// Do nothing, when an error occurs // Do nothing, when an error occurs
log.Debugf("error loading config file: %s", err) log.Debug(i18n.G("error loading config file: %s", err))
return Abra{} return Abra{}
} }
log.Debugf("config file loaded from: %s", configFile) log.Debug(i18n.G("config file loaded from: %s", configFile))
config.configPath = filepath.Dir(configFile) config.configPath = filepath.Dir(configFile)
return config return config
} }
@ -73,26 +74,24 @@ type Abra struct {
// 3. use $HOME/.abra when above two options failed // 3. use $HOME/.abra when above two options failed
func (a Abra) GetAbraDir() string { func (a Abra) GetAbraDir() string {
if dir, exists := os.LookupEnv("ABRA_DIR"); exists && dir != "" { if dir, exists := os.LookupEnv("ABRA_DIR"); exists && dir != "" {
log.Debug("read abra dir from $ABRA_DIR") log.Debug(i18n.G("read abra dir from $ABRA_DIR"))
return dir return dir
} }
if a.AbraDir != "" { if a.AbraDir != "" {
log.Debug("read abra dir from config file") log.Debug(i18n.G("read abra dir from config file"))
if path.IsAbs(a.AbraDir) { if path.IsAbs(a.AbraDir) {
return a.AbraDir return a.AbraDir
} }
// Make the path absolute // Make the path absolute
return path.Join(a.configPath, a.AbraDir) return path.Join(a.configPath, a.AbraDir)
} }
log.Debug("using default abra dir") log.Debug(i18n.G("using default abra dir"))
return os.ExpandEnv("$HOME/.abra") return os.ExpandEnv("$HOME/.abra")
} }
func (a Abra) GetServersDir() string { return path.Join(a.GetAbraDir(), "servers") } func (a Abra) GetServersDir() string { return path.Join(a.GetAbraDir(), "servers") }
func (a Abra) GetRecipesDir() string { return path.Join(a.GetAbraDir(), "recipes") } func (a Abra) GetRecipesDir() string { return path.Join(a.GetAbraDir(), "recipes") }
func (a Abra) GetLogsDir() string { return path.Join(a.GetAbraDir(), "logs") } func (a Abra) GetLogsDir() string { return path.Join(a.GetAbraDir(), "logs") }
func (a Abra) GetVendorDir() string { return path.Join(a.GetAbraDir(), "vendor") }
func (a Abra) GetBackupDir() string { return path.Join(a.GetAbraDir(), "backups") }
func (a Abra) GetCatalogueDir() string { return path.Join(a.GetAbraDir(), "catalogue") } func (a Abra) GetCatalogueDir() string { return path.Join(a.GetAbraDir(), "catalogue") }
var config = LoadAbraConfig() var config = LoadAbraConfig()
@ -102,8 +101,6 @@ var (
SERVERS_DIR = config.GetServersDir() SERVERS_DIR = config.GetServersDir()
RECIPES_DIR = config.GetRecipesDir() RECIPES_DIR = config.GetRecipesDir()
LOGS_DIR = config.GetLogsDir() LOGS_DIR = config.GetLogsDir()
VENDOR_DIR = config.GetVendorDir()
BACKUP_DIR = config.GetBackupDir()
CATALOGUE_DIR = config.GetCatalogueDir() CATALOGUE_DIR = config.GetCatalogueDir()
RECIPES_JSON = path.Join(config.GetCatalogueDir(), "recipes.json") RECIPES_JSON = path.Join(config.GetCatalogueDir(), "recipes.json")
REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud" REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"

View File

@ -1,7 +1,7 @@
package config package config
import ( import (
"fmt" "errors"
"io/fs" "io/fs"
"io/ioutil" "io/ioutil"
"os" "os"
@ -9,6 +9,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
) )
@ -33,7 +34,7 @@ func GetServers() ([]string, error) {
} }
} }
log.Debugf("retrieved %v servers: %s", len(filtered), filtered) log.Debug(i18n.G("retrieved %v servers: %s", len(filtered), filtered))
return filtered, nil return filtered, nil
} }
@ -46,7 +47,7 @@ func ReadServerNames() ([]string, error) {
return nil, err return nil, err
} }
log.Debugf("read %s from %s", strings.Join(serverNames, ","), SERVERS_DIR) log.Debug(i18n.G("read %s from %s", strings.Join(serverNames, ","), SERVERS_DIR))
return serverNames, nil return serverNames, nil
} }
@ -70,7 +71,7 @@ func GetAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
realPath, err := filepath.EvalSymlinks(filePath) realPath, err := filepath.EvalSymlinks(filePath)
if err != nil { if err != nil {
log.Warnf("broken symlink in your abra config folders: %s", filePath) log.Warn(i18n.G("broken symlink in your abra config folders: %s", filePath))
} else { } else {
realFile, err := os.Stat(realPath) realFile, err := os.Stat(realPath)
if err != nil { if err != nil {
@ -94,7 +95,7 @@ func GetAllFoldersInDirectory(directory string) ([]string, error) {
return nil, err return nil, err
} }
if len(files) == 0 { if len(files) == 0 {
return nil, fmt.Errorf("directory is empty: %s", directory) return nil, errors.New(i18n.G("directory is empty: %s", directory))
} }
for _, file := range files { for _, file := range files {
@ -103,7 +104,7 @@ func GetAllFoldersInDirectory(directory string) ([]string, error) {
filePath := path.Join(directory, file.Name()) filePath := path.Join(directory, file.Name())
realDir, err := filepath.EvalSymlinks(filePath) realDir, err := filepath.EvalSymlinks(filePath)
if err != nil { if err != nil {
log.Warnf("broken symlink in your abra config folders: %s", filePath) log.Warn(i18n.G("broken symlink in your abra config folders: %s", filePath))
} else if stat, err := os.Stat(realDir); err == nil && stat.IsDir() { } else if stat, err := os.Stat(realDir); err == nil && stat.IsDir() {
// path is a directory // path is a directory
folders = append(folders, file.Name()) folders = append(folders, file.Name())

View File

@ -2,6 +2,7 @@ package container
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
@ -12,6 +13,7 @@ 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"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"coopcloud.tech/abra/pkg/i18n"
) )
// GetContainer retrieves a container. If noInput is false and the retrievd // GetContainer retrieves a container. If noInput is false and the retrievd
@ -26,7 +28,7 @@ func GetContainer(c context.Context, cl *client.Client, filters filters.Args, no
if len(containers) == 0 { if len(containers) == 0 {
filter := filters.Get("name")[0] filter := filters.Get("name")[0]
return types.Container{}, fmt.Errorf("no containers matching the %v filter found?", filter) return types.Container{}, errors.New(i18n.G("no containers matching the %v filter found?", filter))
} }
if len(containers) > 1 { if len(containers) > 1 {
@ -35,19 +37,19 @@ func GetContainer(c context.Context, cl *client.Client, filters filters.Args, no
containerName := strings.Join(container.Names, " ") containerName := strings.Join(container.Names, " ")
trimmed := strings.TrimPrefix(containerName, "/") trimmed := strings.TrimPrefix(containerName, "/")
created := formatter.HumanDuration(container.Created) created := formatter.HumanDuration(container.Created)
containersRaw = append(containersRaw, fmt.Sprintf("%s (created %v)", trimmed, created)) containersRaw = append(containersRaw, i18n.G("%s (created %v)", trimmed, created))
} }
if noInput { if noInput {
err := fmt.Errorf("expected 1 container but found %v: %s", len(containers), strings.Join(containersRaw, " ")) err := errors.New(i18n.G("expected 1 container but found %v: %s", len(containers), strings.Join(containersRaw, " ")))
return types.Container{}, err return types.Container{}, err
} }
log.Warnf("ambiguous container list received, prompting for input") log.Warnf(i18n.G("ambiguous container list received, prompting for input"))
var response string var response string
prompt := &survey.Select{ prompt := &survey.Select{
Message: "which container are you looking for?", Message: i18n.G("which container are you looking for?"),
Options: containersRaw, Options: containersRaw,
} }
@ -64,7 +66,7 @@ func GetContainer(c context.Context, cl *client.Client, filters filters.Args, no
} }
} }
log.Fatal("failed to match chosen container") log.Fatal(i18n.G("failed to match chosen container"))
} }
return containers[0], nil return containers[0], nil
@ -79,5 +81,6 @@ func GetContainerFromStackAndService(cl *client.Client, stack, service string) (
if err != nil { if err != nil {
return types.Container{}, err return types.Container{}, err
} }
return container, nil return container, nil
} }

View File

@ -8,6 +8,7 @@ import (
"github.com/docker/cli/cli/context" "github.com/docker/cli/cli/context"
contextStore "github.com/docker/cli/cli/context/store" contextStore "github.com/docker/cli/cli/context/store"
cliflags "github.com/docker/cli/cli/flags" cliflags "github.com/docker/cli/cli/flags"
"coopcloud.tech/abra/pkg/i18n"
) )
func NewDefaultDockerContextStore() *command.ContextStoreWithDefault { func NewDefaultDockerContextStore() *command.ContextStoreWithDefault {
@ -30,7 +31,7 @@ func NewDefaultDockerContextStore() *command.ContextStoreWithDefault {
func GetContextEndpoint(ctx contextStore.Metadata) (string, error) { func GetContextEndpoint(ctx contextStore.Metadata) (string, error) {
endpointmeta, ok := ctx.Endpoints["docker"].(context.EndpointMetaBase) endpointmeta, ok := ctx.Endpoints["docker"].(context.EndpointMetaBase)
if !ok { if !ok {
err := errors.New("context lacks Docker endpoint") err := errors.New(i18n.G("context lacks Docker endpoint"))
return "", err return "", err
} }
return endpointmeta.Host, nil return endpointmeta.Host, nil

View File

@ -1,19 +1,21 @@
package dns package dns
import ( import (
"fmt" "errors"
"net" "net"
"coopcloud.tech/abra/pkg/i18n"
) )
// EnsureIPv4 ensures that an ipv4 address is set for a domain name // EnsureIPv4 ensures that an ipv4 address is set for a domain name
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("%s: unable to resolve IPv4 address: %s", domainName, err) return "", errors.New(i18n.G("%s: unable to resolve IPv4 address: %s", domainName, err))
} }
if ipv4 == nil { if ipv4 == nil {
return "", fmt.Errorf("%s: no IPv4 available", domainName) return "", errors.New(i18n.G("%s: no IPv4 available", domainName))
} }
return ipv4.String(), nil return ipv4.String(), nil
@ -33,7 +35,7 @@ func EnsureDomainsResolveSameIPv4(domainName, server string) (string, error) {
} }
if domainIPv4 == "" { if domainIPv4 == "" {
return ipv4, fmt.Errorf("cannot resolve ipv4 for %s?", domainName) return ipv4, errors.New(i18n.G("cannot resolve ipv4 for %s?", domainName))
} }
serverIPv4, err := EnsureIPv4(server) serverIPv4, err := EnsureIPv4(server)
@ -42,12 +44,16 @@ func EnsureDomainsResolveSameIPv4(domainName, server string) (string, error) {
} }
if serverIPv4 == "" { if serverIPv4 == "" {
return ipv4, fmt.Errorf("cannot resolve ipv4 for %s?", server) return ipv4, errors.New(i18n.G("cannot resolve ipv4 for %s?", server))
} }
if domainIPv4 != serverIPv4 { if domainIPv4 != serverIPv4 {
err := "app domain %s (%s) does not appear to resolve to app server %s (%s)?" return ipv4, errors.New(
return ipv4, fmt.Errorf(err, domainName, domainIPv4, server, serverIPv4) i18n.G(
"app domain %s (%s) does not appear to resolve to app server %s (%s)?",
domainName, domainIPv4, server, serverIPv4,
),
)
} }
return ipv4, nil return ipv4, nil

View File

@ -2,20 +2,16 @@ package envfile
import ( import (
"bufio" "bufio"
"fmt" "errors"
"os" "os"
"regexp" "regexp"
"strings" "strings"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"git.coopcloud.tech/toolshed/godotenv" "git.coopcloud.tech/toolshed/godotenv"
"coopcloud.tech/abra/pkg/i18n"
) )
// envVarModifiers is a list of env var modifier strings. These are added to
// env vars as comments and modify their processing by Abra, e.g. determining
// how long secrets should be.
var envVarModifiers = []string{"length"}
// AppEnv is a map of the values in an apps env config // AppEnv is a map of the values in an apps env config
type AppEnv = map[string]string type AppEnv = map[string]string
@ -43,7 +39,7 @@ func ReadEnvWithModifiers(filePath string) (AppEnv, AppModifiers, error) {
return nil, mods, err return nil, mods, err
} }
log.Debugf("read %s from %s", envVars, filePath) log.Debug(i18n.G("read %s from %s", envVars, filePath))
return envVars, mods, nil return envVars, mods, nil
} }
@ -74,16 +70,16 @@ func ReadAbraShEnvVars(abraSh string) (map[string]string, error) {
envVarDef := splitVals[len(splitVals)-1] envVarDef := splitVals[len(splitVals)-1]
keyVal := strings.Split(envVarDef, "=") keyVal := strings.Split(envVarDef, "=")
if len(keyVal) != 2 { if len(keyVal) != 2 {
return envVars, fmt.Errorf("couldn't parse %s", txt) return envVars, errors.New(i18n.G("couldn't parse %s", txt))
} }
envVars[keyVal[0]] = keyVal[1] envVars[keyVal[0]] = keyVal[1]
} }
} }
if len(envVars) > 0 { if len(envVars) > 0 {
log.Debugf("read %s from %s", envVars, abraSh) log.Debug(i18n.G("read %s from %s", envVars, abraSh))
} else { } else {
log.Debugf("read 0 env var exports from %s", abraSh) log.Debug(i18n.G("read 0 env var exports from %s", abraSh))
} }
return envVars, nil return envVars, nil

View File

@ -8,6 +8,7 @@ import (
"strings" "strings"
"time" "time"
"coopcloud.tech/abra/pkg/i18n"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table" "github.com/charmbracelet/lipgloss/table"
"github.com/docker/go-units" "github.com/docker/go-units"
@ -42,7 +43,7 @@ func RemoveSha(str string) string {
func HumanDuration(timestamp int64) string { func HumanDuration(timestamp int64) string {
date := time.Unix(timestamp, 0) date := time.Unix(timestamp, 0)
now := time.Now().UTC() now := time.Now().UTC()
return units.HumanDuration(now.Sub(date)) + " ago" return units.HumanDuration(now.Sub(date)) + i18n.G(" ago")
} }
// CreateTable prepares a table layout for output. // CreateTable prepares a table layout for output.
@ -76,7 +77,7 @@ func CreateTable() (*table.Table, error) {
func PrintTable(t *table.Table) error { 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(i18n.G("detected ABRA_CI=1"))
fmt.Println(t) fmt.Println(t)
return nil return nil
} }
@ -130,7 +131,7 @@ func CreateOverview(header string, rows [][]string) string {
} }
if len(row) > 2 { if len(row) > 2 {
panic("CreateOverview: only accepts rows of len == 2") panic(i18n.G("CreateOverview: only accepts rows of len == 2"))
} }
lenOffset := 4 lenOffset := 4
@ -234,7 +235,7 @@ func StripTagMeta(image string) string {
} }
if originalImage != image { if originalImage != image {
log.Debugf("stripped %s to %s for parsing", originalImage, image) log.Debug(i18n.G("stripped %s to %s for parsing", originalImage, image))
} }
return image return image

View File

@ -3,6 +3,7 @@ package git
import ( 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"
"coopcloud.tech/abra/pkg/i18n"
) )
// Add adds a file to the git index. // Add adds a file to the git index.
@ -18,7 +19,7 @@ func Add(repoPath, path string, dryRun bool) error {
} }
if dryRun { if dryRun {
log.Debugf("dry run: adding %s", path) log.Debug(i18n.G("dry run: adding %s", path))
} else { } else {
worktree.Add(path) worktree.Add(path)
} }

View File

@ -1,11 +1,13 @@
package git package git
import ( import (
"errors"
"fmt" "fmt"
"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"
"coopcloud.tech/abra/pkg/i18n"
) )
// Check if a branch exists in a repo. Use this and not repository.Branch(), // Check if a branch exists in a repo. Use this and not repository.Branch(),
@ -63,7 +65,7 @@ func GetDefaultBranch(repo *git.Repository, repoPath string) (plumbing.Reference
if !HasBranch(repo, "master") { if !HasBranch(repo, "master") {
if !HasBranch(repo, "main") { if !HasBranch(repo, "main") {
return "", fmt.Errorf("failed to select default branch in %s", repoPath) return "", errors.New(i18n.G("failed to select default branch in %s", repoPath))
} }
branch = "main" branch = "main"
} }
@ -90,11 +92,11 @@ func CheckoutDefaultBranch(repo *git.Repository, repoPath string) (plumbing.Refe
} }
if err := worktree.Checkout(checkOutOpts); err != nil { if err := worktree.Checkout(checkOutOpts); err != nil {
log.Debugf("failed to check out %s in %s", branch, repoPath) log.Debug(i18n.G("failed to check out %s in %s", branch, repoPath))
return branch, err return branch, err
} }
log.Debugf("successfully checked out %v in %s", branch, repoPath) log.Debug(i18n.G("successfully checked out %v in %s", branch, repoPath))
return branch, nil return branch, nil
} }

View File

@ -2,6 +2,7 @@ package git
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"os/signal" "os/signal"
@ -10,6 +11,7 @@ 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"
"coopcloud.tech/abra/pkg/i18n"
) )
// gitCloneIgnoreErr checks whether we can ignore a git clone error or not. // gitCloneIgnoreErr checks whether we can ignore a git clone error or not.
@ -44,7 +46,7 @@ func Clone(dir, url string) error {
go func() { go func() {
if _, err := os.Stat(dir); os.IsNotExist(err) { if _, err := os.Stat(dir); os.IsNotExist(err) {
log.Debugf("git clone: %s", url) log.Debug(i18n.G("git clone: %s", url))
_, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{ _, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{
URL: url, URL: url,
@ -54,16 +56,16 @@ func Clone(dir, url string) error {
}) })
if err != nil && gitCloneIgnoreErr(err) { if err != nil && gitCloneIgnoreErr(err) {
log.Debugf("git clone: %s cloned successfully", dir) log.Debug(i18n.G("git clone: %s cloned successfully", dir))
errCh <- nil errCh <- nil
} }
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
errCh <- fmt.Errorf("git clone %s: cancelled due to interrupt", dir) errCh <- errors.New(i18n.G("git clone %s: cancelled due to interrupt", dir))
} }
if err != nil { if err != nil {
log.Debug("git clone: main branch failed, attempting master branch") log.Debug(i18n.G("git clone: main branch failed, attempting master branch"))
_, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{ _, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{
URL: url, URL: url,
@ -73,7 +75,7 @@ func Clone(dir, url string) error {
}) })
if err != nil && gitCloneIgnoreErr(err) { if err != nil && gitCloneIgnoreErr(err) {
log.Debugf("git clone: %s cloned successfully", dir) log.Debug(i18n.G("git clone: %s cloned successfully", dir))
errCh <- nil errCh <- nil
} }
@ -82,9 +84,9 @@ func Clone(dir, url string) error {
} }
} }
log.Debugf("git clone: %s cloned successfully", dir) log.Debug(i18n.G("git clone: %s cloned successfully", dir))
} else { } else {
log.Debugf("git clone: %s already exists", dir) log.Debug(i18n.G("git clone: %s already exists", dir))
} }
errCh <- nil errCh <- nil
@ -95,9 +97,9 @@ func Clone(dir, url string) error {
cancelCtx() cancelCtx()
fmt.Println() // NOTE(d1): newline after ^C fmt.Println() // NOTE(d1): newline after ^C
if err := os.RemoveAll(dir); err != nil { if err := os.RemoveAll(dir); err != nil {
return fmt.Errorf("unable to clean up git clone of %s: %s", dir, err) return errors.New(i18n.G("unable to clean up git clone of %s: %s", dir, err))
} }
return fmt.Errorf("git clone %s: cancelled due to interrupt", dir) return errors.New(i18n.G("git clone %s: cancelled due to interrupt", dir))
case err := <-errCh: case err := <-errCh:
return err return err
} }

View File

@ -1,16 +1,17 @@
package git package git
import ( import (
"fmt" "errors"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"coopcloud.tech/abra/pkg/i18n"
) )
// Commit runs a git commit // Commit runs a git commit
func Commit(repoPath, commitMessage string, dryRun bool) error { func Commit(repoPath, commitMessage string, dryRun bool) error {
if commitMessage == "" { if commitMessage == "" {
return fmt.Errorf("no commit message specified?") return errors.New(i18n.G("no commit message specified?"))
} }
commitRepo, err := git.PlainOpen(repoPath) commitRepo, err := git.PlainOpen(repoPath)
@ -38,9 +39,9 @@ func Commit(repoPath, commitMessage string, dryRun bool) error {
if err != nil { if err != nil {
return err return err
} }
log.Debug("git changes commited") log.Debug(i18n.G("git changes commited"))
} else { } else {
log.Debug("dry run: no changes commited") log.Debug(i18n.G("dry run: no changes commited"))
} }
return nil return nil

View File

@ -1,14 +1,16 @@
package git package git
import ( import (
"fmt" "errors"
"os" "os"
"coopcloud.tech/abra/pkg/i18n"
) )
// EnsureGitRepo ensures a git repo .git folder exists // EnsureGitRepo ensures a git repo .git folder exists
func EnsureGitRepo(repoPath string) error { func EnsureGitRepo(repoPath string) error {
if _, err := os.Stat(repoPath); os.IsNotExist(err) { if _, err := os.Stat(repoPath); os.IsNotExist(err) {
return fmt.Errorf("no .git directory in %s?", repoPath) return errors.New(i18n.G("no .git directory in %s?", repoPath))
} }
return nil return nil
} }

View File

@ -5,6 +5,7 @@ import (
"os/exec" "os/exec"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/i18n"
) )
// getGitDiffArgs builds the `git diff` invocation args. It removes the usage // getGitDiffArgs builds the `git diff` invocation args. It removes the usage
@ -26,7 +27,7 @@ func getGitDiffArgs(repoPath string) []string {
// skips if it cannot find the command on the system. // skips if it cannot find the command on the system.
func DiffUnstaged(path string) error { func DiffUnstaged(path string) error {
if _, err := exec.LookPath("git"); err != nil { if _, err := exec.LookPath("git"); err != nil {
log.Warnf("unable to locate git command, cannot output diff") log.Warnf(i18n.G("unable to locate git command, cannot output diff"))
return nil return nil
} }

View File

@ -1,40 +1,41 @@
package git package git
import ( import (
"fmt" "errors"
"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"
"github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/object"
"coopcloud.tech/abra/pkg/i18n"
) )
// 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 {
repo, err := git.PlainInit(repoPath, false) repo, err := git.PlainInit(repoPath, false)
if err != nil { if err != nil {
return fmt.Errorf("git init: %s", err) return errors.New(i18n.G("git init: %s", err))
} }
if err = SwitchToMain(repo); err != nil { if err = SwitchToMain(repo); err != nil {
return fmt.Errorf("git branch rename: %s", err) return errors.New(i18n.G("git branch rename: %s", err))
} }
log.Debugf("initialised new git repo in %s", repoPath) log.Debug(i18n.G("initialised new git repo in %s", repoPath))
if commit { if commit {
commitRepo, err := git.PlainOpen(repoPath) commitRepo, err := git.PlainOpen(repoPath)
if err != nil { if err != nil {
return fmt.Errorf("git open: %s", err) return errors.New(i18n.G("git open: %s", err))
} }
commitWorktree, err := commitRepo.Worktree() commitWorktree, err := commitRepo.Worktree()
if err != nil { if err != nil {
return fmt.Errorf("git worktree: %s", err) return errors.New(i18n.G("git worktree: %s", err))
} }
if err := commitWorktree.AddWithOptions(&git.AddOptions{All: true}); err != nil { if err := commitWorktree.AddWithOptions(&git.AddOptions{All: true}); err != nil {
return fmt.Errorf("git add: %s", err) return errors.New(i18n.G("git add: %s", err))
} }
var author *object.Signature var author *object.Signature
@ -43,10 +44,10 @@ func Init(repoPath string, commit bool, gitName, gitEmail string) error {
} }
if _, err = commitWorktree.Commit("init", &git.CommitOptions{Author: author}); err != nil { if _, err = commitWorktree.Commit("init", &git.CommitOptions{Author: author}); err != nil {
return fmt.Errorf("git commit: %s", err) return errors.New(i18n.G("git commit: %s", err))
} }
log.Debugf("init committed all files for new git repo in %s", repoPath) log.Debug(i18n.G("init committed all files for new git repo in %s", repoPath))
} }
return nil return nil
@ -56,20 +57,20 @@ func Init(repoPath string, commit bool, gitName, gitEmail string) error {
func SwitchToMain(repo *git.Repository) error { func SwitchToMain(repo *git.Repository) error {
ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.ReferenceName("refs/heads/main")) ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.ReferenceName("refs/heads/main"))
if err := repo.Storer.SetReference(ref); err != nil { if err := repo.Storer.SetReference(ref); err != nil {
return fmt.Errorf("set reference: %s", err) return errors.New(i18n.G("set reference: %s", err))
} }
cfg, err := repo.Config() cfg, err := repo.Config()
if err != nil { if err != nil {
return fmt.Errorf("repo config: %s", err) return errors.New(i18n.G("repo config: %s", err))
} }
cfg.Init.DefaultBranch = "main" cfg.Init.DefaultBranch = "main"
if err := repo.SetConfig(cfg); err != nil { if err := repo.SetConfig(cfg); err != nil {
return fmt.Errorf("repo set config: %s", err) return errors.New(i18n.G("repo set config: %s", err))
} }
log.Debug("set 'main' as the default branch") log.Debug(i18n.G("set 'main' as the default branch"))
return nil return nil
} }

View File

@ -4,12 +4,13 @@ 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/config" "github.com/go-git/go-git/v5/config"
"coopcloud.tech/abra/pkg/i18n"
) )
// Push pushes the latest changes & optionally tags to the default remote // Push pushes the latest changes & optionally tags to the default remote
func Push(repoDir string, remote string, tags bool, dryRun bool) error { func Push(repoDir string, remote string, tags bool, dryRun bool) error {
if dryRun { if dryRun {
log.Debugf("dry run: no git changes pushed in %s", repoDir) log.Debug(i18n.G("dry run: no git changes pushed in %s", repoDir))
return nil return nil
} }
@ -27,7 +28,7 @@ func Push(repoDir string, remote string, tags bool, dryRun bool) error {
return err return err
} }
log.Debugf("git changes pushed") log.Debug(i18n.G("git changes pushed"))
if tags { if tags {
opts.RefSpecs = append(opts.RefSpecs, config.RefSpec("+refs/tags/*:refs/tags/*")) opts.RefSpecs = append(opts.RefSpecs, config.RefSpec("+refs/tags/*:refs/tags/*"))
@ -36,7 +37,7 @@ func Push(repoDir string, remote string, tags bool, dryRun bool) error {
return err return err
} }
log.Debugf("git tags pushed") log.Debug(i18n.G("git tags pushed"))
} }
return nil return nil

View File

@ -2,7 +2,6 @@ package git
import ( import (
"errors" "errors"
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"os/user" "os/user"
@ -13,6 +12,7 @@ import (
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
gitConfigPkg "github.com/go-git/go-git/v5/config" gitConfigPkg "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing/format/gitignore" "github.com/go-git/go-git/v5/plumbing/format/gitignore"
"coopcloud.tech/abra/pkg/i18n"
) )
// IsClean checks if a repo has unstaged changes // IsClean checks if a repo has unstaged changes
@ -23,12 +23,12 @@ func IsClean(repoPath string) (bool, error) {
return false, git.ErrRepositoryNotExists return false, git.ErrRepositoryNotExists
} }
return false, fmt.Errorf("unable to open %s: %s", repoPath, err) return false, errors.New(i18n.G("unable to open %s: %s", repoPath, err))
} }
worktree, err := repo.Worktree() worktree, err := repo.Worktree()
if err != nil { if err != nil {
return false, fmt.Errorf("unable to open worktree of %s: %s", repoPath, err) return false, errors.New(i18n.G("unable to open worktree of %s: %s", repoPath, err))
} }
patterns, err := GetExcludesFiles() patterns, err := GetExcludesFiles()
@ -42,14 +42,14 @@ func IsClean(repoPath string) (bool, error) {
status, err := worktree.Status() status, err := worktree.Status()
if err != nil { if err != nil {
return false, fmt.Errorf("unable to query status of %s: %s", repoPath, err) return false, errors.New(i18n.G("unable to query status of %s: %s", repoPath, err))
} }
if status.String() != "" { if status.String() != "" {
noNewline := strings.TrimSuffix(status.String(), "\n") noNewline := strings.TrimSuffix(status.String(), "\n")
log.Debugf("git status: %s: %s", repoPath, noNewline) log.Debug(i18n.G("git status: %s: %s", repoPath, noNewline))
} else { } else {
log.Debugf("git status: %s: clean", repoPath) log.Debug(i18n.G("git status: %s: clean", repoPath))
} }
return status.IsClean(), nil return status.IsClean(), nil
@ -85,7 +85,7 @@ func parseGitConfig() (*gitConfigPkg.Config, error) {
globalGitConfig := filepath.Join(usr.HomeDir, ".gitconfig") globalGitConfig := filepath.Join(usr.HomeDir, ".gitconfig")
if _, err := os.Stat(globalGitConfig); err != nil { if _, err := os.Stat(globalGitConfig); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
log.Debugf("no %s exists, not reading any global gitignore config", globalGitConfig) log.Debug(i18n.G("no %s exists, not reading any global gitignore config", globalGitConfig))
return cfg, nil return cfg, nil
} }
return cfg, err return cfg, err
@ -127,7 +127,7 @@ func parseExcludesFile(excludesfile string) ([]gitignore.Pattern, error) {
if _, err := os.Stat(excludesfile); err != nil { if _, err := os.Stat(excludesfile); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
log.Debugf("no %s exists, skipping reading gitignore paths", excludesfile) log.Debug(i18n.G("no %s exists, skipping reading gitignore paths", excludesfile))
return ps, nil return ps, nil
} }
return ps, err return ps, err
@ -146,7 +146,7 @@ func parseExcludesFile(excludesfile string) ([]gitignore.Pattern, error) {
} }
} }
log.Debugf("read global ignore paths: %s", strings.Join(pathsRaw, " ")) log.Debug(i18n.G("read global ignore paths: %s", strings.Join(pathsRaw, " ")))
return ps, nil return ps, nil
} }

View File

@ -6,12 +6,13 @@ 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/config" "github.com/go-git/go-git/v5/config"
"coopcloud.tech/abra/pkg/i18n"
) )
// CreateRemote creates a new git remote in a repository // CreateRemote creates a new git remote in a repository
func CreateRemote(repo *git.Repository, name, url string, dryRun bool) error { func CreateRemote(repo *git.Repository, name, url string, dryRun bool) error {
if dryRun { if dryRun {
log.Debugf("dry run: remote %s (%s) not created", name, url) log.Debug(i18n.G("dry run: remote %s (%s) not created", name, url))
return nil return nil
} }

55
pkg/i18n/i18n.go Normal file
View File

@ -0,0 +1,55 @@
package i18n
import (
"embed"
"fmt"
"os"
"slices"
"strings"
"coopcloud.tech/abra/pkg/log"
"github.com/leonelquinteros/gotext"
)
//go:embed locales
var assetFS embed.FS
var Locale = "en"
func LoadLocale() *gotext.Po {
entries, err := assetFS.ReadDir("locales")
if err != nil {
log.Fatalf("i18n: unable to read embedded locales directory: %s", err)
}
var linguas []string
for _, entry := range entries {
if !entry.IsDir() {
continue
}
linguas = append(linguas, entry.Name())
}
locale := os.Getenv("LANG")
if locale != "" {
if slices.Contains(linguas, locale) {
Locale = locale
} else {
log.Debugf("unsupported language: %s (want: %s)", locale, strings.Join(linguas, " "))
}
}
localePath := fmt.Sprintf("locales/%s/LC_MESSAGES/default.po", Locale)
b, err := assetFS.ReadFile(localePath)
if err != nil {
log.Fatalf("i18n: %s", err)
}
po := gotext.NewPo()
po.Parse(b)
return po
}
var G = LoadLocale().Get

4677
pkg/i18n/locales/default.pot Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +0,0 @@
package integration
import (
"os"
"testing"
)
func skipIfNotIntegration(t *testing.T) {
if os.Getenv("ABRA_INTEGRATION") == "" {
t.Skip("missing 'ABRA_INTEGRATION', not running integration tests")
}
}

View File

@ -1,11 +1,13 @@
package lint package lint
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"path" "path"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
@ -16,8 +18,8 @@ import (
) )
var ( var (
Warn = "warn" Warn = i18n.G("warn")
Critical = "critical" Critical = i18n.G("critical")
) )
type LintFunction func(recipe.Recipe) (bool, error) type LintFunction func(recipe.Recipe) (bool, error)
@ -47,10 +49,10 @@ func (l LintRule) Skip(recipe recipe.Recipe) bool {
if l.SkipCondition != nil { if l.SkipCondition != nil {
ok, err := l.SkipCondition(recipe) ok, err := l.SkipCondition(recipe)
if err != nil { if err != nil {
log.Debugf("%s: skip condition: %s", l.Ref, err) log.Debug(i18n.G("%s: skip condition: %s", l.Ref, err))
} }
if ok { if ok {
log.Debugf("skipping %s based on skip condition", l.Ref) log.Debug(i18n.G("skipping %s based on skip condition", l.Ref))
return true return true
} }
} }
@ -62,117 +64,117 @@ var LintRules = map[string][]LintRule{
"warn": { "warn": {
{ {
Ref: "R001", Ref: "R001",
Level: "warn", Level: i18n.G("warn"),
Description: "compose config has expected version", Description: i18n.G("compose config has expected version"),
HowToResolve: "ensure 'version: \"3.8\"' in compose configs", HowToResolve: i18n.G("ensure 'version: \"3.8\"' in compose configs"),
Function: LintComposeVersion, Function: LintComposeVersion,
}, },
{ {
Ref: "R002", Ref: "R002",
Level: "warn", Level: i18n.G("warn"),
Description: "healthcheck enabled for all services", Description: i18n.G("healthcheck enabled for all services"),
HowToResolve: "wire up healthchecks", HowToResolve: i18n.G("wire up healthchecks"),
Function: LintHealthchecks, Function: LintHealthchecks,
}, },
{ {
Ref: "R003", Ref: "R003",
Level: "warn", Level: i18n.G("warn"),
Description: "all images use a tag", Description: i18n.G("all images use a tag"),
HowToResolve: "use a tag for all images", HowToResolve: i18n.G("use a tag for all images"),
Function: LintAllImagesTagged, Function: LintAllImagesTagged,
}, },
{ {
Ref: "R004", Ref: "R004",
Level: "warn", Level: i18n.G("warn"),
Description: "no unstable tags", Description: i18n.G("no unstable tags"),
HowToResolve: "tag all images with stable tags", HowToResolve: i18n.G("tag all images with stable tags"),
Function: LintNoUnstableTags, Function: LintNoUnstableTags,
}, },
{ {
Ref: "R005", Ref: "R005",
Level: "warn", Level: i18n.G("warn"),
Description: "tags use semver-like format", Description: i18n.G("tags use semver-like format"),
HowToResolve: "use semver-like tags", HowToResolve: i18n.G("use semver-like tags"),
Function: LintSemverLikeTags, Function: LintSemverLikeTags,
}, },
{ {
Ref: "R006", Ref: "R006",
Level: "warn", Level: i18n.G("warn"),
Description: "has published catalogue version", Description: i18n.G("has published catalogue version"),
HowToResolve: "publish a recipe version to the catalogue", HowToResolve: i18n.G("publish a recipe version to the catalogue"),
Function: LintHasPublishedVersion, Function: LintHasPublishedVersion,
}, },
{ {
Ref: "R007", Ref: "R007",
Level: "warn", Level: i18n.G("warn"),
Description: "README.md metadata filled in", Description: i18n.G("README.md metadata filled in"),
HowToResolve: "fill out all the metadata", HowToResolve: i18n.G("fill out all the metadata"),
Function: LintMetadataFilledIn, Function: LintMetadataFilledIn,
}, },
{ {
Ref: "R013", Ref: "R013",
Level: "warn", Level: i18n.G("warn"),
Description: "git.coopcloud.tech repo exists", Description: i18n.G("git.coopcloud.tech repo exists"),
HowToResolve: "upload your recipe to git.coopcloud.tech/coop-cloud/...", HowToResolve: i18n.G("upload your recipe to git.coopcloud.tech/coop-cloud/..."),
Function: LintHasRecipeRepo, Function: LintHasRecipeRepo,
}, },
{ {
Ref: "R015", Ref: "R015",
Level: "warn", Level: i18n.G("warn"),
Description: "long secret names", Description: i18n.G("long secret names"),
HowToResolve: "reduce length of secret names to 12 chars", HowToResolve: i18n.G("reduce length of secret names to 12 chars"),
Function: LintSecretLengths, Function: LintSecretLengths,
}, },
}, },
"error": { "error": {
{ {
Ref: "R008", Ref: "R008",
Level: "error", Level: i18n.G("error"),
Description: ".env.sample provided", Description: i18n.G(".env.sample provided"),
HowToResolve: "create an example .env.sample", HowToResolve: i18n.G("create an example .env.sample"),
Function: LintEnvConfigPresent, Function: LintEnvConfigPresent,
}, },
{ {
Ref: "R009", Ref: "R009",
Level: "error", Level: i18n.G("error"),
Description: "one service named 'app'", Description: i18n.G("one service named 'app'"),
HowToResolve: "name a servce 'app'", HowToResolve: i18n.G("name a servce 'app'"),
Function: LintAppService, Function: LintAppService,
}, },
{ {
Ref: "R015", Ref: "R015",
Level: "error", Level: i18n.G("error"),
Description: "deploy labels stanza present", Description: i18n.G("deploy labels stanza present"),
HowToResolve: "include \"deploy: labels: ...\" stanza", HowToResolve: i18n.G("include \"deploy: labels: ...\" stanza"),
Function: LintDeployLabelsPresent, Function: LintDeployLabelsPresent,
}, },
{ {
Ref: "R010", Ref: "R010",
Level: "error", Level: i18n.G("error"),
Description: "traefik routing enabled", Description: i18n.G("traefik routing enabled"),
HowToResolve: "include \"traefik.enable=true\" deploy label", HowToResolve: i18n.G("include \"traefik.enable=true\" deploy label"),
Function: LintTraefikEnabled, Function: LintTraefikEnabled,
SkipCondition: LintTraefikEnabledSkipCondition, SkipCondition: LintTraefikEnabledSkipCondition,
}, },
{ {
Ref: "R011", Ref: "R011",
Level: "error", Level: i18n.G("error"),
Description: "all services have images", Description: i18n.G("all services have images"),
HowToResolve: "ensure \"image: ...\" set on all services", HowToResolve: i18n.G("ensure \"image: ...\" set on all services"),
Function: LintImagePresent, Function: LintImagePresent,
}, },
{ {
Ref: "R012", Ref: "R012",
Level: "error", Level: i18n.G("error"),
Description: "config version are vendored", Description: i18n.G("config version are vendored"),
HowToResolve: "vendor config versions in an abra.sh", HowToResolve: i18n.G("vendor config versions in an abra.sh"),
Function: LintAbraShVendors, Function: LintAbraShVendors,
}, },
{ {
Ref: "R014", Ref: "R014",
Level: "error", Level: i18n.G("error"),
Description: "only annotated tags used for recipe version", Description: i18n.G("only annotated tags used for recipe version"),
HowToResolve: "replace lightweight tag with annotated tag", HowToResolve: i18n.G("replace lightweight tag with annotated tag"),
Function: LintValidTags, Function: LintValidTags,
}, },
}, },
@ -182,7 +184,9 @@ var LintRules = map[string][]LintRule{
// used in code paths such as "app deploy" to avoid nasty surprises but not for // used in code paths such as "app deploy" to avoid nasty surprises but not for
// the typical linting commands, which do handle other levels. // the typical linting commands, which do handle other levels.
func LintForErrors(recipe recipe.Recipe) error { func LintForErrors(recipe recipe.Recipe) error {
log.Debugf("linting for critical errors in %s configs", recipe.Name) log.Debug(i18n.G("linting for critical errors in %s configs", recipe.Name))
var errs string
for level := range LintRules { for level := range LintRules {
if level != "error" { if level != "error" {
@ -196,15 +200,19 @@ func LintForErrors(recipe recipe.Recipe) error {
ok, err := rule.Function(recipe) ok, err := rule.Function(recipe)
if err != nil { if err != nil {
return fmt.Errorf("lint %s: %s", rule.Ref, err) errs += i18n.G("\nlint %s: %s", rule.Ref, err)
} }
if !ok { if !ok {
return fmt.Errorf("lint error in %s configs: \"%s\" failed lint checks (%s)", recipe.Name, rule.Description, rule.Ref) errs += fmt.Sprintf("\n * %s (%s)", rule.Description, rule.Ref)
} }
} }
} }
log.Debugf("linting successful, %s is well configured", recipe.Name) if len(errs) > 0 {
return errors.New(i18n.G("recipe '%s' failed lint checks:\n%s", recipe.Name, errs[1:]))
}
log.Debug(i18n.G("linting successful, %s is well configured", recipe.Name))
return nil return nil
} }
@ -250,7 +258,7 @@ func LintAppService(recipe recipe.Recipe) (bool, error) {
func LintTraefikEnabledSkipCondition(r recipe.Recipe) (bool, error) { func LintTraefikEnabledSkipCondition(r recipe.Recipe) (bool, error) {
sampleEnv, err := r.SampleEnv() sampleEnv, err := r.SampleEnv()
if err != nil { if err != nil {
return false, fmt.Errorf("Unable to discover .env.sample for %s", r.Name) return false, errors.New(i18n.G("unable to discover .env.sample for %s", r.Name))
} }
if _, ok := sampleEnv["DOMAIN"]; !ok { if _, ok := sampleEnv["DOMAIN"]; !ok {
@ -470,7 +478,7 @@ func LintSecretLengths(recipe recipe.Recipe) (bool, error) {
} }
for name := range config.Secrets { for name := range config.Secrets {
if len(name) > 12 { if len(name) > 12 {
return false, fmt.Errorf("secret %s is longer than 12 characters", name) return false, errors.New(i18n.G("secret %s is longer than 12 characters", name))
} }
} }
@ -480,12 +488,12 @@ func LintSecretLengths(recipe recipe.Recipe) (bool, error) {
func LintValidTags(recipe recipe.Recipe) (bool, error) { func LintValidTags(recipe recipe.Recipe) (bool, error) {
repo, err := git.PlainOpen(recipe.Dir) repo, err := git.PlainOpen(recipe.Dir)
if err != nil { if err != nil {
return false, fmt.Errorf("unable to open %s: %s", recipe.Dir, err) return false, errors.New(i18n.G("unable to open %s: %s", recipe.Dir, err))
} }
iter, err := repo.Tags() iter, err := repo.Tags()
if err != nil { if err != nil {
log.Fatalf("unable to list local tags for %s", recipe.Name) log.Fatal(i18n.G("unable to list local tags for %s", recipe.Name))
} }
if err := iter.ForEach(func(ref *plumbing.Reference) error { if err := iter.ForEach(func(ref *plumbing.Reference) error {
@ -493,7 +501,7 @@ func LintValidTags(recipe recipe.Recipe) (bool, error) {
if err != nil { if err != nil {
switch err { switch err {
case plumbing.ErrObjectNotFound: case plumbing.ErrObjectNotFound:
return fmt.Errorf("invalid lightweight tag detected") return errors.New(i18n.G("invalid lightweight tag detected"))
default: default:
return err return err
} }

View File

@ -3,6 +3,7 @@ package logs
import ( import (
"bufio" "bufio"
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -13,6 +14,7 @@ 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"
"coopcloud.tech/abra/pkg/i18n"
) )
type TailOpts struct { type TailOpts struct {
@ -81,7 +83,7 @@ func TailLogs(
} }
if _, err = io.Copy(os.Stdout, logs); err != nil && err != io.EOF { if _, err = io.Copy(os.Stdout, logs); err != nil && err != io.EOF {
errCh <- fmt.Errorf("tailLogs: unable to copy buffer: %s", err) errCh <- errors.New(i18n.G("tailLogs: unable to copy buffer: %s", err))
} }
} }
}(service.ID) }(service.ID)

View File

@ -1,6 +1,7 @@
package recipe package recipe
import ( import (
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@ -13,6 +14,7 @@ import (
loader "coopcloud.tech/abra/pkg/upstream/stack" loader "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/distribution/reference" "github.com/distribution/reference"
composetypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types"
"coopcloud.tech/abra/pkg/i18n"
) )
// GetComposeFiles gets the list of compose files for an app (or recipe if you // GetComposeFiles gets the list of compose files for an app (or recipe if you
@ -24,7 +26,7 @@ func (r Recipe) GetComposeFiles(appEnv map[string]string) ([]string, error) {
if err := ensurePathExists(r.ComposePath); err != nil { if err := ensurePathExists(r.ComposePath); err != nil {
return []string{}, err return []string{}, err
} }
log.Debugf("no COMPOSE_FILE detected, loading default: %s", r.ComposePath) log.Debug(i18n.G("no COMPOSE_FILE detected, loading default: %s", r.ComposePath))
return []string{r.ComposePath}, nil return []string{r.ComposePath}, nil
} }
@ -33,7 +35,7 @@ func (r Recipe) GetComposeFiles(appEnv map[string]string) ([]string, error) {
if err := ensurePathExists(path); err != nil { if err := ensurePathExists(path); err != nil {
return []string{}, err return []string{}, err
} }
log.Debugf("COMPOSE_FILE detected, loading %s", path) log.Debug(i18n.G("COMPOSE_FILE detected, loading %s", path))
return []string{path}, nil return []string{path}, nil
} }
@ -42,7 +44,7 @@ func (r Recipe) GetComposeFiles(appEnv map[string]string) ([]string, error) {
numComposeFiles := strings.Count(composeFileEnvVar, ":") + 1 numComposeFiles := strings.Count(composeFileEnvVar, ":") + 1
envVars := strings.SplitN(composeFileEnvVar, ":", numComposeFiles) envVars := strings.SplitN(composeFileEnvVar, ":", numComposeFiles)
if len(envVars) != numComposeFiles { if len(envVars) != numComposeFiles {
return composeFiles, fmt.Errorf("COMPOSE_FILE (=\"%s\") parsing failed?", composeFileEnvVar) return composeFiles, errors.New(i18n.G("COMPOSE_FILE (=\"%s\") parsing failed?", composeFileEnvVar))
} }
for _, file := range envVars { for _, file := range envVars {
@ -53,8 +55,8 @@ func (r Recipe) GetComposeFiles(appEnv map[string]string) ([]string, error) {
composeFiles = append(composeFiles, path) composeFiles = append(composeFiles, path)
} }
log.Debugf("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", ")) log.Debug(i18n.G("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", ")))
log.Debugf("retrieved %s configs for %s", strings.Join(composeFiles, ", "), r.Name) log.Debug(i18n.G("retrieved %s configs for %s", strings.Join(composeFiles, ", "), r.Name))
return composeFiles, nil return composeFiles, nil
} }
@ -67,7 +69,7 @@ func (r Recipe) GetComposeConfig(env map[string]string) (*composetypes.Config, e
} }
if len(composeFiles) == 0 { if len(composeFiles) == 0 {
return nil, fmt.Errorf("%s is missing a compose.yml or compose.*.yml file?", r.Name) return nil, errors.New(i18n.G("%s is missing a compose.yml or compose.*.yml file?", r.Name))
} }
if env == nil { if env == nil {
@ -102,7 +104,7 @@ func (r Recipe) GetVersionLabelLocal() (string, error) {
} }
if label == "" { if label == "" {
return label, fmt.Errorf("%s has no version label? try running \"abra recipe sync %s\" first?", r.Name, r.Name) return label, errors.New(i18n.G("%s has no version label? try running \"abra recipe sync %s\" first?", r.Name, r.Name))
} }
return label, nil return label, nil
@ -118,7 +120,7 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) {
return false, err return false, err
} }
log.Debugf("considering %s config(s) for tag update", strings.Join(composeFiles, ", ")) log.Debug(i18n.G("considering %s config(s) for tag update", strings.Join(composeFiles, ", ")))
for _, composeFile := range composeFiles { for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}} opts := stack.Deploy{Composefiles: []string{composeFile}}
@ -148,13 +150,13 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) {
case reference.NamedTagged: case reference.NamedTagged:
composeTag = img.(reference.NamedTagged).Tag() composeTag = img.(reference.NamedTagged).Tag()
default: default:
log.Debugf("unable to parse %s, skipping", img) log.Debug(i18n.G("unable to parse %s, skipping", img))
continue continue
} }
composeImage := formatter.StripTagMeta(reference.Path(img)) composeImage := formatter.StripTagMeta(reference.Path(img))
log.Debugf("parsed %s from %s", composeTag, service.Image) log.Debug(i18n.G("parsed %s from %s", composeTag, service.Image))
if image == composeImage { if image == composeImage {
bytes, err := ioutil.ReadFile(composeFile) bytes, err := ioutil.ReadFile(composeFile)
@ -166,7 +168,7 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) {
new := fmt.Sprintf("%s:%s", composeImage, tag) new := fmt.Sprintf("%s:%s", composeImage, tag)
replacedBytes := strings.Replace(string(bytes), old, new, -1) replacedBytes := strings.Replace(string(bytes), old, new, -1)
log.Debugf("updating %s to %s in %s", old, new, compose.Filename) log.Debug(i18n.G("updating %s to %s in %s", old, new, compose.Filename))
if err := os.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil { if err := os.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
return false, err return false, err
@ -186,7 +188,7 @@ func (r Recipe) UpdateLabel(pattern, serviceName, label string) error {
return err return err
} }
log.Debugf("considering %s config(s) for label update", strings.Join(composeFiles, ", ")) log.Debug(i18n.G("considering %s config(s) for label update", strings.Join(composeFiles, ", ")))
for _, composeFile := range composeFiles { for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}} opts := stack.Deploy{Composefiles: []string{composeFile}}
@ -224,27 +226,27 @@ func (r Recipe) UpdateLabel(pattern, serviceName, label string) error {
return err return err
} }
old := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", value) old := i18n.G("coop-cloud.${STACK_NAME}.version=%s", value)
replacedBytes := strings.Replace(string(bytes), old, label, -1) replacedBytes := strings.Replace(string(bytes), old, label, -1)
if old == label { if old == label {
log.Warnf("%s is already set, nothing to do?", label) log.Warnf(i18n.G("%s is already set, nothing to do?", label))
return nil return nil
} }
log.Debugf("updating %s to %s in %s", old, label, compose.Filename) log.Debug(i18n.G("updating %s to %s in %s", old, label, compose.Filename))
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil { if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
return err return err
} }
log.Infof("synced label %s to service %s", label, serviceName) log.Infof(i18n.G("synced label %s to service %s", label, serviceName))
} }
} }
if !discovered { if !discovered {
log.Warn("no existing label found, automagic insertion not supported yet") log.Warn(i18n.G("no existing label found, automagic insertion not supported yet"))
log.Fatalf("add '- \"%s\"' manually to the 'app' service in %s", label, composeFile) log.Fatal(i18n.G("add '- \"%s\"' manually to the 'app' service in %s", label, composeFile))
} }
} }

View File

@ -1,18 +1,20 @@
package recipe package recipe
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path" "path"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
) )
func (r Recipe) SampleEnv() (map[string]string, error) { func (r Recipe) SampleEnv() (map[string]string, error) {
sampleEnv, err := envfile.ReadEnv(r.SampleEnvPath) sampleEnv, err := envfile.ReadEnv(r.SampleEnvPath)
if err != nil { if err != nil {
return sampleEnv, fmt.Errorf("unable to discover .env.sample for %s", r.Name) return sampleEnv, errors.New(i18n.G("unable to discover .env.sample for %s", r.Name))
} }
return sampleEnv, nil return sampleEnv, nil
} }
@ -31,7 +33,7 @@ func (r Recipe) GetReleaseNotes(version string) (string, error) {
return "", err return "", err
} }
title := formatter.BoldStyle.Render(fmt.Sprintf("%s release notes:", version)) title := formatter.BoldStyle.Render(i18n.G("%s release notes:", version))
withTitle := fmt.Sprintf("%s\n%s\n", title, releaseNotes) withTitle := fmt.Sprintf("%s\n%s\n", title, releaseNotes)
return withTitle, nil return withTitle, nil

View File

@ -1,6 +1,7 @@
package recipe package recipe
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"slices" "slices"
@ -15,6 +16,7 @@ import (
"github.com/distribution/reference" "github.com/distribution/reference"
"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"
"coopcloud.tech/abra/pkg/i18n"
) )
type EnsureContext struct { type EnsureContext struct {
@ -40,14 +42,14 @@ func (r Recipe) Ensure(ctx EnsureContext) error {
if !ctx.Offline { if !ctx.Offline {
if err := r.EnsureUpToDate(); err != nil { if err := r.EnsureUpToDate(); err != nil {
log.Fatal(err) return err
} }
} }
if r.EnvVersion != "" && !ctx.IgnoreEnvVersion { if r.EnvVersion != "" && !ctx.IgnoreEnvVersion {
log.Debugf("ensuring env version %s", r.EnvVersion) log.Debug(i18n.G("ensuring env version %s", r.EnvVersion))
if strings.Contains(r.EnvVersion, "+U") { if strings.Contains(r.EnvVersion, "+U") {
log.Fatalf("can not redeploy chaos version (%s) without --chaos", r.EnvVersion) return errors.New(i18n.G("can not redeploy chaos version (%s) without --chaos", r.EnvVersion))
} }
if _, err := r.EnsureVersion(r.EnvVersion); err != nil { if _, err := r.EnsureVersion(r.EnvVersion); err != nil {
@ -146,16 +148,16 @@ func (r Recipe) EnsureVersion(version string) (bool, error) {
joinedTags := strings.Join(parsedTags, ", ") joinedTags := strings.Join(parsedTags, ", ")
if joinedTags != "" { if joinedTags != "" {
log.Debugf("read %s as tags for recipe %s", joinedTags, r.Name) log.Debug(i18n.G("read %s as tags for recipe %s", joinedTags, r.Name))
} }
var opts *git.CheckoutOptions var opts *git.CheckoutOptions
if tagRef.String() == "" { if tagRef.String() == "" {
log.Debugf("attempting to checkout '%s' as chaos commit", version) log.Debug(i18n.G("attempting to checkout '%s' as chaos commit", version))
hash, err := repo.ResolveRevision(plumbing.Revision(version)) hash, err := repo.ResolveRevision(plumbing.Revision(version))
if err != nil { if err != nil {
log.Fatalf("unable to resolve '%s': %s", version, err) log.Fatal(i18n.G("unable to resolve '%s': %s", version, err))
} }
opts = &git.CheckoutOptions{Hash: *hash, Create: false, Force: true} opts = &git.CheckoutOptions{Hash: *hash, Create: false, Force: true}
@ -173,7 +175,7 @@ func (r Recipe) EnsureVersion(version string) (bool, error) {
return isChaosCommit, nil return isChaosCommit, nil
} }
log.Debugf("successfully checked %s out to %s in %s", r.Name, tagRef.Short(), r.Dir) log.Debug(i18n.G("successfully checked %s out to %s in %s", r.Name, tagRef.Short(), r.Dir))
return isChaosCommit, nil return isChaosCommit, nil
} }
@ -182,11 +184,11 @@ func (r Recipe) EnsureVersion(version string) (bool, error) {
func (r Recipe) EnsureIsClean() error { func (r Recipe) EnsureIsClean() error {
isClean, err := gitPkg.IsClean(r.Dir) isClean, err := gitPkg.IsClean(r.Dir)
if err != nil { if err != nil {
return fmt.Errorf("unable to check git clean status in %s: %s", r.Dir, err) return errors.New(i18n.G("unable to check git clean status in %s: %s", r.Dir, err))
} }
if !isClean { if !isClean {
return fmt.Errorf("%s (%s) has locally unstaged changes?", r.Name, r.Dir) return errors.New(i18n.G("%s (%s) has locally unstaged changes?", r.Name, r.Dir))
} }
return nil return nil
@ -220,7 +222,7 @@ func (r Recipe) EnsureLatest() error {
} }
if err := worktree.Checkout(checkOutOpts); err != nil { if err := worktree.Checkout(checkOutOpts); err != nil {
log.Debugf("failed to check out %s in %s", branch, r.Dir) log.Debug(i18n.G("failed to check out %s in %s", branch, r.Dir))
return err return err
} }
@ -231,33 +233,33 @@ func (r Recipe) EnsureLatest() error {
func (r Recipe) EnsureUpToDate() error { func (r Recipe) EnsureUpToDate() error {
repo, err := git.PlainOpen(r.Dir) repo, err := git.PlainOpen(r.Dir)
if err != nil { if err != nil {
return fmt.Errorf("unable to open %s: %s", r.Dir, err) return errors.New(i18n.G("unable to open %s: %s", r.Dir, err))
} }
remotes, err := repo.Remotes() remotes, err := repo.Remotes()
if err != nil { if err != nil {
return fmt.Errorf("unable to read remotes in %s: %s", r.Dir, err) return errors.New(i18n.G("unable to read remotes in %s: %s", r.Dir, err))
} }
if len(remotes) == 0 { if len(remotes) == 0 {
log.Debugf("cannot ensure %s is up-to-date, no git remotes configured", r.Name) log.Debug(i18n.G("cannot ensure %s is up-to-date, no git remotes configured", r.Name))
return nil return nil
} }
worktree, err := repo.Worktree() worktree, err := repo.Worktree()
if err != nil { if err != nil {
return fmt.Errorf("unable to open git work tree in %s: %s", r.Dir, err) return errors.New(i18n.G("unable to open git work tree in %s: %s", r.Dir, err))
} }
branch, err := gitPkg.CheckoutDefaultBranch(repo, r.Dir) branch, err := gitPkg.CheckoutDefaultBranch(repo, r.Dir)
if err != nil { if err != nil {
return fmt.Errorf("unable to check out default branch in %s: %s", r.Dir, err) return errors.New(i18n.G("unable to check out default branch in %s: %s", r.Dir, err))
} }
fetchOpts := &git.FetchOptions{Tags: git.AllTags} fetchOpts := &git.FetchOptions{Tags: git.AllTags}
if err := repo.Fetch(fetchOpts); err != nil { if err := repo.Fetch(fetchOpts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") { if !strings.Contains(err.Error(), "already up-to-date") {
return fmt.Errorf("unable to fetch tags in %s: %s", r.Dir, err) return errors.New(i18n.G("unable to fetch tags in %s: %s", r.Dir, err))
} }
} }
@ -269,11 +271,11 @@ func (r Recipe) EnsureUpToDate() error {
if err := worktree.Pull(opts); err != nil { if err := worktree.Pull(opts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") { if !strings.Contains(err.Error(), "already up-to-date") {
return fmt.Errorf("unable to git pull in %s: %s", r.Dir, err) return errors.New(i18n.G("unable to git pull in %s: %s", r.Dir, err))
} }
} }
log.Debugf("fetched latest git changes for %s", r.Name) log.Debug(i18n.G("fetched latest git changes for %s", r.Name))
return nil return nil
} }
@ -362,7 +364,7 @@ func (r Recipe) Tags() ([]string, error) {
return version1.IsLessThan(version2) return version1.IsLessThan(version2)
}) })
log.Debugf("detected %s as tags for recipe %s", strings.Join(tags, ", "), r.Name) log.Debug(i18n.G("detected %s as tags for recipe %s", strings.Join(tags, ", "), r.Name))
return tags, nil return tags, nil
} }
@ -373,7 +375,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
versions := RecipeVersions{} versions := RecipeVersions{}
log.Debugf("git: opening repository in %s", r.Dir) log.Debug(i18n.G("git: opening repository in %s", r.Dir))
repo, err := git.PlainOpen(r.Dir) repo, err := git.PlainOpen(r.Dir)
if err != nil { if err != nil {
@ -393,7 +395,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) { if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
tag := strings.TrimPrefix(string(ref.Name()), "refs/tags/") tag := strings.TrimPrefix(string(ref.Name()), "refs/tags/")
log.Debugf("processing %s for %s", tag, r.Name) log.Debug(i18n.G("processing %s for %s", tag, r.Name))
checkOutOpts := &git.CheckoutOptions{ checkOutOpts := &git.CheckoutOptions{
Create: false, Create: false,
@ -401,11 +403,11 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
Branch: plumbing.ReferenceName(ref.Name()), Branch: plumbing.ReferenceName(ref.Name()),
} }
if err := worktree.Checkout(checkOutOpts); err != nil { if err := worktree.Checkout(checkOutOpts); err != nil {
log.Debugf("failed to check out %s in %s", tag, r.Dir) log.Debug(i18n.G("failed to check out %s in %s", tag, r.Dir))
return err return err
} }
log.Debugf("git checkout: %s in %s", ref.Name(), r.Dir) log.Debug(i18n.G("git checkout: %s in %s", ref.Name(), r.Dir))
config, err := r.GetComposeConfig(nil) config, err := r.GetComposeConfig(nil)
if err != nil { if err != nil {
@ -429,7 +431,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
case reference.NamedTagged: case reference.NamedTagged:
tag = img.(reference.NamedTagged).Tag() tag = img.(reference.NamedTagged).Tag()
case reference.Named: case reference.Named:
warnMsg = append(warnMsg, fmt.Sprintf("%s service is missing image tag?", path)) warnMsg = append(warnMsg, i18n.G("%s service is missing image tag?", path))
continue continue
} }
@ -453,7 +455,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
sortRecipeVersions(versions) sortRecipeVersions(versions)
log.Debugf("collected %s for %s", versions, r.Dir) log.Debug(i18n.G("collected %s for %s", versions, r.Dir))
var uniqueWarnings []string var uniqueWarnings []string
for _, w := range warnMsg { for _, w := range warnMsg {

View File

@ -13,6 +13,7 @@ import (
"strings" "strings"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/catalogue" "coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
@ -70,7 +71,7 @@ func (r RecipeMeta) LatestVersion() string {
version = tag version = tag
} }
log.Debugf("choosing %s as latest version of %s", version, r.Name) log.Debug(i18n.G("choosing %s as latest version of %s", version, r.Name))
return version return version
} }
@ -126,7 +127,7 @@ func Get(name string) Recipe {
if strings.Contains(name, ":") { if strings.Contains(name, ":") {
split := strings.Split(name, ":") split := strings.Split(name, ":")
if len(split) > 2 { if len(split) > 2 {
log.Fatalf("version seems invalid: %s", name) log.Fatal(i18n.G("version seems invalid: %s", name))
} }
name = split[0] name = split[0]
@ -134,7 +135,7 @@ func Get(name string) Recipe {
versionRaw = version versionRaw = version
if strings.HasSuffix(version, config.DIRTY_DEFAULT) { if strings.HasSuffix(version, config.DIRTY_DEFAULT) {
version = strings.Replace(split[1], config.DIRTY_DEFAULT, "", 1) version = strings.Replace(split[1], config.DIRTY_DEFAULT, "", 1)
log.Debugf("removed dirty suffix from .env version: %s -> %s", split[1], version) log.Debug(i18n.G("removed dirty suffix from .env version: %s -> %s", split[1], version))
} }
} }
@ -143,7 +144,7 @@ func Get(name string) Recipe {
if strings.Contains(name, "/") { if strings.Contains(name, "/") {
u, err := url.Parse(name) u, err := url.Parse(name)
if err != nil { if err != nil {
log.Fatalf("invalid recipe: %s", err) log.Fatal(i18n.G("invalid recipe: %s", err))
} }
u.Scheme = "https" u.Scheme = "https"
gitURL = u.String() + ".git" gitURL = u.String() + ".git"
@ -171,7 +172,7 @@ func Get(name string) Recipe {
dirty, err := r.IsDirty() dirty, err := r.IsDirty()
if err != nil && !errors.Is(err, git.ErrRepositoryNotExists) { if err != nil && !errors.Is(err, git.ErrRepositoryNotExists) {
log.Fatalf("failed to check git status of %s: %s", r.Name, err) log.Fatal(i18n.G("failed to check git status of %s: %s", r.Name, err))
} }
r.Dirty = dirty r.Dirty = dirty
@ -195,16 +196,16 @@ type Recipe struct {
// String outputs a human-friendly string representation. // String outputs a human-friendly string representation.
func (r Recipe) String() string { func (r Recipe) String() string {
out := fmt.Sprintf("{name: %s, ", r.Name) out := i18n.G("{name: %s, ", r.Name)
out += fmt.Sprintf("version : %s, ", r.EnvVersion) out += i18n.G("version : %s, ", r.EnvVersion)
out += fmt.Sprintf("dirty: %v, ", r.Dirty) out += i18n.G("dirty: %v, ", r.Dirty)
out += fmt.Sprintf("dir: %s, ", r.Dir) out += i18n.G("dir: %s, ", r.Dir)
out += fmt.Sprintf("git url: %s, ", r.GitURL) out += i18n.G("git url: %s, ", r.GitURL)
out += fmt.Sprintf("ssh url: %s, ", r.SSHURL) out += i18n.G("ssh url: %s, ", r.SSHURL)
out += fmt.Sprintf("compose: %s, ", r.ComposePath) out += i18n.G("compose: %s, ", r.ComposePath)
out += fmt.Sprintf("readme: %s, ", r.ReadmePath) out += i18n.G("readme: %s, ", r.ReadmePath)
out += fmt.Sprintf("sample env: %s, ", r.SampleEnvPath) out += i18n.G("sample env: %s, ", r.SampleEnvPath)
out += fmt.Sprintf("abra.sh: %s}", r.AbraShPath) out += i18n.G("abra.sh: %s}", r.AbraShPath)
return out return out
} }
@ -233,7 +234,7 @@ func GetRecipeFeaturesAndCategory(r Recipe) (Features, string, []string, error)
feat = Features{} feat = Features{}
) )
log.Debugf("%s: attempt recipe metadata parse", r.ReadmePath) log.Debug(i18n.G("%s: attempt recipe metadata parse", r.ReadmePath))
readmeFS, err := ioutil.ReadFile(r.ReadmePath) readmeFS, err := ioutil.ReadFile(r.ReadmePath)
if err != nil { if err != nil {
@ -321,12 +322,12 @@ func GetImageMetadata(imageRowString, recipeName string) (Image, []string, error
if imageRowString != "" { if imageRowString != "" {
warnMsgs = append( warnMsgs = append(
warnMsgs, warnMsgs,
fmt.Sprintf("%s: image meta has incorrect format: %s", recipeName, imageRowString), i18n.G("%s: image meta has incorrect format: %s", recipeName, imageRowString),
) )
} else { } else {
warnMsgs = append( warnMsgs = append(
warnMsgs, warnMsgs,
fmt.Sprintf("%s: image meta is empty?", recipeName), i18n.G("%s: image meta is empty?", recipeName),
) )
} }
@ -357,14 +358,14 @@ func GetImageMetadata(imageRowString, recipeName string) (Image, []string, error
func GetStringInBetween(recipeName, str, start, end string) (result string, err error) { func GetStringInBetween(recipeName, str, start, end string) (result string, err error) {
s := strings.Index(str, start) s := strings.Index(str, start)
if s == -1 { if s == -1 {
return "", fmt.Errorf("%s: marker string %s not found", recipeName, start) return "", errors.New(i18n.G("%s: marker string %s not found", recipeName, start))
} }
s += len(start) s += len(start)
e := strings.Index(str[s:], end) e := strings.Index(str[s:], end)
if e == -1 { if e == -1 {
return "", fmt.Errorf("%s: end marker %s not found", recipeName, end) return "", errors.New(i18n.G("%s: end marker %s not found", recipeName, end))
} }
return str[s : s+e], nil return str[s : s+e], nil
@ -402,7 +403,7 @@ func readRecipeCatalogueFS(target interface{}) error {
return err return err
} }
log.Debugf("read recipe catalogue from file system cache in %s", config.RECIPES_JSON) log.Debug(i18n.G("read recipe catalogue from file system cache in %s", config.RECIPES_JSON))
return nil return nil
} }
@ -431,7 +432,7 @@ func VersionsOfService(recipe, serviceName string, offline bool) ([]string, erro
} }
} }
log.Debugf("detected versions %s for %s", strings.Join(versions, ", "), recipe) log.Debug(i18n.G("detected versions %s for %s", strings.Join(versions, ", "), recipe))
return versions, nil return versions, nil
} }
@ -454,11 +455,11 @@ func GetRecipeMeta(recipeName string, offline bool) (RecipeMeta, error) {
recipeMeta, ok := catl[recipeName] recipeMeta, ok := catl[recipeName]
if !ok { if !ok {
return RecipeMeta{}, RecipeMissingFromCatalogue{ return RecipeMeta{}, RecipeMissingFromCatalogue{
err: fmt.Sprintf("recipe %s does not exist?", recipeName), err: i18n.G("recipe %s does not exist?", recipeName),
} }
} }
log.Debugf("recipe metadata retrieved for %s", recipeName) log.Debug(i18n.G("recipe metadata retrieved for %s", recipeName))
return recipeMeta, nil return recipeMeta, nil
} }
@ -545,13 +546,13 @@ func ReadReposMetadata(debug bool) (RepoCatalogue, error) {
reposMeta := make(RepoCatalogue) reposMeta := make(RepoCatalogue)
pageIdx := 1 pageIdx := 1
bar := formatter.CreateProgressbar(-1, "collecting recipe listing") bar := formatter.CreateProgressbar(-1, i18n.G("collecting recipe listing"))
for { for {
var reposList []RepoMeta var reposList []RepoMeta
pagedURL := fmt.Sprintf("%s?page=%v", ReposMetadataURL, pageIdx) pagedURL := fmt.Sprintf("%s?page=%v", ReposMetadataURL, pageIdx)
log.Debugf("fetching repo metadata from %s", pagedURL) log.Debug(i18n.G("fetching repo metadata from %s", pagedURL))
if err := web.ReadJSON(pagedURL, &reposList); err != nil { if err := web.ReadJSON(pagedURL, &reposList); err != nil {
return reposMeta, err return reposMeta, err
@ -655,7 +656,7 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string, debug bool) erro
cloneLimiter := limit.New(3) cloneLimiter := limit.New(3)
retrieveBar := formatter.CreateProgressbar(barLength, "retrieving recipes") retrieveBar := formatter.CreateProgressbar(barLength, i18n.G("retrieving recipes"))
ch := make(chan string, barLength) ch := make(chan string, barLength)
for _, repoMeta := range repos { for _, repoMeta := range repos {
go func(rm RepoMeta) { go func(rm RepoMeta) {

View File

@ -5,13 +5,14 @@ import (
"fmt" "fmt"
"os/exec" "os/exec"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
) )
// PassInsertSecret inserts a secret into a pass store. // PassInsertSecret inserts a secret into a pass store.
func PassInsertSecret(secretValue, secretName, appName, server string) error { func PassInsertSecret(secretValue, secretName, appName, server string) error {
if _, err := exec.LookPath("pass"); err != nil { if _, err := exec.LookPath("pass"); err != nil {
return errors.New("pass command not found on $PATH, is it installed?") return errors.New(i18n.G("pass command not found on $PATH, is it installed?"))
} }
cmd := fmt.Sprintf( cmd := fmt.Sprintf(
@ -19,13 +20,13 @@ func PassInsertSecret(secretValue, secretName, appName, server string) error {
secretValue, server, appName, secretName, secretValue, server, appName, secretName,
) )
log.Debugf("attempting to run %s", cmd) log.Debug(i18n.G("attempting to run %s", cmd))
if err := exec.Command("bash", "-c", cmd).Run(); err != nil { if err := exec.Command("bash", "-c", cmd).Run(); err != nil {
return err return err
} }
log.Infof("%s inserted into pass store", secretName) log.Info(i18n.G("%s inserted into pass store", secretName))
return nil return nil
} }
@ -33,7 +34,7 @@ func PassInsertSecret(secretValue, secretName, appName, server string) error {
// PassRmSecret deletes a secret from a pass store. // PassRmSecret deletes a secret from a pass store.
func PassRmSecret(secretName, appName, server string) error { func PassRmSecret(secretName, appName, server string) error {
if _, err := exec.LookPath("pass"); err != nil { if _, err := exec.LookPath("pass"); err != nil {
return errors.New("pass command not found on $PATH, is it installed?") return errors.New(i18n.G("pass command not found on $PATH, is it installed?"))
} }
cmd := fmt.Sprintf( cmd := fmt.Sprintf(
@ -41,13 +42,13 @@ func PassRmSecret(secretName, appName, server string) error {
server, appName, secretName, server, appName, secretName,
) )
log.Debugf("attempting to run %s", cmd) log.Debug(i18n.G("attempting to run %s", cmd))
if err := exec.Command("bash", "-c", cmd).Run(); err != nil { if err := exec.Command("bash", "-c", cmd).Run(); err != nil {
return err return err
} }
log.Infof("%s removed from pass store", secretName) log.Info(i18n.G("%s removed from pass store", secretName))
return nil return nil
} }

View File

@ -5,6 +5,7 @@ package secret
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"slices" "slices"
"strconv" "strconv"
@ -15,6 +16,7 @@ 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/envfile" "coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
loader "coopcloud.tech/abra/pkg/upstream/stack" loader "coopcloud.tech/abra/pkg/upstream/stack"
@ -37,6 +39,9 @@ type Secret struct {
// variable. For Example: // variable. For Example:
// SECRET_FOO=v1 # charset=default,special // SECRET_FOO=v1 # charset=default,special
Charset string Charset string
// Whether or not to skip generation of the secret or not
// For example: SECRET_FOO=v1 # generate=false
SkipGenerate bool
// RemoteName is the name of the secret on the server. For example: // RemoteName is the name of the secret on the server. For example:
// name: ${STACK_NAME}_test_pass_two_${SECRET_TEST_PASS_TWO_VERSION} // name: ${STACK_NAME}_test_pass_two_${SECRET_TEST_PASS_TWO_VERSION}
// With the following: // With the following:
@ -49,16 +54,12 @@ type Secret struct {
// GeneratePassword generates passwords. // GeneratePassword generates passwords.
func GeneratePassword(length uint, charset string) (string, error) { func GeneratePassword(length uint, charset string) (string, error) {
passwords, err := passgen.GeneratePasswords( passwords, err := passgen.GeneratePasswords(1, length, charset)
1,
length,
charset,
)
if err != nil { if err != nil {
return "", err return "", err
} }
log.Debugf("generated %s", strings.Join(passwords, ", ")) log.Debug(i18n.G("generated %s", strings.Join(passwords, ", ")))
return passwords[0], nil return passwords[0], nil
} }
@ -76,7 +77,7 @@ func GeneratePassphrase() (string, error) {
return "", err return "", err
} }
log.Debugf("generated %s", strings.Join(passphrases, ", ")) log.Debug(i18n.G("generated %s", strings.Join(passphrases, ", ")))
return passphrases[0], nil return passphrases[0], nil
} }
@ -91,6 +92,7 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Set the STACK_NAME to be able to generate the remote name correctly. // Set the STACK_NAME to be able to generate the remote name correctly.
appEnv["STACK_NAME"] = stackName appEnv["STACK_NAME"] = stackName
@ -99,6 +101,7 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Read the compose files without injecting environment variables. // Read the compose files without injecting environment variables.
configWithoutEnv, err := loader.LoadComposefile(opts, map[string]string{}, loader.SkipInterpolation) configWithoutEnv, err := loader.LoadComposefile(opts, map[string]string{}, loader.SkipInterpolation)
if err != nil { if err != nil {
@ -113,18 +116,18 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
} }
if len(enabledSecrets) == 0 { if len(enabledSecrets) == 0 {
log.Debugf("not generating app secrets, none enabled in recipe config") log.Debug(i18n.G("not generating app secrets, none enabled in recipe config"))
return nil, nil return nil, nil
} }
secretValues := map[string]Secret{} secretValues := map[string]Secret{}
for secretId, secretConfig := range composeConfig.Secrets { for secretId, secretConfig := range composeConfig.Secrets {
if string(secretConfig.Name[len(secretConfig.Name)-1]) == "_" { if string(secretConfig.Name[len(secretConfig.Name)-1]) == "_" {
return nil, fmt.Errorf("missing version for secret? (%s)", secretId) return nil, errors.New(i18n.G("missing version for secret? (%s)", secretId))
} }
if !(slices.Contains(enabledSecrets, secretId)) { if !(slices.Contains(enabledSecrets, secretId)) {
log.Warnf("%s not enabled in recipe config, skipping", secretId) log.Warnf(i18n.G("%s not enabled in recipe config, skipping", secretId))
continue continue
} }
@ -133,7 +136,7 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
value := Secret{Version: secretVersion, RemoteName: secretConfig.Name} value := Secret{Version: secretVersion, RemoteName: secretConfig.Name}
if len(value.RemoteName) > config.MAX_DOCKER_SECRET_LENGTH { if len(value.RemoteName) > config.MAX_DOCKER_SECRET_LENGTH {
return nil, fmt.Errorf("secret %s is > %d chars when combined with %s", secretId, config.MAX_DOCKER_SECRET_LENGTH, stackName) return nil, errors.New(i18n.G("secret %s is > %d chars when combined with %s", secretId, config.MAX_DOCKER_SECRET_LENGTH, stackName))
} }
// Check if the length modifier is set for this secret. // Check if the length modifier is set for this secret.
@ -146,6 +149,7 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
if !strings.Contains(configWithoutEnv.Secrets[secretId].Name, envName) { if !strings.Contains(configWithoutEnv.Secrets[secretId].Name, envName) {
continue continue
} }
lengthRaw, ok := modifierValues["length"] lengthRaw, ok := modifierValues["length"]
if ok { if ok {
length, err := strconv.Atoi(lengthRaw) length, err := strconv.Atoi(lengthRaw)
@ -155,6 +159,13 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
value.Length = length value.Length = length
} }
generateRaw, ok := modifierValues["generate"]
if ok {
if generateRaw == "false" {
value.SkipGenerate = true
}
}
value.Charset = resolveCharset(modifierValues["charset"]) value.Charset = resolveCharset(modifierValues["charset"])
break break
} }
@ -192,7 +203,13 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
go func(secretName string, secret Secret) { go func(secretName string, secret Secret) {
defer wg.Done() defer wg.Done()
log.Debugf("attempting to generate and store %s on %s", secret.RemoteName, server) if secret.SkipGenerate {
log.Debug(i18n.G("skipping generation of %s (generate=false)", secretName))
ch <- nil
return
}
log.Debug(i18n.G("attempting to generate and store %s on %s", secret.RemoteName, server))
if secret.Length > 0 { if secret.Length > 0 {
password, err := GeneratePassword(uint(secret.Length), secret.Charset) password, err := GeneratePassword(uint(secret.Length), secret.Charset)
@ -203,7 +220,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
if err := client.StoreSecret(cl, secret.RemoteName, password, server); err != nil { if err := client.StoreSecret(cl, secret.RemoteName, password, server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") { if strings.Contains(err.Error(), "AlreadyExists") {
log.Warnf("%s already exists", secret.RemoteName) log.Warnf(i18n.G("%s already exists", secret.RemoteName))
ch <- nil ch <- nil
} else { } else {
ch <- err ch <- err
@ -223,7 +240,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
if err := client.StoreSecret(cl, secret.RemoteName, passphrase, server); err != nil { if err := client.StoreSecret(cl, secret.RemoteName, passphrase, server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") { if strings.Contains(err.Error(), "AlreadyExists") {
log.Warnf("%s already exists", secret.RemoteName) log.Warnf(i18n.G("%s already exists", secret.RemoteName))
ch <- nil ch <- nil
} else { } else {
ch <- err ch <- err
@ -248,7 +265,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
} }
} }
log.Debugf("generated and stored %v on %s", secrets, server) log.Debug(i18n.G("generated and stored %v on %s", secrets, server))
return secretsGenerated, nil return secretsGenerated, nil
} }

View File

@ -6,22 +6,23 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/i18n"
) )
// CreateServerDir creates a server directory under ~/.abra. // CreateServerDir creates a server directory under ~/.abra.
func CreateServerDir(serverName string) error { func CreateServerDir(serverName string) error {
serverPath := path.Join(config.ABRA_DIR, "servers", serverName) serverPath := path.Join(config.ABRA_DIR, "servers", serverName)
if err := os.Mkdir(serverPath, 0764); err != nil { if err := os.Mkdir(serverPath, 0700); err != nil {
if !os.IsExist(err) { if !os.IsExist(err) {
return err return err
} }
log.Debugf("%s already exists", serverPath) log.Debug(i18n.G("%s already exists", serverPath))
return nil return nil
} }
log.Debugf("successfully created %s", serverPath) log.Debug(i18n.G("successfully created %s", serverPath))
return nil return nil
} }

View File

@ -2,10 +2,12 @@ package service
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
@ -25,7 +27,7 @@ func GetServiceByLabel(c context.Context, cl *client.Client, label string, promp
} }
if len(services) == 0 { if len(services) == 0 {
return swarm.Service{}, fmt.Errorf("no services deployed?") return swarm.Service{}, errors.New(i18n.G("no services deployed?"))
} }
var matchingServices []swarm.Service var matchingServices []swarm.Service
@ -36,7 +38,7 @@ func GetServiceByLabel(c context.Context, cl *client.Client, label string, promp
} }
if len(matchingServices) == 0 { if len(matchingServices) == 0 {
return swarm.Service{}, fmt.Errorf("no services deployed matching label '%s'?", label) return swarm.Service{}, errors.New(i18n.G("no services deployed matching label '%s'?", label))
} }
if len(matchingServices) > 1 { if len(matchingServices) > 1 {
@ -48,15 +50,15 @@ func GetServiceByLabel(c context.Context, cl *client.Client, label string, promp
} }
if !prompt { if !prompt {
err := fmt.Errorf("expected 1 service but found %v: %s", len(matchingServices), strings.Join(servicesRaw, " ")) err := errors.New(i18n.G("expected 1 service but found %v: %s", len(matchingServices), strings.Join(servicesRaw, " ")))
return swarm.Service{}, err return swarm.Service{}, err
} }
log.Warnf("ambiguous service list received, prompting for input") log.Warn(i18n.G("ambiguous service list received, prompting for input"))
var response string var response string
prompt := &survey.Select{ prompt := &survey.Select{
Message: "which service are you looking for?", Message: i18n.G("which service are you looking for?"),
Options: servicesRaw, Options: servicesRaw,
} }
@ -72,7 +74,7 @@ func GetServiceByLabel(c context.Context, cl *client.Client, label string, promp
} }
} }
log.Fatal("failed to match chosen service") log.Fatal(i18n.G("failed to match chosen service"))
} }
return matchingServices[0], nil return matchingServices[0], nil
@ -90,7 +92,7 @@ func GetService(c context.Context, cl *client.Client, filters filters.Args, prom
if len(services) == 0 { if len(services) == 0 {
filter := filters.Get("name")[0] filter := filters.Get("name")[0]
return swarm.Service{}, fmt.Errorf("no services matching the %v filter found?", filter) return swarm.Service{}, errors.New(i18n.G("no services matching the %v filter found?", filter))
} }
if len(services) != 1 { if len(services) != 1 {
@ -98,19 +100,19 @@ func GetService(c context.Context, cl *client.Client, filters filters.Args, prom
for _, service := range services { for _, service := range services {
serviceName := service.Spec.Name serviceName := service.Spec.Name
created := formatter.HumanDuration(service.CreatedAt.Unix()) created := formatter.HumanDuration(service.CreatedAt.Unix())
servicesRaw = append(servicesRaw, fmt.Sprintf("%s (created %v)", serviceName, created)) servicesRaw = append(servicesRaw, i18n.G("%s (created %v)", serviceName, created))
} }
if !prompt { if !prompt {
err := fmt.Errorf("expected 1 service but found %v: %s", len(services), strings.Join(servicesRaw, " ")) err := errors.New(i18n.G("expected 1 service but found %v: %s", len(services), strings.Join(servicesRaw, " ")))
return swarm.Service{}, err return swarm.Service{}, err
} }
log.Warnf("ambiguous service list received, prompting for input") log.Warn(i18n.G("ambiguous service list received, prompting for input"))
var response string var response string
prompt := &survey.Select{ prompt := &survey.Select{
Message: "which service are you looking for?", Message: i18n.G("which service are you looking for?"),
Options: servicesRaw, Options: servicesRaw,
} }
@ -126,7 +128,7 @@ func GetService(c context.Context, cl *client.Client, filters filters.Args, prom
} }
} }
log.Fatal("failed to match chosen service") log.Fatal(i18n.G("failed to match chosen service"))
} }
return services[0], nil return services[0], nil

View File

@ -1,8 +1,10 @@
package ssh package ssh
import ( import (
"fmt" "errors"
"strings" "strings"
"coopcloud.tech/abra/pkg/i18n"
) )
// Fatal is a error output wrapper which aims to make SSH failures easier to // Fatal is a error output wrapper which aims to make SSH failures easier to
@ -11,17 +13,17 @@ func Fatal(hostname string, err error) error {
out := err.Error() out := err.Error()
if strings.Contains(out, "Host key verification failed.") { if strings.Contains(out, "Host key verification failed.") {
return fmt.Errorf("SSH host key verification failed for %s", hostname) return errors.New(i18n.G("SSH host key verification failed for %s", hostname))
} else if strings.Contains(out, "Could not resolve hostname") { } else if strings.Contains(out, "Could not resolve hostname") {
return fmt.Errorf("could not resolve hostname for %s", hostname) return errors.New(i18n.G("could not resolve hostname for %s", hostname))
} else if strings.Contains(out, "Connection timed out") { } else if strings.Contains(out, "Connection timed out") {
return fmt.Errorf("connection timed out for %s", hostname) return errors.New(i18n.G("connection timed out for %s", hostname))
} else if strings.Contains(out, "Permission denied") { } else if strings.Contains(out, "Permission denied") {
return fmt.Errorf("ssh auth: permission denied for %s", hostname) return errors.New(i18n.G("ssh auth: permission denied for %s", hostname))
} else if strings.Contains(out, "Network is unreachable") { } else if strings.Contains(out, "Network is unreachable") {
return fmt.Errorf("unable to connect to %s, please check your SSH config", hostname) return errors.New(i18n.G("unable to connect to %s, please check your SSH config", hostname))
} else if strings.Contains(out, "Is the docker daemon running") { } else if strings.Contains(out, "Is the docker daemon running") {
return fmt.Errorf("docker: is the daemon running / your user has docker permissions?") return errors.New(i18n.G("docker: is the daemon running / your user has docker permissions?"))
} }
return err return err

View File

@ -3,13 +3,13 @@ package ui
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"sort" "sort"
"strings" "strings"
"time" "time"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/logs" "coopcloud.tech/abra/pkg/logs"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/docker/cli/cli/command/service/progress" "github.com/docker/cli/cli/command/service/progress"
@ -79,13 +79,13 @@ type stream struct {
} }
func (s stream) String() string { func (s stream) String() string {
out := fmt.Sprintf("{decoder: %v, ", s.decoder) out := i18n.G("{decoder: %v, ", s.decoder)
out += fmt.Sprintf("err: %v, ", s.Err) out += i18n.G("err: %v, ", s.Err)
out += fmt.Sprintf("id: %s, ", s.id) out += i18n.G("id: %s, ", s.id)
out += fmt.Sprintf("name: %s, ", s.Name) out += i18n.G("name: %s, ", s.Name)
out += fmt.Sprintf("reader: %v, ", s.reader) out += i18n.G("reader: %v, ", s.reader)
out += fmt.Sprintf("writer: %v, ", s.writer) out += i18n.G("writer: %v, ", s.writer)
out += fmt.Sprintf("status: %s, ", s.status) out += i18n.G("status: %s}", s.status)
return out return out
} }
@ -118,7 +118,7 @@ func (s stream) process() tea.Msg {
func (s stream) healthcheck(m Model) tea.Msg { func (s stream) healthcheck(m Model) tea.Msg {
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s", s.Name)) filters.Add("name", i18n.G("^%s", s.Name))
containers, err := m.cl.ContainerList(m.ctx, containerTypes.ListOptions{Filters: filters}) containers, err := m.cl.ContainerList(m.ctx, containerTypes.ListOptions{Filters: filters})
if err != nil { if err != nil {
@ -327,10 +327,10 @@ func (m Model) View() string {
status := stream.status status := stream.status
if strings.Contains(stream.status, "converged") && !stream.rollback { if strings.Contains(stream.status, "converged") && !stream.rollback {
status = "succeeded" status = i18n.G("succeeded")
} }
if strings.Contains(stream.status, "rolled back") { if strings.Contains(stream.status, "rolled back") {
status = "rolled back" status = i18n.G("rolled back")
} }
retries := 0 retries := 0
@ -338,7 +338,7 @@ func (m Model) View() string {
retries = stream.retries retries = stream.retries
} }
output := fmt.Sprintf("%s: %s (retries: %v, healthcheck: %s)", output := i18n.G("%s: %s (retries: %v, healthcheck: %s)",
formatter.BoldStyle.Render(short), formatter.BoldStyle.Render(short),
status, status,
retries, retries,

View File

@ -17,7 +17,6 @@ package commandconn
import ( import (
"bytes" "bytes"
"context" "context"
"fmt"
"io" "io"
"net" "net"
"os" "os"
@ -28,6 +27,7 @@ import (
"time" "time"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/i18n"
"github.com/pkg/errors" "github.com/pkg/errors"
exec "golang.org/x/sys/execabs" exec "golang.org/x/sys/execabs"
) )
@ -46,7 +46,7 @@ func New(ctx context.Context, cmd string, args ...string) (net.Conn, error) {
) )
c.cmd = exec.CommandContext(ctx, cmd, args...) c.cmd = exec.CommandContext(ctx, cmd, args...)
// we assume that args never contains sensitive information // we assume that args never contains sensitive information
log.Debugf("commandconn: starting %s with %v", cmd, args) log.Debug(i18n.G("commandconn: starting %s with %v", cmd, args))
c.cmd.Env = os.Environ() c.cmd.Env = os.Environ()
c.cmd.SysProcAttr = &syscall.SysProcAttr{} c.cmd.SysProcAttr = &syscall.SysProcAttr{}
setPdeathsig(c.cmd) setPdeathsig(c.cmd)
@ -62,7 +62,7 @@ func New(ctx context.Context, cmd string, args ...string) (net.Conn, error) {
c.cmd.Stderr = &stderrWriter{ c.cmd.Stderr = &stderrWriter{
stderrMu: &c.stderrMu, stderrMu: &c.stderrMu,
stderr: &c.stderr, stderr: &c.stderr,
debugPrefix: fmt.Sprintf("commandconn (%s):", cmd), debugPrefix: i18n.G("commandconn (%s):", cmd),
} }
c.localAddr = dummyAddr{network: "dummy", s: "dummy-0"} c.localAddr = dummyAddr{network: "dummy", s: "dummy-0"}
c.remoteAddr = dummyAddr{network: "dummy", s: "dummy-1"} c.remoteAddr = dummyAddr{network: "dummy", s: "dummy-1"}
@ -138,7 +138,7 @@ func (c *commandConn) kill() error {
return nil return nil
} }
} }
return errors.Wrapf(werr, "commandconn: failed to wait") return errors.Wrap(werr, i18n.G("commandconn: failed to wait"))
} }
func (c *commandConn) onEOF(eof error) error { func (c *commandConn) onEOF(eof error) error {
@ -159,7 +159,7 @@ func (c *commandConn) onEOF(eof error) error {
c.stderrMu.Lock() c.stderrMu.Lock()
stderr := c.stderr.String() stderr := c.stderr.String()
c.stderrMu.Unlock() c.stderrMu.Unlock()
return errors.Errorf("command %v did not exit after %v: stderr=%q", c.cmd.Args, eof, stderr) return errors.New(i18n.G("command %v did not exit after %v: stderr=%q", c.cmd.Args, eof, stderr))
} }
} }
c.cmdMutex.Unlock() c.cmdMutex.Unlock()
@ -169,7 +169,7 @@ func (c *commandConn) onEOF(eof error) error {
c.stderrMu.Lock() c.stderrMu.Lock()
stderr := c.stderr.String() stderr := c.stderr.String()
c.stderrMu.Unlock() c.stderrMu.Unlock()
return errors.Errorf("command %v has exited with %v, please make sure the URL is valid, and Docker 18.09 or later is installed on the remote host: stderr=%s", c.cmd.Args, werr, stderr) return errors.New(i18n.G("command %v has exited with %v, please make sure the URL is valid, and Docker 18.09 or later is installed on the remote host: stderr=%s", c.cmd.Args, werr, stderr))
} }
func ignorableCloseError(err error) bool { func ignorableCloseError(err error) bool {
@ -236,7 +236,7 @@ func (c *commandConn) Write(p []byte) (int, error) {
func (c *commandConn) Close() error { func (c *commandConn) Close() error {
var err error var err error
if err = c.CloseRead(); err != nil { if err = c.CloseRead(); err != nil {
log.Warnf("commandConn.Close: CloseRead: %v", err) log.Warnf(i18n.G("commandConn.Close: CloseRead: %v", err))
} }
if err = c.CloseWrite(); err != nil { if err = c.CloseWrite(); err != nil {
// muted because https://github.com/docker/compose/issues/8544 // muted because https://github.com/docker/compose/issues/8544
@ -252,15 +252,15 @@ func (c *commandConn) RemoteAddr() net.Addr {
return c.remoteAddr return c.remoteAddr
} }
func (c *commandConn) SetDeadline(t time.Time) error { func (c *commandConn) SetDeadline(t time.Time) error {
log.Debugf("unimplemented call: SetDeadline(%v)", t) log.Debug(i18n.G("unimplemented call: SetDeadline(%v)", t))
return nil return nil
} }
func (c *commandConn) SetReadDeadline(t time.Time) error { func (c *commandConn) SetReadDeadline(t time.Time) error {
log.Debugf("unimplemented call: SetReadDeadline(%v)", t) log.Debug(i18n.G("unimplemented call: SetReadDeadline(%v)", t))
return nil return nil
} }
func (c *commandConn) SetWriteDeadline(t time.Time) error { func (c *commandConn) SetWriteDeadline(t time.Time) error {
log.Debugf("unimplemented call: SetWriteDeadline(%v)", t) log.Debug(i18n.G("unimplemented call: SetWriteDeadline(%v)", t))
return nil return nil
} }

View File

@ -11,6 +11,7 @@ import (
"github.com/docker/cli/cli/context/docker" "github.com/docker/cli/cli/context/docker"
dCliContextStore "github.com/docker/cli/cli/context/store" dCliContextStore "github.com/docker/cli/cli/context/store"
dClient "github.com/docker/docker/client" dClient "github.com/docker/docker/client"
"coopcloud.tech/abra/pkg/i18n"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -34,7 +35,7 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.Conne
case "ssh": case "ssh":
ctxConnDetails, err := ssh.ParseURL(daemonURL) ctxConnDetails, err := ssh.ParseURL(daemonURL)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "ssh host connection is not valid") return nil, errors.Wrap(err, i18n.G("ssh host connection is not valid"))
} }
return &connhelper.ConnectionHelper{ return &connhelper.ConnectionHelper{

View File

@ -11,6 +11,7 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
apiclient "github.com/docker/docker/client" apiclient "github.com/docker/docker/client"
"coopcloud.tech/abra/pkg/i18n"
) )
// RunExec runs a command on a remote container. io.Writer corresponds to the // RunExec runs a command on a remote container. io.Writer corresponds to the
@ -39,7 +40,7 @@ func RunExec(dockerCli command.Cli, client *apiclient.Client, containerID string
execID := response.ID execID := response.ID
if execID == "" { if execID == "" {
return nil, errors.New("exec ID empty") return nil, errors.New(i18n.G("exec ID empty"))
} }
if execOptions.Detach { if execOptions.Detach {
@ -104,12 +105,12 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, client *apiclie
if execOpts.Tty && dockerCli.In().IsTerminal() { if execOpts.Tty && dockerCli.In().IsTerminal() {
if err := MonitorTtySize(ctx, client, dockerCli, execID, true); err != nil { if err := MonitorTtySize(ctx, client, dockerCli, execID, true); err != nil {
fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err) fmt.Fprintln(dockerCli.Err(), i18n.G("Error monitoring TTY size:"), err)
} }
} }
if err := <-errCh; err != nil { if err := <-errCh; err != nil {
log.Debugf("Error hijack: %s", err) log.Debug(i18n.G("Error hijack: %s", err))
return out, err return out, err
} }

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