Compare commits

..

50 Commits

Author SHA1 Message Date
f5c39d8f1a test: check app list doesn't explode if missing .env
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
See #560
2025-08-12 22:15:51 +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
2fbef41a3a test: clean up properly
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-04-24 14:29:54 +02:00
6fb41e5300 fix: dont parse chaos version
All checks were successful
continuous-integration/drone/push Build is passing
See #547
2025-04-24 11:24:14 +00:00
1432f480c7 fix: -T/--tty disables TTY remote request
All checks were successful
continuous-integration/drone/push Build is passing
See #499
2025-04-24 08:57:53 +00:00
83af39771b test: drop unused tagHash
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-24 10:57:34 +02:00
4d1333202e test: flaky test when no RC is available
All checks were successful
continuous-integration/drone/push Build is passing
Fixes https://build.coopcloud.tech/toolshed/abra/2760/1/5
2025-04-24 10:33:49 +02:00
55c24f070c feat: cancel git clone ops gracefully
All checks were successful
continuous-integration/drone/push Build is passing
See #528
2025-04-22 22:56:10 +02:00
229e8eb9da feat: --ssh/--force for recipe fetch
All checks were successful
continuous-integration/drone/push Build is passing
See toolshed/organising#546
2025-04-22 09:35:36 +00:00
b3ab95750e fix: trim final newline on release note
All checks were successful
continuous-integration/drone/push Build is passing
Follow-up #544
2025-04-22 09:23:28 +02:00
de009921a2 fix: show release notes once
All checks were successful
continuous-integration/drone/push Build is passing
See #543
2025-04-22 05:47:03 +00:00
d081bbaefa feat: auto select single server
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See toolshed/organising#513
2025-04-21 21:06:29 +02:00
515b5466ca docs: add missing arg
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #540
2025-04-21 20:17:39 +02:00
6965799bdc chore: publish 0.10.0-beta
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-04-21 19:11:31 +02:00
f75c9a6259 test: clean up test server correctly
All checks were successful
continuous-integration/drone/push Build is passing
Fixes https://build.coopcloud.tech/toolshed/abra/2723/1/5
2025-04-21 19:03:49 +02:00
a43a092ba7 fix: fetch recipe for "app list -S"
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-19 07:28:15 +00:00
fa084a61d2 fix(lint): Improves error message if a lint rule errors
All checks were successful
continuous-integration/drone/push Build is passing
This was detected while debugging #534
2025-04-16 05:12:19 +00:00
895a7fe7d6 fix: don't overwrite recipeVersion
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Fixes https://build.coopcloud.tech/toolshed/abra/2709/1/5
2025-04-15 10:51:53 +02:00
742a726778 fix: latest commit for new recipe version
All checks were successful
continuous-integration/drone/push Build is passing
See #527
2025-04-14 23:55:19 +02:00
2b9a185aff build: go mod tidy 2025-03-23 11:10:05 +01:00
b7c1e87c0b build: go mod vendor
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-03-23 11:08:17 +01:00
cdfb8a08bb chore: publish 0.10.0-rc2-beta
Some checks failed
continuous-integration/drone/tag Build is failing
continuous-integration/drone/push Build is passing
2025-03-23 11:05:03 +01:00
8943cea13f test: get latest via helper 2025-03-23 11:00:04 +01:00
6d64e0edd3 fix: sshPkg.Fatal has more nuance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #507
2025-03-23 10:27:58 +01:00
47045ca8f1 feat: improved deploy progress reporting
All checks were successful
continuous-integration/drone/push Build is passing
See #478
2025-03-23 09:13:36 +00:00
d0f982456e add charset modifier to secret generation (#521)
All checks were successful
continuous-integration/drone/push Build is passing
since we need special chars in passwords for a recipe we are working on, i have added the option to specify a charset in the same way as the length can be setted.
i did not change anything in the behaviour, so if length is not specified, the charset gets ignored whether it is there or not.

you can specify the following:
`charset=default` - Results in passgen.AlphabetDefault being used
`charset=special` - Results in passgen.AlphabetSpecial being used
`charset=safespecial` - Results in `!@#%^&*_-+=` being used (so it is AlphabetSpecial without the dollar sign)
`charset=default,special` or `charset=special,default` - Results in passgen.AlphabetDefault + passgen.AlphabetSpecial being used
`charset=default,safespecial` or `charset=safespecial,default` - Results in passgen.AlphabetDefault + `!@#%^&*_-+=` being used ((so it is AlphabetSpecial without the dollar sign)

PR for the docs: toolshed/docs.coopcloud.tech#271

Co-authored-by: p4u1 <p4u1@noreply.git.coopcloud.tech>
Reviewed-on: #521
Reviewed-by: p4u1 <p4u1@noreply.git.coopcloud.tech>
Co-authored-by: Apfelwurm <Alexander@volzit.de>
Co-committed-by: Apfelwurm <Alexander@volzit.de>
2025-03-21 10:29:21 +00:00
80ad6c6681 fix(app): Properly detects release notes added after a release when upgrading an app (#523)
All checks were successful
continuous-integration/drone/push Build is passing
Fixes #488

Reviewed-on: #523
Reviewed-by: decentral1se <decentral1se@noreply.git.coopcloud.tech>
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2025-03-21 10:25:46 +00:00
cb63cfe9c2 refactor: chaos redundant, shorter message
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-16 13:54:07 +01:00
d1e49d17ce test: on-demand integration tests
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-16 13:22:50 +01:00
1574aa0631 refactor!: status between service/image
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #487 (comment)
2025-03-16 12:42:09 +01:00
1723025fbf build: go 1.24
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
We were running behind and there were quite some deprecations to update.
This was mostly in the upstream copy/pasta package but seems quite
minimal.
2025-03-16 12:31:45 +01:00
a2b678caf6 test: reset after undeploy for a clean env version
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Follows #510.
See https://build.coopcloud.tech/toolshed/abra/2620/1/5.
2025-03-16 11:49:38 +01:00
0a371ec360 fix: integration tests
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-13 08:31:11 +01:00
e58a716fe1 feat(deploy): Simplifies deploy overview (#508)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
This simplifies the deploy overview, to only show 3 version fields:
- CURRENT DEPLOYMENT
- CURRENT ENV
- NEW DEPLOYMENT

It also fixes a few errors around version detection

Reviewed-on: #508
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2025-03-12 16:13:24 +00:00
1500 changed files with 65151 additions and 236849 deletions

View File

@ -3,12 +3,12 @@ kind: pipeline
name: coopcloud.tech/abra name: coopcloud.tech/abra
steps: steps:
- name: make check - name: make check
image: golang:1.22 image: golang:1.24
commands: commands:
- make check - make check
- name: make test - name: make test
image: golang:1.22 image: golang:1.24
environment: environment:
CATL_URL: https://git.coopcloud.tech/toolshed/recipes-catalogue-json.git CATL_URL: https://git.coopcloud.tech/toolshed/recipes-catalogue-json.git
commands: commands:
@ -60,7 +60,31 @@ steps:
- make check - make check
- make test - make test
- name: integration test - name: on-demand integration test
image: appleboy/drone-ssh
settings:
host:
- int.coopcloud.tech
username: abra
key:
from_secret: abra_int_private_key
port: 22
command_timeout: 60m
script_stop: true
request_pty: true
script:
- |
wget https://git.coopcloud.tech/toolshed/abra/raw/branch/main/scripts/tests/run-ci-int -O run-ci-int
chmod +x run-ci-int
sh run-ci-int
when:
ref:
- refs/heads/int-*
depends_on:
- make check
- make test
- name: nightly integration test
image: appleboy/drone-ssh image: appleboy/drone-ssh
settings: settings:
host: host:
@ -87,3 +111,8 @@ steps:
volumes: volumes:
- name: deps - name: deps
temp: {} temp: {}
trigger:
action:
exclude:
- synchronized

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

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

View File

@ -2,7 +2,7 @@ ABRA := ./cmd/abra
KADABRA := ./cmd/kadabra KADABRA := ./cmd/kadabra
COMMIT := $(shell git rev-list -1 HEAD) COMMIT := $(shell git rev-list -1 HEAD)
GOPATH := $(shell go env GOPATH) GOPATH := $(shell go env GOPATH)
GOVERSION := 1.22 GOVERSION := 1.24
LDFLAGS := "-X 'main.Commit=$(COMMIT)'" LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
DIST_LDFLAGS := $(LDFLAGS)" -s -w" DIST_LDFLAGS := $(LDFLAGS)" -s -w"
GCFLAGS := "all=-l -B" GCFLAGS := "all=-l -B"

View File

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

View File

@ -183,7 +183,7 @@ does not).`,
if err := internal.RunCmdRemote( if err := internal.RunCmdRemote(
cl, cl,
app, app,
requestTTY, disableTTY,
app.Recipe.AbraShPath, app.Recipe.AbraShPath,
targetServiceName, cmdName, parsedCmdArgs, remoteUser); err != nil { targetServiceName, cmdName, parsedCmdArgs, remoteUser); err != nil {
log.Fatal(err) log.Fatal(err)
@ -238,7 +238,7 @@ func parseCmdArgs(args []string, isLocal bool) (bool, string) {
var ( var (
local bool local bool
remoteUser string remoteUser string
requestTTY bool disableTTY bool
) )
func init() { func init() {
@ -259,11 +259,11 @@ func init() {
) )
AppCmdCommand.Flags().BoolVarP( AppCmdCommand.Flags().BoolVarP(
&requestTTY, &disableTTY,
"tty", "tty",
"T", "T",
false, false,
"request remote TTY", "disable remote TTY",
) )
AppCmdCommand.Flags().BoolVarP( AppCmdCommand.Flags().BoolVarP(

View File

@ -18,7 +18,7 @@ import (
"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"
"github.com/docker/docker/api/types" containertypes "github.com/docker/docker/api/types/container"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
@ -33,7 +33,7 @@ var AppCpCommand = &cobra.Command{
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,
@ -134,7 +134,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
if err != nil { if err != nil {
return err return err
} }
if _, err := container.RunExec(dcli, cl, containerID, &types.ExecConfig{ if _, err := container.RunExec(dcli, cl, containerID, &containertypes.ExecOptions{
AttachStderr: true, AttachStderr: true,
AttachStdin: true, AttachStdin: true,
AttachStdout: true, AttachStdout: true,
@ -162,7 +162,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
} }
log.Debugf("copy %s from local to %s on container", srcPath, dstPath) log.Debugf("copy %s from local to %s on container", srcPath, dstPath)
copyOpts := types.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
} }
@ -173,7 +173,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
if err != nil { if err != nil {
return err return err
} }
if _, err := container.RunExec(dcli, cl, containerID, &types.ExecConfig{ if _, err := container.RunExec(dcli, cl, containerID, &containertypes.ExecOptions{
AttachStderr: true, AttachStderr: true,
AttachStdin: true, AttachStdin: true,
AttachStdout: true, AttachStdout: true,

View File

@ -3,6 +3,7 @@ package app
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/app"
@ -64,10 +65,8 @@ checkout as-is. Recipe commit hashes are also supported as values for
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
var ( var (
deployWarnMessages []string deployWarnMessages []string
toDeployVersion string toDeployVersion string
isChaosCommit bool
toDeployChaosVersion = config.CHAOS_DEFAULT
) )
app := internal.ValidateApp(args) app := internal.ValidateApp(args)
@ -80,10 +79,6 @@ checkout as-is. Recipe commit hashes are also supported as values for
log.Fatal(err) log.Fatal(err)
} }
if err := lint.LintForErrors(app.Recipe); 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)
@ -100,27 +95,23 @@ checkout as-is. Recipe commit hashes are also supported as values for
log.Fatalf("%s is already deployed", app.Name) log.Fatalf("%s is already deployed", app.Name)
} }
toDeployVersion, toDeployChaosVersion, err = getDeployVersion(args, deployMeta, app) toDeployVersion, err = getDeployVersion(args, deployMeta, app)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(fmt.Errorf("get deploy version: %s", err))
} }
if !internal.Chaos { if !internal.Chaos {
isChaosCommit, err = app.Recipe.EnsureVersion(toDeployVersion) _, err = app.Recipe.EnsureVersion(toDeployVersion)
if err != nil { if err != nil {
log.Fatal(err) log.Fatalf("ensure recipe: %s", err)
} }
}
if isChaosCommit { if err := lint.LintForErrors(app.Recipe); err != nil {
log.Debugf("assuming chaos commit: %s", toDeployVersion) if internal.Chaos {
log.Warn(err)
internal.Chaos = true } else {
toDeployChaosVersion = toDeployVersion log.Fatal(err)
toDeployVersion, err = app.Recipe.GetVersionLabelLocal()
if err != nil {
log.Fatal(err)
}
} }
} }
@ -154,18 +145,14 @@ checkout as-is. Recipe commit hashes are also supported as values for
log.Fatal(err) log.Fatal(err)
} }
toDeployChaosVersionLabel := toDeployChaosVersion
if app.Recipe.Dirty {
toDeployChaosVersionLabel = formatter.AddDirtyMarker(toDeployChaosVersionLabel)
}
appPkg.ExposeAllEnv(stackName, compose, app.Env) appPkg.ExposeAllEnv(stackName, compose, app.Env)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name) appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos) appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
if internal.Chaos { if internal.Chaos {
appPkg.SetChaosVersionLabel(compose, stackName, toDeployChaosVersionLabel) appPkg.SetChaosVersionLabel(compose, stackName, toDeployVersion)
} }
appPkg.SetUpdateLabel(compose, stackName, app.Env) appPkg.SetUpdateLabel(compose, stackName, app.Env)
appPkg.SetVersionLabel(compose, stackName, toDeployVersion)
envVars, err := appPkg.CheckEnv(app) envVars, err := appPkg.CheckEnv(app)
if err != nil { if err != nil {
@ -197,19 +184,12 @@ checkout as-is. Recipe commit hashes are also supported as values for
deployedVersion = deployMeta.Version deployedVersion = deployMeta.Version
} }
toWriteVersion := toDeployVersion
if internal.Chaos || isChaosCommit {
toWriteVersion = toDeployChaosVersion
}
if err := internal.DeployOverview( if err := internal.DeployOverview(
app, app,
deployWarnMessages,
deployedVersion, deployedVersion,
deployMeta.ChaosVersion,
toDeployVersion, toDeployVersion,
toDeployChaosVersion, "",
toWriteVersion, deployWarnMessages,
); err != nil { ); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -221,6 +201,16 @@ checkout as-is. Recipe commit hashes are also supported as values for
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout) log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
if err != nil {
log.Fatal(err)
}
f, err := app.Filters(true, false, serviceNames...)
if err != nil {
log.Fatal(err)
}
if err := stack.RunDeploy( if err := stack.RunDeploy(
cl, cl,
deployOpts, deployOpts,
@ -228,6 +218,7 @@ checkout as-is. Recipe commit hashes are also supported as values for
app.Name, app.Name,
app.Server, app.Server,
internal.DontWaitConverge, internal.DontWaitConverge,
f,
); err != nil { ); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -240,43 +231,28 @@ checkout as-is. Recipe commit hashes are also supported as values for
} }
} }
if err := app.WriteRecipeVersion(toWriteVersion, false); err != nil { if err := app.WriteRecipeVersion(toDeployVersion, false); err != nil {
log.Fatalf("writing recipe version failed: %s", err) log.Fatalf("writing recipe version failed: %s", err)
} }
}, },
} }
func getChaosVersion(app app.App, toDeployVersion, toDeployChaosVersion *string) error { func getLatestVersionOrCommit(app app.App) (string, error) {
var err error
*toDeployChaosVersion, err = app.Recipe.ChaosVersion()
if err != nil {
return err
}
*toDeployVersion, err = app.Recipe.GetVersionLabelLocal()
if err != nil {
return err
}
return nil
}
func getLatestVersionOrCommit(app app.App) (string, string, error) {
versions, err := app.Recipe.Tags() versions, err := app.Recipe.Tags()
if err != nil { if err != nil {
return "", "", err return "", err
} }
if len(versions) > 0 && !internal.Chaos { if len(versions) > 0 && !internal.Chaos {
return versions[len(versions)-1], "", nil return versions[len(versions)-1], nil
} }
head, err := app.Recipe.Head() head, err := app.Recipe.Head()
if err != nil { if err != nil {
return "", "", err return "", err
} }
return "", formatter.SmallSHA(head.String()), nil return formatter.SmallSHA(head.String()), nil
} }
// validateArgsAndFlags ensures compatible args/flags. // validateArgsAndFlags ensures compatible args/flags.
@ -303,48 +279,44 @@ func validateSecrets(cl *dockerClient.Client, app app.App) error {
return nil return nil
} }
func getDeployVersion(cliArgs []string, deployMeta stack.DeployMeta, app app.App) (string, string, error) { func getDeployVersion(cliArgs []string, deployMeta stack.DeployMeta, app app.App) (string, error) {
// Chaos mode overrides everything // Chaos mode overrides everything
if internal.Chaos { if internal.Chaos {
v, err := app.Recipe.ChaosVersion() v, err := app.Recipe.ChaosVersion()
if err != nil { if err != nil {
return "", "", err return "", err
} }
cv, err := app.Recipe.GetVersionLabelLocal() log.Debugf("version: taking chaos version: %s", v)
if err != nil { return v, nil
return "", "", err
}
log.Debugf("version: taking chaos version: %s, %s", v, cv)
return v, cv, 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.Debugf("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") {
return "", fmt.Errorf("version: can not redeploy chaos version %s", app.Recipe.EnvVersionRaw)
}
log.Debugf("version: taking version from .env file: %s", app.Recipe.EnvVersion) log.Debugf("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.Debugf("version: taking deployed version: %s", deployMeta.Version)
return deployMeta.Version, "", nil return deployMeta.Version, nil
} }
v, vc, err := getLatestVersionOrCommit(app) v, err := getLatestVersionOrCommit(app)
log.Debugf("version: taking new recipe versio: %s, %s", v, vc) log.Debugf("version: taking new recipe version: %s", v)
if err != nil { if err != nil {
log.Fatal(err) return "", err
} }
if v == "" { return v, nil
return vc, vc, nil
}
return v, vc, nil
} }
func init() { func init() {

View File

@ -142,10 +142,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" { if version != "unknown" && chaos == "false" {
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatalf("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.Fatal(err) log.Fatalf("unable to retrieve tags for %s: %s", app.Name, err)
} }
parsedVersion, err := tagcmp.Parse(version) parsedVersion, err := tagcmp.Parse(version)

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/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/logs"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -58,23 +59,27 @@ var AppLogsCommand = &cobra.Command{
log.Fatalf("%s is not deployed?", app.Name) log.Fatalf("%s is not deployed?", app.Name)
} }
// TODO var serviceNames []string
// var serviceNames []string if len(args) == 2 {
// if len(args) == 2 { serviceNames = []string{args[1]}
// serviceNames = []string{args[1]} }
// }
// b, err := logs.TailLogs( f, err := app.Filters(true, false, serviceNames...)
// cl, if err != nil {
// app, log.Fatal(err)
// serviceNames, }
// stdErr,
// sinceLogs, opts := logs.TailOpts{
// true, AppName: app.Name,
// ) Services: serviceNames,
// if err != nil { StdErr: stdErr,
// log.Fatal(err) Since: sinceLogs,
// } Filters: f,
}
if err := logs.TailLogs(cl, opts); err != nil {
log.Fatal(err)
}
}, },
} }

View File

@ -109,6 +109,15 @@ var AppNewCommand = &cobra.Command{
if err := recipe.EnsureLatest(); err != nil { if err := recipe.EnsureLatest(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if recipeVersion == "" {
head, err := recipe.Head()
if err != nil {
log.Fatalf("failed to retrieve latest commit for %s: %s", recipe.Name, err)
}
recipeVersion = formatter.SmallSHA(head.String())
}
} }
} }
@ -185,7 +194,7 @@ var AppNewCommand = &cobra.Command{
newAppServer = "local" newAppServer = "local"
} }
log.Infof("%s created successfully (version: %s, chaos: %s)", appDomain, recipeVersion, chaosVersion) log.Infof("%s created (version: %s)", appDomain, recipeVersion)
if len(appSecrets) > 0 { if len(appSecrets) > 0 {
rows := [][]string{} rows := [][]string{}
@ -209,16 +218,7 @@ var AppNewCommand = &cobra.Command{
log.Fatal(err) log.Fatal(err)
} }
if err := app.Recipe.IsDirty(); err != nil { if err := app.WriteRecipeVersion(recipeVersion, false); err != nil {
log.Fatal(err)
}
toWriteVersion := recipeVersion
if internal.Chaos || app.Recipe.Dirty {
toWriteVersion = chaosVersion
}
if err := app.WriteRecipeVersion(toWriteVersion, false); err != nil {
log.Fatalf("writing recipe version failed: %s", err) log.Fatalf("writing recipe version failed: %s", err)
} }
}, },
@ -302,6 +302,12 @@ func ensureServerFlag() error {
return err return err
} }
if len(servers) == 1 {
newAppServer = servers[0]
log.Infof("single server detected, choosing %s automatically", newAppServer)
return nil
}
if newAppServer == "" && !internal.NoInput { if newAppServer == "" && !internal.NoInput {
prompt := &survey.Select{ prompt := &survey.Select{
Message: "Select app server:", Message: "Select app server:",

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"sort"
"strings" "strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
@ -91,9 +92,14 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
return return
} }
services := compose.Services
sort.Slice(services, func(i, j int) bool {
return services[i].Name < services[j].Name
})
var rows [][]string var rows [][]string
allContainerStats := make(map[string]map[string]string) allContainerStats := make(map[string]map[string]string)
for _, service := range compose.Services { for _, service := range services {
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name)) filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name))
@ -143,10 +149,10 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
row := []string{ row := []string{
containerStats["service"], containerStats["service"],
containerStats["status"],
containerStats["image"], containerStats["image"],
dVersion, dVersion,
cVersion, cVersion,
containerStats["status"],
} }
rows = append(rows, row) rows = append(rows, row)
@ -170,10 +176,10 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
headers := []string{ headers := []string{
"SERVICE", "SERVICE",
"STATUS",
"IMAGE", "IMAGE",
"VERSION", "VERSION",
"CHAOS", "CHAOS",
"STATUS",
} }
table. table.

View File

@ -12,6 +12,7 @@ import (
"coopcloud.tech/abra/pkg/ui" "coopcloud.tech/abra/pkg/ui"
upstream "coopcloud.tech/abra/pkg/upstream/service" upstream "coopcloud.tech/abra/pkg/upstream/service"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -94,22 +95,36 @@ Pass "--all-services/-a" to restart all services.`,
for _, serviceName := range serviceNames { for _, serviceName := range serviceNames {
stackServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName) stackServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
service, _, err := cl.ServiceInspectWithRaw(
context.Background(),
stackServiceName,
types.ServiceInspectOptions{},
)
if err != nil {
log.Fatal(err)
}
log.Debugf("attempting to scale %s to 0", stackServiceName) log.Debugf("attempting to scale %s to 0", stackServiceName)
// TODO
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)
} }
// TODO f, err := app.Filters(true, false, serviceName)
if err := stack.WaitOnServices( if err != nil {
cmd.Context(), log.Fatal(err)
cl, }
// TODO
[]ui.ServiceMeta{{Name: stackServiceName, ID: "???"}}, waitOpts := stack.WaitOpts{
app.Name, Services: []ui.ServiceMeta{{Name: stackServiceName, ID: service.ID}},
app.Server, AppName: app.Name,
); err != nil { ServerName: app.Server,
Filters: f,
NoLog: true,
Quiet: true,
}
if err := stack.WaitOnServices(cmd.Context(), cl, waitOpts); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -120,14 +135,7 @@ Pass "--all-services/-a" to restart all services.`,
log.Fatal(err) log.Fatal(err)
} }
if err := stack.WaitOnServices( if err := stack.WaitOnServices(cmd.Context(), cl, waitOpts); err != nil {
cmd.Context(),
cl,
// TODO
[]ui.ServiceMeta{{Name: stackServiceName, ID: "???"}},
app.Name,
app.Server,
); err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -183,24 +183,34 @@ beforehand. See "abra app backup" for more.`,
} }
appPkg.SetUpdateLabel(compose, stackName, app.Env) appPkg.SetUpdateLabel(compose, stackName, app.Env)
chaosVersion := config.CHAOS_DEFAULT
if deployMeta.IsChaos {
chaosVersion = deployMeta.ChaosVersion
}
// NOTE(d1): no release notes implemeneted for rolling back // NOTE(d1): no release notes implemeneted for rolling back
if err := internal.NewVersionOverview( if err := internal.DeployOverview(
app, app,
downgradeWarnMessages,
"rollback",
deployMeta.Version, deployMeta.Version,
chaosVersion,
chosenDowngrade, chosenDowngrade,
"", "",
downgradeWarnMessages,
); err != nil { ); err != nil {
log.Fatal(err) log.Fatal(err)
} }
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
if err != nil {
log.Fatal(err)
}
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
if err != nil {
log.Fatal(err)
}
f, err := app.Filters(true, false, serviceNames...)
if err != nil {
log.Fatal(err)
}
if err := stack.RunDeploy( if err := stack.RunDeploy(
cl, cl,
deployOpts, deployOpts,
@ -208,6 +218,7 @@ beforehand. See "abra app backup" for more.`,
stackName, stackName,
app.Server, app.Server,
internal.DontWaitConverge, internal.DontWaitConverge,
f,
); err != nil { ); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -256,7 +267,7 @@ 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("'%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name) return fmt.Errorf("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)

View File

@ -11,7 +11,7 @@ import (
"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"
"github.com/docker/docker/api/types" containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -64,7 +64,7 @@ var AppRunCommand = &cobra.Command{
} }
userCmd := args[2:] userCmd := args[2:]
execCreateOpts := types.ExecConfig{ execCreateOpts := containertypes.ExecOptions{
AttachStderr: true, AttachStderr: true,
AttachStdin: true, AttachStdin: true,
AttachStdout: true, AttachStdout: true,

View File

@ -233,6 +233,7 @@ var AppSecretRmCommand = &cobra.Command{
Use: "remove <domain> [[secret] | --all] [flags]", Use: "remove <domain> [[secret] | --all] [flags]",
Aliases: []string{"rm"}, Aliases: []string{"rm"},
Short: "Remove a secret", Short: "Remove a secret",
Example: " 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,

View File

@ -54,25 +54,34 @@ Passing "--prune/-p" does not remove those volumes.`,
log.Fatalf("%s is not deployed?", app.Name) log.Fatalf("%s is not deployed?", app.Name)
} }
chaosVersion := config.CHAOS_DEFAULT if err := internal.DeployOverview(
if deployMeta.IsChaos {
chaosVersion = deployMeta.ChaosVersion
}
toWriteVersion := deployMeta.Version
if deployMeta.IsChaos {
toWriteVersion = chaosVersion
}
if err := internal.UndeployOverview(
app, app,
deployMeta.Version, deployMeta.Version,
chaosVersion, config.NO_DOMAIN_DEFAULT,
toWriteVersion, "",
nil,
); err != nil { ); err != nil {
log.Fatal(err) log.Fatal(err)
} }
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil {
log.Fatal(err)
}
opts := stack.Deploy{Composefiles: composeFiles, Namespace: stackName}
compose, err := appPkg.GetAppComposeConfig(app.Name, opts, app.Env)
if err != nil {
log.Fatal(err)
}
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
if err != nil {
log.Fatal(err)
}
log.Info("initialising undeploy")
rmOpts := stack.Remove{ rmOpts := stack.Remove{
Namespaces: []string{stackName}, Namespaces: []string{stackName},
Detach: false, Detach: false,
@ -87,7 +96,9 @@ Passing "--prune/-p" does not remove those volumes.`,
} }
} }
if err := app.WriteRecipeVersion(toWriteVersion, false); err != nil { log.Info("undeploy succeeded 🟢")
if err := app.WriteRecipeVersion(deployMeta.Version, false); err != nil {
log.Fatalf("writing recipe version failed: %s", err) log.Fatalf("writing recipe version failed: %s", err)
} }
}, },

View File

@ -3,6 +3,7 @@ package app
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/app"
@ -14,6 +15,7 @@ import (
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
@ -43,7 +45,8 @@ beforehand. See "abra app backup" for more.`,
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
args []string, args []string,
toComplete string) ([]string, cobra.ShellCompDirective) { toComplete string,
) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l { switch l := len(args); l {
case 0: case 0:
return autocomplete.AppNameComplete() return autocomplete.AppNameComplete()
@ -68,7 +71,13 @@ beforehand. See "abra app backup" for more.`,
app := internal.ValidateApp(args) app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { if err := app.Recipe.Ensure(recipe.EnsureContext{
Chaos: internal.Chaos,
Offline: internal.Offline,
// Ignore the env version for now, to make sure we are at the latest commit.
// This enables us to get release notes, that were added after a release.
IgnoreEnvVersion: true,
}); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -141,10 +150,9 @@ beforehand. See "abra app backup" for more.`,
log.Debugf("choosing %s as version to upgrade", chosenUpgrade) log.Debugf("choosing %s as version to upgrade", chosenUpgrade)
// NOTE(d1): if release notes written after git tag published, read them // Get the release notes before checking out the new version in the
// before we check out the tag and then they'll appear to be missing. this // recipe. This enables us to get release notes, that were added after
// covers when we obviously will forget to write release notes before // a release.
// publishing
if err := getReleaseNotes(app, versions, chosenUpgrade, deployMeta, &upgradeReleaseNotes); err != nil { if err := getReleaseNotes(app, versions, chosenUpgrade, deployMeta, &upgradeReleaseNotes); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -206,23 +214,19 @@ beforehand. See "abra app backup" for more.`,
return return
} }
chaosVersion := config.CHAOS_DEFAULT if upgradeReleaseNotes == "" {
if deployMeta.IsChaos { upgradeWarnMessages = append(
chaosVersion = deployMeta.ChaosVersion upgradeWarnMessages,
fmt.Sprintf("no release notes available for %s", chosenUpgrade),
if deployMeta.ChaosVersion == "" { )
chaosVersion = config.UNKNOWN_DEFAULT
}
} }
if err := internal.NewVersionOverview( if err := internal.DeployOverview(
app, app,
upgradeWarnMessages,
"upgrade",
deployMeta.Version, deployMeta.Version,
chaosVersion,
chosenUpgrade, chosenUpgrade,
upgradeReleaseNotes, upgradeReleaseNotes,
upgradeWarnMessages,
); err != nil { ); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -234,6 +238,16 @@ beforehand. See "abra app backup" for more.`,
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout) log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
if err != nil {
log.Fatal(err)
}
f, err := app.Filters(true, false, serviceNames...)
if err != nil {
log.Fatal(err)
}
if err := stack.RunDeploy( if err := stack.RunDeploy(
cl, cl,
deployOpts, deployOpts,
@ -241,6 +255,7 @@ beforehand. See "abra app backup" for more.`,
stackName, stackName,
app.Server, app.Server,
internal.DontWaitConverge, internal.DontWaitConverge,
f,
); err != nil { ); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -321,6 +336,11 @@ func getReleaseNotes(
} }
if note != "" { if note != "" {
// NOTE(d1): trim any final newline on the end of the note itself before
// we manually handle newlines (for multiple release notes and
// ensuring space between the warning messages)
note = strings.TrimSuffix(note, "\n")
*upgradeReleaseNotes += fmt.Sprintf("%s\n", note) *upgradeReleaseNotes += fmt.Sprintf("%s\n", note)
} }
} }
@ -372,7 +392,7 @@ func validateUpgradeVersionArg(
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version) parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil { if err != nil {
return err return fmt.Errorf("'%s' is not a known version", deployMeta.Version)
} }
if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) && if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) &&
@ -404,9 +424,7 @@ func ensureDeployed(cl *dockerClient.Client, app app.App) (stack.DeployMeta, err
return deployMeta, nil return deployMeta, nil
} }
var ( var showReleaseNotes bool
showReleaseNotes bool
)
func init() { func init() {
AppUpgradeCommand.Flags().BoolVarP( AppUpgradeCommand.Flags().BoolVarP(

View File

@ -12,6 +12,7 @@ import (
"coopcloud.tech/abra/pkg/upstream/container" "coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
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"
) )
@ -47,7 +48,7 @@ func RunBackupCmdRemote(
backupCmd string, backupCmd string,
containerID string, containerID string,
execEnv []string) (io.Writer, error) { execEnv []string) (io.Writer, error) {
execBackupListOpts := types.ExecConfig{ execBackupListOpts := containertypes.ExecOptions{
AttachStderr: true, AttachStderr: true,
AttachStdin: true, AttachStdin: true,
AttachStdout: true, AttachStdout: true,

View File

@ -14,7 +14,7 @@ import (
"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"
"github.com/docker/docker/api/types" containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
@ -24,7 +24,7 @@ import (
func RunCmdRemote( func RunCmdRemote(
cl *dockerClient.Client, cl *dockerClient.Client,
app appPkg.App, app appPkg.App,
requestTTY bool, disableTTY bool,
abraSh, serviceName, cmdName, cmdArgs, remoteUser string) error { abraSh, serviceName, cmdName, cmdArgs, remoteUser string) error {
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName)) filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
@ -42,7 +42,7 @@ func RunCmdRemote(
return err return err
} }
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false} copyOpts := containertypes.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(context.Background(), targetContainer.ID, "/tmp", content, copyOpts); err != nil { if err := cl.CopyToContainer(context.Background(), targetContainer.ID, "/tmp", content, copyOpts); err != nil {
return err return err
} }
@ -55,7 +55,7 @@ func RunCmdRemote(
shell := "/bin/bash" shell := "/bin/bash"
findShell := []string{"test", "-e", shell} findShell := []string{"test", "-e", shell}
execCreateOpts := types.ExecConfig{ execCreateOpts := containertypes.ExecOptions{
AttachStderr: true, AttachStderr: true,
AttachStdin: true, AttachStdin: true,
AttachStdout: true, AttachStdout: true,
@ -84,8 +84,10 @@ func RunCmdRemote(
} }
execCreateOpts.Cmd = cmd execCreateOpts.Cmd = cmd
execCreateOpts.Tty = requestTTY
if !requestTTY { execCreateOpts.Tty = true
if disableTTY {
execCreateOpts.Tty = false
log.Debugf("not requesting a remote TTY") log.Debugf("not requesting a remote TTY")
} }

View File

@ -10,7 +10,6 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
@ -38,96 +37,6 @@ func horizontal(left, mid, right string) string {
return lipgloss.JoinHorizontal(lipgloss.Left, left, mid, right) return lipgloss.JoinHorizontal(lipgloss.Left, left, mid, right)
} }
// NewVersionOverview shows an upgrade or downgrade overview
func NewVersionOverview(
app appPkg.App,
warnMessages []string,
kind,
deployedVersion,
deployedChaosVersion,
toDeployVersion,
releaseNotes string) error {
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = formatComposeFiles(composeFiles)
}
server := app.Server
if app.Server == "default" {
server = "local"
}
domain := app.Domain
if domain == "" {
domain = config.NO_DOMAIN_DEFAULT
}
upperKind := strings.ToUpper(kind)
envVersion, err := recipe.GetEnvVersionRaw(app.Recipe.Name)
if err != nil {
return err
}
if envVersion == "" {
envVersion = config.NO_VERSION_DEFAULT
}
rows := [][]string{
{"DOMAIN", domain},
{"RECIPE", app.Recipe.Name},
{"SERVER", server},
{"CONFIG", deployConfig},
{"CURRENT DEPLOYMENT", "---"},
{"VERSION", formatter.BoldDirtyDefault(deployedVersion)},
{"CHAOS ", formatter.BoldDirtyDefault(deployedChaosVersion)},
{upperKind, "---"},
{"VERSION", formatter.BoldDirtyDefault(toDeployVersion)},
{fmt.Sprintf("%s.ENV", strings.ToUpper(app.Domain)), "---"},
{"CURRENT VERSION", formatter.BoldDirtyDefault(envVersion)},
{"NEW VERSION", formatter.BoldDirtyDefault(toDeployVersion)},
}
overview := formatter.CreateOverview(
fmt.Sprintf("%s OVERVIEW", upperKind),
rows,
)
fmt.Println(overview)
if releaseNotes != "" && toDeployVersion != "" {
fmt.Print(releaseNotes)
} else {
warnMessages = append(
warnMessages,
fmt.Sprintf("no release notes available for %s", toDeployVersion),
)
}
for _, msg := range warnMessages {
log.Warn(msg)
}
if NoInput {
return nil
}
response := false
prompt := &survey.Confirm{Message: "proceed?"}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
log.Fatal("deployment cancelled")
}
return nil
}
func formatComposeFiles(composeFiles string) string { func formatComposeFiles(composeFiles string) string {
return strings.ReplaceAll(composeFiles, ":", "\n") return strings.ReplaceAll(composeFiles, ":", "\n")
} }
@ -135,12 +44,10 @@ func formatComposeFiles(composeFiles string) string {
// DeployOverview shows a deployment overview // DeployOverview shows a deployment overview
func DeployOverview( func DeployOverview(
app appPkg.App, app appPkg.App,
warnMessages []string,
deployedVersion string, deployedVersion string,
deployedChaosVersion string, toDeployVersion string,
toDeployVersion, releaseNotes string,
toDeployChaosVersion string, warnMessages []string,
toWriteVersion string,
) error { ) error {
deployConfig := "compose.yml" deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
@ -157,21 +64,7 @@ func DeployOverview(
domain = config.NO_DOMAIN_DEFAULT domain = config.NO_DOMAIN_DEFAULT
} }
if app.Recipe.Dirty { envVersion := app.Recipe.EnvVersionRaw
toWriteVersion = formatter.AddDirtyMarker(toWriteVersion)
toDeployChaosVersion = formatter.AddDirtyMarker(toDeployChaosVersion)
}
recipeName, exists := app.Env["RECIPE"]
if !exists {
recipeName = app.Env["TYPE"]
}
envVersion, err := recipe.GetEnvVersionRaw(recipeName)
if err != nil {
return err
}
if envVersion == "" { if envVersion == "" {
envVersion = config.NO_VERSION_DEFAULT envVersion = config.NO_VERSION_DEFAULT
} }
@ -181,24 +74,21 @@ func DeployOverview(
{"RECIPE", app.Recipe.Name}, {"RECIPE", app.Recipe.Name},
{"SERVER", server}, {"SERVER", server},
{"CONFIG", deployConfig}, {"CONFIG", deployConfig},
{"", ""},
{"CURRENT DEPLOYMENT", "---"}, {"CURRENT DEPLOYMENT", formatter.BoldDirtyDefault(deployedVersion)},
{"VERSION", formatter.BoldDirtyDefault(deployedVersion)}, {"ENV VERSION", formatter.BoldDirtyDefault(envVersion)},
{"CHAOS", formatter.BoldDirtyDefault(deployedChaosVersion)}, {"NEW DEPLOYMENT", formatter.BoldDirtyDefault(toDeployVersion)},
{"NEW DEPLOYMENT", "---"},
{"VERSION", formatter.BoldDirtyDefault(toDeployVersion)},
{"CHAOS", formatter.BoldDirtyDefault(toDeployChaosVersion)},
{fmt.Sprintf("%s.ENV", strings.ToUpper(app.Name)), "---"},
{"CURRENT VERSION", formatter.BoldDirtyDefault(envVersion)},
{"NEW VERSION", formatter.BoldDirtyDefault(toWriteVersion)},
} }
overview := formatter.CreateOverview("DEPLOY OVERVIEW", rows) deployType := getDeployType(deployedVersion, toDeployVersion)
overview := formatter.CreateOverview(fmt.Sprintf("%s OVERVIEW", deployType), rows)
fmt.Println(overview) fmt.Println(overview)
if releaseNotes != "" {
fmt.Print(releaseNotes)
}
for _, msg := range warnMessages { for _, msg := range warnMessages {
log.Warn(msg) log.Warn(msg)
} }
@ -220,76 +110,34 @@ func DeployOverview(
return nil return nil
} }
// UndeployOverview shows an undeployment overview func getDeployType(currentVersion, newVersion string) string {
func UndeployOverview( if newVersion == config.NO_DOMAIN_DEFAULT {
app appPkg.App, return "UNDEPLOY"
deployedVersion,
deployedChaosVersion,
toWriteVersion string,
) error {
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = formatComposeFiles(composeFiles)
} }
if strings.Contains(newVersion, "+U") {
server := app.Server return "CHAOS DEPLOY"
if app.Server == "default" {
server = "local"
} }
if strings.Contains(currentVersion, "+U") {
domain := app.Domain return "UNCHAOS DEPLOY"
if domain == "" {
domain = config.NO_DOMAIN_DEFAULT
} }
if currentVersion == newVersion {
recipeName, exists := app.Env["RECIPE"] return "REDEPLOY"
if !exists {
recipeName = app.Env["TYPE"]
} }
if currentVersion == config.NO_VERSION_DEFAULT {
envVersion, err := recipe.GetEnvVersionRaw(recipeName) return "NEW DEPLOY"
}
currentParsed, err := tagcmp.Parse(currentVersion)
if err != nil { if err != nil {
return err return "DEPLOY"
} }
newParsed, err := tagcmp.Parse(newVersion)
if envVersion == "" { if err != nil {
envVersion = config.NO_VERSION_DEFAULT return "DEPLOY"
} }
if currentParsed.IsLessThan(newParsed) {
rows := [][]string{ return "UPGRADE"
{"DOMAIN", domain},
{"RECIPE", app.Recipe.Name},
{"SERVER", server},
{"CONFIG", deployConfig},
{"CURRENT DEPLOYMENT", "---"},
{"VERSION", formatter.BoldDirtyDefault(deployedVersion)},
{"CHAOS", formatter.BoldDirtyDefault(deployedChaosVersion)},
{fmt.Sprintf("%s.ENV", strings.ToUpper(app.Name)), "---"},
{"CURRENT VERSION", formatter.BoldDirtyDefault(envVersion)},
{"NEW VERSION", formatter.BoldDirtyDefault(toWriteVersion)},
} }
return "DOWNGRADE"
overview := formatter.CreateOverview("UNDEPLOY OVERVIEW", rows)
fmt.Println(overview)
if NoInput {
return nil
}
response := false
prompt := &survey.Confirm{Message: "proceed?"}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
log.Fatal("undeploy cancelled")
}
return nil
} }
// PostCmds parses a string of commands and executes them inside of the respective services // PostCmds parses a string of commands and executes them inside of the respective services

View File

@ -32,12 +32,12 @@ func ValidateRecipe(args []string, cmdName string) recipe.Recipe {
localRecipes, err := recipe.GetRecipesLocal() localRecipes, err := recipe.GetRecipesLocal()
if err != nil { if err != nil {
log.Fatal(err) log.Debugf("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 }
} }
} }

View File

@ -1,11 +1,15 @@
package recipe package recipe
import ( import (
"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/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/go-git/go-git/v5"
gitCfg "github.com/go-git/go-git/v5/config"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -13,7 +17,16 @@ var RecipeFetchCommand = &cobra.Command{
Use: "fetch [recipe | --all] [flags]", Use: "fetch [recipe | --all] [flags]",
Aliases: []string{"f"}, Aliases: []string{"f"},
Short: "Clone recipe(s) locally", Short: "Clone recipe(s) locally",
Long: `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
abra recipe fetch gitea
# fetch from remote recipe
abra recipe fetch git.foo.org/recipes/myrecipe
# fetch with ssh remote for hacking
abra recipe fetch gitea --ssh`,
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
args []string, args []string,
@ -36,10 +49,39 @@ var RecipeFetchCommand = &cobra.Command{
ensureCtx := internal.GetEnsureContext() ensureCtx := internal.GetEnsureContext()
if recipeName != "" { if recipeName != "" {
r := internal.ValidateRecipe(args, cmd.Name()) r := recipe.Get(recipeName)
if err := r.Ensure(ensureCtx); err != nil { if _, err := os.Stat(r.Dir); !os.IsNotExist(err) {
log.Fatal(err) if !force {
log.Warnf("%s is already fetched", r.Name)
return
}
} }
r = internal.ValidateRecipe(args, cmd.Name())
if sshRemote {
if r.SSHURL == "" {
log.Warnf("unable to discover SSH remote for %s", r.Name)
return
}
repo, err := git.PlainOpen(r.Dir)
if err != nil {
log.Fatalf("unable to open %s: %s", r.Dir, err)
}
if err = repo.DeleteRemote("origin"); err != nil {
log.Fatalf("unable to remove default remote in %s: %s", r.Dir, err)
}
if _, err := repo.CreateRemote(&gitCfg.RemoteConfig{
Name: "origin",
URLs: []string{r.SSHURL},
}); err != nil {
log.Fatalf("unable to set SSH remote in %s: %s", r.Dir, err)
}
}
return return
} }
@ -61,6 +103,8 @@ var RecipeFetchCommand = &cobra.Command{
var ( var (
fetchAllRecipes bool fetchAllRecipes bool
sshRemote bool
force bool
) )
func init() { func init() {
@ -71,4 +115,20 @@ func init() {
false, false,
"fetch all recipes", "fetch all recipes",
) )
RecipeFetchCommand.Flags().BoolVarP(
&sshRemote,
"ssh",
"s",
false,
"automatically set ssh remote",
)
RecipeFetchCommand.Flags().BoolVarP(
&force,
"force",
"f",
false,
"force re-fetch",
)
} }

View File

@ -103,8 +103,7 @@ developer machine. The domain is then set to "default".`,
if _, err := client.New(name, timeout); err != nil { if _, err := client.New(name, timeout); err != nil {
cleanUp(name) cleanUp(name)
log.Debugf("ssh %s error: %s", name, sshPkg.Fatal(name, err)) log.Fatalf("ssh %s error: %s", name, sshPkg.Fatal(name, err))
log.Fatalf("can't ssh to %s, make sure \"ssh %s\" works", name, name)
} }
if created { if created {

View File

@ -441,6 +441,16 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion stri
log.Infof("upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion) log.Infof("upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion)
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
if err != nil {
return err
}
f, err := app.Filters(true, false, serviceNames...)
if err != nil {
return err
}
err = stack.RunDeploy( err = stack.RunDeploy(
cl, cl,
deployOpts, deployOpts,
@ -448,6 +458,7 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion stri
stackName, stackName,
app.Server, app.Server,
true, true,
f,
) )
return err return err

143
go.mod
View File

@ -1,54 +1,59 @@
module coopcloud.tech/abra module coopcloud.tech/abra
go 1.22.7 go 1.23.5
toolchain go1.23.1 toolchain go1.24.1
require ( require (
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb coopcloud.tech/tagcmp v0.0.0-20250427094623-9ea3bbbde8e5
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.2.4 github.com/charmbracelet/bubbletea v1.3.6
github.com/charmbracelet/lipgloss v1.0.0 github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v0.4.0 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 v27.4.1+incompatible github.com/docker/cli v28.3.3+incompatible
github.com/docker/docker v27.4.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.13.1 github.com/go-git/go-git/v5 v5.16.2
github.com/google/go-cmp v0.6.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/muesli/reflow v0.3.0
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.17.1 github.com/schollz/progressbar/v3 v3.18.0
golang.org/x/term v0.28.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.1 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/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // 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.3 // 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/x/ansi v0.6.0 // indirect github.com/charmbracelet/colorprofile v0.3.1 // indirect
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b // indirect github.com/charmbracelet/x/ansi v0.10.1 // 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.5.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/cyphar/filepath-securejoin v0.3.6 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // 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 v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-connections v0.6.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
@ -57,23 +62,23 @@ require (
github.com/fsnotify/fsnotify v1.6.0 // 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.1 // 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.25.1 // 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
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
@ -81,57 +86,57 @@ require (
github.com/miekg/pkcs11 v1.1.1 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mmcloughlin/avo v0.6.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/mountinfo v0.7.2 // 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
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect github.com/muesli/termenv v0.16.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/runc v1.1.13 // indirect github.com/opencontainers/runc v1.1.13 // indirect
github.com/opencontainers/runtime-spec v1.1.0 // indirect github.com/opencontainers/runtime-spec v1.1.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pjbgf/sha1cd v0.3.1 // indirect github.com/pjbgf/sha1cd v0.4.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.61.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.0 // indirect github.com/skeema/knownhosts v1.3.1 // indirect
github.com/spf13/pflag v1.0.5 // 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
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.58.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
go.opentelemetry.io/otel v1.33.0 // indirect go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.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.33.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.33.0 // indirect go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.33.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.33.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.32.0 // indirect golang.org/x/crypto v0.41.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect golang.org/x/exp v0.0.0-20250811191247-51f88131bc50 // indirect
golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.43.0 // indirect
golang.org/x/net v0.34.0 // indirect golang.org/x/sync v0.16.0 // indirect
golang.org/x/sync v0.10.0 // indirect golang.org/x/text v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.12.0 // indirect
golang.org/x/time v0.9.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a // indirect
golang.org/x/tools v0.29.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect google.golang.org/grpc v1.74.2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect google.golang.org/protobuf v1.36.7 // indirect
google.golang.org/grpc v1.69.2 // indirect
google.golang.org/protobuf v1.36.2 // 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
) )
@ -140,19 +145,19 @@ require (
github.com/containers/image v3.0.2+incompatible github.com/containers/image v3.0.2+incompatible
github.com/containers/storage v1.38.2 // indirect github.com/containers/storage v1.38.2 // indirect
github.com/decentral1se/passgen v1.0.1 github.com/decentral1se/passgen v1.0.1
github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect
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.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/prometheus/client_golang v1.20.5 // 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.8.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.29.0 golang.org/x/sys v0.35.0
) )

336
go.sum
View File

@ -24,13 +24,19 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb h1:Ws6WEwKXeaYEkfdkX6AqX1XLPuaCeyStEtxbmEJPllk= coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb h1:Ws6WEwKXeaYEkfdkX6AqX1XLPuaCeyStEtxbmEJPllk=
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ= coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
coopcloud.tech/tagcmp v0.0.0-20250427094623-9ea3bbbde8e5 h1:tphJCjFJw9fdjyKnbU0f7f3z5KtYE8VbUcAfu+oHKg8=
coopcloud.tech/tagcmp v0.0.0-20250427094623-9ea3bbbde8e5/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
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-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
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-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
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=
@ -51,6 +57,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
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.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
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,8 +87,10 @@ 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.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
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=
@ -130,21 +140,36 @@ github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3k
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 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/x/ansi v0.6.0 h1:qOznutrb93gx9oMiGf7caF7bqqubh6YIM0SWKyA08pA= github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
github.com/charmbracelet/x/ansi v0.6.0/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q= github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= 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.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I=
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
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.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
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/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/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
@ -164,8 +189,10 @@ github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2u
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ=
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
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=
@ -211,6 +238,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=
@ -233,6 +264,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=
@ -279,9 +312,10 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= 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=
@ -289,8 +323,8 @@ github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
@ -309,24 +343,30 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI= github.com/docker/cli v28.0.1+incompatible h1:g0h5NQNda3/CxIsaZfH4Tyf6vpxFth7PYl3hgCPOKzs=
github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v28.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v28.3.3+incompatible h1:fp9ZHAr1WWPGdIWBM1b3zLtgCF+83gRdVMTJsUeiyAo=
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 v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4= github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
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.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= 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.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
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=
@ -344,8 +384,8 @@ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:Htrtb
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@ -382,12 +422,14 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA= github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE= 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.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M= github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc= github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
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=
@ -403,6 +445,8 @@ github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTg
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.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
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=
@ -421,6 +465,8 @@ github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
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.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
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=
@ -484,8 +530,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=
github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@ -525,9 +571,12 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
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 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=
@ -541,6 +590,8 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh
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.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
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 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@ -589,8 +640,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -608,6 +659,8 @@ 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=
@ -622,17 +675,15 @@ github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7
github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
@ -658,18 +709,22 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 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/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/mmcloughlin/avo v0.6.0 h1:QH6FU8SKoTLaVs80GA8TJuLNkUYl4VokHKlPhVDg4YY=
github.com/mmcloughlin/avo v0.6.0/go.mod h1:8CoAGaCSYXtCPR+8y18Y9aB/kxb8JSS6FRI7mSkvD+8=
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.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
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=
@ -677,6 +732,8 @@ github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5Xt
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.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
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=
@ -694,10 +751,8 @@ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
@ -734,8 +789,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
@ -765,8 +820,10 @@ github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrap
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 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/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.1 h1:Dh2GYdpJnO84lIw0LJwTFXjcNbasP/bklicSznyAaPI= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.1/go.mod h1:Y8t7jSB/dEI/lQE04A1HVKteqjj9bX5O4+Cex0TCu8s= github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY=
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=
@ -782,8 +839,10 @@ 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.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
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=
@ -791,14 +850,18 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
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.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
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.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
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=
@ -812,28 +875,32 @@ github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
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.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
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.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
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=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/schollz/progressbar/v3 v3.17.1 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U= github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4= github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
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.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
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=
@ -845,8 +912,8 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
@ -861,8 +928,8 @@ github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3
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=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 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 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=
@ -871,8 +938,11 @@ github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
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.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 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=
@ -927,6 +997,8 @@ github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -947,29 +1019,49 @@ 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.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 h1:7F29RDmnlqk6B5d+sUqemt8TBfDqxryYW5gX6L74RFA= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0/go.mod h1:ZiGDq7xwDMKmWDrN1XsXAj0iC7hns+2DhxBFSncNHSE= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= 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 h1:zG8GlgXCJQd5BU98C0hZnBbElszTmUgCNCfYneaDL0A=
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.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
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.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
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.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU= go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q= go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= 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.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
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.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
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.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
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=
@ -994,8 +1086,10 @@ golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWP
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
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=
@ -1006,8 +1100,10 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= 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-20250811191247-51f88131bc50 h1:3yiSh9fhy5/RhCSntf4Sy0Tnx50DmMpQ4MQdKKk4yg4=
golang.org/x/exp v0.0.0-20250811191247-51f88131bc50/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg=
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=
@ -1030,8 +1126,6 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -1073,8 +1167,10 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
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=
@ -1092,8 +1188,10 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
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=
@ -1171,16 +1269,19 @@ 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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/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.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
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.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
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=
@ -1190,16 +1291,20 @@ 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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
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.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
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=
@ -1245,8 +1350,6 @@ golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4X
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -1295,10 +1398,14 @@ google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfG
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24= google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw= google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a h1:DMCgtIAIQGZqJXMVzJF4MV8BlWoJh2ZuFiRdAleyr58=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= 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-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
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-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=
@ -1318,8 +1425,10 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
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=
@ -1333,8 +1442,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.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
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 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
@ -1374,11 +1485,12 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

12
locales/default.pot Normal file
View File

@ -0,0 +1,12 @@
msgid ""
msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"X-Generator: xgotext\n"
#: app.go:11
msgid "Manage apps"
msgstr ""

View File

@ -0,0 +1,20 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-08-04 14:15+0000\n"
"PO-Revision-Date: 2025-08-04 14:15+0000\n"
"Last-Translator: 3wordchant <3wc.coopcloud@doesthisthing.work>\n"
"Language-Team: Spanish <https://translate.coopcloud.tech/projects/"
"co-op-cloud/abra/es/>\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: ENCODING\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.12.2\n"
#: app.go:11
msgid "Manage apps"
msgstr "Gestionar aplicaciones"

View File

@ -655,19 +655,6 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error {
splitted := strings.Split(line, ":") splitted := strings.Split(line, ":")
if a.Recipe.Dirty {
dirtyVersion = fmt.Sprintf("%s%s", version, config.DIRTY_DEFAULT)
if strings.Contains(line, dirtyVersion) {
skipped = true
lines = append(lines, line)
continue
}
line = fmt.Sprintf("%s:%s", splitted[0], dirtyVersion)
lines = append(lines, line)
continue
}
line = fmt.Sprintf("%s:%s", splitted[0], version) line = fmt.Sprintf("%s:%s", splitted[0], version)
lines = append(lines, line) lines = append(lines, line)
} }

View File

@ -223,16 +223,4 @@ func TestWriteRecipeVersionOverwrite(t *testing.T) {
} }
assert.Equal(t, "foo", app.Recipe.EnvVersion) assert.Equal(t, "foo", app.Recipe.EnvVersion)
app.Recipe.Dirty = true
if err := app.WriteRecipeVersion("foo+U", false); err != nil {
t.Fatal(err)
}
app, err = appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "foo+U", app.Recipe.EnvVersion)
} }

View File

@ -44,6 +44,16 @@ func SetChaosVersionLabel(compose *composetypes.Config, stackName string, chaosV
} }
} }
func SetVersionLabel(compose *composetypes.Config, stackName string, version string) {
for _, service := range compose.Services {
if service.Name == "app" {
log.Debugf("set label 'coop-cloud.%s.version' to %v for %s", stackName, version, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName)
service.Deploy.Labels[labelKey] = version
}
}
}
// SetUpdateLabel adds env ENABLE_AUTO_UPDATE as label to enable/disable the // SetUpdateLabel adds env ENABLE_AUTO_UPDATE as label to enable/disable the
// auto update process for this app. The default if this variable is not set is to disable // auto update process for this app. The default if this variable is not set is to disable
// the auto update process. // the auto update process.

View File

@ -1,7 +1,10 @@
package git package git
import ( import (
"context"
"fmt"
"os" "os"
"os/signal"
"strings" "strings"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
@ -22,46 +25,81 @@ func gitCloneIgnoreErr(err error) bool {
return false return false
} }
// Clone runs a git clone which accounts for different default branches. // Clone runs a git clone which accounts for different default branches. This
// function respects Ctrl+C (SIGINT) calls from the user, cancelling the
// context and deleting the (typically) half-baked clone of the repository.
// This avoids broken state for future clone / recipe ops.
func Clone(dir, url string) error { func Clone(dir, url string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) { ctx := context.Background()
log.Debugf("git clone: %s", dir, url) ctx, cancelCtx := context.WithCancel(ctx)
_, err := git.PlainClone(dir, false, &git.CloneOptions{ sigIntCh := make(chan os.Signal, 1)
URL: url, signal.Notify(sigIntCh, os.Interrupt)
Tags: git.AllTags, defer func() {
ReferenceName: plumbing.ReferenceName("refs/heads/main"), signal.Stop(sigIntCh)
SingleBranch: true, cancelCtx()
}) }()
if err != nil && gitCloneIgnoreErr(err) { errCh := make(chan error)
log.Debugf("git clone: %s cloned successfully", dir)
return nil
}
if err != nil { go func() {
log.Debug("git clone: main branch failed, attempting master branch") if _, err := os.Stat(dir); os.IsNotExist(err) {
log.Debugf("git clone: %s", url)
_, err := git.PlainClone(dir, false, &git.CloneOptions{ _, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{
URL: url, URL: url,
Tags: git.AllTags, Tags: git.AllTags,
ReferenceName: plumbing.ReferenceName("refs/heads/master"), ReferenceName: plumbing.ReferenceName("refs/heads/main"),
SingleBranch: true, SingleBranch: true,
}) })
if err != nil && gitCloneIgnoreErr(err) { if err != nil && gitCloneIgnoreErr(err) {
log.Debugf("git clone: %s cloned successfully", dir) log.Debugf("git clone: %s cloned successfully", dir)
return nil errCh <- nil
}
if err := ctx.Err(); err != nil {
errCh <- fmt.Errorf("git clone %s: cancelled due to interrupt", dir)
} }
if err != nil { if err != nil {
return err log.Debug("git clone: main branch failed, attempting master branch")
_, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{
URL: url,
Tags: git.AllTags,
ReferenceName: plumbing.ReferenceName("refs/heads/master"),
SingleBranch: true,
})
if err != nil && gitCloneIgnoreErr(err) {
log.Debugf("git clone: %s cloned successfully", dir)
errCh <- nil
}
if err != nil {
errCh <- err
}
} }
log.Debugf("git clone: %s cloned successfully", dir)
} else {
log.Debugf("git clone: %s already exists", dir)
} }
log.Debugf("git clone: %s cloned successfully", dir) errCh <- nil
} else { }()
log.Debugf("git clone: %s already exists", dir)
select {
case <-sigIntCh:
cancelCtx()
fmt.Println() // NOTE(d1): newline after ^C
if err := os.RemoveAll(dir); err != nil {
return fmt.Errorf("unable to clean up git clone of %s: %s", dir, err)
}
return fmt.Errorf("git clone %s: cancelled due to interrupt", dir)
case err := <-errCh:
return err
} }
return nil return nil

48
pkg/git/clone_test.go Normal file
View File

@ -0,0 +1,48 @@
package git
import (
"fmt"
"os"
"path"
"syscall"
"testing"
"coopcloud.tech/abra/pkg/config"
)
func TestClone(t *testing.T) {
dir := path.Join(config.RECIPES_DIR, "gitea")
os.RemoveAll(dir)
gitURL := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "gitea")
if err := Clone(dir, gitURL); err != nil {
t.Fatalf("unable to git clone gitea: %s", err)
}
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
t.Fatal("gitea repo was not cloned successfully")
}
}
func TestCancelGitClone(t *testing.T) {
dir := path.Join(config.RECIPES_DIR, "gitea")
os.RemoveAll(dir)
go func() {
p, err := os.FindProcess(os.Getpid())
if err != nil {
t.Fatalf("unable to find current process: %s", err)
}
p.Signal(syscall.SIGINT)
}()
gitURL := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "gitea")
if err := Clone(dir, gitURL); err == nil {
t.Fatal("cloning should have been interrupted")
}
if _, err := os.Stat(dir); err != nil && !os.IsNotExist(err) {
t.Fatal("recipe repo was not deleted")
}
}

30
pkg/lang/lang.go Normal file
View File

@ -0,0 +1,30 @@
package lang
import (
"os"
"strings"
)
func GetLocale() string {
if loc := os.Getenv("LC_MESSAGES"); loc != "" {
return NormalizeLocale(loc)
}
if loc := os.Getenv("LANG"); loc != "" {
return NormalizeLocale(loc)
}
return "C.UTF-8"
}
func NormalizeLocale(loc string) string {
if idx := strings.Index(loc, "."); idx != -1 {
return loc[:idx]
}
if idx := strings.Index(loc, "@"); idx != -1 {
return loc[:idx]
}
return loc
}

View File

@ -15,8 +15,10 @@ import (
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
) )
var Warn = "warn" var (
var Critical = "critical" Warn = "warn"
Critical = "critical"
)
type LintFunction func(recipe.Recipe) (bool, error) type LintFunction func(recipe.Recipe) (bool, error)
@ -182,6 +184,8 @@ var LintRules = map[string][]LintRule{
func LintForErrors(recipe recipe.Recipe) error { func LintForErrors(recipe recipe.Recipe) error {
log.Debugf("linting for critical errors in %s configs", recipe.Name) log.Debugf("linting for critical errors in %s configs", recipe.Name)
var errors string
for level := range LintRules { for level := range LintRules {
if level != "error" { if level != "error" {
continue continue
@ -194,14 +198,18 @@ func LintForErrors(recipe recipe.Recipe) error {
ok, err := rule.Function(recipe) ok, err := rule.Function(recipe)
if err != nil { if err != nil {
return err errors += fmt.Sprintf("\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) errors += fmt.Sprintf("\n * %s (%s)", rule.Description, rule.Ref)
} }
} }
} }
if len(errors) > 0 {
return fmt.Errorf("recipe '%s' failed lint checks:\n"+errors[1:], recipe.Name)
}
log.Debugf("linting successful, %s is well configured", recipe.Name) log.Debugf("linting successful, %s is well configured", recipe.Name)
return nil return nil

View File

@ -2,8 +2,10 @@
package log package log
import ( import (
"math"
"os" "os"
tea "github.com/charmbracelet/bubbletea"
charmLog "github.com/charmbracelet/log" charmLog "github.com/charmbracelet/log"
) )
@ -32,3 +34,13 @@ var SetLevel = Logger.SetLevel
var DebugLevel = charmLog.DebugLevel var DebugLevel = charmLog.DebugLevel
var SetOutput = charmLog.SetOutput var SetOutput = charmLog.SetOutput
var SetReportCaller = charmLog.SetReportCaller var SetReportCaller = charmLog.SetReportCaller
type f func() (tea.Model, error)
func Without(fn f) (tea.Model, error) {
l := Logger.GetLevel()
Logger.SetLevel(math.MaxInt)
m, err := fn()
Logger.SetLevel(l)
return m, err
}

View File

@ -1,18 +1,17 @@
package logs package logs
import ( import (
"bytes" "bufio"
"context" "context"
"fmt"
"io" "io"
"os" "os"
"os/signal" "os/signal"
"slices"
"sync" "sync"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
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/api/types/swarm"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
) )
@ -21,7 +20,7 @@ type TailOpts struct {
Services []string Services []string
StdErr bool StdErr bool
Since string Since string
Buffer *bytes.Buffer Buffer *[]string
ToBuffer bool ToBuffer bool
Filters filters.Args Filters filters.Args
} }
@ -32,56 +31,32 @@ func TailLogs(
cl *dockerClient.Client, cl *dockerClient.Client,
opts TailOpts, opts TailOpts,
) error { ) error {
sigintChannel := make(chan os.Signal, 1) sigIntCh := make(chan os.Signal, 1)
signal.Notify(sigintChannel, os.Interrupt) signal.Notify(sigIntCh, os.Interrupt)
defer signal.Stop(sigintChannel) defer signal.Stop(sigIntCh)
f := filters.NewArgs()
for _, name := range opts.Services {
f.Add("name", name)
}
services, err := cl.ServiceList( services, err := cl.ServiceList(
context.Background(), context.Background(),
types.ServiceListOptions{Filters: f}, types.ServiceListOptions{Filters: opts.Filters},
) )
if err != nil { if err != nil {
return err return err
} }
errCh := make(chan error)
waitCh := make(chan struct{}) waitCh := make(chan struct{})
go func() { go func() {
var wg sync.WaitGroup var wg sync.WaitGroup
for _, service := range services { for _, service := range services {
f := filters.NewArgs()
f.Add("name", service.Spec.Name)
tasks, err := cl.TaskList(context.Background(), types.TaskListOptions{Filters: f})
if err != nil {
// TODO
// return nil
}
if len(tasks) > 0 {
// Need to sort the tasks by the CreatedAt field in the inverse order.
// Otherwise they are in the reversed order and not sorted properly.
slices.SortFunc[[]swarm.Task](tasks, func(t1, t2 swarm.Task) int {
return int(t2.Meta.CreatedAt.Unix() - t1.Meta.CreatedAt.Unix())
})
lastTask := tasks[0].Status
if lastTask.State != swarm.TaskStateRunning {
// for _, task := range tasks {
// TODO
// return fmt.Errorf("[%s] %s state %s: %s", service.Spec.Name, task.Meta.CreatedAt.Format(time.RFC3339), task.Status.State, task.Status.Err)
// }
}
}
// Collect the logs in a go routine, so the logs from all services are
// collected in parallel.
wg.Add(1) wg.Add(1)
go func(serviceID string) { go func(serviceID string) {
tail := "50"
if opts.ToBuffer {
// NOTE(d1): more logs from before deployment when analysing via file
tail = "150"
}
logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{ logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{
ShowStderr: true, ShowStderr: true,
ShowStdout: !opts.StdErr, ShowStdout: !opts.StdErr,
@ -89,32 +64,26 @@ func TailLogs(
Until: "", Until: "",
Timestamps: true, Timestamps: true,
Follow: true, Follow: true,
Tail: "50", // TODO: if buf, bump it up! Tail: tail,
Details: false, // TODO: if buf, yes! Details: false,
}) })
if err != nil {
// TODO
// log.Debugf("unable to poll logs: %s", err)
}
defer logs.Close()
if opts.ToBuffer { if err == nil {
_, err = io.Copy(opts.Buffer, logs) defer logs.Close()
if err != nil && err != io.EOF { if opts.ToBuffer {
// TODO buf := bufio.NewScanner(logs)
// log.Debugf("unable to copy buffer: %s", err) for buf.Scan() {
line := fmt.Sprintf("%s: %s", service.Spec.Name, buf.Text())
*opts.Buffer = append(*opts.Buffer, line)
}
logs.Close()
return
} }
logs.Close()
return
}
_, err = io.Copy(os.Stdout, logs) if _, err = io.Copy(os.Stdout, logs); err != nil && err != io.EOF {
if err != nil && err != io.EOF { errCh <- fmt.Errorf("tailLogs: unable to copy buffer: %s", err)
// TODO }
// log.Debugf("unable to copy buffer: %s", err)
} }
logs.Close()
}(service.ID) }(service.ID)
} }
@ -125,8 +94,10 @@ func TailLogs(
select { select {
case <-waitCh: case <-waitCh:
return nil return nil
case <-sigintChannel: case <-sigIntCh:
return nil return nil
case err := <-errCh:
return err
} }
return nil return nil

View File

@ -7,6 +7,7 @@ import (
"sort" "sort"
"strings" "strings"
"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/log" "coopcloud.tech/abra/pkg/log"
@ -45,6 +46,9 @@ func (r Recipe) Ensure(ctx EnsureContext) error {
if r.EnvVersion != "" && !ctx.IgnoreEnvVersion { if r.EnvVersion != "" && !ctx.IgnoreEnvVersion {
log.Debugf("ensuring env version %s", r.EnvVersion) log.Debugf("ensuring env version %s", r.EnvVersion)
if strings.Contains(r.EnvVersion, "+U") {
log.Fatalf("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 {
return err return err
@ -274,19 +278,14 @@ func (r Recipe) EnsureUpToDate() error {
return nil return nil
} }
// IsDirty checks whether a recipe is dirty or not. N.B., if you call IsDirty // IsDirty checks whether a recipe is dirty or not.
// from another Recipe method, you should propagate the pointer reference (*). func (r *Recipe) IsDirty() (bool, error) {
func (r *Recipe) IsDirty() error {
isClean, err := gitPkg.IsClean(r.Dir) isClean, err := gitPkg.IsClean(r.Dir)
if err != nil { if err != nil {
return err return false, err
} }
if !isClean { return !isClean, nil
r.Dirty = true
}
return nil
} }
// ChaosVersion constructs a chaos mode recipe version. // ChaosVersion constructs a chaos mode recipe version.
@ -300,8 +299,12 @@ func (r *Recipe) ChaosVersion() (string, error) {
version = formatter.SmallSHA(head.String()) version = formatter.SmallSHA(head.String())
if err := r.IsDirty(); err != nil { dirty, err := r.IsDirty()
return version, err if err != nil {
return "", err
}
if dirty {
return fmt.Sprintf("%s%s", version, config.DIRTY_DEFAULT), nil
} }
return version, nil return version, nil

View File

@ -15,10 +15,6 @@ func TestIsDirty(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if err := r.IsDirty(); err != nil {
t.Fatal(err)
}
assert.False(t, r.Dirty) assert.False(t, r.Dirty)
fpath := filepath.Join(r.Dir, "foo.txt") fpath := filepath.Join(r.Dir, "foo.txt")
@ -31,9 +27,10 @@ func TestIsDirty(t *testing.T) {
os.Remove(fpath) os.Remove(fpath)
}) })
if err := r.IsDirty(); err != nil { dirty, err := r.IsDirty()
if err != nil {
t.Fatal(err) t.Fatal(err)
} }
assert.True(t, r.Dirty) assert.True(t, dirty)
} }

View File

@ -12,6 +12,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/go-git/go-git/v5"
"coopcloud.tech/abra/pkg/catalogue" "coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
@ -20,7 +22,6 @@ import (
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/web" "coopcloud.tech/abra/pkg/web"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/go-git/go-git/v5"
) )
// RecipeCatalogueURL is the only current recipe catalogue available. // RecipeCatalogueURL is the only current recipe catalogue available.
@ -119,22 +120,9 @@ type Features struct {
SSO string `json:"sso"` SSO string `json:"sso"`
} }
func GetEnvVersionRaw(name string) (string, error) {
var version string
if strings.Contains(name, ":") {
split := strings.Split(name, ":")
if len(split) > 2 {
return version, fmt.Errorf("version seems invalid: %s", name)
}
version = split[1]
}
return version, nil
}
func Get(name string) Recipe { func Get(name string) Recipe {
version := "" version := ""
versionRaw := ""
if strings.Contains(name, ":") { if strings.Contains(name, ":") {
split := strings.Split(name, ":") split := strings.Split(name, ":")
if len(split) > 2 { if len(split) > 2 {
@ -143,6 +131,7 @@ func Get(name string) Recipe {
name = split[0] name = split[0]
version = split[1] version = split[1]
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.Debugf("removed dirty suffix from .env version: %s -> %s", split[1], version)
@ -167,11 +156,12 @@ func Get(name string) Recipe {
dir := path.Join(config.RECIPES_DIR, escapeRecipeName(name)) dir := path.Join(config.RECIPES_DIR, escapeRecipeName(name))
r := Recipe{ r := Recipe{
Name: name, Name: name,
EnvVersion: version, EnvVersion: version,
Dir: dir, EnvVersionRaw: versionRaw,
GitURL: gitURL, Dir: dir,
SSHURL: sshURL, GitURL: gitURL,
SSHURL: sshURL,
ComposePath: path.Join(dir, "compose.yml"), ComposePath: path.Join(dir, "compose.yml"),
ReadmePath: path.Join(dir, "README.md"), ReadmePath: path.Join(dir, "README.md"),
@ -179,20 +169,23 @@ func Get(name string) Recipe {
AbraShPath: path.Join(dir, "abra.sh"), AbraShPath: path.Join(dir, "abra.sh"),
} }
if err := r.IsDirty(); err != nil && !errors.Is(err, git.ErrRepositoryNotExists) { dirty, err := r.IsDirty()
if err != nil && !errors.Is(err, git.ErrRepositoryNotExists) {
log.Fatalf("failed to check git status of %s: %s", r.Name, err) log.Fatalf("failed to check git status of %s: %s", r.Name, err)
} }
r.Dirty = dirty
return r return r
} }
type Recipe struct { type Recipe struct {
Name string Name string
EnvVersion string EnvVersion string
Dirty bool // NOTE(d1): git terminology for unstaged changes EnvVersionRaw string
Dir string Dirty bool // NOTE(d1): git terminology for unstaged changes
GitURL string Dir string
SSHURL string GitURL string
SSHURL string
ComposePath string ComposePath string
ReadmePath string ReadmePath string

View File

@ -34,6 +34,7 @@ func TestGet(t *testing.T) {
recipe: Recipe{ recipe: Recipe{
Name: "foo", Name: "foo",
EnvVersion: "1.2.3", EnvVersion: "1.2.3",
EnvVersionRaw: "1.2.3",
Dir: path.Join(cfg.GetAbraDir(), "/recipes/foo"), Dir: path.Join(cfg.GetAbraDir(), "/recipes/foo"),
GitURL: "https://git.coopcloud.tech/coop-cloud/foo.git", GitURL: "https://git.coopcloud.tech/coop-cloud/foo.git",
SSHURL: "ssh://git@git.coopcloud.tech:2222/coop-cloud/foo.git", SSHURL: "ssh://git@git.coopcloud.tech:2222/coop-cloud/foo.git",
@ -61,6 +62,22 @@ func TestGet(t *testing.T) {
recipe: Recipe{ recipe: Recipe{
Name: "mygit.org/myorg/cool-recipe", Name: "mygit.org/myorg/cool-recipe",
EnvVersion: "1.2.4", EnvVersion: "1.2.4",
EnvVersionRaw: "1.2.4",
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
GitURL: "https://mygit.org/myorg/cool-recipe.git",
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
ComposePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/compose.yml"),
ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/README.md"),
SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/.env.sample"),
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"),
},
},
{
name: "mygit.org/myorg/cool-recipe:1e83340e+U",
recipe: Recipe{
Name: "mygit.org/myorg/cool-recipe",
EnvVersion: "1e83340e",
EnvVersionRaw: "1e83340e+U",
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"), Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
GitURL: "https://mygit.org/myorg/cool-recipe.git", GitURL: "https://mygit.org/myorg/cool-recipe.git",
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git", SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
@ -105,16 +122,3 @@ func TestGetVersionLabelLocalDoesNotUseTimeoutLabel(t *testing.T) {
assert.NotEqual(t, label, defaultTimeoutLabel) assert.NotEqual(t, label, defaultTimeoutLabel)
} }
} }
func TestDirtyMarkerRemoved(t *testing.T) {
r := Get("abra-test-recipe:1e83340e+U")
assert.Equal(t, "1e83340e", r.EnvVersion)
}
func TestGetEnvVersionRaw(t *testing.T) {
v, err := GetEnvVersionRaw("abra-test-recipe:1e83340e+U")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "1e83340e+U", v)
}

View File

@ -33,6 +33,10 @@ type Secret struct {
// variable. For Example: // variable. For Example:
// SECRET_FOO=v1 # length=12 // SECRET_FOO=v1 # length=12
Length int Length int
// Charset comes from the charset modifier at the secret version environment
// variable. For Example:
// SECRET_FOO=v1 # charset=default,special
Charset string
// 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:
@ -43,38 +47,38 @@ type Secret struct {
RemoteName string RemoteName string
} }
// GeneratePasswords generates passwords. // GeneratePassword generates passwords.
func GeneratePasswords(count, length uint) ([]string, error) { func GeneratePassword(length uint, charset string) (string, error) {
passwords, err := passgen.GeneratePasswords( passwords, err := passgen.GeneratePasswords(
count, 1,
length, length,
passgen.AlphabetDefault, charset,
) )
if err != nil { if err != nil {
return nil, err return "", err
} }
log.Debugf("generated %s", strings.Join(passwords, ", ")) log.Debugf("generated %s", strings.Join(passwords, ", "))
return passwords, nil return passwords[0], nil
} }
// GeneratePassphrases generates human readable and rememberable passphrases. // GeneratePassphrase generates human readable and rememberable passphrases.
func GeneratePassphrases(count uint) ([]string, error) { func GeneratePassphrase() (string, error) {
passphrases, err := passgen.GeneratePassphrases( passphrases, err := passgen.GeneratePassphrases(
count, 1,
passgen.PassphraseWordCountDefault, passgen.PassphraseWordCountDefault,
rune('-'), rune('-'),
passgen.PassphraseCasingDefault, passgen.PassphraseCasingDefault,
passgen.WordListDefault, passgen.WordListDefault,
) )
if err != nil { if err != nil {
return nil, err return "", err
} }
log.Debugf("generated %s", strings.Join(passphrases, ", ")) log.Debugf("generated %s", strings.Join(passphrases, ", "))
return passphrases, nil return passphrases[0], nil
} }
// ReadSecretsConfig reads secret names/versions from the recipe config. The // ReadSecretsConfig reads secret names/versions from the recipe config. The
@ -150,6 +154,8 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
} }
value.Length = length value.Length = length
} }
value.Charset = resolveCharset(modifierValues["charset"])
break break
} }
secretValues[secretId] = value secretValues[secretId] = value
@ -158,6 +164,22 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
return secretValues, nil return secretValues, nil
} }
// resolveCharset sets the passgen Alphabet required for a secret
func resolveCharset(input string) string {
switch strings.ToLower(input) {
case "special":
return passgen.AlphabetSpecial
case "safespecial":
return "!@#%^&*_-+="
case "default,special", "special,default":
return passgen.AlphabetDefault + passgen.AlphabetSpecial
case "default,safespecial", "safespecial,default":
return passgen.AlphabetDefault + "!@#%^&*_-+="
default:
return passgen.AlphabetDefault // Fallback to default
}
}
// GenerateSecrets generates secrets locally and sends them to a remote server for storage. // GenerateSecrets generates secrets locally and sends them to a remote server for storage.
func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server string) (map[string]string, error) { func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server string) (map[string]string, error) {
secretsGenerated := map[string]string{} secretsGenerated := map[string]string{}
@ -173,13 +195,13 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
log.Debugf("attempting to generate and store %s on %s", secret.RemoteName, server) log.Debugf("attempting to generate and store %s on %s", secret.RemoteName, server)
if secret.Length > 0 { if secret.Length > 0 {
passwords, err := GeneratePasswords(1, uint(secret.Length)) password, err := GeneratePassword(uint(secret.Length), secret.Charset)
if err != nil { if err != nil {
ch <- err ch <- err
return return
} }
if err := client.StoreSecret(cl, secret.RemoteName, passwords[0], 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("%s already exists", secret.RemoteName)
ch <- nil ch <- nil
@ -191,15 +213,15 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
secretsGenerated[secretName] = passwords[0] secretsGenerated[secretName] = password
} else { } else {
passphrases, err := GeneratePassphrases(1) passphrase, err := GeneratePassphrase()
if err != nil { if err != nil {
ch <- err ch <- err
return return
} }
if err := client.StoreSecret(cl, secret.RemoteName, passphrases[0], 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("%s already exists", secret.RemoteName)
ch <- nil ch <- nil
@ -211,7 +233,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
secretsGenerated[secretName] = passphrases[0] secretsGenerated[secretName] = passphrase
} }
ch <- nil ch <- nil
}(n, v) }(n, v)

View File

@ -17,16 +17,37 @@ func TestReadSecretsConfig(t *testing.T) {
assert.Equal(t, "test_example_com_test_pass_one_v2", secretsFromConfig["test_pass_one"].RemoteName) assert.Equal(t, "test_example_com_test_pass_one_v2", secretsFromConfig["test_pass_one"].RemoteName)
assert.Equal(t, "v2", secretsFromConfig["test_pass_one"].Version) assert.Equal(t, "v2", secretsFromConfig["test_pass_one"].Version)
assert.Equal(t, 0, secretsFromConfig["test_pass_one"].Length) assert.Equal(t, 0, secretsFromConfig["test_pass_one"].Length)
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_one"].Charset)
// Has a length modifier // Has a length modifier
assert.Equal(t, "test_example_com_test_pass_two_v1", secretsFromConfig["test_pass_two"].RemoteName) assert.Equal(t, "test_example_com_test_pass_two_v1", secretsFromConfig["test_pass_two"].RemoteName)
assert.Equal(t, "v1", secretsFromConfig["test_pass_two"].Version) assert.Equal(t, "v1", secretsFromConfig["test_pass_two"].Version)
assert.Equal(t, 10, secretsFromConfig["test_pass_two"].Length) assert.Equal(t, 10, secretsFromConfig["test_pass_two"].Length)
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_two"].Charset)
// Secret name does not include the secret id // Secret name does not include the secret id
assert.Equal(t, "test_example_com_pass_three_v2", secretsFromConfig["test_pass_three"].RemoteName) assert.Equal(t, "test_example_com_pass_three_v2", secretsFromConfig["test_pass_three"].RemoteName)
assert.Equal(t, "v2", secretsFromConfig["test_pass_three"].Version) assert.Equal(t, "v2", secretsFromConfig["test_pass_three"].Version)
assert.Equal(t, 0, secretsFromConfig["test_pass_three"].Length) assert.Equal(t, 0, secretsFromConfig["test_pass_three"].Length)
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_three"].Charset)
// Has a length modifier and a charset=default,safespecial modifier
assert.Equal(t, "test_example_com_test_pass_four_v1", secretsFromConfig["test_pass_four"].RemoteName)
assert.Equal(t, "v1", secretsFromConfig["test_pass_four"].Version)
assert.Equal(t, 12, secretsFromConfig["test_pass_four"].Length)
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#%^&*_-+=", secretsFromConfig["test_pass_four"].Charset)
// Has a length modifier and a charset=default,special modifier
assert.Equal(t, "test_example_com_test_pass_five_v1", secretsFromConfig["test_pass_five"].RemoteName)
assert.Equal(t, "v1", secretsFromConfig["test_pass_five"].Version)
assert.Equal(t, 12, secretsFromConfig["test_pass_five"].Length)
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%^&*_-+=", secretsFromConfig["test_pass_five"].Charset)
// Has only a charset=default,special modifier, which gets setted but ignored in the generation
assert.Equal(t, "test_example_com_test_pass_six_v1", secretsFromConfig["test_pass_six"].RemoteName)
assert.Equal(t, "v1", secretsFromConfig["test_pass_six"].Version)
assert.Equal(t, 0, secretsFromConfig["test_pass_six"].Length)
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%^&*_-+=", secretsFromConfig["test_pass_six"].Charset)
} }
func TestReadSecretsConfigWithLongDomain(t *testing.T) { func TestReadSecretsConfigWithLongDomain(t *testing.T) {

View File

@ -1,3 +1,6 @@
SECRET_TEST_PASS_ONE_VERSION=v2 SECRET_TEST_PASS_ONE_VERSION=v2
SECRET_TEST_PASS_TWO_VERSION=v1 # length=10 SECRET_TEST_PASS_TWO_VERSION=v1 # length=10
SECRET_TEST_PASS_THREE_VERSION=v2 SECRET_TEST_PASS_THREE_VERSION=v2
SECRET_TEST_PASS_FOUR_VERSION=v1 # length=12 charset=default,safespecial
SECRET_TEST_PASS_FIVE_VERSION=v1 # length=12 charset=default,special
SECRET_TEST_PASS_SIX_VERSION=v1 # charset=default,special

View File

@ -8,6 +8,9 @@ services:
- test_pass_one - test_pass_one
- test_pass_two - test_pass_two
- test_pass_three - test_pass_three
- test_pass_four
- test_pass_five
- test_pass_six
secrets: secrets:
test_pass_one: test_pass_one:
@ -19,3 +22,12 @@ secrets:
test_pass_three: test_pass_three:
external: true external: true
name: ${STACK_NAME}_pass_three_${SECRET_TEST_PASS_THREE_VERSION} # secretId and name don't match name: ${STACK_NAME}_pass_three_${SECRET_TEST_PASS_THREE_VERSION} # secretId and name don't match
test_pass_four:
external: true
name: ${STACK_NAME}_test_pass_four_${SECRET_TEST_PASS_FOUR_VERSION}
test_pass_five:
external: true
name: ${STACK_NAME}_test_pass_five_${SECRET_TEST_PASS_FIVE_VERSION}
test_pass_six:
external: true
name: ${STACK_NAME}_test_pass_six_${SECRET_TEST_PASS_SIX_VERSION}

View File

@ -20,6 +20,8 @@ func Fatal(hostname string, err error) error {
return fmt.Errorf("ssh auth: permission denied for %s", hostname) return fmt.Errorf("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 fmt.Errorf("unable to connect to %s, please check your SSH config", hostname)
} else if strings.Contains(out, "Is the docker daemon running") {
return fmt.Errorf("docker: is the daemon running / your user has docker permissions?")
} }
return err return err

View File

@ -1,47 +1,39 @@
package ui package ui
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"sort"
"strings" "strings"
"time" "time"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/logs" "coopcloud.tech/abra/pkg/logs"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/docker/cli/cli/command/service/progress" "github.com/docker/cli/cli/command/service/progress"
containerTypes "github.com/docker/docker/api/types/container" containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/jsonmessage"
"github.com/muesli/reflow/wordwrap"
) )
var IsRunning bool
type statusMsg struct { type statusMsg struct {
stream stream stream stream
jsonMsg jsonmessage.JSONMessage jsonMsg jsonmessage.JSONMessage
} }
type healthState struct { type progressCompleteMsg struct {
log string stream stream
failed bool
} }
type healthcheckMsg struct { type healthcheckMsg struct {
stream stream stream stream
state healthState health string
}
type progressCompleteMsg struct {
stream stream
// NOTE(d1): failure scenarios are as follows
// an error from ServiceProgress
// a rollback
failed bool
} }
type ServiceMeta struct { type ServiceMeta struct {
@ -54,16 +46,15 @@ type Model struct {
cl *dockerClient.Client cl *dockerClient.Client
count int count int
ctx context.Context ctx context.Context
err error
info string
timeout time.Duration timeout time.Duration
width int width int
filters filters.Args
LogsBuffer *bytes.Buffer Streams *[]stream
Logs *[]string
Streams *[]stream Failed bool
Failed bool TimedOut bool
Timeout bool Quit bool
} }
func (m Model) complete() bool { func (m Model) complete() bool {
@ -74,18 +65,17 @@ func (m Model) complete() bool {
} }
type stream struct { type stream struct {
decoder *json.Decoder Name string
id string Err error
reader *io.PipeReader
writer *io.PipeWriter
overallStatus string
rollbackStatus string
verifyStatus string
healthcheckStatus string
Err error decoder *json.Decoder
ErrStatus string id string
Name string reader *io.PipeReader
writer *io.PipeWriter
status string
retries int
health string
rollback bool
} }
func (s stream) String() string { func (s stream) String() string {
@ -95,17 +85,12 @@ func (s stream) String() string {
out += fmt.Sprintf("name: %s, ", s.Name) out += fmt.Sprintf("name: %s, ", s.Name)
out += fmt.Sprintf("reader: %v, ", s.reader) out += fmt.Sprintf("reader: %v, ", s.reader)
out += fmt.Sprintf("writer: %v, ", s.writer) out += fmt.Sprintf("writer: %v, ", s.writer)
out += fmt.Sprintf("overallStatus: %s, ", s.overallStatus) out += fmt.Sprintf("status: %s, ", s.status)
out += fmt.Sprintf("rollbackStatus: %s, ", s.rollbackStatus)
out += fmt.Sprintf("verifyStatus: %s, ", s.verifyStatus)
out += fmt.Sprintf("errStatus: %s, ", s.ErrStatus)
out += fmt.Sprintf("healthcheckStatus: %s}", s.healthcheckStatus)
return out return out
} }
func (s stream) progress(m Model) tea.Msg { func (s stream) progress(m Model) tea.Msg {
if err := progress.ServiceProgress(m.ctx, m.cl, s.id, s.writer); err != nil { if err := progress.ServiceProgress(m.ctx, m.cl, s.id, s.writer); err != nil {
s.Err = err
return progressCompleteMsg{ return progressCompleteMsg{
stream: s, stream: s,
failed: true, failed: true,
@ -123,8 +108,6 @@ func (s stream) process() tea.Msg {
// NOTE(d1): end processing messages // NOTE(d1): end processing messages
return nil return nil
} }
s.Err = err
} }
return statusMsg{ return statusMsg{
@ -154,22 +137,12 @@ func (s stream) healthcheck(m Model) tea.Msg {
return healthcheckMsg{stream: s} return healthcheckMsg{stream: s}
} }
var log string var health string
if containerState.State.Health != nil { if containerState.State.Health != nil {
if len(containerState.State.Health.Log) > 0 { health = containerState.State.Health.Status
entry := containerState.State.Health.Log[0]
if entry.ExitCode < 0 {
log = entry.Output
}
}
return healthcheckMsg{
stream: s,
state: healthState{log: log},
}
} }
return healthcheckMsg{stream: s} return healthcheckMsg{stream: s, health: health}
} }
func DeployInitialModel( func DeployInitialModel(
@ -178,6 +151,7 @@ func DeployInitialModel(
services []ServiceMeta, services []ServiceMeta,
appName string, appName string,
timeout time.Duration, timeout time.Duration,
filters filters.Args,
) Model { ) Model {
var streams []stream var streams []stream
for _, service := range services { for _, service := range services {
@ -189,25 +163,23 @@ func DeployInitialModel(
reader: r, reader: r,
writer: w, writer: w,
decoder: d, decoder: d,
retries: -1, // NOTE(d1): skip first attempt
health: "?",
}) })
} }
infoRenderer := lipgloss.NewStyle(). sort.Slice(streams, func(i, j int) bool {
Bold(true). return streams[i].Name < streams[j].Name
MaxWidth(4). })
Foreground(lipgloss.Color("86"))
infoMarker := infoRenderer.Render("INFO")
var logsBuffer bytes.Buffer
return Model{ return Model{
ctx: ctx, ctx: ctx,
cl: cl, cl: cl,
appName: appName, appName: appName,
timeout: timeout, timeout: timeout,
Streams: &streams, filters: filters,
info: infoMarker, Streams: &streams,
LogsBuffer: &logsBuffer, Logs: &[]string{},
} }
} }
@ -241,14 +213,14 @@ func (m Model) gatherLogs() tea.Msg {
AppName: m.appName, AppName: m.appName,
Services: services, Services: services,
StdErr: true, StdErr: true,
Buffer: m.LogsBuffer, Buffer: m.Logs,
ToBuffer: true, ToBuffer: true,
Filters: m.filters,
} }
if err := logs.TailLogs(m.cl, opts); err != nil { // NOTE(d1): not interested in log polling errors. if we don't see logs it
// TODO // will hopefully be self-evident based on what happened in the deployment
// log.Debugf("gatherLogs: %s", err) logs.TailLogs(m.cl, opts)
}
return nil return nil
} }
@ -267,6 +239,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg: case tea.KeyMsg:
switch msg.String() { switch msg.String() {
case "ctrl+c", "q": case "ctrl+c", "q":
m.Quit = true
return m, tea.Quit return m, tea.Quit
} }
@ -284,18 +257,21 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Quit return m, tea.Quit
} }
case timeoutMsg:
m.TimedOut = true
return m, tea.Quit
case healthcheckMsg: case healthcheckMsg:
if msg.state != (healthState{}) && msg.state.log != "" { for idx, s := range *m.Streams {
msg.stream.healthcheckStatus = strings.ToLower(msg.state.log) if s.id == msg.stream.id {
h := "?"
if msg.stream.Err != nil { if s.health != "" {
msg.stream.ErrStatus = msg.stream.Err.Error() h = s.health
}
for idx, s := range *m.Streams {
if s.id == msg.stream.id {
(*m.Streams)[idx].healthcheckStatus = msg.stream.healthcheckStatus
} }
if msg.health != "" {
h = msg.health
}
(*m.Streams)[idx].health = h
} }
} }
@ -304,34 +280,31 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func() tea.Msg { return msg.stream.healthcheck(m) }, func() tea.Msg { return msg.stream.healthcheck(m) },
) )
case timeoutMsg:
m.Timeout = true
return m, tea.Quit
case statusMsg: case statusMsg:
switch msg.jsonMsg.ID {
case "rollback":
m.Failed = true
msg.stream.rollbackStatus = strings.ToLower(msg.jsonMsg.Status)
case "overall progress":
msg.stream.overallStatus = strings.ToLower(msg.jsonMsg.Status)
case "verify":
msg.stream.verifyStatus = strings.ToLower(msg.jsonMsg.Status)
}
if msg.stream.Err != nil {
msg.stream.ErrStatus = msg.stream.Err.Error()
}
for idx, s := range *m.Streams { for idx, s := range *m.Streams {
if s.id == msg.stream.id { if s.id == msg.stream.id {
switch msg.jsonMsg.ID {
case "rollback": if msg.jsonMsg.ID == "rollback" {
(*m.Streams)[idx].rollbackStatus = msg.stream.rollbackStatus m.Failed = true
case "overall progress": (*m.Streams)[idx].rollback = true
(*m.Streams)[idx].overallStatus = msg.stream.overallStatus }
case "verify":
(*m.Streams)[idx].verifyStatus = msg.stream.verifyStatus if msg.jsonMsg.ID != "overall progress" {
newStatus := strings.ToLower(msg.jsonMsg.Status)
currentStatus := (*m.Streams)[idx].status
if !strings.Contains(currentStatus, "starting") &&
strings.Contains(newStatus, "starting") {
(*m.Streams)[idx].retries += 1
}
if (*m.Streams)[idx].rollback {
if msg.jsonMsg.ID == "rollback" {
(*m.Streams)[idx].status = newStatus
}
} else {
(*m.Streams)[idx].status = newStatus
}
} }
} }
} }
@ -352,70 +325,29 @@ func (m Model) View() string {
split := strings.Split(stream.Name, "_") split := strings.Split(stream.Name, "_")
short := split[len(split)-1] short := split[len(split)-1]
newLineSpacing := " " status := stream.status
for range len(short) { if strings.Contains(stream.status, "converged") && !stream.rollback {
newLineSpacing += " " status = "succeeded"
}
if strings.Contains(stream.status, "rolled back") {
status = "rolled back"
} }
output := fmt.Sprintf("%s %s: %s", retries := 0
m.info, if stream.retries > 0 {
retries = stream.retries
}
output := fmt.Sprintf("%s: %s (retries: %v, healthcheck: %s)",
formatter.BoldStyle.Render(short), formatter.BoldStyle.Render(short),
stream.overallStatus, status,
retries,
stream.health,
) )
if stream.verifyStatus != "" {
output += fmt.Sprintf(" (%s)", stream.verifyStatus)
}
if stream.rollbackStatus != "" {
output += fmt.Sprintf("\n%s%s: %s",
newLineSpacing,
formatter.BoldUnderlineStyle.Render("ROLLBACK"),
stream.rollbackStatus,
)
}
if stream.healthcheckStatus != "" {
output += fmt.Sprintf("\n%s%s: %s",
newLineSpacing,
formatter.BoldUnderlineStyle.Render("HEALTHCHECK"),
wrapHealthstatus(m, stream, newLineSpacing),
)
}
if stream.ErrStatus != "" {
output += fmt.Sprintf("\n%s%s: %s",
newLineSpacing,
formatter.BoldUnderlineStyle.Render("ERROR"),
stream.ErrStatus,
)
}
body.WriteString(output) body.WriteString(output)
body.WriteString("\n") body.WriteString("\n")
} }
return body.String() return body.String()
} }
// wrapHealthstatus wraps the health check output which is mostly quite long.
func wrapHealthstatus(m Model, s stream, newLineSpacing string) string {
// NOTE(d1): the spacing here represents "HEALTCHECK: ". 20 is an arbitrary
// padding chosen in the hope of not overrunning horizontal space.
newLineSpacingWithIndent := newLineSpacing + " "
firstWrap := wordwrap.String(
s.healthcheckStatus,
((m.width - len(newLineSpacingWithIndent)) - 20),
)
var finalWrap string
for idx, line := range strings.Split(firstWrap, "\n") {
if idx == 0 {
finalWrap = line
continue
}
finalWrap += fmt.Sprintf("\n%s%s", newLineSpacingWithIndent, line)
}
return finalWrap
}

View File

@ -9,14 +9,14 @@ import (
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container"
apiclient "github.com/docker/docker/client" apiclient "github.com/docker/docker/client"
) )
// 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
// command output. // command output.
func RunExec(dockerCli command.Cli, client *apiclient.Client, containerID string, func RunExec(dockerCli command.Cli, client *apiclient.Client, containerID string,
execConfig *types.ExecConfig) (io.Writer, error) { execOptions *container.ExecOptions) (io.Writer, error) {
ctx := context.Background() ctx := context.Background()
// We need to check the tty _before_ we do the ContainerExecCreate, because // We need to check the tty _before_ we do the ContainerExecCreate, because
@ -26,13 +26,13 @@ func RunExec(dockerCli command.Cli, client *apiclient.Client, containerID string
if _, err := client.ContainerInspect(ctx, containerID); err != nil { if _, err := client.ContainerInspect(ctx, containerID); err != nil {
return nil, err return nil, err
} }
if !execConfig.Detach { if !execOptions.Detach {
if err := dockerCli.In().CheckTty(execConfig.AttachStdin, execConfig.Tty); err != nil { if err := dockerCli.In().CheckTty(execOptions.AttachStdin, execOptions.Tty); err != nil {
return nil, err return nil, err
} }
} }
response, err := client.ContainerExecCreate(ctx, containerID, *execConfig) response, err := client.ContainerExecCreate(ctx, containerID, *execOptions)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -42,40 +42,40 @@ func RunExec(dockerCli command.Cli, client *apiclient.Client, containerID string
return nil, errors.New("exec ID empty") return nil, errors.New("exec ID empty")
} }
if execConfig.Detach { if execOptions.Detach {
execStartCheck := types.ExecStartCheck{ execStartCheck := container.ExecStartOptions{
Detach: execConfig.Detach, Detach: execOptions.Detach,
Tty: execConfig.Tty, Tty: execOptions.Tty,
} }
return nil, client.ContainerExecStart(ctx, execID, execStartCheck) return nil, client.ContainerExecStart(ctx, execID, execStartCheck)
} }
return interactiveExec(ctx, dockerCli, client, execConfig, execID) return interactiveExec(ctx, dockerCli, client, execOptions, execID)
} }
func interactiveExec(ctx context.Context, dockerCli command.Cli, client *apiclient.Client, func interactiveExec(ctx context.Context, dockerCli command.Cli, client *apiclient.Client,
execConfig *types.ExecConfig, execID string) (io.Writer, error) { execOpts *container.ExecOptions, execID string) (io.Writer, error) {
// Interactive exec requested. // Interactive exec requested.
var ( var (
out, stderr io.Writer out, stderr io.Writer
in io.ReadCloser in io.ReadCloser
) )
if execConfig.AttachStdin { if execOpts.AttachStdin {
in = dockerCli.In() in = dockerCli.In()
} }
if execConfig.AttachStdout { if execOpts.AttachStdout {
out = dockerCli.Out() out = dockerCli.Out()
} }
if execConfig.AttachStderr { if execOpts.AttachStderr {
if execConfig.Tty { if execOpts.Tty {
stderr = dockerCli.Out() stderr = dockerCli.Out()
} else { } else {
stderr = dockerCli.Err() stderr = dockerCli.Err()
} }
} }
execStartCheck := types.ExecStartCheck{ execStartCheck := container.ExecStartOptions{
Tty: execConfig.Tty, Tty: execOpts.Tty,
} }
resp, err := client.ContainerExecAttach(ctx, execID, execStartCheck) resp, err := client.ContainerExecAttach(ctx, execID, execStartCheck)
if err != nil { if err != nil {
@ -94,15 +94,15 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, client *apiclie
outputStream: out, outputStream: out,
errorStream: stderr, errorStream: stderr,
resp: resp, resp: resp,
tty: execConfig.Tty, tty: execOpts.Tty,
detachKeys: execConfig.DetachKeys, detachKeys: execOpts.DetachKeys,
} }
return streamer.stream(ctx) return streamer.stream(ctx)
}() }()
}() }()
if execConfig.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(), "Error monitoring TTY size:", err)
} }

View File

@ -5,7 +5,6 @@ import (
"strings" "strings"
composetypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types"
networktypes "github.com/docker/docker/api/types/network" networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
) )
@ -52,13 +51,13 @@ func AddStackLabel(namespace Namespace, labels map[string]string) map[string]str
type networkMap map[string]composetypes.NetworkConfig type networkMap map[string]composetypes.NetworkConfig
// Networks from the compose-file type to the engine API type // Networks from the compose-file type to the engine API type
func Networks(namespace Namespace, networks networkMap, servicesNetworks map[string]struct{}) (map[string]types.NetworkCreate, []string) { func Networks(namespace Namespace, networks networkMap, servicesNetworks map[string]struct{}) (map[string]networktypes.CreateOptions, []string) {
if networks == nil { if networks == nil {
networks = make(map[string]composetypes.NetworkConfig) networks = make(map[string]composetypes.NetworkConfig)
} }
externalNetworks := []string{} externalNetworks := []string{}
result := make(map[string]types.NetworkCreate) result := make(map[string]networktypes.CreateOptions)
for internalName := range servicesNetworks { for internalName := range servicesNetworks {
network := networks[internalName] network := networks[internalName]
if network.External.External { if network.External.External {
@ -66,7 +65,7 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str
continue continue
} }
createOpts := types.NetworkCreate{ createOpts := networktypes.CreateOptions{
Labels: AddStackLabel(namespace, network.Labels), Labels: AddStackLabel(namespace, network.Labels),
Driver: network.Driver, Driver: network.Driver,
Options: network.DriverOpts, Options: network.DriverOpts,

View File

@ -4,7 +4,6 @@ import (
"testing" "testing"
composetypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
@ -67,7 +66,7 @@ func TestNetworks(t *testing.T) {
Name: "othername", Name: "othername",
}, },
} }
expected := map[string]types.NetworkCreate{ expected := map[string]network.CreateOptions{
"foo_default": { "foo_default": {
Labels: map[string]string{ Labels: map[string]string{
LabelNamespace: "foo", LabelNamespace: "foo",

View File

@ -3,11 +3,15 @@ package stack // https://github.com/docker/cli/blob/master/cli/command/stack/rem
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"os/signal"
"sort" "sort"
"strings" "strings"
"time"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions" "github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client" "github.com/docker/docker/client"
@ -17,57 +21,87 @@ import (
// RunRemove is the swarm implementation of docker stack remove // RunRemove is the swarm implementation of docker stack remove
func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error { func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error {
var errs []string sigIntCh := make(chan os.Signal, 1)
for _, namespace := range opts.Namespaces { signal.Notify(sigIntCh, os.Interrupt)
services, err := GetStackServices(ctx, client, namespace) defer signal.Stop(sigIntCh)
if err != nil {
return err
}
networks, err := getStackNetworks(ctx, client, namespace) waitCh := make(chan struct{})
if err != nil { errCh := make(chan error)
return err
}
var secrets []swarm.Secret go func() {
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.25") { var errs []string
secrets, err = getStackSecrets(ctx, client, namespace) for _, namespace := range opts.Namespaces {
services, err := GetStackServices(ctx, client, namespace)
if err != nil { if err != nil {
return err errCh <- err
return
}
networks, err := getStackNetworks(ctx, client, namespace)
if err != nil {
errCh <- err
return
}
var secrets []swarm.Secret
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.25") {
secrets, err = getStackSecrets(ctx, client, namespace)
if err != nil {
errCh <- err
return
}
}
var configs []swarm.Config
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.30") {
configs, err = getStackConfigs(ctx, client, namespace)
if err != nil {
errCh <- err
return
}
}
if len(services)+len(networks)+len(secrets)+len(configs) == 0 {
log.Warnf("nothing found in stack: %s", namespace)
continue
}
hasError := removeServices(ctx, client, services)
hasError = removeSecrets(ctx, client, secrets) || hasError
hasError = removeConfigs(ctx, client, configs) || hasError
hasError = removeNetworks(ctx, client, networks) || hasError
if hasError {
errs = append(errs, fmt.Sprintf("failed to remove some resources from stack: %s", namespace))
continue
}
log.Info("polling undeploy status")
timeout, err := waitOnTasks(ctx, client, namespace)
if timeout {
errs = append(errs, err.Error())
} else {
if err != nil {
errs = append(errs, fmt.Sprintf("failed to wait on tasks of stack: %s: %s", namespace, err))
}
} }
} }
var configs []swarm.Config if len(errs) > 0 {
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.30") { errCh <- errors.Errorf(strings.Join(errs, "\n"))
configs, err = getStackConfigs(ctx, client, namespace) return
if err != nil {
return err
}
} }
if len(services)+len(networks)+len(secrets)+len(configs) == 0 { close(waitCh)
log.Warnf("nothing found in stack: %s", namespace) }()
continue
}
hasError := removeServices(ctx, client, services) select {
hasError = removeSecrets(ctx, client, secrets) || hasError case <-waitCh:
hasError = removeConfigs(ctx, client, configs) || hasError return nil
hasError = removeNetworks(ctx, client, networks) || hasError case <-sigIntCh:
return fmt.Errorf("skipping as requested, undeploy still in progress 🟠")
if hasError { case err := <-errCh:
errs = append(errs, fmt.Sprintf("failed to remove some resources from stack: %s", namespace)) return err
continue
}
err = waitOnTasks(ctx, client, namespace)
if err != nil {
errs = append(errs, fmt.Sprintf("failed to wait on tasks of stack: %s: %s", namespace, err))
}
}
if len(errs) > 0 {
return errors.Errorf(strings.Join(errs, "\n"))
} }
return nil return nil
@ -99,7 +133,7 @@ func removeServices(
func removeNetworks( func removeNetworks(
ctx context.Context, ctx context.Context,
client *apiclient.Client, client *apiclient.Client,
networks []types.NetworkResource, networks []network.Inspect,
) bool { ) bool {
var hasError bool var hasError bool
for _, network := range networks { for _, network := range networks {
@ -169,12 +203,23 @@ func terminalState(state swarm.TaskState) bool {
return numberedStates[state] > numberedStates[swarm.TaskStateRunning] return numberedStates[state] > numberedStates[swarm.TaskStateRunning]
} }
func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace string) error { func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace string) (bool, error) {
var timedOut bool
log.Debugf("waiting on undeploy tasks (timeout=%v secs)", WaitTimeout)
go func() {
t := time.Duration(WaitTimeout) * time.Second
<-time.After(t)
log.Debug("timed out on undeploy")
timedOut = true
}()
terminalStatesReached := 0 terminalStatesReached := 0
for { for {
tasks, err := getStackTasks(ctx, client, namespace) tasks, err := getStackTasks(ctx, client, namespace)
if err != nil { if err != nil {
return fmt.Errorf("failed to get tasks: %w", err) return false, fmt.Errorf("failed to get tasks: %w", err)
} }
for _, task := range tasks { for _, task := range tasks {
@ -187,6 +232,11 @@ func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace stri
if terminalStatesReached == len(tasks) { if terminalStatesReached == len(tasks) {
break break
} }
if timedOut {
return true, fmt.Errorf("deployment timed out 🟠")
}
} }
return nil
return false, nil
} }

View File

@ -17,11 +17,13 @@ import (
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/ui" "coopcloud.tech/abra/pkg/ui"
"coopcloud.tech/abra/pkg/upstream/convert" "coopcloud.tech/abra/pkg/upstream/convert"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/stack/formatter" "github.com/docker/cli/cli/command/stack/formatter"
composetypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions" "github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client" "github.com/docker/docker/client"
@ -197,6 +199,7 @@ func RunDeploy(
appName string, appName string,
serverName string, serverName string,
dontWait bool, dontWait bool,
filters filters.Args,
) error { ) error {
log.Info("initialising deployment") log.Info("initialising deployment")
@ -209,7 +212,16 @@ func RunDeploy(
opts.ResolveImage = ResolveImageNever opts.ResolveImage = ResolveImageNever
} }
return deployCompose(context.Background(), cl, opts, cfg, appName, serverName, dontWait) return deployCompose(
context.Background(),
cl,
opts,
cfg,
appName,
serverName,
dontWait,
filters,
)
} }
// validateResolveImageFlag validates the opts.resolveImage command line option // validateResolveImageFlag validates the opts.resolveImage command line option
@ -230,6 +242,7 @@ func deployCompose(
appName string, appName string,
serverName string, serverName string,
dontWait bool, dontWait bool,
filters filters.Args,
) error { ) error {
namespace := convert.NewNamespace(opts.Namespace) namespace := convert.NewNamespace(opts.Namespace)
@ -288,13 +301,14 @@ func deployCompose(
return nil return nil
} }
if err := WaitOnServices( waitOpts := WaitOpts{
ctx, Services: serviceIDs,
cl, AppName: appName,
serviceIDs, ServerName: serverName,
appName, Filters: filters,
serverName, }
); err != nil {
if err := WaitOnServices(ctx, cl, waitOpts); err != nil {
return err return err
} }
@ -322,7 +336,7 @@ func validateExternalNetworks(ctx context.Context, client dockerClient.NetworkAP
// local-scoped networks, so there's no need to inspect them. // local-scoped networks, so there's no need to inspect them.
continue continue
} }
network, err := client.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{}) network, err := client.NetworkInspect(ctx, networkName, networktypes.InspectOptions{})
switch { switch {
case dockerClient.IsErrNotFound(err): case dockerClient.IsErrNotFound(err):
return errors.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed, which you can do by running this on the server: docker network create -d overlay proxy", networkName) return errors.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed, which you can do by running this on the server: docker network create -d overlay proxy", networkName)
@ -379,13 +393,13 @@ func createConfigs(ctx context.Context, cl *dockerClient.Client, configs []swarm
return nil return nil
} }
func createNetworks(ctx context.Context, cl *dockerClient.Client, namespace convert.Namespace, networks map[string]types.NetworkCreate) error { func createNetworks(ctx context.Context, cl *dockerClient.Client, namespace convert.Namespace, networks map[string]networktypes.CreateOptions) error {
existingNetworks, err := getStackNetworks(ctx, cl, namespace.Name()) existingNetworks, err := getStackNetworks(ctx, cl, namespace.Name())
if err != nil { if err != nil {
return err return err
} }
existingNetworkMap := make(map[string]types.NetworkResource) existingNetworkMap := make(map[string]networktypes.Inspect)
for _, network := range existingNetworks { for _, network := range existingNetworks {
existingNetworkMap[network.Name] = network existingNetworkMap[network.Name] = network
} }
@ -413,7 +427,8 @@ func deployServices(
services map[string]swarm.ServiceSpec, services map[string]swarm.ServiceSpec,
namespace convert.Namespace, namespace convert.Namespace,
sendAuth bool, sendAuth bool,
resolveImage string) ([]ui.ServiceMeta, error) { resolveImage string,
) ([]ui.ServiceMeta, error) {
var servicesMeta []ui.ServiceMeta var servicesMeta []ui.ServiceMeta
existingServices, err := GetStackServices(ctx, cl, namespace.Name()) existingServices, err := GetStackServices(ctx, cl, namespace.Name())
@ -433,6 +448,21 @@ func deployServices(
encodedAuth string encodedAuth string
) )
// When sendAuth is set, use the docker cli to retrieve the auth token
// for the image we are deploying.
// This enables using a private registry by running docker login on the
// machine, that abra is executed.
if sendAuth {
dockerCLI, err := command.NewDockerCli()
if err != nil {
log.Errorf("retrieving docker auth token: failed create docker cli: %s", err)
}
encodedAuth, err = command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), image)
if err != nil {
log.Errorf("failed to retrieve registry auth for image %s: %s", image, err)
}
}
if service, exists := existingServiceMap[name]; exists { if service, exists := existingServiceMap[name]; exists {
log.Debugf("updating %s", name) log.Debugf("updating %s", name)
@ -505,8 +535,8 @@ func deployServices(
return servicesMeta, nil return servicesMeta, nil
} }
func getStackNetworks(ctx context.Context, dockerclient client.APIClient, namespace string) ([]types.NetworkResource, error) { func getStackNetworks(ctx context.Context, dockerclient client.APIClient, namespace string) ([]networktypes.Inspect, error) {
return dockerclient.NetworkList(ctx, types.NetworkListOptions{Filters: getStackFilter(namespace)}) return dockerclient.NetworkList(ctx, networktypes.ListOptions{Filters: getStackFilter(namespace)})
} }
func getStackSecrets(ctx context.Context, dockerclient client.APIClient, namespace string) ([]swarm.Secret, error) { func getStackSecrets(ctx context.Context, dockerclient client.APIClient, namespace string) ([]swarm.Secret, error) {
@ -522,64 +552,83 @@ func timestamp() string {
return strings.Replace(ts, ":", "", -1) // get rid of offensive colons return strings.Replace(ts, ":", "", -1) // get rid of offensive colons
} }
func WaitOnServices( type WaitOpts struct {
ctx context.Context, AppName string
cl *dockerClient.Client, Filters filters.Args
services []ui.ServiceMeta, NoLog bool
appName string, Quiet bool
serverName string, ServerName string
) error { Services []ui.ServiceMeta
log.Info("polling deployment status") }
func WaitOnServices(ctx context.Context, cl *dockerClient.Client, opts WaitOpts) error {
timeout := time.Duration(WaitTimeout) * time.Second timeout := time.Duration(WaitTimeout) * time.Second
model := ui.DeployInitialModel(ctx, cl, services, appName, timeout) model := ui.DeployInitialModel(ctx, cl, opts.Services, opts.AppName, timeout, opts.Filters)
tui := tea.NewProgram(model) tui := tea.NewProgram(model)
m, err := tui.Run() if !opts.Quiet {
log.Info("polling deployment status")
}
m, err := log.Without(
func() (tea.Model, error) {
return tui.Run()
},
)
if err != nil { if err != nil {
return err return fmt.Errorf("waitOnServices: error running TUI: %s", err)
} }
deployModel := m.(ui.Model) deployModel := m.(ui.Model)
if deployModel.TimedOut || deployModel.Failed || deployModel.Quit {
logsPath := filepath.Join(
config.LOGS_DIR,
serverName,
fmt.Sprintf("%s_%s", appName, timestamp()),
)
if err := os.MkdirAll(filepath.Join(config.LOGS_DIR, serverName), 0644); err != nil {
// TODO
log.Fatalf("MkdirAll: %s", err)
}
if deployModel.Timeout {
if err := os.WriteFile(logsPath, deployModel.LogsBuffer.Bytes(), 0644); err != nil {
// TODO
log.Fatalf("WriteFile: %s", err)
}
return fmt.Errorf("deployment timed out, logs in %s", logsPath)
}
if deployModel.Failed {
var errs []error var errs []error
if deployModel.Failed {
errs = append(errs, fmt.Errorf("deploy failed 🛑"))
} else if deployModel.TimedOut {
errs = append(errs, fmt.Errorf("deploy timed out 🟠"))
} else {
errs = append(errs, fmt.Errorf("deploy in progress 🟠"))
}
for _, s := range *deployModel.Streams { for _, s := range *deployModel.Streams {
if s.Err != nil { if s.Err != nil {
errs = append(errs, fmt.Errorf("%s: %s", s.Name, s.ErrStatus)) errs = append(errs, fmt.Errorf("%s: %s", s.Name, s.Err))
} }
} }
if err := os.WriteFile(logsPath, deployModel.LogsBuffer.Bytes(), 0644); err != nil { if len(*deployModel.Logs) > 0 && !opts.NoLog {
// TODO logsPath := filepath.Join(
log.Fatalf("WriteFile: %s", err) config.LOGS_DIR,
opts.ServerName,
fmt.Sprintf("%s_%s", opts.AppName, timestamp()),
)
if err := os.MkdirAll(filepath.Join(config.LOGS_DIR, opts.ServerName), 0o764); err != nil {
return fmt.Errorf("waitOnServices: error creating log dir: %s", err)
}
file, err := os.Create(logsPath)
if err != nil {
return fmt.Errorf("waitOnServices: error opening file: %s", err)
}
defer file.Close()
s := strings.Join(*deployModel.Logs, "\n")
if _, err := file.WriteString(s); err != nil {
return fmt.Errorf("waitOnServices: writeFile: %s", err)
}
errs = append(errs, fmt.Errorf("logs: %s", logsPath))
} }
errs = append(errs, fmt.Errorf("deployment failed, logs in %s", logsPath))
return stdlibErr.Join(errs...) return stdlibErr.Join(errs...)
} }
if !opts.Quiet {
log.Info("deploy succeeded 🟢")
}
return nil return nil
} }

View File

@ -1,8 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
ABRA_VERSION="0.9.0-beta" ABRA_VERSION="0.10.1-beta"
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/toolshed/abra/releases/tags/$ABRA_VERSION" ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/toolshed/abra/releases/tags/$ABRA_VERSION"
RC_VERSION="0.10.0-rc1-beta" RC_VERSION="0.10.1-beta"
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/toolshed/abra/releases/tags/$RC_VERSION" RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/toolshed/abra/releases/tags/$RC_VERSION"
for arg in "$@"; do for arg in "$@"; do

View File

@ -3,5 +3,5 @@ STACK := abra_installer_script
default: deploy default: deploy
deploy: deploy:
@DOCKER_CONTEXT=swarm.autonomic.zone docker stack rm $(STACK) && \ @DOCKER_CONTEXT=swarm-0.coopcloud.tech docker stack rm $(STACK) && \
DOCKER_CONTEXT=swarm.autonomic.zone docker stack deploy -c compose.yml $(STACK) DOCKER_CONTEXT=swarm-0.coopcloud.tech docker stack deploy -c compose.yml $(STACK)

View File

@ -22,6 +22,8 @@ setup(){
teardown(){ teardown(){
_reset_recipe _reset_recipe
_undeploy_app _undeploy_app
_undeploy_app2 "gitea.$TEST_SERVER"
_reset_app _reset_app
_reset_tags _reset_tags
@ -73,6 +75,45 @@ teardown(){
assert_success assert_success
} }
# bats test_tags=slow
@test "bail if recipe lint errors and no --chaos" {
# Break the recipe
run sed -i '/traefik.enable=.*/d' "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"
assert_success
# Commit the breakage (so we can test without --chaos)
_set_git_author
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" commit -a -m 'Break recipe'
assert_success
# Make a broken release
run $ABRA recipe sync --patch "$TEST_RECIPE"
run $ABRA recipe release --patch -n "$TEST_RECIPE"
# Make sure we deploy latest
_wipe_env_version
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
assert_failure
assert_output --partial 'failed lint checks'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~1
latestRelease=$(_latest_release)
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -d "$latestRelease"
}
# bats test_tags=slow
@test "warn on recipe lint errors with --chaos" {
# Break the recipe
run sed -i '/traefik.enable=.*/d' "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks --chaos
assert_success
assert_output --partial 'failed lint checks'
}
# bats test_tags=slow # bats test_tags=slow
@test "ensure recipe up to date if no --offline" { @test "ensure recipe up to date if no --offline" {
wantHash=$(_get_n_hash 3) wantHash=$(_get_n_hash 3)
@ -106,8 +147,6 @@ teardown(){
# bats test_tags=slow # bats test_tags=slow
@test "deploy latest commit if no published versions and no --chaos" { @test "deploy latest commit if no published versions and no --chaos" {
latestCommit="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)"
_remove_tags _remove_tags
_wipe_env_version _wipe_env_version
@ -115,7 +154,7 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --offline --no-input --no-converge-checks --offline
assert_success assert_success
assert_output --partial "$latestCommit" assert_output --partial "${_get_head_hash:0:8}"
} }
# bats test_tags=slow # bats test_tags=slow
@ -147,16 +186,6 @@ teardown(){
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE" assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
} }
@test "no deploy if lint error" {
run sed -i '/traefik.enable=.*/d' "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_failure
assert_output --partial 'failed lint checks'
}
# bats test_tags=slow # bats test_tags=slow
@test "error if already deployed and no --force/--chaos" { @test "error if already deployed and no --force/--chaos" {
_deploy_app _deploy_app
@ -222,19 +251,6 @@ teardown(){
run $ABRA app deploy "gitea.$TEST_SERVER" --no-input --no-converge-checks run $ABRA app deploy "gitea.$TEST_SERVER" --no-input --no-converge-checks
assert_success assert_success
assert_output --partial "$latestVersion" assert_output --partial "$latestVersion"
run $ABRA app undeploy "gitea.$TEST_SERVER" --no-input
assert_success
run $ABRA app secret remove "gitea.$TEST_SERVER" --all --no-input
assert_success
run $ABRA app volume remove "gitea.$TEST_SERVER" --no-input
assert_success
run $ABRA app remove "gitea.$TEST_SERVER" --no-input
assert_success
assert_not_exists "$ABRA_DIR/servers/$TEST_SERVER/gitea.$TEST_SERVER.env"
} }
# bats test_tags=slow # bats test_tags=slow
@ -414,8 +430,6 @@ teardown(){
# bats test_tags=slow # bats test_tags=slow
@test "ignore env version on new deploy" { @test "ignore env version on new deploy" {
tagHash=$(_get_tag_hash "0.1.0+1.20.0")
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \ run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
--no-input --no-converge-checks --no-input --no-converge-checks
assert_success assert_success

View File

@ -37,17 +37,10 @@ teardown(){
--no-input --no-converge-checks --no-input --no-converge-checks
assert_success assert_success
# current deployment assert_output --partial 'NEW DEPLOY OVERVIEW'
assert_output --regexp 'VERSION.*N/A' assert_output --partial 'CURRENT DEPLOYMENT N/A'
assert_output --regexp 'CHAOS.*false' assert_output --partial 'ENV VERSION N/A'
assert_output --partial "NEW DEPLOYMENT ${latestRelease}"
# new deployment
assert_output --regexp 'VERSION.* ' + "${latestRelease}"
assert_output --regexp 'CHAOS.*false'
# env version
assert_output --regexp 'CURRENT VERSION.*N/A'
assert_output --regexp 'NEW VERSION.*' + "${latestRelease}"
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \ run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
@ -61,17 +54,10 @@ teardown(){
--no-input --no-converge-checks --no-input --no-converge-checks
assert_success assert_success
# current deployment assert_output --partial 'NEW DEPLOY OVERVIEW'
assert_output --regexp 'VERSION.*N/A' assert_output --partial "CURRENT DEPLOYMENT N/A"
assert_output --regexp 'CHAOS.*false' assert_output --partial "ENV VERSION ${latestRelease}"
assert_output --partial "NEW DEPLOYMENT ${latestRelease}"
# new deployment
assert_output --regexp 'VERSION.* ' + "${latestRelease}"
assert_output --regexp 'CHAOS.*false'
# env version
assert_output --regexp 'CURRENT VERSION.*' + "${latestRelease}"
assert_output --regexp 'NEW VERSION.*' + "${latestRelease}"
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \ run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
@ -90,17 +76,10 @@ teardown(){
--no-input --no-converge-checks --no-input --no-converge-checks
assert_success assert_success
# current deployment assert_output --partial 'NEW DEPLOY OVERVIEW'
assert_output --regexp 'VERSION.*N/A' assert_output --partial "CURRENT DEPLOYMENT N/A"
assert_output --regexp 'CHAOS.*false' assert_output --partial "ENV VERSION 0.1.1+1.20.2"
assert_output --partial "NEW DEPLOYMENT 0.1.1+1.20.2"
# new deployment
assert_output --regexp 'VERSION.*' + "0.1.1+1.20.2"
assert_output --regexp 'CHAOS.*false'
# env version
assert_output --regexp 'CURRENT VERSION.*' + "0.1.1+1.20.2"
assert_output --regexp 'NEW VERSION.*' + "0.1.1+1.20.2"
run grep -q "TYPE=$TEST_RECIPE:0.1.1+1.20.2" \ run grep -q "TYPE=$TEST_RECIPE:0.1.1+1.20.2" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
@ -120,17 +99,10 @@ teardown(){
--no-input --no-converge-checks --ignore-env-version --no-input --no-converge-checks --ignore-env-version
assert_success assert_success
# current deployment assert_output --partial 'NEW DEPLOY OVERVIEW'
assert_output --regexp 'VERSION.*N/A' assert_output --partial "CURRENT DEPLOYMENT N/A"
assert_output --regexp 'CHAOS.*false' assert_output --partial "ENV VERSION 0.1.1+1.20.2"
assert_output --partial "NEW DEPLOYMENT ${latestRelease}"
# new deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*false'
# env version
assert_output --regexp 'CURRENT VERSION.*' + "0.1.1+1.20.2"
assert_output --regexp 'NEW VERSION.*' + "${latestRelease}"
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \ run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
@ -153,17 +125,10 @@ teardown(){
--no-input --no-converge-checks --chaos --no-input --no-converge-checks --chaos
assert_success assert_success
# current deployment assert_output --partial 'CHAOS DEPLOY OVERVIEW'
assert_output --regexp 'VERSION.*' + "${latestRelease}" assert_output --partial "CURRENT DEPLOYMENT ${latestRelease}"
assert_output --regexp 'CHAOS.*false' assert_output --partial "ENV VERSION ${latestRelease}"
assert_output --partial "NEW DEPLOYMENT ${headHash:0:8}+U"
# new deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*' + "${headHash:0:8}+U"
# env version
assert_output --regexp 'CURRENT VERSION.*' + "${latestRelease}"
assert_output --regexp 'NEW VERSION.*' + "${headHash:0:8}+U"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo" run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo" assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
@ -173,7 +138,7 @@ teardown(){
assert_success assert_success
} }
@test "chaos deploy then force deploy" { @test "can not redeploy chaos version without --chaos" {
headHash=$(_get_head_hash) headHash=$(_get_head_hash)
latestRelease=$(_latest_release) latestRelease=$(_latest_release)
@ -189,27 +154,12 @@ teardown(){
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo" assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --force --no-input --no-converge-checks --force --debug
assert_success assert_failure
assert_output --regexp 'can not redeploy chaos version .*' + "${headHash:0:8}+U"
# current deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*' + "${headHash:0:8}+U"
# new deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*false'
# env version
assert_output --regexp 'CURRENT VERSION.*' + "${headHash:0:8}+U"
assert_output --regexp 'NEW VERSION.*' + "${latestRelease}"
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
} }
@test "deploy then force chaos commit deploy" { @test "deploy then force commit deploy" {
headHash=$(_get_head_hash) headHash=$(_get_head_hash)
latestRelease=$(_latest_release) latestRelease=$(_latest_release)
@ -225,17 +175,10 @@ teardown(){
--no-input --no-converge-checks --force --no-input --no-converge-checks --force
assert_success assert_success
# current deployment assert_output --partial 'DEPLOY OVERVIEW'
assert_output --regexp 'VERSION.*' + "${latestRelease}" assert_output --partial "CURRENT DEPLOYMENT ${latestRelease}"
assert_output --regexp 'CHAOS.*false' assert_output --partial "ENV VERSION ${latestRelease}"
assert_output --partial "NEW DEPLOYMENT ${headHash:0:8}"
# new deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*' + "${headHash:0:8}"
# env version
assert_output --regexp 'CURRENT VERSION.*' + "${latestRelease}"
assert_output --regexp 'NEW VERSION.*' + "${headHash:0:8}"
run grep -q "TYPE=$TEST_RECIPE:${headHash:0:8}" \ run grep -q "TYPE=$TEST_RECIPE:${headHash:0:8}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
@ -250,17 +193,10 @@ teardown(){
--no-input --no-converge-checks --chaos --no-input --no-converge-checks --chaos
assert_success assert_success
# current deployment assert_output --partial 'NEW DEPLOY OVERVIEW'
assert_output --regexp 'VERSION.*N/A' assert_output --partial "CURRENT DEPLOYMENT N/A"
assert_output --regexp 'CHAOS.*false' assert_output --partial "ENV VERSION ${latestRelease}"
assert_output --partial "NEW DEPLOYMENT ${headHash:0:8}"
# new deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*' + "${headHash:0:8}"
# env version
assert_output --regexp 'CURRENT VERSION.*' + "${latestRelease}"
assert_output --regexp 'NEW VERSION.*' + "${headHash:0:8}"
run bash -c 'echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"' run bash -c 'echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"'
assert_success assert_success
@ -270,17 +206,28 @@ teardown(){
--no-input --no-converge-checks --chaos --no-input --no-converge-checks --chaos
assert_success assert_success
# current deployment assert_output --partial 'CHAOS DEPLOY OVERVIEW'
assert_output --regexp 'VERSION.*' + "${latestRelease}" assert_output --partial "CURRENT DEPLOYMENT ${headHash:0:8}"
assert_output --regexp 'CHAOS.*' + "${headHash:0:8}" assert_output --partial "ENV VERSION ${headHash:0:8}"
assert_output --partial "NEW DEPLOYMENT ${headHash:0:8}+U"
# new deployment run $ABRA app deploy "$TEST_APP_DOMAIN" \
assert_output --regexp 'VERSION.*' + "${latestRelease}" --no-input --no-converge-checks --chaos
assert_output --regexp 'CHAOS.*' + "${headHash:0:8}+U" assert_success
# env version assert_output --partial 'CHAOS DEPLOY OVERVIEW'
assert_output --regexp 'CURRENT VERSION.*' + "${latestRelease}" assert_output --partial "CURRENT DEPLOYMENT ${headHash:0:8}+U"
assert_output --regexp 'NEW VERSION.*' + "${headHash:0:8}+U" assert_output --partial "ENV VERSION ${headHash:0:8}+U"
assert_output --partial "NEW DEPLOYMENT ${headHash:0:8}+U"
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
assert_output --partial 'CHAOS DEPLOY OVERVIEW'
assert_output --partial "CURRENT DEPLOYMENT ${headHash:0:8}+U"
assert_output --partial "ENV VERSION ${headHash:0:8}+U"
assert_output --partial "NEW DEPLOYMENT ${headHash:0:8}+U"
run grep -q "TYPE=$TEST_RECIPE:${headHash:0:8}" \ run grep -q "TYPE=$TEST_RECIPE:${headHash:0:8}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
@ -302,19 +249,8 @@ teardown(){
--no-input --no-converge-checks --force --no-input --no-converge-checks --force
assert_success assert_success
# current deployment assert_output --partial 'REDEPLOY OVERVIEW'
assert_output --regexp 'VERSION.*' + "${latestRelease}" assert_output --partial "CURRENT DEPLOYMENT ${headHash:0:8}"
assert_output --regexp 'CHAOS.*' + "${headHash:0:8}" assert_output --partial "ENV VERSION ${headHash:0:8}"
assert_output --partial "NEW DEPLOYMENT ${headHash:0:8}"
# new deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*false'
# env version
assert_output --regexp 'CURRENT VERSION.*' + "${headHash:0:8}"
assert_output --regexp 'NEW VERSION.*' + "${latestRelease}"
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
} }

View File

@ -8,6 +8,11 @@ setup_file(){
} }
teardown_file(){ teardown_file(){
if [[ ! -f "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" ]]; then
_new_app
fi
_undeploy_app
_rm_app _rm_app
_rm_server _rm_server
} }
@ -18,6 +23,11 @@ setup(){
} }
teardown(){ teardown(){
if [[ ! -f "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" ]]; then
_new_app
fi
_reset_recipe
_undeploy_app _undeploy_app
} }
@ -87,6 +97,10 @@ teardown(){
assert_success assert_success
refute_output --partial "$TEST_RECIPE" refute_output --partial "$TEST_RECIPE"
assert_output --partial "foo-recipe" assert_output --partial "foo-recipe"
run rm -rf "$ABRA_DIR/servers/foo.com"
assert_success
assert_not_exists "$ABRA_DIR/servers/foo.com"
} }
@test "output is machine readable" { @test "output is machine readable" {
@ -98,3 +112,54 @@ teardown(){
assert_output --partial "$expectedOutput" assert_output --partial "$expectedOutput"
} }
# bats test_tags=slow
@test "list with status fetches recipe" {
_deploy_app
run $ABRA app ls --status
assert_success
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE"
assert_success
run $ABRA app ls --status
assert_success
}
# bats test_tags=slow
@test "list with chaos version" {
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
run $ABRA app ls --status
assert_success
assert_output --partial "+U"
run rm -rf "$ABRA_DIR/servers/foo.com"
assert_success
assert_not_exists "$ABRA_DIR/servers/foo.com"
}
# bats test_tags=slow
@test "list does not fail if missing .env" {
_deploy_app
run $ABRA app ls --status
assert_success
run rm -rf "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
assert_not_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
output=$("$ABRA" app ls --server "$TEST_SERVER" --status --machine)
run diff \
<(jq -S "." <(echo "$output")) \
<(jq -S "." <(echo '{}'))
assert_success
}

View File

@ -214,8 +214,7 @@ teardown(){
--chaos --chaos
assert_success assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_output --partial "version: ${currentHash:0:8}" assert_output --partial "version: ${currentHash:0:8}+U"
assert_output --partial "chaos: ${currentHash:0:8}"
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo" assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo" assert_equal "$(_git_status)" "?? foo"
@ -242,8 +241,7 @@ teardown(){
--chaos --chaos
assert_success assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_output --partial "version: ${currentHash:0:8}" assert_output --partial "version: ${currentHash:0:8}+U"
assert_output --partial "chaos: ${currentHash:0:8}"
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo" assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo" assert_equal "$(_git_status)" "?? foo"
@ -252,3 +250,10 @@ teardown(){
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success assert_success
} }
@test "automatically select single server" {
# NOTE(d1): no --no-input required, single server available
run $ABRA app new "$TEST_RECIPE" --domain "$TEST_APP_DOMAIN"
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
}

View File

@ -19,8 +19,8 @@ setup(){
} }
teardown(){ teardown(){
_reset_app
_undeploy_app _undeploy_app
_reset_app
_reset_recipe _reset_recipe
} }
@ -153,7 +153,7 @@ teardown(){
} }
# bats test_tags=slow # bats test_tags=slow
@test "rollback chaos deployment" { @test "rollback chaos deployment is not possible" {
tagHash=$(_get_tag_hash "0.2.0+1.21.0") tagHash=$(_get_tag_hash "0.2.0+1.21.0")
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$tagHash" run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$tagHash"
assert_success assert_success
@ -163,17 +163,8 @@ teardown(){
assert_output --partial "${tagHash:0:8}" assert_output --partial "${tagHash:0:8}"
run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.1+1.20.2" --no-input --no-converge-checks run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.1+1.20.2" --no-input --no-converge-checks
assert_success assert_failure
assert_output --partial "0.1.1+1.20.2" assert_output --partial 'current deployment' + "${tagHash:0:8}" + 'is not a known version'
assert_output --partial "${tagHash:0:8}"
run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
assert_success
assert_output --partial "0.1.0+1.20.0"
tagHash=$(_get_tag_hash "0.1.1+1.20.2")
refute_output --partial "${tagHash:0:8}"
assert_output --partial "false"
} }
# bats test_tags=slow # bats test_tags=slow

View File

@ -24,7 +24,7 @@ teardown(){
_reset_recipe _reset_recipe
} }
@test "deploy then rollback" { @test "deploy then rollback" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \ run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks --no-input --no-converge-checks
assert_success assert_success
@ -33,23 +33,17 @@ teardown(){
--no-input --no-converge-checks --no-input --no-converge-checks
assert_success assert_success
# current deployment assert_output --partial 'DOWNGRADE OVERVIEW'
assert_output --regexp 'VERSION.*' + "0.2.0+1.21.0" assert_output --partial 'CURRENT DEPLOYMENT 0.2.0+1.21.0'
assert_output --regexp 'CHAOS.*false' assert_output --partial 'ENV VERSION 0.2.0+1.21.0'
assert_output --partial 'NEW DEPLOYMENT 0.1.0+1.20.0'
# rollback
assert_output --regexp 'VERSION.*' + "0.1.0+1.20.0"
# env version
assert_output --regexp 'CURRENT VERSION.*' + "0.2.0+1.21.0"
assert_output --regexp 'NEW VERSION.*' + "0.1.0+1.20.0"
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \ run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success assert_success
} }
@test "force rollback" { @test "force rollback" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \ run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks --no-input --no-converge-checks
assert_success assert_success
@ -58,16 +52,10 @@ teardown(){
--no-input --no-converge-checks --force --no-input --no-converge-checks --force
assert_success assert_success
# current deployment assert_output --partial 'REDEPLOY OVERVIEW'
assert_output --regexp 'VERSION.*' + "0.2.0+1.21.0" assert_output --partial 'CURRENT DEPLOYMENT 0.2.0+1.21.0'
assert_output --regexp 'CHAOS.*false' assert_output --partial 'ENV VERSION 0.2.0+1.21.0'
assert_output --partial 'NEW DEPLOYMENT 0.2.0+1.21.0'
# rollback
assert_output --regexp 'VERSION.*' + "0.2.0+1.21.0"
# env version
assert_output --regexp 'CURRENT VERSION.*' + "0.2.0+1.21.0"
assert_output --regexp 'NEW VERSION.*' + "0.2.0+1.21.0"
run grep -q "TYPE=$TEST_RECIPE:0.2.0+1.21.0" \ run grep -q "TYPE=$TEST_RECIPE:0.2.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
@ -85,16 +73,10 @@ teardown(){
--no-input --no-converge-checks --no-input --no-converge-checks
assert_success assert_success
# current deployment assert_output --partial 'DOWNGRADE OVERVIEW'
assert_output --regexp 'VERSION.*' + "0.2.0+1.21.0" assert_output --partial 'CURRENT DEPLOYMENT 0.2.0+1.21.0'
assert_output --regexp 'CHAOS.*false' assert_output --partial 'ENV VERSION N/A'
assert_output --partial 'NEW DEPLOYMENT 0.1.0+1.20.0'
# rollback
assert_output --regexp 'VERSION.*' + "0.1.0+1.20.0"
# env version
assert_output --regexp 'CURRENT VERSION.*N/A'
assert_output --regexp 'NEW VERSION.*' + "0.2.0+1.21.0"
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \ run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"

View File

@ -92,9 +92,6 @@ teardown(){
run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
assert_success assert_success
# NOTE(d1): ensure not chaos undeploy
assert_output --partial 'false'
} }
# bats test_tags=slow # bats test_tags=slow

View File

@ -33,13 +33,10 @@ teardown(){
run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
assert_success assert_success
# current deployment assert_output --partial 'UNDEPLOY OVERVIEW'
assert_output --regexp 'VERSION.*' + "0.1.0+1.20.0" assert_output --partial 'CURRENT DEPLOYMENT 0.1.0+1.20.0'
assert_output --regexp 'CHAOS.*false' assert_output --partial 'ENV VERSION 0.1.0+1.20.0'
assert_output --partial 'NEW DEPLOYMENT N/A'
# env version
assert_output --regexp 'CURRENT VERSION.*' + "0.1.0+1.20.0"
assert_output --regexp 'NEW VERSION.*' + "0.1.0+1.20.0"
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \ run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
@ -57,13 +54,10 @@ teardown(){
run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
assert_success assert_success
# current deployment assert_output --partial 'UNDEPLOY OVERVIEW'
assert_output --regexp 'VERSION.*' + "${latestRelease}" assert_output --partial "CURRENT DEPLOYMENT ${headHash:0:8}"
assert_output --regexp 'CHAOS.*' + "${headHash:0:8}" assert_output --partial "ENV VERSION ${headHash:0:8}"
assert_output --partial 'NEW DEPLOYMENT N/A'
# env version
assert_output --regexp 'CURRENT VERSION.*' + "${latestRelease}"
assert_output --regexp 'NEW VERSION.*' + "${headHash:0:8}"
run grep -q "TYPE=$TEST_RECIPE:${headHash:0:8}" \ run grep -q "TYPE=$TEST_RECIPE:${headHash:0:8}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
@ -72,7 +66,6 @@ teardown(){
@test "chaos deploy with unstaged commits and undeploy" { @test "chaos deploy with unstaged commits and undeploy" {
headHash=$(_get_head_hash) headHash=$(_get_head_hash)
latestRelease=$(_latest_release)
run bash -c 'echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"' run bash -c 'echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"'
assert_success assert_success
@ -85,13 +78,10 @@ teardown(){
run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
assert_success assert_success
# current deployment assert_output --partial 'UNDEPLOY OVERVIEW'
assert_output --regexp 'VERSION.*' + "${latestRelease}" assert_output --partial "CURRENT DEPLOYMENT ${headHash:0:8}+U"
assert_output --regexp 'CHAOS.*' + "${headHash:0:8}+U" assert_output --partial "ENV VERSION ${headHash:0:8}+U"
assert_output --partial 'NEW DEPLOYMENT N/A'
# env version
assert_output --regexp 'CURRENT VERSION.*' + "${latestRelease}"
assert_output --regexp 'NEW VERSION.*' + "${headHash:0:8}+U"
run grep -q "TYPE=$TEST_RECIPE:${headHash:0:8}" \ run grep -q "TYPE=$TEST_RECIPE:${headHash:0:8}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"

View File

@ -205,7 +205,19 @@ teardown(){
} }
# bats test_tags=slow # bats test_tags=slow
@test "upgrade chaos deployment" { @test "show release note added after release" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input --no-converge-checks
assert_success
assert_output --partial '0.2.0+1.21.0'
run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.3.0+1.21.0" --no-input --no-converge-checks
assert_success
assert_output --partial '0.3.0+1.21.0'
assert_output --partial 'A release note added after the release'
}
# bats test_tags=slow
@test "upgrade commit deployment not possible" {
tagHash=$(_get_tag_hash "0.1.0+1.20.0") tagHash=$(_get_tag_hash "0.1.0+1.20.0")
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$tagHash" run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$tagHash"
assert_success assert_success
@ -215,17 +227,8 @@ teardown(){
assert_output --partial "${tagHash:0:8}" assert_output --partial "${tagHash:0:8}"
run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.1.1+1.20.2" --no-input --no-converge-checks run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.1.1+1.20.2" --no-input --no-converge-checks
assert_success assert_failure
assert_output --partial "0.1.1+1.20.2" assert_output --partial "not a known version"
assert_output --partial "${tagHash:0:8}"
run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input --no-converge-checks
assert_success
assert_output --partial "0.2.0+1.21.0"
tagHash=$(_get_tag_hash "0.1.1+1.20.2")
refute_output --partial "${tagHash:0:8}"
assert_output --partial "false"
} }
@test "chaos commit upgrade not possible" { @test "chaos commit upgrade not possible" {

View File

@ -31,24 +31,17 @@ teardown(){
--no-input --no-converge-checks --no-input --no-converge-checks
assert_success assert_success
# current deployment assert_output --partial 'UPGRADE OVERVIEW'
assert_output --regexp 'VERSION.*' + "0.1.0+1.20.0" assert_output --partial 'CURRENT DEPLOYMENT 0.1.0+1.20.0'
assert_output --regexp 'CHAOS.*false' assert_output --partial 'ENV VERSION 0.1.0+1.20.0'
assert_output --partial 'NEW DEPLOYMENT 0.2.0+1.21.0'
# upgrade
assert_output --regexp 'VERSION.*' + "0.2.0+1.21.0"
assert_output --regexp 'CHAOS.*false'
# env version
assert_output --regexp 'CURRENT VERSION.*' + "0.1.0+1.20.0"
assert_output --regexp 'NEW VERSION.*' + "0.2.0+1.21.0"
run grep -q "TYPE=$TEST_RECIPE:0.2.0+1.21.0" \ run grep -q "TYPE=$TEST_RECIPE:0.2.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success assert_success
} }
@test "force upgrade" { @test "force upgrade" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \ run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks --no-input --no-converge-checks
assert_success assert_success
@ -57,17 +50,10 @@ teardown(){
--no-input --no-converge-checks --force --no-input --no-converge-checks --force
assert_success assert_success
# current deployment assert_output --partial 'REDEPLOY OVERVIEW'
assert_output --regexp 'VERSION.*' + "0.2.0+1.21.0" assert_output --partial 'CURRENT DEPLOYMENT 0.2.0+1.21.0'
assert_output --regexp 'CHAOS.*false' assert_output --partial 'ENV VERSION 0.2.0+1.21.0'
assert_output --partial 'NEW DEPLOYMENT 0.2.0+1.21.0'
# upgrade
assert_output --regexp 'VERSION.*' + "0.2.0+1.21.0"
assert_output --regexp 'CHAOS.*false'
# env version
assert_output --regexp 'CURRENT VERSION.*' + "0.2.0+1.21.0"
assert_output --regexp 'NEW VERSION.*' + "0.2.0+1.21.0"
run grep -q "TYPE=$TEST_RECIPE:0.2.0+1.21.0" \ run grep -q "TYPE=$TEST_RECIPE:0.2.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
@ -87,17 +73,10 @@ teardown(){
--no-input --no-converge-checks --force --no-input --no-converge-checks --force
assert_success assert_success
# current deployment assert_output --partial 'UPGRADE OVERVIEW'
assert_output --regexp 'VERSION.*' + "0.2.0+1.21.0" assert_output --partial 'CURRENT DEPLOYMENT 0.2.0+1.21.0'
assert_output --regexp 'CHAOS.*false' assert_output --partial 'ENV VERSION N/A'
assert_output --partial 'NEW DEPLOYMENT 0.3.1+1.21.0'
# upgrade
assert_output --regexp 'VERSION.*' + "0.2.0+1.21.0"
assert_output --regexp 'CHAOS.*false'
# env version
assert_output --regexp 'CURRENT VERSION.*N/A'
assert_output --regexp 'NEW VERSION.*' + "0.2.0+1.21.0"
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \ run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"

View File

@ -30,6 +30,15 @@ _undeploy_app() {
assert_output --partial 'unknown' assert_output --partial 'unknown'
} }
_undeploy_app2() {
run $ABRA app undeploy "$1" --no-input
run $ABRA app ls --server "$TEST_SERVER" --status
assert_success
assert_output --partial "$1"
assert_output --partial 'unknown'
}
_rm_app() { _rm_app() {
# NOTE(d1): not asserting outcomes on teardown here since some might fail # NOTE(d1): not asserting outcomes on teardown here since some might fail
# depending on what the test created. all commands run through anyway # depending on what the test created. all commands run through anyway

View File

@ -38,6 +38,8 @@ _set_git_author() {
} }
_git_commit() { _git_commit() {
_set_git_author
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" add . run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" add .
assert_success assert_success

View File

@ -25,13 +25,3 @@ teardown(){
run "$HOME/.local/bin/abra" -v run "$HOME/.local/bin/abra" -v
assert_output --partial 'beta' assert_output --partial 'beta'
} }
# bats test_tags=slow
@test "install release candidate from script" {
run bash -c 'curl https://install.abra.coopcloud.tech | bash -s -- --rc'
assert_success
assert_exists "$HOME/.local/bin/abra"
run "$HOME/.local/bin/abra" -v
assert_output --partial '-rc'
}

View File

@ -5,6 +5,16 @@ setup() {
_common_setup _common_setup
} }
teardown(){
run rm -rf "$ABRA_DIR/recipes/matrix-synapse"
assert_success
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
run rm -rf "$ABRA_DIR/recipes/git_coopcloud_tech_coop-cloud_matrix-synapse"
assert_success
assert_not_exists "$ABRA_DIR/recipes/git_coopcloud_tech_coop-cloud_matrix-synapse"
}
# bats test_tags=slow # bats test_tags=slow
@test "recipe fetch all" { @test "recipe fetch all" {
run rm -rf "$ABRA_DIR/recipes/matrix-synapse" run rm -rf "$ABRA_DIR/recipes/matrix-synapse"
@ -35,3 +45,81 @@ setup() {
run $ABRA recipe fetch matrix-synapse --all run $ABRA recipe fetch matrix-synapse --all
assert_failure assert_failure
} }
@test "do not refetch without --force" {
run $ABRA recipe fetch matrix-synapse
assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
run $ABRA recipe fetch matrix-synapse
assert_output --partial "already fetched"
}
@test "refetch with --force" {
run $ABRA recipe fetch matrix-synapse
assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
run $ABRA recipe fetch matrix-synapse --force
assert_success
refute_output --partial "already fetched"
}
@test "refetch with --force does not erase unstaged changes" {
run $ABRA recipe fetch matrix-synapse
assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
run bash -c "echo foo >> $ABRA_DIR/recipes/matrix-synapse/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse/foo"
run $ABRA recipe fetch matrix-synapse --force
assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
assert_exists "$ABRA_DIR/recipes/matrix-synapse/foo"
}
@test "fetch with --ssh" {
run $ABRA recipe fetch matrix-synapse --ssh
assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
run git -C "$ABRA_DIR/recipes/matrix-synapse" remote -v
assert_success
assert_output --partial "ssh://"
}
@test "re-fetch with --ssh/--force" {
run $ABRA recipe fetch matrix-synapse
assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
run git -C "$ABRA_DIR/recipes/matrix-synapse" remote -v
assert_success
assert_output --partial "https://"
run $ABRA recipe fetch matrix-synapse --ssh --force
assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
run git -C "$ABRA_DIR/recipes/matrix-synapse" remote -v
assert_success
assert_output --partial "ssh://"
}
@test "fetch remote recipe" {
run $ABRA recipe fetch git.coopcloud.tech/coop-cloud/matrix-synapse
assert_success
assert_exists "$ABRA_DIR/recipes/git_coopcloud_tech_coop-cloud_matrix-synapse"
}
@test "remote recipe do not refetch without --force" {
run $ABRA recipe fetch git.coopcloud.tech/coop-cloud/matrix-synapse
assert_success
assert_exists "$ABRA_DIR/recipes/git_coopcloud_tech_coop-cloud_matrix-synapse"
run $ABRA recipe fetch git.coopcloud.tech/coop-cloud/matrix-synapse
assert_success
assert_output --partial "already fetched"
}

View File

@ -68,3 +68,27 @@ teardown(){
assert_output --partial 'fooUser' assert_output --partial 'fooUser'
assert_output --partial 'foo@example.com' assert_output --partial 'foo@example.com'
} }
# bats test_tags=slow
@test "recipe new, app new, no releases, latest commit" {
recipeName="foobar"
run $ABRA recipe new "$recipeName"
assert_success
assert_exists "$ABRA_DIR/recipes/$recipeName"
currentHash=$(git -C "$ABRA_DIR/recipes/$recipeName" show -s --format="%H")
domain="$recipeName.$TEST_APP_SERVER"
run $ABRA app new "$recipeName" \
--no-input \
--server "$TEST_SERVER" \
--domain "$domain"
assert_success
assert_output --partial "version: ${currentHash:0:8}"
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$domain.env"
run grep -q "TYPE=$recipeName:${currentHash:0:8}" \
"$ABRA_DIR/servers/$TEST_SERVER/$domain.env"
assert_success
}

View File

@ -26,14 +26,3 @@ teardown(){
run "$HOME/.local/bin/abra" -v run "$HOME/.local/bin/abra" -v
assert_output --partial 'beta' assert_output --partial 'beta'
} }
# bats test_tags=slow
@test "abra upgrade release candidate" {
run $ABRA upgrade --rc
assert_success
assert_output --partial 'Public interest infrastructure'
assert_exists "$HOME/.local/bin/abra"
run "$HOME/.local/bin/abra" -v
assert_output --partial '-rc'
}

View File

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

7
vendor/dario.cat/mergo/FUNDING.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"drips": {
"ethereum": {
"ownedBy": "0x6160020e7102237aC41bdb156e94401692D76930"
}
}
}

View File

@ -85,7 +85,6 @@ Mergo is used by [thousands](https://deps.dev/go/dario.cat%2Fmergo/v1.0.0/depend
* [goreleaser/goreleaser](https://github.com/goreleaser/goreleaser) * [goreleaser/goreleaser](https://github.com/goreleaser/goreleaser)
* [go-micro/go-micro](https://github.com/go-micro/go-micro) * [go-micro/go-micro](https://github.com/go-micro/go-micro)
* [grafana/loki](https://github.com/grafana/loki) * [grafana/loki](https://github.com/grafana/loki)
* [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes)
* [masterminds/sprig](github.com/Masterminds/sprig) * [masterminds/sprig](github.com/Masterminds/sprig)
* [moby/moby](https://github.com/moby/moby) * [moby/moby](https://github.com/moby/moby)
* [slackhq/nebula](https://github.com/slackhq/nebula) * [slackhq/nebula](https://github.com/slackhq/nebula)
@ -191,10 +190,6 @@ func main() {
} }
``` ```
Note: if test are failing due missing package, please execute:
go get gopkg.in/yaml.v3
### Transformers ### Transformers
Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, `time.Time` is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero `time.Time`? Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, `time.Time` is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero `time.Time`?

View File

@ -4,8 +4,8 @@
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ------------------ |
| 0.3.x | :white_check_mark: | | 1.x.x | :white_check_mark: |
| < 0.3 | :x: | | < 1.0 | :x: |
## Security contact information ## Security contact information

View File

@ -3,7 +3,7 @@ reflection interface similar to Go's standard library `json` and `xml` packages.
Compatible with TOML version [v1.0.0](https://toml.io/en/v1.0.0). Compatible with TOML version [v1.0.0](https://toml.io/en/v1.0.0).
Documentation: https://godocs.io/github.com/BurntSushi/toml Documentation: https://pkg.go.dev/github.com/BurntSushi/toml
See the [releases page](https://github.com/BurntSushi/toml/releases) for a See the [releases page](https://github.com/BurntSushi/toml/releases) for a
changelog; this information is also in the git tag annotations (e.g. `git show changelog; this information is also in the git tag annotations (e.g. `git show

View File

@ -196,6 +196,19 @@ func (md *MetaData) PrimitiveDecode(primValue Primitive, v any) error {
return md.unify(primValue.undecoded, rvalue(v)) return md.unify(primValue.undecoded, rvalue(v))
} }
// markDecodedRecursive is a helper to mark any key under the given tmap as
// decoded, recursing as needed
func markDecodedRecursive(md *MetaData, tmap map[string]any) {
for key := range tmap {
md.decoded[md.context.add(key).String()] = struct{}{}
if tmap, ok := tmap[key].(map[string]any); ok {
md.context = append(md.context, key)
markDecodedRecursive(md, tmap)
md.context = md.context[0 : len(md.context)-1]
}
}
}
// unify performs a sort of type unification based on the structure of `rv`, // unify performs a sort of type unification based on the structure of `rv`,
// which is the client representation. // which is the client representation.
// //
@ -222,6 +235,16 @@ func (md *MetaData) unify(data any, rv reflect.Value) error {
if err != nil { if err != nil {
return md.parseErr(err) return md.parseErr(err)
} }
// Assume the Unmarshaler decoded everything, so mark all keys under
// this table as decoded.
if tmap, ok := data.(map[string]any); ok {
markDecodedRecursive(md, tmap)
}
if aot, ok := data.([]map[string]any); ok {
for _, tmap := range aot {
markDecodedRecursive(md, tmap)
}
}
return nil return nil
} }
if v, ok := rvi.(encoding.TextUnmarshaler); ok { if v, ok := rvi.(encoding.TextUnmarshaler); ok {
@ -540,12 +563,14 @@ func (md *MetaData) badtype(dst string, data any) error {
func (md *MetaData) parseErr(err error) error { func (md *MetaData) parseErr(err error) error {
k := md.context.String() k := md.context.String()
d := string(md.data)
return ParseError{ return ParseError{
LastKey: k, Message: err.Error(),
Position: md.keyInfo[k].pos,
Line: md.keyInfo[k].pos.Line,
err: err, err: err,
input: string(md.data), LastKey: k,
Position: md.keyInfo[k].pos.withCol(d),
Line: md.keyInfo[k].pos.Line,
input: d,
} }
} }

View File

@ -402,31 +402,30 @@ func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) {
// Sort keys so that we have deterministic output. And write keys directly // Sort keys so that we have deterministic output. And write keys directly
// underneath this key first, before writing sub-structs or sub-maps. // underneath this key first, before writing sub-structs or sub-maps.
var mapKeysDirect, mapKeysSub []string var mapKeysDirect, mapKeysSub []reflect.Value
for _, mapKey := range rv.MapKeys() { for _, mapKey := range rv.MapKeys() {
k := mapKey.String()
if typeIsTable(tomlTypeOfGo(eindirect(rv.MapIndex(mapKey)))) { if typeIsTable(tomlTypeOfGo(eindirect(rv.MapIndex(mapKey)))) {
mapKeysSub = append(mapKeysSub, k) mapKeysSub = append(mapKeysSub, mapKey)
} else { } else {
mapKeysDirect = append(mapKeysDirect, k) mapKeysDirect = append(mapKeysDirect, mapKey)
} }
} }
var writeMapKeys = func(mapKeys []string, trailC bool) { writeMapKeys := func(mapKeys []reflect.Value, trailC bool) {
sort.Strings(mapKeys) sort.Slice(mapKeys, func(i, j int) bool { return mapKeys[i].String() < mapKeys[j].String() })
for i, mapKey := range mapKeys { for i, mapKey := range mapKeys {
val := eindirect(rv.MapIndex(reflect.ValueOf(mapKey))) val := eindirect(rv.MapIndex(mapKey))
if isNil(val) { if isNil(val) {
continue continue
} }
if inline { if inline {
enc.writeKeyValue(Key{mapKey}, val, true) enc.writeKeyValue(Key{mapKey.String()}, val, true)
if trailC || i != len(mapKeys)-1 { if trailC || i != len(mapKeys)-1 {
enc.wf(", ") enc.wf(", ")
} }
} else { } else {
enc.encode(key.add(mapKey), val) enc.encode(key.add(mapKey.String()), val)
} }
} }
} }
@ -441,8 +440,6 @@ func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) {
} }
} }
const is32Bit = (32 << (^uint(0) >> 63)) == 32
func pointerTo(t reflect.Type) reflect.Type { func pointerTo(t reflect.Type) reflect.Type {
if t.Kind() == reflect.Ptr { if t.Kind() == reflect.Ptr {
return pointerTo(t.Elem()) return pointerTo(t.Elem())
@ -477,15 +474,14 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
frv := eindirect(rv.Field(i)) frv := eindirect(rv.Field(i))
if is32Bit { // Need to make a copy because ... ehm, I don't know why... I guess
// Copy so it works correct on 32bit archs; not clear why this // allocating a new array can cause it to fail(?)
// is needed. See #314, and https://www.reddit.com/r/golang/comments/pnx8v4 //
// This also works fine on 64bit, but 32bit archs are somewhat // Done for: https://github.com/BurntSushi/toml/issues/430
// rare and this is a wee bit faster. // Previously only on 32bit for: https://github.com/BurntSushi/toml/issues/314
copyStart := make([]int, len(start)) copyStart := make([]int, len(start))
copy(copyStart, start) copy(copyStart, start)
start = copyStart start = copyStart
}
// Treat anonymous struct fields with tag names as though they are // Treat anonymous struct fields with tag names as though they are
// not anonymous, like encoding/json does. // not anonymous, like encoding/json does.
@ -507,7 +503,7 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
} }
addFields(rt, rv, nil) addFields(rt, rv, nil)
writeFields := func(fields [][]int) { writeFields := func(fields [][]int, totalFields int) {
for _, fieldIndex := range fields { for _, fieldIndex := range fields {
fieldType := rt.FieldByIndex(fieldIndex) fieldType := rt.FieldByIndex(fieldIndex)
fieldVal := rv.FieldByIndex(fieldIndex) fieldVal := rv.FieldByIndex(fieldIndex)
@ -537,7 +533,7 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
if inline { if inline {
enc.writeKeyValue(Key{keyName}, fieldVal, true) enc.writeKeyValue(Key{keyName}, fieldVal, true)
if fieldIndex[0] != len(fields)-1 { if fieldIndex[0] != totalFields-1 {
enc.wf(", ") enc.wf(", ")
} }
} else { } else {
@ -549,8 +545,10 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
if inline { if inline {
enc.wf("{") enc.wf("{")
} }
writeFields(fieldsDirect)
writeFields(fieldsSub) l := len(fieldsDirect) + len(fieldsSub)
writeFields(fieldsDirect, l)
writeFields(fieldsSub, l)
if inline { if inline {
enc.wf("}") enc.wf("}")
} }

View File

@ -67,21 +67,36 @@ type ParseError struct {
// Position of an error. // Position of an error.
type Position struct { type Position struct {
Line int // Line number, starting at 1. Line int // Line number, starting at 1.
Col int // Error column, starting at 1.
Start int // Start of error, as byte offset starting at 0. Start int // Start of error, as byte offset starting at 0.
Len int // Lenght in bytes. Len int // Length of the error in bytes.
}
func (p Position) withCol(tomlFile string) Position {
var (
pos int
lines = strings.Split(tomlFile, "\n")
)
for i := range lines {
ll := len(lines[i]) + 1 // +1 for the removed newline
if pos+ll >= p.Start {
p.Col = p.Start - pos + 1
if p.Col < 1 { // Should never happen, but just in case.
p.Col = 1
}
break
}
pos += ll
}
return p
} }
func (pe ParseError) Error() string { func (pe ParseError) Error() string {
msg := pe.Message
if msg == "" { // Error from errorf()
msg = pe.err.Error()
}
if pe.LastKey == "" { if pe.LastKey == "" {
return fmt.Sprintf("toml: line %d: %s", pe.Position.Line, msg) return fmt.Sprintf("toml: line %d: %s", pe.Position.Line, pe.Message)
} }
return fmt.Sprintf("toml: line %d (last key %q): %s", return fmt.Sprintf("toml: line %d (last key %q): %s",
pe.Position.Line, pe.LastKey, msg) pe.Position.Line, pe.LastKey, pe.Message)
} }
// ErrorWithPosition returns the error with detailed location context. // ErrorWithPosition returns the error with detailed location context.
@ -92,26 +107,19 @@ func (pe ParseError) ErrorWithPosition() string {
return pe.Error() return pe.Error()
} }
var (
lines = strings.Split(pe.input, "\n")
col = pe.column(lines)
b = new(strings.Builder)
)
msg := pe.Message
if msg == "" {
msg = pe.err.Error()
}
// TODO: don't show control characters as literals? This may not show up // TODO: don't show control characters as literals? This may not show up
// well everywhere. // well everywhere.
var (
lines = strings.Split(pe.input, "\n")
b = new(strings.Builder)
)
if pe.Position.Len == 1 { if pe.Position.Len == 1 {
fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d:\n\n", fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d:\n\n",
msg, pe.Position.Line, col+1) pe.Message, pe.Position.Line, pe.Position.Col)
} else { } else {
fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d-%d:\n\n", fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d-%d:\n\n",
msg, pe.Position.Line, col, col+pe.Position.Len) pe.Message, pe.Position.Line, pe.Position.Col, pe.Position.Col+pe.Position.Len-1)
} }
if pe.Position.Line > 2 { if pe.Position.Line > 2 {
fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-2, expandTab(lines[pe.Position.Line-3])) fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-2, expandTab(lines[pe.Position.Line-3]))
@ -129,7 +137,7 @@ func (pe ParseError) ErrorWithPosition() string {
diff := len(expanded) - len(lines[pe.Position.Line-1]) diff := len(expanded) - len(lines[pe.Position.Line-1])
fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line, expanded) fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line, expanded)
fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", col+diff), strings.Repeat("^", pe.Position.Len)) fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", pe.Position.Col-1+diff), strings.Repeat("^", pe.Position.Len))
return b.String() return b.String()
} }
@ -151,23 +159,6 @@ func (pe ParseError) ErrorWithUsage() string {
return m return m
} }
func (pe ParseError) column(lines []string) int {
var pos, col int
for i := range lines {
ll := len(lines[i]) + 1 // +1 for the removed newline
if pos+ll >= pe.Position.Start {
col = pe.Position.Start - pos
if col < 0 { // Should never happen, but just in case.
col = 0
}
break
}
pos += ll
}
return col
}
func expandTab(s string) string { func expandTab(s string) string {
var ( var (
b strings.Builder b strings.Builder

View File

@ -275,7 +275,9 @@ func (lx *lexer) errorPos(start, length int, err error) stateFn {
func (lx *lexer) errorf(format string, values ...any) stateFn { func (lx *lexer) errorf(format string, values ...any) stateFn {
if lx.atEOF { if lx.atEOF {
pos := lx.getPos() pos := lx.getPos()
pos.Line-- if lx.pos >= 1 && lx.input[lx.pos-1] == '\n' {
pos.Line--
}
pos.Len = 1 pos.Len = 1
pos.Start = lx.pos - 1 pos.Start = lx.pos - 1
lx.items <- item{typ: itemError, pos: pos, err: fmt.Errorf(format, values...)} lx.items <- item{typ: itemError, pos: pos, err: fmt.Errorf(format, values...)}
@ -492,6 +494,9 @@ func lexKeyEnd(lx *lexer) stateFn {
lx.emit(itemKeyEnd) lx.emit(itemKeyEnd)
return lexSkip(lx, lexValue) return lexSkip(lx, lexValue)
default: default:
if r == '\n' {
return lx.errorPrevLine(fmt.Errorf("expected '.' or '=', but got %q instead", r))
}
return lx.errorf("expected '.' or '=', but got %q instead", r) return lx.errorf("expected '.' or '=', but got %q instead", r)
} }
} }
@ -560,6 +565,9 @@ func lexValue(lx *lexer) stateFn {
if r == eof { if r == eof {
return lx.errorf("unexpected EOF; expected value") return lx.errorf("unexpected EOF; expected value")
} }
if r == '\n' {
return lx.errorPrevLine(fmt.Errorf("expected value but found %q instead", r))
}
return lx.errorf("expected value but found %q instead", r) return lx.errorf("expected value but found %q instead", r)
} }
@ -1111,7 +1119,7 @@ func lexBaseNumberOrDate(lx *lexer) stateFn {
case 'x': case 'x':
r = lx.peek() r = lx.peek()
if !isHex(r) { if !isHex(r) {
lx.errorf("not a hexidecimal number: '%s%c'", lx.current(), r) lx.errorf("not a hexadecimal number: '%s%c'", lx.current(), r)
} }
return lexHexInteger return lexHexInteger
} }
@ -1259,23 +1267,6 @@ func isBinary(r rune) bool { return r == '0' || r == '1' }
func isOctal(r rune) bool { return r >= '0' && r <= '7' } func isOctal(r rune) bool { return r >= '0' && r <= '7' }
func isHex(r rune) bool { return (r >= '0' && r <= '9') || (r|0x20 >= 'a' && r|0x20 <= 'f') } func isHex(r rune) bool { return (r >= '0' && r <= '9') || (r|0x20 >= 'a' && r|0x20 <= 'f') }
func isBareKeyChar(r rune, tomlNext bool) bool { func isBareKeyChar(r rune, tomlNext bool) bool {
if tomlNext { return (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') ||
return (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-'
(r >= 'a' && r <= 'z') ||
(r >= '0' && r <= '9') ||
r == '_' || r == '-' ||
r == 0xb2 || r == 0xb3 || r == 0xb9 || (r >= 0xbc && r <= 0xbe) ||
(r >= 0xc0 && r <= 0xd6) || (r >= 0xd8 && r <= 0xf6) || (r >= 0xf8 && r <= 0x037d) ||
(r >= 0x037f && r <= 0x1fff) ||
(r >= 0x200c && r <= 0x200d) || (r >= 0x203f && r <= 0x2040) ||
(r >= 0x2070 && r <= 0x218f) || (r >= 0x2460 && r <= 0x24ff) ||
(r >= 0x2c00 && r <= 0x2fef) || (r >= 0x3001 && r <= 0xd7ff) ||
(r >= 0xf900 && r <= 0xfdcf) || (r >= 0xfdf0 && r <= 0xfffd) ||
(r >= 0x10000 && r <= 0xeffff)
}
return (r >= 'A' && r <= 'Z') ||
(r >= 'a' && r <= 'z') ||
(r >= '0' && r <= '9') ||
r == '_' || r == '-'
} }

View File

@ -135,9 +135,6 @@ func (k Key) maybeQuoted(i int) string {
// Like append(), but only increase the cap by 1. // Like append(), but only increase the cap by 1.
func (k Key) add(piece string) Key { func (k Key) add(piece string) Key {
if cap(k) > len(k) {
return append(k, piece)
}
newKey := make(Key, len(k)+1) newKey := make(Key, len(k)+1)
copy(newKey, k) copy(newKey, k)
newKey[len(k)] = piece newKey[len(k)] = piece

View File

@ -50,7 +50,6 @@ func parse(data string) (p *parser, err error) {
// it anyway. // it anyway.
if strings.HasPrefix(data, "\xff\xfe") || strings.HasPrefix(data, "\xfe\xff") { // UTF-16 if strings.HasPrefix(data, "\xff\xfe") || strings.HasPrefix(data, "\xfe\xff") { // UTF-16
data = data[2:] data = data[2:]
//lint:ignore S1017 https://github.com/dominikh/go-tools/issues/1447
} else if strings.HasPrefix(data, "\xef\xbb\xbf") { // UTF-8 } else if strings.HasPrefix(data, "\xef\xbb\xbf") { // UTF-8
data = data[3:] data = data[3:]
} }
@ -65,7 +64,7 @@ func parse(data string) (p *parser, err error) {
if i := strings.IndexRune(data[:ex], 0); i > -1 { if i := strings.IndexRune(data[:ex], 0); i > -1 {
return nil, ParseError{ return nil, ParseError{
Message: "files cannot contain NULL bytes; probably using UTF-16; TOML files must be UTF-8", Message: "files cannot contain NULL bytes; probably using UTF-16; TOML files must be UTF-8",
Position: Position{Line: 1, Start: i, Len: 1}, Position: Position{Line: 1, Col: 1, Start: i, Len: 1},
Line: 1, Line: 1,
input: data, input: data,
} }
@ -92,8 +91,9 @@ func parse(data string) (p *parser, err error) {
func (p *parser) panicErr(it item, err error) { func (p *parser) panicErr(it item, err error) {
panic(ParseError{ panic(ParseError{
Message: err.Error(),
err: err, err: err,
Position: it.pos, Position: it.pos.withCol(p.lx.input),
Line: it.pos.Len, Line: it.pos.Len,
LastKey: p.current(), LastKey: p.current(),
}) })
@ -102,7 +102,7 @@ func (p *parser) panicErr(it item, err error) {
func (p *parser) panicItemf(it item, format string, v ...any) { func (p *parser) panicItemf(it item, format string, v ...any) {
panic(ParseError{ panic(ParseError{
Message: fmt.Sprintf(format, v...), Message: fmt.Sprintf(format, v...),
Position: it.pos, Position: it.pos.withCol(p.lx.input),
Line: it.pos.Len, Line: it.pos.Len,
LastKey: p.current(), LastKey: p.current(),
}) })
@ -111,7 +111,7 @@ func (p *parser) panicItemf(it item, format string, v ...any) {
func (p *parser) panicf(format string, v ...any) { func (p *parser) panicf(format string, v ...any) {
panic(ParseError{ panic(ParseError{
Message: fmt.Sprintf(format, v...), Message: fmt.Sprintf(format, v...),
Position: p.pos, Position: p.pos.withCol(p.lx.input),
Line: p.pos.Line, Line: p.pos.Line,
LastKey: p.current(), LastKey: p.current(),
}) })
@ -123,10 +123,11 @@ func (p *parser) next() item {
if it.typ == itemError { if it.typ == itemError {
if it.err != nil { if it.err != nil {
panic(ParseError{ panic(ParseError{
Position: it.pos, Message: it.err.Error(),
err: it.err,
Position: it.pos.withCol(p.lx.input),
Line: it.pos.Line, Line: it.pos.Line,
LastKey: p.current(), LastKey: p.current(),
err: it.err,
}) })
} }
@ -527,7 +528,7 @@ func numUnderscoresOK(s string) bool {
} }
} }
// isHexis a superset of all the permissable characters surrounding an // isHex is a superset of all the permissible characters surrounding an
// underscore. // underscore.
accept = isHex(r) accept = isHex(r)
} }

View File

@ -49,16 +49,16 @@ func ShiftNBytesLeft(dst, x []byte, n int) {
dst = append(dst, make([]byte, n/8)...) dst = append(dst, make([]byte, n/8)...)
} }
// XorBytesMut assumes equal input length, replaces X with X XOR Y // XorBytesMut replaces X with X XOR Y. len(X) must be >= len(Y).
func XorBytesMut(X, Y []byte) { func XorBytesMut(X, Y []byte) {
for i := 0; i < len(X); i++ { for i := 0; i < len(Y); i++ {
X[i] ^= Y[i] X[i] ^= Y[i]
} }
} }
// XorBytes assumes equal input length, puts X XOR Y into Z // XorBytes puts X XOR Y into Z. len(Z) and len(X) must be >= len(Y).
func XorBytes(Z, X, Y []byte) { func XorBytes(Z, X, Y []byte) {
for i := 0; i < len(X); i++ { for i := 0; i < len(Y); i++ {
Z[i] = X[i] ^ Y[i] Z[i] = X[i] ^ Y[i]
} }
} }

View File

@ -109,8 +109,10 @@ func (o *ocb) Seal(dst, nonce, plaintext, adata []byte) []byte {
if len(nonce) > o.nonceSize { if len(nonce) > o.nonceSize {
panic("crypto/ocb: Incorrect nonce length given to OCB") panic("crypto/ocb: Incorrect nonce length given to OCB")
} }
ret, out := byteutil.SliceForAppend(dst, len(plaintext)+o.tagSize) sep := len(plaintext)
o.crypt(enc, out, nonce, adata, plaintext) ret, out := byteutil.SliceForAppend(dst, sep+o.tagSize)
tag := o.crypt(enc, out[:sep], nonce, adata, plaintext)
copy(out[sep:], tag)
return ret return ret
} }
@ -122,12 +124,10 @@ func (o *ocb) Open(dst, nonce, ciphertext, adata []byte) ([]byte, error) {
return nil, ocbError("Ciphertext shorter than tag length") return nil, ocbError("Ciphertext shorter than tag length")
} }
sep := len(ciphertext) - o.tagSize sep := len(ciphertext) - o.tagSize
ret, out := byteutil.SliceForAppend(dst, len(ciphertext)) ret, out := byteutil.SliceForAppend(dst, sep)
ciphertextData := ciphertext[:sep] ciphertextData := ciphertext[:sep]
tag := ciphertext[sep:] tag := o.crypt(dec, out, nonce, adata, ciphertextData)
o.crypt(dec, out, nonce, adata, ciphertextData) if subtle.ConstantTimeCompare(tag, ciphertext[sep:]) == 1 {
if subtle.ConstantTimeCompare(ret[sep:], tag) == 1 {
ret = ret[:sep]
return ret, nil return ret, nil
} }
for i := range out { for i := range out {
@ -137,7 +137,8 @@ func (o *ocb) Open(dst, nonce, ciphertext, adata []byte) ([]byte, error) {
} }
// On instruction enc (resp. dec), crypt is the encrypt (resp. decrypt) // On instruction enc (resp. dec), crypt is the encrypt (resp. decrypt)
// function. It returns the resulting plain/ciphertext with the tag appended. // function. It writes the resulting plain/ciphertext into Y and returns
// the tag.
func (o *ocb) crypt(instruction int, Y, nonce, adata, X []byte) []byte { func (o *ocb) crypt(instruction int, Y, nonce, adata, X []byte) []byte {
// //
// Consider X as a sequence of 128-bit blocks // Consider X as a sequence of 128-bit blocks
@ -194,13 +195,14 @@ func (o *ocb) crypt(instruction int, Y, nonce, adata, X []byte) []byte {
byteutil.XorBytesMut(offset, o.mask.L[bits.TrailingZeros(uint(i+1))]) byteutil.XorBytesMut(offset, o.mask.L[bits.TrailingZeros(uint(i+1))])
blockX := X[i*blockSize : (i+1)*blockSize] blockX := X[i*blockSize : (i+1)*blockSize]
blockY := Y[i*blockSize : (i+1)*blockSize] blockY := Y[i*blockSize : (i+1)*blockSize]
byteutil.XorBytes(blockY, blockX, offset)
switch instruction { switch instruction {
case enc: case enc:
byteutil.XorBytesMut(checksum, blockX)
byteutil.XorBytes(blockY, blockX, offset)
o.block.Encrypt(blockY, blockY) o.block.Encrypt(blockY, blockY)
byteutil.XorBytesMut(blockY, offset) byteutil.XorBytesMut(blockY, offset)
byteutil.XorBytesMut(checksum, blockX)
case dec: case dec:
byteutil.XorBytes(blockY, blockX, offset)
o.block.Decrypt(blockY, blockY) o.block.Decrypt(blockY, blockY)
byteutil.XorBytesMut(blockY, offset) byteutil.XorBytesMut(blockY, offset)
byteutil.XorBytesMut(checksum, blockY) byteutil.XorBytesMut(checksum, blockY)
@ -216,31 +218,24 @@ func (o *ocb) crypt(instruction int, Y, nonce, adata, X []byte) []byte {
o.block.Encrypt(pad, offset) o.block.Encrypt(pad, offset)
chunkX := X[blockSize*m:] chunkX := X[blockSize*m:]
chunkY := Y[blockSize*m : len(X)] chunkY := Y[blockSize*m : len(X)]
byteutil.XorBytes(chunkY, chunkX, pad[:len(chunkX)])
// P_* || bit(1) || zeroes(127) - len(P_*)
switch instruction { switch instruction {
case enc: case enc:
paddedY := append(chunkX, byte(128)) byteutil.XorBytesMut(checksum, chunkX)
paddedY = append(paddedY, make([]byte, blockSize-len(chunkX)-1)...) checksum[len(chunkX)] ^= 128
byteutil.XorBytesMut(checksum, paddedY) byteutil.XorBytes(chunkY, chunkX, pad[:len(chunkX)])
// P_* || bit(1) || zeroes(127) - len(P_*)
case dec: case dec:
paddedX := append(chunkY, byte(128)) byteutil.XorBytes(chunkY, chunkX, pad[:len(chunkX)])
paddedX = append(paddedX, make([]byte, blockSize-len(chunkY)-1)...) // P_* || bit(1) || zeroes(127) - len(P_*)
byteutil.XorBytesMut(checksum, paddedX) byteutil.XorBytesMut(checksum, chunkY)
checksum[len(chunkY)] ^= 128
} }
byteutil.XorBytes(tag, checksum, offset)
byteutil.XorBytesMut(tag, o.mask.lDol)
o.block.Encrypt(tag, tag)
byteutil.XorBytesMut(tag, o.hash(adata))
copy(Y[blockSize*m+len(chunkY):], tag[:o.tagSize])
} else {
byteutil.XorBytes(tag, checksum, offset)
byteutil.XorBytesMut(tag, o.mask.lDol)
o.block.Encrypt(tag, tag)
byteutil.XorBytesMut(tag, o.hash(adata))
copy(Y[blockSize*m:], tag[:o.tagSize])
} }
return Y byteutil.XorBytes(tag, checksum, offset)
byteutil.XorBytesMut(tag, o.mask.lDol)
o.block.Encrypt(tag, tag)
byteutil.XorBytesMut(tag, o.hash(adata))
return tag[:o.tagSize]
} }
// This hash function is used to compute the tag. Per design, on empty input it // This hash function is used to compute the tag. Per design, on empty input it

View File

@ -7,6 +7,7 @@ package armor
import ( import (
"encoding/base64" "encoding/base64"
"io" "io"
"sort"
) )
var armorHeaderSep = []byte(": ") var armorHeaderSep = []byte(": ")
@ -159,8 +160,15 @@ func encode(out io.Writer, blockType string, headers map[string]string, checksum
return return
} }
for k, v := range headers { keys := make([]string, len(headers))
err = writeSlices(out, []byte(k), armorHeaderSep, []byte(v), newline) i := 0
for k := range headers {
keys[i] = k
i++
}
sort.Strings(keys)
for _, k := range keys {
err = writeSlices(out, []byte(k), armorHeaderSep, []byte(headers[k]), newline)
if err != nil { if err != nil {
return return
} }

View File

@ -6,6 +6,7 @@
package errors // import "github.com/ProtonMail/go-crypto/openpgp/errors" package errors // import "github.com/ProtonMail/go-crypto/openpgp/errors"
import ( import (
"fmt"
"strconv" "strconv"
) )
@ -178,3 +179,32 @@ type ErrMalformedMessage string
func (dke ErrMalformedMessage) Error() string { func (dke ErrMalformedMessage) Error() string {
return "openpgp: malformed message " + string(dke) return "openpgp: malformed message " + string(dke)
} }
type messageTooLargeError int
func (e messageTooLargeError) Error() string {
return "openpgp: decompressed message size exceeds provided limit"
}
// ErrMessageTooLarge is returned if the read data from
// a compressed packet exceeds the provided limit.
var ErrMessageTooLarge error = messageTooLargeError(0)
// ErrEncryptionKeySelection is returned if encryption key selection fails (v2 API).
type ErrEncryptionKeySelection struct {
PrimaryKeyId string
PrimaryKeyErr error
EncSelectionKeyId *string
EncSelectionErr error
}
func (eks ErrEncryptionKeySelection) Error() string {
prefix := fmt.Sprintf("openpgp: key selection for primary key %s:", eks.PrimaryKeyId)
if eks.PrimaryKeyErr != nil {
return fmt.Sprintf("%s invalid primary key: %s", prefix, eks.PrimaryKeyErr)
}
if eks.EncSelectionKeyId != nil {
return fmt.Sprintf("%s invalid encryption key %s: %s", prefix, *eks.EncSelectionKeyId, eks.EncSelectionErr)
}
return fmt.Sprintf("%s no encryption key: %s", prefix, eks.EncSelectionErr)
}

View File

@ -37,7 +37,7 @@ func (conf *AEADConfig) Mode() AEADMode {
// ChunkSizeByte returns the byte indicating the chunk size. The effective // ChunkSizeByte returns the byte indicating the chunk size. The effective
// chunk size is computed with the formula uint64(1) << (chunkSizeByte + 6) // chunk size is computed with the formula uint64(1) << (chunkSizeByte + 6)
// limit to 16 = 4 MiB // limit chunkSizeByte to 16 which equals to 2^22 = 4 MiB
// https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-5.13.2 // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-5.13.2
func (conf *AEADConfig) ChunkSizeByte() byte { func (conf *AEADConfig) ChunkSizeByte() byte {
if conf == nil || conf.ChunkSize == 0 { if conf == nil || conf.ChunkSize == 0 {
@ -49,8 +49,8 @@ func (conf *AEADConfig) ChunkSizeByte() byte {
switch { switch {
case exponent < 6: case exponent < 6:
exponent = 6 exponent = 6
case exponent > 16: case exponent > 22:
exponent = 16 exponent = 22
} }
return byte(exponent - 6) return byte(exponent - 6)

View File

@ -3,7 +3,6 @@
package packet package packet
import ( import (
"bytes"
"crypto/cipher" "crypto/cipher"
"encoding/binary" "encoding/binary"
"io" "io"
@ -15,12 +14,11 @@ import (
type aeadCrypter struct { type aeadCrypter struct {
aead cipher.AEAD aead cipher.AEAD
chunkSize int chunkSize int
initialNonce []byte nonce []byte
associatedData []byte // Chunk-independent associated data associatedData []byte // Chunk-independent associated data
chunkIndex []byte // Chunk counter chunkIndex []byte // Chunk counter
packetTag packetType // SEIP packet (v2) or AEAD Encrypted Data packet packetTag packetType // SEIP packet (v2) or AEAD Encrypted Data packet
bytesProcessed int // Amount of plaintext bytes encrypted/decrypted bytesProcessed int // Amount of plaintext bytes encrypted/decrypted
buffer bytes.Buffer // Buffered bytes across chunks
} }
// computeNonce takes the incremental index and computes an eXclusive OR with // computeNonce takes the incremental index and computes an eXclusive OR with
@ -28,12 +26,12 @@ type aeadCrypter struct {
// 5.16.1 and 5.16.2). It returns the resulting nonce. // 5.16.1 and 5.16.2). It returns the resulting nonce.
func (wo *aeadCrypter) computeNextNonce() (nonce []byte) { func (wo *aeadCrypter) computeNextNonce() (nonce []byte) {
if wo.packetTag == packetTypeSymmetricallyEncryptedIntegrityProtected { if wo.packetTag == packetTypeSymmetricallyEncryptedIntegrityProtected {
return append(wo.initialNonce, wo.chunkIndex...) return wo.nonce
} }
nonce = make([]byte, len(wo.initialNonce)) nonce = make([]byte, len(wo.nonce))
copy(nonce, wo.initialNonce) copy(nonce, wo.nonce)
offset := len(wo.initialNonce) - 8 offset := len(wo.nonce) - 8
for i := 0; i < 8; i++ { for i := 0; i < 8; i++ {
nonce[i+offset] ^= wo.chunkIndex[i] nonce[i+offset] ^= wo.chunkIndex[i]
} }
@ -62,8 +60,9 @@ func (wo *aeadCrypter) incrementIndex() error {
type aeadDecrypter struct { type aeadDecrypter struct {
aeadCrypter // Embedded ciphertext opener aeadCrypter // Embedded ciphertext opener
reader io.Reader // 'reader' is a partialLengthReader reader io.Reader // 'reader' is a partialLengthReader
chunkBytes []byte
peekedBytes []byte // Used to detect last chunk peekedBytes []byte // Used to detect last chunk
eof bool buffer []byte // Buffered decrypted bytes
} }
// Read decrypts bytes and reads them into dst. It decrypts when necessary and // Read decrypts bytes and reads them into dst. It decrypts when necessary and
@ -71,59 +70,44 @@ type aeadDecrypter struct {
// and an error. // and an error.
func (ar *aeadDecrypter) Read(dst []byte) (n int, err error) { func (ar *aeadDecrypter) Read(dst []byte) (n int, err error) {
// Return buffered plaintext bytes from previous calls // Return buffered plaintext bytes from previous calls
if ar.buffer.Len() > 0 { if len(ar.buffer) > 0 {
return ar.buffer.Read(dst) n = copy(dst, ar.buffer)
} ar.buffer = ar.buffer[n:]
return
// Return EOF if we've previously validated the final tag
if ar.eof {
return 0, io.EOF
} }
// Read a chunk // Read a chunk
tagLen := ar.aead.Overhead() tagLen := ar.aead.Overhead()
cipherChunkBuf := new(bytes.Buffer) copy(ar.chunkBytes, ar.peekedBytes) // Copy bytes peeked in previous chunk or in initialization
_, errRead := io.CopyN(cipherChunkBuf, ar.reader, int64(ar.chunkSize+tagLen)) bytesRead, errRead := io.ReadFull(ar.reader, ar.chunkBytes[tagLen:])
cipherChunk := cipherChunkBuf.Bytes() if errRead != nil && errRead != io.EOF && errRead != io.ErrUnexpectedEOF {
if errRead != nil && errRead != io.EOF {
return 0, errRead return 0, errRead
} }
if len(cipherChunk) > 0 { if bytesRead > 0 {
decrypted, errChunk := ar.openChunk(cipherChunk) ar.peekedBytes = ar.chunkBytes[bytesRead:bytesRead+tagLen]
decrypted, errChunk := ar.openChunk(ar.chunkBytes[:bytesRead])
if errChunk != nil { if errChunk != nil {
return 0, errChunk return 0, errChunk
} }
// Return decrypted bytes, buffering if necessary // Return decrypted bytes, buffering if necessary
if len(dst) < len(decrypted) { n = copy(dst, decrypted)
n = copy(dst, decrypted[:len(dst)]) ar.buffer = decrypted[n:]
ar.buffer.Write(decrypted[len(dst):]) return
} else {
n = copy(dst, decrypted)
}
} }
// Check final authentication tag return 0, io.EOF
if errRead == io.EOF {
errChunk := ar.validateFinalTag(ar.peekedBytes)
if errChunk != nil {
return n, errChunk
}
ar.eof = true // Mark EOF for when we've returned all buffered data
}
return
} }
// Close is noOp. The final authentication tag of the stream was already // Close checks the final authentication tag of the stream.
// checked in the last Read call. In the future, this function could be used to // In the future, this function could also be used to wipe the reader
// wipe the reader and peeked, decrypted bytes, if necessary. // and peeked & decrypted bytes, if necessary.
func (ar *aeadDecrypter) Close() (err error) { func (ar *aeadDecrypter) Close() (err error) {
if !ar.eof { errChunk := ar.validateFinalTag(ar.peekedBytes)
errChunk := ar.validateFinalTag(ar.peekedBytes) if errChunk != nil {
if errChunk != nil { return errChunk
return errChunk
}
} }
return nil return nil
} }
@ -132,20 +116,13 @@ func (ar *aeadDecrypter) Close() (err error) {
// the underlying plaintext and an error. It accesses peeked bytes from next // the underlying plaintext and an error. It accesses peeked bytes from next
// chunk, to identify the last chunk and decrypt/validate accordingly. // chunk, to identify the last chunk and decrypt/validate accordingly.
func (ar *aeadDecrypter) openChunk(data []byte) ([]byte, error) { func (ar *aeadDecrypter) openChunk(data []byte) ([]byte, error) {
tagLen := ar.aead.Overhead()
// Restore carried bytes from last call
chunkExtra := append(ar.peekedBytes, data...)
// 'chunk' contains encrypted bytes, followed by an authentication tag.
chunk := chunkExtra[:len(chunkExtra)-tagLen]
ar.peekedBytes = chunkExtra[len(chunkExtra)-tagLen:]
adata := ar.associatedData adata := ar.associatedData
if ar.aeadCrypter.packetTag == packetTypeAEADEncrypted { if ar.aeadCrypter.packetTag == packetTypeAEADEncrypted {
adata = append(ar.associatedData, ar.chunkIndex...) adata = append(ar.associatedData, ar.chunkIndex...)
} }
nonce := ar.computeNextNonce() nonce := ar.computeNextNonce()
plainChunk, err := ar.aead.Open(nil, nonce, chunk, adata) plainChunk, err := ar.aead.Open(data[:0:len(data)], nonce, data, adata)
if err != nil { if err != nil {
return nil, errors.ErrAEADTagVerification return nil, errors.ErrAEADTagVerification
} }
@ -183,27 +160,29 @@ func (ar *aeadDecrypter) validateFinalTag(tag []byte) error {
type aeadEncrypter struct { type aeadEncrypter struct {
aeadCrypter // Embedded plaintext sealer aeadCrypter // Embedded plaintext sealer
writer io.WriteCloser // 'writer' is a partialLengthWriter writer io.WriteCloser // 'writer' is a partialLengthWriter
chunkBytes []byte
offset int
} }
// Write encrypts and writes bytes. It encrypts when necessary and buffers extra // Write encrypts and writes bytes. It encrypts when necessary and buffers extra
// plaintext bytes for next call. When the stream is finished, Close() MUST be // plaintext bytes for next call. When the stream is finished, Close() MUST be
// called to append the final tag. // called to append the final tag.
func (aw *aeadEncrypter) Write(plaintextBytes []byte) (n int, err error) { func (aw *aeadEncrypter) Write(plaintextBytes []byte) (n int, err error) {
// Append plaintextBytes to existing buffered bytes for n != len(plaintextBytes) {
n, err = aw.buffer.Write(plaintextBytes) copied := copy(aw.chunkBytes[aw.offset:aw.chunkSize], plaintextBytes[n:])
if err != nil { n += copied
return n, err aw.offset += copied
}
// Encrypt and write chunks if aw.offset == aw.chunkSize {
for aw.buffer.Len() >= aw.chunkSize { encryptedChunk, err := aw.sealChunk(aw.chunkBytes[:aw.offset])
plainChunk := aw.buffer.Next(aw.chunkSize) if err != nil {
encryptedChunk, err := aw.sealChunk(plainChunk) return n, err
if err != nil { }
return n, err _, err = aw.writer.Write(encryptedChunk)
} if err != nil {
_, err = aw.writer.Write(encryptedChunk) return n, err
if err != nil { }
return n, err aw.offset = 0
} }
} }
return return
@ -215,9 +194,8 @@ func (aw *aeadEncrypter) Write(plaintextBytes []byte) (n int, err error) {
func (aw *aeadEncrypter) Close() (err error) { func (aw *aeadEncrypter) Close() (err error) {
// Encrypt and write a chunk if there's buffered data left, or if we haven't // Encrypt and write a chunk if there's buffered data left, or if we haven't
// written any chunks yet. // written any chunks yet.
if aw.buffer.Len() > 0 || aw.bytesProcessed == 0 { if aw.offset > 0 || aw.bytesProcessed == 0 {
plainChunk := aw.buffer.Bytes() lastEncryptedChunk, err := aw.sealChunk(aw.chunkBytes[:aw.offset])
lastEncryptedChunk, err := aw.sealChunk(plainChunk)
if err != nil { if err != nil {
return err return err
} }
@ -263,7 +241,7 @@ func (aw *aeadEncrypter) sealChunk(data []byte) ([]byte, error) {
} }
nonce := aw.computeNextNonce() nonce := aw.computeNextNonce()
encrypted := aw.aead.Seal(nil, nonce, data, adata) encrypted := aw.aead.Seal(data[:0], nonce, data, adata)
aw.bytesProcessed += len(data) aw.bytesProcessed += len(data)
if err := aw.aeadCrypter.incrementIndex(); err != nil { if err := aw.aeadCrypter.incrementIndex(); err != nil {
return nil, err return nil, err

View File

@ -65,24 +65,28 @@ func (ae *AEADEncrypted) decrypt(key []byte) (io.ReadCloser, error) {
blockCipher := ae.cipher.new(key) blockCipher := ae.cipher.new(key)
aead := ae.mode.new(blockCipher) aead := ae.mode.new(blockCipher)
// Carry the first tagLen bytes // Carry the first tagLen bytes
chunkSize := decodeAEADChunkSize(ae.chunkSizeByte)
tagLen := ae.mode.TagLength() tagLen := ae.mode.TagLength()
peekedBytes := make([]byte, tagLen) chunkBytes := make([]byte, chunkSize+tagLen*2)
peekedBytes := chunkBytes[chunkSize+tagLen:]
n, err := io.ReadFull(ae.Contents, peekedBytes) n, err := io.ReadFull(ae.Contents, peekedBytes)
if n < tagLen || (err != nil && err != io.EOF) { if n < tagLen || (err != nil && err != io.EOF) {
return nil, errors.AEADError("Not enough data to decrypt:" + err.Error()) return nil, errors.AEADError("Not enough data to decrypt:" + err.Error())
} }
chunkSize := decodeAEADChunkSize(ae.chunkSizeByte)
return &aeadDecrypter{ return &aeadDecrypter{
aeadCrypter: aeadCrypter{ aeadCrypter: aeadCrypter{
aead: aead, aead: aead,
chunkSize: chunkSize, chunkSize: chunkSize,
initialNonce: ae.initialNonce, nonce: ae.initialNonce,
associatedData: ae.associatedData(), associatedData: ae.associatedData(),
chunkIndex: make([]byte, 8), chunkIndex: make([]byte, 8),
packetTag: packetTypeAEADEncrypted, packetTag: packetTypeAEADEncrypted,
}, },
reader: ae.Contents, reader: ae.Contents,
peekedBytes: peekedBytes}, nil chunkBytes: chunkBytes,
peekedBytes: peekedBytes,
}, nil
} }
// associatedData for chunks: tag, version, cipher, mode, chunk size byte // associatedData for chunks: tag, version, cipher, mode, chunk size byte

View File

@ -98,6 +98,16 @@ func (c *Compressed) parse(r io.Reader) error {
return err return err
} }
// LimitedBodyReader wraps the provided body reader with a limiter that restricts
// the number of bytes read to the specified limit.
// If limit is nil, the reader is unbounded.
func (c *Compressed) LimitedBodyReader(limit *int64) io.Reader {
if limit == nil {
return c.Body
}
return &LimitReader{R: c.Body, N: *limit}
}
// compressedWriterCloser represents the serialized compression stream // compressedWriterCloser represents the serialized compression stream
// header and the compressor. Its Close() method ensures that both the // header and the compressor. Its Close() method ensures that both the
// compressor and serialized stream header are closed. Its Write() // compressor and serialized stream header are closed. Its Write()
@ -159,3 +169,24 @@ func SerializeCompressed(w io.WriteCloser, algo CompressionAlgo, cc *Compression
return return
} }
// LimitReader is an io.Reader that fails with MessageToLarge if read bytes exceed N.
type LimitReader struct {
R io.Reader // underlying reader
N int64 // max bytes allowed
}
func (l *LimitReader) Read(p []byte) (int, error) {
if l.N <= 0 {
return 0, errors.ErrMessageTooLarge
}
n, err := l.R.Read(p)
l.N -= int64(n)
if err == nil && l.N <= 0 {
err = errors.ErrMessageTooLarge
}
return n, err
}

View File

@ -173,6 +173,16 @@ type Config struct {
// weaknesses in the hash algo, potentially hindering e.g. some chosen-prefix attacks. // weaknesses in the hash algo, potentially hindering e.g. some chosen-prefix attacks.
// The default behavior, when the config or flag is nil, is to enable the feature. // The default behavior, when the config or flag is nil, is to enable the feature.
NonDeterministicSignaturesViaNotation *bool NonDeterministicSignaturesViaNotation *bool
// InsecureAllowAllKeyFlagsWhenMissing determines how a key without valid key flags is handled.
// When set to true, a key without flags is treated as if all flags are enabled.
// This behavior is consistent with GPG.
InsecureAllowAllKeyFlagsWhenMissing bool
// MaxDecompressedMessageSize specifies the maximum number of bytes that can be
// read from a compressed packet. This serves as an upper limit to prevent
// excessively large decompressed messages.
MaxDecompressedMessageSize *int64
} }
func (c *Config) Random() io.Reader { func (c *Config) Random() io.Reader {
@ -403,6 +413,20 @@ func (c *Config) RandomizeSignaturesViaNotation() bool {
return *c.NonDeterministicSignaturesViaNotation return *c.NonDeterministicSignaturesViaNotation
} }
func (c *Config) AllowAllKeyFlagsWhenMissing() bool {
if c == nil {
return false
}
return c.InsecureAllowAllKeyFlagsWhenMissing
}
func (c *Config) DecompressedMessageSizeLimit() *int64 {
if c == nil {
return nil
}
return c.MaxDecompressedMessageSize
}
// BoolPointer is a helper function to set a boolean pointer in the Config. // BoolPointer is a helper function to set a boolean pointer in the Config.
// e.g., config.CheckPacketSequence = BoolPointer(true) // e.g., config.CheckPacketSequence = BoolPointer(true)
func BoolPointer(value bool) *bool { func BoolPointer(value bool) *bool {

View File

@ -1048,12 +1048,17 @@ func (pk *PublicKey) VerifyDirectKeySignature(sig *Signature) (err error) {
// KeyIdString returns the public key's fingerprint in capital hex // KeyIdString returns the public key's fingerprint in capital hex
// (e.g. "6C7EE1B8621CC013"). // (e.g. "6C7EE1B8621CC013").
func (pk *PublicKey) KeyIdString() string { func (pk *PublicKey) KeyIdString() string {
return fmt.Sprintf("%X", pk.Fingerprint[12:20]) return fmt.Sprintf("%016X", pk.KeyId)
} }
// KeyIdShortString returns the short form of public key's fingerprint // KeyIdShortString returns the short form of public key's fingerprint
// in capital hex, as shown by gpg --list-keys (e.g. "621CC013"). // in capital hex, as shown by gpg --list-keys (e.g. "621CC013").
// This function will return the full key id for v5 and v6 keys
// since the short key id is undefined for them.
func (pk *PublicKey) KeyIdShortString() string { func (pk *PublicKey) KeyIdShortString() string {
if pk.Version >= 5 {
return pk.KeyIdString()
}
return fmt.Sprintf("%X", pk.Fingerprint[16:20]) return fmt.Sprintf("%X", pk.Fingerprint[16:20])
} }

View File

@ -1288,7 +1288,9 @@ func (sig *Signature) buildSubpackets(issuer PublicKey) (subpackets []outputSubp
if sig.IssuerKeyId != nil && sig.Version == 4 { if sig.IssuerKeyId != nil && sig.Version == 4 {
keyId := make([]byte, 8) keyId := make([]byte, 8)
binary.BigEndian.PutUint64(keyId, *sig.IssuerKeyId) binary.BigEndian.PutUint64(keyId, *sig.IssuerKeyId)
subpackets = append(subpackets, outputSubpacket{true, issuerSubpacket, true, keyId}) // Note: making this critical breaks RPM <=4.16.
// See: https://github.com/ProtonMail/go-crypto/issues/263
subpackets = append(subpackets, outputSubpacket{true, issuerSubpacket, false, keyId})
} }
// Notation Data // Notation Data
for _, notation := range sig.Notations { for _, notation := range sig.Notations {

View File

@ -70,8 +70,10 @@ func (se *SymmetricallyEncrypted) decryptAead(inputKey []byte) (io.ReadCloser, e
aead, nonce := getSymmetricallyEncryptedAeadInstance(se.Cipher, se.Mode, inputKey, se.Salt[:], se.associatedData()) aead, nonce := getSymmetricallyEncryptedAeadInstance(se.Cipher, se.Mode, inputKey, se.Salt[:], se.associatedData())
// Carry the first tagLen bytes // Carry the first tagLen bytes
chunkSize := decodeAEADChunkSize(se.ChunkSizeByte)
tagLen := se.Mode.TagLength() tagLen := se.Mode.TagLength()
peekedBytes := make([]byte, tagLen) chunkBytes := make([]byte, chunkSize+tagLen*2)
peekedBytes := chunkBytes[chunkSize+tagLen:]
n, err := io.ReadFull(se.Contents, peekedBytes) n, err := io.ReadFull(se.Contents, peekedBytes)
if n < tagLen || (err != nil && err != io.EOF) { if n < tagLen || (err != nil && err != io.EOF) {
return nil, errors.StructuralError("not enough data to decrypt:" + err.Error()) return nil, errors.StructuralError("not enough data to decrypt:" + err.Error())
@ -81,12 +83,13 @@ func (se *SymmetricallyEncrypted) decryptAead(inputKey []byte) (io.ReadCloser, e
aeadCrypter: aeadCrypter{ aeadCrypter: aeadCrypter{
aead: aead, aead: aead,
chunkSize: decodeAEADChunkSize(se.ChunkSizeByte), chunkSize: decodeAEADChunkSize(se.ChunkSizeByte),
initialNonce: nonce, nonce: nonce,
associatedData: se.associatedData(), associatedData: se.associatedData(),
chunkIndex: make([]byte, 8), chunkIndex: nonce[len(nonce)-8:],
packetTag: packetTypeSymmetricallyEncryptedIntegrityProtected, packetTag: packetTypeSymmetricallyEncryptedIntegrityProtected,
}, },
reader: se.Contents, reader: se.Contents,
chunkBytes: chunkBytes,
peekedBytes: peekedBytes, peekedBytes: peekedBytes,
}, nil }, nil
} }
@ -130,16 +133,20 @@ func serializeSymmetricallyEncryptedAead(ciphertext io.WriteCloser, cipherSuite
aead, nonce := getSymmetricallyEncryptedAeadInstance(cipherSuite.Cipher, cipherSuite.Mode, inputKey, salt, prefix) aead, nonce := getSymmetricallyEncryptedAeadInstance(cipherSuite.Cipher, cipherSuite.Mode, inputKey, salt, prefix)
chunkSize := decodeAEADChunkSize(chunkSizeByte)
tagLen := aead.Overhead()
chunkBytes := make([]byte, chunkSize+tagLen)
return &aeadEncrypter{ return &aeadEncrypter{
aeadCrypter: aeadCrypter{ aeadCrypter: aeadCrypter{
aead: aead, aead: aead,
chunkSize: decodeAEADChunkSize(chunkSizeByte), chunkSize: chunkSize,
associatedData: prefix, associatedData: prefix,
chunkIndex: make([]byte, 8), nonce: nonce,
initialNonce: nonce, chunkIndex: nonce[len(nonce)-8:],
packetTag: packetTypeSymmetricallyEncryptedIntegrityProtected, packetTag: packetTypeSymmetricallyEncryptedIntegrityProtected,
}, },
writer: ciphertext, writer: ciphertext,
chunkBytes: chunkBytes,
}, nil }, nil
} }
@ -149,10 +156,10 @@ func getSymmetricallyEncryptedAeadInstance(c CipherFunction, mode AEADMode, inpu
encryptionKey := make([]byte, c.KeySize()) encryptionKey := make([]byte, c.KeySize())
_, _ = readFull(hkdfReader, encryptionKey) _, _ = readFull(hkdfReader, encryptionKey)
// Last 64 bits of nonce are the counter nonce = make([]byte, mode.IvLength())
nonce = make([]byte, mode.IvLength()-8)
_, _ = readFull(hkdfReader, nonce) // Last 64 bits of nonce are the counter
_, _ = readFull(hkdfReader, nonce[:len(nonce)-8])
blockCipher := c.new(encryptionKey) blockCipher := c.new(encryptionKey)
aead = mode.new(blockCipher) aead = mode.new(blockCipher)

View File

@ -259,7 +259,7 @@ FindLiteralData:
} }
switch p := p.(type) { switch p := p.(type) {
case *packet.Compressed: case *packet.Compressed:
if err := packets.Push(p.Body); err != nil { if err := packets.Push(p.LimitedBodyReader(config.DecompressedMessageSizeLimit())); err != nil {
return nil, err return nil, err
} }
case *packet.OnePassSignature: case *packet.OnePassSignature:

View File

@ -253,34 +253,12 @@ func writeAndSign(payload io.WriteCloser, candidateHashes []uint8, signed *Entit
} }
var hash crypto.Hash var hash crypto.Hash
for _, hashId := range candidateHashes {
if h, ok := algorithm.HashIdToHash(hashId); ok && h.Available() {
hash = h
break
}
}
// If the hash specified by config is a candidate, we'll use that.
if configuredHash := config.Hash(); configuredHash.Available() {
for _, hashId := range candidateHashes {
if h, ok := algorithm.HashIdToHash(hashId); ok && h == configuredHash {
hash = h
break
}
}
}
if hash == 0 {
hashId := candidateHashes[0]
name, ok := algorithm.HashIdToString(hashId)
if !ok {
name = "#" + strconv.Itoa(int(hashId))
}
return nil, errors.InvalidArgumentError("cannot encrypt because no candidate hash functions are compiled in. (Wanted " + name + " in this case.)")
}
var salt []byte var salt []byte
if signer != nil { if signer != nil {
if hash, err = selectHash(candidateHashes, config.Hash(), signer); err != nil {
return nil, err
}
var opsVersion = 3 var opsVersion = 3
if signer.Version == 6 { if signer.Version == 6 {
opsVersion = signer.Version opsVersion = signer.Version
@ -558,13 +536,34 @@ func (s signatureWriter) Close() error {
return s.encryptedData.Close() return s.encryptedData.Close()
} }
func selectHashForSigningKey(config *packet.Config, signer *packet.PublicKey) crypto.Hash {
acceptableHashes := acceptableHashesToWrite(signer)
hash, ok := algorithm.HashToHashId(config.Hash())
if !ok {
return config.Hash()
}
for _, acceptableHashes := range acceptableHashes {
if acceptableHashes == hash {
return config.Hash()
}
}
if len(acceptableHashes) > 0 {
defaultAcceptedHash, ok := algorithm.HashIdToHash(acceptableHashes[0])
if ok {
return defaultAcceptedHash
}
}
return config.Hash()
}
func createSignaturePacket(signer *packet.PublicKey, sigType packet.SignatureType, config *packet.Config) *packet.Signature { func createSignaturePacket(signer *packet.PublicKey, sigType packet.SignatureType, config *packet.Config) *packet.Signature {
sigLifetimeSecs := config.SigLifetime() sigLifetimeSecs := config.SigLifetime()
hash := selectHashForSigningKey(config, signer)
return &packet.Signature{ return &packet.Signature{
Version: signer.Version, Version: signer.Version,
SigType: sigType, SigType: sigType,
PubKeyAlgo: signer.PubKeyAlgo, PubKeyAlgo: signer.PubKeyAlgo,
Hash: config.Hash(), Hash: hash,
CreationTime: config.Now(), CreationTime: config.Now(),
IssuerKeyId: &signer.KeyId, IssuerKeyId: &signer.KeyId,
IssuerFingerprint: signer.Fingerprint, IssuerFingerprint: signer.Fingerprint,
@ -618,3 +617,74 @@ func handleCompression(compressed io.WriteCloser, candidateCompression []uint8,
} }
return data, nil return data, nil
} }
// selectHash selects the preferred hash given the candidateHashes and the configuredHash
func selectHash(candidateHashes []byte, configuredHash crypto.Hash, signer *packet.PrivateKey) (hash crypto.Hash, err error) {
acceptableHashes := acceptableHashesToWrite(&signer.PublicKey)
candidateHashes = intersectPreferences(acceptableHashes, candidateHashes)
for _, hashId := range candidateHashes {
if h, ok := algorithm.HashIdToHash(hashId); ok && h.Available() {
hash = h
break
}
}
// If the hash specified by config is a candidate, we'll use that.
if configuredHash.Available() {
for _, hashId := range candidateHashes {
if h, ok := algorithm.HashIdToHash(hashId); ok && h == configuredHash {
hash = h
break
}
}
}
if hash == 0 {
if len(acceptableHashes) > 0 {
if h, ok := algorithm.HashIdToHash(acceptableHashes[0]); ok {
hash = h
} else {
return 0, errors.UnsupportedError("no candidate hash functions are compiled in.")
}
} else {
return 0, errors.UnsupportedError("no candidate hash functions are compiled in.")
}
}
return
}
func acceptableHashesToWrite(singingKey *packet.PublicKey) []uint8 {
switch singingKey.PubKeyAlgo {
case packet.PubKeyAlgoEd448:
return []uint8{
hashToHashId(crypto.SHA512),
hashToHashId(crypto.SHA3_512),
}
case packet.PubKeyAlgoECDSA, packet.PubKeyAlgoEdDSA:
if curve, err := singingKey.Curve(); err == nil {
if curve == packet.Curve448 ||
curve == packet.CurveNistP521 ||
curve == packet.CurveBrainpoolP512 {
return []uint8{
hashToHashId(crypto.SHA512),
hashToHashId(crypto.SHA3_512),
}
} else if curve == packet.CurveBrainpoolP384 ||
curve == packet.CurveNistP384 {
return []uint8{
hashToHashId(crypto.SHA384),
hashToHashId(crypto.SHA512),
hashToHashId(crypto.SHA3_512),
}
}
}
}
return []uint8{
hashToHashId(crypto.SHA256),
hashToHashId(crypto.SHA384),
hashToHashId(crypto.SHA512),
hashToHashId(crypto.SHA3_256),
hashToHashId(crypto.SHA3_512),
}
}

View File

@ -1,62 +0,0 @@
package backoff
import (
"context"
"time"
)
// BackOffContext is a backoff policy that stops retrying after the context
// is canceled.
type BackOffContext interface { // nolint: golint
BackOff
Context() context.Context
}
type backOffContext struct {
BackOff
ctx context.Context
}
// WithContext returns a BackOffContext with context ctx
//
// ctx must not be nil
func WithContext(b BackOff, ctx context.Context) BackOffContext { // nolint: golint
if ctx == nil {
panic("nil context")
}
if b, ok := b.(*backOffContext); ok {
return &backOffContext{
BackOff: b.BackOff,
ctx: ctx,
}
}
return &backOffContext{
BackOff: b,
ctx: ctx,
}
}
func getContext(b BackOff) context.Context {
if cb, ok := b.(BackOffContext); ok {
return cb.Context()
}
if tb, ok := b.(*backOffTries); ok {
return getContext(tb.delegate)
}
return context.Background()
}
func (b *backOffContext) Context() context.Context {
return b.ctx
}
func (b *backOffContext) NextBackOff() time.Duration {
select {
case <-b.ctx.Done():
return Stop
default:
return b.BackOff.NextBackOff()
}
}

View File

@ -1,216 +0,0 @@
package backoff
import (
"math/rand"
"time"
)
/*
ExponentialBackOff is a backoff implementation that increases the backoff
period for each retry attempt using a randomization function that grows exponentially.
NextBackOff() is calculated using the following formula:
randomized interval =
RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor])
In other words NextBackOff() will range between the randomization factor
percentage below and above the retry interval.
For example, given the following parameters:
RetryInterval = 2
RandomizationFactor = 0.5
Multiplier = 2
the actual backoff period used in the next retry attempt will range between 1 and 3 seconds,
multiplied by the exponential, that is, between 2 and 6 seconds.
Note: MaxInterval caps the RetryInterval and not the randomized interval.
If the time elapsed since an ExponentialBackOff instance is created goes past the
MaxElapsedTime, then the method NextBackOff() starts returning backoff.Stop.
The elapsed time can be reset by calling Reset().
Example: Given the following default arguments, for 10 tries the sequence will be,
and assuming we go over the MaxElapsedTime on the 10th try:
Request # RetryInterval (seconds) Randomized Interval (seconds)
1 0.5 [0.25, 0.75]
2 0.75 [0.375, 1.125]
3 1.125 [0.562, 1.687]
4 1.687 [0.8435, 2.53]
5 2.53 [1.265, 3.795]
6 3.795 [1.897, 5.692]
7 5.692 [2.846, 8.538]
8 8.538 [4.269, 12.807]
9 12.807 [6.403, 19.210]
10 19.210 backoff.Stop
Note: Implementation is not thread-safe.
*/
type ExponentialBackOff struct {
InitialInterval time.Duration
RandomizationFactor float64
Multiplier float64
MaxInterval time.Duration
// After MaxElapsedTime the ExponentialBackOff returns Stop.
// It never stops if MaxElapsedTime == 0.
MaxElapsedTime time.Duration
Stop time.Duration
Clock Clock
currentInterval time.Duration
startTime time.Time
}
// Clock is an interface that returns current time for BackOff.
type Clock interface {
Now() time.Time
}
// ExponentialBackOffOpts is a function type used to configure ExponentialBackOff options.
type ExponentialBackOffOpts func(*ExponentialBackOff)
// Default values for ExponentialBackOff.
const (
DefaultInitialInterval = 500 * time.Millisecond
DefaultRandomizationFactor = 0.5
DefaultMultiplier = 1.5
DefaultMaxInterval = 60 * time.Second
DefaultMaxElapsedTime = 15 * time.Minute
)
// NewExponentialBackOff creates an instance of ExponentialBackOff using default values.
func NewExponentialBackOff(opts ...ExponentialBackOffOpts) *ExponentialBackOff {
b := &ExponentialBackOff{
InitialInterval: DefaultInitialInterval,
RandomizationFactor: DefaultRandomizationFactor,
Multiplier: DefaultMultiplier,
MaxInterval: DefaultMaxInterval,
MaxElapsedTime: DefaultMaxElapsedTime,
Stop: Stop,
Clock: SystemClock,
}
for _, fn := range opts {
fn(b)
}
b.Reset()
return b
}
// WithInitialInterval sets the initial interval between retries.
func WithInitialInterval(duration time.Duration) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.InitialInterval = duration
}
}
// WithRandomizationFactor sets the randomization factor to add jitter to intervals.
func WithRandomizationFactor(randomizationFactor float64) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.RandomizationFactor = randomizationFactor
}
}
// WithMultiplier sets the multiplier for increasing the interval after each retry.
func WithMultiplier(multiplier float64) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.Multiplier = multiplier
}
}
// WithMaxInterval sets the maximum interval between retries.
func WithMaxInterval(duration time.Duration) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.MaxInterval = duration
}
}
// WithMaxElapsedTime sets the maximum total time for retries.
func WithMaxElapsedTime(duration time.Duration) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.MaxElapsedTime = duration
}
}
// WithRetryStopDuration sets the duration after which retries should stop.
func WithRetryStopDuration(duration time.Duration) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.Stop = duration
}
}
// WithClockProvider sets the clock used to measure time.
func WithClockProvider(clock Clock) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.Clock = clock
}
}
type systemClock struct{}
func (t systemClock) Now() time.Time {
return time.Now()
}
// SystemClock implements Clock interface that uses time.Now().
var SystemClock = systemClock{}
// Reset the interval back to the initial retry interval and restarts the timer.
// Reset must be called before using b.
func (b *ExponentialBackOff) Reset() {
b.currentInterval = b.InitialInterval
b.startTime = b.Clock.Now()
}
// NextBackOff calculates the next backoff interval using the formula:
// Randomized interval = RetryInterval * (1 ± RandomizationFactor)
func (b *ExponentialBackOff) NextBackOff() time.Duration {
// Make sure we have not gone over the maximum elapsed time.
elapsed := b.GetElapsedTime()
next := getRandomValueFromInterval(b.RandomizationFactor, rand.Float64(), b.currentInterval)
b.incrementCurrentInterval()
if b.MaxElapsedTime != 0 && elapsed+next > b.MaxElapsedTime {
return b.Stop
}
return next
}
// GetElapsedTime returns the elapsed time since an ExponentialBackOff instance
// is created and is reset when Reset() is called.
//
// The elapsed time is computed using time.Now().UnixNano(). It is
// safe to call even while the backoff policy is used by a running
// ticker.
func (b *ExponentialBackOff) GetElapsedTime() time.Duration {
return b.Clock.Now().Sub(b.startTime)
}
// Increments the current interval by multiplying it with the multiplier.
func (b *ExponentialBackOff) incrementCurrentInterval() {
// Check for overflow, if overflow is detected set the current interval to the max interval.
if float64(b.currentInterval) >= float64(b.MaxInterval)/b.Multiplier {
b.currentInterval = b.MaxInterval
} else {
b.currentInterval = time.Duration(float64(b.currentInterval) * b.Multiplier)
}
}
// Returns a random value from the following interval:
// [currentInterval - randomizationFactor * currentInterval, currentInterval + randomizationFactor * currentInterval].
func getRandomValueFromInterval(randomizationFactor, random float64, currentInterval time.Duration) time.Duration {
if randomizationFactor == 0 {
return currentInterval // make sure no randomness is used when randomizationFactor is 0.
}
var delta = randomizationFactor * float64(currentInterval)
var minInterval = float64(currentInterval) - delta
var maxInterval = float64(currentInterval) + delta
// Get a random value from the range [minInterval, maxInterval].
// The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then
// we want a 33% chance for selecting either 1, 2 or 3.
return time.Duration(minInterval + (random * (maxInterval - minInterval + 1)))
}

View File

@ -1,146 +0,0 @@
package backoff
import (
"errors"
"time"
)
// An OperationWithData is executing by RetryWithData() or RetryNotifyWithData().
// The operation will be retried using a backoff policy if it returns an error.
type OperationWithData[T any] func() (T, error)
// An Operation is executing by Retry() or RetryNotify().
// The operation will be retried using a backoff policy if it returns an error.
type Operation func() error
func (o Operation) withEmptyData() OperationWithData[struct{}] {
return func() (struct{}, error) {
return struct{}{}, o()
}
}
// Notify is a notify-on-error function. It receives an operation error and
// backoff delay if the operation failed (with an error).
//
// NOTE that if the backoff policy stated to stop retrying,
// the notify function isn't called.
type Notify func(error, time.Duration)
// Retry the operation o until it does not return error or BackOff stops.
// o is guaranteed to be run at least once.
//
// If o returns a *PermanentError, the operation is not retried, and the
// wrapped error is returned.
//
// Retry sleeps the goroutine for the duration returned by BackOff after a
// failed operation returns.
func Retry(o Operation, b BackOff) error {
return RetryNotify(o, b, nil)
}
// RetryWithData is like Retry but returns data in the response too.
func RetryWithData[T any](o OperationWithData[T], b BackOff) (T, error) {
return RetryNotifyWithData(o, b, nil)
}
// RetryNotify calls notify function with the error and wait duration
// for each failed attempt before sleep.
func RetryNotify(operation Operation, b BackOff, notify Notify) error {
return RetryNotifyWithTimer(operation, b, notify, nil)
}
// RetryNotifyWithData is like RetryNotify but returns data in the response too.
func RetryNotifyWithData[T any](operation OperationWithData[T], b BackOff, notify Notify) (T, error) {
return doRetryNotify(operation, b, notify, nil)
}
// RetryNotifyWithTimer calls notify function with the error and wait duration using the given Timer
// for each failed attempt before sleep.
// A default timer that uses system timer is used when nil is passed.
func RetryNotifyWithTimer(operation Operation, b BackOff, notify Notify, t Timer) error {
_, err := doRetryNotify(operation.withEmptyData(), b, notify, t)
return err
}
// RetryNotifyWithTimerAndData is like RetryNotifyWithTimer but returns data in the response too.
func RetryNotifyWithTimerAndData[T any](operation OperationWithData[T], b BackOff, notify Notify, t Timer) (T, error) {
return doRetryNotify(operation, b, notify, t)
}
func doRetryNotify[T any](operation OperationWithData[T], b BackOff, notify Notify, t Timer) (T, error) {
var (
err error
next time.Duration
res T
)
if t == nil {
t = &defaultTimer{}
}
defer func() {
t.Stop()
}()
ctx := getContext(b)
b.Reset()
for {
res, err = operation()
if err == nil {
return res, nil
}
var permanent *PermanentError
if errors.As(err, &permanent) {
return res, permanent.Err
}
if next = b.NextBackOff(); next == Stop {
if cerr := ctx.Err(); cerr != nil {
return res, cerr
}
return res, err
}
if notify != nil {
notify(err, next)
}
t.Start(next)
select {
case <-ctx.Done():
return res, ctx.Err()
case <-t.C():
}
}
}
// PermanentError signals that the operation should not be retried.
type PermanentError struct {
Err error
}
func (e *PermanentError) Error() string {
return e.Err.Error()
}
func (e *PermanentError) Unwrap() error {
return e.Err
}
func (e *PermanentError) Is(target error) bool {
_, ok := target.(*PermanentError)
return ok
}
// Permanent wraps the given err in a *PermanentError.
func Permanent(err error) error {
if err == nil {
return nil
}
return &PermanentError{
Err: err,
}
}

View File

@ -1,38 +0,0 @@
package backoff
import "time"
/*
WithMaxRetries creates a wrapper around another BackOff, which will
return Stop if NextBackOff() has been called too many times since
the last time Reset() was called
Note: Implementation is not thread-safe.
*/
func WithMaxRetries(b BackOff, max uint64) BackOff {
return &backOffTries{delegate: b, maxTries: max}
}
type backOffTries struct {
delegate BackOff
maxTries uint64
numTries uint64
}
func (b *backOffTries) NextBackOff() time.Duration {
if b.maxTries == 0 {
return Stop
}
if b.maxTries > 0 {
if b.maxTries <= b.numTries {
return Stop
}
b.numTries++
}
return b.delegate.NextBackOff()
}
func (b *backOffTries) Reset() {
b.numTries = 0
b.delegate.Reset()
}

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